/** * Browser-side networking: thin REST wrappers, per-game session storage, and * a NetworkTransport wrapping the authenticated WebSocket. UI code (index.html) * never touches fetch()/WebSocket directly — it goes through this module. */ // Re-exported purely so the UI can decide what to show (enable the Roll // button, render choice options, ...). Authoritative state always comes from // the server's `state` broadcast — the UI never re-derives it locally. export { getLegalIntents, isPlayersTurn } from '/shared/game.js'; export { board } from '/shared/board.js'; async function postJson(url, body) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`); return data; } export function createGame(hostName) { return postJson('/api/games', { hostName }); } export function joinGame(code, name) { return postJson(`/api/games/${encodeURIComponent(code)}/join`, { name }); } export async function fetchGame(code) { const res = await fetch(`/api/games/${encodeURIComponent(code)}`); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`); return data; } // --- Per-game session persistence (localStorage), so a reload/reconnect // resumes as the same player instead of joining again. --- const STORAGE_PREFIX = 'lifegame:session:'; export function saveSession(code, session) { localStorage.setItem(STORAGE_PREFIX + code, JSON.stringify(session)); } export function loadSession(code) { const raw = localStorage.getItem(STORAGE_PREFIX + code); return raw ? JSON.parse(raw) : null; } // --- WebSocket transport --- export class NetworkTransport { constructor() { this.ws = null; this._stateHandlers = []; this._errorHandlers = []; } /** Opens the socket, authenticates with the session token, and resolves * once the first authoritative state has been received. */ connect(sessionToken) { return new Promise((resolve, reject) => { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const ws = new WebSocket(`${proto}://${location.host}/ws`); this.ws = ws; let settled = false; ws.onopen = () => ws.send(JSON.stringify({ type: 'AUTH', token: sessionToken })); ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'state') { if (!settled) { settled = true; resolve(); } this._stateHandlers.forEach((cb) => cb(msg)); } else if (msg.type === 'error') { this._errorHandlers.forEach((cb) => cb(msg)); } }; ws.onerror = () => { if (!settled) { settled = true; reject(new Error('WebSocket connection failed')); } }; ws.onclose = (event) => { if (!settled) { settled = true; reject(new Error(`Connection closed (${event.code})`)); } }; }); } sendIntent(intent) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(intent)); } } onState(cb) { this._stateHandlers.push(cb); } onError(cb) { this._errorHandlers.push(cb); } close() { this.ws?.close(); } }