authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:04:53-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:04:53-07:00
logb9e22de6a354a97c2ecc772ad00754bfbd7333e6
treef83e2f30a8d78cdaa86fd274cef81fcdd18bd874
parentf3b43a148b9e39192be5d43415289ac06eca1443
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve desktop update state read failures (#3370)

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

2 files changed, 52 insertions(+), 5 deletions(-)

apps/web/src/state/desktopUpdate.test.ts+20-2
......@@ -1,7 +1,7 @@
11import type { DesktopUpdateState } from "@t3tools/contracts";
22import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
33import { AtomRegistry } from "effect/unstable/reactivity";
4import { describe, expect, it, vi } from "vite-plus/test";
4import { afterEach, describe, expect, it, vi } from "vite-plus/test";
55
66import { createDesktopUpdateStateAtom } from "./desktopUpdate";
77
......@@ -22,6 +22,10 @@ const baseState: DesktopUpdateState = {
2222 canRetry: false,
2323};
2424
25afterEach(() => {
26 vi.restoreAllMocks();
27});
28
2529describe("desktopUpdateStateAtom", () => {
2630 it("loads once, retains state, and follows desktop update events", async () => {
2731 let listener: ((state: DesktopUpdateState) => void) | undefined;
......@@ -91,8 +95,11 @@ describe("desktopUpdateStateAtom", () => {
9195
9296 it("keeps listening when the initial desktop state read fails", async () => {
9397 let listener: ((state: DesktopUpdateState) => void) | undefined;
98 const cause = new Error("IPC unavailable");
99 const reportError = vi.spyOn(console, "log").mockImplementation(() => undefined);
100 const getUpdateState = vi.fn(async () => Promise.reject(cause));
94101 const atom = createDesktopUpdateStateAtom(() => ({
95 getUpdateState: async () => Promise.reject(new Error("IPC unavailable")),
102 getUpdateState,
96103 onUpdateState: (nextListener) => {
97104 listener = nextListener;
98105 return () => undefined;
......@@ -102,6 +109,17 @@ describe("desktopUpdateStateAtom", () => {
102109 registry.mount(atom);
103110
104111 await vi.waitFor(() => expect(listener).toBeDefined());
112 await vi.waitFor(() => expect(reportError).toHaveBeenCalledOnce());
113 expect(getUpdateState).toHaveBeenCalledTimes(3);
114 const [, errorMessage, errorContext] = reportError.mock.calls[0] ?? [];
115 expect(errorMessage).toBe("Failed to read the initial desktop update state after 3 attempts.");
116 expect(errorContext).toMatchObject({
117 errorTag: "DesktopUpdateStateReadError",
118 attemptCount: 3,
119 });
120 expect(errorContext).not.toHaveProperty("error");
121 expect(errorContext).not.toHaveProperty("cause");
122
105123 listener?.(baseState);
106124 await vi.waitFor(() => {
107125 expect(AsyncResult.getOrElse(registry.get(atom), () => null)).toEqual(baseState);
apps/web/src/state/desktopUpdate.ts+32-3
......@@ -2,12 +2,27 @@ import { useAtomValue } from "@effect/atom-react";
22import type { DesktopBridge, DesktopUpdateState } from "@t3tools/contracts";
33import * as Effect from "effect/Effect";
44import * as Queue from "effect/Queue";
5import * as Schema from "effect/Schema";
56import * as Stream from "effect/Stream";
67import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
78import { Atom } from "effect/unstable/reactivity";
89
910type DesktopUpdateBridge = Pick<DesktopBridge, "getUpdateState" | "onUpdateState">;
1011
12const INITIAL_STATE_READ_ATTEMPT_COUNT = 3;
13
14export class DesktopUpdateStateReadError extends Schema.TaggedErrorClass<DesktopUpdateStateReadError>()(
15 "DesktopUpdateStateReadError",
16 {
17 attemptCount: Schema.Number,
18 cause: Schema.Defect(),
19 },
20) {
21 override get message(): string {
22 return `Failed to read the initial desktop update state after ${this.attemptCount} attempts.`;
23 }
24}
25
1126function getDesktopUpdateBridge(): DesktopUpdateBridge | undefined {
1227 return typeof window === "undefined" ? undefined : window.desktopBridge;
1328}
......@@ -32,9 +47,23 @@ export function createDesktopUpdateStateAtom(getBridge: () => DesktopUpdateBridg
3247 (unsubscribe) => Effect.sync(unsubscribe),
3348 );
3449
35 const initialState = yield* Effect.tryPromise(() => bridge.getUpdateState()).pipe(
36 Effect.retry({ times: 2 }),
37 Effect.orElseSucceed(() => null),
50 const initialState = yield* Effect.tryPromise({
51 try: () => bridge.getUpdateState(),
52 catch: (cause) =>
53 new DesktopUpdateStateReadError({
54 attemptCount: INITIAL_STATE_READ_ATTEMPT_COUNT,
55 cause,
56 }),
57 }).pipe(
58 Effect.retry({ times: INITIAL_STATE_READ_ATTEMPT_COUNT - 1 }),
59 Effect.catchTags({
60 DesktopUpdateStateReadError: (error) =>
61 Effect.logError(error.message, {
62 errorTag: error._tag,
63 attemptCount: error.attemptCount,
64 stack: error.stack,
65 }).pipe(Effect.as(null)),
66 }),
3867 );
3968 if (!receivedUpdate && initialState !== null) {
4069 Queue.offerUnsafe(queue, initialState);