diff --git a/apps/desktop/src/electron/ElectronApp.test.ts b/apps/desktop/src/electron/ElectronApp.test.ts index f6ed5cb1df7..f3ce3b4b5f4 100644 --- a/apps/desktop/src/electron/ElectronApp.test.ts +++ b/apps/desktop/src/electron/ElectronApp.test.ts @@ -100,6 +100,44 @@ describe("ElectronApp", () => { }).pipe(Effect.provide(ElectronApp.layer)), ); + it.effect("reports which app metadata property failed", () => + Effect.gen(function* () { + const cause = new Error("version unavailable"); + getVersionMock.mockImplementationOnce(() => { + throw cause; + }); + + const electronApp = yield* ElectronApp.ElectronApp; + const error = yield* electronApp.metadata.pipe(Effect.flip); + + assert.instanceOf(error, ElectronApp.ElectronAppMetadataReadError); + assert.strictEqual(error.property, "app-version"); + assert.strictEqual(error.cause, cause); + assert.strictEqual( + error.message, + 'Failed to read Electron app metadata property "app-version".', + ); + }).pipe(Effect.provide(ElectronApp.layer)), + ); + + it.effect("preserves Electron readiness failures", () => + Effect.gen(function* () { + const cause = new Error("ready failed"); + whenReadyMock.mockRejectedValueOnce(cause); + + const electronApp = yield* ElectronApp.ElectronApp; + const error = yield* electronApp.whenReady.pipe(Effect.flip); + + assert.instanceOf(error, ElectronApp.ElectronAppWhenReadyError); + assert.strictEqual(error.isPackaged, true); + assert.strictEqual(error.cause, cause); + assert.strictEqual( + error.message, + "Failed to wait for the Electron app to become ready (packaged: true).", + ); + }).pipe(Effect.provide(ElectronApp.layer)), + ); + it.effect("scopes app event listeners", () => Effect.gen(function* () { const listener = vi.fn(); diff --git a/apps/desktop/src/electron/ElectronApp.ts b/apps/desktop/src/electron/ElectronApp.ts index 3e894001e10..0af8691f6c4 100644 --- a/apps/desktop/src/electron/ElectronApp.ts +++ b/apps/desktop/src/electron/ElectronApp.ts @@ -1,6 +1,7 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Electron from "electron"; @@ -13,12 +14,36 @@ export interface ElectronAppMetadata { readonly runningUnderArm64Translation: boolean; } +export class ElectronAppMetadataReadError extends Schema.TaggedErrorClass()( + "ElectronAppMetadataReadError", + { + property: Schema.Literals(["app-version", "app-path"]), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to read Electron app metadata property "${this.property}".`; + } +} + +export class ElectronAppWhenReadyError extends Schema.TaggedErrorClass()( + "ElectronAppWhenReadyError", + { + isPackaged: Schema.Boolean, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to wait for the Electron app to become ready (packaged: ${this.isPackaged}).`; + } +} + export class ElectronApp extends Context.Service< ElectronApp, { - readonly metadata: Effect.Effect; + readonly metadata: Effect.Effect; readonly name: Effect.Effect; - readonly whenReady: Effect.Effect; + readonly whenReady: Effect.Effect; readonly quit: Effect.Effect; readonly exit: (code: number) => Effect.Effect; readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect; @@ -63,15 +88,40 @@ const addScopedAppListener = >( ).pipe(Effect.asVoid); export const make = ElectronApp.of({ - metadata: Effect.sync(() => ({ - appVersion: Electron.app.getVersion(), - appPath: Electron.app.getAppPath(), - isPackaged: Electron.app.isPackaged, - resourcesPath: process.resourcesPath, - runningUnderArm64Translation: Electron.app.runningUnderARM64Translation === true, - })), + metadata: Effect.gen(function* () { + const appVersion = yield* Effect.try({ + try: () => Electron.app.getVersion(), + catch: (cause) => + new ElectronAppMetadataReadError({ + property: "app-version", + cause, + }), + }); + const appPath = yield* Effect.try({ + try: () => Electron.app.getAppPath(), + catch: (cause) => + new ElectronAppMetadataReadError({ + property: "app-path", + cause, + }), + }); + + return { + appVersion, + appPath, + isPackaged: Electron.app.isPackaged, + resourcesPath: process.resourcesPath, + runningUnderArm64Translation: Electron.app.runningUnderARM64Translation === true, + }; + }), name: Effect.sync(() => Electron.app.name), - whenReady: Effect.promise(() => Electron.app.whenReady()).pipe(Effect.asVoid), + whenReady: Effect.gen(function* () { + const isPackaged = Electron.app.isPackaged; + yield* Effect.tryPromise({ + try: () => Electron.app.whenReady(), + catch: (cause) => new ElectronAppWhenReadyError({ isPackaged, cause }), + }); + }), quit: Effect.sync(() => { Electron.app.quit(); }),