| author | |
| committer | |
| log | c7b375e9b0f0581a977a2745e3afbf5732aecb15 |
| tree | 4fb0a87e66e0eb7d6ab0bd185b3e0d4a19c936e5 |
| parent | abc253d97dc1d976cf15e69276f2ac7375b26310 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>2 files changed, 385 insertions(+), 54 deletions(-)
apps/desktop/src/electron/ElectronWindow.test.ts+194-10| ... | ... | @@ -1,26 +1,39 @@ |
| 1 | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 3 | import * as Cause from "effect/Cause"; | |
| 2 | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; | |
| 3 | 6 | import type * as Electron from "electron"; |
| 4 | 7 | import { beforeEach, vi } from "vite-plus/test"; |
| 5 | 8 | |
| 6 | const { appFocusMock, getAllWindowsMock } = vi.hoisted(() => ({ | |
| 7 | appFocusMock: vi.fn(), | |
| 8 | getAllWindowsMock: vi.fn(), | |
| 9 | })); | |
| 9 | const { appFocusMock, browserWindowMock, getAllWindowsMock, getFocusedWindowMock } = vi.hoisted( | |
| 10 | () => ({ | |
| 11 | appFocusMock: vi.fn(), | |
| 12 | browserWindowMock: vi.fn(function BrowserWindowMock() {}), | |
| 13 | getAllWindowsMock: vi.fn(), | |
| 14 | getFocusedWindowMock: vi.fn(), | |
| 15 | }), | |
| 16 | ); | |
| 10 | 17 | |
| 11 | 18 | vi.mock("electron", () => ({ |
| 12 | 19 | app: { |
| 13 | 20 | focus: appFocusMock, |
| 14 | 21 | }, |
| 15 | BrowserWindow: { | |
| 22 | BrowserWindow: Object.assign(browserWindowMock, { | |
| 16 | 23 | getAllWindows: getAllWindowsMock, |
| 17 | }, | |
| 24 | getFocusedWindow: getFocusedWindowMock, | |
| 25 | }), | |
| 18 | 26 | })); |
| 19 | 27 | |
| 20 | 28 | import * as ElectronWindow from "./ElectronWindow.ts"; |
| 21 | 29 | |
| 22 | function makeBrowserWindow(input: { readonly destroyed: boolean }) { | |
| 30 | const TestLayer = ElectronWindow.layer.pipe( | |
| 31 | Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), | |
| 32 | ); | |
| 33 | ||
| 34 | function makeBrowserWindow(input: { readonly id: number; readonly destroyed: boolean }) { | |
| 23 | 35 | return { |
| 36 | id: input.id, | |
| 24 | 37 | isDestroyed: vi.fn(() => input.destroyed), |
| 25 | 38 | } as unknown as Electron.BrowserWindow; |
| 26 | 39 | } |
| ... | ... | @@ -28,13 +41,78 @@ function makeBrowserWindow(input: { readonly destroyed: boolean }) { |
| 28 | 41 | describe("ElectronWindow", () => { |
| 29 | 42 | beforeEach(() => { |
| 30 | 43 | appFocusMock.mockReset(); |
| 44 | browserWindowMock.mockReset(); | |
| 31 | 45 | getAllWindowsMock.mockReset(); |
| 46 | getFocusedWindowMock.mockReset(); | |
| 32 | 47 | }); |
| 33 | 48 | |
| 49 | it.effect("preserves schema-safe creation context and the Electron cause", () => | |
| 50 | Effect.gen(function* () { | |
| 51 | const cause = new Error("native BrowserWindow construction failed"); | |
| 52 | browserWindowMock.mockImplementationOnce(function BrowserWindowFailure() { | |
| 53 | throw cause; | |
| 54 | }); | |
| 55 | const options = { | |
| 56 | title: "T3 Code", | |
| 57 | width: 1100, | |
| 58 | height: 780, | |
| 59 | minWidth: 840, | |
| 60 | minHeight: 620, | |
| 61 | show: false, | |
| 62 | modal: false, | |
| 63 | frame: true, | |
| 64 | transparent: false, | |
| 65 | backgroundColor: "#101010", | |
| 66 | icon: {} as Electron.NativeImage, | |
| 67 | webPreferences: { | |
| 68 | preload: "/tmp/preload.js", | |
| 69 | partition: "persist:t3code-preview-test", | |
| 70 | sandbox: true, | |
| 71 | contextIsolation: true, | |
| 72 | nodeIntegration: false, | |
| 73 | webviewTag: true, | |
| 74 | spellcheck: true, | |
| 75 | }, | |
| 76 | } satisfies Electron.BrowserWindowConstructorOptions; | |
| 77 | const electronWindow = yield* ElectronWindow.ElectronWindow; | |
| 78 | ||
| 79 | const error = yield* electronWindow.create(options).pipe(Effect.flip); | |
| 80 | ||
| 81 | assert.instanceOf(error, ElectronWindow.ElectronWindowCreateError); | |
| 82 | assert.isTrue(ElectronWindow.isElectronWindowCreateError(error)); | |
| 83 | assert.deepEqual(error.options, { | |
| 84 | title: "T3 Code", | |
| 85 | width: 1100, | |
| 86 | height: 780, | |
| 87 | minWidth: 840, | |
| 88 | minHeight: 620, | |
| 89 | show: false, | |
| 90 | modal: false, | |
| 91 | frame: true, | |
| 92 | transparent: false, | |
| 93 | backgroundColor: "#101010", | |
| 94 | webPreferences: { | |
| 95 | preload: "/tmp/preload.js", | |
| 96 | partition: "persist:t3code-preview-test", | |
| 97 | sandbox: true, | |
| 98 | contextIsolation: true, | |
| 99 | nodeIntegration: false, | |
| 100 | webviewTag: true, | |
| 101 | }, | |
| 102 | }); | |
| 103 | assert.isFalse("icon" in error.options); | |
| 104 | assert.isFalse("spellcheck" in error.options.webPreferences); | |
| 105 | assert.strictEqual(error.cause, cause); | |
| 106 | assert.equal(error.message, 'Failed to create Electron BrowserWindow "T3 Code" (1100x780).'); | |
| 107 | assert.notInclude(error.message, cause.message); | |
| 108 | assert.deepEqual(browserWindowMock.mock.calls, [[options]]); | |
| 109 | }).pipe(Effect.provide(TestLayer)), | |
| 110 | ); | |
| 111 | ||
| 34 | 112 | it.effect("skips windows destroyed before appearance sync runs", () => |
| 35 | 113 | Effect.gen(function* () { |
| 36 | const liveWindow = makeBrowserWindow({ destroyed: false }); | |
| 37 | const destroyedWindow = makeBrowserWindow({ destroyed: true }); | |
| 114 | const liveWindow = makeBrowserWindow({ id: 1, destroyed: false }); | |
| 115 | const destroyedWindow = makeBrowserWindow({ id: 2, destroyed: true }); | |
| 38 | 116 | getAllWindowsMock.mockReturnValue([destroyedWindow, liveWindow]); |
| 39 | 117 | |
| 40 | 118 | const syncedWindows: Electron.BrowserWindow[] = []; |
| ... | ... | @@ -46,6 +124,112 @@ describe("ElectronWindow", () => { |
| 46 | 124 | ); |
| 47 | 125 | |
| 48 | 126 | assert.deepEqual(syncedWindows, [liveWindow]); |
| 49 | }).pipe(Effect.provide(ElectronWindow.layer)), | |
| 127 | }).pipe(Effect.provide(TestLayer)), | |
| 128 | ); | |
| 129 | ||
| 130 | it.effect("preserves window enumeration failures as structured defects", () => | |
| 131 | Effect.gen(function* () { | |
| 132 | const cause = new Error("window enumeration failed"); | |
| 133 | getAllWindowsMock.mockImplementationOnce(() => { | |
| 134 | throw cause; | |
| 135 | }); | |
| 136 | ||
| 137 | const electronWindow = yield* ElectronWindow.ElectronWindow; | |
| 138 | const exit = yield* Effect.exit(electronWindow.currentMainOrFirst); | |
| 139 | ||
| 140 | assert.equal(exit._tag, "Failure"); | |
| 141 | if (exit._tag === "Failure") { | |
| 142 | const error = Cause.squash(exit.cause); | |
| 143 | assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); | |
| 144 | assert.equal(error.operation, "list-windows"); | |
| 145 | assert.equal(error.platform, "linux"); | |
| 146 | assert.isNull(error.windowId); | |
| 147 | assert.isNull(error.channel); | |
| 148 | assert.strictEqual(error.cause, cause); | |
| 149 | assert.notInclude(error.message, cause.message); | |
| 150 | } | |
| 151 | }).pipe(Effect.provide(TestLayer)), | |
| 152 | ); | |
| 153 | ||
| 154 | it.effect("preserves reveal failures with the target window", () => | |
| 155 | Effect.gen(function* () { | |
| 156 | const cause = new Error("window restore failed"); | |
| 157 | const window = { | |
| 158 | id: 41, | |
| 159 | isDestroyed: vi.fn(() => false), | |
| 160 | isMinimized: vi.fn(() => true), | |
| 161 | restore: vi.fn(() => { | |
| 162 | throw cause; | |
| 163 | }), | |
| 164 | } as unknown as Electron.BrowserWindow; | |
| 165 | ||
| 166 | const electronWindow = yield* ElectronWindow.ElectronWindow; | |
| 167 | const exit = yield* Effect.exit(electronWindow.reveal(window)); | |
| 168 | ||
| 169 | assert.equal(exit._tag, "Failure"); | |
| 170 | if (exit._tag === "Failure") { | |
| 171 | const error = Cause.squash(exit.cause); | |
| 172 | assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); | |
| 173 | assert.equal(error.operation, "reveal-window"); | |
| 174 | assert.equal(error.windowId, 41); | |
| 175 | assert.isNull(error.channel); | |
| 176 | assert.strictEqual(error.cause, cause); | |
| 177 | } | |
| 178 | }).pipe(Effect.provide(TestLayer)), | |
| 179 | ); | |
| 180 | ||
| 181 | it.effect("preserves message delivery failures with window and channel context", () => | |
| 182 | Effect.gen(function* () { | |
| 183 | const cause = new Error("renderer send failed"); | |
| 184 | const window = { | |
| 185 | id: 42, | |
| 186 | isDestroyed: vi.fn(() => false), | |
| 187 | webContents: { | |
| 188 | send: vi.fn(() => { | |
| 189 | throw cause; | |
| 190 | }), | |
| 191 | }, | |
| 192 | } as unknown as Electron.BrowserWindow; | |
| 193 | getAllWindowsMock.mockReturnValueOnce([window]); | |
| 194 | ||
| 195 | const electronWindow = yield* ElectronWindow.ElectronWindow; | |
| 196 | const exit = yield* Effect.exit(electronWindow.sendAll("desktop:update", { ready: true })); | |
| 197 | ||
| 198 | assert.equal(exit._tag, "Failure"); | |
| 199 | if (exit._tag === "Failure") { | |
| 200 | const error = Cause.squash(exit.cause); | |
| 201 | assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); | |
| 202 | assert.equal(error.operation, "send-window-message"); | |
| 203 | assert.equal(error.windowId, 42); | |
| 204 | assert.equal(error.channel, "desktop:update"); | |
| 205 | assert.strictEqual(error.cause, cause); | |
| 206 | } | |
| 207 | }).pipe(Effect.provide(TestLayer)), | |
| 208 | ); | |
| 209 | ||
| 210 | it.effect("preserves destroy failures with the target window", () => | |
| 211 | Effect.gen(function* () { | |
| 212 | const cause = new Error("window destroy failed"); | |
| 213 | const window = { | |
| 214 | id: 43, | |
| 215 | destroy: vi.fn(() => { | |
| 216 | throw cause; | |
| 217 | }), | |
| 218 | } as unknown as Electron.BrowserWindow; | |
| 219 | getAllWindowsMock.mockReturnValueOnce([window]); | |
| 220 | ||
| 221 | const electronWindow = yield* ElectronWindow.ElectronWindow; | |
| 222 | const exit = yield* Effect.exit(electronWindow.destroyAll); | |
| 223 | ||
| 224 | assert.equal(exit._tag, "Failure"); | |
| 225 | if (exit._tag === "Failure") { | |
| 226 | const error = Cause.squash(exit.cause); | |
| 227 | assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); | |
| 228 | assert.equal(error.operation, "destroy-window"); | |
| 229 | assert.equal(error.windowId, 43); | |
| 230 | assert.isNull(error.channel); | |
| 231 | assert.strictEqual(error.cause, cause); | |
| 232 | } | |
| 233 | }).pipe(Effect.provide(TestLayer)), | |
| 50 | 234 | ); |
| 51 | 235 | }); |
apps/desktop/src/electron/ElectronWindow.ts+191-44| ... | ... | @@ -8,14 +8,69 @@ import * as Schema from "effect/Schema"; |
| 8 | 8 | |
| 9 | 9 | import * as Electron from "electron"; |
| 10 | 10 | |
| 11 | const ElectronWindowCreateOptions = Schema.Struct({ | |
| 12 | title: Schema.NullOr(Schema.String), | |
| 13 | width: Schema.NullOr(Schema.Number), | |
| 14 | height: Schema.NullOr(Schema.Number), | |
| 15 | minWidth: Schema.NullOr(Schema.Number), | |
| 16 | minHeight: Schema.NullOr(Schema.Number), | |
| 17 | show: Schema.NullOr(Schema.Boolean), | |
| 18 | modal: Schema.NullOr(Schema.Boolean), | |
| 19 | frame: Schema.NullOr(Schema.Boolean), | |
| 20 | transparent: Schema.NullOr(Schema.Boolean), | |
| 21 | backgroundColor: Schema.NullOr(Schema.String), | |
| 22 | webPreferences: Schema.Struct({ | |
| 23 | preload: Schema.NullOr(Schema.String), | |
| 24 | partition: Schema.NullOr(Schema.String), | |
| 25 | sandbox: Schema.NullOr(Schema.Boolean), | |
| 26 | contextIsolation: Schema.NullOr(Schema.Boolean), | |
| 27 | nodeIntegration: Schema.NullOr(Schema.Boolean), | |
| 28 | webviewTag: Schema.NullOr(Schema.Boolean), | |
| 29 | }), | |
| 30 | }); | |
| 31 | ||
| 32 | const ElectronWindowOperation = Schema.Literals([ | |
| 33 | "list-windows", | |
| 34 | "get-focused-window", | |
| 35 | "inspect-window", | |
| 36 | "reveal-window", | |
| 37 | "send-window-message", | |
| 38 | "destroy-window", | |
| 39 | ]); | |
| 40 | ||
| 11 | 41 | export class ElectronWindowCreateError extends Schema.TaggedErrorClass<ElectronWindowCreateError>()( |
| 12 | 42 | "ElectronWindowCreateError", |
| 13 | 43 | { |
| 44 | options: ElectronWindowCreateOptions, | |
| 45 | cause: Schema.Defect(), | |
| 46 | }, | |
| 47 | ) { | |
| 48 | override get message(): string { | |
| 49 | const title = this.options.title === null ? "" : ` "${this.options.title}"`; | |
| 50 | const dimensions = | |
| 51 | this.options.width === null || this.options.height === null | |
| 52 | ? "" | |
| 53 | : ` (${this.options.width}x${this.options.height})`; | |
| 54 | return `Failed to create Electron BrowserWindow${title}${dimensions}.`; | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | export const isElectronWindowCreateError = Schema.is(ElectronWindowCreateError); | |
| 59 | ||
| 60 | export class ElectronWindowOperationError extends Schema.TaggedErrorClass<ElectronWindowOperationError>()( | |
| 61 | "ElectronWindowOperationError", | |
| 62 | { | |
| 63 | operation: ElectronWindowOperation, | |
| 64 | platform: Schema.String, | |
| 65 | windowId: Schema.NullOr(Schema.Number), | |
| 66 | channel: Schema.NullOr(Schema.String), | |
| 14 | 67 | cause: Schema.Defect(), |
| 15 | 68 | }, |
| 16 | 69 | ) { |
| 17 | 70 | override get message(): string { |
| 18 | return "Failed to create Electron BrowserWindow."; | |
| 71 | const window = this.windowId === null ? "" : ` for window ${this.windowId}`; | |
| 72 | const channel = this.channel === null ? "" : ` on channel ${JSON.stringify(this.channel)}`; | |
| 73 | return `Electron window operation ${JSON.stringify(this.operation)} failed${window}${channel} on ${this.platform}.`; | |
| 19 | 74 | } |
| 20 | 75 | } |
| 21 | 76 | |
| ... | ... | @@ -43,9 +98,38 @@ export const make = Effect.gen(function* () { |
| 43 | 98 | const platform = yield* HostProcessPlatform; |
| 44 | 99 | const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()); |
| 45 | 100 | |
| 46 | const liveMain = Ref.get(mainWindowRef).pipe( | |
| 47 | Effect.map(Option.filter((value) => !value.isDestroyed())), | |
| 48 | ); | |
| 101 | const listWindows = Effect.try({ | |
| 102 | try: () => Electron.BrowserWindow.getAllWindows(), | |
| 103 | catch: (cause) => | |
| 104 | new ElectronWindowOperationError({ | |
| 105 | operation: "list-windows", | |
| 106 | platform, | |
| 107 | windowId: null, | |
| 108 | channel: null, | |
| 109 | cause, | |
| 110 | }), | |
| 111 | }).pipe(Effect.orDie); | |
| 112 | ||
| 113 | const isWindowDestroyed = (window: Electron.BrowserWindow) => | |
| 114 | Effect.try({ | |
| 115 | try: () => window.isDestroyed(), | |
| 116 | catch: (cause) => | |
| 117 | new ElectronWindowOperationError({ | |
| 118 | operation: "inspect-window", | |
| 119 | platform, | |
| 120 | windowId: window.id, | |
| 121 | channel: null, | |
| 122 | cause, | |
| 123 | }), | |
| 124 | }).pipe(Effect.orDie); | |
| 125 | ||
| 126 | const liveMain = Effect.gen(function* () { | |
| 127 | const main = yield* Ref.get(mainWindowRef); | |
| 128 | if (Option.isNone(main) || (yield* isWindowDestroyed(main.value))) { | |
| 129 | return Option.none<Electron.BrowserWindow>(); | |
| 130 | } | |
| 131 | return main; | |
| 132 | }); | |
| 49 | 133 | |
| 50 | 134 | const currentMainOrFirst = Effect.gen(function* () { |
| 51 | 135 | const main = yield* liveMain; |
| ... | ... | @@ -53,27 +137,60 @@ export const make = Effect.gen(function* () { |
| 53 | 137 | return main; |
| 54 | 138 | } |
| 55 | 139 | |
| 56 | return Option.fromNullishOr(Electron.BrowserWindow.getAllWindows()[0] ?? null).pipe( | |
| 57 | Option.filter((window) => !window.isDestroyed()), | |
| 58 | ); | |
| 140 | const first = Option.fromNullishOr((yield* listWindows)[0] ?? null); | |
| 141 | if (Option.isNone(first) || (yield* isWindowDestroyed(first.value))) { | |
| 142 | return Option.none<Electron.BrowserWindow>(); | |
| 143 | } | |
| 144 | return first; | |
| 59 | 145 | }); |
| 60 | 146 | |
| 61 | const focusedMainOrFirst = Effect.sync(() => | |
| 62 | Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null).pipe( | |
| 63 | Option.filter((window) => !window.isDestroyed()), | |
| 64 | ), | |
| 65 | ).pipe( | |
| 66 | Effect.flatMap((focused) => | |
| 67 | Option.isSome(focused) ? Effect.succeed(focused) : currentMainOrFirst, | |
| 68 | ), | |
| 69 | ); | |
| 147 | const focusedMainOrFirst = Effect.gen(function* () { | |
| 148 | const focused = yield* Effect.try({ | |
| 149 | try: () => Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null), | |
| 150 | catch: (cause) => | |
| 151 | new ElectronWindowOperationError({ | |
| 152 | operation: "get-focused-window", | |
| 153 | platform, | |
| 154 | windowId: null, | |
| 155 | channel: null, | |
| 156 | cause, | |
| 157 | }), | |
| 158 | }).pipe(Effect.orDie); | |
| 159 | if (Option.isSome(focused) && !(yield* isWindowDestroyed(focused.value))) { | |
| 160 | return focused; | |
| 161 | } | |
| 162 | return yield* currentMainOrFirst; | |
| 163 | }); | |
| 70 | 164 | |
| 71 | 165 | return ElectronWindow.of({ |
| 72 | create: (options) => | |
| 73 | Effect.try({ | |
| 166 | create: (options) => { | |
| 167 | const webPreferences = options.webPreferences; | |
| 168 | const diagnosticOptions = { | |
| 169 | title: options.title ?? null, | |
| 170 | width: options.width ?? null, | |
| 171 | height: options.height ?? null, | |
| 172 | minWidth: options.minWidth ?? null, | |
| 173 | minHeight: options.minHeight ?? null, | |
| 174 | show: options.show ?? null, | |
| 175 | modal: options.modal ?? null, | |
| 176 | frame: options.frame ?? null, | |
| 177 | transparent: options.transparent ?? null, | |
| 178 | backgroundColor: options.backgroundColor ?? null, | |
| 179 | webPreferences: { | |
| 180 | preload: webPreferences?.preload ?? null, | |
| 181 | partition: webPreferences?.partition ?? null, | |
| 182 | sandbox: webPreferences?.sandbox ?? null, | |
| 183 | contextIsolation: webPreferences?.contextIsolation ?? null, | |
| 184 | nodeIntegration: webPreferences?.nodeIntegration ?? null, | |
| 185 | webviewTag: webPreferences?.webviewTag ?? null, | |
| 186 | }, | |
| 187 | } satisfies typeof ElectronWindowCreateOptions.Type; | |
| 188 | ||
| 189 | return Effect.try({ | |
| 74 | 190 | try: () => new Electron.BrowserWindow(options), |
| 75 | catch: (cause) => new ElectronWindowCreateError({ cause }), | |
| 76 | }), | |
| 191 | catch: (cause) => new ElectronWindowCreateError({ options: diagnosticOptions, cause }), | |
| 192 | }); | |
| 193 | }, | |
| 77 | 194 | main: liveMain, |
| 78 | 195 | currentMainOrFirst, |
| 79 | 196 | focusedMainOrFirst, |
| ... | ... | @@ -89,45 +206,75 @@ export const make = Effect.gen(function* () { |
| 89 | 206 | return Option.none(); |
| 90 | 207 | }), |
| 91 | 208 | reveal: (window) => |
| 92 | Effect.sync(() => { | |
| 93 | if (window.isDestroyed()) { | |
| 94 | return; | |
| 95 | } | |
| 209 | Effect.try({ | |
| 210 | try: () => { | |
| 211 | if (window.isDestroyed()) { | |
| 212 | return; | |
| 213 | } | |
| 96 | 214 | |
| 97 | if (window.isMinimized()) { | |
| 98 | window.restore(); | |
| 99 | } | |
| 215 | if (window.isMinimized()) { | |
| 216 | window.restore(); | |
| 217 | } | |
| 100 | 218 | |
| 101 | if (!window.isVisible()) { | |
| 102 | window.show(); | |
| 103 | } | |
| 219 | if (!window.isVisible()) { | |
| 220 | window.show(); | |
| 221 | } | |
| 104 | 222 | |
| 105 | if (platform === "darwin") { | |
| 106 | Electron.app.focus({ steal: true }); | |
| 107 | } | |
| 223 | if (platform === "darwin") { | |
| 224 | Electron.app.focus({ steal: true }); | |
| 225 | } | |
| 108 | 226 | |
| 109 | window.focus(); | |
| 110 | }), | |
| 227 | window.focus(); | |
| 228 | }, | |
| 229 | catch: (cause) => | |
| 230 | new ElectronWindowOperationError({ | |
| 231 | operation: "reveal-window", | |
| 232 | platform, | |
| 233 | windowId: window.id, | |
| 234 | channel: null, | |
| 235 | cause, | |
| 236 | }), | |
| 237 | }).pipe(Effect.orDie), | |
| 111 | 238 | sendAll: (channel, ...args) => |
| 112 | Effect.sync(() => { | |
| 113 | for (const window of Electron.BrowserWindow.getAllWindows()) { | |
| 114 | if (window.isDestroyed()) { | |
| 239 | Effect.gen(function* () { | |
| 240 | for (const window of yield* listWindows) { | |
| 241 | if (yield* isWindowDestroyed(window)) { | |
| 115 | 242 | continue; |
| 116 | 243 | } |
| 117 | window.webContents.send(channel, ...args); | |
| 244 | yield* Effect.try({ | |
| 245 | try: () => window.webContents.send(channel, ...args), | |
| 246 | catch: (cause) => | |
| 247 | new ElectronWindowOperationError({ | |
| 248 | operation: "send-window-message", | |
| 249 | platform, | |
| 250 | windowId: window.id, | |
| 251 | channel, | |
| 252 | cause, | |
| 253 | }), | |
| 254 | }).pipe(Effect.orDie); | |
| 118 | 255 | } |
| 119 | 256 | }), |
| 120 | destroyAll: Effect.sync(() => { | |
| 121 | for (const window of Electron.BrowserWindow.getAllWindows()) { | |
| 122 | window.destroy(); | |
| 257 | destroyAll: Effect.gen(function* () { | |
| 258 | for (const window of yield* listWindows) { | |
| 259 | yield* Effect.try({ | |
| 260 | try: () => window.destroy(), | |
| 261 | catch: (cause) => | |
| 262 | new ElectronWindowOperationError({ | |
| 263 | operation: "destroy-window", | |
| 264 | platform, | |
| 265 | windowId: window.id, | |
| 266 | channel: null, | |
| 267 | cause, | |
| 268 | }), | |
| 269 | }).pipe(Effect.orDie); | |
| 123 | 270 | } |
| 124 | 271 | }), |
| 125 | 272 | syncAllAppearance: Effect.fn("desktop.electron.window.syncAllAppearance")(function* <E, R>( |
| 126 | 273 | sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>, |
| 127 | 274 | ) { |
| 128 | const windows = Electron.BrowserWindow.getAllWindows(); | |
| 275 | const windows = yield* listWindows; | |
| 129 | 276 | for (const window of windows) { |
| 130 | if (window.isDestroyed()) { | |
| 277 | if (yield* isWindowDestroyed(window)) { | |
| 131 | 278 | continue; |
| 132 | 279 | } |
| 133 | 280 | yield* sync(window); |