@serwist/background-sync 8.0.0
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 +1 -0
- package/dist/BackgroundSyncPlugin.d.ts +23 -0
- package/dist/BackgroundSyncPlugin.d.ts.map +1 -0
- package/dist/Queue.d.ts +170 -0
- package/dist/Queue.d.ts.map +1 -0
- package/dist/QueueDb.d.ts +90 -0
- package/dist/QueueDb.d.ts.map +1 -0
- package/dist/QueueStore.d.ts +75 -0
- package/dist/QueueStore.d.ts.map +1 -0
- package/dist/StorableRequest.d.ts +52 -0
- package/dist/StorableRequest.d.ts.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +707 -0
- package/dist/index.old.cjs +712 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import { assert, SerwistError, logger, getFriendlyURL } from '@serwist/core/internal';
|
|
2
|
+
import { openDB } from 'idb';
|
|
3
|
+
|
|
4
|
+
const DB_VERSION = 3;
|
|
5
|
+
const DB_NAME = "serwist-background-sync";
|
|
6
|
+
const REQUEST_OBJECT_STORE_NAME = "requests";
|
|
7
|
+
const QUEUE_NAME_INDEX = "queueName";
|
|
8
|
+
/**
|
|
9
|
+
* A class to interact directly an IndexedDB created specifically to save and
|
|
10
|
+
* retrieve QueueStoreEntries. This class encapsulates all the schema details
|
|
11
|
+
* to store the representation of a Queue.
|
|
12
|
+
*
|
|
13
|
+
* @private
|
|
14
|
+
*/ class QueueDb {
|
|
15
|
+
_db = null;
|
|
16
|
+
/**
|
|
17
|
+
* Add QueueStoreEntry to underlying db.
|
|
18
|
+
*
|
|
19
|
+
* @param entry
|
|
20
|
+
*/ async addEntry(entry) {
|
|
21
|
+
const db = await this.getDb();
|
|
22
|
+
const tx = db.transaction(REQUEST_OBJECT_STORE_NAME, "readwrite", {
|
|
23
|
+
durability: "relaxed"
|
|
24
|
+
});
|
|
25
|
+
await tx.store.add(entry);
|
|
26
|
+
await tx.done;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns the first entry id in the ObjectStore.
|
|
30
|
+
*
|
|
31
|
+
* @returns
|
|
32
|
+
*/ async getFirstEntryId() {
|
|
33
|
+
const db = await this.getDb();
|
|
34
|
+
const cursor = await db.transaction(REQUEST_OBJECT_STORE_NAME).store.openCursor();
|
|
35
|
+
return cursor?.value.id;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get all the entries filtered by index
|
|
39
|
+
*
|
|
40
|
+
* @param queueName
|
|
41
|
+
* @returns
|
|
42
|
+
*/ async getAllEntriesByQueueName(queueName) {
|
|
43
|
+
const db = await this.getDb();
|
|
44
|
+
const results = await db.getAllFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
|
|
45
|
+
return results ? results : new Array();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns the number of entries filtered by index
|
|
49
|
+
*
|
|
50
|
+
* @param queueName
|
|
51
|
+
* @returns
|
|
52
|
+
*/ async getEntryCountByQueueName(queueName) {
|
|
53
|
+
const db = await this.getDb();
|
|
54
|
+
return db.countFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Deletes a single entry by id.
|
|
58
|
+
*
|
|
59
|
+
* @param id the id of the entry to be deleted
|
|
60
|
+
*/ async deleteEntry(id) {
|
|
61
|
+
const db = await this.getDb();
|
|
62
|
+
await db.delete(REQUEST_OBJECT_STORE_NAME, id);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
*
|
|
66
|
+
* @param queueName
|
|
67
|
+
* @returns
|
|
68
|
+
*/ async getFirstEntryByQueueName(queueName) {
|
|
69
|
+
return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), "next");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
*
|
|
73
|
+
* @param queueName
|
|
74
|
+
* @returns
|
|
75
|
+
*/ async getLastEntryByQueueName(queueName) {
|
|
76
|
+
return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), "prev");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns either the first or the last entries, depending on direction.
|
|
80
|
+
* Filtered by index.
|
|
81
|
+
*
|
|
82
|
+
* @param direction
|
|
83
|
+
* @param query
|
|
84
|
+
* @returns
|
|
85
|
+
* @private
|
|
86
|
+
*/ async getEndEntryFromIndex(query, direction) {
|
|
87
|
+
const db = await this.getDb();
|
|
88
|
+
const cursor = await db.transaction(REQUEST_OBJECT_STORE_NAME).store.index(QUEUE_NAME_INDEX).openCursor(query, direction);
|
|
89
|
+
return cursor?.value;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns an open connection to the database.
|
|
93
|
+
*
|
|
94
|
+
* @private
|
|
95
|
+
*/ async getDb() {
|
|
96
|
+
if (!this._db) {
|
|
97
|
+
this._db = await openDB(DB_NAME, DB_VERSION, {
|
|
98
|
+
upgrade: this._upgradeDb
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return this._db;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Upgrades QueueDB
|
|
105
|
+
*
|
|
106
|
+
* @param db
|
|
107
|
+
* @param oldVersion
|
|
108
|
+
* @private
|
|
109
|
+
*/ _upgradeDb(db, oldVersion) {
|
|
110
|
+
if (oldVersion > 0 && oldVersion < DB_VERSION) {
|
|
111
|
+
if (db.objectStoreNames.contains(REQUEST_OBJECT_STORE_NAME)) {
|
|
112
|
+
db.deleteObjectStore(REQUEST_OBJECT_STORE_NAME);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const objStore = db.createObjectStore(REQUEST_OBJECT_STORE_NAME, {
|
|
116
|
+
autoIncrement: true,
|
|
117
|
+
keyPath: "id"
|
|
118
|
+
});
|
|
119
|
+
objStore.createIndex(QUEUE_NAME_INDEX, QUEUE_NAME_INDEX, {
|
|
120
|
+
unique: false
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A class to manage storing requests from a Queue in IndexedDB,
|
|
127
|
+
* indexed by their queue name for easier access.
|
|
128
|
+
*
|
|
129
|
+
* Most developers will not need to access this class directly;
|
|
130
|
+
* it is exposed for advanced use cases.
|
|
131
|
+
*/ class QueueStore {
|
|
132
|
+
_queueName;
|
|
133
|
+
_queueDb;
|
|
134
|
+
/**
|
|
135
|
+
* Associates this instance with a Queue instance, so entries added can be
|
|
136
|
+
* identified by their queue name.
|
|
137
|
+
*
|
|
138
|
+
* @param queueName
|
|
139
|
+
*/ constructor(queueName){
|
|
140
|
+
this._queueName = queueName;
|
|
141
|
+
this._queueDb = new QueueDb();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Append an entry last in the queue.
|
|
145
|
+
*
|
|
146
|
+
* @param entry
|
|
147
|
+
*/ async pushEntry(entry) {
|
|
148
|
+
if (process.env.NODE_ENV !== "production") {
|
|
149
|
+
assert.isType(entry, "object", {
|
|
150
|
+
moduleName: "@serwist/background-sync",
|
|
151
|
+
className: "QueueStore",
|
|
152
|
+
funcName: "pushEntry",
|
|
153
|
+
paramName: "entry"
|
|
154
|
+
});
|
|
155
|
+
assert.isType(entry.requestData, "object", {
|
|
156
|
+
moduleName: "@serwist/background-sync",
|
|
157
|
+
className: "QueueStore",
|
|
158
|
+
funcName: "pushEntry",
|
|
159
|
+
paramName: "entry.requestData"
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// Don't specify an ID since one is automatically generated.
|
|
163
|
+
delete entry.id;
|
|
164
|
+
entry.queueName = this._queueName;
|
|
165
|
+
await this._queueDb.addEntry(entry);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Prepend an entry first in the queue.
|
|
169
|
+
*
|
|
170
|
+
* @param entry
|
|
171
|
+
*/ async unshiftEntry(entry) {
|
|
172
|
+
if (process.env.NODE_ENV !== "production") {
|
|
173
|
+
assert.isType(entry, "object", {
|
|
174
|
+
moduleName: "@serwist/background-sync",
|
|
175
|
+
className: "QueueStore",
|
|
176
|
+
funcName: "unshiftEntry",
|
|
177
|
+
paramName: "entry"
|
|
178
|
+
});
|
|
179
|
+
assert.isType(entry.requestData, "object", {
|
|
180
|
+
moduleName: "@serwist/background-sync",
|
|
181
|
+
className: "QueueStore",
|
|
182
|
+
funcName: "unshiftEntry",
|
|
183
|
+
paramName: "entry.requestData"
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const firstId = await this._queueDb.getFirstEntryId();
|
|
187
|
+
if (firstId) {
|
|
188
|
+
// Pick an ID one less than the lowest ID in the object store.
|
|
189
|
+
entry.id = firstId - 1;
|
|
190
|
+
} else {
|
|
191
|
+
// Otherwise let the auto-incrementor assign the ID.
|
|
192
|
+
delete entry.id;
|
|
193
|
+
}
|
|
194
|
+
entry.queueName = this._queueName;
|
|
195
|
+
await this._queueDb.addEntry(entry);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Removes and returns the last entry in the queue matching the `queueName`.
|
|
199
|
+
*
|
|
200
|
+
* @returns
|
|
201
|
+
*/ async popEntry() {
|
|
202
|
+
return this._removeEntry(await this._queueDb.getLastEntryByQueueName(this._queueName));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Removes and returns the first entry in the queue matching the `queueName`.
|
|
206
|
+
*
|
|
207
|
+
* @returns
|
|
208
|
+
*/ async shiftEntry() {
|
|
209
|
+
return this._removeEntry(await this._queueDb.getFirstEntryByQueueName(this._queueName));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Returns all entries in the store matching the `queueName`.
|
|
213
|
+
*
|
|
214
|
+
* @returns
|
|
215
|
+
*/ async getAll() {
|
|
216
|
+
return await this._queueDb.getAllEntriesByQueueName(this._queueName);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Returns the number of entries in the store matching the `queueName`.
|
|
220
|
+
*
|
|
221
|
+
* @returns
|
|
222
|
+
*/ async size() {
|
|
223
|
+
return await this._queueDb.getEntryCountByQueueName(this._queueName);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Deletes the entry for the given ID.
|
|
227
|
+
*
|
|
228
|
+
* WARNING: this method does not ensure the deleted entry belongs to this
|
|
229
|
+
* queue (i.e. matches the `queueName`). But this limitation is acceptable
|
|
230
|
+
* as this class is not publicly exposed. An additional check would make
|
|
231
|
+
* this method slower than it needs to be.
|
|
232
|
+
*
|
|
233
|
+
* @param id
|
|
234
|
+
*/ async deleteEntry(id) {
|
|
235
|
+
await this._queueDb.deleteEntry(id);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Removes and returns the first or last entry in the queue (based on the
|
|
239
|
+
* `direction` argument) matching the `queueName`.
|
|
240
|
+
*
|
|
241
|
+
* @returns
|
|
242
|
+
* @private
|
|
243
|
+
*/ async _removeEntry(entry) {
|
|
244
|
+
if (entry) {
|
|
245
|
+
await this.deleteEntry(entry.id);
|
|
246
|
+
}
|
|
247
|
+
return entry;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const serializableProperties = [
|
|
252
|
+
"method",
|
|
253
|
+
"referrer",
|
|
254
|
+
"referrerPolicy",
|
|
255
|
+
"mode",
|
|
256
|
+
"credentials",
|
|
257
|
+
"cache",
|
|
258
|
+
"redirect",
|
|
259
|
+
"integrity",
|
|
260
|
+
"keepalive"
|
|
261
|
+
];
|
|
262
|
+
/**
|
|
263
|
+
* A class to make it easier to serialize and de-serialize requests so they
|
|
264
|
+
* can be stored in IndexedDB.
|
|
265
|
+
*
|
|
266
|
+
* Most developers will not need to access this class directly;
|
|
267
|
+
* it is exposed for advanced use cases.
|
|
268
|
+
*/ class StorableRequest {
|
|
269
|
+
_requestData;
|
|
270
|
+
/**
|
|
271
|
+
* Converts a Request object to a plain object that can be structured
|
|
272
|
+
* cloned or JSON-stringified.
|
|
273
|
+
*
|
|
274
|
+
* @param request
|
|
275
|
+
* @returns
|
|
276
|
+
*/ static async fromRequest(request) {
|
|
277
|
+
const requestData = {
|
|
278
|
+
url: request.url,
|
|
279
|
+
headers: {}
|
|
280
|
+
};
|
|
281
|
+
// Set the body if present.
|
|
282
|
+
if (request.method !== "GET") {
|
|
283
|
+
// Use ArrayBuffer to support non-text request bodies.
|
|
284
|
+
// NOTE: we can't use Blobs becuse Safari doesn't support storing
|
|
285
|
+
// Blobs in IndexedDB in some cases:
|
|
286
|
+
// https://github.com/dfahlander/Dexie.js/issues/618#issuecomment-398348457
|
|
287
|
+
requestData.body = await request.clone().arrayBuffer();
|
|
288
|
+
}
|
|
289
|
+
request.headers.forEach((value, key)=>{
|
|
290
|
+
requestData.headers[key] = value;
|
|
291
|
+
});
|
|
292
|
+
// Add all other serializable request properties
|
|
293
|
+
for (const prop of serializableProperties){
|
|
294
|
+
if (request[prop] !== undefined) {
|
|
295
|
+
requestData[prop] = request[prop];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return new StorableRequest(requestData);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Accepts an object of request data that can be used to construct a
|
|
302
|
+
* `Request` but can also be stored in IndexedDB.
|
|
303
|
+
*
|
|
304
|
+
* @param requestData An object of request data that includes the `url` plus any relevant properties of
|
|
305
|
+
* [requestInit](https://fetch.spec.whatwg.org/#requestinit).
|
|
306
|
+
*/ constructor(requestData){
|
|
307
|
+
if (process.env.NODE_ENV !== "production") {
|
|
308
|
+
assert.isType(requestData, "object", {
|
|
309
|
+
moduleName: "@serwist/background-sync",
|
|
310
|
+
className: "StorableRequest",
|
|
311
|
+
funcName: "constructor",
|
|
312
|
+
paramName: "requestData"
|
|
313
|
+
});
|
|
314
|
+
assert.isType(requestData.url, "string", {
|
|
315
|
+
moduleName: "@serwist/background-sync",
|
|
316
|
+
className: "StorableRequest",
|
|
317
|
+
funcName: "constructor",
|
|
318
|
+
paramName: "requestData.url"
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// If the request's mode is `navigate`, convert it to `same-origin` since
|
|
322
|
+
// navigation requests can't be constructed via script.
|
|
323
|
+
if (requestData["mode"] === "navigate") {
|
|
324
|
+
requestData["mode"] = "same-origin";
|
|
325
|
+
}
|
|
326
|
+
this._requestData = requestData;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Returns a deep clone of the instances `_requestData` object.
|
|
330
|
+
*
|
|
331
|
+
* @returns
|
|
332
|
+
*/ toObject() {
|
|
333
|
+
const requestData = Object.assign({}, this._requestData);
|
|
334
|
+
requestData.headers = Object.assign({}, this._requestData.headers);
|
|
335
|
+
if (requestData.body) {
|
|
336
|
+
requestData.body = requestData.body.slice(0);
|
|
337
|
+
}
|
|
338
|
+
return requestData;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Converts this instance to a Request.
|
|
342
|
+
*
|
|
343
|
+
* @returns
|
|
344
|
+
*/ toRequest() {
|
|
345
|
+
return new Request(this._requestData.url, this._requestData);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Creates and returns a deep clone of the instance.
|
|
349
|
+
*
|
|
350
|
+
* @returns
|
|
351
|
+
*/ clone() {
|
|
352
|
+
return new StorableRequest(this.toObject());
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const TAG_PREFIX = "serwist-background-sync";
|
|
357
|
+
const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes
|
|
358
|
+
const queueNames = new Set();
|
|
359
|
+
/**
|
|
360
|
+
* Converts a QueueStore entry into the format exposed by Queue. This entails
|
|
361
|
+
* converting the request data into a real request and omitting the `id` and
|
|
362
|
+
* `queueName` properties.
|
|
363
|
+
*
|
|
364
|
+
* @param queueStoreEntry
|
|
365
|
+
* @returns
|
|
366
|
+
* @private
|
|
367
|
+
*/ const convertEntry = (queueStoreEntry)=>{
|
|
368
|
+
const queueEntry = {
|
|
369
|
+
request: new StorableRequest(queueStoreEntry.requestData).toRequest(),
|
|
370
|
+
timestamp: queueStoreEntry.timestamp
|
|
371
|
+
};
|
|
372
|
+
if (queueStoreEntry.metadata) {
|
|
373
|
+
queueEntry.metadata = queueStoreEntry.metadata;
|
|
374
|
+
}
|
|
375
|
+
return queueEntry;
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* A class to manage storing failed requests in IndexedDB and retrying them
|
|
379
|
+
* later. All parts of the storing and replaying process are observable via
|
|
380
|
+
* callbacks.
|
|
381
|
+
*/ class Queue {
|
|
382
|
+
_name;
|
|
383
|
+
_onSync;
|
|
384
|
+
_maxRetentionTime;
|
|
385
|
+
_queueStore;
|
|
386
|
+
_forceSyncFallback;
|
|
387
|
+
_syncInProgress = false;
|
|
388
|
+
_requestsAddedDuringSync = false;
|
|
389
|
+
/**
|
|
390
|
+
* Creates an instance of Queue with the given options
|
|
391
|
+
*
|
|
392
|
+
* @param name The unique name for this queue. This name must be
|
|
393
|
+
* unique as it's used to register sync events and store requests
|
|
394
|
+
* in IndexedDB specific to this instance. An error will be thrown if
|
|
395
|
+
* a duplicate name is detected.
|
|
396
|
+
* @param options
|
|
397
|
+
*/ constructor(name, { forceSyncFallback, onSync, maxRetentionTime } = {}){
|
|
398
|
+
// Ensure the store name is not already being used
|
|
399
|
+
if (queueNames.has(name)) {
|
|
400
|
+
throw new SerwistError("duplicate-queue-name", {
|
|
401
|
+
name
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
queueNames.add(name);
|
|
405
|
+
}
|
|
406
|
+
this._name = name;
|
|
407
|
+
this._onSync = onSync || this.replayRequests;
|
|
408
|
+
this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME;
|
|
409
|
+
this._forceSyncFallback = Boolean(forceSyncFallback);
|
|
410
|
+
this._queueStore = new QueueStore(this._name);
|
|
411
|
+
this._addSyncListener();
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* @returns
|
|
415
|
+
*/ get name() {
|
|
416
|
+
return this._name;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Stores the passed request in IndexedDB (with its timestamp and any
|
|
420
|
+
* metadata) at the end of the queue.
|
|
421
|
+
*
|
|
422
|
+
* @param entry
|
|
423
|
+
*/ async pushRequest(entry) {
|
|
424
|
+
if (process.env.NODE_ENV !== "production") {
|
|
425
|
+
assert.isType(entry, "object", {
|
|
426
|
+
moduleName: "@serwist/background-sync",
|
|
427
|
+
className: "Queue",
|
|
428
|
+
funcName: "pushRequest",
|
|
429
|
+
paramName: "entry"
|
|
430
|
+
});
|
|
431
|
+
assert.isInstance(entry.request, Request, {
|
|
432
|
+
moduleName: "@serwist/background-sync",
|
|
433
|
+
className: "Queue",
|
|
434
|
+
funcName: "pushRequest",
|
|
435
|
+
paramName: "entry.request"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
await this._addRequest(entry, "push");
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Stores the passed request in IndexedDB (with its timestamp and any
|
|
442
|
+
* metadata) at the beginning of the queue.
|
|
443
|
+
*
|
|
444
|
+
* @param entry
|
|
445
|
+
*/ async unshiftRequest(entry) {
|
|
446
|
+
if (process.env.NODE_ENV !== "production") {
|
|
447
|
+
assert.isType(entry, "object", {
|
|
448
|
+
moduleName: "@serwist/background-sync",
|
|
449
|
+
className: "Queue",
|
|
450
|
+
funcName: "unshiftRequest",
|
|
451
|
+
paramName: "entry"
|
|
452
|
+
});
|
|
453
|
+
assert.isInstance(entry.request, Request, {
|
|
454
|
+
moduleName: "@serwist/background-sync",
|
|
455
|
+
className: "Queue",
|
|
456
|
+
funcName: "unshiftRequest",
|
|
457
|
+
paramName: "entry.request"
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
await this._addRequest(entry, "unshift");
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Removes and returns the last request in the queue (along with its
|
|
464
|
+
* timestamp and any metadata). The returned object takes the form:
|
|
465
|
+
* `{request, timestamp, metadata}`.
|
|
466
|
+
*
|
|
467
|
+
* @returns
|
|
468
|
+
*/ async popRequest() {
|
|
469
|
+
return this._removeRequest("pop");
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Removes and returns the first request in the queue (along with its
|
|
473
|
+
* timestamp and any metadata). The returned object takes the form:
|
|
474
|
+
* `{request, timestamp, metadata}`.
|
|
475
|
+
*
|
|
476
|
+
* @returns
|
|
477
|
+
*/ async shiftRequest() {
|
|
478
|
+
return this._removeRequest("shift");
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Returns all the entries that have not expired (per `maxRetentionTime`).
|
|
482
|
+
* Any expired entries are removed from the queue.
|
|
483
|
+
*
|
|
484
|
+
* @returns
|
|
485
|
+
*/ async getAll() {
|
|
486
|
+
const allEntries = await this._queueStore.getAll();
|
|
487
|
+
const now = Date.now();
|
|
488
|
+
const unexpiredEntries = [];
|
|
489
|
+
for (const entry of allEntries){
|
|
490
|
+
// Ignore requests older than maxRetentionTime. Call this function
|
|
491
|
+
// recursively until an unexpired request is found.
|
|
492
|
+
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
|
|
493
|
+
if (now - entry.timestamp > maxRetentionTimeInMs) {
|
|
494
|
+
await this._queueStore.deleteEntry(entry.id);
|
|
495
|
+
} else {
|
|
496
|
+
unexpiredEntries.push(convertEntry(entry));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return unexpiredEntries;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Returns the number of entries present in the queue.
|
|
503
|
+
* Note that expired entries (per `maxRetentionTime`) are also included in this count.
|
|
504
|
+
*
|
|
505
|
+
* @returns
|
|
506
|
+
*/ async size() {
|
|
507
|
+
return await this._queueStore.size();
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Adds the entry to the QueueStore and registers for a sync event.
|
|
511
|
+
*
|
|
512
|
+
* @param entry
|
|
513
|
+
* @param operation
|
|
514
|
+
* @private
|
|
515
|
+
*/ async _addRequest({ request, metadata, timestamp = Date.now() }, operation) {
|
|
516
|
+
const storableRequest = await StorableRequest.fromRequest(request.clone());
|
|
517
|
+
const entry = {
|
|
518
|
+
requestData: storableRequest.toObject(),
|
|
519
|
+
timestamp
|
|
520
|
+
};
|
|
521
|
+
// Only include metadata if it's present.
|
|
522
|
+
if (metadata) {
|
|
523
|
+
entry.metadata = metadata;
|
|
524
|
+
}
|
|
525
|
+
switch(operation){
|
|
526
|
+
case "push":
|
|
527
|
+
await this._queueStore.pushEntry(entry);
|
|
528
|
+
break;
|
|
529
|
+
case "unshift":
|
|
530
|
+
await this._queueStore.unshiftEntry(entry);
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
if (process.env.NODE_ENV !== "production") {
|
|
534
|
+
logger.log(`Request for '${getFriendlyURL(request.url)}' has ` + `been added to background sync queue '${this._name}'.`);
|
|
535
|
+
}
|
|
536
|
+
// Don't register for a sync if we're in the middle of a sync. Instead,
|
|
537
|
+
// we wait until the sync is complete and call register if
|
|
538
|
+
// `this._requestsAddedDuringSync` is true.
|
|
539
|
+
if (this._syncInProgress) {
|
|
540
|
+
this._requestsAddedDuringSync = true;
|
|
541
|
+
} else {
|
|
542
|
+
await this.registerSync();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Removes and returns the first or last (depending on `operation`) entry
|
|
547
|
+
* from the QueueStore that's not older than the `maxRetentionTime`.
|
|
548
|
+
*
|
|
549
|
+
* @param operation
|
|
550
|
+
* @returns
|
|
551
|
+
* @private
|
|
552
|
+
*/ async _removeRequest(operation) {
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
let entry;
|
|
555
|
+
switch(operation){
|
|
556
|
+
case "pop":
|
|
557
|
+
entry = await this._queueStore.popEntry();
|
|
558
|
+
break;
|
|
559
|
+
case "shift":
|
|
560
|
+
entry = await this._queueStore.shiftEntry();
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
if (entry) {
|
|
564
|
+
// Ignore requests older than maxRetentionTime. Call this function
|
|
565
|
+
// recursively until an unexpired request is found.
|
|
566
|
+
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
|
|
567
|
+
if (now - entry.timestamp > maxRetentionTimeInMs) {
|
|
568
|
+
return this._removeRequest(operation);
|
|
569
|
+
}
|
|
570
|
+
return convertEntry(entry);
|
|
571
|
+
} else {
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Loops through each request in the queue and attempts to re-fetch it.
|
|
577
|
+
* If any request fails to re-fetch, it's put back in the same position in
|
|
578
|
+
* the queue (which registers a retry for the next sync event).
|
|
579
|
+
*/ async replayRequests() {
|
|
580
|
+
let entry;
|
|
581
|
+
while(entry = await this.shiftRequest()){
|
|
582
|
+
try {
|
|
583
|
+
await fetch(entry.request.clone());
|
|
584
|
+
if (process.env.NODE_ENV !== "production") {
|
|
585
|
+
logger.log(`Request for '${getFriendlyURL(entry.request.url)}' ` + `has been replayed in queue '${this._name}'`);
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
await this.unshiftRequest(entry);
|
|
589
|
+
if (process.env.NODE_ENV !== "production") {
|
|
590
|
+
logger.log(`Request for '${getFriendlyURL(entry.request.url)}' ` + `failed to replay, putting it back in queue '${this._name}'`);
|
|
591
|
+
}
|
|
592
|
+
throw new SerwistError("queue-replay-failed", {
|
|
593
|
+
name: this._name
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (process.env.NODE_ENV !== "production") {
|
|
598
|
+
logger.log(`All requests in queue '${this.name}' have successfully ` + `replayed; the queue is now empty!`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Registers a sync event with a tag unique to this instance.
|
|
603
|
+
*/ async registerSync() {
|
|
604
|
+
// See https://github.com/GoogleChrome/workbox/issues/2393
|
|
605
|
+
if ("sync" in self.registration && !this._forceSyncFallback) {
|
|
606
|
+
try {
|
|
607
|
+
await self.registration.sync.register(`${TAG_PREFIX}:${this._name}`);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
// This means the registration failed for some reason, possibly due to
|
|
610
|
+
// the user disabling it.
|
|
611
|
+
if (process.env.NODE_ENV !== "production") {
|
|
612
|
+
logger.warn(`Unable to register sync event for '${this._name}'.`, err);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* In sync-supporting browsers, this adds a listener for the sync event.
|
|
619
|
+
* In non-sync-supporting browsers, or if _forceSyncFallback is true, this
|
|
620
|
+
* will retry the queue on service worker startup.
|
|
621
|
+
*
|
|
622
|
+
* @private
|
|
623
|
+
*/ _addSyncListener() {
|
|
624
|
+
// See https://github.com/GoogleChrome/workbox/issues/2393
|
|
625
|
+
if ("sync" in self.registration && !this._forceSyncFallback) {
|
|
626
|
+
self.addEventListener("sync", (event)=>{
|
|
627
|
+
if (event.tag === `${TAG_PREFIX}:${this._name}`) {
|
|
628
|
+
if (process.env.NODE_ENV !== "production") {
|
|
629
|
+
logger.log(`Background sync for tag '${event.tag}' ` + `has been received`);
|
|
630
|
+
}
|
|
631
|
+
const syncComplete = async ()=>{
|
|
632
|
+
this._syncInProgress = true;
|
|
633
|
+
let syncError;
|
|
634
|
+
try {
|
|
635
|
+
await this._onSync({
|
|
636
|
+
queue: this
|
|
637
|
+
});
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (error instanceof Error) {
|
|
640
|
+
syncError = error;
|
|
641
|
+
// Rethrow the error. Note: the logic in the finally clause
|
|
642
|
+
// will run before this gets rethrown.
|
|
643
|
+
throw syncError;
|
|
644
|
+
}
|
|
645
|
+
} finally{
|
|
646
|
+
// New items may have been added to the queue during the sync,
|
|
647
|
+
// so we need to register for a new sync if that's happened...
|
|
648
|
+
// Unless there was an error during the sync, in which
|
|
649
|
+
// case the browser will automatically retry later, as long
|
|
650
|
+
// as `event.lastChance` is not true.
|
|
651
|
+
if (this._requestsAddedDuringSync && !(syncError && !event.lastChance)) {
|
|
652
|
+
await this.registerSync();
|
|
653
|
+
}
|
|
654
|
+
this._syncInProgress = false;
|
|
655
|
+
this._requestsAddedDuringSync = false;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
event.waitUntil(syncComplete());
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
} else {
|
|
662
|
+
if (process.env.NODE_ENV !== "production") {
|
|
663
|
+
logger.log(`Background sync replaying without background sync event`);
|
|
664
|
+
}
|
|
665
|
+
// If the browser doesn't support background sync, or the developer has
|
|
666
|
+
// opted-in to not using it, retry every time the service worker starts up
|
|
667
|
+
// as a fallback.
|
|
668
|
+
void this._onSync({
|
|
669
|
+
queue: this
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Returns the set of queue names. This is primarily used to reset the list
|
|
675
|
+
* of queue names in tests.
|
|
676
|
+
*
|
|
677
|
+
* @returns
|
|
678
|
+
* @private
|
|
679
|
+
*/ static get _queueNames() {
|
|
680
|
+
return queueNames;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* A class implementing the `fetchDidFail` lifecycle callback. This makes it
|
|
686
|
+
* easier to add failed requests to a background sync Queue.
|
|
687
|
+
*/ class BackgroundSyncPlugin {
|
|
688
|
+
_queue;
|
|
689
|
+
/**
|
|
690
|
+
* @param name See the `@serwist/background-sync.Queue`
|
|
691
|
+
* documentation for parameter details.
|
|
692
|
+
* @param options See the `@serwist/background-sync.Queue`
|
|
693
|
+
* documentation for parameter details.
|
|
694
|
+
*/ constructor(name, options){
|
|
695
|
+
this._queue = new Queue(name, options);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* @param options
|
|
699
|
+
* @private
|
|
700
|
+
*/ fetchDidFail = async ({ request })=>{
|
|
701
|
+
await this._queue.pushRequest({
|
|
702
|
+
request
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export { BackgroundSyncPlugin, Queue, QueueStore, StorableRequest };
|