import type { Track } from './types' type CacheServerDescriptor = { id: string host: string port?: string | number ssl?: boolean } type PersistedTrack = Omit type LibraryCacheRecord = { cacheKey: string updatedAt: number tracks: PersistedTrack[] searchCache: Record } export type LibraryCacheSnapshot = { tracks: PersistedTrack[] searchCache: Record } export type LibraryCacheStats = { snapshotCount: number staleSnapshotCount: number totalBytes: number activeBytes: number trackCount: number searchEntryCount: number updatedAt: number | null } const DATABASE_NAME = 'api-mediaplayer-library-cache' const DATABASE_VERSION = 1 const STORE_NAME = 'snapshots' const MAX_TRACKS = 5000 const MAX_SEARCHES = 250 const textEncoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null export function buildLibraryCacheKey(servers: CacheServerDescriptor[]) { return servers.map((server) => `${server.id}:${server.host}:${server.port ?? ''}:${server.ssl ? 'https' : 'http'}`).join('|') } function openLibraryCacheDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION) request.onupgradeneeded = () => { const database = request.result if (!database.objectStoreNames.contains(STORE_NAME)) { database.createObjectStore(STORE_NAME, { keyPath: 'cacheKey' }) } } request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error ?? new Error('Failed to open library cache database')) }) } function withStore(mode: IDBTransactionMode, handler: (store: IDBObjectStore) => IDBRequest) { return new Promise((resolve, reject) => { openLibraryCacheDatabase() .then((database) => { const transaction = database.transaction(STORE_NAME, mode) const store = transaction.objectStore(STORE_NAME) const request = handler(store) request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed')) transaction.oncomplete = () => database.close() transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB transaction failed')) transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB transaction aborted')) }) .catch(reject) }) } function estimateRecordBytes(record: LibraryCacheRecord) { const payload = JSON.stringify(record) if (!textEncoder) return payload.length return textEncoder.encode(payload).length } async function listLibraryCacheRecords(): Promise { if (typeof indexedDB === 'undefined') return [] const records = await withStore('readonly', (store) => store.getAll()) return Array.isArray(records) ? records : [] } export async function loadLibraryCache(cacheKey: string): Promise { if (!cacheKey || typeof indexedDB === 'undefined') return null const record = await withStore('readonly', (store) => store.get(cacheKey)) if (!record) return null return { tracks: Array.isArray(record.tracks) ? record.tracks : [], searchCache: record.searchCache && typeof record.searchCache === 'object' ? record.searchCache : {}, } } export async function pruneLibraryCache(activeCacheKey: string) { if (!activeCacheKey || typeof indexedDB === 'undefined') return 0 const records = await listLibraryCacheRecords() const staleKeys = records .map((record) => record.cacheKey) .filter((cacheKey) => cacheKey !== activeCacheKey) if (staleKeys.length === 0) return 0 await Promise.all(staleKeys.map((cacheKey) => withStore('readwrite', (store) => store.delete(cacheKey)))) return staleKeys.length } export async function getLibraryCacheStats(activeCacheKey: string): Promise { if (!activeCacheKey || typeof indexedDB === 'undefined') { return { snapshotCount: 0, staleSnapshotCount: 0, totalBytes: 0, activeBytes: 0, trackCount: 0, searchEntryCount: 0, updatedAt: null, } } const records = await listLibraryCacheRecords() const activeRecord = records.find((record) => record.cacheKey === activeCacheKey) return { snapshotCount: records.length, staleSnapshotCount: records.filter((record) => record.cacheKey !== activeCacheKey).length, totalBytes: records.reduce((sum, record) => sum + estimateRecordBytes(record), 0), activeBytes: activeRecord ? estimateRecordBytes(activeRecord) : 0, trackCount: activeRecord?.tracks.length ?? 0, searchEntryCount: activeRecord ? Object.keys(activeRecord.searchCache || {}).length : 0, updatedAt: activeRecord?.updatedAt ?? null, } } export async function saveLibraryCache(cacheKey: string, tracks: Track[], searchCache: Record) { if (!cacheKey || typeof indexedDB === 'undefined') return const trimmedTracks = tracks .filter((track) => track.serverId && track.fileId != null && track.url) .slice(-MAX_TRACKS) .map(({ id: _id, ...track }) => track) const trimmedSearchCache = Object.fromEntries(Object.entries(searchCache).slice(-MAX_SEARCHES)) const record: LibraryCacheRecord = { cacheKey, updatedAt: Date.now(), tracks: trimmedTracks, searchCache: trimmedSearchCache, } await withStore('readwrite', (store) => store.put(record)) await pruneLibraryCache(cacheKey) }