diff --git a/src/main.ts b/src/main.ts index 941459a..bb7044d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3750,26 +3750,32 @@ async function runLiveStatusBatchPoll(): Promise { const logins = ((config.streamers as string[]) || []) .map((s) => normalizeLogin(s)) .filter((s): s is string => Boolean(s)); - if (logins.length === 0) return; - const fresh = await fetchLiveStatusBatch(logins); const changes: Array<{ login: string; isLive: boolean }> = []; - const seen = new Set(fresh.keys()); - for (const [login, isLive] of fresh.entries()) { - const prev = liveStatusByLogin.get(login); - if (prev !== isLive) changes.push({ login, isLive }); - liveStatusByLogin.set(login, isLive); - } - // Streamers that vanished from the watch list (or that GQL didn't - // return for) get evicted so a removed streamer doesn't leave a - // ghost-live dot behind. + const watchedSet = new Set(logins); + + // Always run the eviction pass FIRST — entries left over from a + // streamer that's no longer in the watch list must go regardless + // of whether we're about to fetch fresh data. Previously this + // ran inside the fetch branch only, so removing the last + // streamer left ghost entries in liveStatusByLogin until the + // next add. for (const oldLogin of Array.from(liveStatusByLogin.keys())) { - if (!seen.has(oldLogin)) { + if (!watchedSet.has(oldLogin)) { liveStatusByLogin.delete(oldLogin); changes.push({ login: oldLogin, isLive: false }); } } + if (logins.length > 0) { + const fresh = await fetchLiveStatusBatch(logins); + for (const [login, isLive] of fresh.entries()) { + const prev = liveStatusByLogin.get(login); + if (prev !== isLive) changes.push({ login, isLive }); + liveStatusByLogin.set(login, isLive); + } + } + if (mainWindow && changes.length > 0) { // Renderer only consumes `changes` — initial state comes via // the get-live-status-snapshot IPC at boot. Don't ship the