98 lines
4.1 KiB
TypeScript
98 lines
4.1 KiB
TypeScript
"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<ReturnType<typeof setTimeout> | 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]);
|
|
}
|