// Shared, session-lifetime cache for the bytes behind AuthenticatedImage's `src` (a // `/media/...` hash-addressed path - see backend/files/media_urls.py). Content there is // immutable and hash-addressed (SHA-256), so a cache hit never needs revalidation: the // same `src` string can only ever resolve to the same bytes, for any owner/requester. // // Deliberately NOT Vuex state - this holds Blob/object-URL data that nothing needs // reactive access to (components only bind the resulting object-URL string, which they // hold in their own local state), so a plain module-level Map avoids Vue's reactivity // overhead entirely and sidesteps proxying Blob instances for no benefit. const MAX_BYTES = 150 * 1024 * 1024; // budget for decoded image bytes before evicting LRU entries class FileCache { constructor() { this._entries = new Map(); // key -> {url, size}; Map iteration order doubles as LRU order this._inflight = new Map(); // key -> Promise, de-duplicates concurrent callers this._totalBytes = 0; } _touch(key) { // Delete+re-insert moves this entry to the "most recently used" end of the // Map's iteration order, without needing a separate linked list. const entry = this._entries.get(key); this._entries.delete(key); this._entries.set(key, entry); } _evict(key) { const entry = this._entries.get(key); if (!entry) return; URL.revokeObjectURL(entry.url); this._totalBytes -= entry.size; this._entries.delete(key); } _evictUntilUnderBudget() { for (const key of this._entries.keys()) { if (this._totalBytes <= MAX_BYTES) break; this._evict(key); // oldest (least recently used) first - Map iterates in insertion order } } // fetcher: () => Promise. Called at most once per key even if many components // ask for the same key while the first request is still in flight. async get(key, fetcher) { if (this._entries.has(key)) { this._touch(key); return this._entries.get(key).url; } if (this._inflight.has(key)) { return this._inflight.get(key); } const promise = (async () => { const blob = await fetcher(); const url = URL.createObjectURL(blob); this._entries.set(key, {url, size: blob.size}); this._totalBytes += blob.size; this._evictUntilUnderBudget(); return url; })(); this._inflight.set(key, promise); try { return await promise; } finally { this._inflight.delete(key); } } invalidate(key) { this._evict(key); } clear() { for (const key of [...this._entries.keys()]) this._evict(key); } } // Singleton - every AuthenticatedImage instance shares the same cache. export default new FileCache();