import { useEffect, useRef, useState } from 'react'; export interface LiveBlogStreamEvent { type: 'connected' | 'update' | 'status-change' | 'pin-update' | 'error'; data: unknown; clientId?: string; } export interface LiveBlogStreamOptions { autoReconnect?: boolean; reconnectInterval?: number; maxReconnectAttempts?: number; } export function useLiveBlogStream( liveBlogId: string, options: LiveBlogStreamOptions = {} ) { const defaultOptions: LiveBlogStreamOptions = { autoReconnect: true, reconnectInterval: 3000, maxReconnectAttempts: 10, }; const mergedOptions = { ...defaultOptions, ...options }; const [isConnected, setIsConnected] = useState(false); const [lastEvent, setLastEvent] = useState(null); const [connectionError, setConnectionError] = useState(null); const [reconnectAttempts, setReconnectAttempts] = useState(0); const eventSourceRef = useRef(null); const reconnectTimeoutRef = useRef | null>(null); const lastEventIdRef = useRef(null); const optionsRef = useRef(mergedOptions); const reconnectAttemptsRef = useRef(reconnectAttempts); // Update refs when props change useEffect(() => { optionsRef.current = mergedOptions; }, [mergedOptions]); useEffect(() => { reconnectAttemptsRef.current = reconnectAttempts; }, [reconnectAttempts]); useEffect(() => { if (!liveBlogId) return; const disconnect = () => { if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } setIsConnected(false); setReconnectAttempts(0); console.log('Disconnected from live blog stream'); }; const createConnection = () => { // Close existing connection if any if (eventSourceRef.current) { eventSourceRef.current.close(); } const url = new URL(`${import.meta.env.VITE_API_URL}/live-blogs/${liveBlogId}/stream`, window.location.origin); if (lastEventIdRef.current) { url.searchParams.set('last-event-id', lastEventIdRef.current); } try { eventSourceRef.current = new EventSource(url.toString()); eventSourceRef.current.onopen = () => { setIsConnected(true); setConnectionError(null); setReconnectAttempts(0); console.log(`Connected to live blog stream for ${liveBlogId}`); }; eventSourceRef.current.onmessage = (event) => { try { const parsedEvent: LiveBlogStreamEvent = JSON.parse(event.data); setLastEvent(parsedEvent); // Store last event ID for reconnection if (event.lastEventId) { lastEventIdRef.current = event.lastEventId; } // Handle different event types switch (parsedEvent.type) { case 'connected': console.log('Stream connected:', parsedEvent.clientId); break; case 'update': console.log('New update received:', parsedEvent.data); break; case 'status-change': console.log('Status change received:', parsedEvent.data); break; case 'pin-update': console.log('Pin update received:', parsedEvent.data); break; default: console.log('Unknown event type:', parsedEvent.type); } } catch (error) { console.error('Error parsing SSE event:', error); setLastEvent({ type: 'error', data: 'Failed to parse server event', }); } }; eventSourceRef.current.onerror = () => { console.error('SSE connection error'); setIsConnected(false); setConnectionError('Connection to live blog lost'); // Attempt reconnection if enabled and within limits if (optionsRef.current.autoReconnect && reconnectAttemptsRef.current < (optionsRef.current.maxReconnectAttempts || 10)) { const nextAttempt = reconnectAttemptsRef.current + 1; setReconnectAttempts(nextAttempt); reconnectTimeoutRef.current = setTimeout(() => { console.log(`Attempting reconnection (${nextAttempt}/${optionsRef.current.maxReconnectAttempts || 10})`); createConnection(); }, optionsRef.current.reconnectInterval || 3000); } else if (reconnectAttemptsRef.current >= (optionsRef.current.maxReconnectAttempts || 10)) { setConnectionError('Failed to reconnect after multiple attempts'); } }; } catch (error) { console.error('Failed to create EventSource connection:', error); setConnectionError('Failed to connect to live blog stream'); setIsConnected(false); } }; createConnection(); return () => { disconnect(); }; }, [liveBlogId]); const manualReconnect = () => { setReconnectAttempts(0); setConnectionError(null); // Trigger reconnection by updating liveBlogId dependency // This will cause the useEffect to run again if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } // The useEffect will automatically reconnect }; return { isConnected, lastEvent, connectionError, reconnectAttempts, connect: manualReconnect, disconnect: () => { if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } setIsConnected(false); }, }; }