import type { SongMeta, SongRecord } from '../types.ts'; // Song library in IndexedDB. One object store keyed by song id; the whole // song is small JSON, so records are read and written whole. const DB_NAME = 'midiboot'; const STORE = 'songs'; // Retired object store, kept in the upgrade path so existing databases at // version 2 stay consistent with fresh ones. const INSTRUMENTS = 'instruments'; let dbPromise: Promise | null = null; function openDb(): Promise { if (dbPromise) return dbPromise; const attempt: Promise = new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 2); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE, { keyPath: 'id' }); if (!db.objectStoreNames.contains(INSTRUMENTS)) db.createObjectStore(INSTRUMENTS, { keyPath: 'id' }); }; // A tab holding an older version blocks the upgrade indefinitely; // surface that instead of hanging behind the loading screen. req.onblocked = () => reject(new Error('database upgrade blocked; close other midiboot tabs and reload')); req.onsuccess = () => { // Yield to a future upgrade (a newer tab) instead of blocking it. req.result.onversionchange = () => req.result.close(); resolve(req.result); }; req.onerror = () => reject(req.error); }); // Do not cache a failure: once a blocking tab closes, the next call // should get to try again. dbPromise = attempt; attempt.catch(() => { if (dbPromise === attempt) dbPromise = null; }); return attempt; } function txDone(tx: IDBTransaction): Promise { return new Promise((resolve, reject) => { tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error); }); } export async function putSong(rec: SongRecord): Promise { const db = await openDb(); const tx = db.transaction(STORE, 'readwrite'); tx.objectStore(STORE).put(rec); await txDone(tx); } export async function getSong(id: string): Promise { const db = await openDb(); return new Promise((resolve, reject) => { const req = db.transaction(STORE, 'readonly').objectStore(STORE).get(id); req.onsuccess = () => resolve((req.result as SongRecord) ?? null); req.onerror = () => reject(req.error); }); } export async function deleteSong(id: string): Promise { const db = await openDb(); const tx = db.transaction(STORE, 'readwrite'); tx.objectStore(STORE).delete(id); await txDone(tx); } export async function listSongs(): Promise { const db = await openDb(); return new Promise((resolve, reject) => { const req = db.transaction(STORE, 'readonly').objectStore(STORE).getAll(); req.onsuccess = () => { const recs = (req.result as SongRecord[]) ?? []; const metas = recs.map((r) => ({ id: r.id, name: r.name, updatedAt: r.updatedAt, trackCount: r.song.tracks.length, noteCount: r.song.tracks.reduce((s, t) => s + t.notes.length, 0), })); metas.sort((a, b) => b.updatedAt - a.updatedAt); resolve(metas); }; req.onerror = () => reject(req.error); }); }