'use client' // ST-1115: Client hook for the backlog 3-pane SSE stream. // Mounts in BacklogHydrationWrapper so it survives Server Action refreshes. // Dispatches pbi/story/task change events into useBacklogStore.applyChange. import { useEffect, useRef } from 'react' import { useBacklogStore } from '@/stores/backlog-store' const BACKOFF_START_MS = 1_000 const BACKOFF_MAX_MS = 30_000 type EntityPayload = { op: 'I' | 'U' | 'D' entity: 'pbi' | 'story' | 'task' [key: string]: unknown } export function useBacklogRealtime(productId: string | null) { const sourceRef = useRef(null) const backoffRef = useRef(BACKOFF_START_MS) const reconnectTimerRef = useRef | null>(null) useEffect(() => { if (!productId) return const close = () => { if (sourceRef.current) { sourceRef.current.close() sourceRef.current = null } if (reconnectTimerRef.current) { clearTimeout(reconnectTimerRef.current) reconnectTimerRef.current = null } } const connect = () => { close() const source = new EventSource( `/api/realtime/backlog?product_id=${encodeURIComponent(productId)}`, ) sourceRef.current = source source.addEventListener('ready', () => { backoffRef.current = BACKOFF_START_MS }) source.onmessage = (e) => { if (!e.data) return try { const payload = JSON.parse(e.data) as EntityPayload useBacklogStore .getState() .applyChange(payload.entity, payload.op, payload as Record) } catch (err) { if (process.env.NODE_ENV !== 'production') { console.error('[realtime/backlog] failed to parse event', err, e.data) } } } source.onerror = () => { if (sourceRef.current !== source) return close() if (document.visibilityState === 'hidden') return const delay = backoffRef.current backoffRef.current = Math.min(backoffRef.current * 2, BACKOFF_MAX_MS) reconnectTimerRef.current = setTimeout(connect, delay) } } const onVisibility = () => { if (document.visibilityState === 'hidden') { close() } else if (sourceRef.current === null) { backoffRef.current = BACKOFF_START_MS connect() } } if (document.visibilityState === 'visible') { connect() } document.addEventListener('visibilitychange', onVisibility) return () => { document.removeEventListener('visibilitychange', onVisibility) close() } }, [productId]) }