Merge branch 'feature/lazy-connect'

This commit is contained in:
Timo
2026-06-16 10:47:46 +02:00
3 changed files with 103 additions and 26 deletions
+3
View File
@@ -17,6 +17,9 @@ All notable changes to the KoalaSync browser extension and relay server.
## [v2.3.1] — 2026-06-15 ## [v2.3.1] — 2026-06-15
### Fixed
- **Server: Concurrent peer join race condition and teardown error handling**
### Changed ### Changed
- **Server: Smart unhandled rejection handling (exits after 5/min instead of 1)** - **Server: Smart unhandled rejection handling (exits after 5/min instead of 1)**
- **Server: Optimized admin health metrics allocation** - **Server: Optimized admin health metrics allocation**
+69 -19
View File
@@ -176,6 +176,7 @@ let reconnectAttempts = 0;
let currentServerUrl = null; let currentServerUrl = null;
let roomIdleSince = null; let roomIdleSince = null;
let lastContentHeartbeatAt = null; let lastContentHeartbeatAt = null;
let connectIntent = false;
const MAX_RECONNECT_ATTEMPTS = 20; const MAX_RECONNECT_ATTEMPTS = 20;
const _RECONNECT_BASE_DELAY = 500; const _RECONNECT_BASE_DELAY = 500;
const _RECONNECT_MAX_DELAY = 5000; const _RECONNECT_MAX_DELAY = 5000;
@@ -404,7 +405,12 @@ function clearTargetTabForIdle() {
async function leaveRoomAfterIdleGrace(reason) { async function leaveRoomAfterIdleGrace(reason) {
if (!currentRoom) return; if (!currentRoom) return;
connectIntent = false;
reconnectFailed = false;
reconnectAttempts = 0;
chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null });
emit(EVENTS.LEAVE_ROOM, { peerId }); emit(EVENTS.LEAVE_ROOM, { peerId });
forceDisconnect();
currentRoom = null; currentRoom = null;
currentTabId = null; currentTabId = null;
currentTabTitle = null; currentTabTitle = null;
@@ -457,7 +463,9 @@ async function connect() {
addLog('Browser is offline. Waiting...', 'warn'); addLog('Browser is offline. Waiting...', 'warn');
broadcastConnectionStatus('offline'); broadcastConnectionStatus('offline');
isConnecting = false; isConnecting = false;
scheduleReconnect(); if (currentRoom || connectIntent) {
scheduleReconnect();
}
return; return;
} }
@@ -564,25 +572,32 @@ async function connect() {
isNamespaceJoined = false; isNamespaceJoined = false;
stopPing(); stopPing();
isForceSyncInitiator = false; if (!connectIntent && !currentRoom) {
forceSyncAcks.clear(); isForceSyncInitiator = false;
if (forceSyncTimeout) clearTimeout(forceSyncTimeout); forceSyncAcks.clear();
chrome.storage.session.set({ if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
isForceSyncInitiator: false, chrome.storage.session.set({
forceSyncAcks: [], isForceSyncInitiator: false,
forceSyncDeadline: null forceSyncAcks: [],
}).catch(() => {}); forceSyncDeadline: null
}).catch(() => {});
}
if (currentRoom) { if (currentRoom && !connectIntent) {
currentRoom.peers = []; currentRoom.peers = [];
if (storageInitialized) chrome.storage.session.set({ currentRoom }).catch(() => {}); if (storageInitialized) chrome.storage.session.set({ currentRoom }).catch(() => {});
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {}); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
} }
broadcastConnectionStatus('disconnected'); broadcastConnectionStatus('disconnected');
addLog('Disconnected. Scheduling reconnect...', 'warn'); if (currentRoom || connectIntent) {
socket = null; addLog('Disconnected. Scheduling reconnect...', 'warn');
scheduleReconnect(); socket = null;
scheduleReconnect();
} else {
addLog('Disconnected. No active session — staying disconnected.', 'info');
socket = null;
}
}; };
socket.onerror = () => { socket.onerror = () => {
@@ -597,7 +612,9 @@ async function connect() {
const errMsg = (e && e.message) ? e.message : String(e || 'Unknown connection error'); const errMsg = (e && e.message) ? e.message : String(e || 'Unknown connection error');
addLog(errMsg, logType); addLog(errMsg, logType);
broadcastConnectionStatus('disconnected'); broadcastConnectionStatus('disconnected');
scheduleReconnect(); if (currentRoom || connectIntent) {
scheduleReconnect();
}
} }
} }
@@ -738,6 +755,9 @@ function sendPing() {
addLog('Ping timeout reached, force disconnecting to trigger reconnect', 'warn'); addLog('Ping timeout reached, force disconnecting to trigger reconnect', 'warn');
pendingPingT = null; pendingPingT = null;
forceDisconnect(); forceDisconnect();
if (currentRoom || connectIntent) {
scheduleReconnect();
}
} }
pingTimeout = null; pingTimeout = null;
}, 5000); }, 5000);
@@ -836,6 +856,14 @@ function handleServerEvent(event, data) {
break; break;
case EVENTS.ERROR: case EVENTS.ERROR:
isConnecting = false; isConnecting = false;
// If we get a server error before successfully joining a room,
// clear connectIntent to prevent an infinite reconnect loop.
if (!currentRoom) {
connectIntent = false;
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
reconnectAttempts = 0;
reconnectFailed = false;
}
broadcastConnectionStatus('disconnected'); broadcastConnectionStatus('disconnected');
addLog(`Server Error: ${data.message}`, 'error'); addLog(`Server Error: ${data.message}`, 'error');
chrome.storage.local.get(['browserNotifications', 'locale'], async (settings) => { chrome.storage.local.get(['browserNotifications', 'locale'], async (settings) => {
@@ -1334,7 +1362,7 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'keepAlive') { if (alarm.name === 'keepAlive') {
chrome.storage.session.get('keepAlive', () => {}); chrome.storage.session.get('keepAlive', () => {});
if (!socket || socket.readyState !== WebSocket.OPEN) { if (!socket || socket.readyState !== WebSocket.OPEN) {
if (!reconnectFailed) { if (!reconnectFailed && (currentRoom || connectIntent)) {
connect(); connect();
} }
} else if (currentRoom) { } else if (currentRoom) {
@@ -1417,6 +1445,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
if (message.type === 'CONNECT') { if (message.type === 'CONNECT') {
const settings = await getSettings(); const settings = await getSettings();
connectIntent = !!settings.roomId;
const desiredUrl = resolveServerUrl(settings); const desiredUrl = resolveServerUrl(settings);
if (settings.roomId && currentRoom && currentRoom.roomId === settings.roomId && socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined && desiredUrl === currentServerUrl) { if (settings.roomId && currentRoom && currentRoom.roomId === settings.roomId && socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined && desiredUrl === currentServerUrl) {
@@ -1439,7 +1468,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
} }
if (desiredUrl !== currentServerUrl || !socket || socket.readyState !== WebSocket.OPEN || !isNamespaceJoined) { if (desiredUrl !== currentServerUrl || !socket || socket.readyState !== WebSocket.OPEN || !isNamespaceJoined) {
if (desiredUrl !== currentServerUrl) forceDisconnect(); if (desiredUrl !== currentServerUrl) forceDisconnect();
connect(); if (settings.roomId) connect();
} else if (settings.roomId) { } else if (settings.roomId) {
emit(EVENTS.JOIN_ROOM, { emit(EVENTS.JOIN_ROOM, {
roomId: settings.roomId, roomId: settings.roomId,
@@ -1452,6 +1481,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
} }
sendResponse({ status: 'ok' }); sendResponse({ status: 'ok' });
} else if (message.type === 'RETRY_CONNECT') { } else if (message.type === 'RETRY_CONNECT') {
connectIntent = true;
reconnectFailed = false; reconnectFailed = false;
reconnectStartTime = null; reconnectStartTime = null;
reconnectAttempts = 0; reconnectAttempts = 0;
@@ -1480,6 +1510,10 @@ async function handleAsyncMessage(message, sender, sendResponse) {
ping: currentPingMs ping: currentPingMs
}); });
} else if (message.type === 'LEAVE_ROOM') { } else if (message.type === 'LEAVE_ROOM') {
connectIntent = false;
reconnectFailed = false;
reconnectAttempts = 0;
chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null });
resetAudioProcessingInTab(currentTabId); resetAudioProcessingInTab(currentTabId);
emit(EVENTS.LEAVE_ROOM, { peerId }); emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null; currentRoom = null;
@@ -1510,8 +1544,10 @@ async function handleAsyncMessage(message, sender, sendResponse) {
episodeLobby: null, episodeLobby: null,
expectedAcksCount: 0 expectedAcksCount: 0
}); });
chrome.storage.local.set({ roomId: '', password: '' }).catch(() => {});
addLog('Left Room', 'info'); addLog('Left Room', 'info');
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {}); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
forceDisconnect();
sendResponse({ status: 'ok' }); sendResponse({ status: 'ok' });
} else if (message.type === 'CLEAR_LOGS') { } else if (message.type === 'CLEAR_LOGS') {
logs = []; logs = [];
@@ -1526,6 +1562,16 @@ async function handleAsyncMessage(message, sender, sendResponse) {
} else if (message.type === 'WEB_JOIN_REQUEST') { } else if (message.type === 'WEB_JOIN_REQUEST') {
const { roomId: rawRoomId, password, useCustomServer, serverUrl } = message; const { roomId: rawRoomId, password, useCustomServer, serverUrl } = message;
const roomId = typeof rawRoomId === 'string' ? rawRoomId.replace(/[^a-zA-Z0-9\-]/g, '') : ''; const roomId = typeof rawRoomId === 'string' ? rawRoomId.replace(/[^a-zA-Z0-9\-]/g, '') : '';
if (!roomId) {
const errMsg = { type: 'JOIN_STATUS', success: false, message: 'Invalid room ID' };
chrome.runtime.sendMessage(errMsg).catch(() => {});
chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => chrome.tabs.sendMessage(tab.id, errMsg).catch(() => {}));
});
sendResponse({ status: 'invalid_room_id' });
return;
}
connectIntent = true;
chrome.storage.local.set({ chrome.storage.local.set({
roomId, roomId,
password, password,
@@ -1541,6 +1587,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
for (const tab of tabs) { for (const tab of tabs) {
chrome.tabs.sendMessage(tab.id, { type: 'JOIN_STATUS', success: true, message: 'Already in room' }).catch(() => {}); chrome.tabs.sendMessage(tab.id, { type: 'JOIN_STATUS', success: true, message: 'Already in room' }).catch(() => {});
} }
sendResponse({ status: 'already_joined' });
return; return;
} }
@@ -1554,7 +1601,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
if (desiredUrl !== currentServerUrl || !socket || socket.readyState !== WebSocket.OPEN || !isNamespaceJoined) { if (desiredUrl !== currentServerUrl || !socket || socket.readyState !== WebSocket.OPEN || !isNamespaceJoined) {
if (desiredUrl !== currentServerUrl) forceDisconnect(); if (desiredUrl !== currentServerUrl) forceDisconnect();
connect(); connect();
} else { } else if (roomId) {
emit(EVENTS.JOIN_ROOM, { emit(EVENTS.JOIN_ROOM, {
roomId, roomId,
password, password,
@@ -1939,5 +1986,8 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, _tab) => {
} }
}); });
// Initial Connect // Initial Connect — only if user has an active room configuration
connect(); getSettings().then(settings => {
connectIntent = !!settings.roomId;
if (connectIntent) connect();
}).catch(() => connectIntent = false);
+31 -7
View File
@@ -237,8 +237,7 @@ async function init() {
refreshLogs(); refreshLogs();
refreshHistory(); refreshHistory();
// Default connection status (localized) before async check // Initial Status Check (status shows via GET_STATUS below)
applyConnectionStatus('disconnected');
// Initial Status Check // Initial Status Check
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, async (res) => { chrome.runtime.sendMessage({ type: 'GET_STATUS' }, async (res) => {
@@ -255,6 +254,13 @@ async function init() {
updatePeerList(res.peers); updatePeerList(res.peers);
lastKnownPeers = res.peers || []; lastKnownPeers = res.peers || [];
if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers); if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers);
// If user has a room configured but background is not connected,
// trigger connection now — the popup opening is explicit user intent.
if (res.status === 'disconnected' && localData.roomId) {
chrome.runtime.sendMessage({ type: 'CONNECT' }).catch(() => {});
applyConnectionStatus('connecting');
}
// Populate Tabs using the background's targetTabId // Populate Tabs using the background's targetTabId
await populateTabs(res.peers, res.targetTabId); await populateTabs(res.peers, res.targetTabId);
@@ -1195,11 +1201,23 @@ elements.joinBtn.addEventListener('click', async () => {
if (joinBtnTimeout) clearTimeout(joinBtnTimeout); if (joinBtnTimeout) clearTimeout(joinBtnTimeout);
joinBtnTimeout = setTimeout(() => { joinBtnTimeout = setTimeout(() => {
elements.joinBtn.disabled = false; chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM'); if (res && res.status === 'connecting') {
joinBtnTimeout = null; joinBtnTimeout = setTimeout(() => {
isProcessingConnection = false; elements.joinBtn.disabled = false;
showError(getMessage('ERR_CONN_TIMEOUT')); elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM');
joinBtnTimeout = null;
isProcessingConnection = false;
showError(getMessage('ERR_CONN_TIMEOUT'));
}, 15000);
return;
}
elements.joinBtn.disabled = false;
elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM');
joinBtnTimeout = null;
isProcessingConnection = false;
if (res && res.status !== 'connected') showError(getMessage('ERR_CONN_TIMEOUT'));
});
}, 15000); }, 15000);
const serverUrl = elements.serverUrl.value.trim(); const serverUrl = elements.serverUrl.value.trim();
@@ -1210,6 +1228,8 @@ elements.joinBtn.addEventListener('click', async () => {
showError(getMessage('ERR_INVALID_SERVER_URL')); showError(getMessage('ERR_INVALID_SERVER_URL'));
elements.joinBtn.disabled = false; elements.joinBtn.disabled = false;
elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM'); elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM');
if (joinBtnTimeout) { clearTimeout(joinBtnTimeout); joinBtnTimeout = null; }
isProcessingConnection = false;
return; return;
} }
if (useCustom && serverUrl) { if (useCustom && serverUrl) {
@@ -1220,6 +1240,7 @@ elements.joinBtn.addEventListener('click', async () => {
showError(getMessage('ERR_INVALID_SERVER_URL')); showError(getMessage('ERR_INVALID_SERVER_URL'));
elements.joinBtn.disabled = false; elements.joinBtn.disabled = false;
elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM'); elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM');
if (joinBtnTimeout) { clearTimeout(joinBtnTimeout); joinBtnTimeout = null; }
isProcessingConnection = false; isProcessingConnection = false;
return; return;
} }
@@ -1608,6 +1629,9 @@ chrome.runtime.onMessage.addListener((msg) => {
updatePeerList(msg.peers); updatePeerList(msg.peers);
if (msg.peers) detectPeerChanges(msg.peers); if (msg.peers) detectPeerChanges(msg.peers);
} else if (msg.type === 'CONNECTION_STATUS') { } else if (msg.type === 'CONNECTION_STATUS') {
if (msg.status === 'connected' || msg.status === 'disconnected') {
if (joinBtnTimeout) { clearTimeout(joinBtnTimeout); joinBtnTimeout = null; }
}
if (msg.status === 'connected') { if (msg.status === 'connected') {
clearConnectionErrorTimer(); clearConnectionErrorTimer();
} }