mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-30 12:29:27 +00:00
merge main and harden canonical media recovery
This commit is contained in:
+38
-12
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
|
||||
import { Server } from 'socket.io';
|
||||
import crypto from 'crypto';
|
||||
import dotenv from 'dotenv';
|
||||
import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION, CONTROL_MODES, CAPABILITIES, MAX_MEDIA_TIME } from '../shared/constants.js';
|
||||
import { EVENTS, ERROR_CODES, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION, CONTROL_MODES, CAPABILITIES, FORCE_SYNC_TIMEOUT, MAX_MEDIA_TIME } from '../shared/constants.js';
|
||||
import { createChatEnvelope } from './chat.js';
|
||||
import {
|
||||
commitForceSyncMediaState,
|
||||
@@ -651,10 +651,18 @@ io.on('connection', (socket) => {
|
||||
// Strip undefined keys for clean wire format
|
||||
Object.keys(relayPayload).forEach(k => relayPayload[k] === undefined && delete relayPayload[k]);
|
||||
|
||||
if (eventName === EVENTS.FORCE_SYNC_EXECUTE && !room.forceSyncTarget) {
|
||||
log('ROOM', `Dropped force_sync_execute without a prepared target from ${mapping.peerId}`);
|
||||
room.forceSyncInitiator = null;
|
||||
return;
|
||||
const mediaStateNow = Date.now();
|
||||
if (eventName === EVENTS.FORCE_SYNC_EXECUTE) {
|
||||
const forceSyncTarget = room.forceSyncTarget;
|
||||
const targetExpired = forceSyncTarget
|
||||
&& (!Number.isFinite(forceSyncTarget.preparedAt)
|
||||
|| mediaStateNow - forceSyncTarget.preparedAt > FORCE_SYNC_TIMEOUT);
|
||||
if (!forceSyncTarget || targetExpired) {
|
||||
log('ROOM', `Dropped force_sync_execute ${targetExpired ? 'with an expired target' : 'without a prepared target'} from ${mapping.peerId}`);
|
||||
room.forceSyncInitiator = null;
|
||||
room.forceSyncTarget = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (eventName === EVENTS.PLAY
|
||||
|| eventName === EVENTS.PAUSE
|
||||
@@ -671,7 +679,6 @@ io.on('connection', (socket) => {
|
||||
// Canonical Media State v1: mutate only after rate limiting,
|
||||
// room mapping, Host Control authorization and sanitization.
|
||||
// Heartbeats remain observational and never enter this path.
|
||||
const mediaStateNow = Date.now();
|
||||
updateMediaStateFromControl(room, eventName, relayPayload, mapping.peerId, {
|
||||
now: mediaStateNow,
|
||||
senderPlaybackState: existing.playbackState
|
||||
@@ -691,7 +698,8 @@ io.on('connection', (socket) => {
|
||||
room.forceSyncInitiator = mapping.peerId;
|
||||
room.forceSyncTarget = {
|
||||
initiatorPeerId: mapping.peerId,
|
||||
targetTime: relayPayload.targetTime
|
||||
targetTime: relayPayload.targetTime,
|
||||
preparedAt: mediaStateNow
|
||||
};
|
||||
} else if (eventName === EVENTS.FORCE_SYNC_EXECUTE) {
|
||||
const forceSyncTarget = room.forceSyncTarget;
|
||||
@@ -984,8 +992,7 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
|
||||
// Active Room & Dead Peer Cleanup (Every 2m)
|
||||
const roomCleanupInterval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
export function cleanupInactiveRooms(now = Date.now()) {
|
||||
const roomCutoff = now - (2 * 60 * 60 * 1000); // 2 hours
|
||||
const peerCutoff = now - (5 * 60 * 1000); // 5 minutes
|
||||
|
||||
@@ -1004,7 +1011,13 @@ const roomCleanupInterval = setInterval(() => {
|
||||
}
|
||||
for (const sid of staleSids) {
|
||||
const deadSocket = io.sockets?.sockets?.get(sid);
|
||||
if (deadSocket) deadSocket.leave(roomId);
|
||||
if (deadSocket) {
|
||||
deadSocket.emit(EVENTS.ERROR, {
|
||||
code: ERROR_CODES.PEER_TIMED_OUT,
|
||||
message: 'Removed from room after inactivity'
|
||||
});
|
||||
deadSocket.leave(roomId);
|
||||
}
|
||||
log('CLEANUP', `Pruning dead peer from room ${roomId.substring(0, 3)}***`);
|
||||
try {
|
||||
removePeerFromRoom(sid, roomId, 'reaper');
|
||||
@@ -1016,12 +1029,25 @@ const roomCleanupInterval = setInterval(() => {
|
||||
// 2. Prune empty or inactive rooms
|
||||
const currentRoom = rooms.get(roomId);
|
||||
if (currentRoom && (currentRoom.peers.size === 0 || currentRoom.lastActivity < roomCutoff)) {
|
||||
io.to(roomId).emit(EVENTS.ERROR, { message: 'Room closed' });
|
||||
io.to(roomId).emit(EVENTS.ERROR, {
|
||||
code: ERROR_CODES.ROOM_CLOSED,
|
||||
message: 'Room closed'
|
||||
});
|
||||
// A terminal room timeout is a real leave for every member. Clear
|
||||
// the same socket/peer indexes as an explicit leave so a later join
|
||||
// cannot be mistaken for the stale membership.
|
||||
for (const sid of Array.from(currentRoom.peers)) {
|
||||
const memberSocket = io.sockets?.sockets?.get(sid);
|
||||
if (memberSocket) memberSocket.leave(roomId);
|
||||
removePeerFromRoom(sid, roomId, 'room-timeout');
|
||||
}
|
||||
rooms.delete(roomId);
|
||||
log('CLEANUP', `Deleted room ${roomId.substring(0, 3)}*** (Empty/Inactive)`);
|
||||
}
|
||||
}
|
||||
}, 2 * 60 * 1000);
|
||||
}
|
||||
|
||||
const roomCleanupInterval = setInterval(cleanupInactiveRooms, 2 * 60 * 1000);
|
||||
|
||||
export function startServer(port = PORT, host) {
|
||||
if (httpServer.listening) return Promise.resolve(httpServer);
|
||||
|
||||
Reference in New Issue
Block a user