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