diff --git a/src/lib/events/use-ticket-events.ts b/src/lib/events/use-ticket-events.ts index dbbba47..b389b88 100644 --- a/src/lib/events/use-ticket-events.ts +++ b/src/lib/events/use-ticket-events.ts @@ -3,6 +3,16 @@ import { useEffect, useLayoutEffect, useRef } from "react"; import type { TicketEvent } from "@/lib/events/bus"; +// A connection can go silently dead — e.g. a NAT/router along the path +// drops a long-lived connection without ever sending a proper close, so +// no error ever fires. EventSource only auto-reconnects on an actual +// error, so a silent death leaves the subscription hanging indefinitely +// with no visible symptom until an event is missed (reported: sound/toast +// stopping after ~30 minutes of an idle tab). Forcing a fresh connection +// periodically bounds how long that blind spot can last, regardless of +// the exact cause. +const FORCE_RECONNECT_MS = 4 * 60 * 1000; + /** Subscribes to a ticket-events SSE endpoint for the lifetime of the component. Pass "" to defer subscribing (e.g. until a token is available). */ export function useTicketEvents(url: string, onEvent: (event: TicketEvent) => void) { const handlerRef = useRef(onEvent); @@ -12,14 +22,31 @@ export function useTicketEvents(url: string, onEvent: (event: TicketEvent) => vo useEffect(() => { if (!url) return; - const source = new EventSource(url); - source.onmessage = (e) => { - try { - handlerRef.current(JSON.parse(e.data) as TicketEvent); - } catch { - // malformed payload — ignore rather than crash the subscription - } + + let source: EventSource; + let reconnectTimer: ReturnType; + let cancelled = false; + + function connect() { + source = new EventSource(url); + source.onmessage = (e) => { + try { + handlerRef.current(JSON.parse(e.data) as TicketEvent); + } catch { + // malformed payload — ignore rather than crash the subscription + } + }; + reconnectTimer = setTimeout(() => { + source.close(); + if (!cancelled) connect(); + }, FORCE_RECONNECT_MS); + } + + connect(); + return () => { + cancelled = true; + clearTimeout(reconnectTimer); + source.close(); }; - return () => source.close(); }, [url]); }