diff --git a/apps/web/src/state/desktopUpdate.test.ts b/apps/web/src/state/desktopUpdate.test.ts index 77409ef611d..f6b3081a80f 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,17 @@ 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({ + errorTag: "DesktopUpdateStateReadError", + attemptCount: 3, + }); + expect(errorContext).not.toHaveProperty("error"); + expect(errorContext).not.toHaveProperty("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..75764410625 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,23 @@ 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, { + errorTag: error._tag, + attemptCount: error.attemptCount, + stack: error.stack, + }).pipe(Effect.as(null)), + }), ); if (!receivedUpdate && initialState !== null) { Queue.offerUnsafe(queue, initialState);