Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as Layer from "effect/Layer";
import * as Logger from "effect/Logger";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import * as SubscriptionRef from "effect/SubscriptionRef";
import * as Schema from "effect/Schema";
import * as TestClock from "effect/testing/TestClock";

Expand Down Expand Up @@ -583,8 +584,8 @@ describe("DesktopWindow", () => {
}),
);

it.effect.each(["before", "after"] as const)(
"keeps a closed startup window closed until activation %s backend readiness",
it.effect.each(["before", "after", "after a restart of"] as const)(
"keeps a closed main window closed until activation %s backend readiness",
(activation) =>
Effect.gen(function* () {
const fake = makeFakeBrowserWindow();
Expand All @@ -594,10 +595,14 @@ describe("DesktopWindow", () => {
const windows = yield* DesktopWindow.DesktopWindow;
yield* windows.ensureMain;
fake.windowListeners.get("ready-to-show")?.();
if (activation === "after a restart of") {
yield* windows.handleBackendReady(new URL("http://127.0.0.1:3773"));
}
fake.isDestroyed.mockReturnValue(true);
fake.windowListeners.get("closed")?.();
assert.isTrue(Option.isNone(yield* Ref.get(mainWindow)));
if (activation === "after") {
if (activation === "after a restart of") yield* windows.handleBackendNotReady;
if (activation !== "before") {
yield* windows.handleBackendReady(new URL("http://127.0.0.1:3773"));
assert.equal(yield* Ref.get(createCount), 1);
}
Expand All @@ -610,6 +615,76 @@ describe("DesktopWindow", () => {
}),
);

it.effect.each([
["a revealing menu action", { reveal: true }, 2],
["a non-revealing menu action", { reveal: false }, 1],
] as const)(
"after %s during a restart, a closed main window follows the backend",
([, options, expectedCreates]) =>
Effect.gen(function* () {
const fake = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
yield* Effect.gen(function* () {
const windows = yield* DesktopWindow.DesktopWindow;
yield* windows.handleBackendReady(new URL("http://127.0.0.1:3773"));
fake.isDestroyed.mockReturnValue(true);
fake.windowListeners.get("closed")?.();
yield* windows.handleBackendNotReady;
yield* windows.dispatchMenuAction("open-settings", options);
assert.equal(yield* Ref.get(createCount), 1);
fake.isDestroyed.mockReturnValue(false);
yield* windows.handleBackendReady(new URL("http://127.0.0.1:3773"));
assert.equal(yield* Ref.get(createCount), expectedCreates);
}).pipe(Effect.provide(layerTest({ window: fake.window, createCount, mainWindow })));
}),
);

it.effect("a Dock click after a revealing menu action during a restart opens the window", () =>
Effect.gen(function* () {
const fake = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
yield* Effect.gen(function* () {
const windows = yield* DesktopWindow.DesktopWindow;
yield* windows.handleBackendReady(new URL("http://127.0.0.1:3773"));
fake.isDestroyed.mockReturnValue(true);
fake.windowListeners.get("closed")?.();
yield* windows.handleBackendNotReady;
yield* windows.dispatchMenuAction("open-settings", { reveal: true });
assert.equal(yield* Ref.get(createCount), 1);
fake.isDestroyed.mockReturnValue(false);
yield* windows.activate;
assert.equal(yield* Ref.get(createCount), 2);
yield* windows.handleBackendReady(new URL("http://127.0.0.1:3773"));
assert.equal(yield* Ref.get(createCount), 2);
}).pipe(Effect.provide(layerTest({ window: fake.window, createCount, mainWindow })));
}),
);

it.effect("reopens a closed main window when a restart recovers from a startup failure", () =>
Effect.gen(function* () {
const fake = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
yield* Effect.gen(function* () {
const windows = yield* DesktopWindow.DesktopWindow;
const state = yield* DesktopState.DesktopState;
yield* windows.handleBackendReady(new URL("http://127.0.0.1:3773"));
fake.windowListeners.get("closed")?.();
yield* windows.handleBackendNotReady;
yield* SubscriptionRef.set(state.primaryBackendStartup, {
_tag: "Failing",
reason: "readiness timed out",
managedPolicy: false,
output: "",
});
yield* windows.handleBackendReady(new URL("http://127.0.0.1:3773"));
assert.equal(yield* Ref.get(createCount), 2);
}).pipe(Effect.provide(layerTest({ window: fake.window, createCount, mainWindow })));
}),
);

it.effect.each(["renderer", "backend"] as const)(
"reveals the painted boot shell when %s becomes ready first",
(first) =>
Expand Down
26 changes: 18 additions & 8 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import * as SubscriptionRef from "effect/SubscriptionRef";
import * as Schema from "effect/Schema";
import * as Semaphore from "effect/Semaphore";

Expand Down Expand Up @@ -393,8 +394,11 @@ export const make = Effect.gen(function* () {
const electronApp = yield* ElectronApp.ElectronApp;
const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory;
const backendReadyRef = yield* Ref.make(false);
let backendHasStarted = false;
let mainClosedDuringStartup = false;
// Backend readiness repeats after every restart, and on macOS the app outlives its last
// window, so a window the user closed stays closed until they activate the app.
let mainClosed = false;
// A revealing request that arrived while the backend was down opens the window once it is back.
let revealWhenReady = false;
const splashWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const context = yield* Effect.context<DesktopWindowRuntimeServices>();
const runFork = Effect.runForkWith(context);
Expand Down Expand Up @@ -998,7 +1002,7 @@ export const make = Effect.gen(function* () {
}

window.on("closed", () => {
if (!backendHasStarted) mainClosedDuringStartup = true;
mainClosed = true;
clearDevelopmentLoadRetry();
clearBoundsPersist();
void runPromise(electronWindow.clearMain(Option.some(window)));
Expand All @@ -1010,7 +1014,8 @@ export const make = Effect.gen(function* () {
const createMain = Effect.gen(function* () {
const window = yield* createWindow();
if (Option.isSome(window)) {
mainClosedDuringStartup = false;
mainClosed = false;
revealWhenReady = false;
yield* logWindowInfo("main window created");
}
return window;
Expand Down Expand Up @@ -1106,7 +1111,10 @@ export const make = Effect.gen(function* () {
) {
if (yield* Ref.get(desktopState.quitting)) return;
const existingWindow = yield* reveal ? focusedMainWindow : electronWindow.main;
if (Option.isNone(existingWindow) && (!reveal || (yield* waitingForBackend))) return;
if (Option.isNone(existingWindow) && (!reveal || (yield* waitingForBackend))) {
if (reveal) revealWhenReady = true;
return;
}
const window = Option.isSome(existingWindow) ? existingWindow : yield* ensureMain;
if (Option.isNone(window) || window.value.isDestroyed()) return;
const targetWindow = window.value;
Expand Down Expand Up @@ -1141,7 +1149,7 @@ export const make = Effect.gen(function* () {
yield* revealMain(existingWindow.value);
return;
}
if (mainClosedDuringStartup) {
if (mainClosed) {
yield* ensureMain;
return;
}
Expand All @@ -1158,10 +1166,12 @@ export const make = Effect.gen(function* () {
createMainIfBackendReady,
showConnectingSplash,
handleBackendReady: Effect.fn("desktop.window.handleBackendReady")(function* (httpBaseUrl) {
backendHasStarted = true;
yield* Ref.set(backendReadyRef, true);
yield* logWindowInfo("backend ready", { source: "http", url: httpBaseUrl.href });
if (!mainClosedDuringStartup) yield* createMainIfBackendReady;
// A startup failure surfaced the recovery window, which closes only into a main window.
const recovering =
(yield* SubscriptionRef.get(desktopState.primaryBackendStartup))._tag === "Failing";
if (!mainClosed || revealWhenReady || recovering) yield* createMainIfBackendReady;
}),
handleBackendNotReady: Ref.set(backendReadyRef, false).pipe(
Effect.withSpan("desktop.window.handleBackendNotReady"),
Expand Down
Loading