Fix custom server reconnection and reconnect strategy

- Fix: CONNECT/RETRY_CONNECT always force-disconnect before reconnecting
  (prevents JOIN_ROOM going to official server when custom is selected)
- Add forceDisconnect() helper: clears socket, eventQueue, episodeLobby,
  expectedAcksCount, broadcast status, persists cleanup to storage
- Fix: save useCustomServer to storage on Join Room click (no race condition)
- Fix: trigger RETRY_CONNECT on server mode toggle and custom URL change
- Fix: show error when Custom selected but no URL entered (no silent fallback)
- Fix: button label changed to 'Join / Create Room' in all 6 locales
- Rewrite scheduleReconnect() for two-phase strategy:
  Phase 1 (aggressive): 500ms-5s backoff, max 20 attempts or 5 minutes
  Phase 2 (slow): retry every 5 minutes indefinitely, never give up
- Persist reconnectAttempts/reconnectStartTime/reconnectFailed to session storage
- Remove 'reconnect_failed' status; slow mode shows as 'reconnecting' with retry button
- Add scheduleReconnect() call on offline path to continue retry cycle
This commit is contained in:
Timo
2026-06-01 13:06:23 +02:00
parent 2b5da0dbb7
commit 8cc622bda0
8 changed files with 107 additions and 75 deletions
+72 -58
View File
@@ -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 });