export function connectWs(onMessage, opts = {}) { let ws, retry = 0, closed = false; function open() { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const params = new URLSearchParams(); if (opts.room) params.set('room', opts.room); if (opts.kind) params.set('kind', opts.kind); const qs = params.toString(); ws = new WebSocket(`${proto}://${location.host}/ws${qs ? '?' + qs : ''}`); ws.addEventListener('open', () => { retry = 0; opts.onOpen?.(); }); ws.addEventListener('message', (ev) => { try { onMessage(JSON.parse(ev.data)); } catch { } }); ws.addEventListener('close', () => { opts.onClose?.(); if (closed) return; retry = Math.min(retry + 1, 6); setTimeout(open, 500 * 2 ** retry); }); ws.addEventListener('error', () => ws.close()); } open(); return { // Returns true when the message was actually queued on a live socket, // false when the socket is closing/reconnecting. Callers that need to // surface a transport failure (transport buttons) inspect the result; // fire-and-forget callers can ignore it. send(msg) { if (ws?.readyState !== WebSocket.OPEN) return false; ws.send(JSON.stringify(msg)); return true; }, close() { closed = true; ws?.close(); }, get readyState() { return ws?.readyState; } }; }