feat: implement WebSocket synchronization and HTTP recovery

This commit is contained in:
2026-08-28 23:33:14 +03:00
parent 95b3450317
commit 03b283f892
8 changed files with 524 additions and 13 deletions
+37 -7
View File
@@ -1,10 +1,12 @@
(() => {
const target = document.querySelector('#health');
const role = new URLSearchParams(location.search).get('role') || 'spectator';
const token = localStorage.getItem('battleship.sessionToken') || '';
let socket;
let lastVersion = 0;
let retryIndex = 0;
let retryTimer;
let pollTimer;
let heartbeatTimer;
const labels = {
uptime_ms: 'Время работы (мс)', wifi_state: 'Wi-Fi', rssi_dbm: 'RSSI (дБм)',
free_heap_bytes: 'Свободная память (байт)', min_free_heap_bytes: 'Мин. свободная память (байт)',
@@ -34,10 +36,12 @@
async function pollState() {
try {
const response = await fetch(`/api/state?role=${encodeURIComponent(role)}`, { cache: 'no-store' });
const response = await fetch(`/api/state?version=${lastVersion}`, {
cache: 'no-store', headers: token ? { 'X-Session-Token': token } : {}
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const state = await response.json();
document.title = `Морской бой — версия ${state.version}`;
acceptState(state);
} catch (_) {}
}
@@ -51,14 +55,40 @@
pollTimer = undefined;
}
function stopHeartbeat() {
if (heartbeatTimer) window.clearInterval(heartbeatTimer);
heartbeatTimer = undefined;
}
function acceptState(state) {
if (lastVersion && state.version > lastVersion + 1) pollState();
lastVersion = state.version;
document.title = `Морской бой — версия ${state.version}`;
}
function connectSocket() {
socket = new WebSocket(`ws://${location.host}/ws?role=${encodeURIComponent(role)}`);
socket.onopen = () => { retryIndex = 0; stopPolling(); };
if (!token) { beginPolling(); return; }
const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
socket = new WebSocket(`${scheme}://${location.host}/ws`);
socket.onopen = () => {
socket.send(JSON.stringify({ type: 'hello', token, version: lastVersion }));
};
socket.onmessage = event => {
const state = JSON.parse(event.data);
document.title = `Морской бой — версия ${state.version}`;
try {
const message = JSON.parse(event.data);
if (message.type !== 'state') return;
acceptState(message);
retryIndex = 0;
stopPolling();
if (!heartbeatTimer) {
heartbeatTimer = window.setInterval(() => {
if (socket?.readyState === WebSocket.OPEN) socket.send('{"type":"ping"}');
}, 15000);
}
} catch (_) { socket.close(); }
};
socket.onclose = () => {
stopHeartbeat();
beginPolling();
const delays = [1000, 2000, 5000, 10000];
const delay = delays[Math.min(retryIndex++, delays.length - 1)];