placebo.mk/frontend/src/hooks/useLiveBlogStream.ts
2026-02-03 21:18:25 +01:00

179 lines
5.8 KiB
TypeScript

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<LiveBlogStreamEvent | null>(null);
const [connectionError, setConnectionError] = useState<string | null>(null);
const [reconnectAttempts, setReconnectAttempts] = useState(0);
const eventSourceRef = useRef<EventSource | null>(null);
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const lastEventIdRef = useRef<string | null>(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);
},
};
}