authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:20:46-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:20:46-07:00
log1486a4a2b9c3f5a83d54f5ce591a2f60fba9316f
treebad0a2d71476a6beb9eca68e1af5b2df4841328d
parent1e5f62801b7c4d247247bc3648310a20560e5b43
signature Signed by PGP key B5690EEEBB952194

[codex] Structure Electron updater errors (#3280)

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

2 files changed, 94 insertions(+), 30 deletions(-)

apps/desktop/src/electron/ElectronUpdater.test.ts+57-9
...@@ -1,5 +1,4 @@...@@ -1,5 +1,4 @@
1import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
2import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
4import { beforeEach, vi } from "vite-plus/test";3import { beforeEach, vi } from "vite-plus/test";
54
...@@ -65,16 +64,65 @@ describe("ElectronUpdater", () => {...@@ -65,16 +64,65 @@ describe("ElectronUpdater", () => {
65 const cause = new Error("network unavailable");64 const cause = new Error("network unavailable");
66 autoUpdaterMock.checkForUpdates.mockImplementationOnce(() => Promise.reject(cause));65 autoUpdaterMock.checkForUpdates.mockImplementationOnce(() => Promise.reject(cause));
67 const updater = yield* ElectronUpdater.ElectronUpdater;66 const updater = yield* ElectronUpdater.ElectronUpdater;
67 autoUpdaterMock.channel = "beta";
6868
69 const exit = yield* Effect.exit(updater.checkForUpdates);69 const error = yield* updater.checkForUpdates.pipe(Effect.flip);
7070
71 assert.equal(exit._tag, "Failure");71 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError);
72 if (exit._tag === "Failure") {72 assert.isTrue(ElectronUpdater.isElectronUpdaterError(error));
73 const error = Cause.squash(exit.cause);73 assert.equal(error.channel, "beta");
74 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError);74 assert.strictEqual(error.cause, cause);
75 assert.equal(error.cause, cause);75 assert.equal(error.message, "Electron updater failed to check for updates on channel beta.");
76 assert.equal(error.message, "Electron updater failed to check for updates.");76 assert.notInclude(error.message, cause.message);
77 }77 }).pipe(Effect.provide(ElectronUpdater.layer)),
78 );
79
80 it.effect("preserves the execution-time channel on download failures", () =>
81 Effect.gen(function* () {
82 const cause = new Error("download unavailable");
83 autoUpdaterMock.downloadUpdate.mockImplementationOnce(() => Promise.reject(cause));
84 const updater = yield* ElectronUpdater.ElectronUpdater;
85 autoUpdaterMock.channel = "nightly";
86
87 const error = yield* updater.downloadUpdate.pipe(Effect.flip);
88
89 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterDownloadUpdateError);
90 assert.isTrue(ElectronUpdater.isElectronUpdaterError(error));
91 assert.equal(error.channel, "nightly");
92 assert.strictEqual(error.cause, cause);
93 assert.equal(
94 error.message,
95 "Electron updater failed to download the update on channel nightly.",
96 );
97 assert.notInclude(error.message, cause.message);
98 }).pipe(Effect.provide(ElectronUpdater.layer)),
99 );
100
101 it.effect("preserves quit-and-install flags and the execution-time channel", () =>
102 Effect.gen(function* () {
103 const cause = new Error("quit and install failed");
104 autoUpdaterMock.quitAndInstall.mockImplementationOnce(() => {
105 throw cause;
106 });
107 const updater = yield* ElectronUpdater.ElectronUpdater;
108 autoUpdaterMock.channel = "alpha";
109
110 const error = yield* updater
111 .quitAndInstall({ isSilent: true, isForceRunAfter: false })
112 .pipe(Effect.flip);
113
114 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterQuitAndInstallError);
115 assert.isTrue(ElectronUpdater.isElectronUpdaterError(error));
116 assert.equal(error.channel, "alpha");
117 assert.equal(error.isSilent, true);
118 assert.equal(error.isForceRunAfter, false);
119 assert.strictEqual(error.cause, cause);
120 assert.equal(
121 error.message,
122 "Electron updater failed to quit and install the update on channel alpha (silent: true, force run after: false).",
123 );
124 assert.notInclude(error.message, cause.message);
125 assert.deepEqual(autoUpdaterMock.quitAndInstall.mock.calls, [[true, false]]);
78 }).pipe(Effect.provide(ElectronUpdater.layer)),126 }).pipe(Effect.provide(ElectronUpdater.layer)),
79 );127 );
80});128});
apps/desktop/src/electron/ElectronUpdater.ts+37-21
...@@ -10,40 +10,41 @@ type AutoUpdater = typeof autoUpdater;...@@ -10,40 +10,41 @@ type AutoUpdater = typeof autoUpdater;
1010
11export type ElectronUpdaterFeedUrl = Parameters<AutoUpdater["setFeedURL"]>[0];11export type ElectronUpdaterFeedUrl = Parameters<AutoUpdater["setFeedURL"]>[0];
1212
13const electronUpdaterErrorFields = {
14 cause: Schema.Defect(),
15};
16
17export class ElectronUpdaterCheckForUpdatesError extends Schema.TaggedErrorClass<ElectronUpdaterCheckForUpdatesError>()(13export class ElectronUpdaterCheckForUpdatesError extends Schema.TaggedErrorClass<ElectronUpdaterCheckForUpdatesError>()(
18 "ElectronUpdaterCheckForUpdatesError",14 "ElectronUpdaterCheckForUpdatesError",
19 {15 {
20 ...electronUpdaterErrorFields,16 channel: Schema.NullOr(Schema.String),
17 cause: Schema.Defect(),
21 },18 },
22) {19) {
23 override get message(): string {20 override get message(): string {
24 return "Electron updater failed to check for updates.";21 return `Electron updater failed to check for updates on channel ${this.channel ?? "default"}.`;
25 }22 }
26}23}
2724
28export class ElectronUpdaterDownloadUpdateError extends Schema.TaggedErrorClass<ElectronUpdaterDownloadUpdateError>()(25export class ElectronUpdaterDownloadUpdateError extends Schema.TaggedErrorClass<ElectronUpdaterDownloadUpdateError>()(
29 "ElectronUpdaterDownloadUpdateError",26 "ElectronUpdaterDownloadUpdateError",
30 {27 {
31 ...electronUpdaterErrorFields,28 channel: Schema.NullOr(Schema.String),
29 cause: Schema.Defect(),
32 },30 },
33) {31) {
34 override get message(): string {32 override get message(): string {
35 return "Electron updater failed to download the update.";33 return `Electron updater failed to download the update on channel ${this.channel ?? "default"}.`;
36 }34 }
37}35}
3836
39export class ElectronUpdaterQuitAndInstallError extends Schema.TaggedErrorClass<ElectronUpdaterQuitAndInstallError>()(37export class ElectronUpdaterQuitAndInstallError extends Schema.TaggedErrorClass<ElectronUpdaterQuitAndInstallError>()(
40 "ElectronUpdaterQuitAndInstallError",38 "ElectronUpdaterQuitAndInstallError",
41 {39 {
42 ...electronUpdaterErrorFields,40 channel: Schema.NullOr(Schema.String),
41 isSilent: Schema.Boolean,
42 isForceRunAfter: Schema.Boolean,
43 cause: Schema.Defect(),
43 },44 },
44) {45) {
45 override get message(): string {46 override get message(): string {
46 return "Electron updater failed to quit and install the update.";47 return `Electron updater failed to quit and install the update on channel ${this.channel ?? "default"} (silent: ${this.isSilent}, force run after: ${this.isForceRunAfter}).`;
47 }48 }
48}49}
4950
...@@ -116,18 +117,33 @@ export const make = ElectronUpdater.of({...@@ -116,18 +117,33 @@ export const make = ElectronUpdater.of({
116 autoUpdater.disableDifferentialDownload = value;117 autoUpdater.disableDifferentialDownload = value;
117 return Effect.void;118 return Effect.void;
118 }),119 }),
119 checkForUpdates: Effect.tryPromise({120 checkForUpdates: Effect.suspend(() => {
120 try: () => autoUpdater.checkForUpdates(),121 const channel = autoUpdater.channel;
121 catch: (cause) => new ElectronUpdaterCheckForUpdatesError({ cause }),122 return Effect.tryPromise({
122 }).pipe(Effect.asVoid),123 try: () => autoUpdater.checkForUpdates(),
123 downloadUpdate: Effect.tryPromise({124 catch: (cause) => new ElectronUpdaterCheckForUpdatesError({ channel, cause }),
124 try: () => autoUpdater.downloadUpdate(),125 }).pipe(Effect.asVoid);
125 catch: (cause) => new ElectronUpdaterDownloadUpdateError({ cause }),126 }),
126 }).pipe(Effect.asVoid),127 downloadUpdate: Effect.suspend(() => {
128 const channel = autoUpdater.channel;
129 return Effect.tryPromise({
130 try: () => autoUpdater.downloadUpdate(),
131 catch: (cause) => new ElectronUpdaterDownloadUpdateError({ channel, cause }),
132 }).pipe(Effect.asVoid);
133 }),
127 quitAndInstall: ({ isSilent, isForceRunAfter }) =>134 quitAndInstall: ({ isSilent, isForceRunAfter }) =>
128 Effect.try({135 Effect.suspend(() => {
129 try: () => autoUpdater.quitAndInstall(isSilent, isForceRunAfter),136 const channel = autoUpdater.channel;
130 catch: (cause) => new ElectronUpdaterQuitAndInstallError({ cause }),137 return Effect.try({
138 try: () => autoUpdater.quitAndInstall(isSilent, isForceRunAfter),
139 catch: (cause) =>
140 new ElectronUpdaterQuitAndInstallError({
141 channel,
142 isSilent,
143 isForceRunAfter,
144 cause,
145 }),
146 });
131 }),147 }),
132 on: (eventName, listener) => {148 on: (eventName, listener) => {
133 const eventTarget = autoUpdater as unknown as {149 const eventTarget = autoUpdater as unknown as {