From 1eca3fa5bbba8590315a4a25c9098f6e9a641aad Mon Sep 17 00:00:00 2001 From: KoalaDev <6156589+Shik3i@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:18:09 +0200 Subject: [PATCH] fix(host-control-mode): guard against stale tab events, persist desync across SW restart - CONTENT_EVENT now rejected from non-selected tabs (popup exempted via sender.tab check) - HCM_DESYNC_STATE same guard - hcmDesynced persisted in session storage: restored on SW restart, cleared on teardown --- extension/background.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/extension/background.js b/extension/background.js index 7197e1a..e2123e5 100644 --- a/extension/background.js +++ b/extension/background.js @@ -140,7 +140,8 @@ function ensureState() { 'logs', 'history', 'currentRoom', 'lastActionState', 'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks', 'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'reconnectAttempts', 'currentTabId', 'currentTabTitle', - 'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount', 'roomIdleSince', 'lastContentHeartbeatAt' + 'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount', 'roomIdleSince', 'lastContentHeartbeatAt', + 'hcmDesynced' ], (data) => { clearTimeout(storageTimeout); if (data.expectedAcksCount !== undefined) expectedAcksCount = data.expectedAcksCount; @@ -156,6 +157,7 @@ function ensureState() { controlMode = currentRoom.controlMode || CONTROL_MODES.EVERYONE; hostPeerId = currentRoom.hostPeerId || null; } + if (data.hcmDesynced !== undefined) hcmDesynced = data.hcmDesynced; if (data.lastActionState) lastActionState = data.lastActionState; if (data.eventQueue) eventQueue = [...eventQueue, ...data.eventQueue].slice(0, 50); @@ -485,7 +487,8 @@ async function leaveRoomAfterIdleGrace(reason) { currentTabTitle: null, roomIdleSince: null, lastContentHeartbeatAt: null, - episodeLobby: null + episodeLobby: null, + hcmDesynced: false }).catch(() => {}); await chrome.storage.local.set({ roomId: '', password: '' }).catch(() => {}); addLog(reason, 'info'); @@ -1571,7 +1574,7 @@ function leaveOldRoomIfSwitching(newRoomId) { // Notify content.js/popup so they drop any guest-side HCM state from the // previous room (badge/dialog/desync) — H-2/H-3. broadcastControlMode(); - if (storageInitialized) chrome.storage.session.set({ currentRoom: null }); + if (storageInitialized) chrome.storage.session.set({ currentRoom: null, hcmDesynced: false }); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {}); // Reset force sync states @@ -1709,9 +1712,16 @@ async function handleAsyncMessage(message, sender, sendResponse) { sendResponse({ target: getHostSyncTarget() }); } else if (message.type === 'HCM_DESYNC_STATE') { // content.js tells us whether the local user chose to watch on their own. + // Only accept from the currently selected tab. + if (sender.tab && currentTabId && currentTabId !== sender.tab.id) { + sendResponse({ status: 'ignored_unselected_tab' }); + return; + } // Mirrored into heartbeats so the host's UI can show "Solo" instead of - // silently waiting for ACKs that will never come. + // silently waiting for ACKs that will never come. Persisted so the + // heartbeat survives SW restarts (idle timeout, crash). hcmDesynced = !!message.desynced; + if (storageInitialized) chrome.storage.session.set({ hcmDesynced }); sendResponse({ status: 'ok' }); } else if (message.type === 'LEAVE_ROOM') { connectIntent = false; @@ -1752,7 +1762,8 @@ async function handleAsyncMessage(message, sender, sendResponse) { forceSyncAcks: [], forceSyncDeadline: null, episodeLobby: null, - expectedAcksCount: 0 + expectedAcksCount: 0, + hcmDesynced: false }); chrome.storage.local.set({ roomId: '', password: '' }).catch(() => {}); addLog('Left Room', 'info'); @@ -1846,6 +1857,13 @@ async function handleAsyncMessage(message, sender, sendResponse) { } }); } else if (message.type === 'CONTENT_EVENT') { + // Only process media events from the currently selected tab — ignore + // stale content scripts in previously-selected tabs. Popup-originated + // events (no sender.tab) always pass through. + if (sender.tab && currentTabId && currentTabId !== sender.tab.id) { + sendResponse({ status: 'ignored_unselected_tab' }); + return; + } const processEvent = () => { // Host Control Mode (sender-side): a guest in host-only mode must not // drive the room. Don't broadcast; hand the action back to content.js so