๐Ÿ“„ src/downloader/state.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// Canonical, framework-agnostic, synchronous-read state store (ยง5.6).
16
17export type FilePhase = 'unknown' | 'queued' | 'downloading' | 'done' | 'error';
18
19export interface FileState {
20    phase:   FilePhase;
21    loaded?: number;
22    total?:  number;
23    error?:  string;
24}
25
26export interface RenderState {
27    bookData?: import('./worker-protocol').BookData;
28    pages:     Map<number, FilePhase>;
29}
30
31export interface PruningState {
32    active:       boolean;
33    freedBytes:   number;
34    removedCount: number;
35}
36
37type Listener = (changedKey: string) => void;
38
39// Internal mutable state.
40const _files:   Map<string, FileState>   = new Map();
41const _renders: Map<string, RenderState> = new Map();
42let   _pruning: PruningState = { active: false, freedBytes: 0, removedCount: 0 };
43const _listeners: Set<Listener> = new Set();
44
45function _notify(key: string): void {
46    for (const l of _listeners) l(key);
47}
48
49// โ”€โ”€โ”€ Public reader โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
50
51export const downloadState = {
52    get files():   ReadonlyMap<string, FileState>   { return _files; },
53    get renders(): ReadonlyMap<string, RenderState> { return _renders; },
54    get pruning(): Readonly<PruningState>           { return _pruning; },
55
56    get(txId: string): FileState {
57        return _files.get(txId) ?? { phase: 'unknown' };
58    },
59
60    getRender(txId: string): RenderState {
61        return _renders.get(txId) ?? { pages: new Map() };
62    },
63
64    summary(): { active: number; queued: number; loaded: number; total: number } {
65        let active = 0, queued = 0, loaded = 0, total = 0;
66        for (const s of _files.values()) {
67            if (s.phase === 'downloading') { active++; loaded += s.loaded ?? 0; total += s.total ?? 0; }
68            if (s.phase === 'queued')      { queued++; }
69        }
70        return { active, queued, loaded, total };
71    },
72
73    subscribe(listener: Listener): () => void {
74        _listeners.add(listener);
75        return () => _listeners.delete(listener);
76    },
77};
78
79// โ”€โ”€โ”€ Internal writers (not exported from index barrel) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
80
81export function _setFileState(txId: string, patch: Partial<FileState>): void {
82    const prev = _files.get(txId) ?? { phase: 'unknown' };
83    _files.set(txId, { ...prev, ...patch });
84    _notify(`file:${txId}`);
85}
86
87export function _setRenderPage(txId: string, page: number, phase: FilePhase): void {
88    let rs = _renders.get(txId);
89    if (!rs) { rs = { pages: new Map() }; _renders.set(txId, rs); }
90    rs.pages.set(page, phase);
91    _notify(`render:${txId}`);
92}
93
94export function _setBookData(
95    txId: string,
96    bookData: import('./worker-protocol').BookData,
97): void {
98    let rs = _renders.get(txId);
99    if (!rs) { rs = { pages: new Map() }; _renders.set(txId, rs); }
100    rs.bookData = bookData;
101    _notify(`render:${txId}`);
102}
103
104export function _clearRender(txId: string): void {
105    if (_renders.delete(txId)) _notify(`render:${txId}`);
106}
107
108export function _clearFileState(txId: string): void {
109    if (_files.delete(txId)) _notify(`file:${txId}`);
110}
111
112export function _setPruning(patch: Partial<PruningState>): void {
113    _pruning = { ..._pruning, ...patch };
114    _notify('pruning');
115}
116
117export function _hydrateFiles(entries: Array<[string, FileState]>): void {
118    for (const [txId, state] of entries) _files.set(txId, state);
119}
120
121export function _hydrateRenders(
122    entries: Array<[string, Array<[number, FilePhase]>]>,
123): void {
124    for (const [txId, pages] of entries) {
125        const rs: RenderState = _renders.get(txId) ?? { pages: new Map() };
126        for (const [page, phase] of pages) rs.pages.set(page, phase);
127        _renders.set(txId, rs);
128    }
129}
130
131// v1 compat: re-emit named window CustomEvents from the store (ยง5.6.2).
132export function _emitLegacyEvent(name: string, detail: unknown): void {
133    if (typeof window !== 'undefined') {
134        window.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
135    }
136}
137