Compare commits

..

2 Commits

Author SHA1 Message Date
xRangerDE
1b8624d88a release: 4.6.57 live-status poller — eviction on empty list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:51:42 +02:00
xRangerDE
77e4c84c45 fix: live-status poller — eviction now runs even when watch list is empty
Subtle leak in runLiveStatusBatchPoll: the eviction pass (which
removes liveStatusByLogin entries for streamers no longer in
config.streamers) ran INSIDE the fetch branch — but the fetch
branch is skipped early when logins.length === 0.

Concretely: if a user had 3 streamers all marked live, then
removed all 3, the poll would early-return at length-check,
leaving stale liveStatusByLogin entries forever (until app
restart) — main-process memory + an inaccurate
get-live-status-snapshot IPC response.

Renderer wasn't visibly affected because renderStreamers only
looks up entries for streamers in the rendered list, but the
underlying state was wrong.

Restructured so the eviction pass always runs first based on the
current watch list, then the fetch + diff only runs when the list
is non-empty. Empty-list case still emits "removed -> offline"
changes to the renderer so its parallel map stays in sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:51:42 +02:00
3 changed files with 21 additions and 15 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "twitch-vod-manager",
"version": "4.6.56",
"version": "4.6.57",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "twitch-vod-manager",
"version": "4.6.56",
"version": "4.6.57",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{
"name": "twitch-vod-manager",
"version": "4.6.56",
"version": "4.6.57",
"description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js",
"author": "xRangerDE",

View File

@ -3750,24 +3750,30 @@ async function runLiveStatusBatchPoll(): Promise<void> {
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());
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 (!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);
}
// 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.
for (const oldLogin of Array.from(liveStatusByLogin.keys())) {
if (!seen.has(oldLogin)) {
liveStatusByLogin.delete(oldLogin);
changes.push({ login: oldLogin, isLive: false });
}
}
if (mainWindow && changes.length > 0) {