fix(web): harden timeline sse reconnect lifecycle

This commit is contained in:
2026-03-31 23:06:07 +02:00
parent 73ef3b2bba
commit c3b3dffb6e
2 changed files with 25 additions and 6 deletions
+21 -3
View File
@@ -15,10 +15,27 @@ export function useTimelineSSE() {
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) => {
try {
const data = JSON.parse(event.data as string) as { type: string };
@@ -35,6 +52,7 @@ export function useTimelineSSE() {
void queryClient.invalidateQueries({ queryKey: [["timeline", "getMyEntriesView"]] });
void queryClient.invalidateQueries({ queryKey: [["timeline", "getHolidayOverlays"]] });
void queryClient.invalidateQueries({ queryKey: [["timeline", "getMyHolidayOverlays"]] });
void queryClient.invalidateQueries({ queryKey: [["vacation", "list"]] });
void queryClient.invalidateQueries({ queryKey: [["allocation", "list"]] });
break;
@@ -82,18 +100,18 @@ export function useTimelineSSE() {
es.onerror = () => {
es?.close();
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
reconnectTimeout.current = setTimeout(connect, delay);
scheduleReconnect();
};
}
connect();
return () => {
isDisposed = true;
es?.close();
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current);
reconnectTimeout.current = null;
}
};
}, [queryClient]);