mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
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:
@@ -132,8 +132,14 @@ chrome.runtime.onConnect.addListener((port) => {
|
||||
}
|
||||
});
|
||||
|
||||
let _persistLastSeqTimer = null;
|
||||
function _persistLastSeq() {
|
||||
if (storageInitialized) chrome.storage.session.set({ lastSeqBySender });
|
||||
if (!storageInitialized) return;
|
||||
if (_persistLastSeqTimer) clearTimeout(_persistLastSeqTimer);
|
||||
_persistLastSeqTimer = setTimeout(() => {
|
||||
_persistLastSeqTimer = null;
|
||||
chrome.storage.session.set({ lastSeqBySender });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// --- Boot Sequence Lock ---
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Generiere deine interne ID neu und verbinde dich erneut",
|
||||
"REGEN_ID_DESC": "Verwende dies, wenn du Fehler wegen doppelter Identität siehst.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Anderes Problem? Öffne ein GitHub Issue",
|
||||
"TOAST_ID_REGENERATED": "Identität neu generiert — Verbindung wird wiederhergestellt…",
|
||||
"LABEL_CONN_STATUS": "Verbindungsstatus",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Aktueller WebSocket-Verbindungsstatus",
|
||||
"CONN_STATUS_DISCONNECTED": "Getrennt",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Regenerar tu ID interno y volver a conectarte",
|
||||
"REGEN_ID_DESC": "Usa esto si ves errores de 'Identidad duplicada'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "¿Tienes otro problema? Abre un reporte en GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identidad regenerada — reconectando…",
|
||||
"LABEL_CONN_STATUS": "Estado de la conexión",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Estado actual de la conexión WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Desconectado",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Régénérer votre identifiant interne et vous reconnecter",
|
||||
"REGEN_ID_DESC": "Utilisez cette option si vous rencontrez des erreurs de 'Double identité'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Autre problème? Ouvrez un Issue GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identité régénérée — reconnexion…",
|
||||
"LABEL_CONN_STATUS": "Statut de la connexion",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Statut actuel de la connexion WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Déconnecté",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Genera un nuovo ID interno e riconnettiti",
|
||||
"REGEN_ID_DESC": "Usa questa opzione se riscontri errori di 'Identità Duplicata'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Hai altri problemi? Apri una segnalazione su GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identità rigenerata — riconnessione…",
|
||||
"LABEL_CONN_STATUS": "Stato Connessione",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Stato attuale della connessione",
|
||||
"CONN_STATUS_DISCONNECTED": "Disconnesso",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "内部IDを再生成して再接続します",
|
||||
"REGEN_ID_DESC": "「Duplicate Identity」(ID重複)エラーが表示される場合に使用します。",
|
||||
"REGEN_ID_OTHER_ISSUE": "他の問題?GitHub Issueを開く",
|
||||
"TOAST_ID_REGENERATED": "IDを再生成しました — 再接続中…",
|
||||
"LABEL_CONN_STATUS": "接続状態",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "現在のWebSocket接続状態",
|
||||
"CONN_STATUS_DISCONNECTED": "切断されました",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "내부 ID를 재생성하고 다시 연결합니다",
|
||||
"REGEN_ID_DESC": "\"중복된 ID\" 오류가 발생할 경우에 사용하세요.",
|
||||
"REGEN_ID_OTHER_ISSUE": "다른 문제? GitHub Issue 열기",
|
||||
"TOAST_ID_REGENERATED": "ID가 재생성되었습니다 — 다시 연결 중…",
|
||||
"LABEL_CONN_STATUS": "연결 상태",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "현재 WebSocket 연결 상태",
|
||||
"CONN_STATUS_DISCONNECTED": "연결 끊김",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Genereer uw interne ID opnieuw en maak opnieuw verbinding",
|
||||
"REGEN_ID_DESC": "Gebruik dit als u fouten ziet over 'Duplicate Identity'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Ander probleem? Open een GitHub Issue",
|
||||
"TOAST_ID_REGENERATED": "Identiteit opnieuw gegenereerd — opnieuw verbinden…",
|
||||
"LABEL_CONN_STATUS": "Verbindingsstatus",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Huidige WebSocket-verbindingsstatus",
|
||||
"CONN_STATUS_DISCONNECTED": "Verbinding verbroken",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Wygeneruj ponownie swój identyfikator i połącz się ponownie",
|
||||
"REGEN_ID_DESC": "Użyj tego, jeśli widzisz błędy o zduplikowanej tożsamości.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Inny problem? Otwórz Issue na GitHub",
|
||||
"TOAST_ID_REGENERATED": "Tożsamość wygenerowana ponownie — ponowne łączenie…",
|
||||
"LABEL_CONN_STATUS": "Status połączenia",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Bieżący stan połączenia WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Rozłączono",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Gerar um novo ID interno e conectar novamente",
|
||||
"REGEN_ID_DESC": "Use esta opção se aparecer o erro de 'Identidade duplicada'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Outro problema? Abra um relatório no GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identidade regenerada — reconectando…",
|
||||
"LABEL_CONN_STATUS": "Status da conexão",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Status atual da conexão WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Desconectado",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Regerar o seu ID interno e voltar a ligar",
|
||||
"REGEN_ID_DESC": "Use isto se encontrar erros de 'Identidade Duplicada'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Outro problema? Abra um relatório no GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identidade regenerada — a voltar a ligar…",
|
||||
"LABEL_CONN_STATUS": "Estado da Ligação",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Estado atual da ligação",
|
||||
"CONN_STATUS_DISCONNECTED": "Desligado",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Сбросить ваш внутренний идентификатор и переподключиться",
|
||||
"REGEN_ID_DESC": "Используйте при появлении ошибок дублирования идентификации.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Другая проблема? Откройте Issue на GitHub",
|
||||
"TOAST_ID_REGENERATED": "Идентичность перегенерирована — переподключение…",
|
||||
"LABEL_CONN_STATUS": "Статус подключения",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Текущий статус WebSocket-соединения с сервером",
|
||||
"CONN_STATUS_DISCONNECTED": "Отключено",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Dahili kimliğinizi yeniden oluşturun ve tekrar bağlanın",
|
||||
"REGEN_ID_DESC": "\"Duplicate Identity\" (Mükerrer Kimlik) hataları görüyorsanız bunu kullanın.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Başka bir sorun? GitHub Issue açın",
|
||||
"TOAST_ID_REGENERATED": "Kimlik yeniden oluşturuldu — yeniden bağlanılıyor…",
|
||||
"LABEL_CONN_STATUS": "Bağlantı Durumu",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Mevcut WebSocket bağlantı durumu",
|
||||
"CONN_STATUS_DISCONNECTED": "Bağlantı Kesildi",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Повторно згенеруйте свій внутрішній ідентифікатор і повторно підключіться",
|
||||
"REGEN_ID_DESC": "Використовуйте це, якщо ви бачите помилку \"Дублікати ідентифікатора\".",
|
||||
"REGEN_ID_OTHER_ISSUE": "Інша проблема? Відкрийте випуск GitHub",
|
||||
"TOAST_ID_REGENERATED": "Ідентичність перегенеровано — повторне підключення…",
|
||||
"LABEL_CONN_STATUS": "Статус підключення",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Поточний стан підключення WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Відключено",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "重新生成您的内部 ID 并重新连接",
|
||||
"REGEN_ID_DESC": "如果您看到“重复身份”错误,请使用此选项。",
|
||||
"REGEN_ID_OTHER_ISSUE": "其他问题?打开 GitHub 问题",
|
||||
"TOAST_ID_REGENERATED": "身份已重新生成 — 正在重新连接…",
|
||||
"LABEL_CONN_STATUS": "连接状态",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "当前WebSocket连接状态",
|
||||
"CONN_STATUS_DISCONNECTED": "已断开连接",
|
||||
|
||||
@@ -245,6 +245,9 @@ try {
|
||||
await w(ho,'force_sync_prepare'); await w(hg,'force_sync_prepare');
|
||||
ho._m.length = hc._m.length = hg._m.length = 0;
|
||||
// owner demotes the co-host mid-flight — the EXECUTE must still go through.
|
||||
// Wait out the per-room role-change debounce (M-4) so this demote broadcasts:
|
||||
// in real usage the host can't promote, run a force-sync, and demote inside 500ms.
|
||||
await new Promise(r => setTimeout(r, 550));
|
||||
s(ho,'set_peer_role',{peerId:'cohost',controller:false});
|
||||
await w(hc,'control_mode');
|
||||
ho._m.length = hc._m.length = hg._m.length = 0;
|
||||
|
||||
+16
-7
@@ -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)}***`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user