Add authentication, Redis pub/sub, and error handling to SSE

Improvements:
- Add Redis pub/sub support for multi-server deployments
- Add authentication check to SSE stream endpoint
- Add 30s heartbeat keepalive for long-lived connections
- Implement exponential backoff reconnection logic in client
- Fix TypeScript types (websiteId optional, timer types)
- Use specific query key invalidation instead of broad match
- Fix undefined access in session-events listener map
This commit is contained in:
Arthur Sepiol 2025-12-10 16:06:18 +03:00
parent ef9a382cdd
commit 5874cf80f5
4 changed files with 114 additions and 11 deletions

View file

@ -1,18 +1,61 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
export function useSessionStream(websiteId: string) {
const MAX_RETRY_DELAY = 30000;
const INITIAL_RETRY_DELAY = 1000;
export function useSessionStream(websiteId?: string) {
const queryClient = useQueryClient();
const retryCountRef = useRef(0);
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
if (!websiteId) return;
const eventSource = new EventSource(`/api/websites/${websiteId}/sessions/stream`);
let eventSource: EventSource | null = null;
let isMounted = true;
eventSource.onmessage = () => {
queryClient.invalidateQueries({ queryKey: ['sessions'] });
const connect = () => {
if (!isMounted) return;
eventSource = new EventSource(`/api/websites/${websiteId}/sessions/stream`);
eventSource.onmessage = event => {
try {
const data = JSON.parse(event.data);
if (data.sessionId) {
retryCountRef.current = 0;
queryClient.invalidateQueries({ queryKey: ['sessions', { websiteId }] });
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to parse session event:', error);
}
};
eventSource.onerror = () => {
eventSource?.close();
if (!isMounted) return;
const delay = Math.min(
INITIAL_RETRY_DELAY * Math.pow(2, retryCountRef.current),
MAX_RETRY_DELAY,
);
retryCountRef.current += 1;
retryTimeoutRef.current = setTimeout(connect, delay);
};
};
return () => eventSource.close();
connect();
return () => {
isMounted = false;
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
}
eventSource?.close();
};
}, [websiteId, queryClient]);
}