hushfox / net.js
CyberMax-tools's picture
Deploy hushfox
0cc8837 verified
Raw History Blame Contribute Delete
2.67 kB
// 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 { /* */ } },
};
}