const WebSocket = require('ws'); const os = require('os'); const { encode, decode } = require('@msgpack/msgpack'); const SOCKET_URL = process.env.REALTIME_SOCKET_URL; let ws = null; let onDisconnect = null; let currentSessionId = null; const QUEUE_MAX = 500; const pendingQueue = []; function flushQueue() { if (!ws || ws.readyState !== WebSocket.OPEN) return; while (pendingQueue.length > 0) { const buf = pendingQueue.shift(); try { ws.send(buf); } catch (err) { console.error('[REALTIME|WS] Errore flush:', err.message); return; } } } function enqueue(buf) { if (pendingQueue.length >= QUEUE_MAX) { pendingQueue.shift(); } pendingQueue.push(buf); } /** * Apre una connessione WebSocket al server realtime usando il token temporaneo. * @param {string} socketToken - Token temporaneo ottenuto da auth.authenticate() * @param {Function} onClose - Callback chiamata quando la connessione si chiude * @returns {Promise} true se la connessione e' riuscita */ function connect(socketToken, onClose) { return new Promise((resolve) => { if (!SOCKET_URL) { console.error('[REALTIME|WS] REALTIME_SOCKET_URL non configurato nel .env'); return resolve(false); } onDisconnect = onClose; try { const wsUrl = `${SOCKET_URL}/?token=${encodeURIComponent(socketToken)}`; ws = new WebSocket(wsUrl); } catch (err) { console.error('[REALTIME|WS] Errore creazione:', err.message); return resolve(false); } ws.on('open', () => { console.log('[REALTIME|WS] Connesso'); const initPayload = { _t: 'init', uptime: Math.floor(os.uptime()) }; ws.send(encode(initPayload)); console.log('[REALTIME|WS] Init inviato:', initPayload); flushQueue(); resolve(true); }); ws.on('message', (raw) => { let msg; try { msg = decode(raw); } catch (err) { console.warn('[REALTIME|WS] payload non msgpack:', err.message); return; } if (msg && typeof msg === 'object') { if (msg._t === 'hello') { currentSessionId = msg.sessionId || null; console.log(`[REALTIME|WS] hello dal server (session=${currentSessionId})`); return; } if (msg._t === 'session_id') { currentSessionId = msg.sessionId || null; console.log(`[REALTIME|WS] session_reset ack: ${msg.prev} -> ${msg.sessionId}`); return; } if (msg._t === 'ruleset_update') { console.log(`[REALTIME|WS] ruleset_update type=${msg.type} v=${msg.ruleset?.version?.str || '?'}`); try { require('../rulesets').applyRemote(msg.type, msg.ruleset, { source: 'push' }); } catch (err) { console.warn('[REALTIME|WS] ruleset apply error:', err.message); } return; } if (msg._t === 'kiosk_layout_update') { console.log(`[REALTIME|WS] kiosk_layout_update v=${msg.layout?.version || '?'}`); try { require('../../tools/kiosk/server-layout-store').applyRemote(msg.layout); } catch (err) { console.warn('[REALTIME|WS] layout apply error:', err.message); } return; } if (msg._t === 'error') { console.warn('[REALTIME|WS] error dal server:', msg.message); return; } if (msg._t === 'ack') { return; // diagnostica, no-op } } }); ws.on('ping', () => { /* ws library risponde con pong automaticamente */ }); ws.on('error', (err) => { console.error(`[REALTIME|WS] Errore: ${err.message}`); resolve(false); }); ws.on('close', (code) => { console.log(`[REALTIME|WS] Disconnesso (code: ${code})`); ws = null; currentSessionId = null; if (onDisconnect) onDisconnect(); }); }); } /** * Invia dati al server tramite WebSocket, codificati in msgpack. * @param {Array} data - Array nel formato [timestamp, measurement, fields] */ function send(data) { let buf; try { const [timestamp, measurement, fields] = data; const packet = { ts: timestamp, _m: measurement, ...fields }; buf = encode(packet); } catch (err) { console.error('[REALTIME|WS] Errore encode:', err.message); return; } if (!ws || ws.readyState !== WebSocket.OPEN) { enqueue(buf); return; } try { ws.send(buf); } catch (err) { console.error('[REALTIME|WS] Errore invio:', err.message); } } function sendRaw(obj) { let buf; try { buf = encode(obj); } catch (err) { console.error('[REALTIME|WS] Errore encode raw:', err.message); return; } if (!ws || ws.readyState !== WebSocket.OPEN) { enqueue(buf); return; } try { ws.send(buf); } catch (err) { console.error('[REALTIME|WS] Errore invio raw:', err.message); } } function isConnected() { return ws !== null && ws.readyState === WebSocket.OPEN; } function getSessionId() { return currentSessionId; } function close() { onDisconnect = null; if (ws) { ws.close(); ws = null; currentSessionId = null; } } module.exports = { connect, send, sendRaw, isConnected, getSessionId, close };