1import { useAtomValue } from "@effect/atom-react";
2import {
3 linkableMachines,
4 linkedEnvironmentNames,
5 offersLinkedEnvironments,
6} from "@t3tools/client-runtime/link-request";
7import { createPeerLinkEnvironmentAtoms } from "@t3tools/client-runtime/state/peerLinks";
8import { AuthAccessReadScope, type EnvironmentId } from "@t3tools/contracts";
9import { Atom } from "effect/reactivity";
10import { useEffect, useEffectEvent } from "react";
11
12import { connectionAtomRuntime } from "../connection/runtime";
13import { useEnvironmentIdentities, useEnvironments } from "./environments";
14import { environmentPresentations } from "./presentation";
15import { relayEnvironmentDiscovery } from "./relay";
16import { useEnvironmentQuery } from "./query";
17import { useEnvironmentsWithScope } from "./session";
18import { useAtomCommand } from "./use-atom-command";
19
20export const peerLinkEnvironment = createPeerLinkEnvironmentAtoms(connectionAtomRuntime);
21
22/** Whether `environmentId` offers links, so its threads may name another machine. */
23export const offersLinkedEnvironmentsAtom = Atom.family((environmentId: EnvironmentId) =>
24 Atom.make((get) =>
25 offersLinkedEnvironments(get(environmentPresentations.serverConfigsAtom).get(environmentId)),
26 ).pipe(Atom.withLabel(`peer-links:offered:${environmentId}`)),
27);
28
29const offersNoLinksAtom = Atom.make(false).pipe(Atom.withLabel("peer-links:offered:none"));
30
31/**
32 * The machines `environmentId` links to, by name, while it offers links, and none otherwise.
33 * Its agents can read threads there. Listing asks each linked machine whether it answers, so
34 * every caller shares one briefly cached list.
35 */
36export function useLinkedEnvironments(
37 environmentId: EnvironmentId | null,
38): ReadonlyMap<EnvironmentId, string> {
39 const offered = useAtomValue(
40 environmentId === null ? offersNoLinksAtom : offersLinkedEnvironmentsAtom(environmentId),
41 );
42 const identities = useEnvironmentIdentities();
43 const links = useEnvironmentQuery(
44 offered && environmentId !== null
45 ? peerLinkEnvironment.list({ environmentId, input: {} })
46 : null,
47 );
48 return linkedEnvironmentNames({
49 links: offered ? (links.data?.links ?? []) : [],
50 environments: identities,
51 });
52}
53
54export function usePeerLinkHosts() {
55 const { environments } = useEnvironments();
56 const supported = environments.filter(
57 (environment) =>
58 environment.connection.phase === "connected" &&
59 offersLinkedEnvironments(environment.serverConfig),
60 );
61 const readable = useEnvironmentsWithScope(supported, AuthAccessReadScope);
62 return supported.filter((environment) => readable.has(environment.environmentId));
63}
64
65/**
66 * The user's other machines, for picking one to link `environmentId` to,
67 * including those only on their T3 Connect account. Reads that environment's
68 * links only while `enabled`, since listing them asks every linked machine
69 * whether it answers.
70 */
71export function useLinkableMachines(environmentId: EnvironmentId, enabled: boolean) {
72 const { environments } = useEnvironments();
73 const discovery = useAtomValue(relayEnvironmentDiscovery.stateValueAtom);
74 const refreshDiscovery = useAtomCommand(relayEnvironmentDiscovery.refresh, {
75 reportFailure: false,
76 });
77 // Connect hosts are listed only once something asked for them, so ask on open.
78 const discoverIfUnasked = useEffectEvent(() => {
79 if (discovery.environments.size === 0 && !discovery.refreshing) void refreshDiscovery();
80 });
81 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- discovery follows the picker's visibility, which a card's mount or a dialog's open sets
82 useEffect(() => {
83 if (enabled) discoverIfUnasked();
84 }, [enabled]);
85 const links = useEnvironmentQuery(
86 enabled ? peerLinkEnvironment.list({ environmentId, input: {} }) : null,
87 );
88 return linkableMachines({
89 environments,
90 threadEnvironmentId: environmentId,
91 links: links.data?.links ?? [],
92 connect: discovery.environments,
93 });
94}