/** * IndexedDB backing for PdfByteStore — survives page reloads. * * Keys are scoped by session so multi-session demos don't share bytes. * Failures are logged and ignored so a broken IDB never blocks upload. */ import type { DocumentId } from "@citation-evidence/engine/shared"; const DB_NAME = "citation-evidence-pdf-bytes"; const DB_VERSION = 1; const STORE_NAME = "bytes"; export interface PersistedByteEntry { readonly key: string; readonly sessionId: string; readonly documentId: string; readonly contentType: string; readonly bytes: ArrayBuffer; } function entryKey(sessionId: string, documentId: string): string { return `${sessionId}::${documentId}`; } function openDb(): Promise { return new Promise((resolve, reject) => { if (typeof indexedDB === "undefined") { reject(new Error("indexedDB not available")); return; } const req = indexedDB.open(DB_NAME, DB_VERSION); req.onerror = () => reject(req.error ?? new Error("indexedDB open failed")); req.onsuccess = () => resolve(req.result); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: "key" }); store.createIndex("sessionId", "sessionId", { unique: false }); } }; }); } function idbReq(request: IDBRequest): Promise { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error ?? new Error("idb request failed")); }); } function txDone(tx: IDBTransaction): Promise { return new Promise((resolve, reject) => { tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error ?? new Error("idb tx failed")); tx.onabort = () => reject(tx.error ?? new Error("idb tx aborted")); }); } export async function idbPutBytes( sessionId: string, documentId: DocumentId, bytes: Uint8Array, contentType: string, ): Promise { try { const db = await openDb(); try { const copy = bytes.buffer.slice( bytes.byteOffset, bytes.byteOffset + bytes.byteLength, ) as ArrayBuffer; const entry: PersistedByteEntry = { key: entryKey(sessionId, documentId), sessionId, documentId, contentType, bytes: copy, }; const tx = db.transaction(STORE_NAME, "readwrite"); tx.objectStore(STORE_NAME).put(entry); await txDone(tx); } finally { db.close(); } } catch (err) { console.warn("idbPutBytes failed", err); } } export async function idbDeleteBytes( sessionId: string, documentId: DocumentId, ): Promise { try { const db = await openDb(); try { const tx = db.transaction(STORE_NAME, "readwrite"); tx.objectStore(STORE_NAME).delete(entryKey(sessionId, documentId)); await txDone(tx); } finally { db.close(); } } catch (err) { console.warn("idbDeleteBytes failed", err); } } export async function idbClearSession(sessionId: string): Promise { try { const db = await openDb(); try { const tx = db.transaction(STORE_NAME, "readwrite"); const store = tx.objectStore(STORE_NAME); const index = store.index("sessionId"); const keys = await idbReq(index.getAllKeys(IDBKeyRange.only(sessionId))); for (const key of keys) { store.delete(key); } await txDone(tx); } finally { db.close(); } } catch (err) { console.warn("idbClearSession failed", err); } } export async function idbListSession( sessionId: string, ): Promise { try { const db = await openDb(); try { const tx = db.transaction(STORE_NAME, "readonly"); const index = tx.objectStore(STORE_NAME).index("sessionId"); const rows = await idbReq(index.getAll(IDBKeyRange.only(sessionId))); await txDone(tx); return rows as PersistedByteEntry[]; } finally { db.close(); } } catch (err) { console.warn("idbListSession failed", err); return []; } }