offline-data-manager 1.0.1 → 1.0.3

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.
98
-
99
- ## File structure
69
+ ---
100
70
 
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
71
+ ## Quick start
110
72
 
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,157 +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
- ### `registerFile(entry)`
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.
196
+
197
+ Because registering a file wakes the loop, there is no need to call `startDownloads()` again after registering new files at runtime.
198
+
199
+ #### `stopDownloads()`
200
+ Stops the loop gracefully. In-flight downloads are aborted and set to `paused`. Call `startDownloads()` again to resume.
201
+
202
+ ```js
203
+ await ODM.stopDownloads();
204
+ ```
181
205
 
182
- Registers a single file. No-op if version hasn't strictly increased.
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.
183
208
 
184
- ### `registerFiles(entries)`
209
+ ```js
210
+ await ODM.retryFailed();
211
+ ```
185
212
 
186
- Registers an array of files. Removes non-protected entries absent from the list.
213
+ #### `isDownloading()`
214
+ Returns `true` if the loop is currently running.
187
215
 
188
216
  ```js
189
- const { registered, removed } = await offlineDataManager.registerFiles([...]);
217
+ ODM.isDownloading(); // → boolean
190
218
  ```
191
219
 
192
- ### `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.
229
+
230
+ ---
231
+
232
+ ### Connectivity monitoring
233
+
234
+ #### `startMonitoring()` / `stopMonitoring()`
235
+ Monitors `window` online/offline events.
193
236
 
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.
237
+ - Going **offline**: immediately pauses all active downloads (avoids burning retry attempts).
238
+ - Coming back **online**: wakes the download loop to resume automatically.
195
239
 
196
240
  ```js
197
- 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)
198
243
  ```
199
244
 
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.
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.
201
246
 
202
- ### `retrieve(id)`
247
+ #### `isOnline()` / `isMonitoring()`
248
+ ```js
249
+ ODM.isOnline(); // → boolean (navigator.onLine)
250
+ ODM.isMonitoring(); // → boolean
251
+ ```
252
+
253
+ ---
254
+
255
+ ### Retrieve
203
256
 
204
- Returns the stored file information `{ data: ArrayBuffer, mimeType: string }`. Works for both `complete` and `expired` entries.
257
+ #### `retrieve(id)`
258
+ Returns the stored ArrayBuffer and resolved MIME type for a completed or expired file.
205
259
 
206
260
  ```js
207
- const fileInfo = await offlineDataManager.retrieve('json-data');
261
+ const { data, mimeType } = await ODM.retrieve('poi-data');
208
262
 
209
- const decoder = new TextDecoder("utf-8");
210
- const text = decoder.decode(fileInfo.data);
263
+ // Text / JSON
264
+ const text = new TextDecoder().decode(data);
265
+ const json = JSON.parse(text);
266
+
267
+ // Binary (e.g. PMTiles, zip)
268
+ const { data: mapBuffer } = await ODM.retrieve('base-map');
269
+ // pass mapBuffer to PMTiles, JSZip, etc.
211
270
  ```
212
271
 
213
- ### `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.
273
+
274
+ ---
214
275
 
215
- Returns all entries merged with queue state, plus storage summary.
276
+ ### Status
216
277
 
278
+ #### `view()`
279
+ Returns all entries merged with queue state, plus a storage summary.
217
280
  ```js
218
- const { items, storage } = await offlineDataManager.view();
281
+ const { items, storage } = await ODM.view();
219
282
  // items[n]: { id, mimeType, version, downloadStatus, storedBytes,
220
283
  // bytesDownloaded, progress, completedAt, expiresAt, ... }
221
284
  // storage: { usageBytes, quotaBytes, availableBytes, ...Formatted }
222
285
  ```
223
286
 
224
- ### `getStatus(id)`
225
-
287
+ #### `getStatus(id)`
226
288
  Full merged status for one file, or `null` if not registered.
227
289
 
