mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-31 13:08:15 +00:00
Merge branch 'feature/lazy-connect'
This commit is contained in:
@@ -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
@@ -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
@@ -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) => {
|
||||||
@@ -256,6 +255,13 @@ async function init() {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user