From 6da74c41681bcbc4a6fd0f8435fc045c2f00a854 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 03:39:50 -0700 Subject: [PATCH 1/2] [codex] structure Electron window creation errors Capture a schema-safe BrowserWindow request snapshot while preserving the exact native constructor failure. Co-authored-by: codex --- .../src/electron/ElectronWindow.test.ts | 82 +++++++++++++++++-- apps/desktop/src/electron/ElectronWindow.ts | 62 ++++++++++++-- 2 files changed, 133 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/electron/ElectronWindow.test.ts b/apps/desktop/src/electron/ElectronWindow.test.ts index cc6c6484245..cfd3b0beebc 100644 --- a/apps/desktop/src/electron/ElectronWindow.test.ts +++ b/apps/desktop/src/electron/ElectronWindow.test.ts @@ -3,18 +3,23 @@ import * as Effect from "effect/Effect"; import type * as Electron from "electron"; import { beforeEach, vi } from "vite-plus/test"; -const { appFocusMock, getAllWindowsMock } = vi.hoisted(() => ({ - appFocusMock: vi.fn(), - getAllWindowsMock: vi.fn(), -})); +const { appFocusMock, browserWindowMock, getAllWindowsMock, getFocusedWindowMock } = vi.hoisted( + () => ({ + appFocusMock: vi.fn(), + browserWindowMock: vi.fn(function BrowserWindowMock() {}), + getAllWindowsMock: vi.fn(), + getFocusedWindowMock: vi.fn(), + }), +); vi.mock("electron", () => ({ app: { focus: appFocusMock, }, - BrowserWindow: { + BrowserWindow: Object.assign(browserWindowMock, { getAllWindows: getAllWindowsMock, - }, + getFocusedWindow: getFocusedWindowMock, + }), })); import * as ElectronWindow from "./ElectronWindow.ts"; @@ -28,9 +33,74 @@ function makeBrowserWindow(input: { readonly destroyed: boolean }) { describe("ElectronWindow", () => { beforeEach(() => { appFocusMock.mockReset(); + browserWindowMock.mockReset(); getAllWindowsMock.mockReset(); + getFocusedWindowMock.mockReset(); }); + it.effect("preserves schema-safe creation context and the Electron cause", () => + Effect.gen(function* () { + const cause = new Error("native BrowserWindow construction failed"); + browserWindowMock.mockImplementationOnce(function BrowserWindowFailure() { + throw cause; + }); + const options = { + title: "T3 Code", + width: 1100, + height: 780, + minWidth: 840, + minHeight: 620, + show: false, + modal: false, + frame: true, + transparent: false, + backgroundColor: "#101010", + icon: {} as Electron.NativeImage, + webPreferences: { + preload: "/tmp/preload.js", + partition: "persist:t3code-preview-test", + sandbox: true, + contextIsolation: true, + nodeIntegration: false, + webviewTag: true, + spellcheck: true, + }, + } satisfies Electron.BrowserWindowConstructorOptions; + const electronWindow = yield* ElectronWindow.ElectronWindow; + + const error = yield* electronWindow.create(options).pipe(Effect.flip); + + assert.instanceOf(error, ElectronWindow.ElectronWindowCreateError); + assert.isTrue(ElectronWindow.isElectronWindowCreateError(error)); + assert.deepEqual(error.options, { + title: "T3 Code", + width: 1100, + height: 780, + minWidth: 840, + minHeight: 620, + show: false, + modal: false, + frame: true, + transparent: false, + backgroundColor: "#101010", + webPreferences: { + preload: "/tmp/preload.js", + partition: "persist:t3code-preview-test", + sandbox: true, + contextIsolation: true, + nodeIntegration: false, + webviewTag: true, + }, + }); + assert.isFalse("icon" in error.options); + assert.isFalse("spellcheck" in error.options.webPreferences); + assert.strictEqual(error.cause, cause); + assert.equal(error.message, 'Failed to create Electron BrowserWindow "T3 Code" (1100x780).'); + assert.notInclude(error.message, cause.message); + assert.deepEqual(browserWindowMock.mock.calls, [[options]]); + }).pipe(Effect.provide(ElectronWindow.layer)), + ); + it.effect("skips windows destroyed before appearance sync runs", () => Effect.gen(function* () { const liveWindow = makeBrowserWindow({ destroyed: false }); diff --git a/apps/desktop/src/electron/ElectronWindow.ts b/apps/desktop/src/electron/ElectronWindow.ts index 0bf98a9610e..0d211cbeb87 100644 --- a/apps/desktop/src/electron/ElectronWindow.ts +++ b/apps/desktop/src/electron/ElectronWindow.ts @@ -8,17 +8,46 @@ import * as Schema from "effect/Schema"; import * as Electron from "electron"; +const ElectronWindowCreateOptions = Schema.Struct({ + title: Schema.NullOr(Schema.String), + width: Schema.NullOr(Schema.Number), + height: Schema.NullOr(Schema.Number), + minWidth: Schema.NullOr(Schema.Number), + minHeight: Schema.NullOr(Schema.Number), + show: Schema.NullOr(Schema.Boolean), + modal: Schema.NullOr(Schema.Boolean), + frame: Schema.NullOr(Schema.Boolean), + transparent: Schema.NullOr(Schema.Boolean), + backgroundColor: Schema.NullOr(Schema.String), + webPreferences: Schema.Struct({ + preload: Schema.NullOr(Schema.String), + partition: Schema.NullOr(Schema.String), + sandbox: Schema.NullOr(Schema.Boolean), + contextIsolation: Schema.NullOr(Schema.Boolean), + nodeIntegration: Schema.NullOr(Schema.Boolean), + webviewTag: Schema.NullOr(Schema.Boolean), + }), +}); + export class ElectronWindowCreateError extends Schema.TaggedErrorClass()( "ElectronWindowCreateError", { + options: ElectronWindowCreateOptions, cause: Schema.Defect(), }, ) { override get message(): string { - return "Failed to create Electron BrowserWindow."; + const title = this.options.title === null ? "" : ` "${this.options.title}"`; + const dimensions = + this.options.width === null || this.options.height === null + ? "" + : ` (${this.options.width}x${this.options.height})`; + return `Failed to create Electron BrowserWindow${title}${dimensions}.`; } } +export const isElectronWindowCreateError = Schema.is(ElectronWindowCreateError); + export class ElectronWindow extends Context.Service< ElectronWindow, { @@ -69,11 +98,34 @@ export const make = Effect.gen(function* () { ); return ElectronWindow.of({ - create: (options) => - Effect.try({ + create: (options) => { + const webPreferences = options.webPreferences; + const diagnosticOptions = { + title: options.title ?? null, + width: options.width ?? null, + height: options.height ?? null, + minWidth: options.minWidth ?? null, + minHeight: options.minHeight ?? null, + show: options.show ?? null, + modal: options.modal ?? null, + frame: options.frame ?? null, + transparent: options.transparent ?? null, + backgroundColor: options.backgroundColor ?? null, + webPreferences: { + preload: webPreferences?.preload ?? null, + partition: webPreferences?.partition ?? null, + sandbox: webPreferences?.sandbox ?? null, + contextIsolation: webPreferences?.contextIsolation ?? null, + nodeIntegration: webPreferences?.nodeIntegration ?? null, + webviewTag: webPreferences?.webviewTag ?? null, + }, + } satisfies typeof ElectronWindowCreateOptions.Type; + + return Effect.try({ try: () => new Electron.BrowserWindow(options), - catch: (cause) => new ElectronWindowCreateError({ cause }), - }), + catch: (cause) => new ElectronWindowCreateError({ options: diagnosticOptions, cause }), + }); + }, main: liveMain, currentMainOrFirst, focusedMainOrFirst, From 2effa73b17b4c4a33b56ea0874d5131dd481294d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 04:59:02 -0700 Subject: [PATCH 2/2] refactor(desktop): structure Electron window operations Co-authored-by: codex --- .../src/electron/ElectronWindow.test.ts | 124 ++++++++++++- apps/desktop/src/electron/ElectronWindow.ts | 173 ++++++++++++++---- 2 files changed, 253 insertions(+), 44 deletions(-) diff --git a/apps/desktop/src/electron/ElectronWindow.test.ts b/apps/desktop/src/electron/ElectronWindow.test.ts index cfd3b0beebc..b59f8572739 100644 --- a/apps/desktop/src/electron/ElectronWindow.test.ts +++ b/apps/desktop/src/electron/ElectronWindow.test.ts @@ -1,5 +1,8 @@ import { assert, describe, it } from "@effect/vitest"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; import type * as Electron from "electron"; import { beforeEach, vi } from "vite-plus/test"; @@ -24,8 +27,13 @@ vi.mock("electron", () => ({ import * as ElectronWindow from "./ElectronWindow.ts"; -function makeBrowserWindow(input: { readonly destroyed: boolean }) { +const TestLayer = ElectronWindow.layer.pipe( + Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), +); + +function makeBrowserWindow(input: { readonly id: number; readonly destroyed: boolean }) { return { + id: input.id, isDestroyed: vi.fn(() => input.destroyed), } as unknown as Electron.BrowserWindow; } @@ -98,13 +106,13 @@ describe("ElectronWindow", () => { assert.equal(error.message, 'Failed to create Electron BrowserWindow "T3 Code" (1100x780).'); assert.notInclude(error.message, cause.message); assert.deepEqual(browserWindowMock.mock.calls, [[options]]); - }).pipe(Effect.provide(ElectronWindow.layer)), + }).pipe(Effect.provide(TestLayer)), ); it.effect("skips windows destroyed before appearance sync runs", () => Effect.gen(function* () { - const liveWindow = makeBrowserWindow({ destroyed: false }); - const destroyedWindow = makeBrowserWindow({ destroyed: true }); + const liveWindow = makeBrowserWindow({ id: 1, destroyed: false }); + const destroyedWindow = makeBrowserWindow({ id: 2, destroyed: true }); getAllWindowsMock.mockReturnValue([destroyedWindow, liveWindow]); const syncedWindows: Electron.BrowserWindow[] = []; @@ -116,6 +124,112 @@ describe("ElectronWindow", () => { ); assert.deepEqual(syncedWindows, [liveWindow]); - }).pipe(Effect.provide(ElectronWindow.layer)), + }).pipe(Effect.provide(TestLayer)), + ); + + it.effect("preserves window enumeration failures as structured defects", () => + Effect.gen(function* () { + const cause = new Error("window enumeration failed"); + getAllWindowsMock.mockImplementationOnce(() => { + throw cause; + }); + + const electronWindow = yield* ElectronWindow.ElectronWindow; + const exit = yield* Effect.exit(electronWindow.currentMainOrFirst); + + assert.equal(exit._tag, "Failure"); + if (exit._tag === "Failure") { + const error = Cause.squash(exit.cause); + assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); + assert.equal(error.operation, "list-windows"); + assert.equal(error.platform, "linux"); + assert.isNull(error.windowId); + assert.isNull(error.channel); + assert.strictEqual(error.cause, cause); + assert.notInclude(error.message, cause.message); + } + }).pipe(Effect.provide(TestLayer)), + ); + + it.effect("preserves reveal failures with the target window", () => + Effect.gen(function* () { + const cause = new Error("window restore failed"); + const window = { + id: 41, + isDestroyed: vi.fn(() => false), + isMinimized: vi.fn(() => true), + restore: vi.fn(() => { + throw cause; + }), + } as unknown as Electron.BrowserWindow; + + const electronWindow = yield* ElectronWindow.ElectronWindow; + const exit = yield* Effect.exit(electronWindow.reveal(window)); + + assert.equal(exit._tag, "Failure"); + if (exit._tag === "Failure") { + const error = Cause.squash(exit.cause); + assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); + assert.equal(error.operation, "reveal-window"); + assert.equal(error.windowId, 41); + assert.isNull(error.channel); + assert.strictEqual(error.cause, cause); + } + }).pipe(Effect.provide(TestLayer)), + ); + + it.effect("preserves message delivery failures with window and channel context", () => + Effect.gen(function* () { + const cause = new Error("renderer send failed"); + const window = { + id: 42, + isDestroyed: vi.fn(() => false), + webContents: { + send: vi.fn(() => { + throw cause; + }), + }, + } as unknown as Electron.BrowserWindow; + getAllWindowsMock.mockReturnValueOnce([window]); + + const electronWindow = yield* ElectronWindow.ElectronWindow; + const exit = yield* Effect.exit(electronWindow.sendAll("desktop:update", { ready: true })); + + assert.equal(exit._tag, "Failure"); + if (exit._tag === "Failure") { + const error = Cause.squash(exit.cause); + assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); + assert.equal(error.operation, "send-window-message"); + assert.equal(error.windowId, 42); + assert.equal(error.channel, "desktop:update"); + assert.strictEqual(error.cause, cause); + } + }).pipe(Effect.provide(TestLayer)), + ); + + it.effect("preserves destroy failures with the target window", () => + Effect.gen(function* () { + const cause = new Error("window destroy failed"); + const window = { + id: 43, + destroy: vi.fn(() => { + throw cause; + }), + } as unknown as Electron.BrowserWindow; + getAllWindowsMock.mockReturnValueOnce([window]); + + const electronWindow = yield* ElectronWindow.ElectronWindow; + const exit = yield* Effect.exit(electronWindow.destroyAll); + + assert.equal(exit._tag, "Failure"); + if (exit._tag === "Failure") { + const error = Cause.squash(exit.cause); + assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); + assert.equal(error.operation, "destroy-window"); + assert.equal(error.windowId, 43); + assert.isNull(error.channel); + assert.strictEqual(error.cause, cause); + } + }).pipe(Effect.provide(TestLayer)), ); }); diff --git a/apps/desktop/src/electron/ElectronWindow.ts b/apps/desktop/src/electron/ElectronWindow.ts index 0d211cbeb87..dacb2eebb47 100644 --- a/apps/desktop/src/electron/ElectronWindow.ts +++ b/apps/desktop/src/electron/ElectronWindow.ts @@ -29,6 +29,15 @@ const ElectronWindowCreateOptions = Schema.Struct({ }), }); +const ElectronWindowOperation = Schema.Literals([ + "list-windows", + "get-focused-window", + "inspect-window", + "reveal-window", + "send-window-message", + "destroy-window", +]); + export class ElectronWindowCreateError extends Schema.TaggedErrorClass()( "ElectronWindowCreateError", { @@ -48,6 +57,23 @@ export class ElectronWindowCreateError extends Schema.TaggedErrorClass()( + "ElectronWindowOperationError", + { + operation: ElectronWindowOperation, + platform: Schema.String, + windowId: Schema.NullOr(Schema.Number), + channel: Schema.NullOr(Schema.String), + cause: Schema.Defect(), + }, +) { + override get message(): string { + const window = this.windowId === null ? "" : ` for window ${this.windowId}`; + const channel = this.channel === null ? "" : ` on channel ${JSON.stringify(this.channel)}`; + return `Electron window operation ${JSON.stringify(this.operation)} failed${window}${channel} on ${this.platform}.`; + } +} + export class ElectronWindow extends Context.Service< ElectronWindow, { @@ -72,9 +98,38 @@ export const make = Effect.gen(function* () { const platform = yield* HostProcessPlatform; const mainWindowRef = yield* Ref.make>(Option.none()); - const liveMain = Ref.get(mainWindowRef).pipe( - Effect.map(Option.filter((value) => !value.isDestroyed())), - ); + const listWindows = Effect.try({ + try: () => Electron.BrowserWindow.getAllWindows(), + catch: (cause) => + new ElectronWindowOperationError({ + operation: "list-windows", + platform, + windowId: null, + channel: null, + cause, + }), + }).pipe(Effect.orDie); + + const isWindowDestroyed = (window: Electron.BrowserWindow) => + Effect.try({ + try: () => window.isDestroyed(), + catch: (cause) => + new ElectronWindowOperationError({ + operation: "inspect-window", + platform, + windowId: window.id, + channel: null, + cause, + }), + }).pipe(Effect.orDie); + + const liveMain = Effect.gen(function* () { + const main = yield* Ref.get(mainWindowRef); + if (Option.isNone(main) || (yield* isWindowDestroyed(main.value))) { + return Option.none(); + } + return main; + }); const currentMainOrFirst = Effect.gen(function* () { const main = yield* liveMain; @@ -82,20 +137,30 @@ export const make = Effect.gen(function* () { return main; } - return Option.fromNullishOr(Electron.BrowserWindow.getAllWindows()[0] ?? null).pipe( - Option.filter((window) => !window.isDestroyed()), - ); + const first = Option.fromNullishOr((yield* listWindows)[0] ?? null); + if (Option.isNone(first) || (yield* isWindowDestroyed(first.value))) { + return Option.none(); + } + return first; }); - const focusedMainOrFirst = Effect.sync(() => - Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null).pipe( - Option.filter((window) => !window.isDestroyed()), - ), - ).pipe( - Effect.flatMap((focused) => - Option.isSome(focused) ? Effect.succeed(focused) : currentMainOrFirst, - ), - ); + const focusedMainOrFirst = Effect.gen(function* () { + const focused = yield* Effect.try({ + try: () => Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null), + catch: (cause) => + new ElectronWindowOperationError({ + operation: "get-focused-window", + platform, + windowId: null, + channel: null, + cause, + }), + }).pipe(Effect.orDie); + if (Option.isSome(focused) && !(yield* isWindowDestroyed(focused.value))) { + return focused; + } + return yield* currentMainOrFirst; + }); return ElectronWindow.of({ create: (options) => { @@ -141,45 +206,75 @@ export const make = Effect.gen(function* () { return Option.none(); }), reveal: (window) => - Effect.sync(() => { - if (window.isDestroyed()) { - return; - } + Effect.try({ + try: () => { + if (window.isDestroyed()) { + return; + } - if (window.isMinimized()) { - window.restore(); - } + if (window.isMinimized()) { + window.restore(); + } - if (!window.isVisible()) { - window.show(); - } + if (!window.isVisible()) { + window.show(); + } - if (platform === "darwin") { - Electron.app.focus({ steal: true }); - } + if (platform === "darwin") { + Electron.app.focus({ steal: true }); + } - window.focus(); - }), + window.focus(); + }, + catch: (cause) => + new ElectronWindowOperationError({ + operation: "reveal-window", + platform, + windowId: window.id, + channel: null, + cause, + }), + }).pipe(Effect.orDie), sendAll: (channel, ...args) => - Effect.sync(() => { - for (const window of Electron.BrowserWindow.getAllWindows()) { - if (window.isDestroyed()) { + Effect.gen(function* () { + for (const window of yield* listWindows) { + if (yield* isWindowDestroyed(window)) { continue; } - window.webContents.send(channel, ...args); + yield* Effect.try({ + try: () => window.webContents.send(channel, ...args), + catch: (cause) => + new ElectronWindowOperationError({ + operation: "send-window-message", + platform, + windowId: window.id, + channel, + cause, + }), + }).pipe(Effect.orDie); } }), - destroyAll: Effect.sync(() => { - for (const window of Electron.BrowserWindow.getAllWindows()) { - window.destroy(); + destroyAll: Effect.gen(function* () { + for (const window of yield* listWindows) { + yield* Effect.try({ + try: () => window.destroy(), + catch: (cause) => + new ElectronWindowOperationError({ + operation: "destroy-window", + platform, + windowId: window.id, + channel: null, + cause, + }), + }).pipe(Effect.orDie); } }), syncAllAppearance: Effect.fn("desktop.electron.window.syncAllAppearance")(function* ( sync: (window: Electron.BrowserWindow) => Effect.Effect, ) { - const windows = Electron.BrowserWindow.getAllWindows(); + const windows = yield* listWindows; for (const window of windows) { - if (window.isDestroyed()) { + if (yield* isWindowDestroyed(window)) { continue; } yield* sync(window);