// Subscribes to /ws/events/:id and emits per-message callbacks. // // Authenticates via short-lived ticket: before each connect we POST // /auth/ws-ticket (bearer-authed) to mint a one-shot ticket, then pass it // on the WS handshake as `?ticket=…`. Tickets expire ~60s after mint, so we // always mint fresh — even on reconnects. // // Auto-reconnects with exponential backoff up to 30s. Returns a cleanup fn // the caller invokes (e.g. inside onUnmounted). interface WSMessage { type: string event_id: string payload: any timestamp: string } interface WSTicket { ticket: string expires_at: string } export function useEventWS(eventId: string, onMessage: (msg: WSMessage) => void) { if (import.meta.server) return () => {} const config = useRuntimeConfig() const wsBase = (config.public.wsBase as string) || '' let ws: WebSocket | null = null let attempt = 0 let stopped = false let reconnectTimer: ReturnType | null = null async function mintTicket(): Promise { try { const t = await useApi('/auth/ws-ticket', { method: 'POST', body: { event_id: eventId }, }) return t.ticket } catch { return null } } async function connect() { if (stopped) return const ticket = await mintTicket() if (stopped) return if (!ticket) { // Couldn't get a ticket (likely 401 — session expired). Back off and // retry; useApi will have already attempted refresh on its own. const backoff = Math.min(30_000, 500 * Math.pow(2, attempt++)) reconnectTimer = setTimeout(connect, backoff) return } ws = new WebSocket(`${wsBase}/ws/events/${eventId}?ticket=${encodeURIComponent(ticket)}`) ws.onopen = () => { attempt = 0 } ws.onmessage = (e) => { try { const msg = JSON.parse(e.data) as WSMessage onMessage(msg) } catch { /* ignore */ } } ws.onclose = () => { if (stopped) return const backoff = Math.min(30_000, 500 * Math.pow(2, attempt++)) reconnectTimer = setTimeout(connect, backoff) } ws.onerror = () => { ws?.close() } } connect() return function stop() { stopped = true if (reconnectTimer) clearTimeout(reconnectTimer) ws?.close() } }