diff --git a/extension/background.js b/extension/background.js index 949bfac..746b13e 100644 --- a/extension/background.js +++ b/extension/background.js @@ -55,7 +55,7 @@ function ensureState() { chrome.storage.session.get([ 'logs', 'history', 'currentRoom', 'lastActionState', 'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks', - 'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'currentTabId', 'currentTabTitle', + 'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'reconnectAttempts', 'currentTabId', 'currentTabTitle', 'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount' ], (data) => { clearTimeout(storageTimeout); @@ -79,6 +79,7 @@ function ensureState() { } if (data.reconnectFailed !== undefined) reconnectFailed = data.reconnectFailed; if (data.reconnectStartTime) reconnectStartTime = data.reconnectStartTime; + if (data.reconnectAttempts !== undefined) reconnectAttempts = data.reconnectAttempts; // Recover Force Sync Timeout if (data.forceSyncDeadline) { @@ -271,6 +272,50 @@ function addLog(message, type = 'info') { } // --- WebSocket Client --- +function forceDisconnect() { + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + if (episodeLobbyTimeout) { + clearTimeout(episodeLobbyTimeout); + episodeLobbyTimeout = null; + } + episodeLobby = null; + if (forceSyncTimeout) { + clearTimeout(forceSyncTimeout); + forceSyncTimeout = null; + } + if (socket) { + socket.onopen = null; + socket.onmessage = null; + socket.onclose = null; + socket.onerror = null; + socket.close(); + socket = null; + } + isConnecting = false; + isNamespaceJoined = false; + isForceSyncInitiator = false; + expectedAcksCount = 0; + forceSyncAcks.clear(); + eventQueue = []; + chrome.storage.session.set({ + isForceSyncInitiator: false, + forceSyncAcks: [], + forceSyncDeadline: null, + expectedAcksCount: 0, + eventQueue: [], + episodeLobby: null + }).catch(() => {}); + if (currentRoom) { + currentRoom.peers = []; + if (storageInitialized) chrome.storage.session.set({ currentRoom }); + chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {}); + } + broadcastConnectionStatus('disconnected'); +} + async function connect() { if (isConnecting) return; isConnecting = true; @@ -303,11 +348,7 @@ async function connect() { addLog('Browser is offline. Waiting...', 'warn'); broadcastConnectionStatus('offline'); isConnecting = false; - return; - } - - if (reconnectFailed) { - isConnecting = false; + scheduleReconnect(); return; } @@ -353,10 +394,10 @@ async function connect() { // --- Phase 5: Event Listeners --- socket.onopen = () => { reconnectAttempts = 0; - addLog('WebSocket Connection Opened', 'success'); reconnectStartTime = null; reconnectFailed = false; - chrome.storage.session.set({ reconnectFailed: false }); + addLog('WebSocket Connection Opened', 'success'); + chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null }).catch(() => {}); isNamespaceJoined = false; socket.send('40'); }; @@ -452,13 +493,10 @@ function broadcastConnectionStatus(status) { function updateBadgeStatus() { const isConnected = socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined; - const isReconnecting = !isConnected && reconnectAttempts > 0 && !reconnectFailed; + const isReconnecting = !isConnected && reconnectAttempts > 0; const status = isConnected ? 'connected' : (isConnecting || (socket && socket.readyState === WebSocket.CONNECTING) ? 'connecting' : (isReconnecting ? 'reconnecting' : 'disconnected')); - if (reconnectFailed) { - chrome.action.setBadgeText({ text: 'ERR' }); - chrome.action.setBadgeBackgroundColor({ color: '#ef4444' }); - } else if (status === 'reconnecting') { + if (status === 'reconnecting') { chrome.action.setBadgeText({ text: '...' }); chrome.action.setBadgeBackgroundColor({ color: '#f59e0b' }); } else if (status === 'connecting') { @@ -517,36 +555,27 @@ function showNotification(senderName, action) { function scheduleReconnect() { if (reconnectTimer) return; - if (reconnectFailed) { - return; - } - if (!reconnectStartTime) reconnectStartTime = Date.now(); - // Check 5 minute cap (300,000ms) - if (Date.now() - reconnectStartTime > 300000) { - reconnectFailed = true; - chrome.storage.session.set({ reconnectFailed: true }); - addLog('Reconnection failed after 5 minutes.', 'error'); - broadcastConnectionStatus('reconnect_failed'); - return; - } - + const elapsed = Date.now() - reconnectStartTime; reconnectAttempts++; - // Cap at max attempts to prevent infinite loops - if (reconnectAttempts > MAX_RECONNECT_ATTEMPTS) { + if (!reconnectFailed && (elapsed > 300000 || reconnectAttempts > MAX_RECONNECT_ATTEMPTS)) { reconnectFailed = true; - chrome.storage.session.set({ reconnectFailed: true }); - addLog('Reconnection failed after max attempts.', 'error'); - broadcastConnectionStatus('reconnect_failed'); - return; + addLog('Switching to slow reconnect mode (every 5 minutes)', 'warn'); } - // Aggressive reconnect: 500ms base, cap at 5s, no exponential growth beyond that - const delay = Math.min(_RECONNECT_BASE_DELAY * Math.pow(1.5, reconnectAttempts - 1), _RECONNECT_MAX_DELAY); + const delay = reconnectFailed + ? 300000 + : Math.min(_RECONNECT_BASE_DELAY * Math.pow(1.5, reconnectAttempts - 1), _RECONNECT_MAX_DELAY); - addLog(`Reconnect in ${Math.round(delay)}ms (attempt ${reconnectAttempts})`, 'warn'); + if (reconnectFailed) { + addLog(`Slow reconnect in 5min (attempt ${reconnectAttempts})`, 'info'); + } else { + addLog(`Reconnect in ${Math.round(delay)}ms (attempt ${reconnectAttempts})`, 'warn'); + } + + chrome.storage.session.set({ reconnectFailed, reconnectAttempts, reconnectStartTime }).catch(() => {}); reconnectTimer = setTimeout(() => { reconnectTimer = null; @@ -1196,42 +1225,26 @@ async function handleAsyncMessage(message, sender, sendResponse) { reconnectFailed = false; reconnectStartTime = null; reconnectAttempts = 0; - chrome.storage.session.set({ reconnectFailed: false }); + chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null }); const settings = await getSettings(); if (settings.roomId) { leaveOldRoomIfSwitching(settings.roomId); } - if (socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined) { - if (settings.roomId) { - emit(EVENTS.JOIN_ROOM, { - roomId: settings.roomId, - password: settings.password, - peerId, - username: settings.username, - tabTitle: currentTabTitle, - protocolVersion: PROTOCOL_VERSION - }); - } - } else { - connect(); - } + forceDisconnect(); + connect(); sendResponse({ status: 'ok' }); } else if (message.type === 'RETRY_CONNECT') { reconnectFailed = false; reconnectStartTime = null; reconnectAttempts = 0; - if (reconnectTimer) { - clearTimeout(reconnectTimer); - reconnectTimer = null; - } - chrome.storage.session.set({ reconnectFailed: false }); + chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null }); + forceDisconnect(); connect(); sendResponse({ status: 'ok' }); } else if (message.type === 'GET_STATUS') { const isConnected = socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined; - const isReconnecting = !isConnected && reconnectAttempts > 0 && !reconnectFailed; + const isReconnecting = !isConnected && reconnectAttempts > 0; let status = isConnected ? 'connected' : (isConnecting || (socket && socket.readyState === WebSocket.CONNECTING) ? 'connecting' : (isReconnecting ? 'reconnecting' : 'disconnected')); - if (reconnectFailed) status = 'reconnect_failed'; sendResponse({ status, peerId, @@ -1239,7 +1252,8 @@ async function handleAsyncMessage(message, sender, sendResponse) { lastActionState, targetTabId: currentTabId, episodeLobby: episodeLobby, - reconnectAttempts + reconnectAttempts, + reconnectSlowMode: reconnectFailed }); } else if (message.type === 'LEAVE_ROOM') { emit(EVENTS.LEAVE_ROOM, { peerId }); diff --git a/extension/locales/de.json b/extension/locales/de.json index fb4089a..fe6e820 100644 --- a/extension/locales/de.json +++ b/extension/locales/de.json @@ -26,7 +26,7 @@ "LABEL_PASSWORD_TOOLTIP": "Optionales Passwort, um den Raumzugang zu beschränken", "PLACEHOLDER_PASSWORD": "Raum-Passwort (optional)", "PLACEHOLDER_PASSWORD_TOOLTIP": "Passwort für den Raum (leer lassen, wenn keines vorhanden)", - "BTN_JOIN_ROOM": "Raum beitreten", + "BTN_JOIN_ROOM": "Raum beitreten / erstellen", "BTN_JOIN_ROOM_TOOLTIP": "Mit dem Raum verbinden", "LABEL_PUBLIC_ROOMS": "Öffentliche Räume", "LABEL_PUBLIC_ROOMS_TOOLTIP": "Liste der öffentlich verfügbaren Räume auf diesem Server", diff --git a/extension/locales/en.json b/extension/locales/en.json index 386412d..4bff23a 100644 --- a/extension/locales/en.json +++ b/extension/locales/en.json @@ -26,7 +26,7 @@ "LABEL_PASSWORD_TOOLTIP": "Optional password to restrict room access", "PLACEHOLDER_PASSWORD": "Room Password (optional)", "PLACEHOLDER_PASSWORD_TOOLTIP": "Password for the room (leave empty if none)", - "BTN_JOIN_ROOM": "Join Room", + "BTN_JOIN_ROOM": "Join / Create Room", "BTN_JOIN_ROOM_TOOLTIP": "Connect to the room", "LABEL_PUBLIC_ROOMS": "Public Rooms", "LABEL_PUBLIC_ROOMS_TOOLTIP": "List of publicly available rooms on this server", diff --git a/extension/locales/es.json b/extension/locales/es.json index 20edae4..6587de4 100644 --- a/extension/locales/es.json +++ b/extension/locales/es.json @@ -26,7 +26,7 @@ "LABEL_PASSWORD_TOOLTIP": "Contraseña opcional para restringir el acceso a la sala", "PLACEHOLDER_PASSWORD": "Contraseña de la sala (opcional)", "PLACEHOLDER_PASSWORD_TOOLTIP": "Contraseña para la sala (dejar vacío si no hay)", - "BTN_JOIN_ROOM": "Unirse a la sala", + "BTN_JOIN_ROOM": "Unirse / Crear sala", "BTN_JOIN_ROOM_TOOLTIP": "Conectarse a la sala", "LABEL_PUBLIC_ROOMS": "Salas públicas", "LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista de salas públicas disponibles en este servidor", diff --git a/extension/locales/fr.json b/extension/locales/fr.json index 8cd4ba5..8fe7b04 100644 --- a/extension/locales/fr.json +++ b/extension/locales/fr.json @@ -26,7 +26,7 @@ "LABEL_PASSWORD_TOOLTIP": "Mot de passe optionnel pour restreindre l'accès au salon", "PLACEHOLDER_PASSWORD": "Mot de passe du salon (optionnel)", "PLACEHOLDER_PASSWORD_TOOLTIP": "Mot de passe du salon (laisser vide si aucun)", - "BTN_JOIN_ROOM": "Rejoindre le salon", + "BTN_JOIN_ROOM": "Rejoindre / Créer le salon", "BTN_JOIN_ROOM_TOOLTIP": "Se connecter au salon", "LABEL_PUBLIC_ROOMS": "Salons publics", "LABEL_PUBLIC_ROOMS_TOOLTIP": "Liste des salons publics disponibles sur ce serveur", diff --git a/extension/locales/pt-BR.json b/extension/locales/pt-BR.json index b5d3618..1b97389 100644 --- a/extension/locales/pt-BR.json +++ b/extension/locales/pt-BR.json @@ -26,7 +26,7 @@ "LABEL_PASSWORD_TOOLTIP": "Senha opcional para restringir o acesso à sala", "PLACEHOLDER_PASSWORD": "Senha da sala (opcional)", "PLACEHOLDER_PASSWORD_TOOLTIP": "Senha para a sala (deixe em branco se não houver)", - "BTN_JOIN_ROOM": "Entrar na sala", + "BTN_JOIN_ROOM": "Entrar / Criar sala", "BTN_JOIN_ROOM_TOOLTIP": "Conectar à sala", "LABEL_PUBLIC_ROOMS": "Salas públicas", "LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista de salas públicas disponíveis neste servidor", diff --git a/extension/locales/ru.json b/extension/locales/ru.json index cbca229..17d79a8 100644 --- a/extension/locales/ru.json +++ b/extension/locales/ru.json @@ -26,7 +26,7 @@ "LABEL_PASSWORD_TOOLTIP": "Дополнительный пароль для ограничения доступа к комнате", "PLACEHOLDER_PASSWORD": "Пароль комнаты (опционально)", "PLACEHOLDER_PASSWORD_TOOLTIP": "Пароль от комнаты (оставьте пустым, если его нет)", - "BTN_JOIN_ROOM": "Войти в комнату", + "BTN_JOIN_ROOM": "Войти / Создать комнату", "BTN_JOIN_ROOM_TOOLTIP": "Подключиться к комнате", "LABEL_PUBLIC_ROOMS": "Публичные комнаты", "LABEL_PUBLIC_ROOMS_TOOLTIP": "Список публично доступных комнат на этом сервере", diff --git a/extension/popup.js b/extension/popup.js index 11f6f16..c6b4a78 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -59,6 +59,7 @@ let localPeerId = null; let lastPeersJson = null; let lastKnownPeers = []; let isDevTabVisible = false; +let reconnectSlowMode = false; let joinBtnTimeout = null; let forceSyncResetTimer = null; let popupIntervals = []; @@ -128,6 +129,7 @@ async function init() { } if (res) { localPeerId = res.peerId; + reconnectSlowMode = res.reconnectSlowMode || false; applyConnectionStatus(res.status); updatePeerList(res.peers); lastKnownPeers = res.peers || []; @@ -671,10 +673,9 @@ function applyConnectionStatus(status) { const connected = status === 'connected'; const connecting = status === 'connecting'; const reconnecting = status === 'reconnecting'; - const failed = status === 'reconnect_failed'; if (elements.connDot) { - elements.connDot.className = 'status-dot ' + (connected ? 'status-online' : (failed ? 'status-offline' : ((connecting || reconnecting) ? 'status-online' : 'status-offline'))); + elements.connDot.className = 'status-dot ' + (connected ? 'status-online' : ((connecting || reconnecting) ? 'status-online' : 'status-offline')); if (reconnecting) { elements.connDot.style.background = '#f59e0b'; @@ -682,7 +683,7 @@ function applyConnectionStatus(status) { } else if (connecting) { elements.connDot.style.background = '#fbbf24'; elements.connDot.style.boxShadow = '0 0 8px #fbbf24'; - } else if (failed) { + } else if (!connected) { elements.connDot.style.background = '#ef4444'; elements.connDot.style.boxShadow = 'none'; } else { @@ -692,15 +693,15 @@ function applyConnectionStatus(status) { } if (elements.connText) { - elements.connText.textContent = connected ? getMessage('STATUS_CONNECTED') : (reconnecting ? getMessage('STATUS_RECONNECTING') : (connecting ? getMessage('STATUS_CONNECTING') : (failed ? getMessage('STATUS_FAILED') : getMessage('STATUS_DISCONNECTED')))); + elements.connText.textContent = connected ? getMessage('STATUS_CONNECTED') : (reconnecting ? getMessage('STATUS_RECONNECTING') : (connecting ? getMessage('STATUS_CONNECTING') : getMessage('STATUS_DISCONNECTED'))); } if (elements.retryBtn) { - elements.retryBtn.style.display = failed ? 'block' : 'none'; + elements.retryBtn.style.display = reconnecting && reconnectSlowMode ? 'block' : 'none'; } if (elements.joinBtn) { if (connecting || reconnecting) { - elements.joinBtn.disabled = true; + elements.joinBtn.disabled = !reconnectSlowMode; elements.joinBtn.textContent = connecting ? getMessage('BTN_STATE_JOINING') : getMessage('BTN_STATE_RECONNECTING'); } else { elements.joinBtn.disabled = false; @@ -858,9 +859,12 @@ function setServerMode(custom) { elements.serverOfficial.classList.toggle('active', !custom); elements.serverCustom.classList.toggle('active', custom); elements.serverUrl.style.display = custom ? 'block' : 'none'; - chrome.storage.sync.get(['useCustomServer'], (data) => { + chrome.storage.sync.get(['useCustomServer', 'serverUrl'], (data) => { if (data.useCustomServer !== custom) { chrome.storage.sync.set({ useCustomServer: custom }); + if (!custom || data.serverUrl) { + chrome.runtime.sendMessage({ type: 'RETRY_CONNECT' }); + } } }); } @@ -919,6 +923,9 @@ elements.serverUrl.addEventListener('change', () => { elements.serverUrl.value = url; chrome.storage.sync.set({ serverUrl: url }); } + if (elements.serverCustom.classList.contains('active') && url) { + chrome.runtime.sendMessage({ type: 'RETRY_CONNECT' }); + } }); elements.tabs.forEach(btn => { @@ -993,6 +1000,12 @@ elements.joinBtn.addEventListener('click', async () => { const useCustom = elements.serverCustom.classList.contains('active'); // Proactive URL Validation + if (useCustom && !serverUrl) { + showError(getMessage('ERR_INVALID_SERVER_URL')); + elements.joinBtn.disabled = false; + elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM'); + return; + } if (useCustom && serverUrl) { try { const urlToCheck = serverUrl.includes('://') ? serverUrl : 'ws://' + serverUrl; @@ -1008,7 +1021,7 @@ elements.joinBtn.addEventListener('click', async () => { const roomId = roomIdInput || Math.random().toString(36).substring(2, 8).toUpperCase(); const password = elements.password.value; - await chrome.storage.sync.set({ serverUrl, roomId, password }); + await chrome.storage.sync.set({ serverUrl, roomId, password, useCustomServer: useCustom }); elements.roomId.value = roomId; // Tell background to connect @@ -1338,20 +1351,25 @@ chrome.runtime.onMessage.addListener((msg) => { updatePeerList(msg.peers); if (msg.peers) detectPeerChanges(msg.peers); } else if (msg.type === 'CONNECTION_STATUS') { - applyConnectionStatus(msg.status); + if (msg.status !== 'reconnecting') { + applyConnectionStatus(msg.status); + reconnectSlowMode = false; + } if (msg.status === 'connected') { chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => { if (res && res.peers) updatePeerList(res.peers); if (res && res.lastActionState) updateLastActionUI(res.lastActionState, res.peers); }); } - if (msg.status === 'disconnected' || msg.status === 'reconnect_failed') { + if (msg.status === 'disconnected') { elements.joinBtn.disabled = false; - elements.joinBtn.textContent = 'Join Room'; + elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM'); } if (msg.status === 'reconnecting') { chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => { if (chrome.runtime.lastError) return; + if (res && res.reconnectSlowMode !== undefined) reconnectSlowMode = res.reconnectSlowMode; + applyConnectionStatus(msg.status); if (res && res.reconnectAttempts !== undefined) { if (elements.connText) { elements.connText.textContent = `Reconnecting... (${res.reconnectAttempts})`;