"use client"; import { SSE_EVENT_TYPES } from "@capakraken/shared"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef } from "react"; import { getTimelineSseInvalidationKeys, parseTimelineSseEvent } from "./timelineSsePolicy.js"; /** * Connects to the SSE timeline endpoint and invalidates React Query caches * when allocation/project change events arrive. */ export function useTimelineSSE() { const queryClient = useQueryClient(); const reconnectTimeout = useRef | null>(null); useEffect(() => { let es: EventSource | null = null; let reconnectAttempts = 0; let isDisposed = false; function scheduleReconnect() { if (isDisposed || reconnectTimeout.current) return; reconnectAttempts++; const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); reconnectTimeout.current = setTimeout(() => { reconnectTimeout.current = null; if (isDisposed) return; connect(); }, delay); } function connect() { if (isDisposed) return; es = new EventSource("/api/sse/timeline"); es.onopen = () => { reconnectAttempts = 0; }; es.onmessage = (event) => { const eventType = parseTimelineSseEvent(String(event.data)); if (!eventType) { return; } if (eventType === SSE_EVENT_TYPES.PING) { reconnectAttempts = 0; return; } for (const queryKey of getTimelineSseInvalidationKeys(eventType)) { void queryClient.invalidateQueries({ queryKey }); } }; es.onerror = () => { es?.close(); scheduleReconnect(); }; } connect(); return () => { isDisposed = true; es?.close(); if (reconnectTimeout.current) { clearTimeout(reconnectTimeout.current); reconnectTimeout.current = null; } }; }, [queryClient]); }