jam-tracks / frontend /src /utils /presetCache.js
Mina Emadi
Add IndexedDB preset caching with background prefetch
d2f89e9
/**
* IndexedDB cache for preset stem PCM data.
* Stores raw int16 bytes + metadata so preset loads are instant after first visit.
*
* Bump CACHE_VERSION whenever preset audio files change on the server —
* old entries will be ignored and re-fetched.
*/
const DB_NAME = 'jam-tracks-presets'
const DB_VERSION = 1
const STORE_NAME = 'stems'
export const CACHE_VERSION = 1
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION)
req.onupgradeneeded = (e) => {
const db = e.target.result
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME)
}
}
req.onsuccess = (e) => resolve(e.target.result)
req.onerror = (e) => reject(e.target.error)
})
}
function dbGet(db, key) {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readonly')
const req = tx.objectStore(STORE_NAME).get(key)
req.onsuccess = (e) => resolve(e.target.result)
req.onerror = (e) => reject(e.target.error)
})
}
function dbPut(db, key, value) {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite')
const req = tx.objectStore(STORE_NAME).put(value, key)
req.onsuccess = () => resolve()
req.onerror = (e) => reject(e.target.error)
})
}
function stemKey(presetName, stemName) {
return `v${CACHE_VERSION}:${presetName}:${stemName}`
}
/** Returns true if all stemNames for this preset are cached. */
export async function isCached(presetName, stemNames) {
try {
const db = await openDB()
const checks = await Promise.all(stemNames.map(s => dbGet(db, stemKey(presetName, s))))
return checks.every(v => v != null)
} catch {
return false
}
}
/**
* Returns cached data for all stems or null if any stem is missing.
* Shape: { stemName: { bytes: ArrayBuffer, sampleRate, numChannels, numFrames } }
*/
export async function getCachedPreset(presetName, stemNames) {
try {
const db = await openDB()
const result = {}
for (const stemName of stemNames) {
const entry = await dbGet(db, stemKey(presetName, stemName))
if (!entry) return null
result[stemName] = entry
}
return result
} catch {
return null
}
}
/** Store a single stem's PCM data in IndexedDB. */
export async function cacheStem(presetName, stemName, data) {
try {
const db = await openDB()
await dbPut(db, stemKey(presetName, stemName), data)
} catch (err) {
console.warn('[presetCache] Failed to cache stem:', err)
}
}