74 lines
1.9 KiB
TypeScript
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]);
|
|
}
|