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 +221 -148
- 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 -5
- package/types/registry.d.ts +1 -1
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.
|
|
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
|
-
|
|
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,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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
209
|
+
```js
|
|
210
|
+
await ODM.retryFailed();
|
|
211
|
+
```
|
|
185
212
|
|
|
186
|
-
|
|
213
|
+
#### `isDownloading()`
|
|
214
|
+
Returns `true` if the loop is currently running.
|
|
187
215
|
|
|
188
216
|
```js
|
|
189
|
-
|
|
217
|
+
ODM.isDownloading(); // → boolean
|
|
190
218
|
```
|
|
191
219
|
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
+
ODM.startMonitoring(); // call once, typically at startup before startDownloads()
|
|
242
|
+
ODM.stopMonitoring(); // remove listeners (e.g. in tests)
|
|
198
243
|
```
|
|
199
244
|
|
|
200
|
-
|
|
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
|
-
|
|
247
|
+
#### `isOnline()` / `isMonitoring()`
|
|
248
|
+
```js
|
|
249
|
+
ODM.isOnline(); // → boolean (navigator.onLine)
|
|
250
|
+
ODM.isMonitoring(); // → boolean
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### Retrieve
|
|
203
256
|
|
|
204
|
-
|
|
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
|
|
261
|
+
const { data, mimeType } = await ODM.retrieve('poi-data');
|
|
208
262
|
|
|
209
|
-
|
|
210
|
-
const text =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
225
|
-
|
|
287
|
+
#### `getStatus(id)`
|
|
226
288
|
Full merged status for one file, or `null` if not registered.
|
|
227
289
|
|
|
228
|
-
|
|
290
|
+
#### `isReady(id)`
|
|
291
|
+
Returns `true` if the file has data available (`complete` or `expired`).
|
|
229
292
|
|
|
230
|
-
|
|
293
|
+
---
|
|
231
294
|
|
|
232
|
-
###
|
|
295
|
+
### Delete
|
|
233
296
|
|
|
297
|
+
#### `delete(id, options?)`
|
|
234
298
|
```js
|
|
235
|
-
await
|
|
236
|
-
await
|
|
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
|
-
|
|
240
|
-
|
|
303
|
+
#### `deleteAll(options?)`
|
|
241
304
|
```js
|
|
242
|
-
await
|
|
243
|
-
await
|
|
305
|
+
await ODM.deleteAll();
|
|
306
|
+
await ODM.deleteAll({ removeRegistry: true });
|
|
244
307
|
```
|
|
245
308
|
|
|
246
|
-
|
|
309
|
+
---
|
|
247
310
|
|
|
248
|
-
|
|
311
|
+
### Events
|
|
249
312
|
|
|
250
313
|
```js
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
329
|
+
---
|
|
256
330
|
|
|
257
|
-
###
|
|
331
|
+
### Storage
|
|
258
332
|
|
|
259
333
|
```js
|
|
260
|
-
|
|
261
|
-
|
|
334
|
+
const { usage, quota, available } = await ODM.getStorageEstimate();
|
|
335
|
+
await ODM.requestPersistentStorage();
|
|
336
|
+
await ODM.isPersistentStorage();
|
|
262
337
|
```
|
|
263
338
|
|
|
339
|
+
---
|
|
264
340
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
### `resumeInterruptedDownloads()`
|
|
341
|
+
### Service worker
|
|
268
342
|
|
|
269
|
-
|
|
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(
|
|
347
|
+
event.waitUntil(
|
|
348
|
+
(async () => {
|
|
349
|
+
ODM.startMonitoring();
|
|
350
|
+
ODM.startDownloads({ concurrency: 2 });
|
|
351
|
+
})()
|
|
352
|
+
);
|
|
274
353
|
});
|
|
275
354
|
```
|
|
276
355
|
|
|
277
|
-
|
|
356
|
+
---
|
|
278
357
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
366
|
+
The build script is zero-dependency — pure Node.js 18+, no Rollup or Webpack required.
|
|
294
367
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
- **
|
|
306
|
-
- **
|
|
307
|
-
- **
|
|
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
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
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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 };
|
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[]>;
|