From 2682fa9b4d76a667fbfc19bfa5c225d5832d611f Mon Sep 17 00:00:00 2001 From: humunkulud Date: Thu, 15 Oct 2026 16:05:49 +0200 Subject: [PATCH 1/4] fix(desktop): open the window while the backend starts --- apps/desktop/src/app/DesktopApp.ts | 15 ++ .../DesktopLocalEnvironmentAuth.test.ts | 114 +++++++++++++ .../backend/DesktopLocalEnvironmentAuth.ts | 6 +- apps/desktop/src/window/DesktopWindow.test.ts | 152 +++++++++++++++++- apps/desktop/src/window/DesktopWindow.ts | 25 +-- 5 files changed, 300 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index 24dbf2232f005..61e971a93e448 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -253,6 +253,21 @@ const bootstrap = Effect.gen(function* () { } yield* primaryBackend.start; yield* logBootstrapInfo("bootstrap backend start requested"); + // The bundled renderer can paint while the configured backend boots. WSL-only + // keeps its splash because preflight can replace the primary endpoint. + const config = yield* primaryBackend.currentConfig; + if ( + !(settings.wslOnly === true && settings.wslBackendEnabled === true) && + Option.isSome(config) && + Option.isNone(config.value.preflightFailure) && + !(yield* Ref.get(state.quitting)) + ) { + yield* desktopWindow.ensureMain.pipe( + Effect.catch((error) => + logStartupError("failed to open main window during backend startup", { error }), + ), + ); + } yield* appActivation.start.pipe( Effect.tap(() => logBootstrapInfo("desktop app control socket ready")), Effect.catch((error) => logStartupError("desktop app control socket unavailable", { error })), diff --git a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts index 3f0bd82d5888d..87dafeed68d42 100644 --- a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts +++ b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts @@ -1,4 +1,5 @@ import { assert, describe, it } from "@effect/vitest"; +import * as Deferred from "effect/Deferred"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; @@ -67,6 +68,7 @@ describe("DesktopLocalEnvironmentAuth", () => { { id: PRIMARY_LOCAL_ENVIRONMENT_ID, label: Effect.succeed("Windows"), + waitForReady: () => Effect.succeed(true), currentConfig: Effect.succeedSome(config), }, ]), @@ -120,6 +122,7 @@ describe("DesktopLocalEnvironmentAuth", () => { { id: PRIMARY_LOCAL_ENVIRONMENT_ID, label: Effect.succeed("Windows"), + waitForReady: () => Effect.succeed(true), currentConfig: Effect.succeedSome({ ...config, bootstrap: { ...config.bootstrap, desktopBootstrapSecret: "desktop-secret" }, @@ -167,10 +170,121 @@ describe("DesktopLocalEnvironmentAuth", () => { { id: PRIMARY_LOCAL_ENVIRONMENT_ID, label: Effect.succeed("Windows"), + waitForReady: () => Effect.succeed(true), currentConfig: Effect.succeedSome(config), }, ]), } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]); + + const makeWaitingAuth = Effect.gen(function* () { + const readiness = yield* Deferred.make(); + const waiting = yield* Deferred.make(); + const configRef = yield* Ref.make(config); + const requests = yield* Ref.make>([]); + const poolLayer = Layer.succeed(DesktopBackendPool.DesktopBackendPool, { + list: Effect.succeed([ + { + id: PRIMARY_LOCAL_ENVIRONMENT_ID, + currentConfig: Ref.get(configRef).pipe(Effect.asSome), + waitForReady: () => + Deferred.succeed(waiting, undefined).pipe(Effect.andThen(Deferred.await(readiness))), + }, + ]), + } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]); + const httpLayer = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => { + const body = + request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : ""; + return Ref.update(requests, (previous) => [ + ...previous, + { url: request.url, credential: new URLSearchParams(body).get("subject_token") }, + ]).pipe(Effect.as(tokenResponse(request))); + }), + ); + const auth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth.pipe( + Effect.provide( + DesktopLocalEnvironmentAuth.layer.pipe(Layer.provide(Layer.mergeAll(poolLayer, httpLayer))), + ), + ); + return { auth, readiness, waiting, configRef, requests }; + }); + + it.effect("waits through a slow startup and exchanges once using the ready backend config", () => + Effect.gen(function* () { + const { auth, readiness, waiting, configRef, requests } = yield* makeWaitingAuth; + const first = yield* auth.getBearerToken.pipe(Effect.forkChild); + const second = yield* auth.getBearerToken.pipe(Effect.forkChild); + yield* Deferred.await(waiting); + yield* TestClock.adjust(Duration.seconds(30)); + assert.deepStrictEqual(yield* Ref.get(requests), []); + + // A restart can replace the endpoint and credential while startup is pending. + yield* Ref.set(configRef, { + ...config, + httpBaseUrl: new URL("http://127.0.0.1:4773"), + bootstrap: { ...config.bootstrap, desktopBootstrapToken: "replacement-token" }, + }); + yield* Deferred.succeed(readiness, true); + + assert.strictEqual(yield* Fiber.join(first), "desktop-bearer-token"); + assert.strictEqual(yield* Fiber.join(second), "desktop-bearer-token"); + assert.deepStrictEqual(yield* Ref.get(requests), [ + { url: "http://127.0.0.1:4773/oauth/token", credential: "replacement-token" }, + ]); + }).pipe(Effect.provide(TestClock.layer())), + ); + + it.effect("stops waiting without an HTTP exchange when the backend stops", () => + Effect.gen(function* () { + const { auth, readiness, waiting, requests } = yield* makeWaitingAuth; + const pending = yield* auth.getBearerToken.pipe(Effect.flip, Effect.forkChild); + yield* Deferred.await(waiting); + yield* Deferred.succeed(readiness, false); + + const error = yield* Fiber.join(pending); + assert.strictEqual(error._tag, "DesktopLocalEnvironmentAuthBackendNotConfiguredError"); + assert.deepStrictEqual(yield* Ref.get(requests), []); + }), + ); + + it.effect("releases the auth lock when a readiness wait is interrupted", () => + Effect.gen(function* () { + const { auth, readiness, waiting, requests } = yield* makeWaitingAuth; + const pending = yield* auth.getBearerToken.pipe(Effect.forkChild); + yield* Deferred.await(waiting); + yield* Fiber.interrupt(pending); + assert.deepStrictEqual(yield* Ref.get(requests), []); + + yield* Deferred.succeed(readiness, true); + assert.strictEqual(yield* auth.getBearerToken, "desktop-bearer-token"); + assert.strictEqual((yield* Ref.get(requests)).length, 1); + }), + ); + + it.effect("fails without an HTTP exchange when the primary backend is absent", () => + Effect.gen(function* () { + const poolLayer = Layer.succeed(DesktopBackendPool.DesktopBackendPool, { + list: Effect.succeed([]), + } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]); + const httpLayer = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make(() => Effect.die("Unexpected HTTP exchange")), + ); + const error = yield* Effect.gen(function* () { + const auth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth; + return yield* auth.getBearerToken.pipe(Effect.flip); + }).pipe( + Effect.provide( + DesktopLocalEnvironmentAuth.layer.pipe( + Layer.provide(Layer.mergeAll(poolLayer, httpLayer)), + ), + ), + ); + assert.strictEqual(error._tag, "DesktopLocalEnvironmentAuthBackendNotConfiguredError"); + }), + ); + // Answers the first `failures` exchanges with `failure`, then with a token. const makeExchange = (failures: number, failure: () => Response) => Effect.gen(function* () { diff --git a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts index d137d502e6c5e..8c3c4ec0a4709 100644 --- a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts +++ b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts @@ -84,7 +84,11 @@ export const make = Effect.gen(function* () { const instances = yield* pool.list; const primary = instances.find((instance) => instance.id === PRIMARY_LOCAL_ENVIRONMENT_ID); - const configOption = primary === undefined ? Option.none() : yield* primary.currentConfig; + // The bundled renderer can request auth before the backend finishes starting. + if (primary === undefined || !(yield* primary.waitForReady(Duration.infinity))) { + return yield* new DesktopLocalEnvironmentAuthBackendNotConfiguredError(); + } + const configOption = yield* primary.currentConfig; if (Option.isNone(configOption)) { return yield* new DesktopLocalEnvironmentAuthBackendNotConfiguredError(); } diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index c9a49b1d735be..589891667f52d 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -220,6 +220,8 @@ function layerTest(input: { readonly window: Electron.BrowserWindow; readonly createCount: Ref.Ref; readonly mainWindow: Ref.Ref>; + readonly beforeCreate?: Effect.Effect; + readonly onClearMain?: Effect.Effect; readonly createdWindowOptions?: Electron.BrowserWindowConstructorOptions[]; readonly desktopSettings?: DesktopAppSettings.DesktopSettings; readonly mainWindowBoundsUpdates?: DesktopAppSettings.DesktopWindowBounds[]; @@ -275,6 +277,7 @@ function layerTest(input: { input.createdWindowOptions?.push(options); }).pipe( Effect.andThen(Ref.update(input.createCount, (count) => count + 1)), + Effect.andThen(input.beforeCreate ?? Effect.void), Effect.as(input.window), ), main: Ref.get(input.mainWindow), @@ -284,7 +287,10 @@ function layerTest(input: { ? Ref.get(input.mainWindow) : Effect.succeedSome(input.focusedWindow), setMain: (window) => Ref.set(input.mainWindow, Option.some(window)), - clearMain: () => Ref.set(input.mainWindow, Option.none()), + clearMain: () => + Ref.set(input.mainWindow, Option.none()).pipe( + Effect.andThen(input.onClearMain ?? Effect.void), + ), prepareReveal: () => Effect.succeed(false), reveal: (window) => Effect.sync(() => input.onReveal?.(window)), sendAll: () => Effect.void, @@ -669,6 +675,150 @@ describe("DesktopWindow", () => { }), ); + it.effect("opens the configured renderer before backend readiness without opening it twice", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + assert.strictEqual(yield* desktopWindow.ensureMain, fakeWindow.window); + assert.equal(yield* Ref.get(createCount), 1); + assert.deepEqual(fakeWindow.loadURL.mock.calls, [["t3code-dev://app/"]]); + + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + assert.equal(yield* Ref.get(createCount), 1); + assert.equal(fakeWindow.loadURL.mock.calls.length, 1); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("shares an in-flight main window creation with activation and backend readiness", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const createStarted = yield* Deferred.make(); + const finishCreate = yield* Deferred.make(); + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + beforeCreate: Deferred.succeed(createStarted, undefined).pipe( + Effect.andThen(Deferred.await(finishCreate)), + ), + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + const first = yield* desktopWindow.ensureMain.pipe( + Effect.forkChild({ startImmediately: true }), + ); + yield* Deferred.await(createStarted); + const ready = yield* desktopWindow + .handleBackendReady(new URL("http://127.0.0.1:3773")) + .pipe(Effect.forkChild({ startImmediately: true })); + const second = yield* desktopWindow.ensureMain.pipe( + Effect.forkChild({ startImmediately: true }), + ); + const activation = yield* desktopWindow.activate.pipe( + Effect.forkChild({ startImmediately: true }), + ); + assert.equal(yield* Ref.get(createCount), 1); + + yield* Deferred.succeed(finishCreate, undefined); + assert.strictEqual(yield* Fiber.join(first), fakeWindow.window); + assert.strictEqual(yield* Fiber.join(second), fakeWindow.window); + yield* Fiber.join(ready); + yield* Fiber.join(activation); + assert.equal(yield* Ref.get(createCount), 1); + assert.equal(fakeWindow.loadURL.mock.calls.length, 1); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("keeps an early main window closed when the backend becomes ready", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const cleared = yield* Deferred.make(); + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + onClearMain: Deferred.succeed(cleared, undefined).pipe(Effect.asVoid), + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.ensureMain; + const closed = fakeWindow.windowListeners.get("closed"); + if (!closed) { + return yield* Effect.die("window closed listener was not registered"); + } + closed(); + yield* Deferred.await(cleared); + + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + assert.equal(yield* Ref.get(createCount), 1); + assert.isTrue(Option.isNone(yield* Ref.get(mainWindow))); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("reopens an explicitly closed early main window on activation before readiness", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const cleared = yield* Deferred.make(); + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + onClearMain: Deferred.succeed(cleared, undefined).pipe(Effect.asVoid), + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.ensureMain; + const closed = fakeWindow.windowListeners.get("closed"); + if (!closed) { + return yield* Effect.die("window closed listener was not registered"); + } + closed(); + yield* Deferred.await(cleared); + + yield* desktopWindow.activate; + assert.equal(yield* Ref.get(createCount), 2); + assert.strictEqual(Option.getOrThrow(yield* Ref.get(mainWindow)), fakeWindow.window); + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + assert.equal(yield* Ref.get(createCount), 2); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("retries a failed early main window creation when the backend becomes ready", () => + Effect.gen(function* () { + const main = makeFakeBrowserWindow(); + const scenario = yield* makeSplashScenario([null, main.window]); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + const earlyExit = yield* Effect.exit(desktopWindow.ensureMain); + assert.equal(earlyExit._tag, "Failure"); + assert.isTrue(Option.isNone(yield* Ref.get(scenario.mainWindow))); + + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + assert.equal(yield* Ref.get(scenario.createCalls), 2); + assert.strictEqual(Option.getOrThrow(yield* Ref.get(scenario.mainWindow)), main.window); + }).pipe(Effect.provide(scenario.layer)); + }), + ); + it.effect( "opens and reopens the window without backend readiness when local execution is disabled", () => diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 75393d54a1b2f..930ab9e1390f9 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -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 Semaphore from "effect/Semaphore"; import * as Electron from "electron"; @@ -334,12 +335,11 @@ export const make = Effect.gen(function* () { const clientSettings = yield* DesktopClientSettings.DesktopClientSettings; const electronApp = yield* ElectronApp.ElectronApp; const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory; - // Window-side latch for the primary backend's readiness. Set by - // handleBackendReady (driven by the pool's onReady callback), cleared - // by handleBackendNotReady (driven by onShutdown). Only consumed by - // createMainIfBackendReady, which gates the post-readiness window - // open in development and the macOS "activate without windows" path. + // WSL-only startup and recovery still wait for readiness; native bootstrap + // can open the bundled renderer as soon as its endpoint is configured. const backendReadyRef = yield* Ref.make(false); + const mainWindowMutex = yield* Semaphore.make(1); + let mainWindowClosed = false; // The transient "Connecting to WSL" splash window, tracked separately so it // is never mistaken for the real main window. const splashWindowRef = yield* Ref.make>(Option.none()); @@ -863,6 +863,7 @@ export const make = Effect.gen(function* () { } window.on("closed", () => { + mainWindowClosed = true; clearDevelopmentLoadRetry(); clearBoundsPersist(); void runPromise(electronWindow.clearMain(Option.some(window))); @@ -873,6 +874,7 @@ export const make = Effect.gen(function* () { const createMain = Effect.gen(function* () { const window = yield* createWindow(); + mainWindowClosed = false; yield* electronWindow.setMain(window); yield* logWindowInfo("main window created"); return window; @@ -884,7 +886,7 @@ export const make = Effect.gen(function* () { return existingWindow.value; } return yield* createMain; - }).pipe(Effect.withSpan("desktop.window.ensureMain")); + }).pipe(mainWindowMutex.withPermits(1), Effect.withSpan("desktop.window.ensureMain")); const revealOrCreateMain = Effect.gen(function* () { const window = yield* ensureMain; @@ -901,9 +903,7 @@ export const make = Effect.gen(function* () { const createMainIfBackendReady = Effect.gen(function* () { if (yield* waitingForBackend) return; - const existingWindow = yield* currentMainWindow; - if (Option.isSome(existingWindow)) return; - yield* createMain; + yield* ensureMain; }).pipe(Effect.withSpan("desktop.window.createMainIfBackendReady")); const showConnectingSplash = Effect.gen(function* () { @@ -1006,6 +1006,10 @@ export const make = Effect.gen(function* () { return; } } + if (mainWindowClosed) { + yield* ensureMain; + return; + } yield* createMainIfBackendReady; }).pipe(Effect.withSpan("desktop.window.activate")), createMainIfBackendReady, @@ -1013,7 +1017,8 @@ export const make = Effect.gen(function* () { handleBackendReady: Effect.fn("desktop.window.handleBackendReady")(function* (httpBaseUrl) { yield* Ref.set(backendReadyRef, true); yield* logWindowInfo("backend ready", { source: "http", url: httpBaseUrl.href }); - yield* createMainIfBackendReady; + // A cold start may finish after the user has already closed its window. + if (!mainWindowClosed) yield* createMainIfBackendReady; }), handleBackendNotReady: Ref.set(backendReadyRef, false).pipe( Effect.withSpan("desktop.window.handleBackendNotReady"), From 65bc192d2e3666d2fb7cccd1bf7753770033d655 Mon Sep 17 00:00:00 2001 From: humunkulud Date: Fri, 16 Oct 2026 17:50:09 +0200 Subject: [PATCH 2/4] fix(desktop): bound startup auth and preserve restart activation --- .../DesktopLocalEnvironmentAuth.test.ts | 32 ++++++++++++++- .../backend/DesktopLocalEnvironmentAuth.ts | 4 +- apps/desktop/src/window/DesktopWindow.test.ts | 39 +++++++++++++++++++ apps/desktop/src/window/DesktopWindow.ts | 4 +- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts index 87dafeed68d42..bc5a068334e7e 100644 --- a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts +++ b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts @@ -4,6 +4,7 @@ import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; 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 HttpClient from "effect/http/HttpClient"; import * as HttpClientRequest from "effect/http/HttpClientRequest"; @@ -186,8 +187,12 @@ describe("DesktopLocalEnvironmentAuth", () => { { id: PRIMARY_LOCAL_ENVIRONMENT_ID, currentConfig: Ref.get(configRef).pipe(Effect.asSome), - waitForReady: () => - Deferred.succeed(waiting, undefined).pipe(Effect.andThen(Deferred.await(readiness))), + waitForReady: (timeout: Duration.Duration) => + Deferred.succeed(waiting, undefined).pipe( + Effect.andThen(Deferred.await(readiness)), + Effect.timeoutOption(timeout), + Effect.map(Option.getOrElse(() => false)), + ), }, ]), } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]); @@ -248,6 +253,29 @@ describe("DesktopLocalEnvironmentAuth", () => { }), ); + it.effect("bounds readiness waiting and allows a new request after backend recovery", () => + Effect.gen(function* () { + const { auth, readiness, waiting, requests } = yield* makeWaitingAuth; + const settled = yield* Deferred.make(); + const pending = yield* auth.getBearerToken.pipe( + Effect.flip, + Effect.ensuring(Deferred.succeed(settled, undefined)), + Effect.forkChild, + ); + yield* Deferred.await(waiting); + yield* TestClock.adjust(Duration.minutes(2)); + + assert.isTrue(yield* Deferred.isDone(settled)); + const error = yield* Fiber.join(pending); + assert.strictEqual(error._tag, "DesktopLocalEnvironmentAuthBackendNotConfiguredError"); + assert.deepStrictEqual(yield* Ref.get(requests), []); + + yield* Deferred.succeed(readiness, true); + assert.strictEqual(yield* auth.getBearerToken, "desktop-bearer-token"); + assert.strictEqual((yield* Ref.get(requests)).length, 1); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("releases the auth lock when a readiness wait is interrupted", () => Effect.gen(function* () { const { auth, readiness, waiting, requests } = yield* makeWaitingAuth; diff --git a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts index 8c3c4ec0a4709..253ed14a7b9b2 100644 --- a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts +++ b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts @@ -16,6 +16,8 @@ import * as HttpClient from "effect/http/HttpClient"; import * as DesktopBackendPool from "./DesktopBackendPool.ts"; +const BACKEND_READINESS_TIMEOUT = Duration.minutes(2); + // A loopback /oauth/token exchange can fail transiently while the backend // settles (a 502-504, a refused or reset connection, a timeout). Retry those // for as long as the renderer's own bootstrap does; a rejected credential or a @@ -85,7 +87,7 @@ export const make = Effect.gen(function* () { const instances = yield* pool.list; const primary = instances.find((instance) => instance.id === PRIMARY_LOCAL_ENVIRONMENT_ID); // The bundled renderer can request auth before the backend finishes starting. - if (primary === undefined || !(yield* primary.waitForReady(Duration.infinity))) { + if (primary === undefined || !(yield* primary.waitForReady(BACKEND_READINESS_TIMEOUT))) { return yield* new DesktopLocalEnvironmentAuthBackendNotConfiguredError(); } const configOption = yield* primary.currentConfig; diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 589891667f52d..712e761663770 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -801,6 +801,45 @@ describe("DesktopWindow", () => { }), ); + for (const closeBeforeStop of [true, false]) { + it.effect( + `waits for a previously ready backend on activation when closed ${closeBeforeStop ? "before" : "after"} it stops`, + () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const cleared = yield* Deferred.make(); + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + onClearMain: Deferred.succeed(cleared, undefined).pipe(Effect.asVoid), + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + if (!closeBeforeStop) yield* desktopWindow.handleBackendNotReady; + const closed = fakeWindow.windowListeners.get("closed"); + if (!closed) return yield* Effect.die("window closed listener was not registered"); + closed(); + yield* Deferred.await(cleared); + if (closeBeforeStop) yield* desktopWindow.handleBackendNotReady; + + yield* desktopWindow.activate; + assert.equal(yield* Ref.get(createCount), 1); + assert.isTrue(Option.isNone(yield* Ref.get(mainWindow))); + + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + yield* desktopWindow.activate; + assert.equal(yield* Ref.get(createCount), 2); + assert.strictEqual(Option.getOrThrow(yield* Ref.get(mainWindow)), fakeWindow.window); + }).pipe(Effect.provide(layer)); + }), + ); + } + it.effect("retries a failed early main window creation when the backend becomes ready", () => Effect.gen(function* () { const main = makeFakeBrowserWindow(); diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 930ab9e1390f9..ac3f65039abb0 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -338,6 +338,7 @@ export const make = Effect.gen(function* () { // WSL-only startup and recovery still wait for readiness; native bootstrap // can open the bundled renderer as soon as its endpoint is configured. const backendReadyRef = yield* Ref.make(false); + let backendHasBeenReady = false; const mainWindowMutex = yield* Semaphore.make(1); let mainWindowClosed = false; // The transient "Connecting to WSL" splash window, tracked separately so it @@ -1006,7 +1007,7 @@ export const make = Effect.gen(function* () { return; } } - if (mainWindowClosed) { + if (mainWindowClosed && !backendHasBeenReady) { yield* ensureMain; return; } @@ -1016,6 +1017,7 @@ export const make = Effect.gen(function* () { showConnectingSplash, handleBackendReady: Effect.fn("desktop.window.handleBackendReady")(function* (httpBaseUrl) { yield* Ref.set(backendReadyRef, true); + backendHasBeenReady = true; yield* logWindowInfo("backend ready", { source: "http", url: httpBaseUrl.href }); // A cold start may finish after the user has already closed its window. if (!mainWindowClosed) yield* createMainIfBackendReady; From 10cafd5e3ad366c40a0896cfd2ce77fcedfb43d3 Mon Sep 17 00:00:00 2001 From: humunkulud Date: Fri, 16 Oct 2026 17:53:33 +0200 Subject: [PATCH 3/4] fix(desktop): retain activation during backend recovery --- apps/desktop/src/window/DesktopWindow.test.ts | 1 - apps/desktop/src/window/DesktopWindow.ts | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 802c197cee093..3e69424aabf4c 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -827,7 +827,6 @@ describe("DesktopWindow", () => { assert.isTrue(Option.isNone(yield* Ref.get(mainWindow))); yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); - yield* desktopWindow.activate; assert.equal(yield* Ref.get(createCount), 2); assert.strictEqual(Option.getOrThrow(yield* Ref.get(mainWindow)), fakeWindow.window); }).pipe(Effect.provide(layer)); diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 08f79a5047546..609112b7886e0 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -938,6 +938,8 @@ export const make = Effect.gen(function* () { yield* electronWindow.reveal(existingWindow.value); return; } + const reopenDuringStartup = mainWindowClosed && !backendHasBeenReady; + mainWindowClosed = false; const backendReady = yield* Ref.get(backendReadyRef); if (!backendReady) { const splash = yield* Ref.get(splashWindowRef); @@ -946,7 +948,7 @@ export const make = Effect.gen(function* () { return; } } - if (mainWindowClosed && !backendHasBeenReady) { + if (reopenDuringStartup) { yield* ensureMain; return; } From d21154d58a7170dc4e92d79b53ddbc3fb38f1497 Mon Sep 17 00:00:00 2001 From: humunkulud Date: Fri, 16 Oct 2026 17:54:18 +0200 Subject: [PATCH 4/4] test(desktop): use table-driven recovery regressions --- apps/desktop/src/window/DesktopWindow.test.ts | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 3e69424aabf4c..3fc84b8707dbb 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -796,43 +796,41 @@ describe("DesktopWindow", () => { }), ); - for (const closeBeforeStop of [true, false]) { - it.effect( - `waits for a previously ready backend on activation when closed ${closeBeforeStop ? "before" : "after"} it stops`, - () => - Effect.gen(function* () { - const fakeWindow = makeFakeBrowserWindow(); - const createCount = yield* Ref.make(0); - const mainWindow = yield* Ref.make>(Option.none()); - const cleared = yield* Deferred.make(); - const layer = layerTest({ - window: fakeWindow.window, - createCount, - mainWindow, - onClearMain: Deferred.succeed(cleared, undefined).pipe(Effect.asVoid), - }); - - yield* Effect.gen(function* () { - const desktopWindow = yield* DesktopWindow.DesktopWindow; - yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); - if (!closeBeforeStop) yield* desktopWindow.handleBackendNotReady; - const closed = fakeWindow.windowListeners.get("closed"); - if (!closed) return yield* Effect.die("window closed listener was not registered"); - closed(); - yield* Deferred.await(cleared); - if (closeBeforeStop) yield* desktopWindow.handleBackendNotReady; - - yield* desktopWindow.activate; - assert.equal(yield* Ref.get(createCount), 1); - assert.isTrue(Option.isNone(yield* Ref.get(mainWindow))); - - yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); - assert.equal(yield* Ref.get(createCount), 2); - assert.strictEqual(Option.getOrThrow(yield* Ref.get(mainWindow)), fakeWindow.window); - }).pipe(Effect.provide(layer)); - }), - ); - } + it.effect.each(["before", "after"] as const)( + "waits for a previously ready backend on activation when closed %s it stops", + (closeOrder) => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const cleared = yield* Deferred.make(); + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + onClearMain: Deferred.succeed(cleared, undefined).pipe(Effect.asVoid), + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + if (closeOrder === "after") yield* desktopWindow.handleBackendNotReady; + const closed = fakeWindow.windowListeners.get("closed"); + if (!closed) return yield* Effect.die("window closed listener was not registered"); + closed(); + yield* Deferred.await(cleared); + if (closeOrder === "before") yield* desktopWindow.handleBackendNotReady; + + yield* desktopWindow.activate; + assert.equal(yield* Ref.get(createCount), 1); + assert.isTrue(Option.isNone(yield* Ref.get(mainWindow))); + + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + assert.equal(yield* Ref.get(createCount), 2); + assert.strictEqual(Option.getOrThrow(yield* Ref.get(mainWindow)), fakeWindow.window); + }).pipe(Effect.provide(layer)); + }), + ); it.effect("retries a failed early main window creation when the backend becomes ready", () => Effect.gen(function* () {