mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 18:51:11 -05:00
193 lines
4.8 KiB
TypeScript
193 lines
4.8 KiB
TypeScript
/**
|
|
* 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<SubmissionQueueDB> | null = null;
|
|
|
|
async function getDB(): Promise<IDBPDatabase<SubmissionQueueDB>> {
|
|
if (dbInstance) return dbInstance;
|
|
|
|
dbInstance = await openDB<SubmissionQueueDB>(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<string> {
|
|
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<number> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void>
|
|
): 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<number> {
|
|
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<boolean> {
|
|
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;
|
|
}
|
|
}
|