1import type { DesktopSnapShotState } from "@t3tools/contracts";
2import * as Option from "effect/Option";
3import { Atom, type AtomRegistry } from "effect/reactivity";
4
5import { getDesktopSnapShotBridge } from "../lib/desktopSnapShot";
6
7export interface SnapShotSetupError {
8 readonly title: string;
9 readonly message: string;
10}
11
12export function snapShotSetupError(title: string, error: unknown): SnapShotSetupError {
13 return { title, message: error instanceof Error ? error.message : "Try again." };
14}
15
16export interface DesktopSnapShotLoad {
17 readonly state: DesktopSnapShotState | null;
18 /** Why the last background read (load, focus, shortcut change) failed; a later success clears it. */
19 readonly refreshError: SnapShotSetupError | null;
20}
21
22const EMPTY_LOAD: DesktopSnapShotLoad = { state: null, refreshError: null };
23
24// Only the newest read lands, so a slow answer cannot regress the state. Dropping the atom bumps
25// it too, so a read that was in flight when the page closed does not seed the next visit.
26let stateRequestId = 0;
27
28/**
29 * The desktop capture state. Reading it starts a load, then re-reads on window focus (the user
30 * may have changed OS permissions) and when the desktop reports a shortcut change.
31 */
32export const desktopSnapShotAtom = Atom.writable(
33 (get): DesktopSnapShotLoad => {
34 get.addFinalizer(() => {
35 stateRequestId++;
36 });
37 const bridge = getDesktopSnapShotBridge();
38 if (!bridge) return EMPTY_LOAD;
39 const refresh = () =>
40 void refreshDesktopSnapShotState(get.registry, (error) => {
41 get.setSelf({
42 ...Option.getOrElse(get.self<DesktopSnapShotLoad>(), () => EMPTY_LOAD),
43 refreshError: snapShotSetupError("Couldn't check capture setup", error),
44 });
45 });
46 window.addEventListener("focus", refresh);
47 get.addFinalizer(() => window.removeEventListener("focus", refresh));
48 get.addFinalizer(
49 bridge.onSnapShotEvent((event) => {
50 if (event.type === "shortcut-changed") refresh();
51 }),
52 );
53 refresh();
54 return EMPTY_LOAD;
55 },
56 (ctx, value: DesktopSnapShotLoad) => ctx.setSelf(value),
57).pipe(Atom.withLabel("desktop:snap-shot:state"));
58
59/**
60 * Reads the capture state now and stores it. `onError` hears only a failure of the newest read;
61 * the result is `undefined` whenever this read did not produce the current state.
62 */
63export async function refreshDesktopSnapShotState(
64 registry: AtomRegistry.AtomRegistry,
65 onError: (error: unknown) => void,
66): Promise<DesktopSnapShotState | undefined> {
67 const bridge = getDesktopSnapShotBridge();
68 if (!bridge) return undefined;
69 const requestId = ++stateRequestId;
70 try {
71 const next = await bridge.getSnapShotState();
72 if (requestId === stateRequestId) {
73 registry.set(desktopSnapShotAtom, { state: next, refreshError: null });
74 }
75 return next;
76 } catch (error) {
77 if (requestId === stateRequestId) onError(error);
78 return undefined;
79 }
80}