From 4808b4d2cb6df5c81bc47fb7f5817d3236a372a7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 06:30:40 -0700 Subject: [PATCH 1/2] refactor(web): preserve desktop update read failures Co-authored-by: codex --- apps/web/src/state/desktopUpdate.test.ts | 23 ++++++++++++++++-- apps/web/src/state/desktopUpdate.ts | 31 +++++++++++++++++++++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/apps/web/src/state/desktopUpdate.test.ts b/apps/web/src/state/desktopUpdate.test.ts index 77409ef611d..84569f02997 100644 --- a/apps/web/src/state/desktopUpdate.test.ts +++ b/apps/web/src/state/desktopUpdate.test.ts @@ -1,7 +1,7 @@ import type { DesktopUpdateState } from "@t3tools/contracts"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { AtomRegistry } from "effect/unstable/reactivity"; -import { describe, expect, it, vi } from "vite-plus/test"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { createDesktopUpdateStateAtom } from "./desktopUpdate"; @@ -22,6 +22,10 @@ const baseState: DesktopUpdateState = { canRetry: false, }; +afterEach(() => { + vi.restoreAllMocks(); +}); + describe("desktopUpdateStateAtom", () => { it("loads once, retains state, and follows desktop update events", async () => { let listener: ((state: DesktopUpdateState) => void) | undefined; @@ -91,8 +95,11 @@ describe("desktopUpdateStateAtom", () => { it("keeps listening when the initial desktop state read fails", async () => { let listener: ((state: DesktopUpdateState) => void) | undefined; + const cause = new Error("IPC unavailable"); + const reportError = vi.spyOn(console, "log").mockImplementation(() => undefined); + const getUpdateState = vi.fn(async () => Promise.reject(cause)); const atom = createDesktopUpdateStateAtom(() => ({ - getUpdateState: async () => Promise.reject(new Error("IPC unavailable")), + getUpdateState, onUpdateState: (nextListener) => { listener = nextListener; return () => undefined; @@ -102,6 +109,18 @@ describe("desktopUpdateStateAtom", () => { registry.mount(atom); await vi.waitFor(() => expect(listener).toBeDefined()); + await vi.waitFor(() => expect(reportError).toHaveBeenCalledOnce()); + expect(getUpdateState).toHaveBeenCalledTimes(3); + const [, errorMessage, errorContext] = reportError.mock.calls[0] ?? []; + expect(errorMessage).toBe("Failed to read the initial desktop update state after 3 attempts."); + expect(errorContext).toMatchObject({ + error: { + _tag: "DesktopUpdateStateReadError", + attemptCount: 3, + cause, + }, + }); + listener?.(baseState); await vi.waitFor(() => { expect(AsyncResult.getOrElse(registry.get(atom), () => null)).toEqual(baseState); diff --git a/apps/web/src/state/desktopUpdate.ts b/apps/web/src/state/desktopUpdate.ts index d08169770c3..2cb48c89af9 100644 --- a/apps/web/src/state/desktopUpdate.ts +++ b/apps/web/src/state/desktopUpdate.ts @@ -2,12 +2,27 @@ import { useAtomValue } from "@effect/atom-react"; import type { DesktopBridge, DesktopUpdateState } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Queue from "effect/Queue"; +import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { Atom } from "effect/unstable/reactivity"; type DesktopUpdateBridge = Pick; +const INITIAL_STATE_READ_ATTEMPT_COUNT = 3; + +export class DesktopUpdateStateReadError extends Schema.TaggedErrorClass()( + "DesktopUpdateStateReadError", + { + attemptCount: Schema.Number, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to read the initial desktop update state after ${this.attemptCount} attempts.`; + } +} + function getDesktopUpdateBridge(): DesktopUpdateBridge | undefined { return typeof window === "undefined" ? undefined : window.desktopBridge; } @@ -32,9 +47,19 @@ export function createDesktopUpdateStateAtom(getBridge: () => DesktopUpdateBridg (unsubscribe) => Effect.sync(unsubscribe), ); - const initialState = yield* Effect.tryPromise(() => bridge.getUpdateState()).pipe( - Effect.retry({ times: 2 }), - Effect.orElseSucceed(() => null), + const initialState = yield* Effect.tryPromise({ + try: () => bridge.getUpdateState(), + catch: (cause) => + new DesktopUpdateStateReadError({ + attemptCount: INITIAL_STATE_READ_ATTEMPT_COUNT, + cause, + }), + }).pipe( + Effect.retry({ times: INITIAL_STATE_READ_ATTEMPT_COUNT - 1 }), + Effect.catchTags({ + DesktopUpdateStateReadError: (error) => + Effect.logError(error.message, { error }).pipe(Effect.as(null)), + }), ); if (!receivedUpdate && initialState !== null) { Queue.offerUnsafe(queue, initialState); From 775fe69186b68ccf12d448a0cc36cb122a447f94 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 10:16:54 -0700 Subject: [PATCH 2/2] fix(web): sanitize desktop update failure logs Co-authored-by: codex --- apps/web/src/state/desktopUpdate.test.ts | 9 ++++----- apps/web/src/state/desktopUpdate.ts | 6 +++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/web/src/state/desktopUpdate.test.ts b/apps/web/src/state/desktopUpdate.test.ts index 84569f02997..f6b3081a80f 100644 --- a/apps/web/src/state/desktopUpdate.test.ts +++ b/apps/web/src/state/desktopUpdate.test.ts @@ -114,12 +114,11 @@ describe("desktopUpdateStateAtom", () => { const [, errorMessage, errorContext] = reportError.mock.calls[0] ?? []; expect(errorMessage).toBe("Failed to read the initial desktop update state after 3 attempts."); expect(errorContext).toMatchObject({ - error: { - _tag: "DesktopUpdateStateReadError", - attemptCount: 3, - cause, - }, + errorTag: "DesktopUpdateStateReadError", + attemptCount: 3, }); + expect(errorContext).not.toHaveProperty("error"); + expect(errorContext).not.toHaveProperty("cause"); listener?.(baseState); await vi.waitFor(() => { diff --git a/apps/web/src/state/desktopUpdate.ts b/apps/web/src/state/desktopUpdate.ts index 2cb48c89af9..75764410625 100644 --- a/apps/web/src/state/desktopUpdate.ts +++ b/apps/web/src/state/desktopUpdate.ts @@ -58,7 +58,11 @@ export function createDesktopUpdateStateAtom(getBridge: () => DesktopUpdateBridg Effect.retry({ times: INITIAL_STATE_READ_ATTEMPT_COUNT - 1 }), Effect.catchTags({ DesktopUpdateStateReadError: (error) => - Effect.logError(error.message, { error }).pipe(Effect.as(null)), + Effect.logError(error.message, { + errorTag: error._tag, + attemptCount: error.attemptCount, + stack: error.stack, + }).pipe(Effect.as(null)), }), ); if (!receivedUpdate && initialState !== null) {