mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-14 05:37:40 +02:00
89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
// One source of truth for "is our local backend answering", fed by the global fetch
|
|
// interceptor (config.ts) and consumed by the reconnecting pill. Exists so an unreachable
|
|
// backend can never again present as a silent forever-spinner (ENG-241/ENG-242): every
|
|
// consumer reads the same signal, and a background probe self-heals the moment it returns.
|
|
|
|
type ReachabilityListener = (reachable: boolean) => void;
|
|
|
|
let reachableNow = true;
|
|
let failStreak = 0;
|
|
const listeners = new Set<ReachabilityListener>();
|
|
let probeTimer: ReturnType<typeof setInterval> | null = null;
|
|
// Injected by config.ts with the UN-intercepted fetch so probes never recurse into retry logic.
|
|
let prober: (() => Promise<unknown>) | null = null;
|
|
|
|
export function backendReachable(): boolean {
|
|
return reachableNow;
|
|
}
|
|
|
|
export function onBackendReachability(cb: ReachabilityListener): () => void {
|
|
listeners.add(cb);
|
|
return () => { listeners.delete(cb); };
|
|
}
|
|
|
|
export function setBackendProber(fn: () => Promise<unknown>): void {
|
|
prober = fn;
|
|
}
|
|
|
|
function emit(value: boolean): void {
|
|
listeners.forEach((l) => { try { l(value); } catch { /* a listener must never break the signal */ } });
|
|
}
|
|
|
|
function startProbe(): void {
|
|
if (probeTimer || !prober) return;
|
|
probeTimer = setInterval(() => {
|
|
void (prober as () => Promise<unknown>)()
|
|
.then(() => noteBackendSuccess())
|
|
.catch(() => { /* still down; keep probing */ });
|
|
}, 1500);
|
|
}
|
|
|
|
function stopProbe(): void {
|
|
if (probeTimer) { clearInterval(probeTimer); probeTimer = null; }
|
|
}
|
|
|
|
export function noteBackendFailure(): void {
|
|
failStreak++;
|
|
// Two consecutive failures = down. One lone failure is never a state flip, so a single
|
|
// dropped request can't flash the reconnecting UI.
|
|
if (reachableNow && failStreak >= 2) {
|
|
reachableNow = false;
|
|
emit(false);
|
|
startProbe();
|
|
return;
|
|
}
|
|
// Waiting for a SECOND user request to corroborate the first failure means an idle app never
|
|
// notices its backend died (measured: backend down 24s, signal still green, no pill). Ask the
|
|
// prober instead: it answers in ms and a healthy backend clears the streak, so this cannot flap.
|
|
if (reachableNow && failStreak === 1) setTimeout(() => { if (reachableNow) probeNow(); }, 300);
|
|
}
|
|
|
|
export function noteBackendSuccess(): void {
|
|
failStreak = 0;
|
|
if (!reachableNow) {
|
|
reachableNow = true;
|
|
stopProbe();
|
|
emit(true);
|
|
}
|
|
}
|
|
|
|
// A request that STALLS (a squatted or wedged port accepts the connection and never answers) would
|
|
// otherwise take the full attempt timeout before counting as a failure, leaving the user staring at
|
|
// a spinner for ~25s+ with the signal still green. The stall watchdog asks the prober directly, so
|
|
// "is the backend actually alive" is answered in seconds regardless of how long the request hangs.
|
|
export function probeNow(): void {
|
|
if (!prober) return;
|
|
void prober().then(() => noteBackendSuccess()).catch(() => noteBackendFailure());
|
|
}
|
|
|
|
export function noteRequestStalled(): void {
|
|
if (!reachableNow) return;
|
|
probeNow();
|
|
// Two probe failures are what flips the signal, matching the normal path; a backend that answers
|
|
// either probe is simply slow, not down, and nothing is shown.
|
|
setTimeout(() => { if (reachableNow) probeNow(); }, 1200);
|
|
}
|
|
|
|
// Harness/debug handle: lets a live session (CDP, support) read the signal without a store import.
|
|
(window as unknown as { __OSW_CONN?: object }).__OSW_CONN = { backendReachable, onBackendReachability };
|