offline-data-manager 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # offline-data-manager
2
2
 
3
- A service-worker-friendly library for registering, downloading, and storing files offline using IndexedDB.
3
+ A service-worker-friendly library for registering, downloading, and storing files offline using IndexedDB. All files are stored as ArrayBuffers — the library has no knowledge of file contents. Parsing, decompression, and interpretation are the caller's responsibility.
4
4
 
5
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
6
 
@@ -25,6 +25,8 @@ A service-worker-friendly library for registering, downloading, and storing file
25
25
  > [!TIP]
26
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
27
 
28
+ ---
29
+
28
30
  ## Setup
29
31
 
30
32
  ### With NPM Modules
@@ -37,79 +39,81 @@ npm install offline-data-manager
37
39
 
38
40
  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
41
 
40
- ## Running the test harness
41
-
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://`).
43
-
44
- ```bash
45
- # From the offline-data-manager directory:
46
- npm run dev
47
- # Then open: http://localhost:3000/test/index.html
48
- ```
42
+ ---
49
43
 
50
- Or with any static server you prefer:
44
+ ## File structure
51
45
 
52
- ```bash
53
- npx serve . # then open http://localhost:3000/test/index.html
54
- python3 -m http.server 3000 # then open http://localhost:3000/test/index.html
55
46
  ```
56
- ---
47
+ src/
48
+ index.js — Public API (import from here)
49
+ db.js — IndexedDB setup and helpers
50
+ registry.js — registerFile, registerFiles, view, isReady, getStatus, TTL/expiry
51
+ downloader.js — Persistent download loop, chunked Range requests, retry, connectivity
52
+ deleter.js — deleteFile, deleteAllFiles
53
+ events.js — Lightweight event emitter
54
+ storage.js — Storage quota utilities
55
+ connectivity.js — Online/offline monitoring
57
56
 
58
- ## How to handle retrieved data
57
+ test/
58
+ index.html — Interactive test harness (imports from ../src/index.js)
59
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.
60
+ dist/
61
+ offline-data-manager.esm.js — ES module bundle (Vite, Webpack 5+)
62
+ offline-data-manager.cjs — CommonJS bundle (Node.js require())
63
+ offline-data-manager.umd.js — UMD bundle (browser <script> tag, AMD)
64
+ offline-data-manager.umd.min.js — Minified UMD bundle
61
65
 
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
- }
66
+ build.mjs — Zero-dependency build script (node build.mjs)
94
67
  ```
95
68
 
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.
69
+ ---
98
70
 
99
- ## File structure
71
+ ## Quick start
100
72
 
101
- ```
102
- src/
103
- index.js — Public API (import from here)
104
- db.js — IndexedDB setup and helpers
105
- registry.js — registerFile, registerFiles, view, isReady, getStatus, TTL/expiry
106
- downloader.js — downloadFiles, chunked Range requests, retry, resume, expiry evaluation
107
- deleter.js — deleteFile, deleteAllFiles
108
- events.js — Lightweight event emitter
109
- storage.js — Storage quota utilities
110
-
111
- test/
112
- index.html — Interactive test harness (imports from ../src/index.js)
73
+ ```js
74
+ import ODM from './src/index.js';
75
+
76
+ // 1. Register files
77
+ await ODM.registerFiles([
78
+ {
79
+ id: 'base-map',
80
+ downloadUrl: 'https://example.com/map.pmtiles',
81
+ mimeType: 'application/vnd.pmtiles',
82
+ version: 1,
83
+ protected: true,
84
+ priority: 1,
85
+ ttl: 86400,
86
+ },
87
+ {
88
+ id: 'poi-data',
89
+ downloadUrl: 'https://example.com/poi.json',
90
+ // mimeType omitted — inferred from Content-Type response header
91
+ version: 1,
92
+ priority: 5,
93
+ },
94
+ ]);
95
+
96
+ // 2. Subscribe to events
97
+ ODM.on('progress', ({ id, percent }) => console.log(`${id}: ${percent}%`));
98
+ ODM.on('complete', ({ id, mimeType }) => console.log(`${id} ready (${mimeType})`));
99
+ ODM.on('error', ({ id, error, willRetry }) => console.error(id, error));
100
+ ODM.on('connectivity', ({ online }) => console.log(online ? 'back online' : 'offline'));
101
+
102
+ // 3. Start connectivity monitoring
103
+ ODM.startMonitoring();
104
+
105
+ // 4. Start the persistent download loop — call once at startup
106
+ ODM.startDownloads({ concurrency: 2 });
107
+
108
+ // Later: register a new file — the loop picks it up automatically, no extra call needed
109
+ await ODM.registerFile({ id: 'new-layer', downloadUrl: '...', version: 1 });
110
+
111
+ // Retrieve stored data
112
+ const { data, mimeType } = await ODM.retrieve('poi-data');
113
+ const json = JSON.parse(new TextDecoder().decode(data));
114
+
115
+ // Pass to a library that accepts ArrayBuffers (e.g. PMTiles)
116
+ const { data: mapBuffer } = await ODM.retrieve('base-map');
113
117
  ```
114
118
 
115
119
  ---
