1import { useAtomValue } from "@effect/atom-react";
2import { PREVIEW_STREAM_BASE_PATH } from "@t3tools/client-runtime/preview/server-browser-stream";
3import {
4 type DeviceHubAccess,
5 resolveDeviceHubAccess,
6} from "@t3tools/client-runtime/state/deviceHubAccess";
7import type { EnvironmentId } from "@t3tools/contracts";
8import * as Effect from "effect/Effect";
9import * as Option from "effect/Option";
10import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
11
12import { connectionAtomRuntime } from "../connection/runtime";
13import { appAtomRegistry } from "../rpc/atomRegistry";
14import { environmentSession } from "./session";
15
16// Re-pairing changes the prepared connection, invalidating its cached ticket.
17const previewStreamAccessAtom = Atom.family((environmentId: EnvironmentId) =>
18 connectionAtomRuntime
19 .atom((get) => {
20 const prepared = Option.getOrNull(
21 get(environmentSession.preparedConnectionValueAtom(environmentId)),
22 );
23 if (prepared === null) return Effect.never;
24 return resolveDeviceHubAccess({ prepared, hubBasePath: PREVIEW_STREAM_BASE_PATH });
25 })
26 .pipe(Atom.setIdleTTL(60_000), Atom.withLabel(`preview-stream-access:${environmentId}`)),
27);
28
29export function usePreviewStreamAccess(environmentId: EnvironmentId): DeviceHubAccess | null {
30 const result = useAtomValue(previewStreamAccessAtom(environmentId));
31 return AsyncResult.isSuccess(result) ? result.value : null;
32}
33
34export function refreshPreviewStreamAccess(environmentId: EnvironmentId): void {
35 appAtomRegistry.refresh(previewStreamAccessAtom(environmentId));
36}
37
38/** Fresh stream access for a long-lived viewer outside React, e.g. system picture in picture. */
39export function readPreviewStreamAccess(
40 environmentId: EnvironmentId,
41 refresh: boolean,
42): Promise<DeviceHubAccess | null> {
43 const atom = previewStreamAccessAtom(environmentId);
44 if (refresh) appAtomRegistry.refresh(atom);
45 return Effect.runPromise(
46 AtomRegistry.getResult(appAtomRegistry, atom, { suspendOnWaiting: true }).pipe(
47 Effect.timeout("10 seconds"),
48 Effect.orElseSucceed(() => null),
49 ),
50 );
51}