offline-data-manager 1.0.0 → 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ricky Brundritt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Readme.md CHANGED
@@ -1,9 +1,42 @@
1
1
  # offline-data-manager
2
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.
3
+ A service-worker-friendly library for registering, downloading, and storing files offline using IndexedDB.
4
+
5
+ [Try the sample app](https://rbrundritt.github.io/offline-data-manager/samples/) - Note that all the UI is from the sample app. This library only provides an API interface for managing offline data workflows.
6
+
7
+ > [!NOTE]
8
+ > Since this code runs in JavaScript and will most likely be used within a website, the data it accesses either needs to be on the same domain or hosted on a server with CORS enabled. Alternatively, you can pass cross domain requests through a CORS enabled proxy.
4
9
 
5
10
  ---
6
11
 
12
+ ## Features
13
+
14
+ - Create a register of data to download.
15
+ - Register items individually or in batches.
16
+ - Set a priority for each item to ensure more critical data is downloaded first.
17
+ - Optional mark it protected so that if a delete occurs, the registry persists and will redownload the data the next time download process it triggered.
18
+ - Optionally set a "time to live" (ttl) value so that data is automatically updated after a period of time. Expired data will persist until updated data has been downloaded, at which point the expired data will be replaced.
19
+ - Downloads data and stores as an ArrayBuffer in `indexedDB`.
20
+ - Files larger than 5MB are downloaded in 2MB chunks and merged back together when all chunks have been downloaded. This allows for downloads to be interupted and continue without having to start over from the beginning. This is useful if the user refreshes the page or leaves and comes back later.
21
+ - If a download fails, the retry option will attempt to redownload the data using an expotential backoff method up to 5 tries.
22
+ - Online/Offline state is monitored. Downloads are paused and resumed based on the state.
23
+ - Storage limits are monitored and not exceed. This information is also easily retrievable.
24
+
25
+ > [!TIP]
26
+ > If you use this with a single service worker, the download process will only ever have one instance running, download, and writing data. It will also persist between open pages of your app. For example, if you start the process on one webpage, open a second that's on the same domain, then close the first webpage, the service worker would continue to function uninterrupted.
27
+
28
+ ## Setup
29
+
30
+ ### With NPM Modules
31
+
32
+ ```bash
33
+ npm install offline-data-manager
34
+ ```
35
+
36
+ ### Browser (without modules)
37
+
38
+ Download and host the [dist/umd/offline-data-manager.js](dist/umd/offline-data-manager.js) file then add a script tag in your webapp pointing to this file. A global `offlineDataManager` class will be available in JavaScript.
39
+
7
40
  ## Running the test harness
8
41
 
9
42
  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://`).
@@ -20,9 +53,49 @@ Or with any static server you prefer:
20
53
  npx serve . # then open http://localhost:3000/test/index.html
21
54
  python3 -m http.server 3000 # then open http://localhost:3000/test/index.html
22
55
  ```
23
-
24
56
  ---
25
57
 
58
+ ## How to handle retrieved data
59
+
60
+ Once the data is downloaded, it is stored in `indexedDB` as an `ArrayBuffer` regardless of the content type. You could check the mimetype if you aren't certain of the type, although that usually shouldn't be the case. Note that the mimetype is only for the main file and not any contents it has. So a Zip file will have a mimetype of `octet-stream` even if it contains a bunch of PNG images which would have a mimetype of `image/png`. As a best practice, when registering the data, add any additional insights you have on the data into the `metadata` option to make it easier to process the data later. Here is some insights on how to get this into a more usable format.
61
+
62
+ ```js
63
+ try {
64
+ //Check that the file is ready to be access (status: complete or expired))
65
+ if(await offlineDataManager.retrieve(id)) {
66
+ //Retrieve the data. Format is { data: ArrayBuffer, mimeType: string }
67
+ const myFile = await offlineDataManager.retrieve(id);
68
+
69
+ //Retrieve the file ArrayBuffer.
70
+ const decoder = new TextDecoder("utf-8");
71
+ const text = decoder.decode(myFile.data);
72
+
73
+ //Get the data as json.
74
+ const json = JSON.parse(text);
75
+
76
+ //Get as data Uri.
77
+ function arrayBufferToDataUri(buffer, mimeType) {
78
+ const blob = new Blob([buffer], { type: mimeType });
79
+ return new Promise(resolve => {
80
+ const reader = new FileReader();
81
+ reader.onloadend = () => resolve(reader.result);
82
+ reader.readAsDataURL(blob);
83
+ });
84
+ }
85
+
86
+ const dataUri = await arrayBufferToDataUri(myFile.data, myFile.mimeType);
87
+
88
+ //Load the data Uri into an img tag.
89
+ document.getElementById('myImage').src = dataUri;
90
+ }
91
+ } catch(e) {
92
+ //If we get here it is likely the file ID is not in the registry or the file hasn't been downloaded yet.
93
+ }
94
+ ```
95
+
96
+ > [!TIP]
97
+ > If using this in a service worker to fullfil `fetch` requests, you can pass the `ArrayBuffer` as the content as is, no data transformation needed, and you will also have the content type information readily available as well.
98
+
26
99
  ## File structure
27
100
 
28
101
  ```
@@ -59,20 +132,20 @@ test/
59
132
 
60
133
  ### `version`
61
134
 
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.
135
+ When `registerFiles()` is called with a higher version, the queue resets to `pending` but the existing array buffer stays in IDB and remains accessible via `retrieve()` until the new download completes and overwrites it.
63
136
 
64
137
  ### `protected`
65
138
 
66
139
  | Value | On delete | Registry |
67
140
  |---|---|---|
68
- | `true` | Blob cleared, queue reset to `pending` | **Survives** — re-downloaded on next `downloadFiles()` |
141
+ | `true` | File data cleared, queue reset to `pending` | **Survives** — re-downloaded on next `downloadFiles()` |
69
142
  | `false` | Fully removed | **Removed** |
70
143
 
71
144
  Pass `{ removeRegistry: true }` to `delete()` to force full removal of a protected entry.
72
145
 
73
146
  ### `ttl`
74
147
 
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`.
148
+ 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 file data remains accessible throughout — there is no gap in availability. On completion the TTL clock resets from the new `completedAt`.
76
149
 
77
150
  ---
78
151
 
@@ -83,8 +156,8 @@ Time-to-live in seconds. On each `downloadFiles()` call, entries whose `complete
83
156
  | `pending` | Queued, not yet started |
84
157
  | `in-progress` | Actively downloading |
85
158
  | `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 |
159
+ | `complete` | File data stored and fresh |
160
+ | `expired` | File data stored but TTL has elapsed; still accessible, re-download queued |
88
161
  | `failed` | Exhausted all retries |
89
162
  | `deferred` | Skipped due to insufficient storage; retried next run |
90
163
 
@@ -92,6 +165,18 @@ Time-to-live in seconds. On each `downloadFiles()` call, entries whose `complete
92
165
 
93
166
  ## API
94
167
 
168
+ ### `setDBInfo(dbName, dbVersion)`
169
+
170
+ Overrides the default DB name and version number. If used, it should be done before using any other part of this API. By default the database name is `'offline-data-manager'` and the version is 1.
171
+
172
+ ```js
173
+ //Set the db name and version.
174
+ offlineDataManager.setDBInfo('my-offline-db', 2);
175
+
176
+ //Set the db version online. Database name will continue to be 'offline-data-manager'.
177
+ offlineDataManager.setDBInfo(null, 2);
178
+ ```
179
+
95
180
  ### `registerFile(entry)`
96
181
 
97
182
  Registers a single file. No-op if version hasn't strictly increased.
@@ -101,7 +186,7 @@ Registers a single file. No-op if version hasn't strictly increased.
101
186
  Registers an array of files. Removes non-protected entries absent from the list.
102
187
 
103
188
  ```js
104
- const { registered, removed } = await ODM.registerFiles([...]);
189
+ const { registered, removed } = await offlineDataManager.registerFiles([...]);
105
190
  ```
106
191
 
107
192
  ### `downloadFiles(options?)`
@@ -109,21 +194,20 @@ const { registered, removed } = await ODM.registerFiles([...]);
109
194
  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
195
 
111
196
  ```js
112
- await ODM.downloadFiles({ concurrency: 2, resumeOnly: false, retryFailed: false });
197
+ await offlineDataManager.downloadFiles({ concurrency: 2, resumeOnly: false, retryFailed: false });
113
198
  ```
114
199
 
115
200
  - `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
201
 
117
202
  ### `retrieve(id)`
118
203
 
119
- Returns the stored `Blob`. Works for both `complete` and `expired` entries.
204
+ Returns the stored file information `{ data: ArrayBuffer, mimeType: string }`. Works for both `complete` and `expired` entries.
120
205
 
121
206
  ```js
122
- const blob = await ODM.retrieve('json-data');
123
- const json = JSON.parse(await blob.text());
207
+ const fileInfo = await offlineDataManager.retrieve('json-data');
124
208
 
125
- const mapBlob = await ODM.retrieve('zip-data');
126
- const mapBuffer = await mapBlob.arrayBuffer(); // Process binary data.
209
+ const decoder = new TextDecoder("utf-8");
210
+ const text = decoder.decode(fileInfo.data);
127
211
  ```
128
212
 
129
213
  ### `view()`
@@ -131,7 +215,7 @@ const mapBuffer = await mapBlob.arrayBuffer(); // Process binary data.
131
215
  Returns all entries merged with queue state, plus storage summary.
132
216
 
133
217
  ```js
134
- const { items, storage } = await ODM.view();
218
+ const { items, storage } = await offlineDataManager.view();
135
219
  // items[n]: { id, mimeType, version, downloadStatus, storedBytes,
136
220
  // bytesDownloaded, progress, completedAt, expiresAt, ... }
137
221
  // storage: { usageBytes, quotaBytes, availableBytes, ...Formatted }
@@ -143,20 +227,20 @@ Full merged status for one file, or `null` if not registered.
143
227
 
144
228
  ### `isReady(id)`
145
229
 
146
- Returns `true` if the file has a blob available (`complete` or `expired`).
230
+ Returns `true` if the file has a data available (`complete` or `expired`).
147
231
 
148
232
  ### `delete(id, options?)`
149
233
 
150
234
  ```js
151
- await ODM.delete('poi-data'); // respects protected flag
152
- await ODM.delete('base-map', { removeRegistry: true }); // force full removal
235
+ await offlineDataManager.delete('poi-data'); // respects protected flag
236
+ await offlineDataManager.delete('base-map', { removeRegistry: true }); // force full removal
153
237
  ```
154
238
 
155
239
  ### `deleteAll(options?)`
156
240
 
157
241
  ```js
158
- await ODM.deleteAll();
159
- await ODM.deleteAll({ removeRegistry: true });
242
+ await offlineDataManager.deleteAll();
243
+ await offlineDataManager.deleteAll({ removeRegistry: true });
160
244
  ```
161
245
 
162
246
  ### `startMonitoring()` / `stopMonitoring()`
@@ -164,8 +248,8 @@ await ODM.deleteAll({ removeRegistry: true });
164
248
  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
249
 
166
250
  ```js
167
- ODM.startMonitoring(); // call once after first downloadFiles()
168
- ODM.stopMonitoring(); // remove listeners (e.g. in tests)
251
+ offlineDataManager.startMonitoring(); // call once after first downloadFiles()
252
+ offlineDataManager.stopMonitoring(); // remove listeners (e.g. in tests)
169
253
  ```
170
254
 
171
255
  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.
@@ -173,8 +257,8 @@ Emits a `'connectivity'` event `{ online: boolean }` on every change. `navigator
173
257
  ### `isOnline()` / `isMonitoring()`
174
258
 
175
259
  ```js
176
- ODM.isOnline(); // → boolean (navigator.onLine)
177
- ODM.isMonitoring(); // → boolean
260
+ offlineDataManager.isOnline(); // → boolean (navigator.onLine)
261
+ offlineDataManager.isMonitoring(); // → boolean
178
262
  ```
179
263
 
180
264
 
@@ -186,41 +270,42 @@ Resumes `paused` and `in-progress` entries. Call in a service worker `activate`
186
270
 
187
271
  ```js
188
272
  self.addEventListener('activate', (event) => {
189
- event.waitUntil(ODM.resumeInterruptedDownloads());
273
+ event.waitUntil(offlineDataManager.resumeInterruptedDownloads());
190
274
  });
191
275
  ```
192
276
 
193
277
  ### Events
194
278
 
195
279
  ```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 }) => {});
