"use client"; import { SSE_EVENT_TYPES } from "@capakraken/shared"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef } from "react"; /** * 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; function connect() { es = new EventSource("/api/sse/timeline"); es.onmessage = (event) => { try { const data = JSON.parse(event.data as string) as { type: string }; switch (data.type) { case SSE_EVENT_TYPES.ALLOCATION_CREATED: case SSE_EVENT_TYPES.ALLOCATION_UPDATED: case SSE_EVENT_TYPES.ALLOCATION_DELETED: void queryClient.invalidateQueries({ queryKey: [["timeline", "getEntries"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getEntriesView"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getMyEntriesView"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getHolidayOverlays"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getMyHolidayOverlays"]] }); void queryClient.invalidateQueries({ queryKey: [["allocation", "list"]] }); break; case SSE_EVENT_TYPES.PROJECT_SHIFTED: void queryClient.invalidateQueries({ queryKey: [["timeline", "getEntries"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getEntriesView"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getMyEntriesView"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getHolidayOverlays"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getMyHolidayOverlays"]] }); void queryClient.invalidateQueries({ queryKey: [["project", "list"]] }); break; case SSE_EVENT_TYPES.BUDGET_WARNING: void queryClient.invalidateQueries({ queryKey: [["timeline", "getBudgetStatus"]] }); break; case SSE_EVENT_TYPES.NOTIFICATION_CREATED: void queryClient.invalidateQueries({ queryKey: [["notification", "list"]] }); void queryClient.invalidateQueries({ queryKey: [["notification", "unreadCount"]] }); break; case SSE_EVENT_TYPES.TASK_ASSIGNED: case SSE_EVENT_TYPES.TASK_COMPLETED: case SSE_EVENT_TYPES.TASK_STATUS_CHANGED: void queryClient.invalidateQueries({ queryKey: [["notification", "listTasks"]] }); void queryClient.invalidateQueries({ queryKey: [["notification", "taskCounts"]] }); void queryClient.invalidateQueries({ queryKey: [["notification", "unreadCount"]] }); void queryClient.invalidateQueries({ queryKey: [["notification", "list"]] }); break; case SSE_EVENT_TYPES.REMINDER_DUE: void queryClient.invalidateQueries({ queryKey: [["notification", "list"]] }); void queryClient.invalidateQueries({ queryKey: [["notification", "unreadCount"]] }); void queryClient.invalidateQueries({ queryKey: [["notification", "listReminders"]] }); break; case SSE_EVENT_TYPES.PING: reconnectAttempts = 0; // Reset on successful ping break; } } catch { // Ignore parse errors } }; es.onerror = () => { es?.close(); reconnectAttempts++; const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); reconnectTimeout.current = setTimeout(connect, delay); }; } connect(); return () => { es?.close(); if (reconnectTimeout.current) { clearTimeout(reconnectTimeout.current); } }; }, [queryClient]); }