📄 src/downloader/meta-db.ts
D-OPEN SOVEREIGN
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 *
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 *
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15// IndexedDB dl2_meta: small structured records only — no file bytes.
16
17export interface BookData {
18 txId: string;
19 dimensions: Array<{ width: number; height: number }>;
20 parsed: boolean;
21}
22
23export interface DownloadRecord {
24 txId: string;
25 fileType: 'pdf' | 'preview';
26 sizeBytes: number;
27 downloadedAt: number;
28 lastAccessedAt: number;
29}
30
31const DB_NAME = 'dl2_meta';
32const DB_VERSION = 1;
33
34let _db: IDBDatabase | null = null;
35
36export const openMetaDb = (): Promise<IDBDatabase> => {
37 if (_db) return Promise.resolve(_db);
38 return new Promise((resolve, reject) => {
39 const req = indexedDB.open(DB_NAME, DB_VERSION);
40 req.onupgradeneeded = (e) => {
41 const db = (e.target as IDBOpenDBRequest).result;
42 if (!db.objectStoreNames.contains('book_data')) {
43 db.createObjectStore('book_data', { keyPath: 'txId' });
44 }
45 if (!db.objectStoreNames.contains('downloads')) {
46 db.createObjectStore('downloads', { keyPath: 'txId' });
47 }
48 if (!db.objectStoreNames.contains('migration_checkpoints')) {
49 db.createObjectStore('migration_checkpoints', { keyPath: 'cacheName' });
50 }
51 };
52 req.onsuccess = (e) => {
53 _db = (e.target as IDBOpenDBRequest).result;
54 resolve(_db!);
55 };
56 req.onerror = () => reject(req.error);
57 });
58};
59
60const idbGet = <T>(store: string, key: string): Promise<T | undefined> =>
61 openMetaDb().then(db => new Promise((resolve, reject) => {
62 const tx = db.transaction(store, 'readonly');
63 const req = tx.objectStore(store).get(key);
64 req.onsuccess = () => resolve(req.result as T | undefined);
65 req.onerror = () => reject(req.error);
66 }));
67
68const idbPut = (store: string, value: unknown): Promise<void> =>
69 openMetaDb().then(db => new Promise((resolve, reject) => {
70 const tx = db.transaction(store, 'readwrite');
71 const req = tx.objectStore(store).put(value);
72 req.onsuccess = () => resolve();
73 req.onerror = () => reject(req.error);
74 }));
75
76const idbDelete = (store: string, key: string): Promise<void> =>
77 openMetaDb().then(db => new Promise((resolve, reject) => {
78 const tx = db.transaction(store, 'readwrite');
79 const req = tx.objectStore(store).delete(key);
80 req.onsuccess = () => resolve();
81 req.onerror = () => reject(req.error);
82 }));
83
84const idbGetAll = <T>(store: string): Promise<T[]> =>
85 openMetaDb().then(db => new Promise((resolve, reject) => {
86 const tx = db.transaction(store, 'readonly');
87 const req = tx.objectStore(store).getAll();
88 req.onsuccess = () => resolve(req.result as T[]);
89 req.onerror = () => reject(req.error);
90 }));
91
92export const getBookData = (txId: string) => idbGet<BookData>('book_data', txId);
93export const storeBookData = (data: BookData) => idbPut('book_data', data);
94export const getAllBookData = () => idbGetAll<BookData>('book_data');
95export const deleteBookData = (txId: string) => idbDelete('book_data', txId);
96
97export const getDownloadRecord = (txId: string) => idbGet<DownloadRecord>('downloads', txId);
98export const putDownloadRecord = (record: DownloadRecord) => idbPut('downloads', record);
99export const getAllDownloadRecords = () => idbGetAll<DownloadRecord>('downloads');
100export const deleteDownloadRecord = (txId: string) => idbDelete('downloads', txId);
101
102export const getMigrationCheckpoint = (cacheName: string): Promise<number> =>
103 idbGet<{ cacheName: string; idx: number }>('migration_checkpoints', cacheName)
104 .then(r => r?.idx ?? 0);
105
106export const saveMigrationCheckpoint = (cacheName: string, idx: number): Promise<void> =>
107 idbPut('migration_checkpoints', { cacheName, idx });
108
109export const clearMigrationCheckpoint = (cacheName: string): Promise<void> =>
110 idbDelete('migration_checkpoints', cacheName);
111
112// Close the open connection (if any) so a subsequent indexedDB.deleteDatabase()
113// doesn't block waiting for this tab's own connection to release.
114export const closeMetaDb = (): void => {
115 _db?.close();
116 _db = null;
117};
118
119// Reset the singleton for tests.
120export const _resetDbForTest = () => { _db = null; };
121