1import type { SongMeta, SongRecord } from '../types.ts';
3// Song library in IndexedDB. One object store keyed by song id; the whole
4// song is small JSON, so records are read and written whole.
6const DB_NAME = 'midiboot';
7const STORE = 'songs';
8const INSTRUMENTS = 'instruments';
10let dbPromise: Promise<IDBDatabase> | null = null;
12function openDb(): Promise<IDBDatabase> {
13 if (dbPromise) return dbPromise;
14 dbPromise = new Promise((resolve, reject) => {
15 const req = indexedDB.open(DB_NAME, 2);
16 req.onupgradeneeded = () => {
17 const db = req.result;
18 if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE, { keyPath: 'id' });
19 if (!db.objectStoreNames.contains(INSTRUMENTS)) db.createObjectStore(INSTRUMENTS, { keyPath: 'id' });
20 };
21 req.onsuccess = () => resolve(req.result);
22 req.onerror = () => reject(req.error);
23 });
24 return dbPromise;
25}
27function txDone(tx: IDBTransaction): Promise<void> {
28 return new Promise((resolve, reject) => {
29 tx.oncomplete = () => resolve();
30 tx.onerror = () => reject(tx.error);
31 tx.onabort = () => reject(tx.error);
32 });
33}
35export async function putSong(rec: SongRecord): Promise<void> {
36 const db = await openDb();
37 const tx = db.transaction(STORE, 'readwrite');
38 tx.objectStore(STORE).put(rec);
39 await txDone(tx);
40}
42export async function getSong(id: string): Promise<SongRecord | null> {
43 const db = await openDb();
44 return new Promise((resolve, reject) => {
45 const req = db.transaction(STORE, 'readonly').objectStore(STORE).get(id);
46 req.onsuccess = () => resolve((req.result as SongRecord) ?? null);
47 req.onerror = () => reject(req.error);
48 });
49}
51export async function deleteSong(id: string): Promise<void> {
52 const db = await openDb();
53 const tx = db.transaction(STORE, 'readwrite');
54 tx.objectStore(STORE).delete(id);
55 await txDone(tx);
56}
58export async function listSongs(): Promise<SongMeta[]> {
59 const db = await openDb();
60 return new Promise((resolve, reject) => {
61 const req = db.transaction(STORE, 'readonly').objectStore(STORE).getAll();
62 req.onsuccess = () => {
63 const recs = (req.result as SongRecord[]) ?? [];
64 const metas = recs.map<SongMeta>((r) => ({
65 id: r.id,
66 name: r.name,
67 updatedAt: r.updatedAt,
68 trackCount: r.song.tracks.length,
69 noteCount: r.song.tracks.reduce((s, t) => s + t.notes.length, 0),
70 }));
71 metas.sort((a, b) => b.updatedAt - a.updatedAt);
72 resolve(metas);
73 };
74 req.onerror = () => reject(req.error);
75 });
76}
78// Imported instrument bundles (format numbl-instrument-v1), keyed by
79// bundle id. Whole records again: a bundle is tens of kilobytes of JSON.
81export interface ImportedInstrumentRecord {
82 id: string; // the bundle id
83 text: string; // the bundle JSON, kept verbatim as imported
84 importedAt: number;
85}
87export async function putImportedInstrument(rec: ImportedInstrumentRecord): Promise<void> {
88 const db = await openDb();
89 const tx = db.transaction(INSTRUMENTS, 'readwrite');
90 tx.objectStore(INSTRUMENTS).put(rec);
91 await txDone(tx);
92}
94export async function deleteImportedInstrument(id: string): Promise<void> {
95 const db = await openDb();
96 const tx = db.transaction(INSTRUMENTS, 'readwrite');
97 tx.objectStore(INSTRUMENTS).delete(id);
98 await txDone(tx);
99}
101export async function listImportedInstruments(): Promise<ImportedInstrumentRecord[]> {
102 const db = await openDb();
103 return new Promise((resolve, reject) => {
104 const req = db.transaction(INSTRUMENTS, 'readonly').objectStore(INSTRUMENTS).getAll();
105 req.onsuccess = () => resolve((req.result as ImportedInstrumentRecord[]) ?? []);
106 req.onerror = () => reject(req.error);
107 });
108}