@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/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 };