mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 20:18:14 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 385602c194 | |||
| bf0fa55b9d | |||
| f063fd5f3d | |||
| 0a555942f8 | |||
| d3f680e313 |
@@ -10,7 +10,7 @@ This guide walks through the complete user flow of KoalaSync, from creating a ro
|
||||
2. The extension adds a small icon to your browser toolbar.
|
||||
3. On first install, a unique 8-character **Peer ID** is generated locally and stored in `chrome.storage.local`. This ID is never sent to any external service — it only travels to the relay server when you join a room.
|
||||
|
||||
> **What's stored locally**: `peerId` (8-char hex), `username` (customizable), `serverUrl`, `filterNoise` preference. All stored via `chrome.storage.sync` and `chrome.storage.local`.
|
||||
> **What's stored locally**: `peerId` (8-char hex), `username` (customizable, defaults to a readable adjective-noun pair), `serverUrl`, `filterNoise` preference. All stored via `chrome.storage.sync` and `chrome.storage.local`.
|
||||
|
||||
---
|
||||
|
||||
@@ -109,7 +109,7 @@ Both users now need to select which browser tab contains the video to sync:
|
||||
4. Tabs with a **matching video title** are highlighted with a ⭐ prefix for easy identification.
|
||||
5. Selecting a tab causes `background.js` to set `currentTabId` and inject `content.js` into that tab via `chrome.scripting.executeScript`.
|
||||
|
||||
> **What `content.js` does on injection**: Finds the first `<video>` element on the page and attaches event listeners for `play`, `pause`, `seeked`, `timeupdate`, and `volumechange`. It uses an `expectedEvents` Set to distinguish between user actions and programmatic actions (loop prevention).
|
||||
> **What `content.js` does on injection**: Finds the first `<video>` element on the page and attaches event listeners for `play`, `pause`, `seeked`, and `loadeddata`. (Time and volume state are tracked via a 15-second heartbeat interval, not continuous event listeners). It uses an `expectedEvents` Set to distinguish between user actions and programmatic actions (loop prevention).
|
||||
|
||||
---
|
||||
|
||||
@@ -155,7 +155,7 @@ While in a room, two heartbeats keep the session alive:
|
||||
|
||||
| Heartbeat | Interval | Source | Purpose |
|
||||
|:----------|:---------|:-------|:--------|
|
||||
| **Background** | 30 seconds | `background.js` | Signals "I'm still connected" even without a video |
|
||||
| **Background** | 1 minute | `background.js` | Signals "I'm still connected" and handles 5-min auto-reconnect fallback |
|
||||
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, `mediaTitle`, `playbackState`, `volume`, `muted` |
|
||||
|
||||
- **Server Reaper**: Every 2 minutes, the server checks for peers with no activity for 5+ minutes and disconnects them ("dead peer pruning").
|
||||
|
||||
+43
-33
@@ -107,6 +107,7 @@ let reconnectStartTime = null; // New: track when reconnection started
|
||||
let reconnectFailed = false; // New: true if we hit the 5-min cap
|
||||
let slowReconnectTimer = null; // Infinite slow background reconnect timer
|
||||
let isSlowReconnectAttempt = false; // True during slow background reconnect execution
|
||||
let lastSlowReconnectAttempt = 0;
|
||||
|
||||
// Force Sync Coordination
|
||||
let isForceSyncInitiator = false;
|
||||
@@ -172,12 +173,19 @@ async function getPeerId() {
|
||||
async function getSettings() {
|
||||
return new Promise(resolve => {
|
||||
chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username'], (data) => {
|
||||
let username = data.username;
|
||||
if (!username) {
|
||||
const adjs = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave', 'Calm', 'Sneaky', 'Lazy', 'Wild', 'Chill', 'Lucky', 'Epic'];
|
||||
const nouns = ['Koala', 'Panda', 'Tiger', 'Eagle', 'Fox', 'Bear', 'Wolf', 'Lion', 'Hawk', 'Seal', 'Owl', 'Shark'];
|
||||
username = `${adjs[Math.floor(Math.random() * adjs.length)]}${nouns[Math.floor(Math.random() * nouns.length)]}`;
|
||||
chrome.storage.sync.set({ username });
|
||||
}
|
||||
resolve({
|
||||
serverUrl: data.serverUrl || '',
|
||||
useCustomServer: data.useCustomServer || false,
|
||||
roomId: data.roomId || '',
|
||||
password: data.password || '',
|
||||
username: data.username || ''
|
||||
username: username
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -240,8 +248,7 @@ async function connect() {
|
||||
|
||||
if (reconnectFailed && !isCurrentSlowRetry) {
|
||||
isConnecting = false;
|
||||
scheduleSlowReconnect(); // Keep checking in the background
|
||||
return;
|
||||
return; // Let keepAlive alarm handle the 5-min retry interval
|
||||
}
|
||||
|
||||
broadcastConnectionStatus('connecting');
|
||||
@@ -353,6 +360,9 @@ async function connect() {
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: null
|
||||
});
|
||||
|
||||
// Cancel any active episode lobby
|
||||
clearEpisodeLobbyState();
|
||||
|
||||
if (currentRoom) {
|
||||
currentRoom.peers = [];
|
||||
@@ -405,6 +415,7 @@ function showNotification(senderName, action) {
|
||||
const label = action === 'play' ? 'started playback' :
|
||||
action === 'pause' ? 'paused playback' :
|
||||
action === 'seek' ? 'seeked the video' :
|
||||
action === 'force_sync_prepare' ? 'started force sync' :
|
||||
action === 'force_sync_execute' ? 'synchronized everyone' : action;
|
||||
|
||||
// Find username in current room if available
|
||||
@@ -429,7 +440,6 @@ function scheduleReconnect() {
|
||||
isSlowReconnectAttempt = false;
|
||||
|
||||
if (reconnectFailed) {
|
||||
scheduleSlowReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -441,7 +451,6 @@ function scheduleReconnect() {
|
||||
chrome.storage.session.set({ reconnectFailed: true });
|
||||
addLog('Reconnection failed after 5 minutes. Entering slow background retry mode.', 'error');
|
||||
broadcastConnectionStatus('reconnect_failed');
|
||||
scheduleSlowReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -452,19 +461,7 @@ function scheduleReconnect() {
|
||||
}, reconnectDelay);
|
||||
}
|
||||
|
||||
function scheduleSlowReconnect() {
|
||||
if (slowReconnectTimer || socket || isConnecting) return;
|
||||
|
||||
slowReconnectTimer = setTimeout(async () => {
|
||||
slowReconnectTimer = null;
|
||||
await ensureState();
|
||||
if (reconnectFailed && !socket && !isConnecting) {
|
||||
addLog('Performing background reconnection attempt...', 'info');
|
||||
isSlowReconnectAttempt = true;
|
||||
connect();
|
||||
}
|
||||
}, 300000); // 5 minutes (300,000ms)
|
||||
}
|
||||
// Slow reconnect logic is now handled in the keepAlive alarm
|
||||
|
||||
function emit(event, data) {
|
||||
if (socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined) {
|
||||
@@ -589,17 +586,16 @@ function handleServerEvent(event, data) {
|
||||
addToHistory(event, data.senderId);
|
||||
showNotification(data.senderId, event);
|
||||
|
||||
// Force Sync Execute Remote Reactive Update
|
||||
updateLocalPeerState(data.senderId, {
|
||||
playbackState: 'playing'
|
||||
});
|
||||
// (The sender's state is updated below with everyone else)
|
||||
}
|
||||
|
||||
// Reset reactive update locks for all peers so the next playing heartbeat is accepted immediately
|
||||
// Force Sync Execute Remote Reactive Update:
|
||||
// Set all peers to playing and apply a reactive lock to block stale heartbeats
|
||||
if (currentRoom && Array.isArray(currentRoom.peers)) {
|
||||
currentRoom.peers.forEach(peer => {
|
||||
if (peer && typeof peer === 'object') {
|
||||
peer.lastReactiveUpdate = 0;
|
||||
peer.playbackState = 'playing';
|
||||
peer.lastReactiveUpdate = Date.now();
|
||||
}
|
||||
});
|
||||
if (storageInitialized) chrome.storage.session.set({ currentRoom });
|
||||
@@ -749,19 +745,23 @@ function executeForceSync() {
|
||||
forceSyncDeadline: null
|
||||
});
|
||||
|
||||
// Reset reactive update locks for all peers so the next playing heartbeat is accepted immediately
|
||||
// Set all peers to playing and apply a reactive lock to block stale heartbeats
|
||||
if (currentRoom && Array.isArray(currentRoom.peers)) {
|
||||
currentRoom.peers.forEach(peer => {
|
||||
if (peer && typeof peer === 'object') {
|
||||
peer.lastReactiveUpdate = 0;
|
||||
peer.playbackState = 'playing';
|
||||
peer.lastReactiveUpdate = Date.now();
|
||||
}
|
||||
});
|
||||
if (storageInitialized) chrome.storage.session.set({ currentRoom });
|
||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
|
||||
}
|
||||
|
||||
emit(EVENTS.FORCE_SYNC_EXECUTE, {});
|
||||
routeToContent(EVENTS.FORCE_SYNC_EXECUTE, {});
|
||||
const executionTimestamp = Date.now();
|
||||
updateLastAction(EVENTS.FORCE_SYNC_EXECUTE, 'You', executionTimestamp);
|
||||
|
||||
emit(EVENTS.FORCE_SYNC_EXECUTE, { actionTimestamp: executionTimestamp });
|
||||
routeToContent(EVENTS.FORCE_SYNC_EXECUTE, { actionTimestamp: executionTimestamp });
|
||||
addLog('Force Sync Executed', 'success');
|
||||
}
|
||||
|
||||
@@ -912,7 +912,16 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
|
||||
if (alarm.name === 'keepAlive') {
|
||||
chrome.storage.session.get('keepAlive', () => {});
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
connect();
|
||||
if (reconnectFailed) {
|
||||
if (Date.now() - lastSlowReconnectAttempt >= 300000) {
|
||||
lastSlowReconnectAttempt = Date.now();
|
||||
isSlowReconnectAttempt = true;
|
||||
addLog('Alarm triggered 5-min slow reconnect attempt', 'info');
|
||||
connect();
|
||||
}
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
} else if (currentRoom) {
|
||||
// Heartbeat Logic: Always include identity metadata
|
||||
const settings = await getSettings();
|
||||
@@ -949,6 +958,9 @@ function leaveOldRoomIfSwitching(newRoomId) {
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: null
|
||||
});
|
||||
|
||||
// Cancel any active episode lobby
|
||||
clearEpisodeLobbyState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,9 +1058,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
} else if (message.type === 'GET_HISTORY') {
|
||||
sendResponse(history);
|
||||
} else if (message.type === 'GET_ROOM_LIST') {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(`42${JSON.stringify([EVENTS.GET_ROOMS])}`);
|
||||
}
|
||||
emit(EVENTS.GET_ROOMS, {});
|
||||
sendResponse({ status: 'ok' });
|
||||
} else if (message.type === 'WEB_JOIN_REQUEST') {
|
||||
const { roomId, password, useCustomServer, serverUrl } = message;
|
||||
@@ -1256,7 +1266,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
|
||||
// Check setting
|
||||
const epSettings = await chrome.storage.sync.get(['autoSyncNextEpisode']);
|
||||
if (!epSettings.autoSyncNextEpisode) {
|
||||
if (epSettings.autoSyncNextEpisode === false) {
|
||||
addLog(`Episode change detected ("${newTitle}") but Auto-Sync is disabled.`, 'info');
|
||||
sendResponse({ status: 'disabled' });
|
||||
return;
|
||||
|
||||
@@ -519,7 +519,7 @@
|
||||
observerTimeout = setTimeout(checkVideo, 1000 - (now - lastMutate));
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||
|
||||
// --- SHARED_HEARTBEAT_INJECT_START ---
|
||||
const HEARTBEAT_INTERVAL_VAL = 15000;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "KoalaSync",
|
||||
"version": "1.5.3",
|
||||
"version": "1.6.0",
|
||||
"description": "Watch party extension to synchronize video playback on YouTube, Twitch, Netflix, and HTML5 sites in real-time with friends.",
|
||||
"permissions": [
|
||||
"storage",
|
||||
|
||||
@@ -289,7 +289,13 @@
|
||||
<button id="pauseBtn" class="primary" style="flex:1; background: var(--error);">⏸ Pause</button>
|
||||
</div>
|
||||
|
||||
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); width: 100%; margin-bottom: 15px;">⚡ Force Sync Everyone</button>
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 15px; align-items: stretch;">
|
||||
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); flex: 1;">⚡ Force Sync</button>
|
||||
<select id="forceSyncMode" style="width: auto; min-width: 130px; background: var(--card); border: 1px solid #334155; color: white; padding: 10px 8px; border-radius: 8px; font-size: 11px; font-family: inherit; cursor: pointer; align-self: stretch;" title="Choose sync target">
|
||||
<option value="jump-to-others">Jump to Others</option>
|
||||
<option value="jump-to-me">Jump to Me</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- NEW: Last Action Status Card -->
|
||||
<label>Last Activity Status</label>
|
||||
|
||||
+82
-29
@@ -8,6 +8,7 @@ const elements = {
|
||||
copyInvite: document.getElementById('copyInvite'),
|
||||
targetTab: document.getElementById('targetTab'),
|
||||
forceSyncBtn: document.getElementById('forceSyncBtn'),
|
||||
forceSyncMode: document.getElementById('forceSyncMode'),
|
||||
peerList: document.getElementById('peerList'),
|
||||
logList: document.getElementById('logList'),
|
||||
clearLogs: document.getElementById('clearLogs'),
|
||||
@@ -53,13 +54,22 @@ let lastPeersJson = null;
|
||||
// --- Initialization ---
|
||||
async function init() {
|
||||
// Load Settings
|
||||
const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username', 'autoSyncNextEpisode']);
|
||||
const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username', 'autoSyncNextEpisode', 'forceSyncMode']);
|
||||
let username = data.username;
|
||||
if (!username) {
|
||||
const adjs = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave', 'Calm', 'Sneaky', 'Lazy', 'Wild', 'Chill', 'Lucky', 'Epic'];
|
||||
const nouns = ['Koala', 'Panda', 'Tiger', 'Eagle', 'Fox', 'Bear', 'Wolf', 'Lion', 'Hawk', 'Seal', 'Owl', 'Shark'];
|
||||
username = `${adjs[Math.floor(Math.random() * adjs.length)]}${nouns[Math.floor(Math.random() * nouns.length)]}`;
|
||||
chrome.storage.sync.set({ username });
|
||||
}
|
||||
|
||||
elements.serverUrl.value = data.serverUrl || '';
|
||||
elements.roomId.value = data.roomId || '';
|
||||
elements.password.value = data.password || '';
|
||||
elements.username.value = data.username || '';
|
||||
elements.username.value = username;
|
||||
elements.filterNoise.checked = data.filterNoise !== false;
|
||||
elements.autoSyncNextEpisode.checked = !!data.autoSyncNextEpisode;
|
||||
elements.autoSyncNextEpisode.checked = data.autoSyncNextEpisode !== false;
|
||||
elements.forceSyncMode.value = data.forceSyncMode || 'jump-to-others';
|
||||
|
||||
// Set Version Info
|
||||
const versionEl = document.getElementById('appVersion');
|
||||
@@ -221,6 +231,13 @@ function getVolumeIcon(volume, muted) {
|
||||
let activePeers = [];
|
||||
let interpolationInterval = null;
|
||||
|
||||
function stopInterpolation() {
|
||||
if (interpolationInterval) {
|
||||
clearInterval(interpolationInterval);
|
||||
interpolationInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startInterpolation() {
|
||||
if (interpolationInterval) return;
|
||||
interpolationInterval = setInterval(() => {
|
||||
@@ -239,7 +256,11 @@ function startInterpolation() {
|
||||
function updatePeerList(peers) {
|
||||
if (!peers) return;
|
||||
activePeers = peers;
|
||||
if (!interpolationInterval) startInterpolation();
|
||||
if (peers.length === 0) {
|
||||
stopInterpolation();
|
||||
} else if (!interpolationInterval) {
|
||||
startInterpolation();
|
||||
}
|
||||
|
||||
// UI Throttle: Only re-render if the peer state actually changed (excluding time interpolation)
|
||||
const stateToHash = peers.map(p => ({
|
||||
@@ -475,7 +496,7 @@ function applyConnectionStatus(status) {
|
||||
// Preserve icons for Remote Control buttons
|
||||
elements.playBtn.textContent = '▶ Play';
|
||||
elements.pauseBtn.textContent = '⏸ Pause';
|
||||
elements.forceSyncBtn.textContent = '⚡ Force Sync Everyone';
|
||||
elements.forceSyncBtn.textContent = '⚡ Force Sync';
|
||||
}
|
||||
|
||||
function updateHistory(history) {
|
||||
@@ -642,6 +663,10 @@ elements.autoSyncNextEpisode.addEventListener('change', () => {
|
||||
chrome.storage.sync.set({ autoSyncNextEpisode: elements.autoSyncNextEpisode.checked });
|
||||
});
|
||||
|
||||
elements.forceSyncMode.addEventListener('change', () => {
|
||||
chrome.storage.sync.set({ forceSyncMode: elements.forceSyncMode.value });
|
||||
});
|
||||
|
||||
elements.serverUrl.addEventListener('input', () => {
|
||||
chrome.storage.sync.set({ serverUrl: elements.serverUrl.value });
|
||||
});
|
||||
@@ -744,7 +769,9 @@ elements.createRoomBtn.addEventListener('click', () => {
|
||||
const animals = ['koala', 'panda', 'tiger', 'eagle', 'fox', 'bear'];
|
||||
const adj = ['happy', 'cool', 'fast', 'smart', 'brave', 'calm'];
|
||||
const id = `${adj[Math.floor(Math.random() * adj.length)]}-${animals[Math.floor(Math.random() * animals.length)]}-${Math.floor(Math.random() * 100)}`;
|
||||
const pass = Math.random().toString(36).substring(2, 8);
|
||||
const array = new Uint32Array(1);
|
||||
window.crypto.getRandomValues(array);
|
||||
const pass = array[0].toString(36).substring(0, 6);
|
||||
|
||||
elements.roomId.value = id;
|
||||
elements.password.value = pass;
|
||||
@@ -773,10 +800,32 @@ elements.forceSyncBtn.addEventListener('click', async () => {
|
||||
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
|
||||
if (!status || !status.targetTabId) return;
|
||||
|
||||
// Lockout to prevent spamming
|
||||
const mode = elements.forceSyncMode.value;
|
||||
let targetTime = null;
|
||||
|
||||
if (mode === 'jump-to-others') {
|
||||
if (!localPeerId) {
|
||||
showError('Identity not yet loaded. Wait a moment and try again.');
|
||||
return;
|
||||
}
|
||||
const peers = status.peers || [];
|
||||
const otherTimes = peers
|
||||
.filter(p => typeof p === 'object' && p.peerId !== localPeerId && p.currentTime != null && !isNaN(p.currentTime))
|
||||
.map(p => p.currentTime);
|
||||
|
||||
if (otherTimes.length === 0) {
|
||||
showError('No other peers with a known time. Switch to "Jump to Me".');
|
||||
return;
|
||||
}
|
||||
|
||||
otherTimes.sort((a, b) => a - b);
|
||||
const mid = Math.floor(otherTimes.length / 2);
|
||||
targetTime = otherTimes.length % 2 !== 0 ? otherTimes[mid] : (otherTimes[mid - 1] + otherTimes[mid]) / 2;
|
||||
}
|
||||
|
||||
const originalText = elements.forceSyncBtn.textContent;
|
||||
elements.forceSyncBtn.disabled = true;
|
||||
elements.forceSyncBtn.textContent = 'Syncing...';
|
||||
elements.forceSyncBtn.textContent = mode === 'jump-to-others' ? `Syncing to group (${formatTime(targetTime)})...` : 'Syncing...';
|
||||
setTimeout(() => {
|
||||
elements.forceSyncBtn.disabled = false;
|
||||
elements.forceSyncBtn.textContent = originalText;
|
||||
@@ -792,26 +841,30 @@ elements.forceSyncBtn.addEventListener('click', async () => {
|
||||
});
|
||||
};
|
||||
|
||||
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (response) => {
|
||||
if (chrome.runtime.lastError || !response || response.currentTime === undefined) {
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['content.js']
|
||||
}).then(() => {
|
||||
setTimeout(() => {
|
||||
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (retryResponse) => {
|
||||
if (retryResponse && retryResponse.currentTime !== undefined) {
|
||||
sendForceSync(retryResponse.currentTime);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}).catch(() => {
|
||||
showError('Could not connect to video tab.');
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendForceSync(response.currentTime);
|
||||
});
|
||||
if (mode === 'jump-to-me') {
|
||||
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (response) => {
|
||||
if (chrome.runtime.lastError || !response || response.currentTime === undefined) {
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['content.js']
|
||||
}).then(() => {
|
||||
setTimeout(() => {
|
||||
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (retryResponse) => {
|
||||
if (retryResponse && retryResponse.currentTime !== undefined) {
|
||||
sendForceSync(retryResponse.currentTime);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}).catch(() => {
|
||||
showError('Could not connect to video tab.');
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendForceSync(response.currentTime);
|
||||
});
|
||||
} else {
|
||||
sendForceSync(targetTime);
|
||||
}
|
||||
});
|
||||
|
||||
elements.playBtn.addEventListener('click', () => {
|
||||
@@ -850,7 +903,7 @@ async function refreshLogs() {
|
||||
logs.forEach(log => {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry log-${log.type}`;
|
||||
const timeStr = log.timestamp.split('T')[1].split('.')[0];
|
||||
const timeStr = log.timestamp?.split('T')?.[1]?.split('.')[0] || '?';
|
||||
entry.textContent = `[${timeStr}] ${log.message}`;
|
||||
elements.logList.appendChild(entry);
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "1.5.3",
|
||||
"version": "1.6.0",
|
||||
"description": "KoalaSync Build Scripts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
+21
-15
@@ -3,7 +3,7 @@ import { createServer } from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import dotenv from 'dotenv';
|
||||
import { EVENTS, OFFICIAL_SERVER_TOKEN } from '../shared/constants.js';
|
||||
import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION } from '../shared/constants.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -125,12 +125,8 @@ function checkEventRate(socketId) {
|
||||
* @param {string} socketId - The socket.id being removed.
|
||||
* @param {string} roomId - The room it belongs to.
|
||||
* @param {string} reason - Log label ('disconnect', 'leave', 'reaper', 'dedupe', 'room-switch').
|
||||
* @param {boolean} [emitLeave=true] - Set false when the socket.io room leave
|
||||
* is handled by the caller (e.g. reaper calls
|
||||
* socket.leave() before us, or dedupe calls
|
||||
* oldSocket.leave() before disconnecting).
|
||||
*/
|
||||
function removePeerFromRoom(socketId, roomId, reason, emitLeave = true) {
|
||||
function removePeerFromRoom(socketId, roomId, reason) {
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) return;
|
||||
|
||||
@@ -225,7 +221,7 @@ io.on('connection', (socket) => {
|
||||
|
||||
try {
|
||||
// Protocol check
|
||||
if (protocolVersion !== '1.0.0') {
|
||||
if (protocolVersion !== PROTOCOL_VERSION) {
|
||||
log('AUTH', `Protocol mismatch from ${peerId}: ${protocolVersion}`);
|
||||
socket.emit(EVENTS.ERROR, { message: 'Incompatible protocol version' });
|
||||
return;
|
||||
@@ -279,18 +275,23 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
|
||||
// Peer Deduplication: Remove existing socket for the same peerId
|
||||
// Snapshot stale SIDs first to avoid mutating the Map during iteration
|
||||
const dedupeSids = [];
|
||||
for (const [sid, data] of room.peerData.entries()) {
|
||||
if (data.peerId === peerId && sid !== socket.id) {
|
||||
const oldSocket = io.sockets.sockets.get(sid);
|
||||
if (oldSocket) {
|
||||
oldSocket.emit(EVENTS.ERROR, { message: 'Deduplication: Another session with this ID joined. Disconnecting...' });
|
||||
oldSocket.leave(roomId);
|
||||
oldSocket.disconnect(true);
|
||||
log('DEDUPE', `Kicked old session for peer ${peerId}`);
|
||||
}
|
||||
removePeerFromRoom(sid, roomId, 'dedupe');
|
||||
dedupeSids.push(sid);
|
||||
}
|
||||
}
|
||||
for (const sid of dedupeSids) {
|
||||
const oldSocket = io.sockets.sockets.get(sid);
|
||||
if (oldSocket) {
|
||||
oldSocket.emit(EVENTS.ERROR, { message: 'Deduplication: Another session with this ID joined. Disconnecting...' });
|
||||
oldSocket.leave(roomId);
|
||||
oldSocket.disconnect(true);
|
||||
log('DEDUPE', `Kicked old session for peer ${peerId}`);
|
||||
}
|
||||
removePeerFromRoom(sid, roomId, 'dedupe');
|
||||
}
|
||||
}
|
||||
|
||||
socket.join(roomId);
|
||||
@@ -404,6 +405,11 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
|
||||
socket.on(EVENTS.EVENT_ACK, (data) => {
|
||||
if (!checkEventRate(socket.id)) {
|
||||
log('SECURITY', `Event rate limit exceeded for socket (ACK): ${socket.id}`);
|
||||
socket.disconnect(true);
|
||||
return;
|
||||
}
|
||||
if (!data || typeof data !== 'object') return;
|
||||
if (typeof data.targetId !== 'string') return;
|
||||
if (data.actionTimestamp !== undefined && (typeof data.actionTimestamp !== 'number' || !Number.isFinite(data.actionTimestamp))) return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "1.5.3",
|
||||
"date": "2026-05-18T18:10:54Z"
|
||||
"version": "1.6.0",
|
||||
"date": "2026-05-22T22:19:11Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user