1import { useAtomMount, useAtomSet, useAtomValue } from "@effect/atom-react";
2import type { DeviceHubAccess } from "@t3tools/client-runtime/state/deviceHubAccess";
3import type { DeviceActionInput, DeviceSummary, EnvironmentId } from "@t3tools/contracts";
4import { useRef, useState } from "react";
5import { deviceDetailAtom, deviceDetailReadAtom, deviceEnvironment } from "~/state/device";
6import { formatEnvironmentQueryError } from "~/state/query";
7import { useAtomCommand } from "~/state/use-atom-command";
8import { deviceForegroundAtom, NO_FOREGROUND_ATOM, NOOP_ATOM } from "./deviceHubAtoms";
9
10type ActionBody = DeviceActionInput extends infer A
11 ? A extends { readonly type: string }
12 ? Omit<A, "hostId" | "deviceId">
13 : never
14 : never;
15
16/** Mount keyed by device. */
17export function useDeviceControls(options: {
18 environmentId: EnvironmentId;
19 device: DeviceSummary;
20 access: DeviceHubAccess | null;
21 visible: boolean;
22}) {
23 const { environmentId, device, access, visible } = options;
24 const runAction = useAtomCommand(deviceEnvironment.action, { reportFailure: false });
25 const { hostId, id: deviceId } = device;
26 const key = { environmentId, hostId, deviceId };
27 const { detail, error } = useAtomValue(deviceDetailAtom(key));
28 const setView = useAtomSet(deviceDetailAtom(key));
29 useAtomMount(visible ? deviceDetailReadAtom(key) : NOOP_ATOM);
30 const foreground = useAtomValue(
31 access && visible
32 ? deviceForegroundAtom({ access, platform: device.platform, deviceId })
33 : NO_FOREGROUND_ATOM,
34 );
35 const [pending, setPending] = useState(false);
36 // Hiding does not cancel host commands, so actions stay serialized until settlement
37 // rather than letting a reopened panel race the previous command.
38 const busy = useRef(false);
39
40 const available = detail !== null && visible;
41 const act = async (body: ActionBody) => {
42 if (busy.current || !available) return false;
43 busy.current = true;
44 setPending(true);
45 setView((current) => ({ ...current, error: null }));
46 return runAction({
47 environmentId,
48 input: { hostId, deviceId, ...body } as DeviceActionInput,
49 })
50 .then((result) => {
51 setView((current) =>
52 result._tag === "Success"
53 ? { detail: result.value, error: null }
54 : { ...current, error: formatEnvironmentQueryError(result.cause) },
55 );
56 return result._tag === "Success";
57 })
58 .finally(() => {
59 busy.current = false;
60 setPending(false);
61 });
62 };
63
64 return {
65 detail,
66 pending,
67 error,
68 act,
69 disabled: pending || !detail || !visible,
70 foregroundApp: foreground === undefined ? (detail?.foregroundApp ?? null) : foreground,
71 };
72}
73
74export type DeviceControls = ReturnType<typeof useDeviceControls>;