/* ── how much of this the phone may hold ──────────────────────────────── The desktop bounds the identical map with these two numbers (src/lib/state.svelte.js: ART_THUMB_CACHE_MAX, ART_THUMB_CACHE_MAX_CHARS, trimmed in insertion order by trimResolvedCache). Kept exactly as they are rather than invented here: `album_art` answers with base64 JPEG of a 360px thumbnail on both, so 512 covers is the same budget either side. Until now nothing on the phone gave a byte of it back — one browse of a 114-cover library retains every one of them for the WebView's lifetime, and a catalog crawl adds to the same map without limit. */ const ART_MAX = 512; const ART_MAX_CHARS = 16 * 1024 * 1024; /* The desktop may drop any cold entry, because every caller there asks through getArt(path). The phone's templates INDEX this map — art[path] in Cover, Release, Sheet, Viewer, Artist, Discography, Home and the media card — so a key something is painting would blank on the spot, and Cover's `visible` action re-arms only when its path PROP changes, so an evicted cover that is still mounted would never come back at all. Hence two rules the desktop does not need: never the entry that has just landed, and never one that a connected is showing. Oldest first otherwise — Object.keys is insertion order here, because every key is a file path, an https URL or a "stream:" string and never integer-like, which would be hoisted to the front and go first. A remembered failure ("" so it is not retried) is evictable like anything else: past the cap a retry is the cheaper of the two. The map is allowed to sit OVER its cap for as long as one screen needs all of it — a mounted grid of 600 covers loses none of them — and it comes down the moment that screen goes. Called from pump() and nowhere else, which is never inside an $effect: Object.keys on a $state proxy reads the version signal, so an effect that called this would re-run on every cover that lands. The two writers that do not go through pump() — loadArt's data: short-circuit and a stream's tag art in player.svelte.js — are therefore bounded at the next cover that does, which on any screen worth the name is the next one. Under the cap it is a key count and a char sum and returns; the pass over the screen's images is built once, lazily, and only when there is something to drop, because reading a base64 src is a copy of the whole data URL. */ function trimArt(justAdded) { const keys = Object.keys(art); let chars = 0; for (const k of keys) { const v = art[k]; if (typeof v === "string") chars += v.length; } let live = keys.length; if (live <= ART_MAX && chars <= ART_MAX_CHARS) return; let painted = null; for (const k of keys) { if (live <= ART_MAX && chars <= ART_MAX_CHARS) break; if (k === justAdded) continue; const v = art[k]; if (v) { if (!painted) { painted = new Set(); /* the attribute, not the .src property: for a data URL they are the same string and the property re-serializes it. The fallback is for an whose src was set as a property instead — none today, and a miss here is the one way this could blank a cover. */ for (const img of document.images) { if (!img.isConnected) continue; const src = img.getAttribute("src") || img.src; if (src) painted.add(src); } } if (painted.has(v)) continue; } delete art[k]; if (typeof v === "string") chars -= v.length; live--; } }