Files
mangabaka-seeder/src/web/history-db.ts
T
Lucas WintherandClaude Opus 5.5 2da992fd81 feat(web): history of searches on the page
Each search is kept in IndexedDB with its covers and edits; the History list and
a shelf of recent series on the start screen reopen it instantly. Searching a
series again keeps the edits of covers that are still there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:35:06 +02:00

61 lines
2.3 KiB
TypeScript

import { summarize, type HistoryStore, type SavedSearch, type SearchSummary } from "../core/saved";
// Spec 002 §3.2: IndexedDB on the page's origin. `summaries` is what the list reads; `searches` holds the bytes.
const DB = "mbseed";
const SUMMARIES = "summaries";
const SEARCHES = "searches";
const done = <T>(req: IDBRequest<T>): Promise<T> =>
new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error ?? new Error("IndexedDB request failed"));
});
const committed = (tx: IDBTransaction): Promise<void> =>
new Promise((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB transaction failed"));
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted"));
});
/** The page's history, or null when this browser won't give us IndexedDB (private windows, blocked storage). */
export async function openHistory(factory: IDBFactory | undefined = globalThis.indexedDB): Promise<HistoryStore | null> {
if (!factory) return null;
let db: IDBDatabase;
try {
const req = factory.open(DB, 1);
req.onupgradeneeded = () => {
req.result.createObjectStore(SUMMARIES, { keyPath: "seriesId" });
req.result.createObjectStore(SEARCHES, { keyPath: "seriesId" });
};
db = await done(req);
} catch {
return null;
}
const write = async (run: (summaries: IDBObjectStore, searches: IDBObjectStore) => void) => {
const tx = db.transaction([SUMMARIES, SEARCHES], "readwrite");
run(tx.objectStore(SUMMARIES), tx.objectStore(SEARCHES));
await committed(tx);
};
return {
list: async () => {
const all = await done(db.transaction(SUMMARIES).objectStore(SUMMARIES).getAll() as IDBRequest<SearchSummary[]>);
return all.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
},
get: async (id) => (await done(db.transaction(SEARCHES).objectStore(SEARCHES).get(id) as IDBRequest<SavedSearch | undefined>)) ?? null,
put: (s) => write((summaries, searches) => {
summaries.put(summarize(s));
searches.put(s);
}),
remove: (id) => write((summaries, searches) => {
summaries.delete(id);
searches.delete(id);
}),
clear: () => write((summaries, searches) => {
summaries.clear();
searches.clear();
}),
};
}