authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:10:45-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:10:45-07:00
log5a2c92e86d16ecba823fdc61e163b8c35969f668
treebb965ee9ca88edc1d43cf4613987771cec84653f
parent61aade9ea4e7df15fd196d9805a00d24ed715f7c
signature Signed by PGP key B5690EEEBB952194

[codex] Structure Electron app boundary failures (#3301)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 98 insertions(+), 10 deletions(-)

apps/desktop/src/electron/ElectronApp.test.ts+38
......@@ -100,6 +100,44 @@ describe("ElectronApp", () => {
100100 }).pipe(Effect.provide(ElectronApp.layer)),
101101 );
102102
103 it.effect("reports which app metadata property failed", () =>
104 Effect.gen(function* () {
105 const cause = new Error("version unavailable");
106 getVersionMock.mockImplementationOnce(() => {
107 throw cause;
108 });
109
110 const electronApp = yield* ElectronApp.ElectronApp;
111 const error = yield* electronApp.metadata.pipe(Effect.flip);
112
113 assert.instanceOf(error, ElectronApp.ElectronAppMetadataReadError);
114 assert.strictEqual(error.property, "app-version");
115 assert.strictEqual(error.cause, cause);
116 assert.strictEqual(
117 error.message,
118 'Failed to read Electron app metadata property "app-version".',
119 );
120 }).pipe(Effect.provide(ElectronApp.layer)),
121 );
122
123 it.effect("preserves Electron readiness failures", () =>
124 Effect.gen(function* () {
125 const cause = new Error("ready failed");
126 whenReadyMock.mockRejectedValueOnce(cause);
127
128 const electronApp = yield* ElectronApp.ElectronApp;
129 const error = yield* electronApp.whenReady.pipe(Effect.flip);
130
131 assert.instanceOf(error, ElectronApp.ElectronAppWhenReadyError);
132 assert.strictEqual(error.isPackaged, true);
133 assert.strictEqual(error.cause, cause);
134 assert.strictEqual(
135 error.message,
136 "Failed to wait for the Electron app to become ready (packaged: true).",
137 );
138 }).pipe(Effect.provide(ElectronApp.layer)),
139 );
140
103141 it.effect("scopes app event listeners", () =>
104142 Effect.gen(function* () {
105143 const listener = vi.fn();
apps/desktop/src/electron/ElectronApp.ts+60-10
......@@ -1,6 +1,7 @@
11import * as Context from "effect/Context";
22import * as Effect from "effect/Effect";
33import * as Layer from "effect/Layer";
4import * as Schema from "effect/Schema";
45import * as Scope from "effect/Scope";
56
67import * as Electron from "electron";
......@@ -13,12 +14,36 @@ export interface ElectronAppMetadata {
1314 readonly runningUnderArm64Translation: boolean;
1415}
1516
17export class ElectronAppMetadataReadError extends Schema.TaggedErrorClass<ElectronAppMetadataReadError>()(
18 "ElectronAppMetadataReadError",
19 {
20 property: Schema.Literals(["app-version", "app-path"]),
21 cause: Schema.Defect(),
22 },
23) {
24 override get message(): string {
25 return `Failed to read Electron app metadata property "${this.property}".`;
26 }
27}
28
29export class ElectronAppWhenReadyError extends Schema.TaggedErrorClass<ElectronAppWhenReadyError>()(
30 "ElectronAppWhenReadyError",
31 {
32 isPackaged: Schema.Boolean,
33 cause: Schema.Defect(),
34 },
35) {
36 override get message(): string {
37 return `Failed to wait for the Electron app to become ready (packaged: ${this.isPackaged}).`;
38 }
39}
40
1641export class ElectronApp extends Context.Service<
1742 ElectronApp,
1843 {
19 readonly metadata: Effect.Effect<ElectronAppMetadata>;
44 readonly metadata: Effect.Effect<ElectronAppMetadata, ElectronAppMetadataReadError>;
2045 readonly name: Effect.Effect<string>;
21 readonly whenReady: Effect.Effect<void>;
46 readonly whenReady: Effect.Effect<void, ElectronAppWhenReadyError>;
2247 readonly quit: Effect.Effect<void>;
2348 readonly exit: (code: number) => Effect.Effect<void>;
2449 readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect<void>;
......@@ -63,15 +88,40 @@ const addScopedAppListener = <Args extends ReadonlyArray<unknown>>(
6388 ).pipe(Effect.asVoid);
6489
6590export const make = ElectronApp.of({
66 metadata: Effect.sync(() => ({
67 appVersion: Electron.app.getVersion(),
68 appPath: Electron.app.getAppPath(),
69 isPackaged: Electron.app.isPackaged,
70 resourcesPath: process.resourcesPath,
71 runningUnderArm64Translation: Electron.app.runningUnderARM64Translation === true,
72 })),
91 metadata: Effect.gen(function* () {
92 const appVersion = yield* Effect.try({
93 try: () => Electron.app.getVersion(),
94 catch: (cause) =>
95 new ElectronAppMetadataReadError({
96 property: "app-version",
97 cause,
98 }),
99 });
100 const appPath = yield* Effect.try({
101 try: () => Electron.app.getAppPath(),
102 catch: (cause) =>
103 new ElectronAppMetadataReadError({
104 property: "app-path",
105 cause,
106 }),
107 });
108
109 return {
110 appVersion,
111 appPath,
112 isPackaged: Electron.app.isPackaged,
113 resourcesPath: process.resourcesPath,
114 runningUnderArm64Translation: Electron.app.runningUnderARM64Translation === true,
115 };
116 }),
73117 name: Effect.sync(() => Electron.app.name),
74 whenReady: Effect.promise(() => Electron.app.whenReady()).pipe(Effect.asVoid),
118 whenReady: Effect.gen(function* () {
119 const isPackaged = Electron.app.isPackaged;
120 yield* Effect.tryPromise({
121 try: () => Electron.app.whenReady(),
122 catch: (cause) => new ElectronAppWhenReadyError({ isPackaged, cause }),
123 });
124 }),
75125 quit: Effect.sync(() => {
76126 Electron.app.quit();
77127 }),