Files
next-tjwater-frontend/src/features/agent/hooks/use-agent-connection.ts
T

81 lines
3.4 KiB
TypeScript

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<AgentApiClient, "getHealth">, 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<typeof setTimeout> | undefined;
let timeout: ReturnType<typeof setTimeout> | 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 };
}