📄 src/downloader/lru.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// Self-pruning LRU: deletes oldest PDFs when storage exceeds 80% of quota.
16import { CACHES } from './keys';
17import { getAllDownloadRecords, deleteDownloadRecord, deleteBookData } from './meta-db';
18import { deletePdf } from './cache-store';
19import type { WorkerMessage } from './worker-protocol';
20
21const PRUNE_THRESHOLD = 0.80; // trigger pruning above this fraction
22const PRUNE_TARGET = 0.65; // free down to this fraction
23
24export interface PruneResult {
25 removed: number;
26 freedBytes: number;
27 prunedTxIds: string[];
28}
29
30export async function runLruPrune(
31 onStart?: (targetFreeBytes: number) => void,
32): Promise<PruneResult | null> {
33 const estimate = await navigator.storage.estimate();
34 const usage = estimate.usage ?? 0;
35 const quota = estimate.quota ?? Infinity;
36
37 if (usage / quota <= PRUNE_THRESHOLD) return null;
38
39 const targetFree = usage - quota * PRUNE_TARGET;
40 onStart?.(targetFree);
41
42 const records = await getAllDownloadRecords();
43 // PDFs only — previews are too small to matter.
44 const pdfs = records
45 .filter(r => r.fileType === 'pdf')
46 .sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
47
48 let freedBytes = 0;
49 let removed = 0;
50 const prunedTxIds: string[] = [];
51
52 for (const record of pdfs) {
53 if (freedBytes >= targetFree) break;
54 await deletePdf(record.txId);
55 await deleteDownloadRecord(record.txId);
56 await deleteBookData(record.txId);
57 await prunePagesByTxId(record.txId);
58 freedBytes += record.sizeBytes;
59 removed++;
60 prunedTxIds.push(record.txId);
61 }
62
63 return { removed, freedBytes, prunedTxIds };
64}
65
66// Emit StoragePruneStarted / StoragePruned messages to self (worker scope).
67export async function runLruPruneAndEmit(
68 postMsg: (msg: WorkerMessage) => void,
69): Promise<void> {
70 const estimate = await navigator.storage.estimate();
71 const usage = estimate.usage ?? 0;
72 const quota = estimate.quota ?? Infinity;
73 if (usage / quota <= PRUNE_THRESHOLD) return;
74
75 const targetFree = usage - quota * PRUNE_TARGET;
76 postMsg({ type: 'StoragePruneStarted', targetFreeBytes: targetFree });
77
78 const result = await runLruPrune();
79 if (result) {
80 postMsg({
81 type: 'StoragePruned',
82 removed: result.removed,
83 freedBytes: result.freedBytes,
84 prunedTxIds: result.prunedTxIds,
85 });
86 }
87}
88
89// Delete an entry from the pages cache by txId prefix.
90export async function prunePagesByTxId(txId: string): Promise<void> {
91 const cache = await caches.open(CACHES.pages);
92 const keys = await cache.keys();
93 const prefix = `/pages/${encodeURIComponent(txId)}/`;
94 await Promise.all(
95 keys
96 .filter(r => r.url.includes(prefix))
97 .map(r => cache.delete(r)),
98 );
99}
100