Skip to content

Commit b9e22de

Browse files
[codex] Preserve desktop update state read failures (#3370)
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent f3b43a1 commit b9e22de

2 files changed

Lines changed: 52 additions & 5 deletions

File tree

apps/web/src/state/desktopUpdate.test.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { DesktopUpdateState } from "@t3tools/contracts";
22
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
33
import { AtomRegistry } from "effect/unstable/reactivity";
4-
import { describe, expect, it, vi } from "vite-plus/test";
4+
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
55

66
import { createDesktopUpdateStateAtom } from "./desktopUpdate";
77

@@ -22,6 +22,10 @@ const baseState: DesktopUpdateState = {
2222
canRetry: false,
2323
};
2424

25+
afterEach(() => {
26+
vi.restoreAllMocks();
27+
});
28+
2529
describe("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

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,27 @@ import { useAtomValue } from "@effect/atom-react";
22
import type { DesktopBridge, DesktopUpdateState } from "@t3tools/contracts";
33
import * as Effect from "effect/Effect";
44
import * as Queue from "effect/Queue";
5+
import * as Schema from "effect/Schema";
56
import * as Stream from "effect/Stream";
67
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
78
import { Atom } from "effect/unstable/reactivity";
89

910
type DesktopUpdateBridge = Pick<DesktopBridge, "getUpdateState" | "onUpdateState">;
1011

12+
const INITIAL_STATE_READ_ATTEMPT_COUNT = 3;
13+
14+
export 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+
1126
function 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);

0 commit comments

Comments
 (0)