280
+ const unsub = offlineDataManager.on('progress', ({ id, bytesDownloaded, totalBytes, percent }) => {});
281
+ offlineDataManager.on('complete', ({ id }) => {});
282
+ offlineDataManager.on('expired', ({ id }) => {});
283
+ offlineDataManager.on('error', ({ id, error, retryCount, willRetry }) => {});
284
+ offlineDataManager.on('deferred', ({ id, reason }) => {});
285
+ offlineDataManager.on('registered', ({ id, reason }) => {}); // reason: 'new' | 'version-updated'
286
+ offlineDataManager.on('deleted', ({ id, registryRemoved }) => {});
287
+ offlineDataManager.on('status', ({ id, status }) => {});
288
+
289
+ offlineDataManager.once('complete', ({ id }) => {});
206
290
  unsub(); // remove listener
207
291
  ```
208
292
 
209
293
  ### Storage
210
294
 
211
295
  ```js
212
- const { usage, quota, available } = await ODM.getStorageEstimate();
213
- await ODM.requestPersistentStorage();
214
- await ODM.isPersistentStorage();
296
+ const { usage, quota, available } = await offlineDataManager.getStorageEstimate();
297
+ await offlineDataManager.requestPersistentStorage();
298
+ await offlineDataManager.isPersistentStorage();
215
299
  ```
216
300
 
217
301
  ---
218
302
 
219
303
  ## Notes
220
304
 
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()`.
305
+ - **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 surfaced in `view()` / `getStatus()`.
222
306
  - **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
