diff --git a/extension/background.js b/extension/background.js index 214f38c..93012b4 100644 --- a/extension/background.js +++ b/extension/background.js @@ -11,11 +11,28 @@ let currentTabId = null; let currentTabTitle = null; // New: for Smart Matching let logs = []; let history = []; // New: for Action History +let storageInitialized = false; +let pendingLogs = []; +let pendingHistory = []; // Restore state from session storage chrome.storage.session.get(['logs', 'history'], (data) => { if (data.logs) logs = data.logs; if (data.history) history = data.history; + storageInitialized = true; + + if (pendingLogs.length > 0) { + logs.unshift(...pendingLogs); + if (logs.length > 50) logs = logs.slice(0, 50); + chrome.storage.session.set({ logs }); + pendingLogs = []; + } + if (pendingHistory.length > 0) { + history.unshift(...pendingHistory); + if (history.length > 20) history = history.slice(0, 20); + chrome.storage.session.set({ history }); + pendingHistory = []; + } }); let reconnectTimer = null; @@ -56,9 +73,13 @@ function addLog(message, type = 'info') { message, type }; - logs.unshift(log); - if (logs.length > 50) logs.pop(); - chrome.storage.session.set({ logs }); + if (!storageInitialized) { + pendingLogs.unshift(log); + } else { + logs.unshift(log); + if (logs.length > 50) logs.pop(); + chrome.storage.session.set({ logs }); + } chrome.runtime.sendMessage({ type: 'LOG_UPDATE', log }).catch(() => {}); } @@ -260,9 +281,13 @@ function addToHistory(action, senderId) { senderId: senderId || 'You', timestamp: new Date().toISOString() }; - history.unshift(historyEntry); - if (history.length > 20) history.pop(); - chrome.storage.session.set({ history }); + if (!storageInitialized) { + pendingHistory.unshift(historyEntry); + } else { + history.unshift(historyEntry); + if (history.length > 20) history.pop(); + chrome.storage.session.set({ history }); + } chrome.runtime.sendMessage({ type: 'HISTORY_UPDATE', history }).catch(() => {}); } diff --git a/extension/content.js b/extension/content.js index e987282..46a6873 100644 --- a/extension/content.js +++ b/extension/content.js @@ -7,7 +7,7 @@ if (window.koalaSyncInjected) return; window.koalaSyncInjected = true; - let isProcessingCommand = false; + let processingCommandUntil = 0; // --- Helper: find the best video element on the page --- function findVideo() { @@ -20,7 +20,7 @@ const video = findVideo(); if (!video) return; - isProcessingCommand = true; + processingCommandUntil = Date.now() + 1000; try { const host = window.location.hostname.toLowerCase(); const isYouTube = host.includes('youtube.com'); @@ -60,9 +60,6 @@ } } catch (e) { console.error('KoalaSync Media Action Error:', e); - } finally { - // Guarantee reset even on early returns in YouTube/Twitch blocks - setTimeout(() => { isProcessingCommand = false; }, 1000); } } @@ -91,6 +88,12 @@ // Listen for commands from background.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'get_current_time') { + const video = findVideo(); + sendResponse({ currentTime: video ? video.currentTime : undefined }); + return true; + } + if (message.type === 'SERVER_COMMAND') { const { action, payload } = message; @@ -101,17 +104,17 @@ } else if (action === 'seek') { tryMediaAction('seek', payload); } else if (action === 'force_sync_prepare') { - isProcessingCommand = true; + processingCommandUntil = Date.now() + 10000; const video = findVideo(); if (video) { video.pause(); video.currentTime = payload.targetTime; pollSeekReady(payload.targetTime).then(() => { chrome.runtime.sendMessage({ type: 'FORCE_SYNC_ACK' }); - setTimeout(() => { isProcessingCommand = false; }, 1000); + processingCommandUntil = Date.now() + 1000; }); } else { - isProcessingCommand = false; + processingCommandUntil = 0; } } else if (action === 'force_sync_execute') { tryMediaAction('play'); @@ -121,7 +124,7 @@ // Detect native events function reportEvent(action) { - if (isProcessingCommand) return; + if (Date.now() < processingCommandUntil) return; const video = findVideo(); if (!video) return; @@ -147,15 +150,24 @@ // SPA Navigation Handler (MutationObserver) let lastMutate = 0; + let observerTimeout = null; + + function checkVideo() { + lastMutate = Date.now(); + const video = findVideo(); + if (video && !video.dataset.koalaAttached) { + console.log('KoalaSync: New video detected via navigation.'); + setupListeners(); + } + } + const observer = new MutationObserver(() => { const now = Date.now(); if (now - lastMutate >= 1000) { - lastMutate = now; - const video = findVideo(); - if (video && !video.dataset.koalaAttached) { - console.log('KoalaSync: New video detected via navigation.'); - setupListeners(); - } + checkVideo(); + } else { + if (observerTimeout) clearTimeout(observerTimeout); + observerTimeout = setTimeout(checkVideo, 1000 - (now - lastMutate)); } }); observer.observe(document.body, { childList: true, subtree: true }); diff --git a/extension/popup.js b/extension/popup.js index 9e5f3f8..33cb05e 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -337,12 +337,15 @@ function showError(msg) { elements.password.style.borderColor = 'var(--error)'; // Shake effect - document.getElementById('tab-room').animate([ - { transform: 'translateX(0)' }, - { transform: 'translateX(-5px)' }, - { transform: 'translateX(5px)' }, - { transform: 'translateX(0)' } - ], { duration: 200, iterations: 2 }); + const activeTab = document.querySelector('.tab-content.active'); + if (activeTab) { + activeTab.animate([ + { transform: 'translateX(0)' }, + { transform: 'translateX(-5px)' }, + { transform: 'translateX(5px)' }, + { transform: 'translateX(0)' } + ], { duration: 200, iterations: 2 }); + } setTimeout(() => { if (elements.roomError) elements.roomError.style.display = 'none'; diff --git a/server/index.js b/server/index.js index ba70d36..fbeb5f6 100644 --- a/server/index.js +++ b/server/index.js @@ -84,6 +84,16 @@ setInterval(() => { const eventCounts = new Map(); // socketId -> { count, resetTime } +// Clean up connection counts to prevent memory leak +setInterval(() => { + const now = Date.now(); + for (const [ip, entry] of connectionCounts.entries()) { + if (now > entry.resetTime) { + connectionCounts.delete(ip); + } + } +}, 60000); + function checkConnectionRate(ip) { const now = Date.now(); const entry = connectionCounts.get(ip) || { count: 0, resetTime: now + 60000 }; @@ -150,6 +160,9 @@ io.on('connection', (socket) => { // Cleanup old room if re-joining const oldMapping = socketToRoom.get(socket.id); + if (oldMapping && oldMapping.roomId === roomId) { + return; // Already in this room, ignore to prevent spam + } if (oldMapping && oldMapping.roomId !== roomId) { socket.leave(oldMapping.roomId); const oldRoom = rooms.get(oldMapping.roomId); @@ -232,6 +245,8 @@ io.on('connection', (socket) => { return; } + if (!data || typeof data !== 'object') return; // Prevent null/invalid payload crash + const mapping = socketToRoom.get(socket.id); if (mapping) { const room = rooms.get(mapping.roomId);