browser / src /lib /imageCache.ts
lrigamai's picture
Group LoRAs by character, add table view and docked detail panel
45fb44a
Raw History Blame Contribute Delete
2.39 kB
// 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 <LoraPreview> showing that image.
const loads = new Map<string, Promise<string>>();
// 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<string, string>();
/** 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<string> {
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<string> {
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 <img> 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<string>): Promise<void> {
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
}
}