81 lines
3.4 KiB
TypeScript
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 };
|
|
}
|