๐ src/downloader/worker/file-worker.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// File download worker (Phase A: dedicated). Handles GetFile + CancelDownload.
16import { fetchFromGateways, DEFAULT_GATEWAYS } from '../fetcher';
17import { putPdf, putPreview } from '../cache-store';
18import { putDownloadRecord } from '../meta-db';
19import { runLruPruneAndEmit } from '../lru';
20import type { WorkerCommand, WorkerMessage } from '../worker-protocol';
21
22// Gateways can be overridden via an init message (future extension).
23let gateways = DEFAULT_GATEWAYS;
24
25function post(msg: WorkerMessage): void {
26 (self as unknown as Worker).postMessage(msg);
27}
28
29async function handleGetFile(txId: string, mime: 'pdf' | 'preview'): Promise<void> {
30 try {
31 const response = await fetchFromGateways(txId, gateways, (loaded, total) => {
32 post({ type: 'DownloadProgress', txId, loaded, total });
33 });
34
35 // Determine correct Content-Type.
36 const ct = mime === 'pdf'
37 ? 'application/pdf'
38 : (response.headers.get('content-type') || 'application/octet-stream');
39
40 // Content-Length can't be trusted here: fetcher.ts strips it (it would
41 // otherwise describe the pre-decompression byte count for gzipped
42 // payloads), so the real size is measured from the decoded body itself.
43 const buffer = await response.arrayBuffer();
44 const sizeBytes = buffer.byteLength;
45
46 // Store in the correct named cache.
47 const headers = new Headers(response.headers);
48 headers.set('Content-Type', ct);
49 headers.set('Content-Length', String(sizeBytes));
50 headers.set('X-DL2-Stored', new Date().toISOString());
51 const cloned = new Response(buffer, { status: response.status, headers });
52
53 if (mime === 'pdf') {
54 await putPdf(txId, cloned);
55 } else {
56 await putPreview(txId, cloned);
57 }
58
59 // Bookkeeping.
60 await putDownloadRecord({
61 txId,
62 fileType: mime,
63 sizeBytes,
64 downloadedAt: Date.now(),
65 lastAccessedAt: Date.now(),
66 });
67
68 post({ type: 'DownloadFinished', txId, key: txId, mime });
69 // Resolve the orchestrator's rpc() โ keyed by `${txId}:${mime}`, matching
70 // rpcRequestKey() in client/worker-client.ts, so a concurrent pdf + preview
71 // rpc() for the same book each resolve with their own result instead of
72 // whichever one happens to post its FileRpcResponse first (ยง5.5 / ยง13.3).
73 post({ type: 'FileRpcResponse', ok: true, key: `${txId}:${mime}` });
74
75 // Self-prune after every successful write.
76 await runLruPruneAndEmit(post);
77 } catch (err: unknown) {
78 const msg = err instanceof Error ? err.message : String(err);
79 post({ type: 'DownloadError', txId, error: msg, retryable: true });
80 post({ type: 'FileRpcResponse', ok: false, key: `${txId}:${mime}`, error: msg });
81 }
82}
83
84(self as unknown as Worker).onmessage = (e: MessageEvent<WorkerCommand>) => {
85 const cmd = e.data;
86 if (cmd.type === 'GetFile') {
87 handleGetFile(cmd.txId, cmd.mime);
88 } else if (cmd.type === 'CancelDownload') {
89 // Phase A stub โ full cancellation via AbortController in Phase B.
90 }
91 // ProcessPages is handled by the mu-worker, not here.
92};
93