Created
June 5, 2025 16:21
-
-
Save ccurtin/2f01a227cd42ade9f9455067f1cfbb9e to your computer and use it in GitHub Desktop.
Made this quickly for building out a custom DAW(Digital Audio Workstation), so that when users have multiple tabs open, their actions/data persist.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* storageEngine provides IndexedDB-based persistence for timeline clips, | |
* with real-time pub/sub updates using BroadcastChannel. | |
* | |
* @namespace storageEngine | |
* @property {IDBDatabase|null} db - The open IndexedDB instance | |
* @property {string} dbName - Database name | |
* @property {string} storeName - Object store name for clips | |
* @property {BroadcastChannel} channel - Cross-tab communication channel | |
* @property {Set<Function>} subscribers - Local listeners for updates | |
*/ | |
const storageEngine = { | |
db: null, | |
dbName: "clipTimelineDB", | |
storeName: "clips", | |
channel: new BroadcastChannel("clip-timeline-storage"), | |
subscribers: new Set(), | |
document_storage: null, // Document storage for fallback mode(can be local file system or other storage) | |
/** | |
* Opens (or creates) the IndexedDB database. | |
* @returns {Promise<void>} | |
* @example | |
* await storageEngine.open(); | |
*/ | |
async open() { | |
if (this.db) return; | |
return new Promise((resolve, reject) => { | |
const request = indexedDB.open(this.dbName, 1); | |
request.onupgradeneeded = (event) => { | |
const db = event.target.result; | |
if (!db.objectStoreNames.contains(this.storeName)) { | |
db.createObjectStore(this.storeName, { keyPath: "id" }); | |
} | |
}; | |
request.onsuccess = (event) => { | |
this.db = event.target.result; | |
resolve(); | |
}; | |
request.onerror = (event) => reject(event.target.error); | |
}); | |
}, | |
/** | |
* Saves a clip object to IndexedDB. | |
* @param {Object} clip - The clip object to save (must have a unique `id` property) | |
* @returns {Promise<void>} | |
* @example | |
* await storageEngine.saveClip({ id: "c1", startTime: 0, duration: 2 }); | |
*/ | |
async saveClip(clip) { | |
await this.open(); | |
return new Promise((resolve, reject) => { | |
const tx = this.db.transaction(this.storeName, "readwrite"); | |
const store = tx.objectStore(this.storeName); | |
const req = store.put(clip); | |
req.onsuccess = () => { | |
this._notify({ type: "save", clip }); | |
resolve(); | |
}; | |
req.onerror = (e) => reject(e.target.error); | |
}); | |
}, | |
/** | |
* Retrieves a single clip by its ID. | |
* @param {string} id - The clip ID | |
* @returns {Promise<Object|null>} The clip object or null if not found | |
* @example | |
* const clip = await storageEngine.getClip("c1"); | |
*/ | |
async getClip(id) { | |
await this.open(); | |
return new Promise((resolve, reject) => { | |
const tx = this.db.transaction(this.storeName, "readonly"); | |
const store = tx.objectStore(this.storeName); | |
const req = store.get(id); | |
req.onsuccess = () => resolve(req.result); | |
req.onerror = (e) => reject(e.target.error); | |
}); | |
}, | |
/** | |
* Retrieves all clips from storage. | |
* @returns {Promise<Object[]>} Array of clip objects | |
* @example | |
* const allClips = await storageEngine.getAllClips(); | |
*/ | |
async getAllClips() { | |
await this.open(); | |
return new Promise((resolve, reject) => { | |
const tx = this.db.transaction(this.storeName, "readonly"); | |
const store = tx.objectStore(this.storeName); | |
const req = store.getAll(); | |
req.onsuccess = () => resolve(req.result || []); | |
req.onerror = (e) => reject(e.target.error); | |
}); | |
}, | |
/** | |
* Deletes a clip by ID. | |
* @param {string} id - The clip ID to delete | |
* @returns {Promise<void>} | |
* @example | |
* await storageEngine.deleteClip("c1"); | |
*/ | |
async deleteClip(id) { | |
await this.open(); | |
return new Promise((resolve, reject) => { | |
const tx = this.db.transaction(this.storeName, "readwrite"); | |
const store = tx.objectStore(this.storeName); | |
const req = store.delete(id); | |
req.onsuccess = () => { | |
this._notify({ type: "delete", id }); | |
resolve(); | |
}; | |
req.onerror = (e) => reject(e.target.error); | |
}); | |
}, | |
/** | |
* Subscribe to storage changes. Handler is called with the change event. | |
* @param {Function} handler | |
* @returns {Function} Unsubscribe function | |
*/ | |
subscribe(handler) { | |
this.subscribers.add(handler); | |
return () => this.subscribers.delete(handler); | |
}, | |
_notify(event) { | |
// Notify local subscribers | |
this.subscribers.forEach((cb) => cb(event)); | |
// Broadcast to other tabs/contexts | |
this.channel.postMessage(event); | |
}, | |
_handleChannelMessage(event) { | |
// Ignore events from this tab; only notify local subscribers | |
this.subscribers.forEach((cb) => cb(event.data)); | |
}, | |
}; | |
// Listen to BroadcastChannel for real-time cross-tab updates | |
storageEngine.channel.addEventListener("message", (ev) => | |
storageEngine._handleChannelMessage(ev) | |
); | |
// Example usage: | |
storageEngine.subscribe((event) => { | |
console.log("Storage event:", event); | |
}); | |
// ------------------------------------ // | |
// Add a clip | |
await storageEngine.saveClip({ | |
id: "c1", | |
startTime: 0, | |
duration: 2, | |
}); | |
// Get all clips | |
const clips = await storageEngine.getAllClips(); | |
console.log("Clips:", clips); | |
// Delete a clip | |
await storageEngine.deleteClip("c1"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment