1import { useAtomValue } from "@effect/atom-react";
2import type { DesktopBridge, DesktopUpdateState } from "@t3tools/contracts";
3import * as Effect from "effect/Effect";
4import * as Queue from "effect/Queue";
5import * as Schema from "effect/Schema";
6import * as Stream from "effect/Stream";
7import * as AsyncResult from "effect/reactivity/AsyncResult";
8import { Atom } from "effect/reactivity";
9
10type DesktopUpdateBridge = Pick<DesktopBridge, "getUpdateState" | "onUpdateState">;
11
12const INITIAL_STATE_READ_ATTEMPT_COUNT = 3;
13
14export class DesktopUpdateStateReadError extends Schema.TaggedError<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
26function getDesktopUpdateBridge(): DesktopUpdateBridge | undefined {
27 return typeof window === "undefined" ? undefined : window.desktopBridge;
28}
29
30export function createDesktopUpdateStateAtom(getBridge: () => DesktopUpdateBridge | undefined) {
31 const updates = Stream.callback<DesktopUpdateState | null>((queue) =>
32 Effect.gen(function* () {
33 const bridge = getBridge();
34 if (!bridge) {
35 Queue.offerUnsafe(queue, null);
36 return yield* Effect.never;
37 }
38
39 let receivedUpdate = false;
40 yield* Effect.acquireRelease(
41 Effect.sync(() =>
42 bridge.onUpdateState((state) => {
43 receivedUpdate = true;
44 Queue.offerUnsafe(queue, state);
45 }),
46 ),
47 (unsubscribe) => Effect.sync(unsubscribe),
48 );
49
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 error,
63 errorTag: error._tag,
64 attemptCount: error.attemptCount,
65 }).pipe(Effect.as(null)),
66 }),
67 );
68 if (!receivedUpdate && initialState !== null) {
69 Queue.offerUnsafe(queue, initialState);
70 }
71
72 return yield* Effect.never;
73 }),
74 );
75
76 return Atom.make(updates, { initialValue: null }).pipe(
77 Atom.keepAlive,
78 Atom.withLabel("desktop:update-state"),
79 );
80}
81
82const desktopUpdateStateAtom = createDesktopUpdateStateAtom(getDesktopUpdateBridge);
83
84export function useDesktopUpdateState(): DesktopUpdateState | null {
85 return AsyncResult.getOrElse(useAtomValue(desktopUpdateStateAtom), () => null);
86}