/** * Offline audio queue using IndexedDB. * Stores recorded audio blobs for later processing when connectivity is available. */ const DB_NAME = 'sakhi_offline' const DB_VERSION = 2 const STORE_NAME = 'recordings' const CHUNKS_STORE = 'chunks' function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION) req.onupgradeneeded = () => { const db = req.result if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'id' }) } if (!db.objectStoreNames.contains(CHUNKS_STORE)) { const store = db.createObjectStore(CHUNKS_STORE, { keyPath: 'id', autoIncrement: true }) store.createIndex('sessionId', 'sessionId', { unique: false }) } } req.onsuccess = () => resolve(req.result) req.onerror = () => reject(req.error) }) } export async function saveRecording(audioBlob, visitType = 'auto', label = '', metadata = null) { const db = await openDB() const entry = { id: Date.now(), date: new Date().toLocaleString('en-IN'), audioBlob, audioType: audioBlob.type, size: audioBlob.size, visitType, metadata: metadata || null, label: label || `Recording ${new Date().toLocaleTimeString('en-IN')}`, status: 'pending', } return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite') tx.objectStore(STORE_NAME).put(entry) tx.oncomplete = () => resolve(entry) tx.onerror = () => reject(tx.error) }) } export async function getQueue() { const db = await openDB() return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly') const req = tx.objectStore(STORE_NAME).getAll() req.onsuccess = () => resolve(req.result.sort((a, b) => b.id - a.id)) req.onerror = () => reject(req.error) }) } export async function getRecording(id) { const db = await openDB() return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly') const req = tx.objectStore(STORE_NAME).get(id) req.onsuccess = () => resolve(req.result) req.onerror = () => reject(req.error) }) } export async function updateRecordingStatus(id, status) { const db = await openDB() return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite') const store = tx.objectStore(STORE_NAME) const req = store.get(id) req.onsuccess = () => { const entry = req.result if (entry) { entry.status = status store.put(entry) } tx.oncomplete = () => resolve(entry) } req.onerror = () => reject(req.error) }) } export async function removeRecording(id) { const db = await openDB() return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite') tx.objectStore(STORE_NAME).delete(id) tx.oncomplete = () => resolve() tx.onerror = () => reject(tx.error) }) } export async function clearQueue() { const db = await openDB() return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite') tx.objectStore(STORE_NAME).clear() tx.oncomplete = () => resolve() tx.onerror = () => reject(tx.error) }) } export async function appendChunk(sessionId, chunk, visitType = 'auto', metadata = null) { const db = await openDB() const entry = { sessionId, blob: chunk, blobType: chunk.type, size: chunk.size, visitType, metadata: metadata || null, createdAt: Date.now(), } return new Promise((resolve, reject) => { const tx = db.transaction(CHUNKS_STORE, 'readwrite') const req = tx.objectStore(CHUNKS_STORE).add(entry) req.onsuccess = () => resolve(req.result) tx.onerror = () => reject(tx.error) }) } export async function assembleChunks(sessionId) { const db = await openDB() return new Promise((resolve, reject) => { const tx = db.transaction(CHUNKS_STORE, 'readonly') const index = tx.objectStore(CHUNKS_STORE).index('sessionId') const req = index.getAll(sessionId) req.onsuccess = () => { const rows = req.result || [] if (!rows.length) { resolve(null); return } rows.sort((a, b) => a.id - b.id) const type = rows[0].blobType || 'audio/webm' const blob = new Blob(rows.map((r) => r.blob), { type }) resolve({ blob, visitType: rows[0].visitType, metadata: rows[0].metadata || null, chunkCount: rows.length, firstSeen: rows[0].createdAt, }) } req.onerror = () => reject(req.error) }) } export async function listOrphanedSessions() { const db = await openDB() return new Promise((resolve, reject) => { const tx = db.transaction(CHUNKS_STORE, 'readonly') const req = tx.objectStore(CHUNKS_STORE).getAll() req.onsuccess = () => { const rows = req.result || [] const bySession = new Map() for (const r of rows) { const cur = bySession.get(r.sessionId) if (!cur) { bySession.set(r.sessionId, { sessionId: r.sessionId, visitType: r.visitType, chunkCount: 1, totalSize: r.size || 0, firstSeen: r.createdAt, lastSeen: r.createdAt, }) } else { cur.chunkCount += 1 cur.totalSize += r.size || 0 if (r.createdAt < cur.firstSeen) cur.firstSeen = r.createdAt if (r.createdAt > cur.lastSeen) cur.lastSeen = r.createdAt } } resolve(Array.from(bySession.values()).sort((a, b) => b.firstSeen - a.firstSeen)) } req.onerror = () => reject(req.error) }) } export async function clearChunks(sessionId) { const db = await openDB() return new Promise((resolve, reject) => { const tx = db.transaction(CHUNKS_STORE, 'readwrite') const store = tx.objectStore(CHUNKS_STORE) const index = store.index('sessionId') const req = index.openCursor(IDBKeyRange.only(sessionId)) req.onsuccess = () => { const cursor = req.result if (cursor) { cursor.delete() cursor.continue() } } tx.oncomplete = () => resolve() tx.onerror = () => reject(tx.error) }) }