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