Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions apps/desktop/src/electron/ElectronApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
70 changes: 60 additions & 10 deletions apps/desktop/src/electron/ElectronApp.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,12 +14,36 @@ export interface ElectronAppMetadata {
readonly runningUnderArm64Translation: boolean;
}

export class ElectronAppMetadataReadError extends Schema.TaggedErrorClass<ElectronAppMetadataReadError>()(
"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>()(
"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<ElectronAppMetadata>;
readonly metadata: Effect.Effect<ElectronAppMetadata, ElectronAppMetadataReadError>;
readonly name: Effect.Effect<string>;
readonly whenReady: Effect.Effect<void>;
readonly whenReady: Effect.Effect<void, ElectronAppWhenReadyError>;
readonly quit: Effect.Effect<void>;
readonly exit: (code: number) => Effect.Effect<void>;
readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect<void>;
Expand Down Expand Up @@ -63,15 +88,40 @@ const addScopedAppListener = <Args extends ReadonlyArray<unknown>>(
).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();
}),
Expand Down
Loading