live blog implemented
This commit is contained in:
1 parent
e8893c1aae
commit
0351726eeb
36 files changed
+7759
-253
No files matched your search
@@ -0,0 +1,172 @@
|
||||
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 [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<NodeJS.Timeout | null>(null);
|
||||
const lastEventIdRef = useRef<string | null>(null);
|
||||
const optionsRef = useRef(options);
|
||||
const reconnectAttemptsRef = useRef(reconnectAttempts);
|
||||
|
||||
// Update refs when props change
|
||||
useEffect(() => {
|
||||
optionsRef.current = options;
|
||||
}, [options]);
|
||||
|
||||
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}/api/v1/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 || 5)) {
|
||||
const nextAttempt = reconnectAttemptsRef.current + 1;
|
||||
setReconnectAttempts(nextAttempt);
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
console.log(`Attempting reconnection (${nextAttempt}/${optionsRef.current.maxReconnectAttempts || 5})`);
|
||||
createConnection();
|
||||
}, optionsRef.current.reconnectInterval || 3000);
|
||||
} else if (reconnectAttemptsRef.current >= (optionsRef.current.maxReconnectAttempts || 5)) {
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user