@@ -18,6 +18,12 @@ let selectedPosition = null; // Position to restore after voting
let lastSyncTime = null; // For connection status
let shouldScrollSelectedIntoView = false; // Only scroll on local actions
+const INITIAL_RECONNECT_DELAY = 1000;
+const MAX_RECONNECT_DELAY = 30000;
+let reconnectAttempts = 0;
+let reconnectTimer = null;
+let manualDisconnect = false;
+
const CHECK_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
const startScreen = document.getElementById('start-screen');
@@ -49,7 +55,16 @@ document.getElementById('join-code').addEventListener('keydown', (e) => {
});
document.getElementById('leave-btn').addEventListener('click', () => {
- if (ws) ws.close();
+ manualDisconnect = true;
+ if (reconnectTimer) {
+ clearTimeout(reconnectTimer);
+ reconnectTimer = null;
+ }
+ reconnectAttempts = 0;
+ if (ws) {
+ ws.close();
+ ws = null;
+ }
startScreen.classList.remove('hidden');
listScreen.classList.add('hidden');
currentRoom = null;
@@ -172,7 +187,15 @@ document.getElementById('add-bulk-btn').addEventListener('click', () => {
// Core functions
let messageQueue = [];
-function joinRoom(code) {
+function joinRoom(code, { isReconnect = false } = {}) {
+ manualDisconnect = false;
+ if (!isReconnect) {
+ reconnectAttempts = 0;
+ }
+ if (reconnectTimer) {
+ clearTimeout(reconnectTimer);
+ reconnectTimer = null;
+ }
currentRoom = code;
setUIEnabled(false, listScreen, listContainer);
@@ -182,6 +205,11 @@ function joinRoom(code) {
ws = new WebSocket(`${wsScheme}//${location.host}/ws?room=${code}&user=${userId}`);
ws.onopen = () => {
+ reconnectAttempts = 0;
+ if (reconnectTimer) {
+ clearTimeout(reconnectTimer);
+ reconnectTimer = null;
+ }
updateConnectionStatus('connected');
// Flush queued messages
while (messageQueue.length > 0) {
@@ -198,28 +226,36 @@ function joinRoom(code) {
ws.onerror = (error) => {
console.error('WebSocket error:', error);
+ setUIEnabled(false, listScreen, listContainer);
updateConnectionStatus('disconnected');
};
- ws.onclose = (event) => {
+ ws.onclose = () => {
+ ws = null;
setUIEnabled(false, listScreen, listContainer);
updateConnectionStatus('disconnected');
- if (!event.wasClean) {
- console.error('Connection closed unexpectedly');
- alert('Connection lost. Please try rejoining.');
- startScreen.classList.remove('hidden');
- listScreen.classList.add('hidden');
- currentRoom = null;
- items = [];
- messageQueue = [];
- listContainer.innerHTML = '';
+ if (manualDisconnect || !currentRoom) {
+ return;
}
+ scheduleReconnect();
};
startScreen.classList.add('hidden');
listScreen.classList.remove('hidden');
}
+function scheduleReconnect() {
+ if (reconnectTimer || manualDisconnect || !currentRoom) {
+ return;
+ }
+ const delay = Math.min(INITIAL_RECONNECT_DELAY * (2 ** reconnectAttempts), MAX_RECONNECT_DELAY);
+ reconnectTimer = setTimeout(() => {
+ reconnectTimer = null;
+ joinRoom(currentRoom, { isReconnect: true });
+ }, delay);
+ reconnectAttempts += 1;
+}
+
function sendMessage(msg) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
@@ -6,6 +6,7 @@ import { getSortedItems } from './render.js';
export function setupKeyboardShortcuts(options) {
const { items, vote, deleteItem, editItem, undo, render, getSelectedItemId, setSelectedItemId, setShouldScroll } = options;
+ const listContainer = document.getElementById('list-container');
document.addEventListener('keydown', (e) => {
// Open help modal on '?'
@@ -28,6 +29,8 @@ export function setupKeyboardShortcuts(options) {
// Ignore if typing in input/textarea
if (e.target.matches('input, textarea')) return;
+ if (listContainer && listContainer.classList.contains('disabled')) return;
+
// Undo
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
e.preventDefault();