"use client"; import { SSE_EVENT_TYPES } from "@capakraken/shared"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef } from "react"; import { getTimelineSseInvalidationKeys, getTimelineSseResyncKeys, parseTimelineSseEvent, } from "./timelineSsePolicy.js"; export function useTimelineSSE() { const queryClient = useQueryClient(); const reconnectTimeout = useRef | null>(null); useEffect(() => { let es: EventSource | null = null; let reconnectAttempts = 0; let isDisposed = false; let shouldResyncOnOpen = false; const canObserveVisibility = typeof document !== "undefined" && "visibilityState" in document; function clearReconnectTimer() { if (!reconnectTimeout.current) return; clearTimeout(reconnectTimeout.current); reconnectTimeout.current = null; } function closeConnection() { es?.close(); es = null; } 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; if (canObserveVisibility && document.visibilityState === "hidden") return; es = new EventSource("/api/sse/timeline"); es.onopen = () => { reconnectAttempts = 0; if (!shouldResyncOnOpen) { return; } shouldResyncOnOpen = false; for (const queryKey of getTimelineSseResyncKeys()) { void queryClient.invalidateQueries({ queryKey }); } }; 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 = () => { shouldResyncOnOpen = true; closeConnection(); scheduleReconnect(); }; } function handleVisibilityChange() { if (!canObserveVisibility) return; if (document.visibilityState === "hidden") { if (es || reconnectTimeout.current) { shouldResyncOnOpen = true; } clearReconnectTimer(); closeConnection(); return; } reconnectAttempts = 0; if (!es) { connect(); } } if (canObserveVisibility) { document.addEventListener("visibilitychange", handleVisibilityChange); } connect(); return () => { isDisposed = true; if (canObserveVisibility) { document.removeEventListener("visibilitychange", handleVisibilityChange); } closeConnection(); clearReconnectTimer(); }; }, [queryClient]); }