import { getCurrentAccessToken } from './auth'; const MAX_ENTRIES = 250; /** Shared blob URLs for authenticated Matrix media (avatars, thumbs, etc.). */ const blobCache = new Map(); const inflight = new Map>(); export const isAuthenticatedMediaUrl = (url: string): boolean => url.includes('/_matrix/client/v1/media/download') || url.includes('/_matrix/client/v1/media/thumbnail') || (url.includes('/_matrix/media/') && (url.includes('/download/') || url.includes('/thumbnail/'))); export const getCachedAuthenticatedMediaUrl = (src: string | undefined): string | undefined => { if (!src) return undefined; return blobCache.get(src); }; const touch = (src: string, blobUrl: string) => { // Re-insert for LRU ordering (Map preserves insertion order). if (blobCache.has(src)) blobCache.delete(src); blobCache.set(src, blobUrl); while (blobCache.size > MAX_ENTRIES) { const oldest = blobCache.keys().next().value as string | undefined; if (!oldest) break; const oldUrl = blobCache.get(oldest); blobCache.delete(oldest); if (oldUrl) URL.revokeObjectURL(oldUrl); } }; const fetchBlobUrl = async (src: string): Promise => { const accessToken = getCurrentAccessToken(); let response = await fetch(src, { method: 'GET', headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined, }); if (!response.ok && response.status === 401 && accessToken) { response = await fetch(src, { method: 'GET' }); } if (!response.ok) { throw new Error(`Failed to fetch authenticated media: ${response.status}`); } const blob = await response.blob(); return URL.createObjectURL(blob); }; /** * Returns a stable blob: URL for authenticated media, reusing cache across mounts. * Non-auth URLs are returned as-is. */ export const resolveAuthenticatedMediaUrl = async ( src: string, useAuthentication: boolean ): Promise => { if (!useAuthentication || !isAuthenticatedMediaUrl(src)) { return src; } const cached = blobCache.get(src); if (cached) { touch(src, cached); return cached; } let pending = inflight.get(src); if (!pending) { pending = fetchBlobUrl(src) .then((blobUrl) => { touch(src, blobUrl); inflight.delete(src); return blobUrl; }) .catch((err) => { inflight.delete(src); throw err; }); inflight.set(src, pending); } return pending; };