Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions apps/web/src/state/desktopUpdate.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
35 changes: 32 additions & 3 deletions apps/web/src/state/desktopUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DesktopBridge, "getUpdateState" | "onUpdateState">;

const INITIAL_STATE_READ_ATTEMPT_COUNT = 3;

export class DesktopUpdateStateReadError extends Schema.TaggedErrorClass<DesktopUpdateStateReadError>()(
"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;
}
Expand All @@ -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);
Expand Down
Loading