import { useEffect, useRef, useState } from 'react' import { api } from '../api/client.js' // Subscribe to the public shard live-event SSE stream and keep a rolling buffer // of the most recent events. The browser talks to our own /public/shard/stream // route (plain HTTP EventSource) — never the sidecar's WebSocket — so the token // stays server-side and it works through any reverse proxy. // // EventSource auto-reconnects on drop, so there is no manual retry loop here; a // `connected` flag is exposed for a small live/offline indicator. `filter` (a // Set of kinds, optional) limits which events are buffered. `max` caps the // buffer length. export function useShardFeed({ url, filter, max = 40 } = {}) { const [events, setEvents] = useState([]) const [connected, setConnected] = useState(false) // Keep the latest filter in a ref so re-renders don't tear down the stream. const filterRef = useRef(filter) filterRef.current = filter const streamUrl = url || api.shardStreamUrl useEffect(() => { // EventSource isn't available during SSR / very old browsers — degrade to // "no live feed" rather than throwing. if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') return undefined const es = new EventSource(streamUrl, { withCredentials: true }) es.onopen = () => setConnected(true) es.onerror = () => setConnected(false) // EventSource will retry on its own es.onmessage = (msg) => { let event try { event = JSON.parse(msg.data) } catch { return } if (!event || !event.kind) return const f = filterRef.current if (f && !f.has(event.kind)) return setEvents((prev) => { // Tag with a stable-ish local id for React keys (events carry t but can // collide within a ms) and cap the buffer. const next = [{ ...event, _id: `${event.kind}-${event.t}-${prev.length}` }, ...prev] return next.slice(0, max) }) } return () => es.close() // eslint-disable-next-line react-hooks/exhaustive-deps }, [max, streamUrl]) return { events, connected } }