1import { type PeerLinkSummary, WS_METHODS } from "@t3tools/contracts";
2import * as DateTime from "effect/DateTime";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import { Atom } from "effect/reactivity";
6
7import type { EnvironmentRegistry } from "../connection/registry.ts";
8import { createEnvironmentRpcCommand, createEnvironmentRpcQueryAtomFamily } from "./runtime.ts";
9
10export function createPeerLinkEnvironmentAtoms<R, E>(
11 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,
12) {
13 const list = createEnvironmentRpcQueryAtomFamily(runtime, {
14 label: "environment-data:peer-links:list",
15 tag: WS_METHODS.peerLinksList,
16 // Listing probes every linked environment, so a list stays fresh briefly.
17 staleTimeMs: 10_000,
18 });
19 return {
20 list,
21 link: createEnvironmentRpcCommand(runtime, {
22 label: "environment-data:peer-links:link",
23 tag: WS_METHODS.peerLinksLink,
24 onSuccess: ({ environmentId }, registry) =>
25 Effect.sync(() => registry.refresh(list({ environmentId, input: {} }))),
26 }),
27 unlink: createEnvironmentRpcCommand(runtime, {
28 label: "environment-data:peer-links:unlink",
29 tag: WS_METHODS.peerLinksUnlink,
30 onSuccess: ({ environmentId }, registry) =>
31 Effect.sync(() => registry.refresh(list({ environmentId, input: {} }))),
32 }),
33 // An agent's link card. One answer per card at a time.
34 answerRequest: createEnvironmentRpcCommand(runtime, {
35 label: "environment-data:peer-links:answer-request",
36 tag: WS_METHODS.peerLinksAnswerRequest,
37 concurrency: {
38 mode: "singleFlight",
39 key: ({ environmentId, input }) =>
40 JSON.stringify([environmentId, input.threadId, input.turnItemId]),
41 },
42 onSuccess: ({ environmentId }, registry) =>
43 Effect.sync(() => registry.refresh(list({ environmentId, input: {} }))),
44 }),
45 };
46}
47
48const EXPIRY_WARNING_DAYS = 5;
49
50export type PeerLinkHealth =
51 | { readonly kind: "reachable"; readonly expiresInDays: number | null }
52 | { readonly kind: "unreachable"; readonly detail: string | null }
53 | { readonly kind: "failing"; readonly detail: string }
54 | { readonly kind: "expired" };
55
56/**
57 * Listing only checks that the peer answers, so a link whose last call failed
58 * (the peer revoked it, say) is `failing` with the recorded reason; the next
59 * successful call clears it.
60 */
61export function peerLinkHealth(link: PeerLinkSummary, now: DateTime.Utc): PeerLinkHealth {
62 if (link.status === "expired" || DateTime.isLessThanOrEqualTo(link.expiresAt, now)) {
63 return { kind: "expired" };
64 }
65 if (link.status === "unreachable") return { kind: "unreachable", detail: link.lastError };
66 if (link.lastError !== null) return { kind: "failing", detail: link.lastError };
67 const days = Math.ceil(Duration.toDays(DateTime.distance(now, link.expiresAt)));
68 return { kind: "reachable", expiresInDays: days <= EXPIRY_WARNING_DAYS ? days : null };
69}