From 830f8c44b1ba63802677929f4c9dc27eac78e305 Mon Sep 17 00:00:00 2001 From: Timo <6156589+Shik3i@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:51:29 +0200 Subject: [PATCH] fix: implement background heartbeats and aggressive server-side peer pruning --- extension/background.js | 25 +++++++++++++++++ server/index.js | 60 +++++++++++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/extension/background.js b/extension/background.js index 734d556..30fd75e 100644 --- a/extension/background.js +++ b/extension/background.js @@ -7,6 +7,8 @@ const MAX_RECONNECT_DELAY = 30000; let isConnecting = false; let peerId = null; // initialized via getPeerId() let currentRoom = null; +let lastPeersJson = null; +let heartbeatInterval = null; let currentTabId = null; let currentTabTitle = null; // New: for Smart Matching let logs = []; @@ -48,6 +50,24 @@ let forceSyncAcks = new Set(); let forceSyncTimeout = null; // --- Storage Utils --- +function startHeartbeat() { + stopHeartbeat(); + heartbeatInterval = setInterval(() => { + if (currentRoom) { + emit(EVENTS.PEER_STATUS, { peerId, status: 'heartbeat' }); + } else { + stopHeartbeat(); + } + }, 30000); +} + +function stopHeartbeat() { + if (heartbeatInterval) { + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } +} + async function getPeerId() { const data = await chrome.storage.local.get(['peerId']); if (data.peerId) return data.peerId; @@ -327,6 +347,10 @@ function handleServerEvent(event, data) { if (storageInitialized) chrome.storage.session.set({ currentRoom }); addLog(`Joined Room: ${data.roomId}`, 'success'); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: data.peers }).catch(() => {}); + + // Start background heartbeat + startHeartbeat(); + // Inform Website Bridge chrome.tabs.query({}, (tabs) => { tabs.forEach(tab => { @@ -507,6 +531,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } else if (message.type === 'LEAVE_ROOM') { emit(EVENTS.LEAVE_ROOM, { peerId }); currentRoom = null; + stopHeartbeat(); if (storageInitialized) chrome.storage.session.set({ currentRoom: null }); addLog('Left Room', 'info'); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {}); diff --git a/server/index.js b/server/index.js index ebbe94b..adf77b7 100644 --- a/server/index.js +++ b/server/index.js @@ -237,7 +237,12 @@ io.on('connection', (socket) => { socket.join(roomId); room.peers.add(socket.id); room.peerIds.set(socket.id, peerId); - room.peerData.set(socket.id, { peerId, username: username || null, tabTitle: null }); + room.peerData.set(socket.id, { + peerId, + username: username || null, + tabTitle: null, + lastSeen: Date.now() + }); socketToRoom.set(socket.id, { roomId, peerId }); socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, username: username || null, status: 'joined' }); @@ -274,17 +279,18 @@ io.on('connection', (socket) => { const room = rooms.get(mapping.roomId); if (room) { room.lastActivity = Date.now(); - // Update metadata if it's a peer_status (heartbeat) - if (eventName === EVENTS.PEER_STATUS && (data.tabTitle || data.username)) { - const existing = room.peerData.get(socket.id) || { peerId: mapping.peerId }; - room.peerData.set(socket.id, { - ...existing, - username: data.username || existing.username, - tabTitle: data.tabTitle || existing.tabTitle - }); - } + + // Update peer metadata and lastSeen + const existing = room.peerData.get(socket.id) || { peerId: mapping.peerId }; + room.peerData.set(socket.id, { + ...existing, + username: data.username || existing.username, + tabTitle: data.tabTitle || existing.tabTitle, + lastSeen: Date.now() + }); + + socket.to(mapping.roomId).emit(eventName, { ...data, senderId: mapping.peerId }); } - socket.to(mapping.roomId).emit(eventName, { ...data, senderId: mapping.peerId }); } }); }); @@ -339,17 +345,37 @@ io.on('connection', (socket) => { }); }); -// Inactive Room Cleanup (Every 30m) +// Active Room & Dead Peer Cleanup (Every 2m) setInterval(() => { - const cutoff = Date.now() - (2 * 60 * 60 * 1000); // 2 hours + const now = Date.now(); + const roomCutoff = now - (2 * 60 * 60 * 1000); // 2 hours + const peerCutoff = now - (5 * 60 * 1000); // 5 minutes + for (const [roomId, room] of rooms) { - if (room.lastActivity < cutoff) { - io.to(roomId).emit(EVENTS.ERROR, { message: 'Room closed due to inactivity' }); + // 1. Prune dead peers + for (const [sid, data] of room.peerData.entries()) { + if (data.lastSeen && data.lastSeen < peerCutoff) { + const socket = io.sockets.sockets.get(sid); + if (socket) socket.leave(roomId); + + room.peers.delete(sid); + room.peerIds.delete(sid); + room.peerData.delete(sid); + socketToRoom.delete(sid); + + io.to(roomId).emit(EVENTS.PEER_STATUS, { peerId: data.peerId, status: 'left' }); + log('CLEANUP', `Pruned dead peer ${data.peerId} from room ${roomId}`); + } + } + + // 2. Prune empty or inactive rooms + if (room.peers.size === 0 || room.lastActivity < roomCutoff) { + io.to(roomId).emit(EVENTS.ERROR, { message: 'Room closed' }); rooms.delete(roomId); - log('CLEANUP', `Deleted inactive room: ${roomId.substring(0, 3)}***`); + log('CLEANUP', `Deleted room ${roomId.substring(0, 3)}*** (Empty/Inactive)`); } } -}, 30 * 60 * 1000); +}, 2 * 60 * 1000); httpServer.listen(PORT, () => { log('SERVER', `KoalaSync Relay running on port ${PORT}`);