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 +219 -150
- package/dist/esm/offline-data-manager.js +1 -1
- package/dist/umd/offline-data-manager.js +1 -1
- package/package.json +1 -1
- package/types/downloader.d.ts +37 -30
- package/types/index.d.ts +11 -7
- package/types/registry.d.ts +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
test/
|
|
58
|
+
index.html — Interactive test harness (imports from ../src/index.js)
|
|
59
59
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
71
|
+
## Quick start
|
|
100
72
|
|
|
101
|
-
```
|
|
102
|
-
src/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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` |
|
|
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
|
|
159
|
-
| `complete` |
|
|
160
|
-
| `expired` |
|
|
161
|
-
| `failed` | Exhausted all retries |
|
|
162
|
-
| `deferred` | Skipped due to insufficient storage; retried next
|
|
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
|
-
###
|
|
169
|
+
### Registration
|
|
169
170
|
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
177
|
+
const { registered, removed } = await ODM.registerFiles([...]);
|
|
178
|
+
```
|
|
175
179
|
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
+
#### `stopDownloads()`
|
|
200
|
+
Stops the loop gracefully. In-flight downloads are aborted and set to `paused`. Call `startDownloads()` again to resume.
|
|
185
201
|
|
|
186
|
-
|
|
202
|
+
```js
|
|
203
|
+
await ODM.stopDownloads();
|
|
204
|
+
```
|
|
187
205
|
|
|
188
|
-
|
|
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
|
-
|
|
213
|
+
#### `isDownloading()`
|
|
214
|
+
Returns `true` if the loop is currently running.
|
|
191
215
|
|
|
192
216
|
```js
|
|
193
|
-
|
|
217
|
+
ODM.isDownloading(); // → boolean
|
|
194
218
|
```
|
|
195
219
|
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
+
ODM.startMonitoring(); // call once, typically at startup before startDownloads()
|
|
242
|
+
ODM.stopMonitoring(); // remove listeners (e.g. in tests)
|
|
202
243
|
```
|
|
203
244
|
|
|
204
|
-
|
|
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
|
-
|
|
247
|
+
#### `isOnline()` / `isMonitoring()`
|
|
248
|
+
```js
|
|
249
|
+
ODM.isOnline(); // → boolean (navigator.onLine)
|
|
250
|
+
ODM.isMonitoring(); // → boolean
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
207
254
|
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
214
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
229
|
-
|
|
287
|
+
#### `getStatus(id)`
|
|
230
288
|
Full merged status for one file, or `null` if not registered.
|
|
231
289
|
|
|
232
|
-
|
|
290
|
+
#### `isReady(id)`
|
|
291
|
+
Returns `true` if the file has data available (`complete` or `expired`).
|
|
233
292
|
|
|
234
|
-
|
|
293
|
+
---
|
|
235
294
|
|
|
236
|
-
###
|
|
295
|
+
### Delete
|
|
237
296
|
|
|
297
|
+
#### `delete(id, options?)`
|
|
238
298
|
```js
|
|
239
|
-
await
|
|
240
|
-
await
|
|
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
|
-
|
|
244
|
-
|
|
303
|
+
#### `deleteAll(options?)`
|
|
245
304
|
```js
|
|
246
|
-
await
|
|
247
|
-
await
|
|
305
|
+
await ODM.deleteAll();
|
|
306
|
+
await ODM.deleteAll({ removeRegistry: true });
|
|
248
307
|
```
|
|
249
308
|
|
|
250
|
-
|
|
309
|
+
---
|
|
251
310
|
|
|
252
|
-
|
|
311
|
+
### Events
|
|
253
312
|
|
|
254
313
|
```js
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
329
|
+
---
|
|
260
330
|
|
|
261
|
-
###
|
|
331
|
+
### Storage
|
|
262
332
|
|
|
263
333
|
```js
|
|
264
|
-
|
|
265
|
-
|
|
334
|
+
const { usage, quota, available } = await ODM.getStorageEstimate();
|
|
335
|
+
await ODM.requestPersistentStorage();
|
|
336
|
+
await ODM.isPersistentStorage();
|
|
266
337
|
```
|
|
267
338
|
|
|
339
|
+
---
|
|
268
340
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
### `resumeInterruptedDownloads()`
|
|
341
|
+
### Service worker
|
|
272
342
|
|
|
273
|
-
|
|
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(
|
|
347
|
+
event.waitUntil(
|
|
348
|
+
(async () => {
|
|
349
|
+
ODM.startMonitoring();
|
|
350
|
+
ODM.startDownloads({ concurrency: 2 });
|
|
351
|
+
})()
|
|
352
|
+
);
|
|
278
353
|
});
|
|
279
354
|
```
|
|
280
355
|
|
|
281
|
-
|
|
356
|
+
---
|
|
282
357
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
366
|
+
The build script is zero-dependency — pure Node.js 18+, no Rollup or Webpack required.
|
|
298
367
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
- **
|
|
310
|
-
- **
|
|
311
|
-
- **
|
|
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
package/types/downloader.d.ts
CHANGED
|
@@ -1,50 +1,57 @@
|
|
|
1
|
+
/** Called by registerFile() and the connectivity monitor to wake the loop. */
|
|
2
|
+
export function _notifyNewWork(): void;
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Starts the persistent download loop.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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}
|
|
13
|
-
* @param {number}
|
|
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
|
|
17
|
+
export function startDownloads({ concurrency }?: {
|
|
19
18
|
concurrency?: number | undefined;
|
|
20
|
-
|
|
21
|
-
retryFailed?: boolean | undefined;
|
|
22
|
-
}): Promise<void>;
|
|
19
|
+
}): void;
|
|
23
20
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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.
|
|
47
|
-
*
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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,
|
|
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 };
|
package/types/registry.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
56
|
+
export function getAllStatus(): Promise<{
|
|
57
57
|
items: object[];
|
|
58
58
|
storage: object;
|
|
59
59
|
}>;
|