Files
OmniRoute/src/hooks/useProviderBreakerHealth.ts
Diego Rodrigues de Sa e Souza ee24eb52d4 Release v3.8.33 (#4515)
Release v3.8.33 — full CHANGELOG in the PR body. Blocking gates green (Build, Lint, Unit Tests 8/8, Package Artifact, Quality Gates, Quality Ratchet, Docs Sync, PR Test Policy, test-vitest). Admin-merged over a Node 26 future-compat timer flake (1/4) + an E2E UI flake (3/9) — both verified non-deterministic; full test:unit validated locally (16936 pass).
2026-06-22 03:17:02 -03:00

90 lines
3.0 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import type {
ProviderBreakerSnapshot,
ConnectionCooldownSnapshot,
} from "@/app/(dashboard)/dashboard/combos/live/comboFlowModel";
const DEFAULT_POLL_MS = 5000;
const POLL_TIMEOUT_MS = 8000;
export interface ResilienceHealthSnapshot {
/** Per-provider circuit-breaker state (`providerHealth`). */
providerHealth: Record<string, ProviderBreakerSnapshot>;
/** Per-provider connection-cooldown summary (`connectionHealth`). */
connectionHealth: Record<string, ConnectionCooldownSnapshot>;
}
const EMPTY: ResilienceHealthSnapshot = { providerHealth: {}, connectionHealth: {} };
/**
* Polls `GET /api/monitoring/health` and exposes the resilience overlays the Combo
* Live Studio consumes (U1b): per-provider circuit-breaker state (`providerHealth`)
* and per-provider connection-cooldown summary (`connectionHealth`).
*
* Fail-soft by design: any network/parse error keeps the last known snapshot (or the
* empty default), so the cascade simply shows no resilience badges instead of breaking.
* One poll covers both overlays. Polls every `pollMs` and on mount.
*/
export function useProviderBreakerHealth(pollMs = DEFAULT_POLL_MS): ResilienceHealthSnapshot {
const [snapshot, setSnapshot] = useState<ResilienceHealthSnapshot>(EMPTY);
const inFlightRef = useRef<AbortController | null>(null);
useEffect(() => {
let cancelled = false;
const poll = async () => {
if (inFlightRef.current) return;
const controller = new AbortController();
inFlightRef.current = controller;
const timeoutId = setTimeout(
() => controller.abort("Provider breaker health timeout"),
POLL_TIMEOUT_MS
);
try {
const res = await fetch("/api/monitoring/health", {
signal: controller.signal,
cache: "no-store",
});
if (!res.ok) return;
const json = (await res.json()) as {
providerHealth?: Record<string, ProviderBreakerSnapshot>;
connectionHealth?: Record<string, ConnectionCooldownSnapshot>;
};
if (cancelled || !json || typeof json !== "object") return;
setSnapshot({
providerHealth:
typeof json.providerHealth === "object" && json.providerHealth
? json.providerHealth
: {},
connectionHealth:
typeof json.connectionHealth === "object" && json.connectionHealth
? json.connectionHealth
: {},
});
} catch {
// Fail-soft: keep the previous snapshot; cascade degrades to no badges.
} finally {
clearTimeout(timeoutId);
if (inFlightRef.current === controller) {
inFlightRef.current = null;
}
}
};
poll();
const id = setInterval(poll, pollMs);
return () => {
cancelled = true;
inFlightRef.current?.abort("Provider breaker health unmounted");
inFlightRef.current = null;
clearInterval(id);
};
}, [pollMs]);
return snapshot;
}