// Persistent image cache for preview images. // // Hub `resolve/` URLs answer `Cache-Control: no-store` and redirect to a freshly signed CDN // URL on every request, so the browser's HTTP cache never reuses them. Instead we keep the // bytes in Cache Storage, keyed by the file's content hash from the dataset listing: a hit // skips the network entirely, and a changed image gets a new hash (so a new entry). const CACHE_NAME = "lora-previews-v1"; /** Synthetic request URL per content hash; never fetched, only used as a cache key. */ const keyUrl = (hash: string) => `https://preview-cache.invalid/${hash}`; // One in-flight load per hash, shared by every showing that image. const loads = new Map>(); // Finished loads, readable synchronously so a remounted preview can render the image in its // first frame instead of flashing the placeholder while a promise settles. const ready = new Map(); /** Object URL for an image that has already finished loading, if any. */ export const loadedImage = (hash: string) => ready.get(hash); /** Object URL for the image (from cache, or fetched then cached). Falls back to `url` itself. */ export function cachedImage(url: string, hash: string): Promise { let p = loads.get(hash); if (!p) { p = load(url, hash); loads.set(hash, p); p.then((src) => loads.get(hash) === p && ready.set(hash, src)); } return p; } async function load(url: string, hash: string): Promise { try { const cache = await caches.open(CACHE_NAME); let res = await cache.match(keyUrl(hash)); if (!res) { const net = await fetch(url, { mode: "cors" }); if (!net.ok) throw new Error(`${net.status}`); await cache.put(keyUrl(hash), net.clone()); res = net; } return URL.createObjectURL(await res.blob()); } catch { // Cache Storage unavailable (private mode, blocked storage) or fetch failed: // let the load the URL directly. loads.delete(hash); return url; } } /** Drop cached images that are no longer in the dataset (replaced or removed). */ export async function pruneImageCache(keep: Iterable): Promise { try { const wanted = new Set([...keep].map(keyUrl)); const cache = await caches.open(CACHE_NAME); for (const req of await cache.keys()) if (!wanted.has(req.url)) await cache.delete(req); } catch { // best effort } }