perf: add command sequence numbers, episode-aware sync guard, fix echo suppression

- Add monotonically increasing seq per peer (localSeq + lastSeqBySender)
- Server relays seq field for stale command detection (backward compatible)
- Replace expectedEvents (1500ms timeout) with _suppressTimers (per-type, 300ms)
- Fix FORCE_SYNC_ACK missing seq (stale-ACK could trigger premature exec)
- Fix episode lobby readyPeers asymmetry (initiator never broadcasted readiness)
- Add extractEpisodeId() parsing S01E01/Season 1 Episode 1/Folge 5 patterns
- Add isDifferentEpisode() guard blocking sync commands across different episodes
- add sameEpisode() for format-tolerant lobby title matching
- Guard controlled by autoSyncNextEpisode setting with disable hint in warning
- Reduce reactive lock from 1000ms to 300ms (seq numbers handle ordering)
This commit is contained in:
Koala
2026-05-28 04:30:32 +02:00
parent eca259281a
commit 1fba2fb69c
3 changed files with 128 additions and 45 deletions
+26 -5
View File
@@ -130,6 +130,25 @@ let forceSyncTimeout = null;
let episodeLobby = null; // { expectedTitle, initiatorPeerId, readyPeers: [], createdAt }
let episodeLobbyTimeout = null;
// --- Episode Title Extraction (synced with content.js) ---
function extractEpisodeId(title) {
if (!title || typeof title !== 'string') return null;
const se = title.match(/S(?:eason\s*)?(\d+)[\s\-\.]*E(?:pisode\s*)?(\d+)/i);
if (se) return `S${String(se[1]).padStart(2, '0')}E${String(se[2]).padStart(2, '0')}`;
const ep = title.match(/(?:Episode|Folge|Ep\.?|#)\s*(\d+)/i);
if (ep) return `EP${String(ep[1]).padStart(3, '0')}`;
return null;
}
function sameEpisode(titleA, titleB) {
if (!titleA || !titleB) return true;
const idA = extractEpisodeId(titleA);
const idB = extractEpisodeId(titleB);
if (idA && idB) return idA === idB;
if (idA || idB) return false;
return titleA === titleB;
}
// --- Storage Utils ---
/**
@@ -746,7 +765,7 @@ function handleServerEvent(event, data) {
if (data.senderId && data.expectedTitle) {
addLog(`Episode lobby from ${data.senderId}: "${data.expectedTitle}"`, 'info');
// If we already have a lobby for this same title, treat as dedup
if (episodeLobby && episodeLobby.expectedTitle === data.expectedTitle) {
if (episodeLobby && sameEpisode(episodeLobby.expectedTitle, data.expectedTitle)) {
break; // Already tracking this lobby
}
// Cancel any existing lobby before starting a new one
@@ -755,7 +774,7 @@ function handleServerEvent(event, data) {
episodeLobby = {
expectedTitle: data.expectedTitle,
initiatorPeerId: data.senderId,
readyPeers: [],
readyPeers: [data.senderId], // Initiator is already ready
createdAt: Date.now()
};
persistEpisodeLobby();
@@ -1247,7 +1266,9 @@ async function handleAsyncMessage(message, sender, sendResponse) {
executeForceSync();
}
} else {
emit(EVENTS.FORCE_SYNC_ACK, { peerId });
localSeq++;
chrome.storage.session.set({ localSeq });
emit(EVENTS.FORCE_SYNC_ACK, { peerId, seq: localSeq });
}
sendResponse({ status: 'ok' });
} else if (message.type === 'CMD_ACK') {
@@ -1342,7 +1363,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
}
// If lobby already exists for this title, just mark self ready
if (episodeLobby && episodeLobby.expectedTitle === newTitle) {
if (episodeLobby && sameEpisode(episodeLobby.expectedTitle, newTitle)) {
if (!episodeLobby.readyPeers.includes(peerId)) {
episodeLobby.readyPeers.push(peerId);
persistEpisodeLobby();
@@ -1389,7 +1410,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
sendResponse({ status: 'lobby_created' });
} else if (message.type === 'EPISODE_READY_LOCAL') {
// Content script confirmed it loaded the lobby episode
if (episodeLobby && message.payload && message.payload.title === episodeLobby.expectedTitle) {
if (episodeLobby && message.payload && sameEpisode(message.payload.title, episodeLobby.expectedTitle)) {
if (!episodeLobby.readyPeers.includes(peerId)) {
episodeLobby.readyPeers.push(peerId);
persistEpisodeLobby();