Skip to content

Instantly share code, notes, and snippets.

@ccurtin
Created June 5, 2025 16:21
Show Gist options
  • Save ccurtin/2f01a227cd42ade9f9455067f1cfbb9e to your computer and use it in GitHub Desktop.
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.
/**
* 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