/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
midiboot / src / storage / db.ts
91 lines · 3.2 KBCodeBlameHistory
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 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';
cdfbf5bRender whole tracks through direct-import instrument modulesJeremy Magland 8// Retired object store, kept in the upgrade path so existing databases at
9// version 2 stay consistent with fresh ones.
0557e3bSwitch to exported instrument bundles with numbl driversJeremy Magland 10const INSTRUMENTS = 'instruments';
12let dbPromise: Promise<IDBDatabase> | null = null;
14function openDb(): Promise<IDBDatabase> {
15 if (dbPromise) return dbPromise;
fa5c12eDo not hang on a blocked IndexedDB upgradeJeremy Magland 16 const attempt: Promise<IDBDatabase> = new Promise((resolve, reject) => {
0557e3bSwitch to exported instrument bundles with numbl driversJeremy Magland 17 const req = indexedDB.open(DB_NAME, 2);
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 18 req.onupgradeneeded = () => {
19 const db = req.result;
20 if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE, { keyPath: 'id' });
0557e3bSwitch to exported instrument bundles with numbl driversJeremy Magland 21 if (!db.objectStoreNames.contains(INSTRUMENTS)) db.createObjectStore(INSTRUMENTS, { keyPath: 'id' });
fa5c12eDo not hang on a blocked IndexedDB upgradeJeremy Magland 23 // A tab holding an older version blocks the upgrade indefinitely;
24 // surface that instead of hanging behind the loading screen.
25 req.onblocked = () => reject(new Error('database upgrade blocked; close other midiboot tabs and reload'));
26 req.onsuccess = () => {
27 // Yield to a future upgrade (a newer tab) instead of blocking it.
28 req.result.onversionchange = () => req.result.close();
29 resolve(req.result);
30 };
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 31 req.onerror = () => reject(req.error);
32 });
fa5c12eDo not hang on a blocked IndexedDB upgradeJeremy Magland 33 // Do not cache a failure: once a blocking tab closes, the next call
34 // should get to try again.
35 dbPromise = attempt;
36 attempt.catch(() => {
37 if (dbPromise === attempt) dbPromise = null;
38 });
39 return attempt;
42function txDone(tx: IDBTransaction): Promise<void> {
43 return new Promise((resolve, reject) => {
44 tx.oncomplete = () => resolve();
45 tx.onerror = () => reject(tx.error);
46 tx.onabort = () => reject(tx.error);
47 });
50export async function putSong(rec: SongRecord): Promise<void> {
51 const db = await openDb();
52 const tx = db.transaction(STORE, 'readwrite');
53 tx.objectStore(STORE).put(rec);
54 await txDone(tx);
57export async function getSong(id: string): Promise<SongRecord | null> {
58 const db = await openDb();
59 return new Promise((resolve, reject) => {
60 const req = db.transaction(STORE, 'readonly').objectStore(STORE).get(id);
61 req.onsuccess = () => resolve((req.result as SongRecord) ?? null);
62 req.onerror = () => reject(req.error);
63 });
66export async function deleteSong(id: string): Promise<void> {
67 const db = await openDb();
68 const tx = db.transaction(STORE, 'readwrite');
69 tx.objectStore(STORE).delete(id);
70 await txDone(tx);
73export async function listSongs(): Promise<SongMeta[]> {
74 const db = await openDb();
75 return new Promise((resolve, reject) => {
76 const req = db.transaction(STORE, 'readonly').objectStore(STORE).getAll();
77 req.onsuccess = () => {
78 const recs = (req.result as SongRecord[]) ?? [];
79 const metas = recs.map<SongMeta>((r) => ({
80 id: r.id,
81 name: r.name,
82 updatedAt: r.updatedAt,
83 trackCount: r.song.tracks.length,
84 noteCount: r.song.tracks.reduce((s, t) => s + t.notes.length, 0),
85 }));
86 metas.sort((a, b) => b.updatedAt - a.updatedAt);
87 resolve(metas);
88 };
89 req.onerror = () => reject(req.error);
90 });
↑↓ move↵openescclose