Compare commits

..

2 Commits

Author SHA1 Message Date
xRangerDE
7e60d0e920 release: 4.6.60 bound renderer storyboard cache to 100 entries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:08:43 +02:00
xRangerDE
976ca40963 perf: bound the renderer-side VOD storyboard cache (FIFO 100)
vodStoryboardClientCache was a plain Map<vodId, VodStoryboard | null>
with no eviction. Every VOD ever hovered cached its first sprite
data URL — about 50-200 KB each. Browsing a long-running streamer's
2000-VOD archive could leave the renderer holding 100-400 MB of
hover-only sprite data permanently, with no signal to the user
that it was happening.

Wrapped writes in a rememberStoryboard helper that caps the cache
at 100 entries with FIFO eviction (Map iterator is insertion-ordered
so .keys().next().value is always the oldest). Cache hit / miss
semantics unchanged for the live set — only the dropped-off-the-
back entries get re-fetched if the user scrolls back to a VOD they
hovered hundreds of cards ago, and that re-fetch is fast because
the main-process side has its own metadata cache that survives the
renderer-side eviction.

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

4
package-lock.json generated
View File

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

View File

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

View File

@ -21,6 +21,20 @@ let pendingHoverVodId: string | null = null;
const HOVER_DEBOUNCE_MS = 220; const HOVER_DEBOUNCE_MS = 220;
const FRAME_INTERVAL_MS = 600; const FRAME_INTERVAL_MS = 600;
const FRAMES_TO_CYCLE = 4; const FRAMES_TO_CYCLE = 4;
// Bounded cache — each storyboard data URL is ~50-200 KB, so an
// unbounded cache could balloon to hundreds of MB on a long browsing
// session through a streamer with thousands of VODs. FIFO eviction
// keeps the working set fresh without manual cleanup.
const MAX_CLIENT_STORYBOARD_CACHE = 100;
function rememberStoryboard(vodId: string, sb: VodStoryboard | null): void {
vodStoryboardClientCache.set(vodId, sb);
if (vodStoryboardClientCache.size > MAX_CLIENT_STORYBOARD_CACHE) {
// Map iterator is insertion-ordered — first key is the oldest.
const oldestKey = vodStoryboardClientCache.keys().next().value as string | undefined;
if (oldestKey !== undefined) vodStoryboardClientCache.delete(oldestKey);
}
}
function ensureVodHoverHandlersBound(): void { function ensureVodHoverHandlersBound(): void {
const grid = document.getElementById('vodGrid'); const grid = document.getElementById('vodGrid');
@ -85,7 +99,7 @@ async function activateHoverPreview(card: HTMLElement, vodId: string): Promise<v
} catch (_) { } catch (_) {
storyboard = null; storyboard = null;
} }
vodStoryboardClientCache.set(vodId, storyboard); rememberStoryboard(vodId, storyboard);
} }
// Cursor may have moved on while we awaited; re-check guard. // Cursor may have moved on while we awaited; re-check guard.