228
- ### `isReady(id)`
290
+ #### `isReady(id)`
291
+ Returns `true` if the file has data available (`complete` or `expired`).
229
292
 
230
- Returns `true` if the file has a data available (`complete` or `expired`).
293
+ ---
231
294
 
232
- ### `delete(id, options?)`
295
+ ### Delete
233
296
 
297
+ #### `delete(id, options?)`
234
298
  ```js
235
- await offlineDataManager.delete('poi-data'); // respects protected flag
236
- 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
237
301
  ```
238
302
 
239
- ### `deleteAll(options?)`
240
-
303
+ #### `deleteAll(options?)`
241
304
  ```js
242
- await offlineDataManager.deleteAll();
243
- await offlineDataManager.deleteAll({ removeRegistry: true });
305
+ await ODM.deleteAll();
306
+ await ODM.deleteAll({ removeRegistry: true });
244
307
  ```
245
308
 
246
- ### `startMonitoring()` / `stopMonitoring()`
309
+ ---
247
310
 
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.
311
+ ### Events
249
312
 
250
313
  ```js
251
- offlineDataManager.startMonitoring(); // call once after first downloadFiles()
252
- 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
253
327
  ```
254
328
 
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.
329
+ ---
256
330
 
257
- ### `isOnline()` / `isMonitoring()`
331
+ ### Storage
258
332
 
259
333
  ```js
260
- offlineDataManager.isOnline(); // boolean (navigator.onLine)
261
- offlineDataManager.isMonitoring(); // → boolean
334
+ const { usage, quota, available } = await ODM.getStorageEstimate();
335
+ await ODM.requestPersistentStorage();
336
+ await ODM.isPersistentStorage();
262
337
  ```
263
338
 
339
+ ---
264
340
 
265
- Sets status to `paused`; resumes on next `downloadFiles()`.
266
-
267
- ### `resumeInterruptedDownloads()`
341
+ ### Service worker
268
342
 
269
- 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.
270
344
 
271
345
  ```js
272
346
  self.addEventListener('activate', (event) => {
273
- event.waitUntil(offlineDataManager.resumeInterruptedDownloads());
347
+ event.waitUntil(
348
+ (async () => {
349
+ ODM.startMonitoring();
350
+ ODM.startDownloads({ concurrency: 2 });
351
+ })()
352
+ );
274
353
  });
275
354
  ```
276
355
 
277
- ### Events
356
+ ---
278
357
 
279
- ```js
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 }) => {});
290
- 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
291
364
  ```
292
365
 
293
- ### Storage
366
+ The build script is zero-dependency — pure Node.js 18+, no Rollup or Webpack required.
294
367
 
295
- ```js
296
- const { usage, quota, available } = await offlineDataManager.getStorageEstimate();
297
- await offlineDataManager.requestPersistentStorage();
298
- await offlineDataManager.isPersistentStorage();
299
- ```
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 |
300
374
 
301
375
  ---
302
376
 
303
377
  ## Notes
