📄 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