"use client"; import { useEffect, useRef } from "react"; import { useWebSocket } from "./use-websocket"; import { useRateLimitStore } from "@/store/rate-limit-store"; import { useUsageStore } from "@/store/usage-store"; import type { RateLimitHitEvent, RateLimitLiftedEvent } from "@/types/rate-limits"; /** * Unified shape for all messages arriving on the /ws/system endpoint. * Fields are optional because different message types use different subsets. */ interface SystemWsMessage { type: string; // Rate-limit fields (RATE_LIMIT_HIT / RATE_LIMIT_LIFTED) provider?: string; affectedAgents?: string[]; retryAfterSeconds?: number; // Usage fields (USAGE_SNAPSHOT — aggregate token/cost across active agents) totals?: { input_tokens?: number; output_tokens?: number }; cost_estimate?: number; period?: string; // Shared timestamp?: string; } interface UseRateLimitWebSocketOptions { /** Called when the WebSocket reconnects after a disconnect */ onReconnect?: () => void; } /** * Subscribes to the /ws/system WebSocket (single shared instance mounted in * RateLimitBanner). Handles: * * - RATE_LIMIT_HIT / RATE_LIMIT_LIFTED → dispatched to useRateLimitStore * - USAGE_SNAPSHOT → dispatched to useUsageStore * * Also syncs the live WebSocket connection state into useUsageStore so that * other components (e.g. UsageOverviewPanel) can read it without creating a * second /ws/system connection. * * Accepts an optional onReconnect callback that fires when the connection * recovers from a reconnecting state. */ export function useRateLimitWebSocket(options: UseRateLimitWebSocketOptions = {}) { const { onReconnect } = options; const prevStateRef = useRef(null); // getWebSocketUrl() already supplies the "/ws" base, so the endpoint is just // the path (matching the agents/channels/notifications hooks). Passing // "/ws/system" here produced the doubled "/ws/ws/system" URL. const { state, lastMessage } = useWebSocket( "/system", undefined, true ); // Sync WS connection state into useUsageStore for cross-component visibility. // This is the ONLY place wsState is written; no second useWebSocket call is needed. useEffect(() => { useUsageStore.getState().setWsState(state); }, [state]); // Fire onReconnect when state transitions from reconnecting → connected useEffect(() => { if (prevStateRef.current === "reconnecting" && state === "connected") { onReconnect?.(); } prevStateRef.current = state; }, [state, onReconnect]); // Handle incoming WS messages useEffect(() => { if (!lastMessage) return; const { hitRateLimit, liftRateLimit } = useRateLimitStore.getState(); const { setUsageData } = useUsageStore.getState(); if (lastMessage.type === "RATE_LIMIT_HIT") { const event: RateLimitHitEvent = { type: "RATE_LIMIT_HIT", provider: lastMessage.provider ?? "unknown", affectedAgents: lastMessage.affectedAgents ?? [], retryAfterSeconds: lastMessage.retryAfterSeconds ?? 60, timestamp: lastMessage.timestamp ?? new Date().toISOString(), }; hitRateLimit(event); } else if (lastMessage.type === "RATE_LIMIT_LIFTED") { const event: RateLimitLiftedEvent = { type: "RATE_LIMIT_LIFTED", provider: lastMessage.provider ?? "unknown", timestamp: lastMessage.timestamp ?? new Date().toISOString(), }; liftRateLimit(event); } else if (lastMessage.type === "USAGE_SNAPSHOT") { setUsageData({ tokens_input: lastMessage.totals?.input_tokens ?? 0, tokens_output: lastMessage.totals?.output_tokens ?? 0, total_cost_usd: lastMessage.cost_estimate ?? 0, period: lastMessage.period ?? "live", timestamp: lastMessage.timestamp, }); } }, [lastMessage]); return { wsState: state }; }