Mirror PdfByteStore put/delete/clear into a session-scoped IndexedDB store and expose hydrate() so the umbrella can restore bytes after page reload.
147 lines
4.1 KiB
TypeScript
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 [];
|
|
}
|
|
}
|