fix: unify chat process and reply presentation
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user