UNPKG

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