| 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import type { DesktopBridge, DesktopUpdateState } from "@t3tools/contracts"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Queue from "effect/Queue"; |
| 5 | import * as Schema from "effect/Schema"; |
| 6 | import * as Stream from "effect/Stream"; |
| 7 | import * as AsyncResult from "effect/reactivity/AsyncResult"; |
| 8 | import { Atom } from "effect/reactivity"; |
| 9 | |
| 10 | type DesktopUpdateBridge = Pick<DesktopBridge, "getUpdateState" | "onUpdateState">; |
| 11 | |
| 12 | const INITIAL_STATE_READ_ATTEMPT_COUNT = 3; |
| 13 | |
| 14 | export 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 | |
| 26 | function getDesktopUpdateBridge(): DesktopUpdateBridge | undefined { |
| 27 | return typeof window === "undefined" ? undefined : window.desktopBridge; |
| 28 | } |
| 29 | |
| 30 | export 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 | |
| 82 | const desktopUpdateStateAtom = createDesktopUpdateStateAtom(getDesktopUpdateBridge); |
| 83 | |
| 84 | export function useDesktopUpdateState(): DesktopUpdateState | null { |
| 85 | return AsyncResult.getOrElse(useAtomValue(desktopUpdateStateAtom), () => null); |
| 86 | } |