/** * IndexedDB Failed Uploads Storage * Handles storing and retrying failed uploads with background sync */ const DB_NAME = 'SpeakrFailedUploads'; const DB_VERSION = 1; const STORE_NAME = 'failedUploads'; let dbInstance = null; /** * Initialize IndexedDB */ export const initDB = () => { return new Promise((resolve, reject) => { if (dbInstance) { resolve(dbInstance); return; } const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => { console.error('[FailedUploadsDB] Failed to open database:', request.error); reject(request.error); }; request.onsuccess = () => { dbInstance = request.result; console.log('[FailedUploadsDB] Database opened successfully'); resolve(dbInstance); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Create object store for failed uploads if (!db.objectStoreNames.contains(STORE_NAME)) { const objectStore = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true }); objectStore.createIndex('timestamp', 'timestamp', { unique: false }); objectStore.createIndex('clientId', 'clientId', { unique: false }); console.log('[FailedUploadsDB] Object store created'); } }; }); }; /** * Store a failed upload for later retry */ export const storeFailedUpload = async (uploadData) => { try { const db = await initDB(); // Convert File to ArrayBuffer BEFORE opening transaction. // IDB transactions auto-close when inactive — the async arrayBuffer() // call would cause the transaction to expire before add(). let fileData = uploadData.fileData || null; if (uploadData.file && !fileData) { fileData = await uploadData.file.arrayBuffer(); } const transaction = db.transaction([STORE_NAME], 'readwrite'); const objectStore = transaction.objectStore(STORE_NAME); const failedUpload = { timestamp: Date.now(), clientId: uploadData.clientId || `client-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, fileName: uploadData.file?.name || uploadData.fileName || 'unknown', fileSize: uploadData.file?.size || uploadData.fileSize || 0, notes: uploadData.notes || '', tags: uploadData.tags || [], asrOptions: uploadData.asrOptions || {}, retryCount: uploadData.retryCount || 0, lastError: uploadData.error || '', fileData: fileData, mimeType: uploadData.file?.type || uploadData.mimeType || 'audio/webm' }; const request = objectStore.add(failedUpload); return new Promise((resolve, reject) => { request.onsuccess = () => { console.log('[FailedUploadsDB] Upload stored for retry:', failedUpload.fileName); resolve(request.result); // Returns the ID }; request.onerror = () => { console.error('[FailedUploadsDB] Failed to store upload:', request.error); reject(request.error); }; }); } catch (error) { console.error('[FailedUploadsDB] Error storing failed upload:', error); throw error; } }; /** * Get all failed uploads waiting to retry */ export const getFailedUploads = async () => { try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readonly'); const objectStore = transaction.objectStore(STORE_NAME); return new Promise((resolve, reject) => { const request = objectStore.getAll(); request.onsuccess = () => { console.log(`[FailedUploadsDB] Retrieved ${request.result.length} failed uploads`); resolve(request.result); }; request.onerror = () => { console.error('[FailedUploadsDB] Failed to retrieve uploads:', request.error); reject(request.error); }; }); } catch (error) { console.error('[FailedUploadsDB] Error getting failed uploads:', error); return []; } }; /** * Get a specific failed upload by ID */ export const getFailedUpload = async (id) => { try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readonly'); const objectStore = transaction.objectStore(STORE_NAME); return new Promise((resolve, reject) => { const request = objectStore.get(id); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { console.error('[FailedUploadsDB] Failed to get upload:', request.error); reject(request.error); }; }); } catch (error) { console.error('[FailedUploadsDB] Error getting failed upload:', error); return null; } }; /** * Update retry count for a failed upload */ export const updateRetryCount = async (id, retryCount, error = null) => { try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const objectStore = transaction.objectStore(STORE_NAME); const upload = await getFailedUpload(id); if (!upload) { console.warn('[FailedUploadsDB] Upload not found for retry count update'); return; } upload.retryCount = retryCount; upload.lastRetry = Date.now(); if (error) { upload.lastError = error; } return new Promise((resolve, reject) => { const request = objectStore.put(upload); request.onsuccess = () => { console.log(`[FailedUploadsDB] Updated retry count for upload ${id}: ${retryCount}`); resolve(); }; request.onerror = () => { console.error('[FailedUploadsDB] Failed to update retry count:', request.error); reject(request.error); }; }); } catch (error) { console.error('[FailedUploadsDB] Error updating retry count:', error); } }; /** * Delete a failed upload (after successful retry) */ export const deleteFailedUpload = async (id) => { try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const objectStore = transaction.objectStore(STORE_NAME); return new Promise((resolve, reject) => { const request = objectStore.delete(id); request.onsuccess = () => { console.log('[FailedUploadsDB] Deleted successful upload:', id); resolve(); }; request.onerror = () => { console.error('[FailedUploadsDB] Failed to delete upload:', request.error); reject(request.error); }; }); } catch (error) { console.error('[FailedUploadsDB] Error deleting failed upload:', error); } }; /** * Clear all failed uploads */ export const clearAllFailedUploads = async () => { try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const objectStore = transaction.objectStore(STORE_NAME); return new Promise((resolve, reject) => { const request = objectStore.clear(); request.onsuccess = () => { console.log('[FailedUploadsDB] Cleared all failed uploads'); resolve(); }; request.onerror = () => { console.error('[FailedUploadsDB] Failed to clear uploads:', request.error); reject(request.error); }; }); } catch (error) { console.error('[FailedUploadsDB] Error clearing failed uploads:', error); } }; /** * Get count of failed uploads */ export const getFailedUploadCount = async () => { try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readonly'); const objectStore = transaction.objectStore(STORE_NAME); return new Promise((resolve, reject) => { const request = objectStore.count(); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { console.error('[FailedUploadsDB] Failed to count uploads:', request.error); reject(request.error); }; }); } catch (error) { console.error('[FailedUploadsDB] Error counting failed uploads:', error); return 0; } };