@@ -120,8 +124,8 @@ test/
120
124
  {
121
125
  id: string, // required — unique identifier
122
126
  downloadUrl: string, // required — URL to fetch
123
- version: number, // required — non-negative integer; triggers re-download when increased
124
127
  mimeType: string|null, // optional — inferred from Content-Type header if omitted
128
+ version: number, // required — non-negative integer; triggers re-download when increased
125
129
  protected: boolean, // default false — registry survives deletion; data re-downloaded
126
130
  priority: number, // default 10 — lower number = higher priority
127
131
  ttl: number, // seconds; 0 or omitted = never expires
@@ -131,21 +135,18 @@ test/
131
135
  ```
132
136
 
133
137
  ### `version`
134
-
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.
138
+ When `registerFiles()` is called with a higher version, the queue resets to `pending` but the existing ArrayBuffer stays in IDB and remains accessible via `retrieve()` until the new download completes and overwrites it.
136
139
 
137
140
  ### `protected`
138
-
139
141
  | Value | On delete | Registry |
140
142
  |---|---|---|
141
- | `true` | File data cleared, queue reset to `pending` | **Survives** — re-downloaded on next `downloadFiles()` |
143
+ | `true` | Data cleared, queue reset to `pending` | **Survives** — re-downloaded on next drain cycle |
142
144
  | `false` | Fully removed | **Removed** |
143
145
 
144
146
  Pass `{ removeRegistry: true }` to `delete()` to force full removal of a protected entry.
145
147
 
146
148
  ### `ttl`
147
-
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`.
149
+ Time-to-live in seconds. On each drain cycle, entries whose `completedAt + ttl` has elapsed are flipped to `expired` and queued for re-download. The existing ArrayBuffer remains accessible throughout — there is no gap in availability. On completion the TTL clock resets from the new `completedAt`.
149
150
 
150
151
  ---
151
152
 
@@ -155,161 +156,229 @@ Time-to-live in seconds. On each `downloadFiles()` call, entries whose `complete
155
156
  |---|---|
156
157
  | `pending` | Queued, not yet started |
157
158
  | `in-progress` | Actively downloading |
158
- | `paused` | Aborted mid-flight; resumes on next `downloadFiles()` |
159
- | `complete` | File data stored and fresh |
160
- | `expired` | File data stored but TTL has elapsed; still accessible, re-download queued |
161
- | `failed` | Exhausted all retries |
162
- | `deferred` | Skipped due to insufficient storage; retried next run |
159
+ | `paused` | Aborted mid-flight; loop resumes it on next drain cycle |
160
+ | `complete` | ArrayBuffer stored and fresh |
161
+ | `expired` | ArrayBuffer stored but TTL has elapsed; still accessible, re-download queued |
162
+ | `failed` | Exhausted all retries; call `retryFailed()` to re-queue |
163
+ | `deferred` | Skipped due to insufficient storage; retried next drain cycle |
163
164
 
164
165
  ---
165
166
 
166
167
  ## API
167
168
 
168
- ### `setDBInfo(dbName, dbVersion)`
169
+ ### Registration
169
170
 
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
+ #### `registerFile(entry)`
172
+ Registers a single file. No-op if the version hasn't strictly increased. Wakes the download loop immediately if it's running.
171
173
 
174
+ #### `registerFiles(entries)`
175
+ Registers an array of files and removes any non-protected entries absent from the list.
172
176
  ```js
173
- //Set the db name and version.
174
- offlineDataManager.setDBInfo('my-offline-db', 2);
177
+ const { registered, removed } = await ODM.registerFiles([...]);
178
+ ```
175
179
 
176
- //Set the db version online. Database name will continue to be 'offline-data-manager'.
177
- offlineDataManager.setDBInfo(null, 2);
180
+ ---
181
+
182
+ ### Download loop
183
+
184
+ #### `startDownloads(options?)`
185
+ Starts the persistent download loop. Idempotent — safe to call multiple times.
186
+
187
+ ```js
188
+ ODM.startDownloads({ concurrency: 2 });
178
189
  ```
179
190
 
180
- ### `dbGetAllIds(storeName)`
191
+ The loop:
192
+ 1. Evaluates TTL expiry, then downloads all pending / paused / deferred / expired entries up to `concurrency` in parallel.
193
+ 2. Waits — without polling — for new work to arrive.
194
+ 3. Wakes automatically when `registerFile()` adds a new or updated entry, when the browser comes back online, or when `retryFailed()` is called.
195
+ 4. Exits cleanly when `stopDownloads()` is called.
181
196
 
182
- Retrieves an array of all unique IDs in either of thetwo underlying data stores. Valid store names: `registry` and `downloadQueue`.
197
+ Because registering a file wakes the loop, there is no need to call `startDownloads()` again after registering new files at runtime.
183
198
 
184
- ### `registerFile(entry)`
199
+ #### `stopDownloads()`
200
+ Stops the loop gracefully. In-flight downloads are aborted and set to `paused`. Call `startDownloads()` again to resume.
185
201
 
186
- Registers a single file. No-op if version hasn't strictly increased.
202
+ ```js
203
+ await ODM.stopDownloads();
204
+ ```
187
205
 
188
- ### `registerFiles(entries)`
206
+ #### `retryFailed()`
207
+ Re-queues all `failed` entries and wakes the loop to retry them. `failed` is terminal by design — broken URLs won't loop forever.
208
+
209
+ ```js
210
+ await ODM.retryFailed();
211
+ ```
189
212
 
190
- Registers an array of files. Removes non-protected entries absent from the list.
213
+ #### `isDownloading()`
214
+ Returns `true` if the loop is currently running.
191
215
 
192
216
  ```js
193
- const { registered, removed } = await offlineDataManager.registerFiles([...]);
217
+ ODM.isDownloading(); // → boolean
194
218
  ```
195
219
 
196
- ### `downloadFiles(options?)`
220
+ ---
221
+
222
+ ### Abort
223
+
224
+ #### `abortDownload(id)`
225
+ Aborts a single active download, setting it to `paused`. The loop picks it up again on the next drain cycle.
226
+
227
+ #### `abortAllDownloads()`
228
+ Aborts all active downloads.
197
229
 
198
- 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.
230
+ ---
231
+
232
+ ### Connectivity monitoring
233
+
234
+ #### `startMonitoring()` / `stopMonitoring()`
235
+ Monitors `window` online/offline events.
236
+
237
+ - Going **offline**: immediately pauses all active downloads (avoids burning retry attempts).
238
+ - Coming back **online**: wakes the download loop to resume automatically.
199
239
 
200
240
  ```js
201
- await offlineDataManager.downloadFiles({ concurrency: 2, resumeOnly: false, retryFailed: false });
241
+ ODM.startMonitoring(); // call once, typically at startup before startDownloads()
242
+ ODM.stopMonitoring(); // remove listeners (e.g. in tests)
202
243
  ```
203
244
 
204
- - `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.
245
+ Emits a `'connectivity'` event `{ online: boolean }` on every change. `navigator.onLine` can return `true` on captive portals actual server reachability is confirmed by whether the subsequent fetch succeeds, with failed fetches retrying normally with backoff.
205
246
 
206
- ### `retrieve(id)`
247
+ #### `isOnline()` / `isMonitoring()`
248
+ ```js
249
+ ODM.isOnline(); // → boolean (navigator.onLine)
250
+ ODM.isMonitoring(); // → boolean
251
+ ```
252
+
253
+ ---
207
254
 
208
- Returns the stored file information `{ data: ArrayBuffer, mimeType: string }`. Works for both `complete` and `expired` entries.
255
+ ### Retrieve
256
+
257
+ #### `retrieve(id)`
258
+ Returns the stored ArrayBuffer and resolved MIME type for a completed or expired file.
209
259
 
210
260
  ```js
211
- const fileInfo = await offlineDataManager.retrieve('json-data');
261
+ const { data, mimeType } = await ODM.retrieve('poi-data');
262
+
263
+ // Text / JSON
264
+ const text = new TextDecoder().decode(data);
265
+ const json = JSON.parse(text);
212
266
 
213
- const decoder = new TextDecoder("utf-8");
214
- const text = decoder.decode(fileInfo.data);
267
+ // Binary (e.g. PMTiles, zip)
268
+ const { data: mapBuffer } = await ODM.retrieve('base-map');
269
+ // pass mapBuffer to PMTiles, JSZip, etc.
215
270
  ```
216
271
 
217
- ### `view()`
272
+ Returns data for both `complete` and `expired` entries — expiry only means a refresh is queued, not that the data is gone. Throws if the file is not registered or has no data yet.
218
273
 
219
- Returns all entries merged with queue state, plus storage summary.
274
+ ---
275
+
276
+ ### Status
220
277
 
278
+ #### `getAllStatus()`
279
+ Returns all entries merged with queue state, plus a storage summary.
221
280
  ```js
222
- const { items, storage } = await offlineDataManager.view();
281
+ const { items, storage } = await ODM.getAllStatus();
223
282
  // items[n]: { id, mimeType, version, downloadStatus, storedBytes,
224
283
  // bytesDownloaded, progress, completedAt, expiresAt, ... }
225
284
  // storage: { usageBytes, quotaBytes, availableBytes, ...Formatted }
226
285
  ```
227
286
 
228
- ### `getStatus(id)`
229
-
287
+ #### `getStatus(id)`
230
288
  Full merged status for one file, or `null` if not registered.
231
289
 
232
- ### `isReady(id)`
290
+ #### `isReady(id)`
291
+ Returns `true` if the file has data available (`complete` or `expired`).
233
292
 
234
- Returns `true` if the file has a data available (`complete` or `expired`).
293
+ ---
235
294
 
236
- ### `delete(id, options?)`
295
+ ### Delete
237
296
 
297
+ #### `delete(id, options?)`
238
298
  ```js
239
- await offlineDataManager.delete('poi-data'); // respects protected flag
240
- await offlineDataManager.delete('base-map', { removeRegistry: true }); // force full removal
299
+ await ODM.delete('poi-data'); // respects protected flag
300
+ await ODM.delete('base-map', { removeRegistry: true }); // force full removal
241
301
  ```
242
302
 
243
- ### `deleteAll(options?)`
244
-
303
+ #### `deleteAll(options?)`
245
304
  ```js
246
- await offlineDataManager.deleteAll();
247
- await offlineDataManager.deleteAll({ removeRegistry: true });
305
+ await ODM.deleteAll();
306
+ await ODM.deleteAll({ removeRegistry: true });
248
307
  ```
249
308
 
250
- ### `startMonitoring()` / `stopMonitoring()`
309
+ ---
251
310
 
252
- 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.
311
+ ### Events
253
312
 
254
313
  ```js
255
- offlineDataManager.startMonitoring(); // call once after first downloadFiles()
256
- offlineDataManager.stopMonitoring(); // remove listeners (e.g. in tests)
314
+ const unsub = ODM.on('progress', ({ id, bytesDownloaded, totalBytes, percent }) => {});
315
+ ODM.on('complete', ({ id, mimeType }) => {});
316
+ ODM.on('expired', ({ id }) => {});
317
+ ODM.on('error', ({ id, error, retryCount, willRetry }) => {});
318
+ ODM.on('deferred', ({ id, reason }) => {});
319
+ ODM.on('registered', ({ id, reason }) => {}); // reason: 'new' | 'version-updated'
320
+ ODM.on('deleted', ({ id, registryRemoved }) => {});
321
+ ODM.on('status', ({ id, status }) => {});
322
+ ODM.on('stopped', ({}) => {}); // emitted when stopDownloads() completes
323
+ ODM.on('connectivity', ({ online }) => {});
324
+
325
+ ODM.once('complete', ({ id }) => {});
326
+ unsub(); // remove listener
257
327
  ```
258
328
 
259
- 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.
329
+ ---
260
330
 
261
- ### `isOnline()` / `isMonitoring()`
331
+ ### Storage
262
332
 
263
333
  ```js
264
- offlineDataManager.isOnline(); // boolean (navigator.onLine)
265
- offlineDataManager.isMonitoring(); // → boolean
334
+ const { usage, quota, available } = await ODM.getStorageEstimate();
335
+ await ODM.requestPersistentStorage();
336
+ await ODM.isPersistentStorage();
266
337
  ```
267
338
 
339
+ ---
268
340
 
269
- Sets status to `paused`; resumes on next `downloadFiles()`.
270
-
271
- ### `resumeInterruptedDownloads()`
341
+ ### Service worker
272
342
 
273
- Resumes `paused` and `in-progress` entries. Call in a service worker `activate` event:
343
+ The download loop starts fresh on each page load. Call `startDownloads()` in a service worker `activate` event to resume any downloads interrupted by a previous SW close — pending and paused entries are picked up automatically on the first drain cycle.
274
344
 
275
345
  ```js
276
346
  self.addEventListener('activate', (event) => {
277
- event.waitUntil(offlineDataManager.resumeInterruptedDownloads());
347
+ event.waitUntil(
348
+ (async () => {
349
+ ODM.startMonitoring();
350
+ ODM.startDownloads({ concurrency: 2 });
351
+ })()
352
+ );
278
353
  });
279
354
  ```
280
355
 
281
- ### Events
356
+ ---
282
357
 
283
- ```js
284
- const unsub = offlineDataManager.on('progress', ({ id, bytesDownloaded, totalBytes, percent }) => {});
285
- offlineDataManager.on('complete', ({ id }) => {});
286
- offlineDataManager.on('expired', ({ id }) => {});
287
- offlineDataManager.on('error', ({ id, error, retryCount, willRetry }) => {});
288
- offlineDataManager.on('deferred', ({ id, reason }) => {});
289
- offlineDataManager.on('registered', ({ id, reason }) => {}); // reason: 'new' | 'version-updated'
290
- offlineDataManager.on('deleted', ({ id, registryRemoved }) => {});
291
- offlineDataManager.on('status', ({ id, status }) => {});
292
-
293
- offlineDataManager.once('complete', ({ id }) => {});
294
- unsub(); // remove listener
358
+ ## Building for distribution
359
+
360
+ ```bash
361
+ node build.mjs # produces dist/ (ESM, CJS, UMD, UMD minified)
362
+ node build.mjs --watch # rebuild on src changes
363
+ node build.mjs --no-min # skip minification
295
364
  ```
296
365
 
297
- ### Storage
366
+ The build script is zero-dependency — pure Node.js 18+, no Rollup or Webpack required.
298
367
 
299
- ```js
300
- const { usage, quota, available } = await offlineDataManager.getStorageEstimate();
301
- await offlineDataManager.requestPersistentStorage();
302
- await offlineDataManager.isPersistentStorage();
303
- ```
368
+ | Output | For |
369
+ |---|---|
370
+ | `dist/offline-data-manager.esm.js` | Vite, Webpack 5+, native `<script type=module>` |
371
+ | `dist/offline-data-manager.cjs` | Node.js `require()`, older toolchains |
372
+ | `dist/offline-data-manager.umd.js` | `<script>` tag → `window.OfflineDataManager`, AMD |
373
+ | `dist/offline-data-manager.umd.min.js` | Production `<script>` tag |
304
374
 
305
375
  ---
306
376
 
307
377
  ## Notes
308
378
 
309
- - **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()`.
310
- - **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.
311
- - **Chunking threshold** — files over 5 MB are chunked in 2 MB Range requests. Both constants are in `downloader.js`.
379
+ - **ArrayBuffer storage** — all file data is stored as a raw ArrayBuffer on the queue record alongside its resolved `mimeType`. There is only one IDB store to manage; no separate data store.
380
+ - **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 with the ArrayBuffer and returned by `retrieve()`, `getAllStatus()`, and `getStatus()`.
381
+ - **Persistent loop vs one-shot** — the loop waits on a Promise that resolves only when `registerFile()` or the connectivity monitor calls an internal wake function. There is no polling between drain cycles.
382
+ - **Chunking threshold** — files over 5 MB are downloaded in 2 MB Range request chunks. Both constants are in `downloader.js`.
312
383
  - **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.
313
- - **Storage safety margin** — 10% of quota is reserved before deferring downloads. Configurable in `storage.js`.
314
- - **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.
315
- - **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.
384
+ - **Storage safety margin** — 10% of quota is reserved before deferring downloads. Configurable in `storage.js`.
@@ -1 +1 @@
1
- var H="offline-data-manager",X=1,L=null,s={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function K(e,t){H=e??"offline-data-manager",X=t??1}async function x(){return L||(L=await new Promise((e,t)=>{let o=indexedDB.open(H,X);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 N(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 C(e,t){return U.has(e)||U.set(e,new Set),U.get(e).add(t),()=>P(e,t)}function P(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),P(e,o)};C(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 _(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"},Q=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 F(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 F(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:_(o.usage),quotaFormatted:_(o.quota),availableFormatted:_(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 Q.has(t?.status)}var q=null,W=null,M=!1;function ie(){f("connectivity",{online:!1}),q?.()}function le(){f("connectivity",{online:!0}),W?.()}function ue({pauseAll:e,resumeAll:t}){M||(q=e,W=t,window.addEventListener("offline",ie),window.addEventListener("online",le),M=!0)}function $(){window.removeEventListener("offline",ie),window.removeEventListener("online",le),q=null,W=null,M=!1}function I(){return navigator.onLine??!0}function Y(){return M}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,j=Date.now(),ge=te(j,a);await y(t,{status:l.COMPLETE,data:O,bytesDownloaded:u.byteLength,byteOffset:u.byteLength,completedAt:j,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 G({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 B(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 G({resumeOnly:!0})}function Ee(){ue({pauseAll:v,resumeAll:()=>G(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 k(e,{removeRegistry:t=!1}={}){let o=await w(s.REGISTRY,e);if(!o)throw new Error(`deleteFile: No registered file with id "${e}".`);await B(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 N(s.REGISTRY);return Promise.all(t.map(o=>k(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(!Q.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:K,dbGetAllIds:N,registerFile:F,registerFiles:oe,downloadFiles:G,abortDownload:B,abortAllDownloads:v,resumeInterruptedDownloads:we,startMonitoring:Ee,stopMonitoring:$,isOnline:I,isMonitoring:Y,retrieve:Le,view:ne,getStatus:ae,isReady:se,delete:k,deleteAll:ye,on:C,off:P,once:z,getStorageEstimate:T,requestPersistentStorage:Z,isPersistentStorage:J},lt=Ne;export{v as abortAllDownloads,B as abortDownload,lt as default,ye as deleteAllFiles,k as deleteFile,G as downloadFiles,f as emit,ae as getStatus,T as getStorageEstimate,Y as isMonitoring,I as isOnline,J as isPersistentStorage,se as isReady,P as off,C as on,z as once,F as registerFile,oe as registerFiles,Z as requestPersistentStorage,we as resumeInterruptedDownloads,Le as retrieve,Ee as startMonitoring,$ as stopMonitoring,ne as view};
1
+ var K="offline-data-manager",z=1,L=null,i={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function V(t,e){K=t??"offline-data-manager",z=e??1}async function U(){return L||(L=await new Promise((t,e)=>{let o=indexedDB.open(K,z);o.onupgradeneeded=a=>{let r=a.target.result;if(!r.objectStoreNames.contains(i.REGISTRY)){let n=r.createObjectStore(i.REGISTRY,{keyPath:"id"});n.createIndex("protected","protected",{unique:!1}),n.createIndex("priority","priority",{unique:!1})}if(!r.objectStoreNames.contains(i.DOWNLOAD_QUEUE)){let n=r.createObjectStore(i.DOWNLOAD_QUEUE,{keyPath:"id"});n.createIndex("status","status",{unique:!1}),n.createIndex("priority","priority",{unique:!1})}},o.onsuccess=()=>t(o.result),o.onerror=()=>e(o.error)}),L)}async function p(t,e){let o=await U();return new Promise((a,r)=>{let n=o.transaction(t,"readonly").objectStore(t).get(e);n.onsuccess=()=>a(n.result),n.onerror=()=>r(n.error)})}async function y(t){let e=await U();return new Promise((o,a)=>{let r=e.transaction(t,"readonly").objectStore(t).getAll();r.onsuccess=()=>o(r.result),r.onerror=()=>a(r.error)})}async function N(t){let e=await U();return new Promise((o,a)=>{let r=e.transaction(t,"readonly").objectStore(t).getAllKeys();r.onsuccess=()=>o(r.result),r.onerror=()=>a(r.error)})}async function g(t,e){let o=await U();return new Promise((a,r)=>{let n=o.transaction(t,"readwrite").objectStore(t).put(e);n.onsuccess=()=>a(),n.onerror=()=>r(n.error)})}async function S(t,e){let o=await U();return new Promise((a,r)=>{let n=o.transaction(t,"readwrite").objectStore(t).delete(e);n.onsuccess=()=>a(),n.onerror=()=>r(n.error)})}var T=new Map;function Q(t,e){return T.has(t)||T.set(t,new Set),T.get(t).add(e),()=>_(t,e)}function _(t,e){T.get(t)?.delete(e)}function d(t,e){T.get(t)?.forEach(o=>{try{o(e)}catch(a){console.error(`[offline-data-manager] Error in "${t}" listener:`,a)}})}function Z(t,e){let o=a=>{e(a),_(t,o)};Q(t,o)}async function I(){if(!navigator?.storage?.estimate)return{usage:0,quota:1/0,available:1/0};let{usage:t=0,quota:e=1/0}=await navigator.storage.estimate();return{usage:t,quota:e,available:e-t}}async function J(t){let{available:e,quota:o}=await I();return e-o*.1>=t}async function tt(){return navigator?.storage?.persist?navigator.storage.persist():!1}async function et(){return navigator?.storage?.persisted?navigator.storage.persisted():!1}function P(t){return t===1/0?"\u221E":t<1024?`${t} B`:t<1024**2?`${(t/1024).toFixed(1)} KB`:t<1024**3?`${(t/1024**2).toFixed(1)} MB`:`${(t/1024**3).toFixed(2)} GB`}var F=null,q=null,M=!1;function ot(){d("connectivity",{online:!1}),F?.()}function nt(){d("connectivity",{online:!0}),q?.()}function rt({pauseAll:t,resumeAll:e}){M||(F=t,q=e,window.addEventListener("offline",ot),window.addEventListener("online",nt),M=!0)}function W(){window.removeEventListener("offline",ot),window.removeEventListener("online",nt),F=null,q=null,M=!1}function v(){return navigator.onLine??!0}function $(){return M}var bt=2*1024*1024,St=5*1024*1024,Ot=2,at=5,ht=1e3,E=new Map,b=!1,G=null;function st(){return new Promise(t=>{G=t})}function O(){if(G){let t=G;G=null,t()}}var xt=t=>new Promise(e=>setTimeout(e,t)),Ut=t=>ht*Math.pow(2,t);async function m(t,e){let o=await p(i.DOWNLOAD_QUEUE,t);o&&await g(i.DOWNLOAD_QUEUE,{...o,...e})}async function Tt(t,e){try{let o=await fetch(t,{method:"HEAD",signal:e}),a=o.headers.get("Accept-Ranges")==="bytes",r=o.headers.get("Content-Encoding"),n=!!r&&r!=="identity",s=o.headers.get("Content-Length"),l=s&&!n?parseInt(s,10):null,c=it(o.headers.get("Content-Type"));return{supportsRange:a,totalBytes:l,mimeType:c}}catch{return{supportsRange:!1,totalBytes:null,mimeType:null}}}function it(t){return t&&t.split(";")[0].trim()||null}function lt(t){let e=t.reduce((r,n)=>r+n.byteLength,0),o=new Uint8Array(e),a=0;for(let r of t)o.set(r,a),a+=r.byteLength;return o}async function It(t){let{id:e,downloadUrl:o,ttl:a}=t,r=new AbortController;E.set(e,r);let n=await p(i.DOWNLOAD_QUEUE,e),s=n?.retryCount??0;for(;s<=at;)try{await m(e,{status:u.IN_PROGRESS,lastAttemptAt:Date.now(),retryCount:s,errorMessage:null}),d("status",{id:e,status:u.IN_PROGRESS}),n=await p(i.DOWNLOAD_QUEUE,e);let l=n?.byteOffset??0,c=!1,f=n?.totalBytes??t.totalBytes??null,w=t.mimeType??null;if(l===0){let R=await Tt(o,r.signal);c=R.supportsRange,R.totalBytes&&(f=R.totalBytes,await m(e,{totalBytes:f})),!w&&R.mimeType&&(w=R.mimeType)}else c=!0;let D=c&&f&&f>St,A,x=null;if(D)A=await Lt(e,o,l,f,r.signal);else{let R=await vt(e,o,r.signal);A=R.uint8,x=R.mimeType}let H=w??x??"application/octet-stream",C=A.buffer,X=Date.now(),Rt=wt(X,a);await m(e,{status:u.COMPLETE,data:C,mimeType:H,bytesDownloaded:C.byteLength,byteOffset:C.byteLength,completedAt:X,expiresAt:Rt,errorMessage:null,deferredReason:null}),d("complete",{id:e,mimeType:H}),E.delete(e);return}catch(l){if(l.name==="AbortError"){await m(e,{status:u.PAUSED}),d("status",{id:e,status:u.PAUSED}),E.delete(e);return}if(s++,s>at){await m(e,{status:u.FAILED,retryCount:s,errorMessage:l.message}),d("error",{id:e,error:l,retryCount:s}),E.delete(e);return}let c=Ut(s-1);console.warn(`[offline-data-manager] "${e}" failed (attempt ${s}), retrying in ${c}ms:`,l.message),d("error",{id:e,error:l,retryCount:s,willRetry:!0}),await m(e,{status:u.PENDING,retryCount:s,errorMessage:l.message}),await xt(c)}}async function vt(t,e,o){let a=await fetch(e,{signal:o});if(!a.ok)throw new Error(`HTTP ${a.status} ${a.statusText}`);let r=a.headers.get("Content-Encoding"),n=!!r&&r!=="identity",s=a.headers.get("Content-Length"),l=s&&!n?parseInt(s,10):null,c=it(a.headers.get("Content-Type")),f=a.body.getReader(),w=[],D=0;for(;;){let{done:A,value:x}=await f.read();if(A)break;w.push(x),D+=x.byteLength,await m(t,{bytesDownloaded:D,totalBytes:l}),d("progress",{id:t,bytesDownloaded:D,totalBytes:l,percent:l?Math.round(D/l*100):null})}return{uint8:lt(w),mimeType:c}}async function Lt(t,e,o,a,r){let n=o,s=[],l=o;for(;n<a;){let c=Math.min(n+bt-1,a-1),f=await fetch(e,{signal:r,headers:{Range:`bytes=${n}-${c}`}});if(!f.ok&&f.status!==206)throw new Error(`HTTP ${f.status} on Range bytes=${n}-${c}`);let w=new Uint8Array(await f.arrayBuffer());s.push(w),n+=w.byteLength,l+=w.byteLength,await m(t,{bytesDownloaded:l,byteOffset:n}),d("progress",{id:t,bytesDownloaded:l,totalBytes:a,percent:Math.round(l/a*100)})}return lt(s)}async function Nt(t){await mt();let[e,o]=await Promise.all([y(i.REGISTRY),y(i.DOWNLOAD_QUEUE)]),a=new Map(e.map(l=>[l.id,l])),r=o.filter(l=>[u.PENDING,u.IN_PROGRESS,u.PAUSED,u.DEFERRED,u.EXPIRED].includes(l.status)).sort((l,c)=>(a.get(l.id)?.priority??10)-(a.get(c.id)?.priority??10));if(r.length===0)return;let n=[...r],s=new Set;await new Promise(l=>{function c(){if(!b){l();return}if(n.length===0){s.size===0&&l();return}if(s.size>=t)return;let f=n.shift(),w=a.get(f.id);if(!w){c();return}let D=(async()=>{let A=w.totalBytes??f.totalBytes??0;if(A>0&&!await J(A)){await m(f.id,{status:u.DEFERRED,deferredReason:"insufficient-storage"}),d("deferred",{id:f.id,reason:"insufficient-storage"});return}await It(w)})().finally(()=>{s.delete(D),c()});s.add(D),c()}c()})}function ut({concurrency:t=Ot}={}){b||(b=!0,(async()=>{for(;b;){if(!v()){let e=await y(i.DOWNLOAD_QUEUE);for(let o of e)o.status===u.IN_PROGRESS&&(E.get(o.id)?.abort(),E.delete(o.id),await m(o.id,{status:u.PAUSED,deferredReason:"network-offline"}));d("connectivity",{online:!1}),await st();continue}await Nt(t),b&&await st()}})())}async function dt(){b=!1,O(),await h(),d("stopped",{})}async function ct(){let t=await y(i.DOWNLOAD_QUEUE);for(let e of t)e.status===u.FAILED&&await m(e.id,{status:u.PENDING,retryCount:0,errorMessage:null});O()}function ft(){return b}async function B(t){E.get(t)?.abort(),E.delete(t)}async function h(){for(let[t,e]of E)e.abort(),E.delete(t)}function pt(){rt({pauseAll:h,resumeAll:O})}var u={PENDING:"pending",IN_PROGRESS:"in-progress",PAUSED:"paused",COMPLETE:"complete",EXPIRED:"expired",FAILED:"failed",DEFERRED:"deferred"},k=new Set([u.COMPLETE,u.EXPIRED]);function _t(t){if(!t||typeof t!="object")throw new Error("Registry entry must be an object.");if(!t.id||typeof t.id!="string")throw new Error('Registry entry must have a string "id".');if(!t.downloadUrl||typeof t.downloadUrl!="string")throw new Error(`Entry "${t.id}" must have a string "downloadUrl".`);if(t.mimeType!==void 0&&t.mimeType!==null&&typeof t.mimeType!="string")throw new Error(`Entry "${t.id}" mimeType must be a string or omitted.`);if(typeof t.version!="number"||!Number.isInteger(t.version)||t.version<0)throw new Error(`Entry "${t.id}" version must be a non-negative integer.`);if(t.ttl!==void 0&&(typeof t.ttl!="number"||t.ttl<0))throw new Error(`Entry "${t.id}" ttl must be a non-negative number (seconds).`)}function yt(t){return{id:t,status:u.PENDING,data:null,bytesDownloaded:0,totalBytes:null,byteOffset:0,retryCount:0,lastAttemptAt:null,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}}function wt(t,e){return e?t+e*1e3:null}function Pt(t){return t?Date.now()>=t:!1}async function Y(t){_t(t);let e=Date.now(),o=await p(i.REGISTRY,t.id),a=await p(i.DOWNLOAD_QUEUE,t.id),r={id:t.id,downloadUrl:t.downloadUrl,mimeType:t.mimeType??null,version:t.version,protected:t.protected??!1,priority:t.priority??10,ttl:t.ttl??0,totalBytes:t.totalBytes??null,metadata:t.metadata??{},registeredAt:o?.registeredAt??e,updatedAt:e};if(o){if(t.version>o.version){await g(i.REGISTRY,r);let n=a?{...a,status:u.PENDING,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}:yt(t.id);await g(i.DOWNLOAD_QUEUE,n),d("registered",{id:t.id,reason:"version-updated"}),O()}return}await g(i.REGISTRY,r),await g(i.DOWNLOAD_QUEUE,yt(t.id)),d("registered",{id:t.id,reason:"new"}),O()}async function gt(t){if(!Array.isArray(t))throw new Error("registerFiles expects an array.");let e=new Set(t.map(r=>r.id)),o=await y(i.REGISTRY),a=[];for(let r of o)!e.has(r.id)&&!r.protected&&(await S(i.REGISTRY,r.id),await S(i.DOWNLOAD_QUEUE,r.id),a.push(r.id),d("deleted",{id:r.id,registryRemoved:!0}));for(let r of t)await Y(r);return{registered:t.map(r=>r.id),removed:a}}async function mt(){let t=await y(i.DOWNLOAD_QUEUE),e=[];for(let o of t)o.status===u.COMPLETE&&Pt(o.expiresAt)&&(await g(i.DOWNLOAD_QUEUE,{...o,status:u.EXPIRED}),e.push(o.id),d("expired",{id:o.id}));return e}async function Mt(){let[t,e,o]=await Promise.all([y(i.REGISTRY),y(i.DOWNLOAD_QUEUE),I()]),a=new Map(e.map(n=>[n.id,n]));return{items:t.map(n=>{let s=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:s?.status??null,bytesDownloaded:s?.bytesDownloaded??0,storedBytes:s?.data?.length??null,progress:s?.totalBytes&&s?.bytesDownloaded?Math.round(s.bytesDownloaded/s.totalBytes*100):null,retryCount:s?.retryCount??0,lastAttemptAt:s?.lastAttemptAt??null,errorMessage:s?.errorMessage??null,deferredReason:s?.deferredReason??null,completedAt:s?.completedAt??null,expiresAt:s?.expiresAt??null}}).sort((n,s)=>n.priority-s.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 Et(t){let[e,o]=await Promise.all([p(i.REGISTRY,t),p(i.DOWNLOAD_QUEUE,t)]);return e?{id:e.id,downloadUrl:e.downloadUrl,mimeType:e.mimeType??null,version:e.version,protected:e.protected,priority:e.priority,ttl:e.ttl,totalBytes:e.totalBytes,metadata:e.metadata,registeredAt:e.registeredAt,updatedAt:e.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 Dt(t){let e=await p(i.DOWNLOAD_QUEUE,t);return k.has(e?.status)}async function Gt(t){let e=await p(i.DOWNLOAD_QUEUE,t);e&&await g(i.DOWNLOAD_QUEUE,{...e,status:u.PENDING,data:null,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null})}async function j(t,{removeRegistry:e=!1}={}){let o=await p(i.REGISTRY,t);if(!o)throw new Error(`deleteFile: No registered file with id "${t}".`);await B(t);let a=e||!o.protected;return a?(await S(i.REGISTRY,t),await S(i.DOWNLOAD_QUEUE,t)):await Gt(t),d("deleted",{id:t,registryRemoved:a}),{id:t,registryRemoved:a}}async function At({removeRegistry:t=!1}={}){await h();let e=await N(i.REGISTRY);return Promise.all(e.map(o=>j(o,{removeRegistry:t})))}async function Bt(t){let[e,o]=await Promise.all([p(i.REGISTRY,t),p(i.DOWNLOAD_QUEUE,t)]);if(!e)throw new Error(`retrieve: No registered file with id "${t}".`);if(!k.has(o?.status)||!o?.data)throw new Error(`retrieve: File "${t}" has no data yet (status: ${o?.status??"unknown"}).`);return{data:o.data,mimeType:o.mimeType}}var Ct={setDBInfo:V,dbGetAllIds:N,registerFile:Y,registerFiles:gt,startDownloads:ut,stopDownloads:dt,retryFailed:ct,isDownloading:ft,abortDownload:B,abortAllDownloads:h,startMonitoring:pt,stopMonitoring:W,isOnline:v,isMonitoring:$,retrieve:Bt,view,getStatus:Et,isReady:Dt,delete:j,deleteAll:At,on:Q,off:_,once:Z,getStorageEstimate:I,requestPersistentStorage:tt,isPersistentStorage:et},me=Ct;export{h as abortAllDownloads,B as abortDownload,me as default,At as deleteAllFiles,j as deleteFile,d as emit,Mt as getAllStatus,Et as getStatus,I as getStorageEstimate,ft as isDownloading,$ as isMonitoring,v as isOnline,et as isPersistentStorage,Dt as isReady,_ as off,Q as on,Z as once,Y as registerFile,gt as registerFiles,tt as requestPersistentStorage,Bt as retrieve,ct as retryFailed,ut as startDownloads,pt as startMonitoring,dt as stopDownloads,W as stopMonitoring};
@@ -1 +1 @@
1
- "use strict";var offlineMapData=(()=>{var $=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Re=Object.getOwnPropertyNames;var Se=Object.prototype.hasOwnProperty;var Oe=(e,t)=>{for(var o in t)$(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&&$(e,r,{get:()=>t[r],enumerable:!(a=be(t,r))||a.enumerable});return e};var xe=e=>he($({},"__esModule",{value:!0}),e);var qe={};Oe(qe,{abortAllDownloads:()=>T,abortDownload:()=>P,default:()=>Fe,deleteAllFiles:()=>oe,deleteFile:()=>W,downloadFiles:()=>N,emit:()=>c,getStatus:()=>z,getStorageEstimate:()=>x,isMonitoring:()=>q,isOnline:()=>U,isPersistentStorage:()=>j,isReady:()=>V,off:()=>L,on:()=>G,once:()=>Y,registerFile:()=>C,registerFiles:()=>X,requestPersistentStorage:()=>k,resumeInterruptedDownloads:()=>ee,retrieve:()=>De,startMonitoring:()=>te,stopMonitoring:()=>F,view:()=>K});var ne="offline-data-manager",ae=1,_=null,s={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function se(e,t){ne=e??"offline-data-manager",ae=t??1}async function I(){return _||(_=await new Promise((e,t)=>{let o=indexedDB.open(ne,ae);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 M(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 G(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 Y(e,t){let o=a=>{t(a),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 ie(e){let{available:t,quota:o}=await x();return t-o*.1>=e}async function k(){return navigator?.storage?.persist?navigator.storage.persist():!1}async function j(){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 l={PENDING:"pending",IN_PROGRESS:"in-progress",PAUSED:"paused",COMPLETE:"complete",EXPIRED:"expired",FAILED:"failed",DEFERRED:"deferred"},H=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 C(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 X(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 C(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 K(){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:B(o.usage),quotaFormatted:B(o.quota),availableFormatted:B(o.available)}}}async function z(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 V(e){let t=await w(s.DOWNLOAD_QUEUE,e);return H.has(t?.status)}var Z=null,J=null,Q=!1;function ce(){c("connectivity",{online:!1}),Z?.()}function fe(){c("connectivity",{online:!0}),J?.()}function pe({pauseAll:e,resumeAll:t}){Q||(Z=e,J=t,window.addEventListener("offline",ce),window.addEventListener("online",fe),Q=!0)}function F(){window.removeEventListener("offline",ce),window.removeEventListener("online",fe),Z=null,J=null,Q=!1}function U(){return navigator.onLine??!0}function q(){return Q}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,re=Date.now(),Ae=ue(re,a);await y(t,{status:l.COMPLETE,data:O,bytesDownloaded:u.byteLength,byteOffset:u.byteLength,completedAt:re,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 ee(){await N({resumeOnly:!0})}function te(){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 W(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 oe({removeRegistry:e=!1}={}){await T();let t=await M(s.REGISTRY);return Promise.all(t.map(o=>W(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(!H.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:se,dbGetAllIds:M,registerFile:C,registerFiles:X,downloadFiles:N,abortDownload:P,abortAllDownloads:T,resumeInterruptedDownloads:ee,startMonitoring:te,stopMonitoring:F,isOnline:U,isMonitoring:q,retrieve:De,view:K,getStatus:z,isReady:V,delete:W,deleteAll:oe,on:G,off:L,once:Y,getStorageEstimate:x,requestPersistentStorage:k,isPersistentStorage:j},Fe=Qe;return xe(qe);})();
1
+ "use strict";var offlineMapData=(()=>{var k=Object.defineProperty;var ht=Object.getOwnPropertyDescriptor;var xt=Object.getOwnPropertyNames;var Ut=Object.prototype.hasOwnProperty;var Tt=(t,e)=>{for(var o in e)k(t,o,{get:e[o],enumerable:!0})},It=(t,e,o,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of xt(e))!Ut.call(t,n)&&n!==o&&k(t,n,{get:()=>e[n],enumerable:!(a=ht(e,n))||a.enumerable});return t};var vt=t=>It(k({},"__esModule",{value:!0}),t);var Ht={};Tt(Ht,{abortAllDownloads:()=>S,abortDownload:()=>N,default:()=>jt,deleteAllFiles:()=>at,deleteFile:()=>W,emit:()=>d,getAllStatus:()=>bt,getStatus:()=>nt,getStorageEstimate:()=>h,isDownloading:()=>J,isMonitoring:()=>Q,isOnline:()=>x,isPersistentStorage:()=>H,isReady:()=>rt,off:()=>L,on:()=>M,once:()=>Y,registerFile:()=>q,registerFiles:()=>ot,requestPersistentStorage:()=>j,retrieve:()=>St,retryFailed:()=>Z,startDownloads:()=>z,startMonitoring:()=>tt,stopDownloads:()=>V,stopMonitoring:()=>C});var lt="offline-data-manager",ut=1,_=null,i={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function dt(t,e){lt=t??"offline-data-manager",ut=e??1}async function I(){return _||(_=await new Promise((t,e)=>{let o=indexedDB.open(lt,ut);o.onupgradeneeded=a=>{let n=a.target.result;if(!n.objectStoreNames.contains(i.REGISTRY)){let r=n.createObjectStore(i.REGISTRY,{keyPath:"id"});r.createIndex("protected","protected",{unique:!1}),r.createIndex("priority","priority",{unique:!1})}if(!n.objectStoreNames.contains(i.DOWNLOAD_QUEUE)){let r=n.createObjectStore(i.DOWNLOAD_QUEUE,{keyPath:"id"});r.createIndex("status","status",{unique:!1}),r.createIndex("priority","priority",{unique:!1})}},o.onsuccess=()=>t(o.result),o.onerror=()=>e(o.error)}),_)}async function p(t,e){let o=await I();return new Promise((a,n)=>{let r=o.transaction(t,"readonly").objectStore(t).get(e);r.onsuccess=()=>a(r.result),r.onerror=()=>n(r.error)})}async function y(t){let e=await I();return new Promise((o,a)=>{let n=e.transaction(t,"readonly").objectStore(t).getAll();n.onsuccess=()=>o(n.result),n.onerror=()=>a(n.error)})}async function P(t){let e=await I();return new Promise((o,a)=>{let n=e.transaction(t,"readonly").objectStore(t).getAllKeys();n.onsuccess=()=>o(n.result),n.onerror=()=>a(n.error)})}async function g(t,e){let o=await I();return new Promise((a,n)=>{let r=o.transaction(t,"readwrite").objectStore(t).put(e);r.onsuccess=()=>a(),r.onerror=()=>n(r.error)})}async function O(t,e){let o=await I();return new Promise((a,n)=>{let r=o.transaction(t,"readwrite").objectStore(t).delete(e);r.onsuccess=()=>a(),r.onerror=()=>n(r.error)})}var v=new Map;function M(t,e){return v.has(t)||v.set(t,new Set),v.get(t).add(e),()=>L(t,e)}function L(t,e){v.get(t)?.delete(e)}function d(t,e){v.get(t)?.forEach(o=>{try{o(e)}catch(a){console.error(`[offline-data-manager] Error in "${t}" listener:`,a)}})}function Y(t,e){let o=a=>{e(a),L(t,o)};M(t,o)}async function h(){if(!navigator?.storage?.estimate)return{usage:0,quota:1/0,available:1/0};let{usage:t=0,quota:e=1/0}=await navigator.storage.estimate();return{usage:t,quota:e,available:e-t}}async function ct(t){let{available:e,quota:o}=await h();return e-o*.1>=t}async function j(){return navigator?.storage?.persist?navigator.storage.persist():!1}async function H(){return navigator?.storage?.persisted?navigator.storage.persisted():!1}function G(t){return t===1/0?"\u221E":t<1024?`${t} B`:t<1024**2?`${(t/1024).toFixed(1)} KB`:t<1024**3?`${(t/1024**2).toFixed(1)} MB`:`${(t/1024**3).toFixed(2)} GB`}var X=null,K=null,B=!1;function ft(){d("connectivity",{online:!1}),X?.()}function pt(){d("connectivity",{online:!0}),K?.()}function wt({pauseAll:t,resumeAll:e}){B||(X=t,K=e,window.addEventListener("offline",ft),window.addEventListener("online",pt),B=!0)}function C(){window.removeEventListener("offline",ft),window.removeEventListener("online",pt),X=null,K=null,B=!1}function x(){return navigator.onLine??!0}function Q(){return B}var Lt=2*1024*1024,Nt=5*1024*1024,_t=2,mt=5,Pt=1e3,E=new Map,b=!1,F=null;function yt(){return new Promise(t=>{F=t})}function U(){if(F){let t=F;F=null,t()}}var Mt=t=>new Promise(e=>setTimeout(e,t)),Gt=t=>Pt*Math.pow(2,t);async function m(t,e){let o=await p(i.DOWNLOAD_QUEUE,t);o&&await g(i.DOWNLOAD_QUEUE,{...o,...e})}async function Bt(t,e){try{let o=await fetch(t,{method:"HEAD",signal:e}),a=o.headers.get("Accept-Ranges")==="bytes",n=o.headers.get("Content-Encoding"),r=!!n&&n!=="identity",s=o.headers.get("Content-Length"),l=s&&!r?parseInt(s,10):null,c=gt(o.headers.get("Content-Type"));return{supportsRange:a,totalBytes:l,mimeType:c}}catch{return{supportsRange:!1,totalBytes:null,mimeType:null}}}function gt(t){return t&&t.split(";")[0].trim()||null}function Et(t){let e=t.reduce((n,r)=>n+r.byteLength,0),o=new Uint8Array(e),a=0;for(let n of t)o.set(n,a),a+=n.byteLength;return o}async function Ct(t){let{id:e,downloadUrl:o,ttl:a}=t,n=new AbortController;E.set(e,n);let r=await p(i.DOWNLOAD_QUEUE,e),s=r?.retryCount??0;for(;s<=mt;)try{await m(e,{status:u.IN_PROGRESS,lastAttemptAt:Date.now(),retryCount:s,errorMessage:null}),d("status",{id:e,status:u.IN_PROGRESS}),r=await p(i.DOWNLOAD_QUEUE,e);let l=r?.byteOffset??0,c=!1,f=r?.totalBytes??t.totalBytes??null,w=t.mimeType??null;if(l===0){let R=await Bt(o,n.signal);c=R.supportsRange,R.totalBytes&&(f=R.totalBytes,await m(e,{totalBytes:f})),!w&&R.mimeType&&(w=R.mimeType)}else c=!0;let D=c&&f&&f>Nt,A,T=null;if(D)A=await Ft(e,o,l,f,n.signal);else{let R=await Qt(e,o,n.signal);A=R.uint8,T=R.mimeType}let st=w??T??"application/octet-stream",$=A.buffer,it=Date.now(),Ot=Dt(it,a);await m(e,{status:u.COMPLETE,data:$,mimeType:st,bytesDownloaded:$.byteLength,byteOffset:$.byteLength,completedAt:it,expiresAt:Ot,errorMessage:null,deferredReason:null}),d("complete",{id:e,mimeType:st}),E.delete(e);return}catch(l){if(l.name==="AbortError"){await m(e,{status:u.PAUSED}),d("status",{id:e,status:u.PAUSED}),E.delete(e);return}if(s++,s>mt){await m(e,{status:u.FAILED,retryCount:s,errorMessage:l.message}),d("error",{id:e,error:l,retryCount:s}),E.delete(e);return}let c=Gt(s-1);console.warn(`[offline-data-manager] "${e}" failed (attempt ${s}), retrying in ${c}ms:`,l.message),d("error",{id:e,error:l,retryCount:s,willRetry:!0}),await m(e,{status:u.PENDING,retryCount:s,errorMessage:l.message}),await Mt(c)}}async function Qt(t,e,o){let a=await fetch(e,{signal:o});if(!a.ok)throw new Error(`HTTP ${a.status} ${a.statusText}`);let n=a.headers.get("Content-Encoding"),r=!!n&&n!=="identity",s=a.headers.get("Content-Length"),l=s&&!r?parseInt(s,10):null,c=gt(a.headers.get("Content-Type")),f=a.body.getReader(),w=[],D=0;for(;;){let{done:A,value:T}=await f.read();if(A)break;w.push(T),D+=T.byteLength,await m(t,{bytesDownloaded:D,totalBytes:l}),d("progress",{id:t,bytesDownloaded:D,totalBytes:l,percent:l?Math.round(D/l*100):null})}return{uint8:Et(w),mimeType:c}}async function Ft(t,e,o,a,n){let r=o,s=[],l=o;for(;r<a;){let c=Math.min(r+Lt-1,a-1),f=await fetch(e,{signal:n,headers:{Range:`bytes=${r}-${c}`}});if(!f.ok&&f.status!==206)throw new Error(`HTTP ${f.status} on Range bytes=${r}-${c}`);let w=new Uint8Array(await f.arrayBuffer());s.push(w),r+=w.byteLength,l+=w.byteLength,await m(t,{bytesDownloaded:l,byteOffset:r}),d("progress",{id:t,bytesDownloaded:l,totalBytes:a,percent:Math.round(l/a*100)})}return Et(s)}async function qt(t){await At();let[e,o]=await Promise.all([y(i.REGISTRY),y(i.DOWNLOAD_QUEUE)]),a=new Map(e.map(l=>[l.id,l])),n=o.filter(l=>[u.PENDING,u.IN_PROGRESS,u.PAUSED,u.DEFERRED,u.EXPIRED].includes(l.status)).sort((l,c)=>(a.get(l.id)?.priority??10)-(a.get(c.id)?.priority??10));if(n.length===0)return;let r=[...n],s=new Set;await new Promise(l=>{function c(){if(!b){l();return}if(r.length===0){s.size===0&&l();return}if(s.size>=t)return;let f=r.shift(),w=a.get(f.id);if(!w){c();return}let D=(async()=>{let A=w.totalBytes??f.totalBytes??0;if(A>0&&!await ct(A)){await m(f.id,{status:u.DEFERRED,deferredReason:"insufficient-storage"}),d("deferred",{id:f.id,reason:"insufficient-storage"});return}await Ct(w)})().finally(()=>{s.delete(D),c()});s.add(D),c()}c()})}function z({concurrency:t=_t}={}){b||(b=!0,(async()=>{for(;b;){if(!x()){let e=await y(i.DOWNLOAD_QUEUE);for(let o of e)o.status===u.IN_PROGRESS&&(E.get(o.id)?.abort(),E.delete(o.id),await m(o.id,{status:u.PAUSED,deferredReason:"network-offline"}));d("connectivity",{online:!1}),await yt();continue}await qt(t),b&&await yt()}})())}async function V(){b=!1,U(),await S(),d("stopped",{})}async function Z(){let t=await y(i.DOWNLOAD_QUEUE);for(let e of t)e.status===u.FAILED&&await m(e.id,{status:u.PENDING,retryCount:0,errorMessage:null});U()}function J(){return b}async function N(t){E.get(t)?.abort(),E.delete(t)}async function S(){for(let[t,e]of E)e.abort(),E.delete(t)}function tt(){wt({pauseAll:S,resumeAll:U})}var u={PENDING:"pending",IN_PROGRESS:"in-progress",PAUSED:"paused",COMPLETE:"complete",EXPIRED:"expired",FAILED:"failed",DEFERRED:"deferred"},et=new Set([u.COMPLETE,u.EXPIRED]);function Wt(t){if(!t||typeof t!="object")throw new Error("Registry entry must be an object.");if(!t.id||typeof t.id!="string")throw new Error('Registry entry must have a string "id".');if(!t.downloadUrl||typeof t.downloadUrl!="string")throw new Error(`Entry "${t.id}" must have a string "downloadUrl".`);if(t.mimeType!==void 0&&t.mimeType!==null&&typeof t.mimeType!="string")throw new Error(`Entry "${t.id}" mimeType must be a string or omitted.`);if(typeof t.version!="number"||!Number.isInteger(t.version)||t.version<0)throw new Error(`Entry "${t.id}" version must be a non-negative integer.`);if(t.ttl!==void 0&&(typeof t.ttl!="number"||t.ttl<0))throw new Error(`Entry "${t.id}" ttl must be a non-negative number (seconds).`)}function Rt(t){return{id:t,status:u.PENDING,data:null,bytesDownloaded:0,totalBytes:null,byteOffset:0,retryCount:0,lastAttemptAt:null,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}}function Dt(t,e){return e?t+e*1e3:null}function $t(t){return t?Date.now()>=t:!1}async function q(t){Wt(t);let e=Date.now(),o=await p(i.REGISTRY,t.id),a=await p(i.DOWNLOAD_QUEUE,t.id),n={id:t.id,downloadUrl:t.downloadUrl,mimeType:t.mimeType??null,version:t.version,protected:t.protected??!1,priority:t.priority??10,ttl:t.ttl??0,totalBytes:t.totalBytes??null,metadata:t.metadata??{},registeredAt:o?.registeredAt??e,updatedAt:e};if(o){if(t.version>o.version){await g(i.REGISTRY,n);let r=a?{...a,status:u.PENDING,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}:Rt(t.id);await g(i.DOWNLOAD_QUEUE,r),d("registered",{id:t.id,reason:"version-updated"}),U()}return}await g(i.REGISTRY,n),await g(i.DOWNLOAD_QUEUE,Rt(t.id)),d("registered",{id:t.id,reason:"new"}),U()}async function ot(t){if(!Array.isArray(t))throw new Error("registerFiles expects an array.");let e=new Set(t.map(n=>n.id)),o=await y(i.REGISTRY),a=[];for(let n of o)!e.has(n.id)&&!n.protected&&(await O(i.REGISTRY,n.id),await O(i.DOWNLOAD_QUEUE,n.id),a.push(n.id),d("deleted",{id:n.id,registryRemoved:!0}));for(let n of t)await q(n);return{registered:t.map(n=>n.id),removed:a}}async function At(){let t=await y(i.DOWNLOAD_QUEUE),e=[];for(let o of t)o.status===u.COMPLETE&&$t(o.expiresAt)&&(await g(i.DOWNLOAD_QUEUE,{...o,status:u.EXPIRED}),e.push(o.id),d("expired",{id:o.id}));return e}async function bt(){let[t,e,o]=await Promise.all([y(i.REGISTRY),y(i.DOWNLOAD_QUEUE),h()]),a=new Map(e.map(r=>[r.id,r]));return{items:t.map(r=>{let s=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:s?.status??null,bytesDownloaded:s?.bytesDownloaded??0,storedBytes:s?.data?.length??null,progress:s?.totalBytes&&s?.bytesDownloaded?Math.round(s.bytesDownloaded/s.totalBytes*100):null,retryCount:s?.retryCount??0,lastAttemptAt:s?.lastAttemptAt??null,errorMessage:s?.errorMessage??null,deferredReason:s?.deferredReason??null,completedAt:s?.completedAt??null,expiresAt:s?.expiresAt??null}}).sort((r,s)=>r.priority-s.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 nt(t){let[e,o]=await Promise.all([p(i.REGISTRY,t),p(i.DOWNLOAD_QUEUE,t)]);return e?{id:e.id,downloadUrl:e.downloadUrl,mimeType:e.mimeType??null,version:e.version,protected:e.protected,priority:e.priority,ttl:e.ttl,totalBytes:e.totalBytes,metadata:e.metadata,registeredAt:e.registeredAt,updatedAt:e.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 rt(t){let e=await p(i.DOWNLOAD_QUEUE,t);return et.has(e?.status)}async function kt(t){let e=await p(i.DOWNLOAD_QUEUE,t);e&&await g(i.DOWNLOAD_QUEUE,{...e,status:u.PENDING,data:null,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null})}async function W(t,{removeRegistry:e=!1}={}){let o=await p(i.REGISTRY,t);if(!o)throw new Error(`deleteFile: No registered file with id "${t}".`);await N(t);let a=e||!o.protected;return a?(await O(i.REGISTRY,t),await O(i.DOWNLOAD_QUEUE,t)):await kt(t),d("deleted",{id:t,registryRemoved:a}),{id:t,registryRemoved:a}}async function at({removeRegistry:t=!1}={}){await S();let e=await P(i.REGISTRY);return Promise.all(e.map(o=>W(o,{removeRegistry:t})))}async function St(t){let[e,o]=await Promise.all([p(i.REGISTRY,t),p(i.DOWNLOAD_QUEUE,t)]);if(!e)throw new Error(`retrieve: No registered file with id "${t}".`);if(!et.has(o?.status)||!o?.data)throw new Error(`retrieve: File "${t}" has no data yet (status: ${o?.status??"unknown"}).`);return{data:o.data,mimeType:o.mimeType}}var Yt={setDBInfo:dt,dbGetAllIds:P,registerFile:q,registerFiles:ot,startDownloads:z,stopDownloads:V,retryFailed:Z,isDownloading:J,abortDownload:N,abortAllDownloads:S,startMonitoring:tt,stopMonitoring:C,isOnline:x,isMonitoring:Q,retrieve:St,view,getStatus:nt,isReady:rt,delete:W,deleteAll:at,on:M,off:L,once:Y,getStorageEstimate:h,requestPersistentStorage:j,isPersistentStorage:H},jt=Yt;return vt(Ht);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offline-data-manager",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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",
@@ -1,50 +1,57 @@
1
+ /** Called by registerFile() and the connectivity monitor to wake the loop. */
2
+ export function _notifyNewWork(): void;
1
3
  /**
2
- * Evaluates TTL expiry, then downloads everything that needs it.
4
+ * Starts the persistent download loop.
3
5
  *
4
- * Eligible statuses (when resumeOnly is false):
5
- * pending, in-progress, paused, deferred, expired
6
- * + failed entries when retryFailed: true
6
+ * The loop drains the queue, then waits for new work without polling.
7
+ * It wakes automatically when:
8
+ * - registerFile() / registerFiles() adds a new or updated entry
9
+ * - The browser comes back online after an offline period
10
+ * - retryFailed() is called
7
11
  *
8
- * If the browser is currently offline, the call returns immediately after
9
- * marking any in-progress entries as paused. Downloads will auto-resume
10
- * when connectivity is restored (if startConnectivityMonitor() was called).
12
+ * Idempotent subsequent calls while already running are a no-op.
11
13
  *
12
- * @param {object} [options]
13
- * @param {number} [options.concurrency=2] — max parallel downloads
14
- * @param {boolean} [options.resumeOnly=false] — only resume in-progress/paused
15
- * @param {boolean} [options.retryFailed=false] — re-queue failed entries before running
16
- * @returns {Promise<void>}
14
+ * @param {object} [options]
15
+ * @param {number} [options.concurrency=2] — max parallel downloads
17
16
  */
18
- export function downloadFiles({ concurrency, resumeOnly, retryFailed, }?: {
17
+ export function startDownloads({ concurrency }?: {
19
18
  concurrency?: number | undefined;
20
- resumeOnly?: boolean | undefined;
21
- retryFailed?: boolean | undefined;
22
- }): Promise<void>;
19
+ }): void;
23
20
  /**
24
- * Aborts an active download, setting it to 'paused'.
25
- * It will resume on the next downloadFiles() call.
21
+ * Stops the download loop gracefully.
22
+ *
23
+ * In-flight downloads are aborted and set to 'paused'. They will resume
24
+ * automatically when startDownloads() is called again.
25
+ */
26
+ export function stopDownloads(): Promise<void>;
27
+ /**
28
+ * Re-queues all failed entries and wakes the loop to retry them.
29
+ * Only meaningful when the loop is running via startDownloads().
30
+ */
31
+ export function retryFailed(): Promise<void>;
32
+ /**
33
+ * Returns true if the download loop is currently running.
34
+ * @returns {boolean}
35
+ */
36
+ export function isDownloading(): boolean;
37
+ /**
38
+ * Aborts a single active download, setting it to 'paused'.
39
+ * The loop will pick it up again on the next drain cycle.
26
40
  * @param {string} id
27
41
  */
28
42
  export function abortDownload(id: string): Promise<void>;
29
43
  /**
30
- * Aborts all active downloads.
44
+ * Aborts all active downloads, setting them to 'paused'.
31
45
  */
32
46
  export function abortAllDownloads(): Promise<void>;
33
- /**
34
- * Resumes any downloads interrupted by a previous page or SW close.
35
- * Call this in a service worker 'activate' event.
36
- * @returns {Promise<void>}
37
- */
38
- export function resumeInterruptedDownloads(): Promise<void>;
39
47
  /**
40
48
  * Starts monitoring online/offline connectivity.
41
49
  *
42
- * - Going offline: all active downloads are paused immediately.
43
- * - Coming back online: downloadFiles() is called automatically with the
44
- * same options as the most recent explicit call.
50
+ * Going offline: aborts active downloads immediately (pauses them).
51
+ * Coming back online: wakes the download loop to resume.
45
52
  *
46
- * Idempotent — safe to call multiple times. Call stopConnectivityMonitor()
47
- * to remove the listeners (useful in tests or cleanup).
53
+ * Idempotent — safe to call multiple times.
54
+ * Emits 'connectivity' events: { online: boolean }.
48
55
  */
49
56
  export function startMonitoring(): void;
50
57
  export { stopConnectivityMonitor as stopMonitoring, isOnline, isMonitoring } from "./connectivity.js";
package/types/index.d.ts CHANGED
@@ -4,16 +4,18 @@ declare namespace OfflineDataManager {
4
4
  export { dbGetAllIds };
5
5
  export { registerFile };
6
6
  export { registerFiles };
7
- export { downloadFiles };
7
+ export { startDownloads };
8
+ export { stopDownloads };
9
+ export { retryFailed };
10
+ export { isDownloading };
8
11
  export { abortDownload };
9
12
  export { abortAllDownloads };
10
- export { resumeInterruptedDownloads };
11
13
  export { startMonitoring };
12
14
  export { stopMonitoring };
13
15
  export { isOnline };
14
16
  export { isMonitoring };
15
17
  export { retrieve };
16
- export { view };
18
+ export let view: any;
17
19
  export { getStatus };
18
20
  export { isReady };
19
21
  export { deleteFile as delete };
@@ -27,10 +29,12 @@ declare namespace OfflineDataManager {
27
29
  }
28
30
  import { registerFile } from './registry.js';
29
31
  import { registerFiles } from './registry.js';
30
- import { downloadFiles } from './downloader.js';
32
+ import { startDownloads } from './downloader.js';
33
+ import { stopDownloads } from './downloader.js';
34
+ import { retryFailed } from './downloader.js';
35
+ import { isDownloading } from './downloader.js';
31
36
  import { abortDownload } from './downloader.js';
32
37
  import { abortAllDownloads } from './downloader.js';
33
- import { resumeInterruptedDownloads } from './downloader.js';
34
38
  import { startMonitoring } from './downloader.js';
35
39
  import { stopMonitoring } from './downloader.js';
36
40
  import { isOnline } from './downloader.js';
@@ -50,7 +54,7 @@ export function retrieve(id: string): Promise<{
50
54
  data: ArrayBuffer;
51
55
  mimeType: string;
52
56
  }>;
53
- import { view } from './registry.js';
57
+ import { getAllStatus } from './registry.js';
54
58
  import { getStatus } from './registry.js';
55
59
  import { isReady } from './registry.js';
56
60
  import { deleteFile } from './deleter.js';
@@ -64,4 +68,4 @@ import { requestPersistentStorage } from './storage.js';
64
68
  import { isPersistentStorage } from './storage.js';
65
69
  import { setDBInfo } from './db.js';
66
70
  import { dbGetAllIds } from './db.js';
67
- export { registerFile, registerFiles, downloadFiles, abortDownload, abortAllDownloads, resumeInterruptedDownloads, startMonitoring, stopMonitoring, isOnline, isMonitoring, view, getStatus, isReady, deleteFile, deleteAllFiles, on, off, once, emit, getStorageEstimate, requestPersistentStorage, isPersistentStorage };
71
+ export { registerFile, registerFiles, startDownloads, stopDownloads, retryFailed, isDownloading, abortDownload, abortAllDownloads, startMonitoring, stopMonitoring, isOnline, isMonitoring, getAllStatus, getStatus, isReady, deleteFile, deleteAllFiles, on, off, once, emit, getStorageEstimate, requestPersistentStorage, isPersistentStorage };
@@ -43,7 +43,7 @@ export function registerFiles(entries: object[]): Promise<{
43
43
  * Checks all complete queue entries against their TTL and flips any that have
44
44
  * expired to `expired` status, queuing them for re-download.
45
45
  *
46
- * Called internally by downloadFiles() before deciding what to download.
46
+ * Called internally by the download loop before each drain cycle.
47
47
  * @returns {Promise<string[]>} IDs of entries that were marked expired
48
48
  */
49
49
  export function evaluateExpiry(): Promise<string[]>;
@@ -53,7 +53,7 @@ export function evaluateExpiry(): Promise<string[]>;
53
53
  *
54
54
  * @returns {Promise<{ items: object[], storage: object }>}
55
55
  */
56
- export function view(): Promise<{
56
+ export function getAllStatus(): Promise<{
57
57
  items: object[];
58
58
  storage: object;
59
59
  }>;