perf+fix: debounce seq persistence + role changes, null-clear stale peer state

- server: SET_PEER_ROLE now uses the same per-room 500ms debounce as
  SET_CONTROL_MODE (M-4), preventing a buggy/malicious host from
  thrashing the room's UI with rapid promote/demote bursts.
- server: PEER_STATUS relay treats explicit null as 'clear' for
  tabTitle/mediaTitle/currentTime, so the tab-close heartbeat actually
  zeroes out stale state on other peers instead of being silently
  preserved by the clamp fallback.
- background: _persistLastSeq is now trailing-debounced (500ms),
  cutting storage.session IPC writes from one-per-relayed-event to
  one-per-quiet-window in active rooms.
- locales: add missing TOAST_ID_REGENERATED to all 14 non-English
  locales (was blocking locale coverage test on main).
- test: H-1 force-sync demote test now waits out the debounce window
  to reflect real-world UI timing (a host cannot promote, run a
  force-sync, and demote inside 500ms).
This commit is contained in:
KoalaDev
2026-06-29 01:22:41 +02:00
parent e23f9ab226
commit adab684ed0
17 changed files with 40 additions and 8 deletions
+16 -7
View File
@@ -411,6 +411,7 @@ io.on('connection', (socket) => {
hostPeerId: peerId,
controlMode: CONTROL_MODES.EVERYONE,
lastControlModeChangeAt: 0, // M-4: per-room debounce for control-mode toggles
lastRoleChangeAt: 0, // M-4: per-room debounce for role promote/demote
// Co-Host: peers allowed to drive in 'host-only'. Always includes the owner.
controllers: new Set([peerId]),
// H-1: peerId of the in-flight force-sync initiator. Lets a demoted
@@ -585,10 +586,10 @@ io.on('connection', (socket) => {
room.peerData.set(socket.id, {
...existing,
username: data.username !== undefined ? (clamp(data.username, 30) ?? existing.username) : existing.username,
tabTitle: data.tabTitle !== undefined ? (clamp(data.tabTitle, 100) ?? existing.tabTitle) : existing.tabTitle,
mediaTitle: data.mediaTitle !== undefined ? (clamp(data.mediaTitle, 100) ?? existing.mediaTitle) : existing.mediaTitle,
tabTitle: data.tabTitle === null ? null : (data.tabTitle !== undefined ? (clamp(data.tabTitle, 100) ?? existing.tabTitle) : existing.tabTitle),
mediaTitle: data.mediaTitle === null ? null : (data.mediaTitle !== undefined ? (clamp(data.mediaTitle, 100) ?? existing.mediaTitle) : existing.mediaTitle),
playbackState: data.playbackState !== undefined ? (validState(data.playbackState) ?? existing.playbackState) : existing.playbackState,
currentTime: data.currentTime !== undefined ? (clampNum(data.currentTime, 0, 86400) ?? existing.currentTime) : existing.currentTime,
currentTime: data.currentTime === null ? null : (data.currentTime !== undefined ? (clampNum(data.currentTime, 0, 86400) ?? existing.currentTime) : existing.currentTime),
volume: data.volume !== undefined ? (clampNum(data.volume, 0, 1) ?? existing.volume) : existing.volume,
muted: data.muted !== undefined ? (validBool(data.muted) ?? existing.muted) : existing.muted,
desynced: data.desynced !== undefined ? (validBool(data.desynced) === true) : (existing.desynced || false),
@@ -599,12 +600,12 @@ io.on('connection', (socket) => {
const relayPayload = {
senderId: mapping.peerId,
seq: clampNum(data.seq, 0, Number.MAX_SAFE_INTEGER),
currentTime: clampNum(data.currentTime, 0, 86400),
currentTime: data.currentTime === null ? null : clampNum(data.currentTime, 0, 86400),
targetTime: clampNum(data.targetTime, 0, 86400),
playbackState: validState(data.playbackState),
username: clamp(data.username, 30),
tabTitle: clamp(data.tabTitle, 100),
mediaTitle: clamp(data.mediaTitle, 100),
tabTitle: data.tabTitle === null ? null : clamp(data.tabTitle, 100),
mediaTitle: data.mediaTitle === null ? null : clamp(data.mediaTitle, 100),
volume: clampNum(data.volume, 0, 1),
muted: validBool(data.muted),
desynced: validBool(data.desynced),
@@ -742,6 +743,13 @@ io.on('connection', (socket) => {
const targetPresent = Array.from(room.peerData.values()).some(d => d.peerId === targetPeerId);
if (!targetPresent) return;
const now = Date.now();
if (now - (room.lastRoleChangeAt || 0) < CONTROL_MODE_MIN_INTERVAL_MS) {
log('ROOM', `Role change debounced in ${mapping.roomId.substring(0, 3)}***`);
socket.emit(EVENTS.CONTROL_MODE, controlModePayload(room));
return;
}
if (makeController) {
if (room.controllers.has(targetPeerId)) return; // no-op
room.controllers.add(targetPeerId);
@@ -749,7 +757,8 @@ io.on('connection', (socket) => {
if (!room.controllers.has(targetPeerId)) return; // no-op
room.controllers.delete(targetPeerId);
}
room.lastActivity = Date.now();
room.lastRoleChangeAt = now;
room.lastActivity = now;
io.to(mapping.roomId).emit(EVENTS.CONTROL_MODE, controlModePayload(room));
log('ROOM', `Peer ${targetPeerId} ${makeController ? 'promoted to' : 'demoted from'} controller in ${mapping.roomId.substring(0, 3)}***`);
});