mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-27 11:07:07 +00:00
style: restore classic remote control UI and sync architectural fixes
This commit is contained in:
+310
-177
@@ -21,27 +21,73 @@ let isNamespaceJoined = false;
|
||||
let lastActionState = { action: null, senderId: null, timestamp: 0, acks: [] };
|
||||
let currentCommandSenderId = null; // Track who sent the last command we are executing
|
||||
|
||||
// Restore state from session storage
|
||||
chrome.storage.session.get(['logs', 'history', 'currentRoom', 'lastActionState'], (data) => {
|
||||
if (data.logs) logs = data.logs;
|
||||
if (data.history) history = data.history;
|
||||
if (data.currentRoom) currentRoom = data.currentRoom;
|
||||
if (data.lastActionState) lastActionState = data.lastActionState;
|
||||
storageInitialized = true;
|
||||
|
||||
if (pendingLogs.length > 0) {
|
||||
logs.unshift(...pendingLogs);
|
||||
if (logs.length > 50) logs = logs.slice(0, 50);
|
||||
chrome.storage.session.set({ logs });
|
||||
pendingLogs = [];
|
||||
// --- Boot Sequence Lock ---
|
||||
let restorationTask = null;
|
||||
|
||||
function ensureState() {
|
||||
if (!restorationTask) {
|
||||
restorationTask = new Promise(resolve => {
|
||||
chrome.storage.session.get([
|
||||
'logs', 'history', 'currentRoom', 'lastActionState',
|
||||
'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks',
|
||||
'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime'
|
||||
], (data) => {
|
||||
// Merge data from storage with any early-arriving state
|
||||
// New entries (added during boot) must stay at the top (index 0)
|
||||
if (data.logs) logs = [...logs, ...data.logs].slice(0, 50);
|
||||
if (data.history) history = [...history, ...data.history].slice(0, 20);
|
||||
if (data.currentRoom) currentRoom = data.currentRoom;
|
||||
if (data.lastActionState) lastActionState = data.lastActionState;
|
||||
|
||||
if (data.eventQueue) eventQueue = [...eventQueue, ...data.eventQueue].slice(0, 50);
|
||||
if (data.isForceSyncInitiator !== undefined && isForceSyncInitiator === false) {
|
||||
isForceSyncInitiator = data.isForceSyncInitiator;
|
||||
}
|
||||
if (data.forceSyncAcks) {
|
||||
const mergedAcks = new Set([...forceSyncAcks, ...data.forceSyncAcks]);
|
||||
forceSyncAcks = mergedAcks;
|
||||
}
|
||||
if (data.reconnectFailed !== undefined) reconnectFailed = data.reconnectFailed;
|
||||
if (data.reconnectStartTime) reconnectStartTime = data.reconnectStartTime;
|
||||
|
||||
// Recover Force Sync Timeout
|
||||
if (data.forceSyncDeadline) {
|
||||
const remaining = data.forceSyncDeadline - Date.now();
|
||||
if (remaining > 0 && isForceSyncInitiator) {
|
||||
forceSyncTimeout = setTimeout(() => {
|
||||
if (isForceSyncInitiator) {
|
||||
addLog('Force Sync: Recovered timeout triggered, executing...', 'warn');
|
||||
executeForceSync();
|
||||
}
|
||||
}, remaining);
|
||||
} else if (remaining <= 0 && isForceSyncInitiator) {
|
||||
executeForceSync();
|
||||
}
|
||||
}
|
||||
|
||||
storageInitialized = true;
|
||||
|
||||
// Process any early logs/history that weren't captured in the spread
|
||||
if (pendingLogs.length > 0) {
|
||||
logs = [...pendingLogs, ...logs].slice(0, 50);
|
||||
chrome.storage.session.set({ logs });
|
||||
pendingLogs = [];
|
||||
}
|
||||
if (pendingHistory.length > 0) {
|
||||
history = [...pendingHistory, ...history].slice(0, 20);
|
||||
chrome.storage.session.set({ history });
|
||||
pendingHistory = [];
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
if (pendingHistory.length > 0) {
|
||||
history.unshift(...pendingHistory);
|
||||
if (history.length > 20) history = history.slice(0, 20);
|
||||
chrome.storage.session.set({ history });
|
||||
pendingHistory = [];
|
||||
}
|
||||
});
|
||||
return restorationTask;
|
||||
}
|
||||
|
||||
// Start restoration immediately
|
||||
ensureState();
|
||||
|
||||
let reconnectTimer = null;
|
||||
let reconnectStartTime = null; // New: track when reconnection started
|
||||
@@ -54,14 +100,8 @@ let forceSyncTimeout = null;
|
||||
|
||||
// --- Storage Utils ---
|
||||
function startHeartbeat() {
|
||||
stopHeartbeat();
|
||||
heartbeatInterval = setInterval(() => {
|
||||
if (currentRoom) {
|
||||
emit(EVENTS.PEER_STATUS, { peerId, status: 'heartbeat' });
|
||||
} else {
|
||||
stopHeartbeat();
|
||||
}
|
||||
}, 30000);
|
||||
// Session heartbeats are now handled by the chrome.alarms 'keepAlive' listener
|
||||
// to ensure they survive Service Worker suspension in MV3.
|
||||
}
|
||||
|
||||
function stopHeartbeat() {
|
||||
@@ -113,141 +153,172 @@ function addLog(message, type = 'info') {
|
||||
// --- WebSocket Client ---
|
||||
async function connect() {
|
||||
if (isConnecting) return;
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return;
|
||||
if (!navigator.onLine) {
|
||||
addLog('Browser is offline. Waiting...', 'warn');
|
||||
broadcastConnectionStatus('offline');
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnectFailed) return; // Wait for manual retry
|
||||
|
||||
if (!peerId) peerId = await getPeerId();
|
||||
const settings = await getSettings();
|
||||
|
||||
isConnecting = true;
|
||||
broadcastConnectionStatus('connecting');
|
||||
const isCustomServer = settings.serverUrl && settings.useCustomServer;
|
||||
let finalUrl = isCustomServer ? settings.serverUrl : OFFICIAL_SERVER_URL;
|
||||
|
||||
// Robustness: Ensure finalUrl is not empty and has a protocol
|
||||
if (isCustomServer) {
|
||||
finalUrl = finalUrl.trim();
|
||||
if (!finalUrl.includes('://')) {
|
||||
finalUrl = 'ws://' + finalUrl;
|
||||
}
|
||||
|
||||
// Strict WSS Enforcement
|
||||
const urlObj = new URL(finalUrl);
|
||||
const isLocal = urlObj.hostname === 'localhost' || urlObj.hostname === '127.0.0.1';
|
||||
if (urlObj.protocol !== 'wss:' && !isLocal) {
|
||||
urlObj.protocol = 'wss:';
|
||||
finalUrl = urlObj.toString();
|
||||
addLog('Security: Upgraded to wss:// for remote host.', 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
addLog(`Connecting to ${isCustomServer ? finalUrl : 'Official Server'}...`, 'info');
|
||||
|
||||
let finalUrl = '';
|
||||
try {
|
||||
const url = new URL(finalUrl);
|
||||
url.pathname = '/socket.io/';
|
||||
url.searchParams.set('EIO', '4');
|
||||
url.searchParams.set('transport', 'websocket');
|
||||
url.searchParams.set('version', APP_VERSION);
|
||||
|
||||
if (!isCustomServer) {
|
||||
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
|
||||
} else {
|
||||
// Self-hosted servers use the same official token by design.
|
||||
// This allows users to run their own relay while still using
|
||||
// the official extension — the token is public and not a secret.
|
||||
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
|
||||
// --- Phase 1: Storage ---
|
||||
let settings;
|
||||
try {
|
||||
if (!peerId) peerId = await getPeerId();
|
||||
settings = await getSettings();
|
||||
} catch (e) {
|
||||
throw new Error(`[Storage Error] ${e.message}`);
|
||||
}
|
||||
|
||||
socket = new WebSocket(url.toString());
|
||||
// --- Phase 2: Connection Guard ---
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||
if (isNamespaceJoined) {
|
||||
isConnecting = false;
|
||||
return;
|
||||
}
|
||||
socket.onopen = null;
|
||||
socket.onmessage = null;
|
||||
socket.onclose = null;
|
||||
socket.onerror = null;
|
||||
socket.close();
|
||||
}
|
||||
|
||||
socket.onopen = () => {
|
||||
if (!navigator.onLine) {
|
||||
addLog('Browser is offline. Waiting...', 'warn');
|
||||
broadcastConnectionStatus('offline');
|
||||
isConnecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnectFailed) {
|
||||
isConnecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
broadcastConnectionStatus('connecting');
|
||||
const isCustomServer = settings.serverUrl && settings.useCustomServer;
|
||||
finalUrl = isCustomServer ? settings.serverUrl : OFFICIAL_SERVER_URL;
|
||||
|
||||
// --- Phase 3: URL Validation ---
|
||||
try {
|
||||
if (isCustomServer) {
|
||||
finalUrl = finalUrl.trim();
|
||||
if (!finalUrl.includes('://')) {
|
||||
finalUrl = 'ws://' + finalUrl;
|
||||
}
|
||||
const urlObj = new URL(finalUrl);
|
||||
const isLocal = urlObj.hostname === 'localhost' || urlObj.hostname === '127.0.0.1';
|
||||
if (urlObj.protocol !== 'wss:' && !isLocal) {
|
||||
urlObj.protocol = 'wss:';
|
||||
finalUrl = urlObj.toString();
|
||||
addLog('Security: Upgraded to wss:// for remote host.', 'warn');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`[URL Error] ${e.message}`);
|
||||
}
|
||||
|
||||
addLog(`Connecting to ${isCustomServer ? finalUrl : 'Official Server'}...`, 'info');
|
||||
|
||||
// --- Phase 4: WebSocket Init ---
|
||||
try {
|
||||
const url = new URL(finalUrl);
|
||||
url.pathname = '/socket.io/';
|
||||
url.searchParams.set('EIO', '4');
|
||||
url.searchParams.set('transport', 'websocket');
|
||||
url.searchParams.set('version', APP_VERSION);
|
||||
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
|
||||
|
||||
socket = new WebSocket(url.toString());
|
||||
} catch (e) {
|
||||
throw new Error(`[Connection Error] ${e.message}`);
|
||||
}
|
||||
|
||||
// --- Phase 5: Event Listeners ---
|
||||
socket.onopen = () => {
|
||||
reconnectDelay = 1000;
|
||||
addLog('WebSocket Connection Opened', 'success');
|
||||
reconnectStartTime = null;
|
||||
reconnectFailed = false;
|
||||
isNamespaceJoined = false;
|
||||
|
||||
// Socket.IO Handshake: Send "40" to join default namespace
|
||||
socket.send('40');
|
||||
};
|
||||
|
||||
socket.onmessage = async (event) => {
|
||||
await ensureState();
|
||||
const msg = event.data;
|
||||
if (msg === '2') {
|
||||
socket.send('3');
|
||||
return;
|
||||
}
|
||||
if (msg.startsWith('0')) {
|
||||
addLog(`Socket.IO Handshake: ${msg}`, 'info');
|
||||
} else if (msg.startsWith('40')) {
|
||||
isConnecting = false;
|
||||
isNamespaceJoined = true;
|
||||
broadcastConnectionStatus('connected');
|
||||
addLog('Joined Namespace /', 'success');
|
||||
const settings = await getSettings();
|
||||
if (settings.roomId) {
|
||||
emit(EVENTS.JOIN_ROOM, {
|
||||
roomId: settings.roomId,
|
||||
password: settings.password,
|
||||
peerId,
|
||||
username: settings.username,
|
||||
tabTitle: currentTabTitle,
|
||||
protocolVersion: PROTOCOL_VERSION
|
||||
});
|
||||
}
|
||||
while (eventQueue.length > 0) {
|
||||
const queuedMsg = eventQueue.shift();
|
||||
emit(queuedMsg.event, queuedMsg.data);
|
||||
}
|
||||
eventQueue = [];
|
||||
chrome.storage.session.set({ eventQueue: [] });
|
||||
} else if (msg.startsWith('42')) {
|
||||
try {
|
||||
const payload = JSON.parse(msg.substring(2));
|
||||
handleServerEvent(payload[0], payload[1]);
|
||||
} catch (e) {
|
||||
addLog(`Failed to parse message: ${msg}`, 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
isConnecting = false;
|
||||
isNamespaceJoined = false;
|
||||
|
||||
// Clear Force Sync state
|
||||
isForceSyncInitiator = false;
|
||||
forceSyncAcks.clear();
|
||||
if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: false,
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: null
|
||||
});
|
||||
|
||||
if (currentRoom) {
|
||||
currentRoom.peers = [];
|
||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
|
||||
}
|
||||
broadcastConnectionStatus('disconnected');
|
||||
addLog(`Disconnected. Retrying in ${reconnectDelay / 1000}s...`, 'warn');
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
socket.onerror = (err) => {
|
||||
broadcastConnectionStatus('disconnected');
|
||||
addLog(`WebSocket Error: ${err.message || 'Handshake failed or server unreachable'}`, 'error');
|
||||
socket.close();
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
isConnecting = false;
|
||||
addLog(`Invalid Server URL: ${finalUrl}`, 'error');
|
||||
addLog(e.message, 'error');
|
||||
broadcastConnectionStatus('disconnected');
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const msg = event.data;
|
||||
|
||||
// Engine.IO Ping/Pong
|
||||
if (msg === '2') {
|
||||
socket.send('3'); // Pong
|
||||
return;
|
||||
}
|
||||
|
||||
// Socket.IO Handshake / Packet parsing
|
||||
if (msg.startsWith('0')) {
|
||||
addLog(`Socket.IO Handshake: ${msg}`, 'info');
|
||||
} else if (msg.startsWith('40')) {
|
||||
isNamespaceJoined = true;
|
||||
broadcastConnectionStatus('connected');
|
||||
addLog('Joined Namespace /', 'success');
|
||||
// Auto-rejoin room if we have one in settings
|
||||
if (settings.roomId) {
|
||||
emit(EVENTS.JOIN_ROOM, {
|
||||
roomId: settings.roomId,
|
||||
password: settings.password,
|
||||
peerId,
|
||||
username: settings.username,
|
||||
tabTitle: currentTabTitle,
|
||||
protocolVersion: PROTOCOL_VERSION
|
||||
});
|
||||
}
|
||||
while (eventQueue.length > 0) {
|
||||
const queuedMsg = eventQueue.shift();
|
||||
emit(queuedMsg.event, queuedMsg.data);
|
||||
}
|
||||
eventQueue = []; // Explicitly reset to avoid memory leaks
|
||||
} else if (msg.startsWith('42')) {
|
||||
// Event: 42["event", data]
|
||||
try {
|
||||
const payload = JSON.parse(msg.substring(2));
|
||||
handleServerEvent(payload[0], payload[1]);
|
||||
} catch (e) {
|
||||
addLog(`Failed to parse message: ${msg}`, 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
isConnecting = false;
|
||||
isNamespaceJoined = false;
|
||||
if (currentRoom) {
|
||||
currentRoom.peers = [];
|
||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
|
||||
}
|
||||
broadcastConnectionStatus('disconnected');
|
||||
addLog(`Disconnected. Retrying in ${reconnectDelay / 1000}s...`, 'warn');
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
socket.onerror = (err) => {
|
||||
broadcastConnectionStatus('disconnected');
|
||||
addLog('WebSocket Error', 'error');
|
||||
socket.close();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function broadcastConnectionStatus(status) {
|
||||
chrome.runtime.sendMessage({ type: 'CONNECTION_STATUS', status }).catch(() => {});
|
||||
updateBadgeStatus();
|
||||
@@ -301,6 +372,7 @@ function scheduleReconnect() {
|
||||
// Check 5 minute cap (300,000ms)
|
||||
if (Date.now() - reconnectStartTime > 300000) {
|
||||
reconnectFailed = true;
|
||||
chrome.storage.session.set({ reconnectFailed: true });
|
||||
addLog('Reconnection failed after 5 minutes. Please try again manually.', 'error');
|
||||
broadcastConnectionStatus('reconnect_failed');
|
||||
return;
|
||||
@@ -323,6 +395,7 @@ function emit(event, data) {
|
||||
eventQueue.shift();
|
||||
addLog('Event queue cap reached, dropping oldest event', 'warn');
|
||||
}
|
||||
chrome.storage.session.set({ eventQueue });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,10 +428,12 @@ function handleServerEvent(event, data) {
|
||||
// Start background heartbeat
|
||||
startHeartbeat();
|
||||
|
||||
// Inform Website Bridge
|
||||
// Inform Website Bridge & Popup
|
||||
const joinStatusMsg = { type: 'JOIN_STATUS', success: true, message: 'Joined' };
|
||||
chrome.runtime.sendMessage(joinStatusMsg).catch(() => {});
|
||||
chrome.tabs.query({}, (tabs) => {
|
||||
tabs.forEach(tab => {
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'JOIN_STATUS', success: true, message: 'Joined' }).catch(() => {});
|
||||
chrome.tabs.sendMessage(tab.id, joinStatusMsg).catch(() => {});
|
||||
});
|
||||
});
|
||||
break;
|
||||
@@ -366,6 +441,8 @@ function handleServerEvent(event, data) {
|
||||
chrome.runtime.sendMessage({ type: 'ROOM_LIST', rooms: data.rooms }).catch(() => {});
|
||||
break;
|
||||
case EVENTS.ERROR:
|
||||
isConnecting = false;
|
||||
broadcastConnectionStatus('disconnected');
|
||||
addLog(`Server Error: ${data.message}`, 'error');
|
||||
chrome.notifications.create(`error_${Date.now()}`, {
|
||||
type: 'basic',
|
||||
@@ -373,10 +450,12 @@ function handleServerEvent(event, data) {
|
||||
title: 'KoalaSync Error',
|
||||
message: data.message
|
||||
});
|
||||
// Inform Website Bridge
|
||||
// Inform Website Bridge & Popup
|
||||
const errStatusMsg = { type: 'JOIN_STATUS', success: false, message: data.message };
|
||||
chrome.runtime.sendMessage(errStatusMsg).catch(() => {});
|
||||
chrome.tabs.query({}, (tabs) => {
|
||||
tabs.forEach(tab => {
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'JOIN_STATUS', success: false, message: data.message }).catch(() => {});
|
||||
chrome.tabs.sendMessage(tab.id, errStatusMsg).catch(() => {});
|
||||
});
|
||||
});
|
||||
break;
|
||||
@@ -386,7 +465,7 @@ function handleServerEvent(event, data) {
|
||||
case EVENTS.FORCE_SYNC_PREPARE:
|
||||
if (data.senderId) {
|
||||
addToHistory(event, data.senderId);
|
||||
showNotification(event, data.senderId);
|
||||
showNotification(data.senderId, event);
|
||||
updateLastAction(event, data.senderId);
|
||||
}
|
||||
routeToContent(event, data);
|
||||
@@ -394,7 +473,19 @@ function handleServerEvent(event, data) {
|
||||
case EVENTS.FORCE_SYNC_ACK:
|
||||
if (isForceSyncInitiator) {
|
||||
forceSyncAcks.add(data.senderId);
|
||||
chrome.storage.session.set({ forceSyncAcks: Array.from(forceSyncAcks) });
|
||||
addLog(`Received ACK from ${data.senderId} (${forceSyncAcks.size})`, 'info');
|
||||
|
||||
// Update UI state for buffering progress
|
||||
if (lastActionState && lastActionState.action === EVENTS.FORCE_SYNC_PREPARE) {
|
||||
if (!Array.isArray(lastActionState.acks)) lastActionState.acks = [];
|
||||
if (!lastActionState.acks.includes(data.senderId)) {
|
||||
lastActionState.acks.push(data.senderId);
|
||||
if (storageInitialized) chrome.storage.session.set({ lastActionState });
|
||||
chrome.runtime.sendMessage({ type: 'ACTION_UPDATE', state: lastActionState }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all peers responded
|
||||
const peerCount = currentRoom ? currentRoom.peers.length : 1;
|
||||
if (forceSyncAcks.size >= peerCount) {
|
||||
@@ -413,6 +504,7 @@ function handleServerEvent(event, data) {
|
||||
if (lastActionState && lastActionState.action && data.senderId) {
|
||||
// Correlation Check: Only accept ACK if it matches our current action's timestamp
|
||||
if (data.actionTimestamp === lastActionState.timestamp) {
|
||||
if (!Array.isArray(lastActionState.acks)) lastActionState.acks = [];
|
||||
if (!lastActionState.acks.includes(data.senderId)) {
|
||||
lastActionState.acks.push(data.senderId);
|
||||
if (storageInitialized) chrome.storage.session.set({ lastActionState });
|
||||
@@ -423,6 +515,7 @@ function handleServerEvent(event, data) {
|
||||
break;
|
||||
case EVENTS.PEER_STATUS:
|
||||
if (currentRoom) {
|
||||
if (!Array.isArray(currentRoom.peers)) currentRoom.peers = [];
|
||||
if (data.status === 'joined') {
|
||||
if (!currentRoom.peers.find(p => (p.peerId || p) === data.peerId)) {
|
||||
currentRoom.peers.push({ peerId: data.peerId, username: data.username, tabTitle: data.tabTitle });
|
||||
@@ -460,6 +553,12 @@ function handleServerEvent(event, data) {
|
||||
function executeForceSync() {
|
||||
if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
|
||||
isForceSyncInitiator = false;
|
||||
forceSyncAcks.clear();
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: false,
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: null
|
||||
});
|
||||
emit(EVENTS.FORCE_SYNC_EXECUTE, {});
|
||||
routeToContent(EVENTS.FORCE_SYNC_EXECUTE, {});
|
||||
addLog('Force Sync Executed', 'success');
|
||||
@@ -515,40 +614,56 @@ async function routeToContent(action, payload) {
|
||||
|
||||
// --- Keep-Alive Mechanism ---
|
||||
chrome.alarms.create('keepAlive', { periodInMinutes: 1 });
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
chrome.alarms.onAlarm.addListener(async (alarm) => {
|
||||
await ensureState();
|
||||
if (alarm.name === 'keepAlive') {
|
||||
chrome.storage.session.get('keepAlive', () => {});
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
connect();
|
||||
} else if (currentRoom) {
|
||||
// Heartbeat Logic migrated from setInterval
|
||||
emit(EVENTS.PEER_STATUS, { peerId, status: 'heartbeat' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
setInterval(async () => {
|
||||
await ensureState();
|
||||
// Calling a chrome API keeps the SW alive in MV3 (Chrome 110+)
|
||||
chrome.storage.session.get('keepAlive', () => {});
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
connect();
|
||||
} else if (currentRoom) {
|
||||
// Redundant heartbeat for active SW state
|
||||
emit(EVENTS.PEER_STATUS, { peerId, status: 'heartbeat' });
|
||||
}
|
||||
}, 20000); // every 20s
|
||||
}, 30000); // every 30s
|
||||
|
||||
// --- Extension Message Listeners ---
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
handleAsyncMessage(message, sender, sendResponse);
|
||||
return true; // Keep channel open for async responses
|
||||
});
|
||||
|
||||
async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
await ensureState();
|
||||
|
||||
if (message.type === 'CONNECT') {
|
||||
reconnectFailed = false;
|
||||
reconnectStartTime = null;
|
||||
if (socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined) {
|
||||
// Already connected, but maybe room changed or we need to refresh room state
|
||||
getSettings().then(settings => {
|
||||
if (settings.roomId) {
|
||||
emit(EVENTS.JOIN_ROOM, {
|
||||
roomId: settings.roomId,
|
||||
password: settings.password,
|
||||
peerId,
|
||||
protocolVersion: PROTOCOL_VERSION
|
||||
});
|
||||
}
|
||||
});
|
||||
const settings = await getSettings();
|
||||
if (settings.roomId) {
|
||||
emit(EVENTS.JOIN_ROOM, {
|
||||
roomId: settings.roomId,
|
||||
password: settings.password,
|
||||
peerId,
|
||||
username: settings.username,
|
||||
tabTitle: currentTabTitle,
|
||||
protocolVersion: PROTOCOL_VERSION
|
||||
});
|
||||
}
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
@@ -565,25 +680,35 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
status,
|
||||
peerId,
|
||||
peers: currentRoom ? currentRoom.peers : [],
|
||||
lastActionState
|
||||
lastActionState,
|
||||
targetTabId: currentTabId
|
||||
});
|
||||
// Global return true at the end handles this
|
||||
} else if (message.type === 'LEAVE_ROOM') {
|
||||
emit(EVENTS.LEAVE_ROOM, { peerId });
|
||||
currentRoom = null;
|
||||
currentTabId = null;
|
||||
stopHeartbeat();
|
||||
updateBadgeStatus();
|
||||
if (storageInitialized) chrome.storage.session.set({ currentRoom: null });
|
||||
|
||||
isForceSyncInitiator = false;
|
||||
forceSyncAcks.clear();
|
||||
if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
|
||||
|
||||
chrome.storage.session.set({
|
||||
currentRoom: null,
|
||||
isForceSyncInitiator: false,
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: null
|
||||
});
|
||||
addLog('Left Room', 'info');
|
||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
|
||||
} else if (message.type === 'CLEAR_LOGS') {
|
||||
logs = [];
|
||||
sendResponse({ status: 'ok' });
|
||||
} else if (message.type === 'GET_LOGS') {
|
||||
sendResponse(storageInitialized ? logs : pendingLogs);
|
||||
sendResponse(logs);
|
||||
} else if (message.type === 'GET_HISTORY') {
|
||||
sendResponse(storageInitialized ? history : pendingHistory);
|
||||
sendResponse(history);
|
||||
} else if (message.type === 'GET_ROOM_LIST') {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(`42${JSON.stringify([EVENTS.GET_ROOMS])}`);
|
||||
@@ -595,21 +720,20 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
password,
|
||||
useCustomServer: !!useCustomServer,
|
||||
serverUrl: serverUrl || ''
|
||||
}, () => {
|
||||
}, async () => {
|
||||
broadcastConnectionStatus('connecting');
|
||||
if (socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined) {
|
||||
// FORCE TRANSITION: Emit Join Room directly if already connected
|
||||
getSettings().then(settings => {
|
||||
emit(EVENTS.JOIN_ROOM, {
|
||||
roomId,
|
||||
password,
|
||||
peerId,
|
||||
username: settings.username, // Use local settings, not bridge
|
||||
tabTitle: currentTabTitle,
|
||||
protocolVersion: PROTOCOL_VERSION
|
||||
});
|
||||
addLog(`Joining room via link: ${roomId}`, 'info');
|
||||
const settings = await getSettings();
|
||||
emit(EVENTS.JOIN_ROOM, {
|
||||
roomId,
|
||||
password,
|
||||
peerId,
|
||||
username: settings.username,
|
||||
tabTitle: currentTabTitle,
|
||||
protocolVersion: PROTOCOL_VERSION
|
||||
});
|
||||
addLog(`Joining room via link: ${roomId}`, 'info');
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
@@ -658,11 +782,17 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === EVENTS.FORCE_SYNC_PREPARE) {
|
||||
isForceSyncInitiator = true;
|
||||
forceSyncAcks.clear();
|
||||
const deadline = Date.now() + 5000;
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: true,
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: deadline
|
||||
});
|
||||
addLog('Initiating Force Sync...', 'info');
|
||||
|
||||
// Route to our own content script so we pause and seek
|
||||
routeToContent(EVENTS.FORCE_SYNC_PREPARE, message.payload);
|
||||
|
||||
|
||||
// Timeout if not everyone ACKs
|
||||
forceSyncTimeout = setTimeout(() => {
|
||||
if (isForceSyncInitiator) {
|
||||
@@ -676,6 +806,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
} else if (message.type === 'FORCE_SYNC_ACK') {
|
||||
if (isForceSyncInitiator) {
|
||||
forceSyncAcks.add(peerId);
|
||||
chrome.storage.session.set({ forceSyncAcks: Array.from(forceSyncAcks) });
|
||||
addLog(`Local ACK received (${forceSyncAcks.size})`, 'info');
|
||||
const peerCount = currentRoom ? currentRoom.peers.length : 1;
|
||||
if (forceSyncAcks.size >= peerCount) {
|
||||
@@ -702,6 +833,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
getSettings().then(settings => {
|
||||
emit(EVENTS.PEER_STATUS, { ...message.payload, peerId, username: settings.username, tabTitle: currentTabTitle });
|
||||
});
|
||||
} else if (message.type === 'LOG') {
|
||||
addLog(`[Content] ${message.message}`, message.level || 'info');
|
||||
}
|
||||
return true; // Keep channel open for async responses
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user