chore(extension): bump version, fix targeted injection and race conditions

This commit is contained in:
Timo
2026-04-23 21:58:19 +02:00
parent 0d08711398
commit 4785c9625c
5 changed files with 102 additions and 84 deletions
+54 -35
View File
@@ -30,8 +30,10 @@ function ensureState() {
chrome.storage.session.get([ chrome.storage.session.get([
'logs', 'history', 'currentRoom', 'lastActionState', 'logs', 'history', 'currentRoom', 'lastActionState',
'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks', 'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks',
'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime' 'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'currentTabId', 'currentTabTitle'
], (data) => { ], (data) => {
if (data.currentTabId !== undefined) currentTabId = data.currentTabId;
if (data.currentTabTitle !== undefined) currentTabTitle = data.currentTabTitle;
// Merge data from storage with any early-arriving state // Merge data from storage with any early-arriving state
// New entries (added during boot) must stay at the top (index 0) // New entries (added during boot) must stay at the top (index 0)
if (data.logs) logs = [...logs, ...data.logs].slice(0, 50); if (data.logs) logs = [...logs, ...data.logs].slice(0, 50);
@@ -121,13 +123,12 @@ async function getPeerId() {
async function getSettings() { async function getSettings() {
return new Promise(resolve => { return new Promise(resolve => {
chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'targetTabId', 'username'], (data) => { chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username'], (data) => {
resolve({ resolve({
serverUrl: data.serverUrl || '', serverUrl: data.serverUrl || '',
useCustomServer: data.useCustomServer || false, useCustomServer: data.useCustomServer || false,
roomId: data.roomId || '', roomId: data.roomId || '',
password: data.password || '', password: data.password || '',
targetTabId: data.targetTabId || null,
username: data.username || '' username: data.username || ''
}); });
}); });
@@ -587,10 +588,6 @@ function updateLastAction(action, senderId, timestamp = Date.now()) {
} }
async function routeToContent(action, payload) { async function routeToContent(action, payload) {
const settings = await getSettings();
if (settings.targetTabId) {
currentTabId = settings.targetTabId;
}
if (!currentTabId) return; if (!currentTabId) return;
const tabId = parseInt(currentTabId); const tabId = parseInt(currentTabId);
@@ -819,20 +816,17 @@ async function handleAsyncMessage(message, sender, sendResponse) {
}; };
if (sender.tab) { if (sender.tab) {
getSettings().then(settings => { const senderTabId = sender.tab.id;
const savedTargetId = parseInt(settings.targetTabId);
const senderTabId = sender.tab.id; if (!currentTabId || currentTabId !== senderTabId) {
sendResponse({ status: 'ignored_unselected_tab' });
if (!savedTargetId || savedTargetId !== senderTabId) { return;
sendResponse({ status: 'ignored_unselected_tab' }); }
return;
} currentTabTitle = sender.tab.title ? sender.tab.title.substring(0, 50) : null;
chrome.storage.session.set({ currentTabTitle });
currentTabId = senderTabId; updateBadgeStatus();
currentTabTitle = sender.tab.title ? sender.tab.title.substring(0, 50) : null; processEvent();
updateBadgeStatus();
processEvent();
});
} else { } else {
routeToContent(message.action, message.payload); routeToContent(message.action, message.payload);
processEvent(); processEvent();
@@ -861,21 +855,20 @@ async function handleAsyncMessage(message, sender, sendResponse) {
} }
sendResponse({ status: 'ok' }); sendResponse({ status: 'ok' });
} else if (message.type === 'HEARTBEAT') { } else if (message.type === 'HEARTBEAT') {
getSettings().then(settings => { if (sender.tab) {
if (sender.tab) { const senderTabId = sender.tab.id;
const savedTargetId = parseInt(settings.targetTabId);
const senderTabId = sender.tab.id; if (!currentTabId || currentTabId !== senderTabId) {
sendResponse({ status: 'ignored_unselected_tab' });
if (!savedTargetId || savedTargetId !== senderTabId) { return;
sendResponse({ status: 'ignored_unselected_tab' });
return;
}
currentTabId = senderTabId;
currentTabTitle = sender.tab.title ? sender.tab.title.substring(0, 50) : null;
updateBadgeStatus();
} }
currentTabTitle = sender.tab.title ? sender.tab.title.substring(0, 50) : null;
chrome.storage.session.set({ currentTabTitle });
updateBadgeStatus();
}
getSettings().then(settings => {
const statusPayload = { ...message.payload, peerId, username: settings.username, tabTitle: currentTabTitle }; const statusPayload = { ...message.payload, peerId, username: settings.username, tabTitle: currentTabTitle };
emit(EVENTS.PEER_STATUS, statusPayload); emit(EVENTS.PEER_STATUS, statusPayload);
@@ -890,6 +883,22 @@ async function handleAsyncMessage(message, sender, sendResponse) {
} }
sendResponse({ status: 'ok' }); sendResponse({ status: 'ok' });
}); });
} else if (message.type === 'SET_TARGET_TAB') {
currentTabId = message.tabId;
currentTabTitle = message.tabTitle;
chrome.storage.session.set({ currentTabId, currentTabTitle });
updateBadgeStatus();
if (currentTabId) {
chrome.scripting.executeScript({
target: { tabId: currentTabId },
files: ['content.js']
}).catch(err => {
addLog(`Failed to inject into tab: ${err.message}`, 'warn');
});
}
sendResponse({ status: 'ok' });
} else if (message.type === 'LOG') { } else if (message.type === 'LOG') {
addLog(`[Content] ${message.message}`, message.level || 'info'); addLog(`[Content] ${message.message}`, message.level || 'info');
sendResponse({ status: 'ok' }); sendResponse({ status: 'ok' });
@@ -904,11 +913,21 @@ chrome.tabs.onRemoved.addListener((tabId) => {
if (tabId === currentTabId) { if (tabId === currentTabId) {
currentTabId = null; currentTabId = null;
currentTabTitle = null; currentTabTitle = null;
chrome.storage.sync.set({ targetTabId: null }); chrome.storage.session.set({ currentTabId: null, currentTabTitle: null });
updateBadgeStatus(); updateBadgeStatus();
addLog('Target tab closed.', 'warn'); addLog('Target tab closed.', 'warn');
} }
}); });
// Re-inject on full page refresh
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (currentTabId && tabId === parseInt(currentTabId) && changeInfo.status === 'complete') {
chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
}).catch(() => {});
}
});
// Initial Connect // Initial Connect
connect(); connect();
+22 -24
View File
@@ -25,19 +25,16 @@
PEER_STATUS: "peer_status" PEER_STATUS: "peer_status"
}; };
let lastTargetState = null; let expectedEvents = new Set();
let targetStateTimeout = null; let expectedTimeouts = {};
function setTargetState(state) { function expectEvent(state) {
lastTargetState = state; expectedEvents.add(state);
if (targetStateTimeout) clearTimeout(targetStateTimeout); if (expectedTimeouts[state]) clearTimeout(expectedTimeouts[state]);
if (state !== null) { const timeout = state === 'seek' ? 10000 : 1500;
// Seek events might take longer than play/pause, using 2s for safety expectedTimeouts[state] = setTimeout(() => {
const timeout = state === 'seek' ? 2000 : 1500; expectedEvents.delete(state);
targetStateTimeout = setTimeout(() => { }, timeout);
lastTargetState = null;
}, timeout);
}
} }
function reportLog(message, level = 'info') { function reportLog(message, level = 'info') {
@@ -74,11 +71,11 @@
if (ytButton) { if (ytButton) {
const isCurrentlyPlaying = !video.paused; const isCurrentlyPlaying = !video.paused;
if ((action === EVENTS.PLAY && !isCurrentlyPlaying) || (action === EVENTS.PAUSE && isCurrentlyPlaying)) { if ((action === EVENTS.PLAY && !isCurrentlyPlaying) || (action === EVENTS.PAUSE && isCurrentlyPlaying)) {
setTargetState(action === EVENTS.PLAY ? 'playing' : 'paused'); expectEvent(action === EVENTS.PLAY ? 'playing' : 'paused');
ytButton.click(); ytButton.click();
} }
if (action === EVENTS.SEEK) { if (action === EVENTS.SEEK) {
setTargetState('seek'); expectEvent('seek');
video.currentTime = data.targetTime; video.currentTime = data.targetTime;
} }
return; return;
@@ -90,11 +87,11 @@
if (twitchButton) { if (twitchButton) {
const isCurrentlyPlaying = !video.paused; const isCurrentlyPlaying = !video.paused;
if ((action === EVENTS.PLAY && !isCurrentlyPlaying) || (action === EVENTS.PAUSE && isCurrentlyPlaying)) { if ((action === EVENTS.PLAY && !isCurrentlyPlaying) || (action === EVENTS.PAUSE && isCurrentlyPlaying)) {
setTargetState(action === EVENTS.PLAY ? 'playing' : 'paused'); expectEvent(action === EVENTS.PLAY ? 'playing' : 'paused');
twitchButton.click(); twitchButton.click();
} }
if (action === EVENTS.SEEK) { if (action === EVENTS.SEEK) {
setTargetState('seek'); expectEvent('seek');
video.currentTime = data.targetTime; video.currentTime = data.targetTime;
} }
return; return;
@@ -103,16 +100,16 @@
// Fallback for native HTML5 // Fallback for native HTML5
if (action === EVENTS.PLAY) { if (action === EVENTS.PLAY) {
setTargetState('playing'); expectEvent('playing');
video.play().catch((e) => { video.play().catch((e) => {
reportLog(`Playback prevented: ${e.message}`, 'warn'); reportLog(`Playback prevented: ${e.message}`, 'warn');
setTargetState(null); expectedEvents.delete('playing');
}); });
} else if (action === EVENTS.PAUSE) { } else if (action === EVENTS.PAUSE) {
setTargetState('paused'); expectEvent('paused');
video.pause(); video.pause();
} else if (action === EVENTS.SEEK) { } else if (action === EVENTS.SEEK) {
setTargetState('seek'); expectEvent('seek');
video.currentTime = data.targetTime; video.currentTime = data.targetTime;
} }
} catch (e) { } catch (e) {
@@ -135,7 +132,7 @@
elapsed += interval; elapsed += interval;
const timeDiff = Math.abs(video.currentTime - targetTime); const timeDiff = Math.abs(video.currentTime - targetTime);
const ready = video.readyState >= 3 && timeDiff < 1.0; const ready = video.readyState >= 3 && timeDiff < 2.0;
if (ready) { if (ready) {
clearInterval(timer); clearInterval(timer);
resolve(true); resolve(true);
@@ -175,7 +172,8 @@
reportLog(`Media Action Error: Invalid force sync payload - ${JSON.stringify(payload)}`, 'error'); reportLog(`Media Action Error: Invalid force sync payload - ${JSON.stringify(payload)}`, 'error');
return; return;
} }
setTargetState('paused'); expectEvent('paused');
expectEvent('seek');
video.pause(); video.pause();
video.currentTime = payload.targetTime; video.currentTime = payload.targetTime;
pollSeekReady(payload.targetTime).then((ready) => { pollSeekReady(payload.targetTime).then((ready) => {
@@ -240,8 +238,8 @@
const eventState = action === EVENTS.PLAY ? 'playing' : (action === EVENTS.PAUSE ? 'paused' : (action === EVENTS.SEEK ? 'seek' : null)); const eventState = action === EVENTS.PLAY ? 'playing' : (action === EVENTS.PAUSE ? 'paused' : (action === EVENTS.SEEK ? 'seek' : null));
if (eventState && lastTargetState === eventState) { if (eventState && expectedEvents.has(eventState)) {
setTargetState(null); // Consume the match expectedEvents.delete(eventState); // Consume the match
return; // Ignore event caused by our programmatic action return; // Ignore event caused by our programmatic action
} }
+1 -11
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "KoalaSync", "name": "KoalaSync",
"version": "1.1.1", "version": "1.1.2",
"description": "Synchronize video playback across different tabs and users.", "description": "Synchronize video playback across different tabs and users.",
"permissions": [ "permissions": [
"storage", "storage",
@@ -27,16 +27,6 @@
"type": "module" "type": "module"
}, },
"content_scripts": [ "content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
],
"run_at": "document_idle",
"all_frames": false
},
{ {
"matches": ["https://koalasync.shik3i.net/*"], "matches": ["https://koalasync.shik3i.net/*"],
"js": ["bridge.js"], "js": ["bridge.js"],
+24 -13
View File
@@ -49,7 +49,7 @@ let lastPeersJson = null;
// --- Initialization --- // --- Initialization ---
async function init() { async function init() {
// Load Settings // Load Settings
const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'targetTabId', 'filterNoise', 'username']); const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username']);
elements.serverUrl.value = data.serverUrl || ''; elements.serverUrl.value = data.serverUrl || '';
elements.roomId.value = data.roomId || ''; elements.roomId.value = data.roomId || '';
elements.password.value = data.password || ''; elements.password.value = data.password || '';
@@ -62,21 +62,23 @@ async function init() {
setServerMode(false); setServerMode(false);
} }
// Populate Tabs
await populateTabs();
toggleUIState(!!data.roomId); toggleUIState(!!data.roomId);
updateUI(data.roomId, data.password, data.useCustomServer, data.serverUrl); updateUI(data.roomId, data.password, data.useCustomServer, data.serverUrl);
refreshLogs(); refreshLogs();
refreshHistory(); refreshHistory();
// Initial Status Check // Initial Status Check
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => { chrome.runtime.sendMessage({ type: 'GET_STATUS' }, async (res) => {
if (res) { if (res) {
localPeerId = res.peerId; localPeerId = res.peerId;
applyConnectionStatus(res.status); applyConnectionStatus(res.status);
updatePeerList(res.peers); updatePeerList(res.peers);
if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers); if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers);
// Populate Tabs using the background's targetTabId
await populateTabs(res.peers, res.targetTabId);
} else {
await populateTabs();
} }
}); });
@@ -267,10 +269,16 @@ function updatePeerList(peers) {
populateTabs(peers); populateTabs(peers);
} }
async function populateTabs(providedPeers = null) { async function populateTabs(providedPeers = null, providedTargetTabId = null) {
const data = await chrome.storage.sync.get(['targetTabId', 'filterNoise']); const data = await chrome.storage.sync.get(['filterNoise']);
const isFilterActive = data.filterNoise !== false; const isFilterActive = data.filterNoise !== false;
const currentTargetTabId = data.targetTabId;
// Fallback if not provided directly
let currentTargetTabId = providedTargetTabId;
if (currentTargetTabId === null) {
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
currentTargetTabId = status?.targetTabId;
}
// Use provided peers or fetch if missing // Use provided peers or fetch if missing
let peerIds = providedPeers; let peerIds = providedPeers;
@@ -646,15 +654,18 @@ elements.retryBtn.addEventListener('click', () => {
chrome.runtime.sendMessage({ type: 'RETRY_CONNECT' }); chrome.runtime.sendMessage({ type: 'RETRY_CONNECT' });
}); });
elements.targetTab.addEventListener('change', async () => { elements.targetTab.addEventListener('change', () => {
await chrome.storage.sync.set({ targetTabId: elements.targetTab.value }); const val = elements.targetTab.value;
const tabId = val ? parseInt(val) : null;
const tabTitle = elements.targetTab.options[elements.targetTab.selectedIndex]?.text.replace('⭐ MATCH: ', '') || null;
chrome.runtime.sendMessage({ type: 'SET_TARGET_TAB', tabId, tabTitle });
}); });
elements.forceSyncBtn.addEventListener('click', async () => { elements.forceSyncBtn.addEventListener('click', async () => {
if (elements.forceSyncBtn.disabled) return; if (elements.forceSyncBtn.disabled) return;
const settings = await chrome.storage.sync.get(['targetTabId']); const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
if (!settings.targetTabId) return; if (!status || !status.targetTabId) return;
// Lockout to prevent spamming // Lockout to prevent spamming
const originalText = elements.forceSyncBtn.textContent; const originalText = elements.forceSyncBtn.textContent;
@@ -665,7 +676,7 @@ elements.forceSyncBtn.addEventListener('click', async () => {
elements.forceSyncBtn.textContent = originalText; elements.forceSyncBtn.textContent = originalText;
}, 5000); }, 5000);
const tabId = parseInt(settings.targetTabId); const tabId = parseInt(status.targetTabId);
const sendForceSync = (time) => { const sendForceSync = (time) => {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
+1 -1
View File
@@ -3,7 +3,7 @@
*/ */
export const PROTOCOL_VERSION = "1.0.0"; export const PROTOCOL_VERSION = "1.0.0";
export const APP_VERSION = "1.1.1"; export const APP_VERSION = "1.1.2";
export const OFFICIAL_SERVER_URL = 'wss://sync.shik3i.net'; export const OFFICIAL_SERVER_URL = 'wss://sync.shik3i.net';
export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.shik3i.net'; export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.shik3i.net';