evidence-source/src/browser/byte-store-idb.ts
tegwick b3eaa4b406
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Persist PDF bytes in IndexedDB for session reload.
Mirror PdfByteStore put/delete/clear into a session-scoped IndexedDB store
and expose hydrate() so the umbrella can restore bytes after page reload.
2026-07-30 20:05:24 +02:00

147 lines
4.1 KiB
TypeScript

/**
* 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<IDBDatabase> {
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<T>(request: IDBRequest<T>): Promise<T> {
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<void> {
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<void> {
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<void> {
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<void> {
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<readonly PersistedByteEntry[]> {
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 [];
}
}