99 lines
3.3 KiB
TypeScript
99 lines
3.3 KiB
TypeScript
|
|
import type { Track } from './types'
|
||
|
|
|
||
|
|
type CacheServerDescriptor = {
|
||
|
|
id: string
|
||
|
|
host: string
|
||
|
|
port?: string | number
|
||
|
|
ssl?: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
type PersistedTrack = Omit<Track, 'id'>
|
||
|
|
|
||
|
|
type LibraryCacheRecord = {
|
||
|
|
cacheKey: string
|
||
|
|
updatedAt: number
|
||
|
|
tracks: PersistedTrack[]
|
||
|
|
searchCache: Record<string, number[]>
|
||
|
|
}
|
||
|
|
|
||
|
|
export type LibraryCacheSnapshot = {
|
||
|
|
tracks: PersistedTrack[]
|
||
|
|
searchCache: Record<string, number[]>
|
||
|
|
}
|
||
|
|
|
||
|
|
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<IDBDatabase>((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<T>(mode: IDBTransactionMode, handler: (store: IDBObjectStore) => IDBRequest<T>) {
|
||
|
|
return new Promise<T>((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<LibraryCacheSnapshot | null> {
|
||
|
|
if (!cacheKey || typeof indexedDB === 'undefined') return null
|
||
|
|
|
||
|
|
const record = await withStore<LibraryCacheRecord | undefined>('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<string, number[]>) {
|
||
|
|
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))
|
||
|
|
}
|