import { useEffect, useState, useRef } from 'react'; import { useShadeContext } from './ShadeProvider.js'; export interface ShadeEventEnvelope { source: 'client' | 'server'; seq: number; timestamp: number; name: string; data: Record; } export interface UseShadeEventsResult { events: ShadeEventEnvelope[]; connected: boolean; error: Error | null; } /** * Hook that subscribes to /api/events SSE stream and accumulates events. * * @param maxBuffer Max number of events to keep in memory (default: 200). * Older events are dropped. */ export function useShadeEvents(maxBuffer = 200): UseShadeEventsResult { const ctx = useShadeContext(); const [events, setEvents] = useState([]); const [connected, setConnected] = useState(false); const [error, setError] = useState(null); const eventSourceRef = useRef(null); useEffect(() => { const url = `${ctx.observerUrl}/api/events?token=${encodeURIComponent(ctx.token)}`; const es = new EventSource(url); eventSourceRef.current = es; es.addEventListener('open', () => setConnected(true)); es.addEventListener('error', () => { setConnected(false); setError(new Error('SSE connection error')); }); es.addEventListener('shade', (msg) => { try { const event = JSON.parse((msg as MessageEvent).data) as ShadeEventEnvelope; setEvents((prev) => { const next = [...prev, event]; return next.length > maxBuffer ? next.slice(-maxBuffer) : next; }); } catch (err) { console.error('[Shade] Failed to parse event:', err); } }); return () => { es.close(); eventSourceRef.current = null; setConnected(false); }; }, [ctx.observerUrl, ctx.token, maxBuffer]); return { events, connected, error }; }