/** * Submission Queue with IndexedDB Fallback * * Provides resilience when edge functions are unavailable by queuing * submissions locally and retrying when connectivity is restored. * * Part of Sacred Pipeline Phase 3: Fortify Defenses */ import { openDB, DBSchema, IDBPDatabase } from 'idb'; interface SubmissionQueueDB extends DBSchema { submissions: { key: string; value: { id: string; type: string; data: any; timestamp: number; retries: number; lastAttempt: number | null; error: string | null; }; }; } const DB_NAME = 'thrillwiki-submission-queue'; const DB_VERSION = 1; const STORE_NAME = 'submissions'; const MAX_RETRIES = 3; let dbInstance: IDBPDatabase | null = null; async function getDB(): Promise> { if (dbInstance) return dbInstance; dbInstance = await openDB(DB_NAME, DB_VERSION, { upgrade(db) { if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'id' }); } }, }); return dbInstance; } /** * Queue a submission for later processing */ export async function queueSubmission(type: string, data: any): Promise { const db = await getDB(); const id = crypto.randomUUID(); await db.add(STORE_NAME, { id, type, data, timestamp: Date.now(), retries: 0, lastAttempt: null, error: null, }); console.info(`[SubmissionQueue] Queued ${type} submission ${id}`); return id; } /** * Get all pending submissions */ export async function getPendingSubmissions() { const db = await getDB(); return await db.getAll(STORE_NAME); } /** * Get count of pending submissions */ export async function getPendingCount(): Promise { const db = await getDB(); const all = await db.getAll(STORE_NAME); return all.length; } /** * Remove a submission from the queue */ export async function removeFromQueue(id: string): Promise { const db = await getDB(); await db.delete(STORE_NAME, id); console.info(`[SubmissionQueue] Removed submission ${id}`); } /** * Update submission retry count and error */ export async function updateSubmissionRetry( id: string, error: string ): Promise { const db = await getDB(); const item = await db.get(STORE_NAME, id); if (!item) return; item.retries += 1; item.lastAttempt = Date.now(); item.error = error; await db.put(STORE_NAME, item); } /** * Process all queued submissions * Called when connectivity is restored or on app startup */ export async function processQueue( submitFn: (type: string, data: any) => Promise ): Promise<{ processed: number; failed: number }> { const db = await getDB(); const pending = await db.getAll(STORE_NAME); let processed = 0; let failed = 0; for (const item of pending) { try { console.info(`[SubmissionQueue] Processing ${item.type} submission ${item.id} (attempt ${item.retries + 1})`); await submitFn(item.type, item.data); await db.delete(STORE_NAME, item.id); processed++; console.info(`[SubmissionQueue] Successfully processed ${item.id}`); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); if (item.retries >= MAX_RETRIES - 1) { // Max retries exceeded, remove from queue await db.delete(STORE_NAME, item.id); failed++; console.error(`[SubmissionQueue] Max retries exceeded for ${item.id}:`, errorMsg); } else { // Update retry count await updateSubmissionRetry(item.id, errorMsg); console.warn(`[SubmissionQueue] Retry ${item.retries + 1}/${MAX_RETRIES} failed for ${item.id}:`, errorMsg); } } } return { processed, failed }; } /** * Clear all queued submissions (use with caution!) */ export async function clearQueue(): Promise { const db = await getDB(); const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); const all = await store.getAll(); await store.clear(); await tx.done; console.warn(`[SubmissionQueue] Cleared ${all.length} submissions from queue`); return all.length; } /** * Check if edge function is available */ export async function checkEdgeFunctionHealth( functionUrl: string ): Promise { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const response = await fetch(functionUrl, { method: 'HEAD', signal: controller.signal, }); clearTimeout(timeout); return response.ok || response.status === 405; // 405 = Method Not Allowed is OK } catch (error) { console.error('[SubmissionQueue] Health check failed:', error); return false; } }