UNPKG

23.8 kBJavaScriptView Raw
1this.workbox = this.workbox || {};
2this.workbox.expiration = (function (exports,DBWrapper_mjs,WorkboxError_mjs,assert_mjs,logger_mjs,cacheNames_mjs,index_mjs) {
3 'use strict';
4
5 try {
6 self.workbox.v['workbox:cache-expiration:3.6.3'] = 1;
7 } catch (e) {} // eslint-disable-line
8
9 /*
10 Copyright 2017 Google Inc.
11
12 Licensed under the Apache License, Version 2.0 (the "License");
13 you may not use this file except in compliance with the License.
14 You may obtain a copy of the License at
15
16 https://www.apache.org/licenses/LICENSE-2.0
17
18 Unless required by applicable law or agreed to in writing, software
19 distributed under the License is distributed on an "AS IS" BASIS,
20 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 See the License for the specific language governing permissions and
22 limitations under the License.
23 */
24
25 const URL_KEY = 'url';
26 const TIMESTAMP_KEY = 'timestamp';
27
28 /**
29 * Returns the timestamp model.
30 *
31 * @private
32 */
33 class CacheTimestampsModel {
34 /**
35 *
36 * @param {string} cacheName
37 *
38 * @private
39 */
40 constructor(cacheName) {
41 // TODO Check cacheName
42
43 this._cacheName = cacheName;
44 this._storeName = cacheName;
45
46 this._db = new DBWrapper_mjs.DBWrapper(this._cacheName, 2, {
47 onupgradeneeded: evt => this._handleUpgrade(evt)
48 });
49 }
50
51 /**
52 * Should perform an upgrade of indexedDB.
53 *
54 * @param {Event} evt
55 *
56 * @private
57 */
58 _handleUpgrade(evt) {
59 const db = evt.target.result;
60 if (evt.oldVersion < 2) {
61 // Remove old databases.
62 if (db.objectStoreNames.contains('workbox-cache-expiration')) {
63 db.deleteObjectStore('workbox-cache-expiration');
64 }
65 }
66
67 db.createObjectStore(this._storeName, { keyPath: URL_KEY }).createIndex(TIMESTAMP_KEY, TIMESTAMP_KEY, { unique: false });
68 }
69
70 /**
71 * @param {string} url
72 * @param {number} timestamp
73 *
74 * @private
75 */
76 setTimestamp(url, timestamp) {
77 var _this = this;
78
79 return babelHelpers.asyncToGenerator(function* () {
80 yield _this._db.put(_this._storeName, {
81 [URL_KEY]: new URL(url, location).href,
82 [TIMESTAMP_KEY]: timestamp
83 });
84 })();
85 }
86
87 /**
88 * Get all of the timestamps in the indexedDB.
89 *
90 * @return {Array<Objects>}
91 *
92 * @private
93 */
94 getAllTimestamps() {
95 var _this2 = this;
96
97 return babelHelpers.asyncToGenerator(function* () {
98 return yield _this2._db.getAllMatching(_this2._storeName, {
99 index: TIMESTAMP_KEY
100 });
101 })();
102 }
103
104 /**
105 * Returns the timestamp stored for a given URL.
106 *
107 * @param {string} url
108 * @return {number}
109 *
110 * @private
111 */
112 getTimestamp(url) {
113 var _this3 = this;
114
115 return babelHelpers.asyncToGenerator(function* () {
116 const timestampObject = yield _this3._db.get(_this3._storeName, url);
117 return timestampObject.timestamp;
118 })();
119 }
120
121 /**
122 * @param {string} url
123 *
124 * @private
125 */
126 deleteUrl(url) {
127 var _this4 = this;
128
129 return babelHelpers.asyncToGenerator(function* () {
130 yield _this4._db.delete(_this4._storeName, new URL(url, location).href);
131 })();
132 }
133
134 /**
135 * Removes the underlying IndexedDB object store entirely.
136 */
137 delete() {
138 var _this5 = this;
139
140 return babelHelpers.asyncToGenerator(function* () {
141 yield _this5._db.deleteDatabase();
142 _this5._db = null;
143 })();
144 }
145 }
146
147 /*
148 Copyright 2017 Google Inc.
149
150 Licensed under the Apache License, Version 2.0 (the "License");
151 you may not use this file except in compliance with the License.
152 You may obtain a copy of the License at
153
154 https://www.apache.org/licenses/LICENSE-2.0
155
156 Unless required by applicable law or agreed to in writing, software
157 distributed under the License is distributed on an "AS IS" BASIS,
158 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
159 See the License for the specific language governing permissions and
160 limitations under the License.
161 */
162
163 /**
164 * The `CacheExpiration` class allows you define an expiration and / or
165 * limit on the number of responses stored in a
166 * [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
167 *
168 * @memberof workbox.expiration
169 */
170 class CacheExpiration {
171 /**
172 * To construct a new CacheExpiration instance you must provide at least
173 * one of the `config` properties.
174 *
175 * @param {string} cacheName Name of the cache to apply restrictions to.
176 * @param {Object} config
177 * @param {number} [config.maxEntries] The maximum number of entries to cache.
178 * Entries used the least will be removed as the maximum is reached.
179 * @param {number} [config.maxAgeSeconds] The maximum age of an entry before
180 * it's treated as stale and removed.
181 */
182 constructor(cacheName, config = {}) {
183 {
184 assert_mjs.assert.isType(cacheName, 'string', {
185 moduleName: 'workbox-cache-expiration',
186 className: 'CacheExpiration',
187 funcName: 'constructor',
188 paramName: 'cacheName'
189 });
190
191 if (!(config.maxEntries || config.maxAgeSeconds)) {
192 throw new WorkboxError_mjs.WorkboxError('max-entries-or-age-required', {
193 moduleName: 'workbox-cache-expiration',
194 className: 'CacheExpiration',
195 funcName: 'constructor'
196 });
197 }
198
199 if (config.maxEntries) {
200 assert_mjs.assert.isType(config.maxEntries, 'number', {
201 moduleName: 'workbox-cache-expiration',
202 className: 'CacheExpiration',
203 funcName: 'constructor',
204 paramName: 'config.maxEntries'
205 });
206
207 // TODO: Assert is positive
208 }
209
210 if (config.maxAgeSeconds) {
211 assert_mjs.assert.isType(config.maxAgeSeconds, 'number', {
212 moduleName: 'workbox-cache-expiration',
213 className: 'CacheExpiration',
214 funcName: 'constructor',
215 paramName: 'config.maxAgeSeconds'
216 });
217
218 // TODO: Assert is positive
219 }
220 }
221
222 this._isRunning = false;
223 this._rerunRequested = false;
224 this._maxEntries = config.maxEntries;
225 this._maxAgeSeconds = config.maxAgeSeconds;
226 this._cacheName = cacheName;
227 this._timestampModel = new CacheTimestampsModel(cacheName);
228 }
229
230 /**
231 * Expires entries for the given cache and given criteria.
232 */
233 expireEntries() {
234 var _this = this;
235
236 return babelHelpers.asyncToGenerator(function* () {
237 if (_this._isRunning) {
238 _this._rerunRequested = true;
239 return;
240 }
241 _this._isRunning = true;
242
243 const now = Date.now();
244
245 // First, expire old entries, if maxAgeSeconds is set.
246 const oldEntries = yield _this._findOldEntries(now);
247
248 // Once that's done, check for the maximum size.
249 const extraEntries = yield _this._findExtraEntries();
250
251 // Use a Set to remove any duplicates following the concatenation, then
252 // convert back into an array.
253 const allUrls = [...new Set(oldEntries.concat(extraEntries))];
254
255 yield Promise.all([_this._deleteFromCache(allUrls), _this._deleteFromIDB(allUrls)]);
256
257 {
258 // TODO: break apart entries deleted due to expiration vs size restraints
259 if (allUrls.length > 0) {
260 logger_mjs.logger.groupCollapsed(`Expired ${allUrls.length} ` + `${allUrls.length === 1 ? 'entry' : 'entries'} and removed ` + `${allUrls.length === 1 ? 'it' : 'them'} from the ` + `'${_this._cacheName}' cache.`);
261 logger_mjs.logger.log(`Expired the following ${allUrls.length === 1 ? 'URL' : 'URLs'}:`);
262 allUrls.forEach(function (url) {
263 return logger_mjs.logger.log(` ${url}`);
264 });
265 logger_mjs.logger.groupEnd();
266 } else {
267 logger_mjs.logger.debug(`Cache expiration ran and found no entries to remove.`);
268 }
269 }
270
271 _this._isRunning = false;
272 if (_this._rerunRequested) {
273 _this._rerunRequested = false;
274 _this.expireEntries();
275 }
276 })();
277 }
278
279 /**
280 * Expires entries based on the maximum age.
281 *
282 * @param {number} expireFromTimestamp A timestamp.
283 * @return {Promise<Array<string>>} A list of the URLs that were expired.
284 *
285 * @private
286 */
287 _findOldEntries(expireFromTimestamp) {
288 var _this2 = this;
289
290 return babelHelpers.asyncToGenerator(function* () {
291 {
292 assert_mjs.assert.isType(expireFromTimestamp, 'number', {
293 moduleName: 'workbox-cache-expiration',
294 className: 'CacheExpiration',
295 funcName: '_findOldEntries',
296 paramName: 'expireFromTimestamp'
297 });
298 }
299
300 if (!_this2._maxAgeSeconds) {
301 return [];
302 }
303
304 const expireOlderThan = expireFromTimestamp - _this2._maxAgeSeconds * 1000;
305 const timestamps = yield _this2._timestampModel.getAllTimestamps();
306 const expiredUrls = [];
307 timestamps.forEach(function (timestampDetails) {
308 if (timestampDetails.timestamp < expireOlderThan) {
309 expiredUrls.push(timestampDetails.url);
310 }
311 });
312
313 return expiredUrls;
314 })();
315 }
316
317 /**
318 * @return {Promise<Array>}
319 *
320 * @private
321 */
322 _findExtraEntries() {
323 var _this3 = this;
324
325 return babelHelpers.asyncToGenerator(function* () {
326 const extraUrls = [];
327
328 if (!_this3._maxEntries) {
329 return [];
330 }
331
332 const timestamps = yield _this3._timestampModel.getAllTimestamps();
333 while (timestamps.length > _this3._maxEntries) {
334 const lastUsed = timestamps.shift();
335 extraUrls.push(lastUsed.url);
336 }
337
338 return extraUrls;
339 })();
340 }
341
342 /**
343 * @param {Array<string>} urls Array of URLs to delete from cache.
344 *
345 * @private
346 */
347 _deleteFromCache(urls) {
348 var _this4 = this;
349
350 return babelHelpers.asyncToGenerator(function* () {
351 const cache = yield caches.open(_this4._cacheName);
352 for (const url of urls) {
353 yield cache.delete(url);
354 }
355 })();
356 }
357
358 /**
359 * @param {Array<string>} urls Array of URLs to delete from IDB
360 *
361 * @private
362 */
363 _deleteFromIDB(urls) {
364 var _this5 = this;
365
366 return babelHelpers.asyncToGenerator(function* () {
367 for (const url of urls) {
368 yield _this5._timestampModel.deleteUrl(url);
369 }
370 })();
371 }
372
373 /**
374 * Update the timestamp for the given URL. This ensures the when
375 * removing entries based on maximum entries, most recently used
376 * is accurate or when expiring, the timestamp is up-to-date.
377 *
378 * @param {string} url
379 */
380 updateTimestamp(url) {
381 var _this6 = this;
382
383 return babelHelpers.asyncToGenerator(function* () {
384 {
385 assert_mjs.assert.isType(url, 'string', {
386 moduleName: 'workbox-cache-expiration',
387 className: 'CacheExpiration',
388 funcName: 'updateTimestamp',
389 paramName: 'url'
390 });
391 }
392
393 const urlObject = new URL(url, location);
394 urlObject.hash = '';
395
396 yield _this6._timestampModel.setTimestamp(urlObject.href, Date.now());
397 })();
398 }
399
400 /**
401 * Can be used to check if a URL has expired or not before it's used.
402 *
403 * This requires a look up from IndexedDB, so can be slow.
404 *
405 * Note: This method will not remove the cached entry, call
406 * `expireEntries()` to remove indexedDB and Cache entries.
407 *
408 * @param {string} url
409 * @return {boolean}
410 */
411 isURLExpired(url) {
412 var _this7 = this;
413
414 return babelHelpers.asyncToGenerator(function* () {
415 if (!_this7._maxAgeSeconds) {
416 throw new WorkboxError_mjs.WorkboxError(`expired-test-without-max-age`, {
417 methodName: 'isURLExpired',
418 paramName: 'maxAgeSeconds'
419 });
420 }
421 const urlObject = new URL(url, location);
422 urlObject.hash = '';
423
424 const timestamp = yield _this7._timestampModel.getTimestamp(urlObject.href);
425 const expireOlderThan = Date.now() - _this7._maxAgeSeconds * 1000;
426 return timestamp < expireOlderThan;
427 })();
428 }
429
430 /**
431 * Removes the IndexedDB object store used to keep track of cache expiration
432 * metadata.
433 */
434 delete() {
435 var _this8 = this;
436
437 return babelHelpers.asyncToGenerator(function* () {
438 // Make sure we don't attempt another rerun if we're called in the middle of
439 // a cache expiration.
440 _this8._rerunRequested = false;
441 yield _this8._timestampModel.delete();
442 })();
443 }
444 }
445
446 /*
447 Copyright 2016 Google Inc. All Rights Reserved.
448 Licensed under the Apache License, Version 2.0 (the "License");
449 you may not use this file except in compliance with the License.
450 You may obtain a copy of the License at
451 http://www.apache.org/licenses/LICENSE-2.0
452 Unless required by applicable law or agreed to in writing, software
453 distributed under the License is distributed on an "AS IS" BASIS,
454 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
455 See the License for the specific language governing permissions and
456 limitations under the License.
457 */
458
459 /**
460 * This plugin can be used in the Workbox APIs to regularly enforce a
461 * limit on the age and / or the number of cached requests.
462 *
463 * Whenever a cached request is used or updated, this plugin will look
464 * at the used Cache and remove any old or extra requests.
465 *
466 * When using `maxAgeSeconds`, requests may be used *once* after expiring
467 * because the expiration clean up will not have occurred until *after* the
468 * cached request has been used. If the request has a "Date" header, then
469 * a light weight expiration check is performed and the request will not be
470 * used immediately.
471 *
472 * When using `maxEntries`, the last request to be used will be the request
473 * that is removed from the Cache.
474 *
475 * @memberof workbox.expiration
476 */
477 class Plugin {
478 /**
479 * @param {Object} config
480 * @param {number} [config.maxEntries] The maximum number of entries to cache.
481 * Entries used the least will be removed as the maximum is reached.
482 * @param {number} [config.maxAgeSeconds] The maximum age of an entry before
483 * it's treated as stale and removed.
484 * @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to
485 * automatic deletion if the available storage quota has been exceeded.
486 */
487 constructor(config = {}) {
488 {
489 if (!(config.maxEntries || config.maxAgeSeconds)) {
490 throw new WorkboxError_mjs.WorkboxError('max-entries-or-age-required', {
491 moduleName: 'workbox-cache-expiration',
492 className: 'Plugin',
493 funcName: 'constructor'
494 });
495 }
496
497 if (config.maxEntries) {
498 assert_mjs.assert.isType(config.maxEntries, 'number', {
499 moduleName: 'workbox-cache-expiration',
500 className: 'Plugin',
501 funcName: 'constructor',
502 paramName: 'config.maxEntries'
503 });
504 }
505
506 if (config.maxAgeSeconds) {
507 assert_mjs.assert.isType(config.maxAgeSeconds, 'number', {
508 moduleName: 'workbox-cache-expiration',
509 className: 'Plugin',
510 funcName: 'constructor',
511 paramName: 'config.maxAgeSeconds'
512 });
513 }
514 }
515
516 this._config = config;
517 this._maxAgeSeconds = config.maxAgeSeconds;
518 this._cacheExpirations = new Map();
519
520 if (config.purgeOnQuotaError) {
521 index_mjs.registerQuotaErrorCallback(() => this.deleteCacheAndMetadata());
522 }
523 }
524
525 /**
526 * A simple helper method to return a CacheExpiration instance for a given
527 * cache name.
528 *
529 * @param {string} cacheName
530 * @return {CacheExpiration}
531 *
532 * @private
533 */
534 _getCacheExpiration(cacheName) {
535 if (cacheName === cacheNames_mjs.cacheNames.getRuntimeName()) {
536 throw new WorkboxError_mjs.WorkboxError('expire-custom-caches-only');
537 }
538
539 let cacheExpiration = this._cacheExpirations.get(cacheName);
540 if (!cacheExpiration) {
541 cacheExpiration = new CacheExpiration(cacheName, this._config);
542 this._cacheExpirations.set(cacheName, cacheExpiration);
543 }
544 return cacheExpiration;
545 }
546
547 /**
548 * A "lifecycle" callback that will be triggered automatically by the
549 * `workbox.runtimeCaching` handlers when a `Response` is about to be returned
550 * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
551 * the handler. It allows the `Response` to be inspected for freshness and
552 * prevents it from being used if the `Response`'s `Date` header value is
553 * older than the configured `maxAgeSeconds`.
554 *
555 * @param {Object} options
556 * @param {string} options.cacheName Name of the cache the response is in.
557 * @param {Response} options.cachedResponse The `Response` object that's been
558 * read from a cache and whose freshness should be checked.
559 * @return {Response} Either the `cachedResponse`, if it's
560 * fresh, or `null` if the `Response` is older than `maxAgeSeconds`.
561 *
562 * @private
563 */
564 cachedResponseWillBeUsed({ cacheName, cachedResponse }) {
565 if (!cachedResponse) {
566 return null;
567 }
568
569 let isFresh = this._isResponseDateFresh(cachedResponse);
570
571 // Expire entries to ensure that even if the expiration date has
572 // expired, it'll only be used once.
573 const cacheExpiration = this._getCacheExpiration(cacheName);
574 cacheExpiration.expireEntries();
575
576 return isFresh ? cachedResponse : null;
577 }
578
579 /**
580 * @param {Response} cachedResponse
581 * @return {boolean}
582 *
583 * @private
584 */
585 _isResponseDateFresh(cachedResponse) {
586 if (!this._maxAgeSeconds) {
587 // We aren't expiring by age, so return true, it's fresh
588 return true;
589 }
590
591 // Check if the 'date' header will suffice a quick expiration check.
592 // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
593 // discussion.
594 const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
595 if (dateHeaderTimestamp === null) {
596 // Unable to parse date, so assume it's fresh.
597 return true;
598 }
599
600 // If we have a valid headerTime, then our response is fresh iff the
601 // headerTime plus maxAgeSeconds is greater than the current time.
602 const now = Date.now();
603 return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000;
604 }
605
606 /**
607 * This method will extract the data header and parse it into a useful
608 * value.
609 *
610 * @param {Response} cachedResponse
611 * @return {number}
612 *
613 * @private
614 */
615 _getDateHeaderTimestamp(cachedResponse) {
616 if (!cachedResponse.headers.has('date')) {
617 return null;
618 }
619
620 const dateHeader = cachedResponse.headers.get('date');
621 const parsedDate = new Date(dateHeader);
622 const headerTime = parsedDate.getTime();
623
624 // If the Date header was invalid for some reason, parsedDate.getTime()
625 // will return NaN.
626 if (isNaN(headerTime)) {
627 return null;
628 }
629
630 return headerTime;
631 }
632
633 /**
634 * A "lifecycle" callback that will be triggered automatically by the
635 * `workbox.runtimeCaching` handlers when an entry is added to a cache.
636 *
637 * @param {Object} options
638 * @param {string} options.cacheName Name of the cache that was updated.
639 * @param {string} options.request The Request for the cached entry.
640 *
641 * @private
642 */
643 cacheDidUpdate({ cacheName, request }) {
644 var _this = this;
645
646 return babelHelpers.asyncToGenerator(function* () {
647 {
648 assert_mjs.assert.isType(cacheName, 'string', {
649 moduleName: 'workbox-cache-expiration',
650 className: 'Plugin',
651 funcName: 'cacheDidUpdate',
652 paramName: 'cacheName'
653 });
654 assert_mjs.assert.isInstance(request, Request, {
655 moduleName: 'workbox-cache-expiration',
656 className: 'Plugin',
657 funcName: 'cacheDidUpdate',
658 paramName: 'request'
659 });
660 }
661
662 const cacheExpiration = _this._getCacheExpiration(cacheName);
663 yield cacheExpiration.updateTimestamp(request.url);
664 yield cacheExpiration.expireEntries();
665 })();
666 }
667
668 /**
669 * This is a helper method that performs two operations:
670 *
671 * - Deletes *all* the underlying Cache instances associated with this plugin
672 * instance, by calling caches.delete() on you behalf.
673 * - Deletes the metadata from IndexedDB used to keep track of expiration
674 * details for each Cache instance.
675 *
676 * When using cache expiration, calling this method is preferable to calling
677 * `caches.delete()` directly, since this will ensure that the IndexedDB
678 * metadata is also cleanly removed and open IndexedDB instances are deleted.
679 *
680 * Note that if you're *not* using cache expiration for a given cache, calling
681 * `caches.delete()` and passing in the cache's name should be sufficient.
682 * There is no Workbox-specific method needed for cleanup in that case.
683 */
684 deleteCacheAndMetadata() {
685 var _this2 = this;
686
687 return babelHelpers.asyncToGenerator(function* () {
688 // Do this one at a time instead of all at once via `Promise.all()` to
689 // reduce the chance of inconsistency if a promise rejects.
690 for (const [cacheName, cacheExpiration] of _this2._cacheExpirations) {
691 yield caches.delete(cacheName);
692 yield cacheExpiration.delete();
693 }
694
695 // Reset this._cacheExpirations to its initial state.
696 _this2._cacheExpirations = new Map();
697 })();
698 }
699 }
700
701 /*
702 Copyright 2017 Google Inc.
703
704 Licensed under the Apache License, Version 2.0 (the "License");
705 you may not use this file except in compliance with the License.
706 You may obtain a copy of the License at
707
708 https://www.apache.org/licenses/LICENSE-2.0
709
710 Unless required by applicable law or agreed to in writing, software
711 distributed under the License is distributed on an "AS IS" BASIS,
712 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
713 See the License for the specific language governing permissions and
714 limitations under the License.
715 */
716
717 /*
718 Copyright 2017 Google Inc.
719
720 Licensed under the Apache License, Version 2.0 (the "License");
721 you may not use this file except in compliance with the License.
722 You may obtain a copy of the License at
723
724 https://www.apache.org/licenses/LICENSE-2.0
725
726 Unless required by applicable law or agreed to in writing, software
727 distributed under the License is distributed on an "AS IS" BASIS,
728 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
729 See the License for the specific language governing permissions and
730 limitations under the License.
731 */
732
733 exports.CacheExpiration = CacheExpiration;
734 exports.Plugin = Plugin;
735
736 return exports;
737
738}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core));
739
740//# sourceMappingURL=workbox-cache-expiration.dev.js.map