132 lines
3.2 KiB
JavaScript
132 lines
3.2 KiB
JavaScript
/**
|
||
* 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();
|