1import { useMemo } from "react";
2import { useAtomValue } from "@effect/atom-react";
3import { createDeviceEnvironmentAtoms } from "@t3tools/client-runtime/state/device";
4import {
5 type DeviceHubAccess,
6 resolveDeviceHubAccess,
7} from "@t3tools/client-runtime/state/deviceHubAccess";
8import type { DeviceServiceState, EnvironmentId } from "@t3tools/contracts";
9import * as Effect from "effect/Effect";
10import * as Option from "effect/Option";
11import { AsyncResult, Atom } from "effect/reactivity";
12
13import { connectionAtomRuntime } from "../connection/runtime";
14import { appAtomRegistry } from "../rpc/atomRegistry";
15import { environmentSession } from "./session";
16import { useEnvironmentQuery } from "./query";
17
18export const deviceEnvironment = createDeviceEnvironmentAtoms(connectionAtomRuntime);
19
20const EMPTY_DEVICE_STATE: DeviceServiceState = {
21 hosts: [],
22 hostStatus: "disabled",
23 hostStatuses: {},
24 devices: [],
25 sessions: [],
26 onboardingCompleted: false,
27 agentAccessEnabled: false,
28 hubBasePath: "/api/device-hub",
29 revision: 0,
30};
31
32export function useDeviceState(environmentId: EnvironmentId | null): {
33 readonly state: DeviceServiceState;
34 readonly loaded: boolean;
35} {
36 const query = useEnvironmentQuery(
37 environmentId === null ? null : deviceEnvironment.state({ environmentId, input: {} }),
38 );
39 return { state: query.data ?? EMPTY_DEVICE_STATE, loaded: query.data !== undefined };
40}
41
42/**
43 * Hub access for one environment. Bearer and DPoP connections mint a ticket
44 * here; a stream that gets a 401 back refreshes this atom and reconnects.
45 * Keyed on the prepared connection so a re-pair produces new credentials.
46 */
47const deviceHubAccessAtom = Atom.family((environmentId: EnvironmentId) =>
48 connectionAtomRuntime
49 .atom((get) => {
50 const prepared = Option.getOrNull(
51 get(environmentSession.preparedConnectionValueAtom(environmentId)),
52 );
53 if (prepared === null) return Effect.never;
54 return resolveDeviceHubAccess({ prepared, hubBasePath: EMPTY_DEVICE_STATE.hubBasePath });
55 })
56 .pipe(Atom.setIdleTTL(60_000), Atom.withLabel(`device-hub-access:${environmentId}`)),
57);
58
59export function useDeviceHubAccess(
60 environmentId: EnvironmentId | null,
61 hostId = "local",
62): DeviceHubAccess | null {
63 const result = useAtomValue(
64 environmentId === null ? EMPTY_ACCESS_ATOM : deviceHubAccessAtom(environmentId),
65 );
66 return useMemo(
67 () =>
68 AsyncResult.isSuccess(result)
69 ? { ...result.value, query: { ...result.value.query, hostId } }
70 : null,
71 [result, hostId],
72 );
73}
74
75const EMPTY_ACCESS_ATOM = Atom.make(AsyncResult.initial<DeviceHubAccess, never>()).pipe(
76 Atom.withLabel("device-hub-access:empty"),
77);
78
79export function refreshDeviceHubAccess(environmentId: EnvironmentId): void {
80 appAtomRegistry.refresh(deviceHubAccessAtom(environmentId));
81}