๐Ÿ“„ 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