File size: 2,673 Bytes
0cc8837
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 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 { /* */ } },
  };
}