๐ src/downloader/sw/register.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// SW registration, gating, kill switches, and developer-hygiene API (ยง8).
16import { CACHES } from '../keys';
17import { openMetaDb, closeMetaDb } from '../meta-db';
18
19export interface SwStatus {
20 supported: boolean;
21 registered: boolean;
22 controlling: boolean;
23 scope?: string;
24 version?: string;
25 reason?: string;
26}
27
28export interface SwGateOptions {
29 allowedOrigins?: string[];
30 enableInDev?: boolean;
31}
32
33const SW_SCRIPT = '/dl2-sw.js';
34const DEFAULT_ORIGINS = ['datapond.earth', 'localhost'];
35
36function isAllowedOrigin(origins: string[]): boolean {
37 const host = typeof location !== 'undefined' ? location.hostname : '';
38 return origins.some(o => host === o || host.endsWith(`.${o}`));
39}
40
41function killSwitchState(): 'off' | 'purge' | 'on' | null {
42 if (typeof location !== 'undefined') {
43 const qs = new URLSearchParams(location.search);
44 const v = qs.get('dl2-sw');
45 if (v === 'off') return 'off';
46 if (v === 'purge') return 'purge';
47 }
48 if (typeof localStorage !== 'undefined') {
49 const v = localStorage.getItem('dl2:sw');
50 if (v === 'off') return 'off';
51 if (v === 'on') return 'on';
52 }
53 return null;
54}
55
56export const swStatus = async (): Promise<SwStatus> => {
57 if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
58 return { supported: false, registered: false, controlling: false, reason: 'not-supported' };
59 }
60 const regs = await navigator.serviceWorker.getRegistrations();
61 const ours = regs.find(r =>
62 r.installing?.scriptURL.endsWith(SW_SCRIPT) ||
63 r.waiting?.scriptURL.endsWith(SW_SCRIPT) ||
64 r.active?.scriptURL.endsWith(SW_SCRIPT)
65 );
66 return {
67 supported: true,
68 registered: !!ours,
69 controlling: !!navigator.serviceWorker.controller,
70 scope: ours?.scope,
71 reason: ours ? undefined : 'not-registered',
72 };
73};
74
75export const registerDownloaderSW = async (opts: SwGateOptions = {}): Promise<SwStatus> => {
76 const origins = opts.allowedOrigins ?? DEFAULT_ORIGINS;
77 const enableDev = opts.enableInDev ?? false;
78
79 if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
80 return { supported: false, registered: false, controlling: false, reason: 'not-supported' };
81 }
82
83 // Kill switch checks.
84 const ks = killSwitchState();
85 if (ks === 'purge') {
86 await nukeDownloaderStorage();
87 return { supported: true, registered: false, controlling: false, reason: 'kill-switch-purge' };
88 }
89 if (ks === 'off') {
90 await unregisterDownloaderSW();
91 return { supported: true, registered: false, controlling: false, reason: 'kill-switch-off' };
92 }
93
94 // Origin allowlist.
95 if (!isAllowedOrigin(origins)) {
96 return { supported: true, registered: false, controlling: false, reason: 'origin-not-allowed' };
97 }
98
99 // Dev mode gating.
100 const isDev = typeof (import.meta as any).env !== 'undefined' && (import.meta as any).env.DEV;
101 if (isDev) {
102 if (!enableDev || ks !== 'on') {
103 // Actively unregister any leftover SW from a previous production-preview run.
104 await unregisterDownloaderSW();
105 return { supported: true, registered: false, controlling: false, reason: 'dev-mode' };
106 }
107 }
108
109 await navigator.serviceWorker.register(SW_SCRIPT, { scope: '/' });
110 return swStatus();
111};
112
113export const unregisterDownloaderSW = async (
114 opts: { clearCaches?: boolean; reload?: boolean } = {},
115): Promise<boolean> => {
116 if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return false;
117
118 const regs = await navigator.serviceWorker.getRegistrations();
119 let removed = false;
120
121 for (const reg of regs) {
122 const matches =
123 reg.installing?.scriptURL.endsWith(SW_SCRIPT) ||
124 reg.waiting?.scriptURL.endsWith(SW_SCRIPT) ||
125 reg.active?.scriptURL.endsWith(SW_SCRIPT);
126
127 if (matches) {
128 await reg.unregister();
129 removed = true;
130 }
131 }
132
133 if (opts.clearCaches) await clearDownloaderCaches();
134 if (opts.reload && removed && typeof location !== 'undefined') location.reload();
135
136 return removed;
137};
138
139export const clearDownloaderCaches = async (): Promise<void> => {
140 const names = await caches.keys();
141 await Promise.all(
142 names
143 .filter(n => n.startsWith('dl2-'))
144 .map(n => caches.delete(n)),
145 );
146};
147
148export const nukeDownloaderStorage = async (): Promise<void> => {
149 await unregisterDownloaderSW({ clearCaches: true });
150
151 // Close this tab's own dl2_meta connection first โ deleteDatabase() waits
152 // for every open connection to close and fires no event until they do, so
153 // without this it can hang forever against our own still-open handle.
154 closeMetaDb();
155
156 // Clear the dl2_meta IDB. onblocked fires if some OTHER tab still has a
157 // connection open; we don't wait on it forever โ the caches/SW above are
158 // already cleared, so time out and resolve rather than hang indefinitely.
159 await new Promise<void>((resolve, reject) => {
160 const req = indexedDB.deleteDatabase('dl2_meta');
161 const timer = setTimeout(() => {
162 console.warn('[downloader] dl2_meta deleteDatabase blocked by another tab; giving up waiting');
163 resolve();
164 }, 5_000);
165 req.onsuccess = () => { clearTimeout(timer); resolve(); };
166 req.onerror = () => { clearTimeout(timer); reject(req.error); };
167 req.onblocked = () => { /* let the timeout above resolve */ };
168 });
169};
170