📄 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