179 lines
5.8 KiB
TypeScript
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);
|
|
},
|
|
};
|
|
} |