๐ 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