UNPKG

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