304
378
 
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()`.
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.
307
- - **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()`, `view()`, 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`.
308
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.
309
- - **Storage safety margin** — 10% of quota is reserved before deferring downloads. Configurable in `storage.js`.
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.
384
+ - **Storage safety margin** — 10% of quota is reserved before deferring downloads. Configurable in `storage.js`.
@@ -1 +1 @@
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
+ var K="offline-data-manager",z=1,L=null,i={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function V(e,t){K=e??"offline-data-manager",z=t??1}async function U(){return L||(L=await new Promise((e,t)=>{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=()=>e(o.result),o.onerror=()=>t(o.error)}),L)}async function p(e,t){let o=await U();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 y(e){let t=await U();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 N(e){let t=await U();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 E(e,t){let o=await U();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 S(e,t){let o=await U();return new Promise((a,r)=>{let n=o.transaction(e,"readwrite").objectStore(e).delete(t);n.onsuccess=()=>a(),n.onerror=()=>r(n.error)})}var T=new Map;function Q(e,t){return T.has(e)||T.set(e,new Set),T.get(e).add(t),()=>_(e,t)}function _(e,t){T.get(e)?.delete(t)}function d(e,t){T.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),_(e,o)};Q(e,o)}async function I(){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 J(e){let{available:t,quota:o}=await I();return t-o*.1>=e}async function ee(){return navigator?.storage?.persist?navigator.storage.persist():!1}async function te(){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 F=null,q=null,M=!1;function oe(){d("connectivity",{online:!1}),F?.()}function ne(){d("connectivity",{online:!0}),q?.()}function re({pauseAll:e,resumeAll:t}){M||(F=e,q=t,window.addEventListener("offline",oe),window.addEventListener("online",ne),M=!0)}function W(){window.removeEventListener("offline",oe),window.removeEventListener("online",ne),F=null,q=null,M=!1}function v(){return navigator.onLine??!0}function $(){return M}var Se=2*1024*1024,Oe=5*1024*1024,he=2,ae=5,xe=1e3,g=new Map,b=!1,G=null;function se(){return new Promise(e=>{G=e})}function O(){if(G){let e=G;G=null,e()}}var Ue=e=>new Promise(t=>setTimeout(t,e)),Te=e=>xe*Math.pow(2,e);async function m(e,t){let o=await p(i.DOWNLOAD_QUEUE,e);o&&await E(i.DOWNLOAD_QUEUE,{...o,...t})}async function Ie(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",s=o.headers.get("Content-Length"),l=s&&!n?parseInt(s,10):null,c=ie(o.headers.get("Content-Type"));return{supportsRange:a,totalBytes:l,mimeType:c}}catch{return{supportsRange:!1,totalBytes:null,mimeType:null}}}function ie(e){return e&&e.split(";")[0].trim()||null}function le(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 ve(e){let{id:t,downloadUrl:o,ttl:a}=e,r=new AbortController;g.set(t,r);let n=await p(i.DOWNLOAD_QUEUE,t),s=n?.retryCount??0;for(;s<=ae;)try{await m(t,{status:u.IN_PROGRESS,lastAttemptAt:Date.now(),retryCount:s,errorMessage:null}),d("status",{id:t,status:u.IN_PROGRESS}),n=await p(i.DOWNLOAD_QUEUE,t);let l=n?.byteOffset??0,c=!1,f=n?.totalBytes??e.totalBytes??null,w=e.mimeType??null;if(l===0){let R=await Ie(o,r.signal);c=R.supportsRange,R.totalBytes&&(f=R.totalBytes,await m(t,{totalBytes:f})),!w&&R.mimeType&&(w=R.mimeType)}else c=!0;let D=c&&f&&f>Oe,A,x=null;if(D)A=await Ne(t,o,l,f,r.signal);else{let R=await Le(t,o,r.signal);A=R.uint8,x=R.mimeType}let H=w??x??"application/octet-stream",C=A.buffer,X=Date.now(),be=we(X,a);await m(t,{status:u.COMPLETE,data:C,mimeType:H,bytesDownloaded:C.byteLength,byteOffset:C.byteLength,completedAt:X,expiresAt:be,errorMessage:null,deferredReason:null}),d("complete",{id:t,mimeType:H}),g.delete(t);return}catch(l){if(l.name==="AbortError"){await m(t,{status:u.PAUSED}),d("status",{id:t,status:u.PAUSED}),g.delete(t);return}if(s++,s>ae){await m(t,{status:u.FAILED,retryCount:s,errorMessage:l.message}),d("error",{id:t,error:l,retryCount:s}),g.delete(t);return}let c=Te(s-1);console.warn(`[offline-data-manager] "${t}" failed (attempt ${s}), retrying in ${c}ms:`,l.message),d("error",{id:t,error:l,retryCount:s,willRetry:!0}),await m(t,{status:u.PENDING,retryCount:s,errorMessage:l.message}),await Ue(c)}}async function Le(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",s=a.headers.get("Content-Length"),l=s&&!n?parseInt(s,10):null,c=ie(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(e,{bytesDownloaded:D,totalBytes:l}),d("progress",{id:e,bytesDownloaded:D,totalBytes:l,percent:l?Math.round(D/l*100):null})}return{uint8:le(w),mimeType:c}}async function Ne(e,t,o,a,r){let n=o,s=[],l=o;for(;n<a;){let c=Math.min(n+Se-1,a-1),f=await fetch(t,{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(e,{bytesDownloaded:l,byteOffset:n}),d("progress",{id:e,bytesDownloaded:l,totalBytes:a,percent:Math.round(l/a*100)})}return le(s)}async function _e(e){await me();let[t,o]=await Promise.all([y(i.REGISTRY),y(i.DOWNLOAD_QUEUE)]),a=new Map(t.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>=e)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 ve(w)})().finally(()=>{s.delete(D),c()});s.add(D),c()}c()})}function ue({concurrency:e=he}={}){b||(b=!0,(async()=>{for(;b;){if(!v()){let t=await y(i.DOWNLOAD_QUEUE);for(let o of t)o.status===u.IN_PROGRESS&&(g.get(o.id)?.abort(),g.delete(o.id),await m(o.id,{status:u.PAUSED,deferredReason:"network-offline"}));d("connectivity",{online:!1}),await se();continue}await _e(e),b&&await se()}})())}async function de(){b=!1,O(),await h(),d("stopped",{})}async function ce(){let e=await y(i.DOWNLOAD_QUEUE);for(let t of e)t.status===u.FAILED&&await m(t.id,{status:u.PENDING,retryCount:0,errorMessage:null});O()}function fe(){return b}async function B(e){g.get(e)?.abort(),g.delete(e)}async function h(){for(let[e,t]of g)t.abort(),g.delete(e)}function pe(){re({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 Pe(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 ye(e){return{id:e,status:u.PENDING,data:null,bytesDownloaded:0,totalBytes:null,byteOffset:0,retryCount:0,lastAttemptAt:null,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}}function we(e,t){return t?e+t*1e3:null}function Me(e){return e?Date.now()>=e:!1}async function Y(e){Pe(e);let t=Date.now(),o=await p(i.REGISTRY,e.id),a=await p(i.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 E(i.REGISTRY,r);let n=a?{...a,status:u.PENDING,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}:ye(e.id);await E(i.DOWNLOAD_QUEUE,n),d("registered",{id:e.id,reason:"version-updated"}),O()}return}await E(i.REGISTRY,r),await E(i.DOWNLOAD_QUEUE,ye(e.id)),d("registered",{id:e.id,reason:"new"}),O()}async function Ee(e){if(!Array.isArray(e))throw new Error("registerFiles expects an array.");let t=new Set(e.map(r=>r.id)),o=await y(i.REGISTRY),a=[];for(let r of o)!t.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 e)await Y(r);return{registered:e.map(r=>r.id),removed:a}}async function me(){let e=await y(i.DOWNLOAD_QUEUE),t=[];for(let o of e)o.status===u.COMPLETE&&Me(o.expiresAt)&&(await E(i.DOWNLOAD_QUEUE,{...o,status:u.EXPIRED}),t.push(o.id),d("expired",{id:o.id}));return t}async function ge(){let[e,t,o]=await Promise.all([y(i.REGISTRY),y(i.DOWNLOAD_QUEUE),I()]),a=new Map(t.map(n=>[n.id,n]));return{items:e.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 De(e){let[t,o]=await Promise.all([p(i.REGISTRY,e),p(i.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 Ae(e){let t=await p(i.DOWNLOAD_QUEUE,e);return k.has(t?.status)}async function Ge(e){let t=await p(i.DOWNLOAD_QUEUE,e);t&&await E(i.DOWNLOAD_QUEUE,{...t,status:u.PENDING,data:null,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null})}async function j(e,{removeRegistry:t=!1}={}){let o=await p(i.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 S(i.REGISTRY,e),await S(i.DOWNLOAD_QUEUE,e)):await Ge(e),d("deleted",{id:e,registryRemoved:a}),{id:e,registryRemoved:a}}async function Re({removeRegistry:e=!1}={}){await h();let t=await N(i.REGISTRY);return Promise.all(t.map(o=>j(o,{removeRegistry:e})))}async function Be(e){let[t,o]=await Promise.all([p(i.REGISTRY,e),p(i.DOWNLOAD_QUEUE,e)]);if(!t)throw new Error(`retrieve: No registered file with id "${e}".`);if(!k.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 Ce={setDBInfo:V,dbGetAllIds:N,registerFile:Y,registerFiles:Ee,startDownloads:ue,stopDownloads:de,retryFailed:ce,isDownloading:fe,abortDownload:B,abortAllDownloads:h,startMonitoring:pe,stopMonitoring:W,isOnline:v,isMonitoring:$,retrieve:Be,view:ge,getStatus:De,isReady:Ae,delete:j,deleteAll:Re,on:Q,off:_,once:Z,getStorageEstimate:I,requestPersistentStorage:ee,isPersistentStorage:te},mt=Ce;export{h as abortAllDownloads,B as abortDownload,mt as default,Re as deleteAllFiles,j as deleteFile,d as emit,De as getStatus,I as getStorageEstimate,fe as isDownloading,$ as isMonitoring,v as isOnline,te as isPersistentStorage,Ae as isReady,_ as off,Q as on,Z as once,Y as registerFile,Ee as registerFiles,ee as requestPersistentStorage,Be as retrieve,ce as retryFailed,ue as startDownloads,pe as startMonitoring,de as stopDownloads,W as stopMonitoring,ge as view};
@@ -1 +1 @@
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);})();
1
+ "use strict";var offlineMapData=(()=>{var k=Object.defineProperty;var he=Object.getOwnPropertyDescriptor;var xe=Object.getOwnPropertyNames;var Ue=Object.prototype.hasOwnProperty;var Te=(e,t)=>{for(var o in t)k(e,o,{get:t[o],enumerable:!0})},Ie=(e,t,o,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of xe(t))!Ue.call(e,n)&&n!==o&&k(e,n,{get:()=>t[n],enumerable:!(a=he(t,n))||a.enumerable});return e};var ve=e=>Ie(k({},"__esModule",{value:!0}),e);var He={};Te(He,{abortAllDownloads:()=>S,abortDownload:()=>N,default:()=>je,deleteAllFiles:()=>se,deleteFile:()=>W,emit:()=>d,getStatus:()=>re,getStorageEstimate:()=>h,isDownloading:()=>J,isMonitoring:()=>Q,isOnline:()=>x,isPersistentStorage:()=>H,isReady:()=>ae,off:()=>L,on:()=>M,once:()=>Y,registerFile:()=>q,registerFiles:()=>oe,requestPersistentStorage:()=>j,retrieve:()=>Se,retryFailed:()=>Z,startDownloads:()=>z,startMonitoring:()=>ee,stopDownloads:()=>V,stopMonitoring:()=>C,view:()=>ne});var ue="offline-data-manager",de=1,_=null,i={REGISTRY:"registry",DOWNLOAD_QUEUE:"downloadQueue"};async function ce(e,t){ue=e??"offline-data-manager",de=t??1}async function I(){return _||(_=await new Promise((e,t)=>{let o=indexedDB.open(ue,de);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=()=>e(o.result),o.onerror=()=>t(o.error)}),_)}async function p(e,t){let o=await I();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 y(e){let t=await I();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 P(e){let t=await I();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 E(e,t){let o=await I();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 O(e,t){let o=await I();return new Promise((a,n)=>{let r=o.transaction(e,"readwrite").objectStore(e).delete(t);r.onsuccess=()=>a(),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),()=>L(e,t)}function L(e,t){v.get(e)?.delete(t)}function d(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)};M(e,o)}async function h(){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 fe(e){let{available:t,quota:o}=await h();return t-o*.1>=e}async function j(){return navigator?.storage?.persist?navigator.storage.persist():!1}async function H(){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 X=null,K=null,B=!1;function pe(){d("connectivity",{online:!1}),X?.()}function we(){d("connectivity",{online:!0}),K?.()}function me({pauseAll:e,resumeAll:t}){B||(X=e,K=t,window.addEventListener("offline",pe),window.addEventListener("online",we),B=!0)}function C(){window.removeEventListener("offline",pe),window.removeEventListener("online",we),X=null,K=null,B=!1}function x(){return navigator.onLine??!0}function Q(){return B}var Le=2*1024*1024,Ne=5*1024*1024,_e=2,ye=5,Pe=1e3,g=new Map,b=!1,F=null;function Ee(){return new Promise(e=>{F=e})}function U(){if(F){let e=F;F=null,e()}}var Me=e=>new Promise(t=>setTimeout(t,e)),Ge=e=>Pe*Math.pow(2,e);async function m(e,t){let o=await p(i.DOWNLOAD_QUEUE,e);o&&await E(i.DOWNLOAD_QUEUE,{...o,...t})}async function Be(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",s=o.headers.get("Content-Length"),l=s&&!r?parseInt(s,10):null,c=ge(o.headers.get("Content-Type"));return{supportsRange:a,totalBytes:l,mimeType:c}}catch{return{supportsRange:!1,totalBytes:null,mimeType:null}}}function ge(e){return e&&e.split(";")[0].trim()||null}function De(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 Ce(e){let{id:t,downloadUrl:o,ttl:a}=e,n=new AbortController;g.set(t,n);let r=await p(i.DOWNLOAD_QUEUE,t),s=r?.retryCount??0;for(;s<=ye;)try{await m(t,{status:u.IN_PROGRESS,lastAttemptAt:Date.now(),retryCount:s,errorMessage:null}),d("status",{id:t,status:u.IN_PROGRESS}),r=await p(i.DOWNLOAD_QUEUE,t);let l=r?.byteOffset??0,c=!1,f=r?.totalBytes??e.totalBytes??null,w=e.mimeType??null;if(l===0){let R=await Be(o,n.signal);c=R.supportsRange,R.totalBytes&&(f=R.totalBytes,await m(t,{totalBytes:f})),!w&&R.mimeType&&(w=R.mimeType)}else c=!0;let D=c&&f&&f>Ne,A,T=null;if(D)A=await Fe(t,o,l,f,n.signal);else{let R=await Qe(t,o,n.signal);A=R.uint8,T=R.mimeType}let ie=w??T??"application/octet-stream",$=A.buffer,le=Date.now(),Oe=Ae(le,a);await m(t,{status:u.COMPLETE,data:$,mimeType:ie,bytesDownloaded:$.byteLength,byteOffset:$.byteLength,completedAt:le,expiresAt:Oe,errorMessage:null,deferredReason:null}),d("complete",{id:t,mimeType:ie}),g.delete(t);return}catch(l){if(l.name==="AbortError"){await m(t,{status:u.PAUSED}),d("status",{id:t,status:u.PAUSED}),g.delete(t);return}if(s++,s>ye){await m(t,{status:u.FAILED,retryCount:s,errorMessage:l.message}),d("error",{id:t,error:l,retryCount:s}),g.delete(t);return}let c=Ge(s-1);console.warn(`[offline-data-manager] "${t}" failed (attempt ${s}), retrying in ${c}ms:`,l.message),d("error",{id:t,error:l,retryCount:s,willRetry:!0}),await m(t,{status:u.PENDING,retryCount:s,errorMessage:l.message}),await Me(c)}}async function Qe(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",s=a.headers.get("Content-Length"),l=s&&!r?parseInt(s,10):null,c=ge(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(e,{bytesDownloaded:D,totalBytes:l}),d("progress",{id:e,bytesDownloaded:D,totalBytes:l,percent:l?Math.round(D/l*100):null})}return{uint8:De(w),mimeType:c}}async function Fe(e,t,o,a,n){let r=o,s=[],l=o;for(;r<a;){let c=Math.min(r+Le-1,a-1),f=await fetch(t,{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(e,{bytesDownloaded:l,byteOffset:r}),d("progress",{id:e,bytesDownloaded:l,totalBytes:a,percent:Math.round(l/a*100)})}return De(s)}async function qe(e){await Re();let[t,o]=await Promise.all([y(i.REGISTRY),y(i.DOWNLOAD_QUEUE)]),a=new Map(t.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>=e)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 fe(A)){await m(f.id,{status:u.DEFERRED,deferredReason:"insufficient-storage"}),d("deferred",{id:f.id,reason:"insufficient-storage"});return}await Ce(w)})().finally(()=>{s.delete(D),c()});s.add(D),c()}c()})}function z({concurrency:e=_e}={}){b||(b=!0,(async()=>{for(;b;){if(!x()){let t=await y(i.DOWNLOAD_QUEUE);for(let o of t)o.status===u.IN_PROGRESS&&(g.get(o.id)?.abort(),g.delete(o.id),await m(o.id,{status:u.PAUSED,deferredReason:"network-offline"}));d("connectivity",{online:!1}),await Ee();continue}await qe(e),b&&await Ee()}})())}async function V(){b=!1,U(),await S(),d("stopped",{})}async function Z(){let e=await y(i.DOWNLOAD_QUEUE);for(let t of e)t.status===u.FAILED&&await m(t.id,{status:u.PENDING,retryCount:0,errorMessage:null});U()}function J(){return b}async function N(e){g.get(e)?.abort(),g.delete(e)}async function S(){for(let[e,t]of g)t.abort(),g.delete(e)}function ee(){me({pauseAll:S,resumeAll:U})}var u={PENDING:"pending",IN_PROGRESS:"in-progress",PAUSED:"paused",COMPLETE:"complete",EXPIRED:"expired",FAILED:"failed",DEFERRED:"deferred"},te=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 be(e){return{id:e,status:u.PENDING,data:null,bytesDownloaded:0,totalBytes:null,byteOffset:0,retryCount:0,lastAttemptAt:null,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}}function Ae(e,t){return t?e+t*1e3:null}function $e(e){return e?Date.now()>=e:!1}async function q(e){We(e);let t=Date.now(),o=await p(i.REGISTRY,e.id),a=await p(i.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 E(i.REGISTRY,n);let r=a?{...a,status:u.PENDING,bytesDownloaded:0,byteOffset:0,retryCount:0,errorMessage:null,deferredReason:null,completedAt:null,expiresAt:null}:be(e.id);await E(i.DOWNLOAD_QUEUE,r),d("registered",{id:e.id,reason:"version-updated"}),U()}return}await E(i.REGISTRY,n),await E(i.DOWNLOAD_QUEUE,be(e.id)),d("registered",{id:e.id,reason:"new"}),U()}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 y(i.REGISTRY),a=[];for(let n of o)!t.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 e)await q(n);return{registered:e.map(n=>n.id),removed:a}}async function Re(){let e=await y(i.DOWNLOAD_QUEUE),t=[];for(let o of e)o.status===u.COMPLETE&&$e(o.expiresAt)&&(await E(i.DOWNLOAD_QUEUE,{...o,status:u.EXPIRED}),t.push(o.id),d("expired",{id:o.id}));return t}async function ne(){let[e,t,o]=await Promise.all([y(i.REGISTRY),y(i.DOWNLOAD_QUEUE),h()]),a=new Map(t.map(r=>[r.id,r]));return{items:e.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 re(e){let[t,o]=await Promise.all([p(i.REGISTRY,e),p(i.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 ae(e){let t=await p(i.DOWNLOAD_QUEUE,e);return te.has(t?.status)}async function ke(e){let t=await p(i.DOWNLOAD_QUEUE,e);t&&await E(i.DOWNLOAD_QUEUE,{...t,status:u.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 p(i.REGISTRY,e);if(!o)throw new Error(`deleteFile: No registered file with id "${e}".`);await N(e);let a=t||!o.protected;return a?(await O(i.REGISTRY,e),await O(i.DOWNLOAD_QUEUE,e)):await ke(e),d("deleted",{id:e,registryRemoved:a}),{id:e,registryRemoved:a}}async function se({removeRegistry:e=!1}={}){await S();let t=await P(i.REGISTRY);return Promise.all(t.map(o=>W(o,{removeRegistry:e})))}async function Se(e){let[t,o]=await Promise.all([p(i.REGISTRY,e),p(i.DOWNLOAD_QUEUE,e)]);if(!t)throw new Error(`retrieve: No registered file with id "${e}".`);if(!te.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 Ye={setDBInfo:ce,dbGetAllIds:P,registerFile:q,registerFiles:oe,startDownloads:z,stopDownloads:V,retryFailed:Z,isDownloading:J,abortDownload:N,abortAllDownloads:S,startMonitoring:ee,stopMonitoring:C,isOnline:x,isMonitoring:Q,retrieve:Se,view:ne,getStatus:re,isReady:ae,delete:W,deleteAll:se,on:M,off:L,once:Y,getStorageEstimate:h,requestPersistentStorage:j,isPersistentStorage:H},je=Ye;return ve(He);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offline-data-manager",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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
@@ -1,12 +1,15 @@
1
1
  export default OfflineDataManager;
2
2
  declare namespace OfflineDataManager {
3
3
  export { setDBInfo };
4
+ export { dbGetAllIds };
4
5
  export { registerFile };
5
6
  export { registerFiles };
6
- export { downloadFiles };
7
+ export { startDownloads };
8
+ export { stopDownloads };
9
+ export { retryFailed };
10
+ export { isDownloading };
7
11
  export { abortDownload };
8
12
  export { abortAllDownloads };
9
- export { resumeInterruptedDownloads };
10
13
  export { startMonitoring };
11
14
  export { stopMonitoring };
12
15
  export { isOnline };
@@ -26,10 +29,12 @@ declare namespace OfflineDataManager {
26
29
  }
27
30
  import { registerFile } from './registry.js';
28
31
  import { registerFiles } from './registry.js';
29
- 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';
30
36
  import { abortDownload } from './downloader.js';
31
37
  import { abortAllDownloads } from './downloader.js';
32
- import { resumeInterruptedDownloads } from './downloader.js';
33
38
  import { startMonitoring } from './downloader.js';
34
39
  import { stopMonitoring } from './downloader.js';
35
40
  import { isOnline } from './downloader.js';
@@ -62,4 +67,5 @@ import { getStorageEstimate } from './storage.js';
62
67
  import { requestPersistentStorage } from './storage.js';
63
68
  import { isPersistentStorage } from './storage.js';
64
69
  import { setDBInfo } from './db.js';
65
- export { registerFile, registerFiles, downloadFiles, abortDownload, abortAllDownloads, resumeInterruptedDownloads, startMonitoring, stopMonitoring, isOnline, isMonitoring, view, getStatus, isReady, deleteFile, deleteAllFiles, on, off, once, emit, getStorageEstimate, requestPersistentStorage, isPersistentStorage };
70
+ import { dbGetAllIds } from './db.js';
71
+ export { registerFile, registerFiles, startDownloads, stopDownloads, retryFailed, isDownloading, abortDownload, abortAllDownloads, startMonitoring, stopMonitoring, isOnline, isMonitoring, view, 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[]>;