offline-data-manager 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md ADDED
@@ -0,0 +1,226 @@
1
+ # offline-data-manager
2
+
3
+ A service-worker-friendly library for registering, downloading, and storing files offline using IndexedDB. All files are stored as typed Blobs — the library has no knowledge of file contents. Parsing, decompression, and interpretation are the caller's responsibility.
4
+
5
+ ---
6
+
7
+ ## Running the test harness
8
+
9
+ The test harness imports directly from `src/index.js` using ES modules, so it requires a local HTTP server (browsers block module imports from `file://`).
10
+
11
+ ```bash
12
+ # From the offline-data-manager directory:
13
+ npm run dev
14
+ # Then open: http://localhost:3000/test/index.html
15
+ ```
16
+
17
+ Or with any static server you prefer:
18
+
19
+ ```bash
20
+ npx serve . # then open http://localhost:3000/test/index.html
21
+ python3 -m http.server 3000 # then open http://localhost:3000/test/index.html
22
+ ```
23
+
24
+ ---
25
+
26
+ ## File structure
27
+
28
+ ```
29
+ src/
30
+ index.js — Public API (import from here)
31
+ db.js — IndexedDB setup and helpers
32
+ registry.js — registerFile, registerFiles, view, isReady, getStatus, TTL/expiry
33
+ downloader.js — downloadFiles, chunked Range requests, retry, resume, expiry evaluation
34
+ deleter.js — deleteFile, deleteAllFiles
35
+ events.js — Lightweight event emitter
36
+ storage.js — Storage quota utilities
37
+
38
+ test/
39
+ index.html — Interactive test harness (imports from ../src/index.js)
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Registry entry shape
45
+
46
+ ```js
47
+ {
48
+ id: string, // required — unique identifier
49
+ downloadUrl: string, // required — URL to fetch
50
+ version: number, // required — non-negative integer; triggers re-download when increased
51
+ mimeType: string|null, // optional — inferred from Content-Type header if omitted
52
+ protected: boolean, // default false — registry survives deletion; data re-downloaded
53
+ priority: number, // default 10 — lower number = higher priority
54
+ ttl: number, // seconds; 0 or omitted = never expires
55
+ totalBytes: number|null, // optional size hint for storage checks and progress
56
+ metadata: object, // arbitrary caller key/values
57
+ }
58
+ ```
59
+
60
+ ### `version`
61
+
62
+ When `registerFiles()` is called with a higher version, the queue resets to `pending` but the existing blob stays in IDB and remains accessible via `retrieve()` until the new download completes and overwrites it.
63
+
64
+ ### `protected`
65
+
66
+ | Value | On delete | Registry |
67
+ |---|---|---|
68
+ | `true` | Blob cleared, queue reset to `pending` | **Survives** — re-downloaded on next `downloadFiles()` |
69
+ | `false` | Fully removed | **Removed** |
70
+
71
+ Pass `{ removeRegistry: true }` to `delete()` to force full removal of a protected entry.
72
+
73
+ ### `ttl`
74
+
75
+ Time-to-live in seconds. On each `downloadFiles()` call, entries whose `completedAt + ttl` has elapsed are flipped to `expired` and queued for re-download. The existing blob remains accessible throughout — there is no gap in availability. On completion the TTL clock resets from the new `completedAt`.
76
+
77
+ ---
78
+
79
+ ## Download status values
80
+
81
+ | Status | Meaning |
82
+ |---|---|
83
+ | `pending` | Queued, not yet started |
84
+ | `in-progress` | Actively downloading |
85
+ | `paused` | Aborted mid-flight; resumes on next `downloadFiles()` |
86
+ | `complete` | Blob stored and fresh |
87
+ | `expired` | Blob stored but TTL has elapsed; still accessible, re-download queued |
88
+ | `failed` | Exhausted all retries |
89
+ | `deferred` | Skipped due to insufficient storage; retried next run |
90
+
91
+ ---
92
+
93
+ ## API
94
+
95
+ ### `registerFile(entry)`
96
+
97
+ Registers a single file. No-op if version hasn't strictly increased.
98
+
99
+ ### `registerFiles(entries)`
100
+
101
+ Registers an array of files. Removes non-protected entries absent from the list.
102
+
103
+ ```js
104
+ const { registered, removed } = await ODM.registerFiles([...]);
105
+ ```
106
+
107
+ ### `downloadFiles(options?)`
108
+
109
+ Evaluates TTL expiry, then downloads all pending/paused/deferred/expired entries. Returns immediately if the browser is offline — downloads resume automatically when connectivity is restored if `startMonitoring()` has been called.
110
+
111
+ ```js
112
+ await ODM.downloadFiles({ concurrency: 2, resumeOnly: false, retryFailed: false });
113
+ ```
114
+
115
+ - `retryFailed: true` resets all `failed` entries to `pending` before the run, giving them a fresh set of retries. Useful after fixing a broken URL or restoring connectivity after a long outage.
116
+
117
+ ### `retrieve(id)`
118
+
119
+ Returns the stored `Blob`. Works for both `complete` and `expired` entries.
120
+
121
+ ```js
122
+ const blob = await ODM.retrieve('json-data');
123
+ const json = JSON.parse(await blob.text());
124
+
125
+ const mapBlob = await ODM.retrieve('zip-data');
126
+ const mapBuffer = await mapBlob.arrayBuffer(); // Process binary data.
127
+ ```
128
+
129
+ ### `view()`
130
+
131
+ Returns all entries merged with queue state, plus storage summary.
132
+
133
+ ```js
134
+ const { items, storage } = await ODM.view();
135
+ // items[n]: { id, mimeType, version, downloadStatus, storedBytes,
136
+ // bytesDownloaded, progress, completedAt, expiresAt, ... }
137
+ // storage: { usageBytes, quotaBytes, availableBytes, ...Formatted }
138
+ ```
139
+
140
+ ### `getStatus(id)`
141
+
142
+ Full merged status for one file, or `null` if not registered.
143
+
144
+ ### `isReady(id)`
145
+
146
+ Returns `true` if the file has a blob available (`complete` or `expired`).
147
+
148
+ ### `delete(id, options?)`
149
+
150
+ ```js
151
+ await ODM.delete('poi-data'); // respects protected flag
152
+ await ODM.delete('base-map', { removeRegistry: true }); // force full removal
153
+ ```
154
+
155
+ ### `deleteAll(options?)`
156
+
157
+ ```js
158
+ await ODM.deleteAll();
159
+ await ODM.deleteAll({ removeRegistry: true });
160
+ ```
161
+
162
+ ### `startMonitoring()` / `stopMonitoring()`
163
+
164
+ Monitors `window` online/offline events. Going offline immediately pauses all active downloads (avoiding wasted retry attempts). Coming back online automatically calls `downloadFiles()` with the same options as the most recent explicit call.
165
+
166
+ ```js
167
+ ODM.startMonitoring(); // call once after first downloadFiles()
168
+ ODM.stopMonitoring(); // remove listeners (e.g. in tests)
169
+ ```
170
+
171
+ Emits a `'connectivity'` event `{ online: boolean }` on every change. `navigator.onLine` can return `true` on a captive portal — actual server reachability is confirmed by whether the subsequent fetch succeeds, and failed fetches retry with backoff as normal.
172
+
173
+ ### `isOnline()` / `isMonitoring()`
174
+
175
+ ```js
176
+ ODM.isOnline(); // → boolean (navigator.onLine)
177
+ ODM.isMonitoring(); // → boolean
178
+ ```
179
+
180
+
181
+ Sets status to `paused`; resumes on next `downloadFiles()`.
182
+
183
+ ### `resumeInterruptedDownloads()`
184
+
185
+ Resumes `paused` and `in-progress` entries. Call in a service worker `activate` event:
186
+
187
+ ```js
188
+ self.addEventListener('activate', (event) => {
189
+ event.waitUntil(ODM.resumeInterruptedDownloads());
190
+ });
191
+ ```
192
+
193
+ ### Events
194
+
195
+ ```js
196
+ const unsub = ODM.on('progress', ({ id, bytesDownloaded, totalBytes, percent }) => {});
197
+ ODM.on('complete', ({ id }) => {});
198
+ ODM.on('expired', ({ id }) => {});
199
+ ODM.on('error', ({ id, error, retryCount, willRetry }) => {});
200
+ ODM.on('deferred', ({ id, reason }) => {});
201
+ ODM.on('registered', ({ id, reason }) => {}); // reason: 'new' | 'version-updated'
202
+ ODM.on('deleted', ({ id, registryRemoved }) => {});
203
+ ODM.on('status', ({ id, status }) => {});
204
+
205
+ ODM.once('complete', ({ id }) => {});
206
+ unsub(); // remove listener
207
+ ```
208
+
209
+ ### Storage
210
+
211
+ ```js
212
+ const { usage, quota, available } = await ODM.getStorageEstimate();
213
+ await ODM.requestPersistentStorage();
214
+ await ODM.isPersistentStorage();
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Notes
220
+
221
+ - **MIME type inference** — when `mimeType` is omitted from a registry entry, the downloader reads `Content-Type` from the HEAD probe (or GET response as a fallback) and strips any charset parameters. Falls back to `application/octet-stream` if the server returns nothing useful. The resolved type is stored as `Blob.type` and surfaced in `view()` / `getStatus()`.
222
+ - **Retry failed** — `failed` is a terminal status by design to avoid infinite retry loops on broken URLs. Pass `retryFailed: true` to `downloadFiles()` to explicitly re-queue failed entries when you're ready to try again.
223
+ - **Chunking threshold** — files over 5 MB are chunked in 2 MB Range requests. Both constants are in `downloader.js`.
224
+ - **Content-Encoding** — `Content-Length` is ignored for size tracking when the server applies `gzip`/`br` encoding, avoiding misleading progress numbers. Progress shows as indeterminate instead.
225
+ - **Storage safety margin** — 10% of quota is reserved before deferring downloads. Configurable in `storage.js`.
226
+ - **Blob in IDB** — the Blob is stored directly on the queue record, so there is only one IDB store to manage rather than a separate data store.
@@ -0,0 +1 @@
1
+ var me="offline-data-manager";var I=null,a={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function N(){return I||(I=await new Promise((e,t)=>{let o=indexedDB.open(me,1);o.onupgradeneeded=s=>{let n=s.target.result;if(!n.objectStoreNames.contains(a.REGISTRY)){let r=n.createObjectStore(a.REGISTRY,{keyPath:"id"});r.createIndex("protected","protected",{unique:!1}),r.createIndex("priority","priority",{unique:!1})}if(!n.objectStoreNames.contains(a.DOWNLOAD_QUEUE)){let r=n.createObjectStore(a.DOWNLOAD_QUEUE,{keyPath:"id"});r.createIndex("status","status",{unique:!1}),r.createIndex("priority","priority",{unique:!1})}},o.onsuccess=()=>e(o.result),o.onerror=()=>t(o.error)}),I)}async function w(e,t){let o=await N();return new Promise((s,n)=>{let r=o.transaction(e,"readonly").objectStore(e).get(t);r.onsuccess=()=>s(r.result),r.onerror=()=>n(r.error)})}async function y(e){let t=await N();return new Promise((o,s)=>{let n=t.transaction(e,"readonly").objectStore(e).getAll();n.onsuccess=()=>o(n.result),n.onerror=()=>s(n.error)})}async function D(e,t){let o=await N();return new Promise((s,n)=>{let r=o.transaction(e,"readwrite").objectStore(e).put(t);r.onsuccess=()=>s(),r.onerror=()=>n(r.error)})}async function h(e,t){let o=await N();return new Promise((s,n)=>{let r=o.transaction(e,"readwrite").objectStore(e).delete(t);r.onsuccess=()=>s(),r.onerror=()=>n(r.error)})}var U=new Map;function G(e,t){return U.has(e)||U.set(e,new Set),U.get(e).add(t),()=>L(e,t)}function L(e,t){U.get(e)?.delete(t)}function f(e,t){U.get(e)?.forEach(o=>{try{o(t)}catch(s){console.error(`[offline-data-manager] Error in "${e}" listener:`,s)}})}function j(e,t){let o=s=>{t(s),L(e,o)};G(e,o)}async function x(){if(!navigator?.storage?.estimate)return{usage:0,quota:1/0,available:1/0};let{usage:e=0,quota:t=1/0}=await navigator.storage.estimate();return{usage:e,quota:t,available:t-e}}async function H(e){let{available:t,quota:o}=await x();return t-o*.1>=e}async function X(){return navigator?.storage?.persist?navigator.storage.persist():!1}async function z(){return navigator?.storage?.persisted?navigator.storage.persisted():!1}function _(e){return e===1/0?"\u221E":e<1024?`${e} B`:e<1024**2?`${(e/1024).toFixed(1)} KB`:e<1024**3?`${(e/1024**2).toFixed(1)} MB`:`${(e/1024**3).toFixed(2)} GB`}var u={PENDING:"pending",IN_PROGRESS:"in-progress",PAUSED:"paused",COMPLETE:"complete",EXPIRED:"expired",FAILED:"failed",DEFERRED:"deferred"},C=new Set([u.COMPLETE,u.EXPIRED]);function we(e){if(!e||typeof e!="object")throw new Error("Registry entry must be an object.");if(!e.id||typeof e.id!="string")throw new Error('Registry entry must have a string "id".');if(!e.downloadUrl||typeof e.downloadUrl!="string")throw new Error(`Entry "${e.id}" must have a string "downloadUrl".`);if(e.mimeType!==void 0&&e.mimeType!==null&&typeof e.mimeType!="string")throw new Error(`Entry "${e.id}" mimeType must be a string or omitted.`);if(typeof e.version!="number"||!Number.isInteger(e.version)||e.version<0)throw new Error(`Entry "${e.id}" version must be a non-negative integer.`);if(e.ttl!==void 0&&(typeof e.ttl!="number"||e.ttl<0))throw new Error(`Entry "${e.id}" ttl must be a non-negative number (seconds).`)}function K(e){return{id:e,status:u.PENDING,blob:null,bytesDownloaded:0,totalBytes:null,byteOffset:0,retryCount:0,lastAttemptAt:null,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}}function V(e,t){return t?e+t*1e3:null}function Ee(e){return e?Date.now()>=e:!1}async function Q(e){we(e);let t=Date.now(),o=await w(a.REGISTRY,e.id),s=await w(a.DOWNLOAD_QUEUE,e.id),n={id:e.id,downloadUrl:e.downloadUrl,mimeType:e.mimeType??null,version:e.version,protected:e.protected??!1,priority:e.priority??10,ttl:e.ttl??0,totalBytes:e.totalBytes??null,metadata:e.metadata??{},registeredAt:o?.registeredAt??t,updatedAt:t};if(o){if(e.version>o.version){await D(a.REGISTRY,n);let r=s?{...s,status:u.PENDING,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}:K(e.id);await D(a.DOWNLOAD_QUEUE,r),f("registered",{id:e.id,reason:"version-updated"})}return}await D(a.REGISTRY,n),await D(a.DOWNLOAD_QUEUE,K(e.id)),f("registered",{id:e.id,reason:"new"})}async function Z(e){if(!Array.isArray(e))throw new Error("registerFiles expects an array.");let t=new Set(e.map(n=>n.id)),o=await y(a.REGISTRY),s=[];for(let n of o)!t.has(n.id)&&!n.protected&&(await h(a.REGISTRY,n.id),await h(a.DOWNLOAD_QUEUE,n.id),s.push(n.id),f("deleted",{id:n.id,registryRemoved:!0}));for(let n of e)await Q(n);return{registered:e.map(n=>n.id),removed:s}}async function J(){let e=await y(a.DOWNLOAD_QUEUE),t=[];for(let o of e)o.status===u.COMPLETE&&Ee(o.expiresAt)&&(await D(a.DOWNLOAD_QUEUE,{...o,status:u.EXPIRED}),t.push(o.id),f("expired",{id:o.id}));return t}async function ee(){let[e,t,o]=await Promise.all([y(a.REGISTRY),y(a.DOWNLOAD_QUEUE),x()]),s=new Map(t.map(r=>[r.id,r]));return{items:e.map(r=>{let i=s.get(r.id)??null;return{id:r.id,downloadUrl:r.downloadUrl,mimeType:r.mimeType??i?.blob?.type??null,version:r.version,protected:r.protected,priority:r.priority,ttl:r.ttl,totalBytes:r.totalBytes,metadata:r.metadata,registeredAt:r.registeredAt,updatedAt:r.updatedAt,downloadStatus:i?.status??null,bytesDownloaded:i?.bytesDownloaded??0,storedBytes:i?.blob?.size??null,progress:i?.totalBytes&&i?.bytesDownloaded?Math.round(i.bytesDownloaded/i.totalBytes*100):null,retryCount:i?.retryCount??0,lastAttemptAt:i?.lastAttemptAt??null,errorMessage:i?.errorMessage??null,deferredReason:i?.deferredReason??null,completedAt:i?.completedAt??null,expiresAt:i?.expiresAt??null}}).sort((r,i)=>r.priority-i.priority),storage:{usageBytes:o.usage,quotaBytes:o.quota,availableBytes:o.available,usageFormatted:_(o.usage),quotaFormatted:_(o.quota),availableFormatted:_(o.available)}}}async function te(e){let[t,o]=await Promise.all([w(a.REGISTRY,e),w(a.DOWNLOAD_QUEUE,e)]);return t?{id:t.id,downloadUrl:t.downloadUrl,mimeType:t.mimeType??o?.blob?.type??null,version:t.version,protected:t.protected,priority:t.priority,ttl:t.ttl,totalBytes:t.totalBytes,metadata:t.metadata,registeredAt:t.registeredAt,updatedAt:t.updatedAt,downloadStatus:o?.status??null,bytesDownloaded:o?.bytesDownloaded??0,storedBytes:o?.blob?.size??null,progress:o?.totalBytes&&o?.bytesDownloaded?Math.round(o.bytesDownloaded/o.totalBytes*100):null,retryCount:o?.retryCount??0,lastAttemptAt:o?.lastAttemptAt??null,errorMessage:o?.errorMessage??null,deferredReason:o?.deferredReason??null,completedAt:o?.completedAt??null,expiresAt:o?.expiresAt??null}:null}async function oe(e){let t=await w(a.DOWNLOAD_QUEUE,e);return C.has(t?.status)}var F=null,q=null,P=!1;function re(){f("connectivity",{online:!1}),F?.()}function ne(){f("connectivity",{online:!0}),q?.()}function se({pauseAll:e,resumeAll:t}){P||(F=e,q=t,window.addEventListener("offline",re),window.addEventListener("online",ne),P=!0)}function W(){window.removeEventListener("offline",re),window.removeEventListener("online",ne),F=null,q=null,P=!1}function T(){return navigator.onLine??!0}function $(){return P}var ye=2*1024*1024,ge=5*1024*1024,De=2,ae=5,be=1e3,b=new Map,ie={},Ae=e=>new Promise(t=>setTimeout(t,e)),Re=e=>be*Math.pow(2,e);async function g(e,t){let o=await w(a.DOWNLOAD_QUEUE,e);o&&await D(a.DOWNLOAD_QUEUE,{...o,...t})}async function Se(e,t){try{let o=await fetch(e,{method:"HEAD",signal:t}),s=o.headers.get("Accept-Ranges")==="bytes",n=o.headers.get("Content-Encoding"),r=!!n&&n!=="identity",i=o.headers.get("Content-Length"),d=i&&!r?parseInt(i,10):null,p=le(o.headers.get("Content-Type"));return{supportsRange:s,totalBytes:d,mimeType:p}}catch{return{supportsRange:!1,totalBytes:null,mimeType:null}}}function le(e){return e&&e.split(";")[0].trim()||null}function ue(e){let t=e.reduce((n,r)=>n+r.byteLength,0),o=new Uint8Array(t),s=0;for(let n of e)o.set(n,s),s+=n.byteLength;return o}async function Oe(e){let{id:t,downloadUrl:o,ttl:s}=e,n=new AbortController;b.set(t,n);let r=await w(a.DOWNLOAD_QUEUE,t),i=r?.retryCount??0;for(;i<=ae;)try{await g(t,{status:u.IN_PROGRESS,lastAttemptAt:Date.now(),retryCount:i,errorMessage:null}),f("status",{id:t,status:u.IN_PROGRESS}),r=await w(a.DOWNLOAD_QUEUE,t);let d=r?.byteOffset??0,p=!1,m=r?.totalBytes??e.totalBytes??null,E=e.mimeType??null;if(d===0){let R=await Se(o,n.signal);p=R.supportsRange,R.totalBytes&&(m=R.totalBytes,await g(t,{totalBytes:m})),!E&&R.mimeType&&(E=R.mimeType)}else p=!0;let A=p&&m&&m>ge,l,c=null;if(A)l=await Ue(t,o,d,m,n.signal);else{let R=await he(t,o,n.signal);l=R.buffer,c=R.mimeType}let S=E??c??"application/octet-stream",O=new Blob([l],{type:S}),k=Date.now(),pe=V(k,s);await g(t,{status:u.COMPLETE,blob:O,bytesDownloaded:l.byteLength,byteOffset:l.byteLength,completedAt:k,expiresAt:pe,errorMessage:null,deferredReason:null}),f("complete",{id:t,mimeType:S}),b.delete(t);return}catch(d){if(d.name==="AbortError"){await g(t,{status:u.PAUSED}),f("status",{id:t,status:u.PAUSED}),b.delete(t);return}if(i++,i>ae){await g(t,{status:u.FAILED,retryCount:i,errorMessage:d.message}),f("error",{id:t,error:d,retryCount:i}),b.delete(t);return}let p=Re(i-1);console.warn(`[offline-data-manager] "${t}" failed (attempt ${i}), retrying in ${p}ms:`,d.message),f("error",{id:t,error:d,retryCount:i,willRetry:!0}),await g(t,{status:u.PENDING,retryCount:i,errorMessage:d.message}),await Ae(p)}}async function he(e,t,o){let s=await fetch(t,{signal:o});if(!s.ok)throw new Error(`HTTP ${s.status} ${s.statusText}`);let n=s.headers.get("Content-Encoding"),r=!!n&&n!=="identity",i=s.headers.get("Content-Length"),d=i&&!r?parseInt(i,10):null,p=le(s.headers.get("Content-Type")),m=s.body.getReader(),E=[],A=0;for(;;){let{done:l,value:c}=await m.read();if(l)break;E.push(c),A+=c.byteLength,await g(e,{bytesDownloaded:A,totalBytes:d}),f("progress",{id:e,bytesDownloaded:A,totalBytes:d,percent:d?Math.round(A/d*100):null})}return{buffer:ue(E),mimeType:p}}async function Ue(e,t,o,s,n){let r=o,i=[],d=o;for(;r<s;){let p=Math.min(r+ye-1,s-1),m=await fetch(t,{signal:n,headers:{Range:`bytes=${r}-${p}`}});if(!m.ok&&m.status!==206)throw new Error(`HTTP ${m.status} on Range bytes=${r}-${p}`);let E=new Uint8Array(await m.arrayBuffer());i.push(E),r+=E.byteLength,d+=E.byteLength,await g(e,{bytesDownloaded:d,byteOffset:r}),f("progress",{id:e,bytesDownloaded:d,totalBytes:s,percent:Math.round(d/s*100)})}return ue(i)}async function M({concurrency:e=De,resumeOnly:t=!1,retryFailed:o=!1}={}){if(ie={concurrency:e,resumeOnly:t,retryFailed:o},!T()){let l=await y(a.DOWNLOAD_QUEUE);for(let c of l)c.status===u.IN_PROGRESS&&(b.get(c.id)?.abort(),b.delete(c.id),await g(c.id,{status:u.PAUSED,deferredReason:"network-offline"}));f("connectivity",{online:!1});return}if(await J(),o){let l=await y(a.DOWNLOAD_QUEUE);for(let c of l)c.status===u.FAILED&&await g(c.id,{status:u.PENDING,retryCount:0,errorMessage:null})}let[s,n]=await Promise.all([y(a.REGISTRY),y(a.DOWNLOAD_QUEUE)]),r=new Map(s.map(l=>[l.id,l])),i=t?[u.IN_PROGRESS,u.PAUSED]:[u.PENDING,u.IN_PROGRESS,u.PAUSED,u.DEFERRED,u.EXPIRED],p=[...n.filter(l=>i.includes(l.status)).sort((l,c)=>{let S=r.get(l.id)?.priority??10,O=r.get(c.id)?.priority??10;return S-O})],m=new Set;function E(){if(p.length===0)return;let l=p.shift(),c=r.get(l.id);if(!c)return;let S=(async()=>{let O=c.totalBytes??l.totalBytes??0;if(O>0&&!await H(O)){await g(l.id,{status:u.DEFERRED,deferredReason:"insufficient-storage"}),f("deferred",{id:l.id,reason:"insufficient-storage"});return}await Oe(c)})().finally(()=>{m.delete(S),E()});m.add(S)}let A=Math.min(e,p.length);for(let l=0;l<A;l++)E();await new Promise(l=>{let c=setInterval(()=>{m.size===0&&p.length===0&&(clearInterval(c),l())},100)})}async function B(e){b.get(e)?.abort(),b.delete(e)}async function v(){for(let[e,t]of b)t.abort(),b.delete(e)}async function de(){await M({resumeOnly:!0})}function ce(){se({pauseAll:v,resumeAll:()=>M(ie)})}async function xe(e){let t=await w(a.DOWNLOAD_QUEUE,e);t&&await D(a.DOWNLOAD_QUEUE,{...t,status:u.PENDING,blob:null,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null})}async function Y(e,{removeRegistry:t=!1}={}){let o=await w(a.REGISTRY,e);if(!o)throw new Error(`deleteFile: No registered file with id "${e}".`);await B(e);let s=t||!o.protected;return s?(await h(a.REGISTRY,e),await h(a.DOWNLOAD_QUEUE,e)):await xe(e),f("deleted",{id:e,registryRemoved:s}),{id:e,registryRemoved:s}}async function fe({removeRegistry:e=!1}={}){await v();let t=await y(a.REGISTRY);return Promise.all(t.map(o=>Y(o.id,{removeRegistry:e})))}async function Te(e){let[t,o]=await Promise.all([w(a.REGISTRY,e),w(a.DOWNLOAD_QUEUE,e)]);if(!t)throw new Error(`retrieve: No registered file with id "${e}".`);if(!C.has(o?.status)||!o?.blob)throw new Error(`retrieve: File "${e}" has no data yet (status: ${o?.status??"unknown"}).`);return o.blob}var ve={registerFile:Q,registerFiles:Z,downloadFiles:M,abortDownload:B,abortAllDownloads:v,resumeInterruptedDownloads:de,startMonitoring:ce,stopMonitoring:W,isOnline:T,isMonitoring:$,retrieve:Te,view:ee,getStatus:te,isReady:oe,delete:Y,deleteAll:fe,on:G,off:L,once:j,getStorageEstimate:x,requestPersistentStorage:X,isPersistentStorage:z},st=ve;export{v as abortAllDownloads,B as abortDownload,st as default,fe as deleteAllFiles,Y as deleteFile,M as downloadFiles,f as emit,te as getStatus,x as getStorageEstimate,$ as isMonitoring,T as isOnline,z as isPersistentStorage,oe as isReady,L as off,G as on,j as once,Q as registerFile,Z as registerFiles,X as requestPersistentStorage,de as resumeInterruptedDownloads,Te as retrieve,ce as startMonitoring,W as stopMonitoring,ee as view};
@@ -0,0 +1 @@
1
+ var me="offline-data-manager";var I=null,a={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function N(){return I||(I=await new Promise((e,t)=>{let o=indexedDB.open(me,1);o.onupgradeneeded=s=>{let n=s.target.result;if(!n.objectStoreNames.contains(a.REGISTRY)){let r=n.createObjectStore(a.REGISTRY,{keyPath:"id"});r.createIndex("protected","protected",{unique:!1}),r.createIndex("priority","priority",{unique:!1})}if(!n.objectStoreNames.contains(a.DOWNLOAD_QUEUE)){let r=n.createObjectStore(a.DOWNLOAD_QUEUE,{keyPath:"id"});r.createIndex("status","status",{unique:!1}),r.createIndex("priority","priority",{unique:!1})}},o.onsuccess=()=>e(o.result),o.onerror=()=>t(o.error)}),I)}async function w(e,t){let o=await N();return new Promise((s,n)=>{let r=o.transaction(e,"readonly").objectStore(e).get(t);r.onsuccess=()=>s(r.result),r.onerror=()=>n(r.error)})}async function y(e){let t=await N();return new Promise((o,s)=>{let n=t.transaction(e,"readonly").objectStore(e).getAll();n.onsuccess=()=>o(n.result),n.onerror=()=>s(n.error)})}async function D(e,t){let o=await N();return new Promise((s,n)=>{let r=o.transaction(e,"readwrite").objectStore(e).put(t);r.onsuccess=()=>s(),r.onerror=()=>n(r.error)})}async function h(e,t){let o=await N();return new Promise((s,n)=>{let r=o.transaction(e,"readwrite").objectStore(e).delete(t);r.onsuccess=()=>s(),r.onerror=()=>n(r.error)})}var U=new Map;function G(e,t){return U.has(e)||U.set(e,new Set),U.get(e).add(t),()=>L(e,t)}function L(e,t){U.get(e)?.delete(t)}function f(e,t){U.get(e)?.forEach(o=>{try{o(t)}catch(s){console.error(`[offline-data-manager] Error in "${e}" listener:`,s)}})}function j(e,t){let o=s=>{t(s),L(e,o)};G(e,o)}async function x(){if(!navigator?.storage?.estimate)return{usage:0,quota:1/0,available:1/0};let{usage:e=0,quota:t=1/0}=await navigator.storage.estimate();return{usage:e,quota:t,available:t-e}}async function H(e){let{available:t,quota:o}=await x();return t-o*.1>=e}async function X(){return navigator?.storage?.persist?navigator.storage.persist():!1}async function z(){return navigator?.storage?.persisted?navigator.storage.persisted():!1}function _(e){return e===1/0?"\u221E":e<1024?`${e} B`:e<1024**2?`${(e/1024).toFixed(1)} KB`:e<1024**3?`${(e/1024**2).toFixed(1)} MB`:`${(e/1024**3).toFixed(2)} GB`}var u={PENDING:"pending",IN_PROGRESS:"in-progress",PAUSED:"paused",COMPLETE:"complete",EXPIRED:"expired",FAILED:"failed",DEFERRED:"deferred"},C=new Set([u.COMPLETE,u.EXPIRED]);function we(e){if(!e||typeof e!="object")throw new Error("Registry entry must be an object.");if(!e.id||typeof e.id!="string")throw new Error('Registry entry must have a string "id".');if(!e.downloadUrl||typeof e.downloadUrl!="string")throw new Error(`Entry "${e.id}" must have a string "downloadUrl".`);if(e.mimeType!==void 0&&e.mimeType!==null&&typeof e.mimeType!="string")throw new Error(`Entry "${e.id}" mimeType must be a string or omitted.`);if(typeof e.version!="number"||!Number.isInteger(e.version)||e.version<0)throw new Error(`Entry "${e.id}" version must be a non-negative integer.`);if(e.ttl!==void 0&&(typeof e.ttl!="number"||e.ttl<0))throw new Error(`Entry "${e.id}" ttl must be a non-negative number (seconds).`)}function K(e){return{id:e,status:u.PENDING,blob:null,bytesDownloaded:0,totalBytes:null,byteOffset:0,retryCount:0,lastAttemptAt:null,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}}function V(e,t){return t?e+t*1e3:null}function Ee(e){return e?Date.now()>=e:!1}async function Q(e){we(e);let t=Date.now(),o=await w(a.REGISTRY,e.id),s=await w(a.DOWNLOAD_QUEUE,e.id),n={id:e.id,downloadUrl:e.downloadUrl,mimeType:e.mimeType??null,version:e.version,protected:e.protected??!1,priority:e.priority??10,ttl:e.ttl??0,totalBytes:e.totalBytes??null,metadata:e.metadata??{},registeredAt:o?.registeredAt??t,updatedAt:t};if(o){if(e.version>o.version){await D(a.REGISTRY,n);let r=s?{...s,status:u.PENDING,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}:K(e.id);await D(a.DOWNLOAD_QUEUE,r),f("registered",{id:e.id,reason:"version-updated"})}return}await D(a.REGISTRY,n),await D(a.DOWNLOAD_QUEUE,K(e.id)),f("registered",{id:e.id,reason:"new"})}async function Z(e){if(!Array.isArray(e))throw new Error("registerFiles expects an array.");let t=new Set(e.map(n=>n.id)),o=await y(a.REGISTRY),s=[];for(let n of o)!t.has(n.id)&&!n.protected&&(await h(a.REGISTRY,n.id),await h(a.DOWNLOAD_QUEUE,n.id),s.push(n.id),f("deleted",{id:n.id,registryRemoved:!0}));for(let n of e)await Q(n);return{registered:e.map(n=>n.id),removed:s}}async function J(){let e=await y(a.DOWNLOAD_QUEUE),t=[];for(let o of e)o.status===u.COMPLETE&&Ee(o.expiresAt)&&(await D(a.DOWNLOAD_QUEUE,{...o,status:u.EXPIRED}),t.push(o.id),f("expired",{id:o.id}));return t}async function ee(){let[e,t,o]=await Promise.all([y(a.REGISTRY),y(a.DOWNLOAD_QUEUE),x()]),s=new Map(t.map(r=>[r.id,r]));return{items:e.map(r=>{let i=s.get(r.id)??null;return{id:r.id,downloadUrl:r.downloadUrl,mimeType:r.mimeType??i?.blob?.type??null,version:r.version,protected:r.protected,priority:r.priority,ttl:r.ttl,totalBytes:r.totalBytes,metadata:r.metadata,registeredAt:r.registeredAt,updatedAt:r.updatedAt,downloadStatus:i?.status??null,bytesDownloaded:i?.bytesDownloaded??0,storedBytes:i?.blob?.size??null,progress:i?.totalBytes&&i?.bytesDownloaded?Math.round(i.bytesDownloaded/i.totalBytes*100):null,retryCount:i?.retryCount??0,lastAttemptAt:i?.lastAttemptAt??null,errorMessage:i?.errorMessage??null,deferredReason:i?.deferredReason??null,completedAt:i?.completedAt??null,expiresAt:i?.expiresAt??null}}).sort((r,i)=>r.priority-i.priority),storage:{usageBytes:o.usage,quotaBytes:o.quota,availableBytes:o.available,usageFormatted:_(o.usage),quotaFormatted:_(o.quota),availableFormatted:_(o.available)}}}async function te(e){let[t,o]=await Promise.all([w(a.REGISTRY,e),w(a.DOWNLOAD_QUEUE,e)]);return t?{id:t.id,downloadUrl:t.downloadUrl,mimeType:t.mimeType??o?.blob?.type??null,version:t.version,protected:t.protected,priority:t.priority,ttl:t.ttl,totalBytes:t.totalBytes,metadata:t.metadata,registeredAt:t.registeredAt,updatedAt:t.updatedAt,downloadStatus:o?.status??null,bytesDownloaded:o?.bytesDownloaded??0,storedBytes:o?.blob?.size??null,progress:o?.totalBytes&&o?.bytesDownloaded?Math.round(o.bytesDownloaded/o.totalBytes*100):null,retryCount:o?.retryCount??0,lastAttemptAt:o?.lastAttemptAt??null,errorMessage:o?.errorMessage??null,deferredReason:o?.deferredReason??null,completedAt:o?.completedAt??null,expiresAt:o?.expiresAt??null}:null}async function oe(e){let t=await w(a.DOWNLOAD_QUEUE,e);return C.has(t?.status)}var F=null,q=null,P=!1;function re(){f("connectivity",{online:!1}),F?.()}function ne(){f("connectivity",{online:!0}),q?.()}function se({pauseAll:e,resumeAll:t}){P||(F=e,q=t,window.addEventListener("offline",re),window.addEventListener("online",ne),P=!0)}function W(){window.removeEventListener("offline",re),window.removeEventListener("online",ne),F=null,q=null,P=!1}function T(){return navigator.onLine??!0}function $(){return P}var ye=2*1024*1024,ge=5*1024*1024,De=2,ae=5,be=1e3,b=new Map,ie={},Ae=e=>new Promise(t=>setTimeout(t,e)),Re=e=>be*Math.pow(2,e);async function g(e,t){let o=await w(a.DOWNLOAD_QUEUE,e);o&&await D(a.DOWNLOAD_QUEUE,{...o,...t})}async function Se(e,t){try{let o=await fetch(e,{method:"HEAD",signal:t}),s=o.headers.get("Accept-Ranges")==="bytes",n=o.headers.get("Content-Encoding"),r=!!n&&n!=="identity",i=o.headers.get("Content-Length"),d=i&&!r?parseInt(i,10):null,p=le(o.headers.get("Content-Type"));return{supportsRange:s,totalBytes:d,mimeType:p}}catch{return{supportsRange:!1,totalBytes:null,mimeType:null}}}function le(e){return e&&e.split(";")[0].trim()||null}function ue(e){let t=e.reduce((n,r)=>n+r.byteLength,0),o=new Uint8Array(t),s=0;for(let n of e)o.set(n,s),s+=n.byteLength;return o}async function Oe(e){let{id:t,downloadUrl:o,ttl:s}=e,n=new AbortController;b.set(t,n);let r=await w(a.DOWNLOAD_QUEUE,t),i=r?.retryCount??0;for(;i<=ae;)try{await g(t,{status:u.IN_PROGRESS,lastAttemptAt:Date.now(),retryCount:i,errorMessage:null}),f("status",{id:t,status:u.IN_PROGRESS}),r=await w(a.DOWNLOAD_QUEUE,t);let d=r?.byteOffset??0,p=!1,m=r?.totalBytes??e.totalBytes??null,E=e.mimeType??null;if(d===0){let R=await Se(o,n.signal);p=R.supportsRange,R.totalBytes&&(m=R.totalBytes,await g(t,{totalBytes:m})),!E&&R.mimeType&&(E=R.mimeType)}else p=!0;let A=p&&m&&m>ge,l,c=null;if(A)l=await Ue(t,o,d,m,n.signal);else{let R=await he(t,o,n.signal);l=R.buffer,c=R.mimeType}let S=E??c??"application/octet-stream",O=new Blob([l],{type:S}),k=Date.now(),pe=V(k,s);await g(t,{status:u.COMPLETE,blob:O,bytesDownloaded:l.byteLength,byteOffset:l.byteLength,completedAt:k,expiresAt:pe,errorMessage:null,deferredReason:null}),f("complete",{id:t,mimeType:S}),b.delete(t);return}catch(d){if(d.name==="AbortError"){await g(t,{status:u.PAUSED}),f("status",{id:t,status:u.PAUSED}),b.delete(t);return}if(i++,i>ae){await g(t,{status:u.FAILED,retryCount:i,errorMessage:d.message}),f("error",{id:t,error:d,retryCount:i}),b.delete(t);return}let p=Re(i-1);console.warn(`[offline-data-manager] "${t}" failed (attempt ${i}), retrying in ${p}ms:`,d.message),f("error",{id:t,error:d,retryCount:i,willRetry:!0}),await g(t,{status:u.PENDING,retryCount:i,errorMessage:d.message}),await Ae(p)}}async function he(e,t,o){let s=await fetch(t,{signal:o});if(!s.ok)throw new Error(`HTTP ${s.status} ${s.statusText}`);let n=s.headers.get("Content-Encoding"),r=!!n&&n!=="identity",i=s.headers.get("Content-Length"),d=i&&!r?parseInt(i,10):null,p=le(s.headers.get("Content-Type")),m=s.body.getReader(),E=[],A=0;for(;;){let{done:l,value:c}=await m.read();if(l)break;E.push(c),A+=c.byteLength,await g(e,{bytesDownloaded:A,totalBytes:d}),f("progress",{id:e,bytesDownloaded:A,totalBytes:d,percent:d?Math.round(A/d*100):null})}return{buffer:ue(E),mimeType:p}}async function Ue(e,t,o,s,n){let r=o,i=[],d=o;for(;r<s;){let p=Math.min(r+ye-1,s-1),m=await fetch(t,{signal:n,headers:{Range:`bytes=${r}-${p}`}});if(!m.ok&&m.status!==206)throw new Error(`HTTP ${m.status} on Range bytes=${r}-${p}`);let E=new Uint8Array(await m.arrayBuffer());i.push(E),r+=E.byteLength,d+=E.byteLength,await g(e,{bytesDownloaded:d,byteOffset:r}),f("progress",{id:e,bytesDownloaded:d,totalBytes:s,percent:Math.round(d/s*100)})}return ue(i)}async function M({concurrency:e=De,resumeOnly:t=!1,retryFailed:o=!1}={}){if(ie={concurrency:e,resumeOnly:t,retryFailed:o},!T()){let l=await y(a.DOWNLOAD_QUEUE);for(let c of l)c.status===u.IN_PROGRESS&&(b.get(c.id)?.abort(),b.delete(c.id),await g(c.id,{status:u.PAUSED,deferredReason:"network-offline"}));f("connectivity",{online:!1});return}if(await J(),o){let l=await y(a.DOWNLOAD_QUEUE);for(let c of l)c.status===u.FAILED&&await g(c.id,{status:u.PENDING,retryCount:0,errorMessage:null})}let[s,n]=await Promise.all([y(a.REGISTRY),y(a.DOWNLOAD_QUEUE)]),r=new Map(s.map(l=>[l.id,l])),i=t?[u.IN_PROGRESS,u.PAUSED]:[u.PENDING,u.IN_PROGRESS,u.PAUSED,u.DEFERRED,u.EXPIRED],p=[...n.filter(l=>i.includes(l.status)).sort((l,c)=>{let S=r.get(l.id)?.priority??10,O=r.get(c.id)?.priority??10;return S-O})],m=new Set;function E(){if(p.length===0)return;let l=p.shift(),c=r.get(l.id);if(!c)return;let S=(async()=>{let O=c.totalBytes??l.totalBytes??0;if(O>0&&!await H(O)){await g(l.id,{status:u.DEFERRED,deferredReason:"insufficient-storage"}),f("deferred",{id:l.id,reason:"insufficient-storage"});return}await Oe(c)})().finally(()=>{m.delete(S),E()});m.add(S)}let A=Math.min(e,p.length);for(let l=0;l<A;l++)E();await new Promise(l=>{let c=setInterval(()=>{m.size===0&&p.length===0&&(clearInterval(c),l())},100)})}async function B(e){b.get(e)?.abort(),b.delete(e)}async function v(){for(let[e,t]of b)t.abort(),b.delete(e)}async function de(){await M({resumeOnly:!0})}function ce(){se({pauseAll:v,resumeAll:()=>M(ie)})}async function xe(e){let t=await w(a.DOWNLOAD_QUEUE,e);t&&await D(a.DOWNLOAD_QUEUE,{...t,status:u.PENDING,blob:null,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null})}async function Y(e,{removeRegistry:t=!1}={}){let o=await w(a.REGISTRY,e);if(!o)throw new Error(`deleteFile: No registered file with id "${e}".`);await B(e);let s=t||!o.protected;return s?(await h(a.REGISTRY,e),await h(a.DOWNLOAD_QUEUE,e)):await xe(e),f("deleted",{id:e,registryRemoved:s}),{id:e,registryRemoved:s}}async function fe({removeRegistry:e=!1}={}){await v();let t=await y(a.REGISTRY);return Promise.all(t.map(o=>Y(o.id,{removeRegistry:e})))}async function Te(e){let[t,o]=await Promise.all([w(a.REGISTRY,e),w(a.DOWNLOAD_QUEUE,e)]);if(!t)throw new Error(`retrieve: No registered file with id "${e}".`);if(!C.has(o?.status)||!o?.blob)throw new Error(`retrieve: File "${e}" has no data yet (status: ${o?.status??"unknown"}).`);return o.blob}var ve={registerFile:Q,registerFiles:Z,downloadFiles:M,abortDownload:B,abortAllDownloads:v,resumeInterruptedDownloads:de,startMonitoring:ce,stopMonitoring:W,isOnline:T,isMonitoring:$,retrieve:Te,view:ee,getStatus:te,isReady:oe,delete:Y,deleteAll:fe,on:G,off:L,once:j,getStorageEstimate:x,requestPersistentStorage:X,isPersistentStorage:z},st=ve;export{v as abortAllDownloads,B as abortDownload,st as default,fe as deleteAllFiles,Y as deleteFile,M as downloadFiles,f as emit,te as getStatus,x as getStorageEstimate,$ as isMonitoring,T as isOnline,z as isPersistentStorage,oe as isReady,L as off,G as on,j as once,Q as registerFile,Z as registerFiles,X as requestPersistentStorage,de as resumeInterruptedDownloads,Te as retrieve,ce as startMonitoring,W as stopMonitoring,ee as view};
@@ -0,0 +1 @@
1
+ "use strict";var offlineMapData=(()=>{var W=Object.defineProperty;var Ee=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var ge=Object.prototype.hasOwnProperty;var De=(e,t)=>{for(var o in t)W(e,o,{get:t[o],enumerable:!0})},be=(e,t,o,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of ye(t))!ge.call(e,n)&&n!==o&&W(e,n,{get:()=>t[n],enumerable:!(s=Ee(t,n))||s.enumerable});return e};var Ae=e=>be(W({},"__esModule",{value:!0}),e);var Ce={};De(Ce,{abortAllDownloads:()=>T,abortDownload:()=>L,default:()=>Ge,deleteAllFiles:()=>te,deleteFile:()=>q,downloadFiles:()=>N,emit:()=>c,getStatus:()=>z,getStorageEstimate:()=>U,isMonitoring:()=>F,isOnline:()=>x,isPersistentStorage:()=>k,isReady:()=>K,off:()=>I,on:()=>M,once:()=>$,registerFile:()=>G,registerFiles:()=>H,requestPersistentStorage:()=>Y,resumeInterruptedDownloads:()=>J,retrieve:()=>me,startMonitoring:()=>ee,stopMonitoring:()=>Q,view:()=>X});var Re="offline-data-manager";var _=null,a={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function P(){return _||(_=await new Promise((e,t)=>{let o=indexedDB.open(Re,1);o.onupgradeneeded=s=>{let n=s.target.result;if(!n.objectStoreNames.contains(a.REGISTRY)){let r=n.createObjectStore(a.REGISTRY,{keyPath:"id"});r.createIndex("protected","protected",{unique:!1}),r.createIndex("priority","priority",{unique:!1})}if(!n.objectStoreNames.contains(a.DOWNLOAD_QUEUE)){let r=n.createObjectStore(a.DOWNLOAD_QUEUE,{keyPath:"id"});r.createIndex("status","status",{unique:!1}),r.createIndex("priority","priority",{unique:!1})}},o.onsuccess=()=>e(o.result),o.onerror=()=>t(o.error)}),_)}async function w(e,t){let o=await P();return new Promise((s,n)=>{let r=o.transaction(e,"readonly").objectStore(e).get(t);r.onsuccess=()=>s(r.result),r.onerror=()=>n(r.error)})}async function y(e){let t=await P();return new Promise((o,s)=>{let n=t.transaction(e,"readonly").objectStore(e).getAll();n.onsuccess=()=>o(n.result),n.onerror=()=>s(n.error)})}async function D(e,t){let o=await P();return new Promise((s,n)=>{let r=o.transaction(e,"readwrite").objectStore(e).put(t);r.onsuccess=()=>s(),r.onerror=()=>n(r.error)})}async function h(e,t){let o=await P();return new Promise((s,n)=>{let r=o.transaction(e,"readwrite").objectStore(e).delete(t);r.onsuccess=()=>s(),r.onerror=()=>n(r.error)})}var v=new Map;function M(e,t){return v.has(e)||v.set(e,new Set),v.get(e).add(t),()=>I(e,t)}function I(e,t){v.get(e)?.delete(t)}function c(e,t){v.get(e)?.forEach(o=>{try{o(t)}catch(s){console.error(`[offline-data-manager] Error in "${e}" listener:`,s)}})}function $(e,t){let o=s=>{t(s),I(e,o)};M(e,o)}async function U(){if(!navigator?.storage?.estimate)return{usage:0,quota:1/0,available:1/0};let{usage:e=0,quota:t=1/0}=await navigator.storage.estimate();return{usage:e,quota:t,available:t-e}}async function re(e){let{available:t,quota:o}=await U();return t-o*.1>=e}async function Y(){return navigator?.storage?.persist?navigator.storage.persist():!1}async function k(){return navigator?.storage?.persisted?navigator.storage.persisted():!1}function B(e){return e===1/0?"\u221E":e<1024?`${e} B`:e<1024**2?`${(e/1024).toFixed(1)} KB`:e<1024**3?`${(e/1024**2).toFixed(1)} MB`:`${(e/1024**3).toFixed(2)} GB`}var u={PENDING:"pending",IN_PROGRESS:"in-progress",PAUSED:"paused",COMPLETE:"complete",EXPIRED:"expired",FAILED:"failed",DEFERRED:"deferred"},j=new Set([u.COMPLETE,u.EXPIRED]);function Se(e){if(!e||typeof e!="object")throw new Error("Registry entry must be an object.");if(!e.id||typeof e.id!="string")throw new Error('Registry entry must have a string "id".');if(!e.downloadUrl||typeof e.downloadUrl!="string")throw new Error(`Entry "${e.id}" must have a string "downloadUrl".`);if(e.mimeType!==void 0&&e.mimeType!==null&&typeof e.mimeType!="string")throw new Error(`Entry "${e.id}" mimeType must be a string or omitted.`);if(typeof e.version!="number"||!Number.isInteger(e.version)||e.version<0)throw new Error(`Entry "${e.id}" version must be a non-negative integer.`);if(e.ttl!==void 0&&(typeof e.ttl!="number"||e.ttl<0))throw new Error(`Entry "${e.id}" ttl must be a non-negative number (seconds).`)}function ne(e){return{id:e,status:u.PENDING,blob:null,bytesDownloaded:0,totalBytes:null,byteOffset:0,retryCount:0,lastAttemptAt:null,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}}function se(e,t){return t?e+t*1e3:null}function Oe(e){return e?Date.now()>=e:!1}async function G(e){Se(e);let t=Date.now(),o=await w(a.REGISTRY,e.id),s=await w(a.DOWNLOAD_QUEUE,e.id),n={id:e.id,downloadUrl:e.downloadUrl,mimeType:e.mimeType??null,version:e.version,protected:e.protected??!1,priority:e.priority??10,ttl:e.ttl??0,totalBytes:e.totalBytes??null,metadata:e.metadata??{},registeredAt:o?.registeredAt??t,updatedAt:t};if(o){if(e.version>o.version){await D(a.REGISTRY,n);let r=s?{...s,status:u.PENDING,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}:ne(e.id);await D(a.DOWNLOAD_QUEUE,r),c("registered",{id:e.id,reason:"version-updated"})}return}await D(a.REGISTRY,n),await D(a.DOWNLOAD_QUEUE,ne(e.id)),c("registered",{id:e.id,reason:"new"})}async function H(e){if(!Array.isArray(e))throw new Error("registerFiles expects an array.");let t=new Set(e.map(n=>n.id)),o=await y(a.REGISTRY),s=[];for(let n of o)!t.has(n.id)&&!n.protected&&(await h(a.REGISTRY,n.id),await h(a.DOWNLOAD_QUEUE,n.id),s.push(n.id),c("deleted",{id:n.id,registryRemoved:!0}));for(let n of e)await G(n);return{registered:e.map(n=>n.id),removed:s}}async function ae(){let e=await y(a.DOWNLOAD_QUEUE),t=[];for(let o of e)o.status===u.COMPLETE&&Oe(o.expiresAt)&&(await D(a.DOWNLOAD_QUEUE,{...o,status:u.EXPIRED}),t.push(o.id),c("expired",{id:o.id}));return t}async function X(){let[e,t,o]=await Promise.all([y(a.REGISTRY),y(a.DOWNLOAD_QUEUE),U()]),s=new Map(t.map(r=>[r.id,r]));return{items:e.map(r=>{let i=s.get(r.id)??null;return{id:r.id,downloadUrl:r.downloadUrl,mimeType:r.mimeType??i?.blob?.type??null,version:r.version,protected:r.protected,priority:r.priority,ttl:r.ttl,totalBytes:r.totalBytes,metadata:r.metadata,registeredAt:r.registeredAt,updatedAt:r.updatedAt,downloadStatus:i?.status??null,bytesDownloaded:i?.bytesDownloaded??0,storedBytes:i?.blob?.size??null,progress:i?.totalBytes&&i?.bytesDownloaded?Math.round(i.bytesDownloaded/i.totalBytes*100):null,retryCount:i?.retryCount??0,lastAttemptAt:i?.lastAttemptAt??null,errorMessage:i?.errorMessage??null,deferredReason:i?.deferredReason??null,completedAt:i?.completedAt??null,expiresAt:i?.expiresAt??null}}).sort((r,i)=>r.priority-i.priority),storage:{usageBytes:o.usage,quotaBytes:o.quota,availableBytes:o.available,usageFormatted:B(o.usage),quotaFormatted:B(o.quota),availableFormatted:B(o.available)}}}async function z(e){let[t,o]=await Promise.all([w(a.REGISTRY,e),w(a.DOWNLOAD_QUEUE,e)]);return t?{id:t.id,downloadUrl:t.downloadUrl,mimeType:t.mimeType??o?.blob?.type??null,version:t.version,protected:t.protected,priority:t.priority,ttl:t.ttl,totalBytes:t.totalBytes,metadata:t.metadata,registeredAt:t.registeredAt,updatedAt:t.updatedAt,downloadStatus:o?.status??null,bytesDownloaded:o?.bytesDownloaded??0,storedBytes:o?.blob?.size??null,progress:o?.totalBytes&&o?.bytesDownloaded?Math.round(o.bytesDownloaded/o.totalBytes*100):null,retryCount:o?.retryCount??0,lastAttemptAt:o?.lastAttemptAt??null,errorMessage:o?.errorMessage??null,deferredReason:o?.deferredReason??null,completedAt:o?.completedAt??null,expiresAt:o?.expiresAt??null}:null}async function K(e){let t=await w(a.DOWNLOAD_QUEUE,e);return j.has(t?.status)}var V=null,Z=null,C=!1;function ie(){c("connectivity",{online:!1}),V?.()}function le(){c("connectivity",{online:!0}),Z?.()}function ue({pauseAll:e,resumeAll:t}){C||(V=e,Z=t,window.addEventListener("offline",ie),window.addEventListener("online",le),C=!0)}function Q(){window.removeEventListener("offline",ie),window.removeEventListener("online",le),V=null,Z=null,C=!1}function x(){return navigator.onLine??!0}function F(){return C}var he=2*1024*1024,Ue=5*1024*1024,xe=2,de=5,Te=1e3,b=new Map,ce={},ve=e=>new Promise(t=>setTimeout(t,e)),Ie=e=>Te*Math.pow(2,e);async function g(e,t){let o=await w(a.DOWNLOAD_QUEUE,e);o&&await D(a.DOWNLOAD_QUEUE,{...o,...t})}async function Ne(e,t){try{let o=await fetch(e,{method:"HEAD",signal:t}),s=o.headers.get("Accept-Ranges")==="bytes",n=o.headers.get("Content-Encoding"),r=!!n&&n!=="identity",i=o.headers.get("Content-Length"),d=i&&!r?parseInt(i,10):null,p=fe(o.headers.get("Content-Type"));return{supportsRange:s,totalBytes:d,mimeType:p}}catch{return{supportsRange:!1,totalBytes:null,mimeType:null}}}function fe(e){return e&&e.split(";")[0].trim()||null}function pe(e){let t=e.reduce((n,r)=>n+r.byteLength,0),o=new Uint8Array(t),s=0;for(let n of e)o.set(n,s),s+=n.byteLength;return o}async function Le(e){let{id:t,downloadUrl:o,ttl:s}=e,n=new AbortController;b.set(t,n);let r=await w(a.DOWNLOAD_QUEUE,t),i=r?.retryCount??0;for(;i<=de;)try{await g(t,{status:u.IN_PROGRESS,lastAttemptAt:Date.now(),retryCount:i,errorMessage:null}),c("status",{id:t,status:u.IN_PROGRESS}),r=await w(a.DOWNLOAD_QUEUE,t);let d=r?.byteOffset??0,p=!1,m=r?.totalBytes??e.totalBytes??null,E=e.mimeType??null;if(d===0){let R=await Ne(o,n.signal);p=R.supportsRange,R.totalBytes&&(m=R.totalBytes,await g(t,{totalBytes:m})),!E&&R.mimeType&&(E=R.mimeType)}else p=!0;let A=p&&m&&m>Ue,l,f=null;if(A)l=await Pe(t,o,d,m,n.signal);else{let R=await _e(t,o,n.signal);l=R.buffer,f=R.mimeType}let S=E??f??"application/octet-stream",O=new Blob([l],{type:S}),oe=Date.now(),we=se(oe,s);await g(t,{status:u.COMPLETE,blob:O,bytesDownloaded:l.byteLength,byteOffset:l.byteLength,completedAt:oe,expiresAt:we,errorMessage:null,deferredReason:null}),c("complete",{id:t,mimeType:S}),b.delete(t);return}catch(d){if(d.name==="AbortError"){await g(t,{status:u.PAUSED}),c("status",{id:t,status:u.PAUSED}),b.delete(t);return}if(i++,i>de){await g(t,{status:u.FAILED,retryCount:i,errorMessage:d.message}),c("error",{id:t,error:d,retryCount:i}),b.delete(t);return}let p=Ie(i-1);console.warn(`[offline-data-manager] "${t}" failed (attempt ${i}), retrying in ${p}ms:`,d.message),c("error",{id:t,error:d,retryCount:i,willRetry:!0}),await g(t,{status:u.PENDING,retryCount:i,errorMessage:d.message}),await ve(p)}}async function _e(e,t,o){let s=await fetch(t,{signal:o});if(!s.ok)throw new Error(`HTTP ${s.status} ${s.statusText}`);let n=s.headers.get("Content-Encoding"),r=!!n&&n!=="identity",i=s.headers.get("Content-Length"),d=i&&!r?parseInt(i,10):null,p=fe(s.headers.get("Content-Type")),m=s.body.getReader(),E=[],A=0;for(;;){let{done:l,value:f}=await m.read();if(l)break;E.push(f),A+=f.byteLength,await g(e,{bytesDownloaded:A,totalBytes:d}),c("progress",{id:e,bytesDownloaded:A,totalBytes:d,percent:d?Math.round(A/d*100):null})}return{buffer:pe(E),mimeType:p}}async function Pe(e,t,o,s,n){let r=o,i=[],d=o;for(;r<s;){let p=Math.min(r+he-1,s-1),m=await fetch(t,{signal:n,headers:{Range:`bytes=${r}-${p}`}});if(!m.ok&&m.status!==206)throw new Error(`HTTP ${m.status} on Range bytes=${r}-${p}`);let E=new Uint8Array(await m.arrayBuffer());i.push(E),r+=E.byteLength,d+=E.byteLength,await g(e,{bytesDownloaded:d,byteOffset:r}),c("progress",{id:e,bytesDownloaded:d,totalBytes:s,percent:Math.round(d/s*100)})}return pe(i)}async function N({concurrency:e=xe,resumeOnly:t=!1,retryFailed:o=!1}={}){if(ce={concurrency:e,resumeOnly:t,retryFailed:o},!x()){let l=await y(a.DOWNLOAD_QUEUE);for(let f of l)f.status===u.IN_PROGRESS&&(b.get(f.id)?.abort(),b.delete(f.id),await g(f.id,{status:u.PAUSED,deferredReason:"network-offline"}));c("connectivity",{online:!1});return}if(await ae(),o){let l=await y(a.DOWNLOAD_QUEUE);for(let f of l)f.status===u.FAILED&&await g(f.id,{status:u.PENDING,retryCount:0,errorMessage:null})}let[s,n]=await Promise.all([y(a.REGISTRY),y(a.DOWNLOAD_QUEUE)]),r=new Map(s.map(l=>[l.id,l])),i=t?[u.IN_PROGRESS,u.PAUSED]:[u.PENDING,u.IN_PROGRESS,u.PAUSED,u.DEFERRED,u.EXPIRED],p=[...n.filter(l=>i.includes(l.status)).sort((l,f)=>{let S=r.get(l.id)?.priority??10,O=r.get(f.id)?.priority??10;return S-O})],m=new Set;function E(){if(p.length===0)return;let l=p.shift(),f=r.get(l.id);if(!f)return;let S=(async()=>{let O=f.totalBytes??l.totalBytes??0;if(O>0&&!await re(O)){await g(l.id,{status:u.DEFERRED,deferredReason:"insufficient-storage"}),c("deferred",{id:l.id,reason:"insufficient-storage"});return}await Le(f)})().finally(()=>{m.delete(S),E()});m.add(S)}let A=Math.min(e,p.length);for(let l=0;l<A;l++)E();await new Promise(l=>{let f=setInterval(()=>{m.size===0&&p.length===0&&(clearInterval(f),l())},100)})}async function L(e){b.get(e)?.abort(),b.delete(e)}async function T(){for(let[e,t]of b)t.abort(),b.delete(e)}async function J(){await N({resumeOnly:!0})}function ee(){ue({pauseAll:T,resumeAll:()=>N(ce)})}async function Me(e){let t=await w(a.DOWNLOAD_QUEUE,e);t&&await D(a.DOWNLOAD_QUEUE,{...t,status:u.PENDING,blob:null,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null})}async function q(e,{removeRegistry:t=!1}={}){let o=await w(a.REGISTRY,e);if(!o)throw new Error(`deleteFile: No registered file with id "${e}".`);await L(e);let s=t||!o.protected;return s?(await h(a.REGISTRY,e),await h(a.DOWNLOAD_QUEUE,e)):await Me(e),c("deleted",{id:e,registryRemoved:s}),{id:e,registryRemoved:s}}async function te({removeRegistry:e=!1}={}){await T();let t=await y(a.REGISTRY);return Promise.all(t.map(o=>q(o.id,{removeRegistry:e})))}async function me(e){let[t,o]=await Promise.all([w(a.REGISTRY,e),w(a.DOWNLOAD_QUEUE,e)]);if(!t)throw new Error(`retrieve: No registered file with id "${e}".`);if(!j.has(o?.status)||!o?.blob)throw new Error(`retrieve: File "${e}" has no data yet (status: ${o?.status??"unknown"}).`);return o.blob}var Be={registerFile:G,registerFiles:H,downloadFiles:N,abortDownload:L,abortAllDownloads:T,resumeInterruptedDownloads:J,startMonitoring:ee,stopMonitoring:Q,isOnline:x,isMonitoring:F,retrieve:me,view:X,getStatus:z,isReady:K,delete:q,deleteAll:te,on:M,off:I,once:$,getStorageEstimate:U,requestPersistentStorage:Y,isPersistentStorage:k},Ge=Be;return Ae(Ce);})();
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "offline-data-manager",
3
+ "version": "1.0.0",
4
+ "description": "Service-worker-friendly offline file download and storage manager for JavaScript.",
5
+ "type": "module",
6
+ "main": "dist/umd/offline-data-manager.js",
7
+ "module": "dist/esm/offline-data-manager.js",
8
+ "sideEffects": false,
9
+ "browser": "dist/umd/offline-data-manager.js",
10
+ "types": "types/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/esm/offline-data-manager.js",
14
+ "require": "./dist/umd/offline-data-manager.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "types",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "node build/build.js",
24
+ "build:types": "tsc",
25
+ "prepublishOnly": "npm run build && npm run build:types"
26
+ },
27
+ "keywords": [
28
+ "offline",
29
+ "browser",
30
+ "indexeddb",
31
+ "download",
32
+ "cache"
33
+ ],
34
+ "devDependencies": {
35
+ "esbuild": "^0.27.3"
36
+ },
37
+ "author": "Ricky Brundritt",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/rbrundritt/offline-data-manager"
42
+ }
43
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Starts monitoring window online/offline events.
3
+ * Downloads are paused when offline and resumed when online.
4
+ *
5
+ * @param {object} handlers
6
+ * @param {Function} handlers.pauseAll — called when offline; should abort active downloads
7
+ * @param {Function} handlers.resumeAll — called when online; should call downloadFiles()
8
+ */
9
+ export function startConnectivityMonitor({ pauseAll, resumeAll }: {
10
+ pauseAll: Function;
11
+ resumeAll: Function;
12
+ }): void;
13
+ /**
14
+ * Stops monitoring and removes event listeners.
15
+ * After calling this, online/offline events will no longer trigger pause/resume.
16
+ */
17
+ export function stopConnectivityMonitor(): void;
18
+ /**
19
+ * Returns the current online status from navigator.onLine.
20
+ * Note: true does not guarantee the download servers are reachable,
21
+ * only that the browser believes it has a network connection.
22
+ * @returns {boolean}
23
+ */
24
+ export function isOnline(): boolean;
25
+ /**
26
+ * Returns true if connectivity monitoring is currently active.
27
+ * @returns {boolean}
28
+ */
29
+ export function isMonitoring(): boolean;
package/types/db.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Opens (or returns the cached) database connection.
3
+ * @returns {Promise<IDBDatabase>}
4
+ */
5
+ export function openDB(): Promise<IDBDatabase>;
6
+ /**
7
+ * Get a single record by key.
8
+ * @param {string} storeName
9
+ * @param {string} key
10
+ * @returns {Promise<any|undefined>}
11
+ */
12
+ export function dbGet(storeName: string, key: string): Promise<any | undefined>;
13
+ /**
14
+ * Get all records from a store.
15
+ * @param {string} storeName
16
+ * @returns {Promise<any[]>}
17
+ */
18
+ export function dbGetAll(storeName: string): Promise<any[]>;
19
+ /**
20
+ * Put (insert or replace) a record.
21
+ * @param {string} storeName
22
+ * @param {object} record
23
+ * @returns {Promise<void>}
24
+ */
25
+ export function dbPut(storeName: string, record: object): Promise<void>;
26
+ /**
27
+ * Delete a record by key.
28
+ * @param {string} storeName
29
+ * @param {string} key
30
+ * @returns {Promise<void>}
31
+ */
32
+ export function dbDelete(storeName: string, key: string): Promise<void>;
33
+ export namespace STORES {
34
+ let REGISTRY: string;
35
+ let DOWNLOAD_QUEUE: string;
36
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Deletes a single file's blob and optionally its registry entry.
3
+ *
4
+ * @param {string} id
5
+ * @param {object} [options]
6
+ * @param {boolean} [options.removeRegistry=false] — force registry removal for protected entries
7
+ * @returns {Promise<{ id: string, registryRemoved: boolean }>}
8
+ */
9
+ export function deleteFile(id: string, { removeRegistry }?: {
10
+ removeRegistry?: boolean | undefined;
11
+ }): Promise<{
12
+ id: string;
13
+ registryRemoved: boolean;
14
+ }>;
15
+ /**
16
+ * Deletes all files. Protected entries follow the same rules as deleteFile().
17
+ *
18
+ * @param {object} [options]
19
+ * @param {boolean} [options.removeRegistry=false]
20
+ * @returns {Promise<Array<{ id: string, registryRemoved: boolean }>>}
21
+ */
22
+ export function deleteAllFiles({ removeRegistry }?: {
23
+ removeRegistry?: boolean | undefined;
24
+ }): Promise<Array<{
25
+ id: string;
26
+ registryRemoved: boolean;
27
+ }>>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Evaluates TTL expiry, then downloads everything that needs it.
3
+ *
4
+ * Eligible statuses (when resumeOnly is false):
5
+ * pending, in-progress, paused, deferred, expired
6
+ * + failed entries when retryFailed: true
7
+ *
8
+ * If the browser is currently offline, the call returns immediately after
9
+ * marking any in-progress entries as paused. Downloads will auto-resume
10
+ * when connectivity is restored (if startConnectivityMonitor() was called).
11
+ *
12
+ * @param {object} [options]
13
+ * @param {number} [options.concurrency=2] — max parallel downloads
14
+ * @param {boolean} [options.resumeOnly=false] — only resume in-progress/paused
15
+ * @param {boolean} [options.retryFailed=false] — re-queue failed entries before running
16
+ * @returns {Promise<void>}
17
+ */
18
+ export function downloadFiles({ concurrency, resumeOnly, retryFailed, }?: {
19
+ concurrency?: number | undefined;
20
+ resumeOnly?: boolean | undefined;
21
+ retryFailed?: boolean | undefined;
22
+ }): Promise<void>;
23
+ /**
24
+ * Aborts an active download, setting it to 'paused'.
25
+ * It will resume on the next downloadFiles() call.
26
+ * @param {string} id
27
+ */
28
+ export function abortDownload(id: string): Promise<void>;
29
+ /**
30
+ * Aborts all active downloads.
31
+ */
32
+ export function abortAllDownloads(): Promise<void>;
33
+ /**
34
+ * Resumes any downloads interrupted by a previous page or SW close.
35
+ * Call this in a service worker 'activate' event.
36
+ * @returns {Promise<void>}
37
+ */
38
+ export function resumeInterruptedDownloads(): Promise<void>;
39
+ /**
40
+ * Starts monitoring online/offline connectivity.
41
+ *
42
+ * - Going offline: all active downloads are paused immediately.
43
+ * - Coming back online: downloadFiles() is called automatically with the
44
+ * same options as the most recent explicit call.
45
+ *
46
+ * Idempotent — safe to call multiple times. Call stopConnectivityMonitor()
47
+ * to remove the listeners (useful in tests or cleanup).
48
+ */
49
+ export function startMonitoring(): void;
50
+ export { stopConnectivityMonitor as stopMonitoring, isOnline, isMonitoring } from "./connectivity.js";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Subscribe to an event. Returns an unsubscribe function.
3
+ * @param {string} event
4
+ * @param {Function} listener
5
+ * @returns {Function}
6
+ */
7
+ export function on(event: string, listener: Function): Function;
8
+ /**
9
+ * Unsubscribe from an event.
10
+ * @param {string} event
11
+ * @param {Function} listener
12
+ */
13
+ export function off(event: string, listener: Function): void;
14
+ /**
15
+ * Emit an event to all registered listeners.
16
+ * @param {string} event
17
+ * @param {object} data
18
+ */
19
+ export function emit(event: string, data: object): void;
20
+ /**
21
+ * Subscribe to an event once; auto-removes after first call.
22
+ * @param {string} event
23
+ * @param {Function} listener
24
+ */
25
+ export function once(event: string, listener: Function): void;
@@ -0,0 +1,60 @@
1
+ export default OfflineDataManager;
2
+ declare namespace OfflineDataManager {
3
+ export { registerFile };
4
+ export { registerFiles };
5
+ export { downloadFiles };
6
+ export { abortDownload };
7
+ export { abortAllDownloads };
8
+ export { resumeInterruptedDownloads };
9
+ export { startMonitoring };
10
+ export { stopMonitoring };
11
+ export { isOnline };
12
+ export { isMonitoring };
13
+ export { retrieve };
14
+ export { view };
15
+ export { getStatus };
16
+ export { isReady };
17
+ export { deleteFile as delete };
18
+ export { deleteAllFiles as deleteAll };
19
+ export { on };
20
+ export { off };
21
+ export { once };
22
+ export { getStorageEstimate };
23
+ export { requestPersistentStorage };
24
+ export { isPersistentStorage };
25
+ }
26
+ import { registerFile } from './registry.js';
27
+ import { registerFiles } from './registry.js';
28
+ import { downloadFiles } from './downloader.js';
29
+ import { abortDownload } from './downloader.js';
30
+ import { abortAllDownloads } from './downloader.js';
31
+ import { resumeInterruptedDownloads } from './downloader.js';
32
+ import { startMonitoring } from './downloader.js';
33
+ import { stopMonitoring } from './downloader.js';
34
+ import { isOnline } from './downloader.js';
35
+ import { isMonitoring } from './downloader.js';
36
+ /**
37
+ * Retrieves the stored Blob for a registered file.
38
+ * Returns the Blob as-is — the caller is responsible for interpreting its contents.
39
+ *
40
+ * Returns the blob even if the file is expired; expiry only means a refresh is
41
+ * queued, not that the data is gone.
42
+ *
43
+ * @param {string} id
44
+ * @returns {Promise<Blob>}
45
+ * @throws {Error} if the file is not registered or has no blob yet
46
+ */
47
+ export function retrieve(id: string): Promise<Blob>;
48
+ import { view } from './registry.js';
49
+ import { getStatus } from './registry.js';
50
+ import { isReady } from './registry.js';
51
+ import { deleteFile } from './deleter.js';
52
+ import { deleteAllFiles } from './deleter.js';
53
+ import { on } from './events.js';
54
+ import { off } from './events.js';
55
+ import { once } from './events.js';
56
+ import { emit } from './events.js';
57
+ import { getStorageEstimate } from './storage.js';
58
+ import { requestPersistentStorage } from './storage.js';
59
+ import { isPersistentStorage } from './storage.js';
60
+ export { registerFile, registerFiles, downloadFiles, abortDownload, abortAllDownloads, resumeInterruptedDownloads, startMonitoring, stopMonitoring, isOnline, isMonitoring, view, getStatus, isReady, deleteFile, deleteAllFiles, on, off, once, emit, getStorageEstimate, requestPersistentStorage, isPersistentStorage };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Computes the expiresAt timestamp from a completedAt time and a ttl (seconds).
3
+ * Returns null if ttl is absent, zero, or falsy (meaning never expires).
4
+ *
5
+ * @param {number} completedAt — ms timestamp
6
+ * @param {number|undefined} ttl — seconds
7
+ * @returns {number|null}
8
+ */
9
+ export function computeExpiresAt(completedAt: number, ttl: number | undefined): number | null;
10
+ /**
11
+ * Returns true if an expiresAt timestamp has passed.
12
+ * @param {number|null} expiresAt
13
+ * @returns {boolean}
14
+ */
15
+ export function isExpired(expiresAt: number | null): boolean;
16
+ /**
17
+ * Registers a single file entry.
18
+ *
19
+ * - New entry: added to registry, fresh pending queue entry created.
20
+ * - Existing, version increased: registry updated, queue reset to pending.
21
+ * Existing blob remains accessible until the new download completes.
22
+ * - Existing, version unchanged or lower: no-op.
23
+ *
24
+ * @param {object} entry
25
+ * @returns {Promise<void>}
26
+ */
27
+ export function registerFile(entry: object): Promise<void>;
28
+ /**
29
+ * Registers an array of file entries and removes any registry entries
30
+ * whose IDs are no longer present in the incoming array.
31
+ *
32
+ * Protected entries missing from the new list are left untouched.
33
+ * Non-protected entries missing from the new list are fully removed.
34
+ *
35
+ * @param {object[]} entries
36
+ * @returns {Promise<{ registered: string[], removed: string[] }>}
37
+ */
38
+ export function registerFiles(entries: object[]): Promise<{
39
+ registered: string[];
40
+ removed: string[];
41
+ }>;
42
+ /**
43
+ * Checks all complete queue entries against their TTL and flips any that have
44
+ * expired to `expired` status, queuing them for re-download.
45
+ *
46
+ * Called internally by downloadFiles() before deciding what to download.
47
+ * @returns {Promise<string[]>} IDs of entries that were marked expired
48
+ */
49
+ export function evaluateExpiry(): Promise<string[]>;
50
+ /**
51
+ * Returns a merged view of all registry entries with their current download
52
+ * state, plus a storage summary.
53
+ *
54
+ * @returns {Promise<{ items: object[], storage: object }>}
55
+ */
56
+ export function view(): Promise<{
57
+ items: object[];
58
+ storage: object;
59
+ }>;
60
+ /**
61
+ * Returns the full merged status object for a single registered file,
62
+ * or null if not registered.
63
+ *
64
+ * @param {string} id
65
+ * @returns {Promise<object|null>}
66
+ */
67
+ export function getStatus(id: string): Promise<object | null>;
68
+ /**
69
+ * Returns true if a file has data available (complete or expired).
70
+ * An expired file still has a valid blob — it is simply due for refresh.
71
+ *
72
+ * @param {string} id
73
+ * @returns {Promise<boolean>}
74
+ */
75
+ export function isReady(id: string): Promise<boolean>;
76
+ export namespace DOWNLOAD_STATUS {
77
+ let PENDING: string;
78
+ let IN_PROGRESS: string;
79
+ let PAUSED: string;
80
+ let COMPLETE: string;
81
+ let EXPIRED: string;
82
+ let FAILED: string;
83
+ let DEFERRED: string;
84
+ }
85
+ export const READY_STATUSES: Set<string>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * storage.js
3
+ * Storage quota estimation utilities.
4
+ */
5
+ /**
6
+ * Returns current storage usage and quota from the browser Storage API.
7
+ * @returns {Promise<{ usage: number, quota: number, available: number }>}
8
+ */
9
+ export function getStorageEstimate(): Promise<{
10
+ usage: number;
11
+ quota: number;
12
+ available: number;
13
+ }>;
14
+ /**
15
+ * Returns true if there is sufficient available storage for the given byte count.
16
+ * Reserves 10% of total quota as a safety buffer.
17
+ *
18
+ * @param {number} requiredBytes
19
+ * @returns {Promise<boolean>}
20
+ */
21
+ export function hasEnoughSpace(requiredBytes: number): Promise<boolean>;
22
+ /**
23
+ * Requests persistent storage from the browser.
24
+ * Persistent storage is less likely to be evicted under storage pressure.
25
+ * @returns {Promise<boolean>}
26
+ */
27
+ export function requestPersistentStorage(): Promise<boolean>;
28
+ /**
29
+ * Returns true if persistent storage is already granted.
30
+ * @returns {Promise<boolean>}
31
+ */
32
+ export function isPersistentStorage(): Promise<boolean>;
33
+ /**
34
+ * Formats a byte count as a human-readable string.
35
+ * @param {number} bytes
36
+ * @returns {string}
37
+ */
38
+ export function formatBytes(bytes: number): string;