[#11] WebSocket reconnection with exponential backoff + offline vote queue
- Exponential backoff: 1s → 2s → 4s → … → 30s max - Full-screen overlay when disconnected (spinner + message) - Votes cast offline are queued and replayed on reconnect - Deduplicated queue: same voter+option only queued once - ✓ Reconnected toast on successful reconnect
This commit is contained in:
@@ -628,6 +628,30 @@
|
||||
|
||||
/* Touch highlight */
|
||||
.option-card { -webkit-tap-highlight-color: rgba(0,212,255,0.12); }
|
||||
|
||||
/* ── Connection overlay ─────────────────────────────────── */
|
||||
#wsOverlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(11,13,20,0.92);
|
||||
z-index: 900;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
#wsOverlay.show { display: flex; }
|
||||
#wsOverlay .spinner {
|
||||
width: 40px; height: 40px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
#wsOverlay p { color: var(--text-muted); font-size: 0.85rem; text-align: center; }
|
||||
#wsOverlay .sub { font-size: 0.72rem; color: var(--text-muted); opacity: 0.6; margin-top: -8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -691,6 +715,13 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Connection lost overlay -->
|
||||
<div id="wsOverlay">
|
||||
<div class="spinner"></div>
|
||||
<p>Connection lost</p>
|
||||
<div class="sub">Attempting to reconnect…</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
@@ -745,7 +776,14 @@
|
||||
}
|
||||
|
||||
// ── WebSocket ──────────────────────────────────────────────
|
||||
const RECONNECT_BASE = 1000; // 1s initial
|
||||
const RECONNECT_MAX = 30000; // 30s cap
|
||||
let reconnectDelay = RECONNECT_BASE;
|
||||
let reconnectTimer = null;
|
||||
let offlineVoteQueue = []; // votes cast while disconnected
|
||||
|
||||
function connectWS() {
|
||||
if (ws) { ws.onclose = ws.onerror = null; ws.close(); }
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${location.host}`;
|
||||
ws = new WebSocket(wsUrl);
|
||||
@@ -754,6 +792,17 @@
|
||||
state.wsConnected = true;
|
||||
updateWsStatus('Connected');
|
||||
document.getElementById('wsDot').classList.remove('offline');
|
||||
document.getElementById('wsOverlay').classList.remove('show');
|
||||
reconnectDelay = RECONNECT_BASE; // reset backoff
|
||||
|
||||
// Replay queued votes
|
||||
if (offlineVoteQueue.length > 0) {
|
||||
const queue = [...offlineVoteQueue];
|
||||
offlineVoteQueue = [];
|
||||
queue.forEach(msg => wsSend(msg));
|
||||
}
|
||||
|
||||
showToast('✓ Reconnected', 'success');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
@@ -768,10 +817,7 @@
|
||||
} else if (msg.type === 'vote_update') {
|
||||
msg.results.forEach(r => {
|
||||
const opt = state.options.find(o => o.id === r.id);
|
||||
if (opt) {
|
||||
opt.votes = r.votes;
|
||||
opt.voters = r.voters;
|
||||
}
|
||||
if (opt) { opt.votes = r.votes; opt.voters = r.voters; }
|
||||
});
|
||||
render();
|
||||
} else if (msg.type === 'option_added' || msg.type === 'option_approved') {
|
||||
@@ -794,12 +840,25 @@
|
||||
state.wsConnected = false;
|
||||
updateWsStatus('Reconnecting…');
|
||||
document.getElementById('wsDot').classList.add('offline');
|
||||
setTimeout(connectWS, 3000);
|
||||
document.getElementById('wsOverlay').classList.add('show');
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX);
|
||||
connectWS();
|
||||
}, reconnectDelay);
|
||||
};
|
||||
}
|
||||
|
||||
function wsSend(obj) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj));
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(obj));
|
||||
} else {
|
||||
// Queue vote if offline
|
||||
if (obj.type === 'vote' && !offlineVoteQueue.find(m =>
|
||||
m.type === 'vote' && m.optionId === obj.optionId && m.voterName === obj.voterName)) {
|
||||
offlineVoteQueue.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateWsStatus(text) {
|
||||
|
||||
Reference in New Issue
Block a user