Fix critical sync lockouts, zombie disconnects, play-seek debounces, active lobbies, SW keep-alives, branding matches and ESLint warnings

This commit is contained in:
Timo
2026-05-30 00:02:19 +02:00
parent 5ef059a94f
commit 56955027f9
6 changed files with 144 additions and 12 deletions
+52
View File
@@ -18,6 +18,17 @@ let isNamespaceJoined = false;
let lastActionState = { action: null, senderId: null, timestamp: 0, acks: [] };
let localSeq = 0; // Monotonically increasing command sequence for this peer
const lastSeqBySender = {}; // senderId → last received seq (stale command guard)
const activePorts = new Set(); // New: track active content ports for keep-alive
// --- Keep-Alive Port Listener ---
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'keepAlive') {
activePorts.add(port);
port.onDisconnect.addListener(() => {
activePorts.delete(port);
});
}
});
function _persistLastSeq() {
if (storageInitialized) chrome.storage.session.set({ lastSeqBySender });
@@ -562,9 +573,47 @@ function handleServerEvent(event, data) {
currentRoom = data;
if (currentRoom && Array.isArray(currentRoom.peers)) {
currentRoom.peers = currentRoom.peers.map(p => typeof p === 'object' ? createPeerData(p) : { peerId: p, username: null, tabTitle: null, mediaTitle: null, playbackState: null, currentTime: null, volume: null, muted: null, lastHeartbeat: Date.now() });
// Clear sequence tracking for peers that are no longer in the room
const activePeerIds = new Set(currentRoom.peers.map(p => typeof p === 'object' ? p.peerId : p));
Object.keys(lastSeqBySender).forEach(pId => {
if (!activePeerIds.has(pId)) {
delete lastSeqBySender[pId];
}
});
_persistLastSeq();
} else if (currentRoom) {
currentRoom.peers = [];
}
// Recover server-tracked active Episode Lobby if present
if (data && data.activeLobby && !episodeLobby) {
episodeLobby = {
expectedTitle: data.activeLobby.expectedTitle,
initiatorPeerId: data.activeLobby.initiatorPeerId,
readyPeers: data.activeLobby.readyPeers,
createdAt: Date.now()
};
persistEpisodeLobby();
broadcastLobbyUpdate();
addLog(`Recovered active episode lobby from server: "${episodeLobby.expectedTitle}"`, 'info');
// Notify content script to start polling
if (currentTabId) {
const tabId = parseInt(currentTabId);
if (!isNaN(tabId)) {
chrome.tabs.sendMessage(tabId, {
type: 'EPISODE_LOBBY',
expectedTitle: episodeLobby.expectedTitle
}).catch(() => {});
}
}
// Schedule timeout if we don't already have one
if (!episodeLobbyTimeout) {
episodeLobbyTimeout = setTimeout(() => cancelEpisodeLobby('Timeout'), EPISODE_LOBBY_TIMEOUT);
}
}
if (storageInitialized) chrome.storage.session.set({ currentRoom });
addLog(`Joined Room: ${data?.roomId || 'unknown'}`, 'success');
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: data.peers }).catch(() => {});
@@ -716,6 +765,9 @@ function handleServerEvent(event, data) {
if (!Array.isArray(currentRoom.peers)) currentRoom.peers = [];
if (data.status === 'joined') {
if (!currentRoom.peers.find(p => (p.peerId || p) === data.peerId)) {
delete lastSeqBySender[data.peerId];
_persistLastSeq();
currentRoom.peers.push(createPeerData(data));
if (storageInitialized) chrome.storage.session.set({ currentRoom });
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
+1
View File
@@ -1,3 +1,4 @@
/* global cloneInto */
/**
* KoalaSync Bridge Script
* Injected into sync.koalastuff.net to facilitate communication between
+42 -1
View File
@@ -383,6 +383,18 @@
}
} else if (action === EVENTS.FORCE_SYNC_EXECUTE) {
stopLobbyPoll();
// Network Latency Compensation: pre-seek video by network transit latency
const video = findVideo();
if (video && message.actionTimestamp) {
const latency = (Date.now() - message.actionTimestamp) / 1000;
if (latency > 0.05 && latency < 5.0) {
_setSuppress('seek');
video.currentTime += latency;
reportLog(`Force Sync: Compensated ${Math.round(latency * 1000)}ms network latency`, 'info');
}
}
tryMediaAction(EVENTS.PLAY);
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp, commandSenderId: message.commandSenderId });
actionCompleted = true;
@@ -469,6 +481,17 @@
// Detect native events
function reportEvent(action) {
if (seekDebounceTimer && (action === EVENTS.PLAY || action === EVENTS.PAUSE)) {
clearTimeout(seekDebounceTimer);
seekDebounceTimer = null;
const v = findVideo();
if (v && Number.isFinite(v.currentTime)) {
lastReportedSeekTime = v.currentTime;
reportLog(`[Seek] Debounce flushed immediately due to ${action.toUpperCase()}`, 'info');
reportEvent(EVENTS.SEEK);
}
}
const video = findVideo();
if (!video) return;
@@ -512,7 +535,7 @@
// pause immediately after switching back.
let pageVisible = !document.hidden;
let visibilityGraceUntil = 0;
const VISIBILITY_GRACE_MS = 1000;
const VISIBILITY_GRACE_MS = 300;
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
@@ -705,6 +728,24 @@
// Initial Setup
setupListeners();
// Maintain a persistent keep-alive port connection to prevent background SW suspension
let keepAlivePort = null;
function connectKeepAlivePort() {
try {
if (chrome.runtime.id) {
keepAlivePort = chrome.runtime.connect({ name: 'keepAlive' });
keepAlivePort.onDisconnect.addListener(() => {
keepAlivePort = null;
setTimeout(connectKeepAlivePort, 1000);
});
}
} catch (_e) {
// Extension context invalidated or disabled
}
}
connectKeepAlivePort();
schedulePeriodicHeartbeat();
// Immediate heartbeat on injection — populate peer data without waiting 15s
+20 -9
View File
@@ -563,7 +563,9 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
const hostname = new URL(tab.url).hostname.toLowerCase();
if (domain.endsWith('.')) return hostname.startsWith(domain) || hostname.includes('.' + domain);
if (domain.includes('.')) return hostname === domain || hostname.endsWith('.' + domain);
} catch {}
} catch {
/* ignore invalid URLs */
}
return urlStr.includes(domain);
})) return false;
}
@@ -571,23 +573,32 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
});
// Smart Matching Logic — exclude own tabTitle to prevent self-match (computed once)
const cleanTitle = (rawTitle) => {
if (!rawTitle) return '';
return rawTitle
.replace(/(?:\s*[-\|•]\s*(?:YouTube|Twitch|Jellyfin|Emby|Netflix|Vimeo|Dailymotion).*)$/i, '')
.replace(/^(?:Netflix|Twitch|YouTube|Emby|Jellyfin)\s*[-\|•]\s*/i, '')
.trim();
};
const peerTitles = peerIds
.filter(p => (typeof p === 'object' ? p.peerId : p) !== localPeerId)
.map(p => (typeof p === 'object' ? p.tabTitle : null))
.filter(t => t && t.length > 3);
.filter(t => t && t.length > 3)
.map(t => cleanTitle(t).toLowerCase())
.filter(t => t.length > 3);
filteredTabs.forEach(tab => {
const option = document.createElement('option');
option.value = tab.id;
const title = (tab.title || 'Loading...');
const rawTitle = (tab.title || 'Loading...');
const title = cleanTitle(rawTitle).toLowerCase();
const isMatch = peerTitles.some(pt => {
const t1 = title.toLowerCase();
const t2 = pt.toLowerCase();
return t1.includes(t2) || t2.includes(t1);
const isMatch = title.length > 3 && peerTitles.some(pt => {
return title.includes(pt) || pt.includes(title);
});
let label = title.substring(0, 45) + (title.length > 45 ? '...' : '');
let label = rawTitle.substring(0, 45) + (rawTitle.length > 45 ? '...' : '');
if (isMatch) {
label = `⭐ MATCH: ${label}`;
option.style.fontWeight = 'bold';
@@ -992,7 +1003,7 @@ function handleCreateRoom() {
// Auto-connect
elements.joinBtn.click();
};
}
elements.createRoomBtn.addEventListener('click', handleCreateRoom);
const syncTabCreateRoomBtn = document.getElementById('syncTabCreateRoomBtn');
+29 -2
View File
@@ -196,7 +196,18 @@ function removePeerFromRoom(socketId, roomId, reason) {
// 3. Notify remaining peers (use io.to so the removed socket itself
// doesn't receive it — it has already left or is disconnecting)
io.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
const isPeerStillConnected = Array.from(room.peerData.values()).some(data => data.peerId === peerId);
if (!isPeerStillConnected) {
io.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
}
// 3.5. Clean up active lobby if a peer leaves
if (room.activeLobby) {
room.activeLobby.readyPeers = room.activeLobby.readyPeers.filter(id => id !== peerId);
if (room.activeLobby.readyPeers.length <= 1 || room.activeLobby.initiatorPeerId === peerId) {
room.activeLobby = null; // Dissolve lobby
}
}
// 4. Delete empty room
if (room.peers.size === 0) {
@@ -401,7 +412,8 @@ io.on('connection', (socket) => {
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, username: username || null, tabTitle: tabTitle || null, mediaTitle: mediaTitle || null, status: 'joined' });
socket.emit(EVENTS.ROOM_DATA, {
roomId,
peers: Array.from(room.peers).map(sid => room.peerData.get(sid))
peers: Array.from(room.peers).map(sid => room.peerData.get(sid)),
activeLobby: room.activeLobby || null
});
log('ROOM', `Peer ${peerId} joined: ${roomId.substring(0, 3)}***`);
} catch (err) {
@@ -477,6 +489,21 @@ io.on('connection', (socket) => {
// Strip undefined keys for clean wire format
Object.keys(relayPayload).forEach(k => relayPayload[k] === undefined && delete relayPayload[k]);
socket.to(mapping.roomId).emit(eventName, relayPayload);
// --- Side-effects: Server-side Episode Lobby Tracking ---
if (eventName === EVENTS.EPISODE_LOBBY && relayPayload.expectedTitle) {
room.activeLobby = {
expectedTitle: relayPayload.expectedTitle,
initiatorPeerId: mapping.peerId,
readyPeers: [mapping.peerId]
};
} else if (eventName === EVENTS.EPISODE_READY && room.activeLobby) {
if (!room.activeLobby.readyPeers.includes(mapping.peerId)) {
room.activeLobby.readyPeers.push(mapping.peerId);
}
} else if ((eventName === EVENTS.FORCE_SYNC_PREPARE || eventName === EVENTS.FORCE_SYNC_EXECUTE) && room.activeLobby) {
room.activeLobby = null;
}
}
}
} catch (err) {