2025-11-23 19:54:14 +01:00
|
|
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
2025-11-23 20:57:24 +01:00
|
|
|
import { createHookLogger } from '@/lib/utils/client-logger';
|
|
|
|
|
|
|
|
|
|
const logger = createHookLogger('useEventSource');
|
2025-11-23 19:54:14 +01:00
|
|
|
|
|
|
|
|
export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
|
|
|
|
|
|
|
|
export interface EventSourceMessage<T = any> {
|
|
|
|
|
event: string;
|
|
|
|
|
data: T;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UseEventSourceOptions {
|
|
|
|
|
enabled?: boolean;
|
|
|
|
|
reconnectInterval?: number;
|
|
|
|
|
maxReconnectAttempts?: number;
|
|
|
|
|
onMessage?: (message: EventSourceMessage) => void;
|
|
|
|
|
onError?: (error: Event) => void;
|
|
|
|
|
onConnect?: () => void;
|
|
|
|
|
onDisconnect?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useEventSource(url: string, options: UseEventSourceOptions = {}) {
|
|
|
|
|
const {
|
|
|
|
|
enabled = true,
|
|
|
|
|
reconnectInterval = 3000,
|
|
|
|
|
maxReconnectAttempts = 10,
|
|
|
|
|
onMessage,
|
|
|
|
|
onError,
|
|
|
|
|
onConnect,
|
|
|
|
|
onDisconnect,
|
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
|
|
|
|
|
const [reconnectAttempts, setReconnectAttempts] = useState(0);
|
|
|
|
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
|
|
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
|
|
|
|
|
|
const connect = useCallback(() => {
|
|
|
|
|
if (!enabled || eventSourceRef.current) return;
|
|
|
|
|
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.info('Connecting to SSE', { url });
|
2025-11-23 19:54:14 +01:00
|
|
|
setStatus('connecting');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const eventSource = new EventSource(url);
|
|
|
|
|
eventSourceRef.current = eventSource;
|
|
|
|
|
|
|
|
|
|
eventSource.addEventListener('connected', () => {
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.info('SSE connected successfully', { url });
|
2025-11-23 19:54:14 +01:00
|
|
|
setStatus('connected');
|
|
|
|
|
setReconnectAttempts(0);
|
|
|
|
|
onConnect?.();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
eventSource.addEventListener('heartbeat', (event) => {
|
|
|
|
|
// Keep connection alive
|
|
|
|
|
if (status !== 'connected') {
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.debug('SSE heartbeat received, updating status to connected');
|
2025-11-23 19:54:14 +01:00
|
|
|
setStatus('connected');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
eventSource.addEventListener('process-update', (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.debug('Process update received', {
|
|
|
|
|
processCount: data.processes?.length,
|
|
|
|
|
timestamp: data.timestamp,
|
|
|
|
|
});
|
2025-11-23 19:54:14 +01:00
|
|
|
onMessage?.({ event: 'process-update', data });
|
|
|
|
|
} catch (error) {
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.error('Failed to parse SSE message', error, { event: event.data });
|
2025-11-23 19:54:14 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
eventSource.addEventListener('error', (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse((event as MessageEvent).data);
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.warn('SSE error event received', { error: data });
|
2025-11-23 19:54:14 +01:00
|
|
|
onMessage?.({ event: 'error', data });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Not a message error, connection error
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
eventSource.onerror = (event) => {
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.error('EventSource connection error', event);
|
2025-11-23 19:54:14 +01:00
|
|
|
setStatus('error');
|
|
|
|
|
onError?.(event);
|
|
|
|
|
|
|
|
|
|
// Close current connection
|
|
|
|
|
eventSource.close();
|
|
|
|
|
eventSourceRef.current = null;
|
|
|
|
|
|
|
|
|
|
// Attempt reconnection with exponential backoff
|
|
|
|
|
if (reconnectAttempts < maxReconnectAttempts) {
|
|
|
|
|
const delay = Math.min(reconnectInterval * Math.pow(2, reconnectAttempts), 30000);
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.info('Scheduling reconnection', {
|
|
|
|
|
delay,
|
|
|
|
|
attempt: reconnectAttempts + 1,
|
|
|
|
|
maxAttempts: maxReconnectAttempts,
|
|
|
|
|
});
|
2025-11-23 19:54:14 +01:00
|
|
|
|
|
|
|
|
reconnectTimeoutRef.current = setTimeout(() => {
|
|
|
|
|
setReconnectAttempts((prev) => prev + 1);
|
|
|
|
|
connect();
|
|
|
|
|
}, delay);
|
|
|
|
|
} else {
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.warn('Max reconnection attempts reached, disconnecting', {
|
|
|
|
|
maxAttempts: maxReconnectAttempts,
|
|
|
|
|
});
|
2025-11-23 19:54:14 +01:00
|
|
|
setStatus('disconnected');
|
|
|
|
|
onDisconnect?.();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.error('Failed to create EventSource', error, { url });
|
2025-11-23 19:54:14 +01:00
|
|
|
setStatus('error');
|
|
|
|
|
}
|
|
|
|
|
}, [url, enabled, status, reconnectAttempts, maxReconnectAttempts, reconnectInterval, onMessage, onError, onConnect, onDisconnect]);
|
|
|
|
|
|
|
|
|
|
const disconnect = useCallback(() => {
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.info('Disconnecting from SSE');
|
|
|
|
|
|
2025-11-23 19:54:14 +01:00
|
|
|
if (reconnectTimeoutRef.current) {
|
|
|
|
|
clearTimeout(reconnectTimeoutRef.current);
|
|
|
|
|
reconnectTimeoutRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (eventSourceRef.current) {
|
|
|
|
|
eventSourceRef.current.close();
|
|
|
|
|
eventSourceRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setStatus('disconnected');
|
|
|
|
|
setReconnectAttempts(0);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const reconnect = useCallback(() => {
|
2025-11-23 20:57:24 +01:00
|
|
|
logger.info('Manual reconnection requested');
|
2025-11-23 19:54:14 +01:00
|
|
|
disconnect();
|
|
|
|
|
setReconnectAttempts(0);
|
|
|
|
|
connect();
|
|
|
|
|
}, [disconnect, connect]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (enabled) {
|
|
|
|
|
connect();
|
|
|
|
|
} else {
|
|
|
|
|
disconnect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
disconnect();
|
|
|
|
|
};
|
|
|
|
|
}, [enabled, url]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
status,
|
|
|
|
|
reconnectAttempts,
|
|
|
|
|
reconnect,
|
|
|
|
|
disconnect,
|
|
|
|
|
};
|
|
|
|
|
}
|