Files
SoulStream/handoff/rounds/ins-r41-art-trim.txt
T
KeelrLeek 9f0601027c android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43
to 48 (each patch adversarially verified, 43's transient-notice expectation
fixed), a partial round 49, the pipeline scripts, the Android Auto design and
the icon comparison mock. Also the two harness changes that were never A/B'd —
adaptive waits, which may only shorten a declared wait and never lengthen it,
and a ship that runs the mock walk and the deploy at the same time. The A/B and
its 255-step baseline are in handoff/tools; main stays at what the phone has
actually walked.
2026-09-14 07:57:41 -05:00

73 lines
3.8 KiB
Plaintext

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