import { useEffect } from 'react'; import { sessionEvents } from './sessionEvents'; import { createWsReconnectToast } from './wsReconnectToast'; const RECONNECT_INITIAL_MS = 1000; const RECONNECT_MAX_MS = 30000; export function useUserEvents(): void { useEffect(() => { let ws: WebSocket | null = null; let reconnectTimer: ReturnType | null = null; let reconnectDelay = RECONNECT_INITIAL_MS; let unmounted = false; // v1.8.1: silent on the first disconnect; gray "reconnecting…" after 3 // fails / 15 s; red "connection lost" with a Retry Now action after 60 s. const reconnectToast = createWsReconnectToast({ label: 'Live updates', onRetryNow: () => { if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; reconnectDelay = RECONNECT_INITIAL_MS; connect(); } }, }); const connect = () => { if (unmounted) return; const url = new URL('/api/ws/user', window.location.href); url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(url.toString()); ws.onopen = () => { reconnectDelay = RECONNECT_INITIAL_MS; reconnectToast.onConnected(); }; ws.onmessage = (ev) => { try { const parsed: unknown = JSON.parse(ev.data); if (parsed && typeof (parsed as { type?: unknown }).type === 'string') { sessionEvents.emit(parsed as import('./sessionEvents').SessionEvent); } } catch (err) { console.warn('useUserEvents: failed to parse frame', err); } }; ws.onclose = () => { if (unmounted) return; reconnectToast.onFailure(); const delay = reconnectDelay; reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS); reconnectTimer = setTimeout(connect, delay); }; ws.onerror = () => { // close handler will trigger reconnect; best-effort, ignore failure // because the socket may already be closing try { ws?.close(); } catch {} }; }; connect(); return () => { unmounted = true; reconnectToast.dispose(); if (reconnectTimer) clearTimeout(reconnectTimer); if (ws) try { ws.close(); } catch {} }; }, []); }