From 51e113227169d3505590bf98493e3957097db45e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 03:01:04 -0700 Subject: [PATCH 1/8] [codex] structure desktop update persistence errors Co-authored-by: codex --- .../src/settings/DesktopAppSettings.ts | 67 +++++++++---- .../src/updates/DesktopUpdates.test.ts | 42 +++++++- apps/desktop/src/updates/DesktopUpdates.ts | 98 +++++++++++-------- 3 files changed, 146 insertions(+), 61 deletions(-) diff --git a/apps/desktop/src/settings/DesktopAppSettings.ts b/apps/desktop/src/settings/DesktopAppSettings.ts index e072d80f03e..81aae92f0a3 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.ts @@ -83,12 +83,6 @@ export class DesktopSettingsWriteError extends Schema.TaggedErrorClass new DesktopSettingsWriteError({ operation, path, cause }); - export class DesktopAppSettings extends Context.Service< DesktopAppSettings, { @@ -244,18 +238,46 @@ const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (inp const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`; const encoded = yield* encodeDesktopSettingsJson( toDesktopSettingsDocument(input.settings, input.defaultSettings), - ).pipe(Effect.mapError((cause) => writeError("encode-document", input.settingsPath, cause))); - yield* input.fileSystem - .makeDirectory(directory, { recursive: true }) - .pipe(Effect.mapError((cause) => writeError("create-directory", directory, cause))); - yield* input.fileSystem - .writeFileString(tempPath, `${encoded}\n`) - .pipe(Effect.mapError((cause) => writeError("write-temporary-file", tempPath, cause))); - yield* input.fileSystem - .rename(tempPath, input.settingsPath) - .pipe( - Effect.mapError((cause) => writeError("replace-settings-file", input.settingsPath, cause)), - ); + ).pipe( + Effect.mapError( + (cause) => + new DesktopSettingsWriteError({ + operation: "encode-document", + path: input.settingsPath, + cause, + }), + ), + ); + yield* input.fileSystem.makeDirectory(directory, { recursive: true }).pipe( + Effect.mapError( + (cause) => + new DesktopSettingsWriteError({ + operation: "create-directory", + path: directory, + cause, + }), + ), + ); + yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`).pipe( + Effect.mapError( + (cause) => + new DesktopSettingsWriteError({ + operation: "write-temporary-file", + path: tempPath, + cause, + }), + ), + ); + yield* input.fileSystem.rename(tempPath, input.settingsPath).pipe( + Effect.mapError( + (cause) => + new DesktopSettingsWriteError({ + operation: "replace-settings-file", + path: input.settingsPath, + cause, + }), + ), + ); }); export const make = Effect.gen(function* () { @@ -276,8 +298,13 @@ export const make = Effect.gen(function* () { return crypto.randomUUIDv4.pipe( Effect.map((uuid) => uuid.replace(/-/g, "")), - Effect.mapError((cause) => - writeError("create-temporary-file-name", environment.desktopSettingsPath, cause), + Effect.mapError( + (cause) => + new DesktopSettingsWriteError({ + operation: "create-temporary-file-name", + path: environment.desktopSettingsPath, + cause, + }), ), Effect.flatMap((suffix) => writeSettings({ diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index ad234df0bb5..52164f670f0 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -24,6 +24,7 @@ interface UpdatesHarnessOptions { void, ElectronUpdater.ElectronUpdaterCheckForUpdatesError >; + readonly setUpdateChannelError?: DesktopAppSettings.DesktopSettingsWriteError; readonly env?: Record; } @@ -138,12 +139,23 @@ function makeHarness(options: UpdatesHarnessOptions = {}) { ), ); + const setUpdateChannelError = options.setUpdateChannelError; + const settingsLayer = setUpdateChannelError + ? Layer.succeed(DesktopAppSettings.DesktopAppSettings, { + get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), + load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), + setServerExposureMode: () => Effect.die("unexpected server exposure update"), + setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"), + setUpdateChannel: () => Effect.fail(setUpdateChannelError), + } satisfies DesktopAppSettings.DesktopAppSettings["Service"]) + : DesktopAppSettings.layer; + const layer = DesktopUpdates.layer.pipe( Layer.provideMerge(updaterLayer), Layer.provideMerge(windowLayer), Layer.provideMerge(backendLayer), Layer.provideMerge(DesktopState.layer), - Layer.provideMerge(DesktopAppSettings.layer), + Layer.provideMerge(settingsLayer), Layer.provideMerge( DesktopConfig.layerTest({ T3CODE_HOME: `/tmp/t3-desktop-updates-test-${process.pid}`, @@ -284,6 +296,7 @@ describe("DesktopUpdates", () => { const error = Cause.squash(exit.cause); assert.instanceOf(error, DesktopUpdates.DesktopUpdateActionInProgressError); assert.equal(error.action, "check"); + assert.equal(error.requestedChannel, "nightly"); } yield* Deferred.succeed(releaseCheck, undefined); @@ -292,4 +305,31 @@ describe("DesktopUpdates", () => { ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }), ); + + it.effect("preserves settings failure context when an update channel cannot be persisted", () => { + const diskFailure = new Error("disk exploded"); + const settingsFailure = new DesktopAppSettings.DesktopSettingsWriteError({ + operation: "replace-settings-file", + path: "/tmp/settings.json", + cause: diskFailure, + }); + const harness = makeHarness({ setUpdateChannelError: settingsFailure }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + + const error = yield* updates.setChannel("nightly").pipe(Effect.flip); + + assert.instanceOf(error, DesktopUpdates.DesktopUpdateChannelPersistenceError); + assert.isTrue(DesktopUpdates.isDesktopUpdateSetChannelError(error)); + assert.equal(error.channel, "nightly"); + assert.strictEqual(error.cause, settingsFailure); + assert.strictEqual(error.cause.cause, diskFailure); + assert.equal(error.message, "Failed to persist the nightly desktop update channel."); + assert.notInclude(error.message, diskFailure.message); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); }); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index e9142c369e5..2d09e4e4d87 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -1,9 +1,10 @@ -import type { - DesktopRuntimeInfo, - DesktopUpdateActionResult, - DesktopUpdateChannel, - DesktopUpdateCheckResult, - DesktopUpdateState, +import { + DesktopUpdateChannelSchema, + type DesktopRuntimeInfo, + type DesktopUpdateActionResult, + type DesktopUpdateChannel, + type DesktopUpdateCheckResult, + type DesktopUpdateState, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; @@ -63,30 +64,34 @@ export class DesktopUpdateActionInProgressError extends Schema.TaggedErrorClass< "DesktopUpdateActionInProgressError", { action: Schema.Literals(["check", "download", "install"]), + requestedChannel: DesktopUpdateChannelSchema, }, ) { override get message(): string { - return `Cannot change update tracks while an update ${this.action} action is in progress.`; + return `Cannot change the desktop update channel to ${this.requestedChannel} while an update ${this.action} action is in progress.`; } } -export class DesktopUpdatePersistenceError extends Schema.TaggedErrorClass()( - "DesktopUpdatePersistenceError", +export class DesktopUpdateChannelPersistenceError extends Schema.TaggedErrorClass()( + "DesktopUpdateChannelPersistenceError", { - cause: Schema.Defect(), + channel: DesktopUpdateChannelSchema, + cause: Schema.instanceOf(DesktopAppSettings.DesktopSettingsWriteError), }, ) { override get message(): string { - const detail = this.cause instanceof Error ? this.cause.message : String(this.cause); - return `Failed to persist desktop update settings: ${detail}`; + return `Failed to persist the ${this.channel} desktop update channel.`; } } export type DesktopUpdateConfigureError = never; -export type DesktopUpdateSetChannelError = - | DesktopUpdateActionInProgressError - | DesktopUpdatePersistenceError; +export const DesktopUpdateSetChannelError = Schema.Union([ + DesktopUpdateActionInProgressError, + DesktopUpdateChannelPersistenceError, +]); +export type DesktopUpdateSetChannelError = typeof DesktopUpdateSetChannelError.Type; +export const isDesktopUpdateSetChannelError = Schema.is(DesktopUpdateSetChannelError); export class DesktopUpdates extends Context.Service< DesktopUpdates, @@ -308,8 +313,10 @@ export const make = Effect.gen(function* () { return yield* electronUpdater.checkForUpdates.pipe( Effect.as(true), - Effect.catch( - Effect.fn("desktop.updates.handleCheckForUpdatesFailure")(function* (error) { + Effect.catchTags({ + ElectronUpdaterCheckForUpdatesError: Effect.fn( + "desktop.updates.handleCheckForUpdatesFailure", + )(function* (error) { const failedAt = yield* currentIsoTimestamp; yield* updateState((current) => reduceDesktopUpdateStateOnCheckFailure(current, error.message, failedAt), @@ -317,7 +324,7 @@ export const make = Effect.gen(function* () { yield* logUpdaterError("failed to check for updates", { message: error.message }); return true; }), - ), + }), Effect.ensuring(Ref.set(updateCheckInFlightRef, false)), ); }); @@ -342,15 +349,17 @@ export const make = Effect.gen(function* () { yield* electronUpdater.downloadUpdate; return { accepted: true, completed: true }; }).pipe( - Effect.catch( - Effect.fn("desktop.updates.handleDownloadFailure")(function* (error) { - yield* updateState((current) => - reduceDesktopUpdateStateOnDownloadFailure(current, error.message), - ); - yield* logUpdaterError("failed to download update", { message: error.message }); - return { accepted: true, completed: false }; - }), - ), + Effect.catchTags({ + ElectronUpdaterDownloadUpdateError: Effect.fn("desktop.updates.handleDownloadFailure")( + function* (error) { + yield* updateState((current) => + reduceDesktopUpdateStateOnDownloadFailure(current, error.message), + ); + yield* logUpdaterError("failed to download update", { message: error.message }); + return { accepted: true, completed: false }; + }, + ), + }), Effect.ensuring(Ref.set(updateDownloadInFlightRef, false)), ); }).pipe(Effect.withSpan("desktop.updates.downloadAvailableUpdate")); @@ -377,17 +386,19 @@ export const make = Effect.gen(function* () { }); return { accepted: true, completed: false }; }).pipe( - Effect.catch( - Effect.fn("desktop.updates.handleInstallFailure")(function* (error) { - yield* Ref.set(updateInstallInFlightRef, false); - yield* updateState((current) => - reduceDesktopUpdateStateOnInstallFailure(current, error.message), - ); - yield* Ref.set(desktopState.quitting, false); - yield* logUpdaterError("failed to install update", { message: error.message }); - return { accepted: true, completed: false }; - }), - ), + Effect.catchTags({ + ElectronUpdaterQuitAndInstallError: Effect.fn("desktop.updates.handleInstallFailure")( + function* (error) { + yield* Ref.set(updateInstallInFlightRef, false); + yield* updateState((current) => + reduceDesktopUpdateStateOnInstallFailure(current, error.message), + ); + yield* Ref.set(desktopState.quitting, false); + yield* logUpdaterError("failed to install update", { message: error.message }); + return { accepted: true, completed: false }; + }, + ), + }), ); }).pipe(Effect.withSpan("desktop.updates.installDownloadedUpdate")); @@ -598,7 +609,10 @@ export const make = Effect.gen(function* () { yield* Effect.annotateCurrentSpan({ channel: nextChannel }); const activeAction = yield* activeUpdateAction; if (Option.isSome(activeAction)) { - return yield* new DesktopUpdateActionInProgressError({ action: activeAction.value }); + return yield* new DesktopUpdateActionInProgressError({ + action: activeAction.value, + requestedChannel: nextChannel, + }); } const state = yield* Ref.get(updateStateRef); @@ -608,7 +622,11 @@ export const make = Effect.gen(function* () { yield* desktopSettings .setUpdateChannel(nextChannel) - .pipe(Effect.mapError((cause) => new DesktopUpdatePersistenceError({ cause }))); + .pipe( + Effect.mapError( + (cause) => new DesktopUpdateChannelPersistenceError({ channel: nextChannel, cause }), + ), + ); const enabled = yield* shouldEnableAutoUpdates; yield* setState(createBaseUpdateState(nextChannel, enabled, environment)); From 80d15afc5d98a04e52f74982c01b171b84cf4de7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 06:39:43 -0700 Subject: [PATCH 2/8] Preserve desktop update boundary causes Co-authored-by: codex --- .../src/updates/DesktopUpdates.test.ts | 22 +++++ apps/desktop/src/updates/DesktopUpdates.ts | 80 ++++++++++++++----- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index 52164f670f0..b2883758c4e 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -187,6 +187,28 @@ function makeHarness(options: UpdatesHarnessOptions = {}) { } describe("DesktopUpdates", () => { + it("preserves complete causes for update poller and event failures", () => { + const cause = Cause.combine( + Cause.fail(new Error("updater failed")), + Cause.die(new Error("updater defect")), + ); + const pollerError = new DesktopUpdates.DesktopUpdatePollerError({ + poller: "startup", + cause, + }); + const eventError = new DesktopUpdates.DesktopUpdateEventHandlingError({ + event: "download-progress", + cause, + }); + + assert.strictEqual(pollerError.cause, cause); + assert.equal(pollerError.poller, "startup"); + assert.equal(pollerError.message, "Desktop update startup poller failed."); + assert.strictEqual(eventError.cause, cause); + assert.equal(eventError.event, "download-progress"); + assert.equal(eventError.message, "Failed to handle desktop update download-progress event."); + }); + it.effect("configures the updater and runs startup checks on the test clock", () => { const harness = makeHarness(); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index 2d09e4e4d87..ab19d4dedb7 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -84,6 +84,30 @@ export class DesktopUpdateChannelPersistenceError extends Schema.TaggedErrorClas } } +export class DesktopUpdatePollerError extends Schema.TaggedErrorClass()( + "DesktopUpdatePollerError", + { + poller: Schema.Literals(["startup", "poll"]), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Desktop update ${this.poller} poller failed.`; + } +} + +export class DesktopUpdateEventHandlingError extends Schema.TaggedErrorClass()( + "DesktopUpdateEventHandlingError", + { + event: Schema.Literals(["update-available", "download-progress", "update-downloaded"]), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to handle desktop update ${this.event} event.`; + } +} + export type DesktopUpdateConfigureError = never; export const DesktopUpdateSetChannelError = Schema.Union([ @@ -405,17 +429,25 @@ export const make = Effect.gen(function* () { const startUpdatePollers: Effect.Effect = Effect.gen(function* () { yield* Effect.sleep(AUTO_UPDATE_STARTUP_DELAY).pipe( Effect.andThen(checkForUpdates("startup")), - Effect.catchCause((cause) => - logUpdaterError("startup update check failed", { cause: Cause.pretty(cause) }), - ), + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.void; + } + const error = new DesktopUpdatePollerError({ poller: "startup", cause }); + return logUpdaterError(error.message, { error }); + }), Effect.forkScoped, ); yield* Effect.sleep(AUTO_UPDATE_POLL_INTERVAL).pipe( Effect.andThen(checkForUpdates("poll")), Effect.forever, - Effect.catchCause((cause) => - logUpdaterError("poll update check failed", { cause: Cause.pretty(cause) }), - ), + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.void; + } + const error = new DesktopUpdatePollerError({ poller: "poll", cause }); + return logUpdaterError(error.message, { error }); + }), Effect.forkScoped, ); }).pipe(Effect.withSpan("desktop.updates.startPollers")); @@ -446,11 +478,13 @@ export const make = Effect.gen(function* () { yield* logUpdaterInfo("update available", { version: info.version }); }), ), - Effect.catchCause((cause) => - logUpdaterWarning("ignored malformed update-available event", { - cause: Cause.pretty(cause), - }), - ), + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.void; + } + const error = new DesktopUpdateEventHandlingError({ event: "update-available", cause }); + return logUpdaterWarning(error.message, { error }); + }), ); }); @@ -510,11 +544,13 @@ export const make = Effect.gen(function* () { } }), ), - Effect.catchCause((cause) => - logUpdaterWarning("ignored malformed download-progress event", { - cause: Cause.pretty(cause), - }), - ), + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.void; + } + const error = new DesktopUpdateEventHandlingError({ event: "download-progress", cause }); + return logUpdaterWarning(error.message, { error }); + }), ); }); @@ -529,11 +565,13 @@ export const make = Effect.gen(function* () { yield* logUpdaterInfo("update downloaded", { version: info.version }); }), ), - Effect.catchCause((cause) => - logUpdaterWarning("ignored malformed update-downloaded event", { - cause: Cause.pretty(cause), - }), - ), + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.void; + } + const error = new DesktopUpdateEventHandlingError({ event: "update-downloaded", cause }); + return logUpdaterWarning(error.message, { error }); + }), ); }); From bd7ffb64838fded84e7e32ee502b3ae8b7c77d11 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 09:39:53 -0700 Subject: [PATCH 3/8] fix(desktop): structure updater event failures Co-authored-by: codex --- .../src/updates/DesktopUpdates.test.ts | 29 ++++++++++++++++++ apps/desktop/src/updates/DesktopUpdates.ts | 30 +++++++++++++++---- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index b2883758c4e..4c60a1b2dc1 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -200,6 +200,10 @@ describe("DesktopUpdates", () => { event: "download-progress", cause, }); + const reportedError = new DesktopUpdates.DesktopUpdaterReportedError({ + operation: "download", + cause, + }); assert.strictEqual(pollerError.cause, cause); assert.equal(pollerError.poller, "startup"); @@ -207,6 +211,9 @@ describe("DesktopUpdates", () => { assert.strictEqual(eventError.cause, cause); assert.equal(eventError.event, "download-progress"); assert.equal(eventError.message, "Failed to handle desktop update download-progress event."); + assert.strictEqual(reportedError.cause, cause); + assert.equal(reportedError.operation, "download"); + assert.equal(reportedError.message, "Desktop updater download operation reported an error."); }); it.effect("configures the updater and runs startup checks on the test clock", () => { @@ -256,6 +263,28 @@ describe("DesktopUpdates", () => { ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); + it.effect("keeps raw updater event failures out of update state", () => { + const harness = makeHarness(); + const cause = new Error( + "request failed for https://user:secret@example.com/update?token=secret", + ); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + + harness.emit("error", cause); + yield* flushCallbacks; + + const state = yield* updates.getState; + assert.equal(state.status, "error"); + assert.equal(state.message, "Desktop updater background operation reported an error."); + assert.notInclude(state.message ?? "", "secret"); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + it.effect("persists channel changes through the settings service", () => { const harness = makeHarness(); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index ab19d4dedb7..11a58a6403b 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -108,6 +108,18 @@ export class DesktopUpdateEventHandlingError extends Schema.TaggedErrorClass()( + "DesktopUpdaterReportedError", + { + operation: Schema.Literals(["check", "download", "install", "background"]), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Desktop updater ${this.operation} operation reported an error.`; + } +} + export type DesktopUpdateConfigureError = never; export const DesktopUpdateSetChannelError = Schema.Union([ @@ -497,14 +509,20 @@ export const make = Effect.gen(function* () { }).pipe(Effect.withSpan("desktop.updates.handleUpdateNotAvailable")); const handleUpdaterError = Effect.fn("desktop.updates.handleUpdaterError")(function* ( - error: unknown, + cause: unknown, ) { - const message = error instanceof Error ? error.message : String(error); + const activeAction = yield* activeUpdateAction; + const error = new DesktopUpdaterReportedError({ + operation: Option.getOrElse(activeAction, () => "background" as const), + cause, + }); if (yield* Ref.get(updateInstallInFlightRef)) { yield* Ref.set(updateInstallInFlightRef, false); yield* Ref.set(desktopState.quitting, false); - yield* updateState((current) => reduceDesktopUpdateStateOnInstallFailure(current, message)); - yield* logUpdaterError("updater error", { message }); + yield* updateState((current) => + reduceDesktopUpdateStateOnInstallFailure(current, error.message), + ); + yield* logUpdaterError(error.message, { error }); return; } @@ -514,7 +532,7 @@ export const make = Effect.gen(function* () { yield* updateState((current) => ({ ...current, status: "error", - message, + message: error.message, checkedAt, downloadPercent: null, errorContext, @@ -522,7 +540,7 @@ export const make = Effect.gen(function* () { })); } - yield* logUpdaterError("updater error", { message }); + yield* logUpdaterError(error.message, { error }); }); const handleDownloadProgress = Effect.fn("desktop.updates.handleDownloadProgress")(function* ( From 8e8492454c9cd1af330ce48b5c9685c6bda23fbd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 09:50:40 -0700 Subject: [PATCH 4/8] fix(desktop): retain updater action causes in logs Co-authored-by: codex --- .../src/updates/DesktopUpdates.test.ts | 47 +++++++++++++++++++ apps/desktop/src/updates/DesktopUpdates.ts | 6 +-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index 4c60a1b2dc1..4b4a9744d37 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -7,7 +7,10 @@ import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; +import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; +import * as References from "effect/References"; +import * as Schema from "effect/Schema"; import * as TestClock from "effect/testing/TestClock"; import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts"; @@ -19,6 +22,10 @@ import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as DesktopState from "../app/DesktopState.ts"; import * as DesktopUpdates from "./DesktopUpdates.ts"; +const isElectronUpdaterCheckForUpdatesError = Schema.is( + ElectronUpdater.ElectronUpdaterCheckForUpdatesError, +); + interface UpdatesHarnessOptions { readonly checkForUpdates?: Effect.Effect< void, @@ -285,6 +292,46 @@ describe("DesktopUpdates", () => { ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); + it.effect("logs updater action failures with the exact structured error", () => { + const cause = new Error( + "request failed for https://user:secret@example.com/update?token=secret", + ); + const updaterError = new ElectronUpdater.ElectronUpdaterCheckForUpdatesError({ cause }); + const harness = makeHarness({ checkForUpdates: Effect.fail(updaterError) }); + const loggedErrors: Array = []; + const logger = Logger.make(({ fiber }) => { + const error = fiber.getRef(References.CurrentLogAnnotations).error; + if (error !== undefined) { + loggedErrors.push(error); + } + }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + + yield* updates.check("manual"); + + const state = yield* updates.getState; + const loggedError = loggedErrors.find(isElectronUpdaterCheckForUpdatesError); + assert.isDefined(loggedError); + assert.strictEqual(loggedError, updaterError); + assert.strictEqual(loggedError.cause, cause); + assert.equal(state.message, "Electron updater failed to check for updates."); + assert.notInclude(state.message ?? "", "secret"); + }), + ).pipe( + Effect.provide( + Layer.mergeAll( + TestClock.layer(), + harness.layer, + Logger.layer([logger], { mergeWithExisting: false }), + ), + ), + ); + }); + it.effect("persists channel changes through the settings service", () => { const harness = makeHarness(); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index 11a58a6403b..cc2a7fd464b 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -357,7 +357,7 @@ export const make = Effect.gen(function* () { yield* updateState((current) => reduceDesktopUpdateStateOnCheckFailure(current, error.message, failedAt), ); - yield* logUpdaterError("failed to check for updates", { message: error.message }); + yield* logUpdaterError(error.message, { error }); return true; }), }), @@ -391,7 +391,7 @@ export const make = Effect.gen(function* () { yield* updateState((current) => reduceDesktopUpdateStateOnDownloadFailure(current, error.message), ); - yield* logUpdaterError("failed to download update", { message: error.message }); + yield* logUpdaterError(error.message, { error }); return { accepted: true, completed: false }; }, ), @@ -430,7 +430,7 @@ export const make = Effect.gen(function* () { reduceDesktopUpdateStateOnInstallFailure(current, error.message), ); yield* Ref.set(desktopState.quitting, false); - yield* logUpdaterError("failed to install update", { message: error.message }); + yield* logUpdaterError(error.message, { error }); return { accepted: true, completed: false }; }, ), From 5da873d37c5574f3bc2ac72d8c41d1aa8adbb3a1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 15:43:01 -0700 Subject: [PATCH 5/8] Adapt desktop update test to channel-aware errors Co-authored-by: codex --- apps/desktop/src/updates/DesktopUpdates.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index 4b4a9744d37..8b80e29b201 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -296,7 +296,10 @@ describe("DesktopUpdates", () => { const cause = new Error( "request failed for https://user:secret@example.com/update?token=secret", ); - const updaterError = new ElectronUpdater.ElectronUpdaterCheckForUpdatesError({ cause }); + const updaterError = new ElectronUpdater.ElectronUpdaterCheckForUpdatesError({ + channel: null, + cause, + }); const harness = makeHarness({ checkForUpdates: Effect.fail(updaterError) }); const loggedErrors: Array = []; const logger = Logger.make(({ fiber }) => { @@ -318,7 +321,10 @@ describe("DesktopUpdates", () => { assert.isDefined(loggedError); assert.strictEqual(loggedError, updaterError); assert.strictEqual(loggedError.cause, cause); - assert.equal(state.message, "Electron updater failed to check for updates."); + assert.equal( + state.message, + "Electron updater failed to check for updates on channel default.", + ); assert.notInclude(state.message ?? "", "secret"); }), ).pipe( From bb979bf03020694d57347786df1f576990b8134d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 16:35:42 -0700 Subject: [PATCH 6/8] Recover unexpected desktop update action failures Co-authored-by: codex --- .../src/updates/DesktopUpdates.test.ts | 77 ++++++++++++++++++- apps/desktop/src/updates/DesktopUpdates.ts | 38 +++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index 8b80e29b201..86c3cf53b2d 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -10,6 +10,7 @@ import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; import * as References from "effect/References"; +import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as TestClock from "effect/testing/TestClock"; @@ -32,6 +33,8 @@ interface UpdatesHarnessOptions { ElectronUpdater.ElectronUpdaterCheckForUpdatesError >; readonly setUpdateChannelError?: DesktopAppSettings.DesktopSettingsWriteError; + readonly setDisableDifferentialDownload?: Effect.Effect; + readonly stopBackend?: Effect.Effect; readonly env?: Record; } @@ -75,7 +78,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) { Effect.sync(() => { allowDowngrade = value; }), - setDisableDifferentialDownload: () => Effect.void, + setDisableDifferentialDownload: () => options.setDisableDifferentialDownload ?? Effect.void, checkForUpdates: Effect.sync(() => { checkCount += 1; }).pipe(Effect.andThen(options.checkForUpdates ?? Effect.void)), @@ -111,7 +114,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) { const backendLayer = Layer.succeed(DesktopBackendManager.DesktopBackendManager, { start: Effect.void, - stop: () => Effect.void, + stop: () => options.stopBackend ?? Effect.void, currentConfig: Effect.succeed(Option.none()), snapshot: Effect.succeed({ desiredRunning: false, @@ -211,6 +214,10 @@ describe("DesktopUpdates", () => { operation: "download", cause, }); + const unexpectedActionError = new DesktopUpdates.DesktopUpdateUnexpectedActionError({ + action: "install", + cause, + }); assert.strictEqual(pollerError.cause, cause); assert.equal(pollerError.poller, "startup"); @@ -221,6 +228,12 @@ describe("DesktopUpdates", () => { assert.strictEqual(reportedError.cause, cause); assert.equal(reportedError.operation, "download"); assert.equal(reportedError.message, "Desktop updater download operation reported an error."); + assert.strictEqual(unexpectedActionError.cause, cause); + assert.equal(unexpectedActionError.action, "install"); + assert.equal( + unexpectedActionError.message, + "Desktop update install action failed unexpectedly.", + ); }); it.effect("configures the updater and runs startup checks on the test clock", () => { @@ -338,6 +351,66 @@ describe("DesktopUpdates", () => { ); }); + it.effect("recovers download state after an unexpected setup failure", () => { + let disableDifferentialCalls = 0; + const harness = makeHarness({ + setDisableDifferentialDownload: Effect.suspend(() => { + disableDifferentialCalls += 1; + return disableDifferentialCalls === 1 + ? Effect.void + : Effect.die(new Error("download setup failed")); + }), + }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + harness.emit("update-available", { version: "1.2.4" }); + yield* flushCallbacks; + + const exit = yield* Effect.exit(updates.download); + assert.equal(exit._tag, "Failure"); + + const failedState = yield* updates.getState; + assert.equal(failedState.status, "available"); + assert.equal(failedState.errorContext, "download"); + assert.equal(failedState.message, "Desktop update download action failed unexpectedly."); + + const changedState = yield* updates.setChannel("nightly"); + assert.equal(changedState.channel, "nightly"); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + + it.effect("clears quitting state after an unexpected install setup failure", () => { + const harness = makeHarness({ + stopBackend: Effect.die(new Error("backend stop failed")), + }); + + return Effect.scoped( + Effect.gen(function* () { + const desktopState = yield* DesktopState.DesktopState; + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + harness.emit("update-downloaded", { version: "1.2.4" }); + yield* flushCallbacks; + + const exit = yield* Effect.exit(updates.install); + assert.equal(exit._tag, "Failure"); + assert.isFalse(yield* Ref.get(desktopState.quitting)); + + const failedState = yield* updates.getState; + assert.equal(failedState.status, "downloaded"); + assert.equal(failedState.errorContext, "install"); + assert.equal(failedState.message, "Desktop update install action failed unexpectedly."); + + const changedState = yield* updates.setChannel("nightly"); + assert.equal(changedState.channel, "nightly"); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + it.effect("persists channel changes through the settings service", () => { const harness = makeHarness(); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index cc2a7fd464b..01bc683c9d6 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -120,6 +120,18 @@ export class DesktopUpdaterReportedError extends Schema.TaggedErrorClass()( + "DesktopUpdateUnexpectedActionError", + { + action: Schema.Literals(["download", "install"]), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Desktop update ${this.action} action failed unexpectedly.`; + } +} + export type DesktopUpdateConfigureError = never; export const DesktopUpdateSetChannelError = Schema.Union([ @@ -396,6 +408,18 @@ export const make = Effect.gen(function* () { }, ), }), + Effect.onError((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.void; + } + const error = new DesktopUpdateUnexpectedActionError({ action: "download", cause }); + return Effect.gen(function* () { + yield* updateState((current) => + reduceDesktopUpdateStateOnDownloadFailure(current, error.message), + ); + yield* logUpdaterError(error.message, { error }); + }); + }), Effect.ensuring(Ref.set(updateDownloadInFlightRef, false)), ); }).pipe(Effect.withSpan("desktop.updates.downloadAvailableUpdate")); @@ -435,6 +459,20 @@ export const make = Effect.gen(function* () { }, ), }), + Effect.onError((cause) => + Effect.gen(function* () { + yield* Ref.set(updateInstallInFlightRef, false); + yield* Ref.set(desktopState.quitting, false); + if (Cause.hasInterruptsOnly(cause)) { + return; + } + const error = new DesktopUpdateUnexpectedActionError({ action: "install", cause }); + yield* updateState((current) => + reduceDesktopUpdateStateOnInstallFailure(current, error.message), + ); + yield* logUpdaterError(error.message, { error }); + }), + ), ); }).pipe(Effect.withSpan("desktop.updates.installDownloadedUpdate")); From 0273d7c8377f4bc73ef641a12702be4ef38bc1b8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 16:53:55 -0700 Subject: [PATCH 7/8] Recover desktop update IPC failures Co-authored-by: codex --- .../src/updates/DesktopUpdates.test.ts | 50 +++++++++++++++++-- apps/desktop/src/updates/DesktopUpdates.ts | 27 +++++++--- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index 86c3cf53b2d..a634409d9b1 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -369,8 +369,9 @@ describe("DesktopUpdates", () => { harness.emit("update-available", { version: "1.2.4" }); yield* flushCallbacks; - const exit = yield* Effect.exit(updates.download); - assert.equal(exit._tag, "Failure"); + const result = yield* updates.download; + assert.isTrue(result.accepted); + assert.isFalse(result.completed); const failedState = yield* updates.getState; assert.equal(failedState.status, "available"); @@ -383,6 +384,46 @@ describe("DesktopUpdates", () => { ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); + it.effect("restores download state and permits retry after interruption", () => + Effect.gen(function* () { + const actionStarted = yield* Deferred.make(); + let disableDifferentialCalls = 0; + const harness = makeHarness({ + setDisableDifferentialDownload: Effect.suspend(() => { + disableDifferentialCalls += 1; + if (disableDifferentialCalls === 1) { + return Effect.void; + } + if (disableDifferentialCalls === 2) { + return Deferred.succeed(actionStarted, undefined).pipe(Effect.andThen(Effect.never)); + } + return Effect.void; + }), + }); + + yield* Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + harness.emit("update-available", { version: "1.2.4" }); + yield* flushCallbacks; + + const downloadFiber = yield* updates.download.pipe(Effect.forkScoped); + yield* Deferred.await(actionStarted); + yield* Fiber.interrupt(downloadFiber); + + const interruptedState = yield* updates.getState; + assert.equal(interruptedState.status, "available"); + assert.isNull(interruptedState.message); + + const retry = yield* updates.download; + assert.isTrue(retry.accepted); + assert.isTrue(retry.completed); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }), + ); + it.effect("clears quitting state after an unexpected install setup failure", () => { const harness = makeHarness({ stopBackend: Effect.die(new Error("backend stop failed")), @@ -396,8 +437,9 @@ describe("DesktopUpdates", () => { harness.emit("update-downloaded", { version: "1.2.4" }); yield* flushCallbacks; - const exit = yield* Effect.exit(updates.install); - assert.equal(exit._tag, "Failure"); + const result = yield* updates.install; + assert.isTrue(result.accepted); + assert.isFalse(result.completed); assert.isFalse(yield* Ref.get(desktopState.quitting)); const failedState = yield* updates.getState; diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index 01bc683c9d6..a336466603a 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -408,9 +408,14 @@ export const make = Effect.gen(function* () { }, ), }), - Effect.onError((cause) => { + Effect.onInterrupt(() => + updateState((current) => (current.status === "downloading" ? state : current)).pipe( + Effect.asVoid, + ), + ), + Effect.catchCause((cause) => { if (Cause.hasInterruptsOnly(cause)) { - return Effect.void; + return Effect.failCause(cause); } const error = new DesktopUpdateUnexpectedActionError({ action: "download", cause }); return Effect.gen(function* () { @@ -418,12 +423,18 @@ export const make = Effect.gen(function* () { reduceDesktopUpdateStateOnDownloadFailure(current, error.message), ); yield* logUpdaterError(error.message, { error }); + return { accepted: true, completed: false }; }); }), Effect.ensuring(Ref.set(updateDownloadInFlightRef, false)), ); }).pipe(Effect.withSpan("desktop.updates.downloadAvailableUpdate")); + const resetInstallAction = Effect.all( + [Ref.set(updateInstallInFlightRef, false), Ref.set(desktopState.quitting, false)], + { discard: true }, + ); + const installDownloadedUpdate = Effect.gen(function* () { const state = yield* Ref.get(updateStateRef); if ( @@ -449,28 +460,28 @@ export const make = Effect.gen(function* () { Effect.catchTags({ ElectronUpdaterQuitAndInstallError: Effect.fn("desktop.updates.handleInstallFailure")( function* (error) { - yield* Ref.set(updateInstallInFlightRef, false); + yield* resetInstallAction; yield* updateState((current) => reduceDesktopUpdateStateOnInstallFailure(current, error.message), ); - yield* Ref.set(desktopState.quitting, false); yield* logUpdaterError(error.message, { error }); return { accepted: true, completed: false }; }, ), }), - Effect.onError((cause) => + Effect.onInterrupt(() => resetInstallAction), + Effect.catchCause((cause) => Effect.gen(function* () { - yield* Ref.set(updateInstallInFlightRef, false); - yield* Ref.set(desktopState.quitting, false); if (Cause.hasInterruptsOnly(cause)) { - return; + return yield* Effect.failCause(cause); } + yield* resetInstallAction; const error = new DesktopUpdateUnexpectedActionError({ action: "install", cause }); yield* updateState((current) => reduceDesktopUpdateStateOnInstallFailure(current, error.message), ); yield* logUpdaterError(error.message, { error }); + return { accepted: true, completed: false }; }), ), ); From 5c21085bfb04a0c04f96827bef6c0dd0a889c89d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 16:56:20 -0700 Subject: [PATCH 8/8] Redact desktop updater log causes Co-authored-by: codex --- .../src/updates/DesktopUpdates.test.ts | 25 ++++---- apps/desktop/src/updates/DesktopUpdates.ts | 62 +++++++++++++++---- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index a634409d9b1..4c90afb2a12 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -11,7 +11,6 @@ import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; import * as References from "effect/References"; import * as Ref from "effect/Ref"; -import * as Schema from "effect/Schema"; import * as TestClock from "effect/testing/TestClock"; import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts"; @@ -23,10 +22,6 @@ import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as DesktopState from "../app/DesktopState.ts"; import * as DesktopUpdates from "./DesktopUpdates.ts"; -const isElectronUpdaterCheckForUpdatesError = Schema.is( - ElectronUpdater.ElectronUpdaterCheckForUpdatesError, -); - interface UpdatesHarnessOptions { readonly checkForUpdates?: Effect.Effect< void, @@ -305,7 +300,7 @@ describe("DesktopUpdates", () => { ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); - it.effect("logs updater action failures with the exact structured error", () => { + it.effect("logs bounded updater failure context without exposing the cause", () => { const cause = new Error( "request failed for https://user:secret@example.com/update?token=secret", ); @@ -314,11 +309,11 @@ describe("DesktopUpdates", () => { cause, }); const harness = makeHarness({ checkForUpdates: Effect.fail(updaterError) }); - const loggedErrors: Array = []; + const loggedAnnotations: Array> = []; const logger = Logger.make(({ fiber }) => { - const error = fiber.getRef(References.CurrentLogAnnotations).error; - if (error !== undefined) { - loggedErrors.push(error); + const annotations = fiber.getRef(References.CurrentLogAnnotations); + if (annotations.errorTag === "ElectronUpdaterCheckForUpdatesError") { + loggedAnnotations.push(annotations); } }); @@ -330,10 +325,12 @@ describe("DesktopUpdates", () => { yield* updates.check("manual"); const state = yield* updates.getState; - const loggedError = loggedErrors.find(isElectronUpdaterCheckForUpdatesError); - assert.isDefined(loggedError); - assert.strictEqual(loggedError, updaterError); - assert.strictEqual(loggedError.cause, cause); + const loggedAnnotation = loggedAnnotations.at(-1); + assert.isDefined(loggedAnnotation); + assert.equal(loggedAnnotation.errorTag, "ElectronUpdaterCheckForUpdatesError"); + assert.isNull(loggedAnnotation.channel); + assert.notProperty(loggedAnnotation, "error"); + assert.notInclude(Object.values(loggedAnnotation).map(String).join(" "), "secret"); assert.equal( state.message, "Electron updater failed to check for updates on channel default.", diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index a336466603a..aecbdcfc3e8 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -369,7 +369,10 @@ export const make = Effect.gen(function* () { yield* updateState((current) => reduceDesktopUpdateStateOnCheckFailure(current, error.message, failedAt), ); - yield* logUpdaterError(error.message, { error }); + yield* logUpdaterError(error.message, { + errorTag: error._tag, + channel: error.channel, + }); return true; }), }), @@ -403,7 +406,10 @@ export const make = Effect.gen(function* () { yield* updateState((current) => reduceDesktopUpdateStateOnDownloadFailure(current, error.message), ); - yield* logUpdaterError(error.message, { error }); + yield* logUpdaterError(error.message, { + errorTag: error._tag, + channel: error.channel, + }); return { accepted: true, completed: false }; }, ), @@ -422,7 +428,10 @@ export const make = Effect.gen(function* () { yield* updateState((current) => reduceDesktopUpdateStateOnDownloadFailure(current, error.message), ); - yield* logUpdaterError(error.message, { error }); + yield* logUpdaterError(error.message, { + errorTag: error._tag, + action: error.action, + }); return { accepted: true, completed: false }; }); }), @@ -464,7 +473,12 @@ export const make = Effect.gen(function* () { yield* updateState((current) => reduceDesktopUpdateStateOnInstallFailure(current, error.message), ); - yield* logUpdaterError(error.message, { error }); + yield* logUpdaterError(error.message, { + errorTag: error._tag, + channel: error.channel, + isSilent: error.isSilent, + isForceRunAfter: error.isForceRunAfter, + }); return { accepted: true, completed: false }; }, ), @@ -480,7 +494,10 @@ export const make = Effect.gen(function* () { yield* updateState((current) => reduceDesktopUpdateStateOnInstallFailure(current, error.message), ); - yield* logUpdaterError(error.message, { error }); + yield* logUpdaterError(error.message, { + errorTag: error._tag, + action: error.action, + }); return { accepted: true, completed: false }; }), ), @@ -495,7 +512,10 @@ export const make = Effect.gen(function* () { return Effect.void; } const error = new DesktopUpdatePollerError({ poller: "startup", cause }); - return logUpdaterError(error.message, { error }); + return logUpdaterError(error.message, { + errorTag: error._tag, + poller: error.poller, + }); }), Effect.forkScoped, ); @@ -507,7 +527,10 @@ export const make = Effect.gen(function* () { return Effect.void; } const error = new DesktopUpdatePollerError({ poller: "poll", cause }); - return logUpdaterError(error.message, { error }); + return logUpdaterError(error.message, { + errorTag: error._tag, + poller: error.poller, + }); }), Effect.forkScoped, ); @@ -544,7 +567,10 @@ export const make = Effect.gen(function* () { return Effect.void; } const error = new DesktopUpdateEventHandlingError({ event: "update-available", cause }); - return logUpdaterWarning(error.message, { error }); + return logUpdaterWarning(error.message, { + errorTag: error._tag, + event: error.event, + }); }), ); }); @@ -571,7 +597,10 @@ export const make = Effect.gen(function* () { yield* updateState((current) => reduceDesktopUpdateStateOnInstallFailure(current, error.message), ); - yield* logUpdaterError(error.message, { error }); + yield* logUpdaterError(error.message, { + errorTag: error._tag, + operation: error.operation, + }); return; } @@ -589,7 +618,10 @@ export const make = Effect.gen(function* () { })); } - yield* logUpdaterError(error.message, { error }); + yield* logUpdaterError(error.message, { + errorTag: error._tag, + operation: error.operation, + }); }); const handleDownloadProgress = Effect.fn("desktop.updates.handleDownloadProgress")(function* ( @@ -616,7 +648,10 @@ export const make = Effect.gen(function* () { return Effect.void; } const error = new DesktopUpdateEventHandlingError({ event: "download-progress", cause }); - return logUpdaterWarning(error.message, { error }); + return logUpdaterWarning(error.message, { + errorTag: error._tag, + event: error.event, + }); }), ); }); @@ -637,7 +672,10 @@ export const make = Effect.gen(function* () { return Effect.void; } const error = new DesktopUpdateEventHandlingError({ event: "update-downloaded", cause }); - return logUpdaterWarning(error.message, { error }); + return logUpdaterWarning(error.message, { + errorTag: error._tag, + event: error.event, + }); }), ); });