// Hushfox room client: one WebSocket to the room's Durable Object, auto-reconnect with the same player id (the seat // and score survive a locked phone for 15 s), server clock offset for the shared timer. (c) 2026 CyberMax. export const API_DEFAULT = 'https://hushfox.cybermaxtools.com'; /** The rooms API base. Local test pages may pass ?api=http://127.0.0.1:PORT (only honoured on localhost). */ export function apiBase() { try { const q = new URLSearchParams(location.search).get('api'); if (q && /^(localhost|127\.0\.0\.1)$/.test(location.hostname) && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(q)) return q; } catch { /* */ } return window.HUSHFOX_API || API_DEFAULT; } export async function createRoom(api = apiBase()) { const r = await fetch(`${api}/api/rooms`, { method: 'POST' }); if (!r.ok) throw new Error('Could not create a room'); return (await r.json()).code; } export async function roomInfo(code, api = apiBase()) { const r = await fetch(`${api}/api/rooms/${encodeURIComponent(code)}`); return r.json(); } /** Connect to a room. handlers: { state(s), error(msg), status('open'|'retry'|'gone') }. Returns { send, close }. */ export function connect(code, pid, name, handlers, api = apiBase()) { let ws = null, closed = false, tries = 0, timer = 0, ping = 0; const open = () => { ws = new WebSocket(`${api.replace(/^http/, 'ws')}/ws/${code}`); ws.addEventListener('open', () => { tries = 0; ws.send(JSON.stringify({ t: 'hello', pid, name })); handlers.status && handlers.status('open'); clearInterval(ping); ping = setInterval(() => { try { ws.send('{"t":"ping"}'); } catch { /* */ } }, 25000); }); ws.addEventListener('message', (e) => { let m; try { m = JSON.parse(e.data); } catch { return; } if (m.t === 'state') { m.s.offset = m.s.now - Date.now(); handlers.state(m.s); } else if (m.t === 'error') handlers.error && handlers.error(m.error); else if (m.t === 'gone') { closed = true; handlers.status && handlers.status('gone'); } }); ws.addEventListener('close', (e) => { clearInterval(ping); if (closed || e.code === 4000 || e.code === 4001) { if (e.code !== 4000) handlers.status && handlers.status('gone'); return; } handlers.status && handlers.status('retry'); tries++; timer = setTimeout(open, Math.min(8000, 400 * 2 ** Math.min(tries, 5))); }); }; open(); return { send(o) { if (ws && ws.readyState === 1) { ws.send(JSON.stringify(o)); return true; } return false; }, close() { closed = true; clearTimeout(timer); clearInterval(ping); try { ws.close(); } catch { /* */ } }, }; }