/** * common.js – Stato condiviso tra i widget overlay via WebSocket * * Ogni widget si connette al server Express+WS e ascolta gli aggiornamenti. * Il pannello di controllo invia comandi/aggiornamenti, il server li * propaga a tutti i widget connessi. */ // ---- Config ---- const WS_URL = (() => { const loc = window.location; const protocol = loc.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${loc.host}/ws`; })(); let ws = null; let currentState = {}; let subscribers = []; let reconnectTimer = null; let connectAttempts = 0; // ---- WebSocket connection ---- function connect() { if (ws && (ws.readyState === 0 || ws.readyState === 1)) return; ws = new WebSocket(WS_URL); ws.onopen = () => { console.log('🟢 WS connected'); connectAttempts = 0; }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === 'STATE_UPDATE' && msg.state) { currentState = msg.state; notifySubscribers(); } } catch (err) { console.error('❌ WS message error:', err); } }; ws.onclose = () => { console.log('🔴 WS disconnected, reconnecting...'); scheduleReconnect(); }; ws.onerror = (err) => { console.error('❌ WS error:', err); ws.close(); }; } function scheduleReconnect() { if (reconnectTimer) clearTimeout(reconnectTimer); const delay = Math.min(1000 * 2 ** connectAttempts, 10000); connectAttempts++; reconnectTimer = setTimeout(connect, delay); } // ---- Subscribe to state updates ---- /** * Si iscrive agli aggiornamenti di stato. * @param {(state: object) => void} callback * @returns {() => void} unsubscribe function */ function onStateUpdate(callback) { subscribers.push(callback); // Chiamata immediata con stato corrente (se già presente) if (Object.keys(currentState).length > 0) { callback({ ...currentState }); } return () => { subscribers = subscribers.filter((cb) => cb !== callback); }; } function notifySubscribers() { const state = { ...currentState }; for (const cb of subscribers) { try { cb(state); } catch (e) { console.error(e); } } } /** * Invia un comando al server (ADD_POINT, RESET_GAME, RESET_MATCH, SET_DEUCE, SET_SERVER) */ function sendCommand(command, player = null) { const msg = { type: 'COMMAND', command }; if (player !== null) msg.player = player; sendMessage(msg); } /** * Invia un aggiornamento parziale di stato al server */ function sendStateUpdate(partial) { sendMessage({ type: 'STATE_UPDATE', state: partial }); } function sendMessage(msg) { if (ws && ws.readyState === 1) { ws.send(JSON.stringify(msg)); } else { console.warn('⚠️ WS not connected, cannot send'); } } /** * Utility: mappa punti → label tennis */ function pointLabel(state) { // Tiebreak: mostra punti numerici if (state.tiebreak) { return [String(state.tiebreakPoints[0]), String(state.tiebreakPoints[1])]; } const p1 = state.points[0]; const p2 = state.points[1]; if (state.deuce) { if (state.advantage === 1) return ['AD', '40']; if (state.advantage === 2) return ['40', 'AD']; return ['40', '40']; } const map = ['0', '15', '30', '40']; return [map[p1] || '40', map[p2] || '40']; } // ---- Auto-connect ---- connect();