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 } const DATABASE_NAME = 'api-mediaplayer-library-cache' const DATABASE_VERSION = 1 const STORE_NAME = 'snapshots' const MAX_TRACKS = 5000 const MAX_SEARCHES = 250 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) }) } 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 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)) }