Files
CapaKraken/apps/web/src/hooks/useTimelineSSE.ts
T

74 lines
1.9 KiB
TypeScript

"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<ReturnType<typeof setTimeout> | 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]);
}