import { useCallback, useEffect, useRef, useState } from "react"; import type { AgentApiClient } from "../api/client"; import type { AgentConnectionStatus } from "../types"; /** Service readiness is independent of a conversation's business result. */ export function useAgentConnection(client: Pick, scope: string) { const [snapshot, setSnapshot] = useState({ client, scope, status: "connecting" as AgentConnectionStatus, recoveryVersion: 0 }); const check = useRef<() => void>(() => {}); const recheck = useCallback(() => check.current(), []); useEffect(() => { let disposed = false; let timer: ReturnType | undefined; let timeout: ReturnType | undefined; let controller: AbortController | undefined; let failures = 0; let status: AgentConnectionStatus = "connecting"; let recoveryVersion = 0; const publish = (next: AgentConnectionStatus) => { if (disposed) return; if (next === "online" && status === "offline") recoveryVersion++; status = next; setSnapshot((previous) => previous.client === client && previous.scope === scope && previous.status === status && previous.recoveryVersion === recoveryVersion ? previous : { client, scope, status, recoveryVersion }); }; const probe = async () => { if (disposed || controller) return; clearTimeout(timer); if (!navigator.onLine) { publish("offline"); return; } const pending = new AbortController(); controller = pending; timeout = setTimeout(() => pending.abort(), 5_000); try { await client.getHealth(pending.signal); if (disposed || controller !== pending) return; failures = 0; publish("online"); } catch { if (disposed || controller !== pending) return; failures++; publish("offline"); } finally { if (controller === pending) { clearTimeout(timeout); controller = undefined; if (!disposed) timer = setTimeout(() => void probe(), failures ? Math.min(1_000 * 2 ** Math.min(failures - 1, 4), 15_000) : 15_000); } } }; const reconnect = () => { void probe(); }; const visible = () => { if (document.visibilityState === "visible") reconnect(); }; const offline = () => { clearTimeout(timer); clearTimeout(timeout); const pending = controller; controller = undefined; pending?.abort(); publish("offline"); }; check.current = reconnect; publish("connecting"); reconnect(); window.addEventListener("focus", reconnect); window.addEventListener("online", reconnect); window.addEventListener("offline", offline); document.addEventListener("visibilitychange", visible); return () => { disposed = true; clearTimeout(timer); clearTimeout(timeout); controller?.abort(); check.current = () => {}; window.removeEventListener("focus", reconnect); window.removeEventListener("online", reconnect); window.removeEventListener("offline", offline); document.removeEventListener("visibilitychange", visible); }; }, [client, scope]); return { connectionStatus: snapshot.client === client && snapshot.scope === scope ? snapshot.status : "connecting" as const, recoveryVersion: snapshot.client === client && snapshot.scope === scope ? snapshot.recoveryVersion : 0, recheck }; }