/** Pull every card thumbnail into the browser's caches once, while the kiosk is idle. * * Cover URLs carry a version and are served `immutable`, so once a thumbnail has been * fetched the browser never asks for it again - but the *first* search over the whole * library would otherwise fetch, and decode, hundreds of them while the user waits. This * does that work up front, a few at a time in idle periods. The `Image`s are held so * their decoded bitmaps stay resident: RAM is not the constraint on the machine this is * for (`?pi=1`, see lib/lowPower.ts), the main thread during a keystroke is. */ import { useEffect } from "react"; import { coverUrl } from "../api/client"; import type { Album } from "../api/types"; const BATCH = 6; const idle = (run: () => void): void => { if (typeof requestIdleCallback === "function") requestIdleCallback(run, { timeout: 2000 }); else setTimeout(run, 50); }; /** Module-level on purpose: dropping these would let the browser discard what they hold. */ const held = new Map(); export function useCoverWarmup(albums: Album[], enabled: boolean): void { useEffect(() => { if (!enabled) return; let cancelled = false; const pending = albums.filter((a) => a.has_cover && !a.locked); let next = 0; const step = () => { if (cancelled || next >= pending.length) return; const batch = pending.slice(next, next + BATCH); next += BATCH; void Promise.all( batch.map((album) => { const url = coverUrl(album.id, { size: "thumb", version: album.cover_v }); if (held.has(url)) return undefined; const img = new Image(); img.decoding = "async"; img.src = url; held.set(url, img); return img.decode().catch(() => undefined); }), ).then(() => idle(step)); }; idle(step); return () => { cancelled = true; }; }, [albums, enabled]); }