/* ── 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 <img> 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 <img> 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--;
    }
}