307
  - **Chunking threshold** — files over 5 MB are chunked in 2 MB Range requests. Both constants are in `downloader.js`.
224
308
  - **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
309
  - **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.
310
+ - **ArrayBuffer in IDB** — the ArrayBuffer is stored directly on the queue record, so there is only one IDB store to manage rather than a separate data store.
311
+ - **ArrayBuffer vs Blob** - Using a Blob would allow for the content type to be defined within the blob itself without the need to specify it as a seperate value in the DB record, however there are known issues with storing Blob objects in indexedDB in certain browser versions.
@@ -1 +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};
1
+ var j="offline-data-manager",H=1,L=null,s={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function X(e,t){j=e??"offline-data-manager",H=t??1}async function x(){return L||(L=await new Promise((e,t)=>{let o=indexedDB.open(j,H);o.onupgradeneeded=a=>{let n=a.target.result;if(!n.objectStoreNames.contains(s.REGISTRY)){let r=n.createObjectStore(s.REGISTRY,{keyPath:"id"});r.createIndex("protected","protected",{unique:!1}),r.createIndex("priority","priority",{unique:!1})}if(!n.objectStoreNames.contains(s.DOWNLOAD_QUEUE)){let r=n.createObjectStore(s.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)}),L)}async function w(e,t){let o=await x();return new Promise((a,n)=>{let r=o.transaction(e,"readonly").objectStore(e).get(t);r.onsuccess=()=>a(r.result),r.onerror=()=>n(r.error)})}async function g(e){let t=await x();return new Promise((o,a)=>{let n=t.transaction(e,"readonly").objectStore(e).getAll();n.onsuccess=()=>o(n.result),n.onerror=()=>a(n.error)})}async function K(e){let t=await x();return new Promise((o,a)=>{let n=t.transaction(e,"readonly").objectStore(e).getAllKeys();n.onsuccess=()=>o(n.result),n.onerror=()=>a(n.error)})}async function D(e,t){let o=await x();return new Promise((a,n)=>{let r=o.transaction(e,"readwrite").objectStore(e).put(t);r.onsuccess=()=>a(),r.onerror=()=>n(r.error)})}async function h(e,t){let o=await x();return new Promise((a,n)=>{let r=o.transaction(e,"readwrite").objectStore(e).delete(t);r.onsuccess=()=>a(),r.onerror=()=>n(r.error)})}var U=new Map;function B(e,t){return U.has(e)||U.set(e,new Set),U.get(e).add(t),()=>N(e,t)}function N(e,t){U.get(e)?.delete(t)}function f(e,t){U.get(e)?.forEach(o=>{try{o(t)}catch(a){console.error(`[offline-data-manager] Error in "${e}" listener:`,a)}})}function z(e,t){let o=a=>{t(a),N(e,o)};B(e,o)}async function T(){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 V(e){let{available:t,quota:o}=await T();return t-o*.1>=e}async function Z(){return navigator?.storage?.persist?navigator.storage.persist():!1}async function J(){return navigator?.storage?.persisted?navigator.storage.persisted():!1}function P(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 l={PENDING:"pending",IN_PROGRESS:"in-progress",PAUSED:"paused",COMPLETE:"complete",EXPIRED:"expired",FAILED:"failed",DEFERRED:"deferred"},C=new Set([l.COMPLETE,l.EXPIRED]);function De(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 ee(e){return{id:e,status:l.PENDING,data:null,bytesDownloaded:0,totalBytes:null,byteOffset:0,retryCount:0,lastAttemptAt:null,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}}function te(e,t){return t?e+t*1e3:null}function Ae(e){return e?Date.now()>=e:!1}async function Q(e){De(e);let t=Date.now(),o=await w(s.REGISTRY,e.id),a=await w(s.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(s.REGISTRY,n);let r=a?{...a,status:l.PENDING,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}:ee(e.id);await D(s.DOWNLOAD_QUEUE,r),f("registered",{id:e.id,reason:"version-updated"})}return}await D(s.REGISTRY,n),await D(s.DOWNLOAD_QUEUE,ee(e.id)),f("registered",{id:e.id,reason:"new"})}async function oe(e){if(!Array.isArray(e))throw new Error("registerFiles expects an array.");let t=new Set(e.map(n=>n.id)),o=await g(s.REGISTRY),a=[];for(let n of o)!t.has(n.id)&&!n.protected&&(await h(s.REGISTRY,n.id),await h(s.DOWNLOAD_QUEUE,n.id),a.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:a}}async function re(){let e=await g(s.DOWNLOAD_QUEUE),t=[];for(let o of e)o.status===l.COMPLETE&&Ae(o.expiresAt)&&(await D(s.DOWNLOAD_QUEUE,{...o,status:l.EXPIRED}),t.push(o.id),f("expired",{id:o.id}));return t}async function ne(){let[e,t,o]=await Promise.all([g(s.REGISTRY),g(s.DOWNLOAD_QUEUE),T()]),a=new Map(t.map(r=>[r.id,r]));return{items:e.map(r=>{let i=a.get(r.id)??null;return{id:r.id,downloadUrl:r.downloadUrl,mimeType:r.mimeType,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?.data?.length??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:P(o.usage),quotaFormatted:P(o.quota),availableFormatted:P(o.available)}}}async function ae(e){let[t,o]=await Promise.all([w(s.REGISTRY,e),w(s.DOWNLOAD_QUEUE,e)]);return t?{id:t.id,downloadUrl:t.downloadUrl,mimeType:t.mimeType??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?.data?.length??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 se(e){let t=await w(s.DOWNLOAD_QUEUE,e);return C.has(t?.status)}var F=null,q=null,_=!1;function ie(){f("connectivity",{online:!1}),F?.()}function le(){f("connectivity",{online:!0}),q?.()}function ue({pauseAll:e,resumeAll:t}){_||(F=e,q=t,window.addEventListener("offline",ie),window.addEventListener("online",le),_=!0)}function W(){window.removeEventListener("offline",ie),window.removeEventListener("online",le),F=null,q=null,_=!1}function I(){return navigator.onLine??!0}function $(){return _}var be=2*1024*1024,Re=5*1024*1024,Se=2,de=5,Oe=1e3,A=new Map,fe={},he=e=>new Promise(t=>setTimeout(t,e)),xe=e=>Oe*Math.pow(2,e);async function y(e,t){let o=await w(s.DOWNLOAD_QUEUE,e);o&&await D(s.DOWNLOAD_QUEUE,{...o,...t})}async function Ue(e,t){try{let o=await fetch(e,{method:"HEAD",signal:t}),a=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=pe(o.headers.get("Content-Type"));return{supportsRange:a,totalBytes:d,mimeType:p}}catch{return{supportsRange:!1,totalBytes:null,mimeType:null}}}function pe(e){return e&&e.split(";")[0].trim()||null}function me(e){let t=e.reduce((n,r)=>n+r.byteLength,0),o=new Uint8Array(t),a=0;for(let n of e)o.set(n,a),a+=n.byteLength;return o}async function Te(e){let{id:t,downloadUrl:o,ttl:a}=e,n=new AbortController;A.set(t,n);let r=await w(s.DOWNLOAD_QUEUE,t),i=r?.retryCount??0;for(;i<=de;)try{await y(t,{status:l.IN_PROGRESS,lastAttemptAt:Date.now(),retryCount:i,errorMessage:null}),f("status",{id:t,status:l.IN_PROGRESS}),r=await w(s.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 Ue(o,n.signal);p=R.supportsRange,R.totalBytes&&(m=R.totalBytes,await y(t,{totalBytes:m})),!E&&R.mimeType&&(E=R.mimeType)}else p=!0;let b=p&&m&&m>Re,u,c=null;if(b)u=await ce(t,o,d,m,n.signal);else{let R=await Ie(t,o,n.signal);u=R.buffer,c=R.mimeType}let S=E??c??"application/octet-stream",O=ce.buffer,k=Date.now(),ge=te(k,a);await y(t,{status:l.COMPLETE,data:O,bytesDownloaded:u.byteLength,byteOffset:u.byteLength,completedAt:k,expiresAt:ge,errorMessage:null,deferredReason:null,mimeType:S}),f("complete",{id:t,mimeType:S}),A.delete(t);return}catch(d){if(d.name==="AbortError"){await y(t,{status:l.PAUSED}),f("status",{id:t,status:l.PAUSED}),A.delete(t);return}if(i++,i>de){await y(t,{status:l.FAILED,retryCount:i,errorMessage:d.message}),f("error",{id:t,error:d,retryCount:i}),A.delete(t);return}let p=xe(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 y(t,{status:l.PENDING,retryCount:i,errorMessage:d.message}),await he(p)}}async function Ie(e,t,o){let a=await fetch(t,{signal:o});if(!a.ok)throw new Error(`HTTP ${a.status} ${a.statusText}`);let n=a.headers.get("Content-Encoding"),r=!!n&&n!=="identity",i=a.headers.get("Content-Length"),d=i&&!r?parseInt(i,10):null,p=pe(a.headers.get("Content-Type")),m=a.body.getReader(),E=[],b=0;for(;;){let{done:u,value:c}=await m.read();if(u)break;E.push(c),b+=c.byteLength,await y(e,{bytesDownloaded:b,totalBytes:d}),f("progress",{id:e,bytesDownloaded:b,totalBytes:d,percent:d?Math.round(b/d*100):null})}return{buffer:me(E),mimeType:p}}async function ce(e,t,o,a,n){let r=o,i=[],d=o;for(;r<a;){let p=Math.min(r+be-1,a-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 y(e,{bytesDownloaded:d,byteOffset:r}),f("progress",{id:e,bytesDownloaded:d,totalBytes:a,percent:Math.round(d/a*100)})}return me(i)}async function M({concurrency:e=Se,resumeOnly:t=!1,retryFailed:o=!1}={}){if(fe={concurrency:e,resumeOnly:t,retryFailed:o},!I()){let u=await g(s.DOWNLOAD_QUEUE);for(let c of u)c.status===l.IN_PROGRESS&&(A.get(c.id)?.abort(),A.delete(c.id),await y(c.id,{status:l.PAUSED,deferredReason:"network-offline"}));f("connectivity",{online:!1});return}if(await re(),o){let u=await g(s.DOWNLOAD_QUEUE);for(let c of u)c.status===l.FAILED&&await y(c.id,{status:l.PENDING,retryCount:0,errorMessage:null})}let[a,n]=await Promise.all([g(s.REGISTRY),g(s.DOWNLOAD_QUEUE)]),r=new Map(a.map(u=>[u.id,u])),i=t?[l.IN_PROGRESS,l.PAUSED]:[l.PENDING,l.IN_PROGRESS,l.PAUSED,l.DEFERRED,l.EXPIRED],p=[...n.filter(u=>i.includes(u.status)).sort((u,c)=>{let S=r.get(u.id)?.priority??10,O=r.get(c.id)?.priority??10;return S-O})],m=new Set;function E(){if(p.length===0)return;let u=p.shift(),c=r.get(u.id);if(!c)return;let S=(async()=>{let O=c.totalBytes??u.totalBytes??0;if(O>0&&!await V(O)){await y(u.id,{status:l.DEFERRED,deferredReason:"insufficient-storage"}),f("deferred",{id:u.id,reason:"insufficient-storage"});return}await Te(c)})().finally(()=>{m.delete(S),E()});m.add(S)}let b=Math.min(e,p.length);for(let u=0;u<b;u++)E();await new Promise(u=>{let c=setInterval(()=>{m.size===0&&p.length===0&&(clearInterval(c),u())},100)})}async function G(e){A.get(e)?.abort(),A.delete(e)}async function v(){for(let[e,t]of A)t.abort(),A.delete(e)}async function we(){await M({resumeOnly:!0})}function Ee(){ue({pauseAll:v,resumeAll:()=>M(fe)})}async function ve(e){let t=await w(s.DOWNLOAD_QUEUE,e);t&&await D(s.DOWNLOAD_QUEUE,{...t,status:l.PENDING,data: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(s.REGISTRY,e);if(!o)throw new Error(`deleteFile: No registered file with id "${e}".`);await G(e);let a=t||!o.protected;return a?(await h(s.REGISTRY,e),await h(s.DOWNLOAD_QUEUE,e)):await ve(e),f("deleted",{id:e,registryRemoved:a}),{id:e,registryRemoved:a}}async function ye({removeRegistry:e=!1}={}){await v();let t=await K(s.REGISTRY);return Promise.all(t.map(o=>Y(o,{removeRegistry:e})))}async function Le(e){let[t,o]=await Promise.all([w(s.REGISTRY,e),w(s.DOWNLOAD_QUEUE,e)]);if(!t)throw new Error(`retrieve: No registered file with id "${e}".`);if(!C.has(o?.status)||!o?.data)throw new Error(`retrieve: File "${e}" has no data yet (status: ${o?.status??"unknown"}).`);return{data:o.data,mimeType:o.mimeType}}var Ne={setDBInfo:X,registerFile:Q,registerFiles:oe,downloadFiles:M,abortDownload:G,abortAllDownloads:v,resumeInterruptedDownloads:we,startMonitoring:Ee,stopMonitoring:W,isOnline:I,isMonitoring:$,retrieve:Le,view:ne,getStatus:ae,isReady:se,delete:Y,deleteAll:ye,on:B,off:N,once:z,getStorageEstimate:T,requestPersistentStorage:Z,isPersistentStorage:J},lt=Ne;export{v as abortAllDownloads,G as abortDownload,lt as default,ye as deleteAllFiles,Y as deleteFile,M as downloadFiles,f as emit,ae as getStatus,T as getStorageEstimate,$ as isMonitoring,I as isOnline,J as isPersistentStorage,se as isReady,N as off,B as on,z as once,Q as registerFile,oe as registerFiles,Z as requestPersistentStorage,we as resumeInterruptedDownloads,Le as retrieve,Ee as startMonitoring,W as stopMonitoring,ne as view};
@@ -1 +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);})();
1
+ "use strict";var offlineMapData=(()=>{var W=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Re=Object.getOwnPropertyNames;var Se=Object.prototype.hasOwnProperty;var Oe=(e,t)=>{for(var o in t)W(e,o,{get:t[o],enumerable:!0})},he=(e,t,o,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of Re(t))!Se.call(e,r)&&r!==o&&W(e,r,{get:()=>t[r],enumerable:!(a=be(t,r))||a.enumerable});return e};var xe=e=>he(W({},"__esModule",{value:!0}),e);var qe={};Oe(qe,{abortAllDownloads:()=>T,abortDownload:()=>P,default:()=>Fe,deleteAllFiles:()=>te,deleteFile:()=>q,downloadFiles:()=>N,emit:()=>c,getStatus:()=>K,getStorageEstimate:()=>x,isMonitoring:()=>F,isOnline:()=>U,isPersistentStorage:()=>k,isReady:()=>z,off:()=>L,on:()=>M,once:()=>$,registerFile:()=>B,registerFiles:()=>H,requestPersistentStorage:()=>Y,resumeInterruptedDownloads:()=>J,retrieve:()=>De,startMonitoring:()=>ee,stopMonitoring:()=>Q,view:()=>X});var re="offline-data-manager",ne=1,_=null,s={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function ae(e,t){re=e??"offline-data-manager",ne=t??1}async function I(){return _||(_=await new Promise((e,t)=>{let o=indexedDB.open(re,ne);o.onupgradeneeded=a=>{let r=a.target.result;if(!r.objectStoreNames.contains(s.REGISTRY)){let n=r.createObjectStore(s.REGISTRY,{keyPath:"id"});n.createIndex("protected","protected",{unique:!1}),n.createIndex("priority","priority",{unique:!1})}if(!r.objectStoreNames.contains(s.DOWNLOAD_QUEUE)){let n=r.createObjectStore(s.DOWNLOAD_QUEUE,{keyPath:"id"});n.createIndex("status","status",{unique:!1}),n.createIndex("priority","priority",{unique:!1})}},o.onsuccess=()=>e(o.result),o.onerror=()=>t(o.error)}),_)}async function w(e,t){let o=await I();return new Promise((a,r)=>{let n=o.transaction(e,"readonly").objectStore(e).get(t);n.onsuccess=()=>a(n.result),n.onerror=()=>r(n.error)})}async function g(e){let t=await I();return new Promise((o,a)=>{let r=t.transaction(e,"readonly").objectStore(e).getAll();r.onsuccess=()=>o(r.result),r.onerror=()=>a(r.error)})}async function se(e){let t=await I();return new Promise((o,a)=>{let r=t.transaction(e,"readonly").objectStore(e).getAllKeys();r.onsuccess=()=>o(r.result),r.onerror=()=>a(r.error)})}async function D(e,t){let o=await I();return new Promise((a,r)=>{let n=o.transaction(e,"readwrite").objectStore(e).put(t);n.onsuccess=()=>a(),n.onerror=()=>r(n.error)})}async function h(e,t){let o=await I();return new Promise((a,r)=>{let n=o.transaction(e,"readwrite").objectStore(e).delete(t);n.onsuccess=()=>a(),n.onerror=()=>r(n.error)})}var v=new Map;function M(e,t){return v.has(e)||v.set(e,new Set),v.get(e).add(t),()=>L(e,t)}function L(e,t){v.get(e)?.delete(t)}function c(e,t){v.get(e)?.forEach(o=>{try{o(t)}catch(a){console.error(`[offline-data-manager] Error in "${e}" listener:`,a)}})}function $(e,t){let o=a=>{t(a),L(e,o)};M(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 ie(e){let{available:t,quota:o}=await x();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 G(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 l={PENDING:"pending",IN_PROGRESS:"in-progress",PAUSED:"paused",COMPLETE:"complete",EXPIRED:"expired",FAILED:"failed",DEFERRED:"deferred"},j=new Set([l.COMPLETE,l.EXPIRED]);function Ue(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 le(e){return{id:e,status:l.PENDING,data:null,bytesDownloaded:0,totalBytes:null,byteOffset:0,retryCount:0,lastAttemptAt:null,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}}function ue(e,t){return t?e+t*1e3:null}function Te(e){return e?Date.now()>=e:!1}async function B(e){Ue(e);let t=Date.now(),o=await w(s.REGISTRY,e.id),a=await w(s.DOWNLOAD_QUEUE,e.id),r={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(s.REGISTRY,r);let n=a?{...a,status:l.PENDING,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}:le(e.id);await D(s.DOWNLOAD_QUEUE,n),c("registered",{id:e.id,reason:"version-updated"})}return}await D(s.REGISTRY,r),await D(s.DOWNLOAD_QUEUE,le(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(r=>r.id)),o=await g(s.REGISTRY),a=[];for(let r of o)!t.has(r.id)&&!r.protected&&(await h(s.REGISTRY,r.id),await h(s.DOWNLOAD_QUEUE,r.id),a.push(r.id),c("deleted",{id:r.id,registryRemoved:!0}));for(let r of e)await B(r);return{registered:e.map(r=>r.id),removed:a}}async function de(){let e=await g(s.DOWNLOAD_QUEUE),t=[];for(let o of e)o.status===l.COMPLETE&&Te(o.expiresAt)&&(await D(s.DOWNLOAD_QUEUE,{...o,status:l.EXPIRED}),t.push(o.id),c("expired",{id:o.id}));return t}async function X(){let[e,t,o]=await Promise.all([g(s.REGISTRY),g(s.DOWNLOAD_QUEUE),x()]),a=new Map(t.map(n=>[n.id,n]));return{items:e.map(n=>{let i=a.get(n.id)??null;return{id:n.id,downloadUrl:n.downloadUrl,mimeType:n.mimeType,version:n.version,protected:n.protected,priority:n.priority,ttl:n.ttl,totalBytes:n.totalBytes,metadata:n.metadata,registeredAt:n.registeredAt,updatedAt:n.updatedAt,downloadStatus:i?.status??null,bytesDownloaded:i?.bytesDownloaded??0,storedBytes:i?.data?.length??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((n,i)=>n.priority-i.priority),storage:{usageBytes:o.usage,quotaBytes:o.quota,availableBytes:o.available,usageFormatted:G(o.usage),quotaFormatted:G(o.quota),availableFormatted:G(o.available)}}}async function K(e){let[t,o]=await Promise.all([w(s.REGISTRY,e),w(s.DOWNLOAD_QUEUE,e)]);return t?{id:t.id,downloadUrl:t.downloadUrl,mimeType:t.mimeType??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?.data?.length??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 z(e){let t=await w(s.DOWNLOAD_QUEUE,e);return j.has(t?.status)}var V=null,Z=null,C=!1;function ce(){c("connectivity",{online:!1}),V?.()}function fe(){c("connectivity",{online:!0}),Z?.()}function pe({pauseAll:e,resumeAll:t}){C||(V=e,Z=t,window.addEventListener("offline",ce),window.addEventListener("online",fe),C=!0)}function Q(){window.removeEventListener("offline",ce),window.removeEventListener("online",fe),V=null,Z=null,C=!1}function U(){return navigator.onLine??!0}function F(){return C}var Ie=2*1024*1024,ve=5*1024*1024,Le=2,me=5,Ne=1e3,A=new Map,Ee={},Pe=e=>new Promise(t=>setTimeout(t,e)),_e=e=>Ne*Math.pow(2,e);async function y(e,t){let o=await w(s.DOWNLOAD_QUEUE,e);o&&await D(s.DOWNLOAD_QUEUE,{...o,...t})}async function Me(e,t){try{let o=await fetch(e,{method:"HEAD",signal:t}),a=o.headers.get("Accept-Ranges")==="bytes",r=o.headers.get("Content-Encoding"),n=!!r&&r!=="identity",i=o.headers.get("Content-Length"),d=i&&!n?parseInt(i,10):null,p=ye(o.headers.get("Content-Type"));return{supportsRange:a,totalBytes:d,mimeType:p}}catch{return{supportsRange:!1,totalBytes:null,mimeType:null}}}function ye(e){return e&&e.split(";")[0].trim()||null}function ge(e){let t=e.reduce((r,n)=>r+n.byteLength,0),o=new Uint8Array(t),a=0;for(let r of e)o.set(r,a),a+=r.byteLength;return o}async function Ge(e){let{id:t,downloadUrl:o,ttl:a}=e,r=new AbortController;A.set(t,r);let n=await w(s.DOWNLOAD_QUEUE,t),i=n?.retryCount??0;for(;i<=me;)try{await y(t,{status:l.IN_PROGRESS,lastAttemptAt:Date.now(),retryCount:i,errorMessage:null}),c("status",{id:t,status:l.IN_PROGRESS}),n=await w(s.DOWNLOAD_QUEUE,t);let d=n?.byteOffset??0,p=!1,m=n?.totalBytes??e.totalBytes??null,E=e.mimeType??null;if(d===0){let R=await Me(o,r.signal);p=R.supportsRange,R.totalBytes&&(m=R.totalBytes,await y(t,{totalBytes:m})),!E&&R.mimeType&&(E=R.mimeType)}else p=!0;let b=p&&m&&m>ve,u,f=null;if(b)u=await we(t,o,d,m,r.signal);else{let R=await Be(t,o,r.signal);u=R.buffer,f=R.mimeType}let S=E??f??"application/octet-stream",O=we.buffer,oe=Date.now(),Ae=ue(oe,a);await y(t,{status:l.COMPLETE,data:O,bytesDownloaded:u.byteLength,byteOffset:u.byteLength,completedAt:oe,expiresAt:Ae,errorMessage:null,deferredReason:null,mimeType:S}),c("complete",{id:t,mimeType:S}),A.delete(t);return}catch(d){if(d.name==="AbortError"){await y(t,{status:l.PAUSED}),c("status",{id:t,status:l.PAUSED}),A.delete(t);return}if(i++,i>me){await y(t,{status:l.FAILED,retryCount:i,errorMessage:d.message}),c("error",{id:t,error:d,retryCount:i}),A.delete(t);return}let p=_e(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 y(t,{status:l.PENDING,retryCount:i,errorMessage:d.message}),await Pe(p)}}async function Be(e,t,o){let a=await fetch(t,{signal:o});if(!a.ok)throw new Error(`HTTP ${a.status} ${a.statusText}`);let r=a.headers.get("Content-Encoding"),n=!!r&&r!=="identity",i=a.headers.get("Content-Length"),d=i&&!n?parseInt(i,10):null,p=ye(a.headers.get("Content-Type")),m=a.body.getReader(),E=[],b=0;for(;;){let{done:u,value:f}=await m.read();if(u)break;E.push(f),b+=f.byteLength,await y(e,{bytesDownloaded:b,totalBytes:d}),c("progress",{id:e,bytesDownloaded:b,totalBytes:d,percent:d?Math.round(b/d*100):null})}return{buffer:ge(E),mimeType:p}}async function we(e,t,o,a,r){let n=o,i=[],d=o;for(;n<a;){let p=Math.min(n+Ie-1,a-1),m=await fetch(t,{signal:r,headers:{Range:`bytes=${n}-${p}`}});if(!m.ok&&m.status!==206)throw new Error(`HTTP ${m.status} on Range bytes=${n}-${p}`);let E=new Uint8Array(await m.arrayBuffer());i.push(E),n+=E.byteLength,d+=E.byteLength,await y(e,{bytesDownloaded:d,byteOffset:n}),c("progress",{id:e,bytesDownloaded:d,totalBytes:a,percent:Math.round(d/a*100)})}return ge(i)}async function N({concurrency:e=Le,resumeOnly:t=!1,retryFailed:o=!1}={}){if(Ee={concurrency:e,resumeOnly:t,retryFailed:o},!U()){let u=await g(s.DOWNLOAD_QUEUE);for(let f of u)f.status===l.IN_PROGRESS&&(A.get(f.id)?.abort(),A.delete(f.id),await y(f.id,{status:l.PAUSED,deferredReason:"network-offline"}));c("connectivity",{online:!1});return}if(await de(),o){let u=await g(s.DOWNLOAD_QUEUE);for(let f of u)f.status===l.FAILED&&await y(f.id,{status:l.PENDING,retryCount:0,errorMessage:null})}let[a,r]=await Promise.all([g(s.REGISTRY),g(s.DOWNLOAD_QUEUE)]),n=new Map(a.map(u=>[u.id,u])),i=t?[l.IN_PROGRESS,l.PAUSED]:[l.PENDING,l.IN_PROGRESS,l.PAUSED,l.DEFERRED,l.EXPIRED],p=[...r.filter(u=>i.includes(u.status)).sort((u,f)=>{let S=n.get(u.id)?.priority??10,O=n.get(f.id)?.priority??10;return S-O})],m=new Set;function E(){if(p.length===0)return;let u=p.shift(),f=n.get(u.id);if(!f)return;let S=(async()=>{let O=f.totalBytes??u.totalBytes??0;if(O>0&&!await ie(O)){await y(u.id,{status:l.DEFERRED,deferredReason:"insufficient-storage"}),c("deferred",{id:u.id,reason:"insufficient-storage"});return}await Ge(f)})().finally(()=>{m.delete(S),E()});m.add(S)}let b=Math.min(e,p.length);for(let u=0;u<b;u++)E();await new Promise(u=>{let f=setInterval(()=>{m.size===0&&p.length===0&&(clearInterval(f),u())},100)})}async function P(e){A.get(e)?.abort(),A.delete(e)}async function T(){for(let[e,t]of A)t.abort(),A.delete(e)}async function J(){await N({resumeOnly:!0})}function ee(){pe({pauseAll:T,resumeAll:()=>N(Ee)})}async function Ce(e){let t=await w(s.DOWNLOAD_QUEUE,e);t&&await D(s.DOWNLOAD_QUEUE,{...t,status:l.PENDING,data: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(s.REGISTRY,e);if(!o)throw new Error(`deleteFile: No registered file with id "${e}".`);await P(e);let a=t||!o.protected;return a?(await h(s.REGISTRY,e),await h(s.DOWNLOAD_QUEUE,e)):await Ce(e),c("deleted",{id:e,registryRemoved:a}),{id:e,registryRemoved:a}}async function te({removeRegistry:e=!1}={}){await T();let t=await se(s.REGISTRY);return Promise.all(t.map(o=>q(o,{removeRegistry:e})))}async function De(e){let[t,o]=await Promise.all([w(s.REGISTRY,e),w(s.DOWNLOAD_QUEUE,e)]);if(!t)throw new Error(`retrieve: No registered file with id "${e}".`);if(!j.has(o?.status)||!o?.data)throw new Error(`retrieve: File "${e}" has no data yet (status: ${o?.status??"unknown"}).`);return{data:o.data,mimeType:o.mimeType}}var Qe={setDBInfo:ae,registerFile:B,registerFiles:H,downloadFiles:N,abortDownload:P,abortAllDownloads:T,resumeInterruptedDownloads:J,startMonitoring:ee,stopMonitoring:Q,isOnline:U,isMonitoring:F,retrieve:De,view:X,getStatus:K,isReady:z,delete:q,deleteAll:te,on:M,off:L,once:$,getStorageEstimate:x,requestPersistentStorage:Y,isPersistentStorage:k},Fe=Qe;return xe(qe);})();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "offline-data-manager",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Service-worker-friendly offline file download and storage manager for JavaScript.",
5
5
  "type": "module",
6
6
  "main": "dist/umd/offline-data-manager.js",
7
7
  "module": "dist/esm/offline-data-manager.js",
8
- "sideEffects": false,
9
8
  "browser": "dist/umd/offline-data-manager.js",
9
+ "sideEffects": false,
10
10
  "types": "types/index.d.ts",
11
11
  "exports": {
12
12
  ".": {
package/types/db.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Overrides the default DB name and version number.
3
+ * @param {string|undefined} dbName Optional DB name. Default: 'offline-data-manager'
4
+ * @param {number|undefined} dbVersion Optional DB version. Default: 1
5
+ */
6
+ export function setDBInfo(dbName: string | undefined, dbVersion: number | undefined): Promise<void>;
1
7
  /**
2
8
  * Opens (or returns the cached) database connection.
3
9
  * @returns {Promise<IDBDatabase>}
@@ -16,6 +22,12 @@ export function dbGet(storeName: string, key: string): Promise<any | undefined>;
16
22
  * @returns {Promise<any[]>}
17
23
  */
18
24
  export function dbGetAll(storeName: string): Promise<any[]>;
25
+ /**
26
+ * Get all record ids from a store.
27
+ * @param {string} storeName
28
+ * @returns {Promise<string[]>}
29
+ */
30
+ export function dbGetAllIds(storeName: string): Promise<string[]>;
19
31
  /**
20
32
  * Put (insert or replace) a record.
21
33
  * @param {string} storeName
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Deletes a single file's blob and optionally its registry entry.
2
+ * Deletes a single file's array buffer and optionally its registry entry.
3
3
  *
4
4
  * @param {string} id
5
5
  * @param {object} [options]
package/types/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export default OfflineDataManager;
2
2
  declare namespace OfflineDataManager {
3
+ export { setDBInfo };
3
4
  export { registerFile };
4
5
  export { registerFiles };
5
6
  export { downloadFiles };
@@ -34,17 +35,20 @@ import { stopMonitoring } from './downloader.js';
34
35
  import { isOnline } from './downloader.js';
35
36
  import { isMonitoring } from './downloader.js';
36
37
  /**
37
- * Retrieves the stored Blob for a registered file.
38
- * Returns the Blob as-is the caller is responsible for interpreting its contents.
38
+ * Retrieves the stored file data for a registered file.
39
+ * Returns the array buffer and content type.
39
40
  *
40
- * Returns the blob even if the file is expired; expiry only means a refresh is
41
+ * Returns the data even if the file is expired; expiry only means a refresh is
41
42
  * queued, not that the data is gone.
42
43
  *
43
44
  * @param {string} id
44
- * @returns {Promise<Blob>}
45
- * @throws {Error} if the file is not registered or has no blob yet
45
+ * @returns {Promise<{ data: ArrayBuffer, mimeType: string }>}
46
+ * @throws {Error} if the file is not registered or has no data yet
46
47
  */
47
- export function retrieve(id: string): Promise<Blob>;
48
+ export function retrieve(id: string): Promise<{
49
+ data: ArrayBuffer;
50
+ mimeType: string;
51
+ }>;
48
52
  import { view } from './registry.js';
49
53
  import { getStatus } from './registry.js';
50
54
  import { isReady } from './registry.js';
@@ -57,4 +61,5 @@ import { emit } from './events.js';
57
61
  import { getStorageEstimate } from './storage.js';
58
62
  import { requestPersistentStorage } from './storage.js';
59
63
  import { isPersistentStorage } from './storage.js';
64
+ import { setDBInfo } from './db.js';
60
65
  export { registerFile, registerFiles, downloadFiles, abortDownload, abortAllDownloads, resumeInterruptedDownloads, startMonitoring, stopMonitoring, isOnline, isMonitoring, view, getStatus, isReady, deleteFile, deleteAllFiles, on, off, once, emit, getStorageEstimate, requestPersistentStorage, isPersistentStorage };
@@ -18,7 +18,7 @@ export function isExpired(expiresAt: number | null): boolean;
18
18
  *
19
19
  * - New entry: added to registry, fresh pending queue entry created.
20
20
  * - Existing, version increased: registry updated, queue reset to pending.
21
- * Existing blob remains accessible until the new download completes.
21
+ * Existing array buffer remains accessible until the new download completes.
22
22
  * - Existing, version unchanged or lower: no-op.
23
23
  *
24
24
  * @param {object} entry
@@ -67,7 +67,7 @@ export function view(): Promise<{
67
67
  export function getStatus(id: string): Promise<object | null>;
68
68
  /**
69
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.
70
+ * An expired file still has a valid array buffer — it is simply due for refresh.
71
71
  *
72
72
  * @param {string} id
73
73
  * @returns {Promise<boolean>}
@@ -1 +0,0 @@
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};