File size: 3,064 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
"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;
}