UNPKG

20.2 kBJavaScriptView Raw
1this.workbox = this.workbox || {};
2this.workbox.expiration = (function (exports,DBWrapper_mjs,WorkboxError_mjs,assert_mjs,logger_mjs,cacheNames_mjs) {
3'use strict';
4
5try {
6 self.workbox.v['workbox:cache-expiration:3.1.0'] = 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
25const URL_KEY = 'url';
26const TIMESTAMP_KEY = 'timestamp';
27
28/**
29 * Returns the timestamp model.
30 *
31 * @private
32 */
33class 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/*
136 Copyright 2017 Google Inc.
137
138 Licensed under the Apache License, Version 2.0 (the "License");
139 you may not use this file except in compliance with the License.
140 You may obtain a copy of the License at
141
142 https://www.apache.org/licenses/LICENSE-2.0
143
144 Unless required by applicable law or agreed to in writing, software
145 distributed under the License is distributed on an "AS IS" BASIS,
146 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
147 See the License for the specific language governing permissions and
148 limitations under the License.
149*/
150
151/**
152 * The `CacheExpiration` class allows you define an expiration and / or
153 * limit on the number of responses stored in a
154 * [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
155 *
156 * @memberof workbox.expiration
157 */
158class CacheExpiration {
159 /**
160 * To construct a new CacheExpiration instance you must provide at least
161 * one of the `config` properties.
162 *
163 * @param {string} cacheName Name of the cache to apply restrictions to.
164 * @param {Object} config
165 * @param {number} [config.maxEntries] The maximum number of entries to cache.
166 * Entries used the least will be removed as the maximum is reached.
167 * @param {number} [config.maxAgeSeconds] The maximum age of an entry before
168 * it's treated as stale and removed.
169 */
170 constructor(cacheName, config = {}) {
171 {
172 assert_mjs.assert.isType(cacheName, 'string', {
173 moduleName: 'workbox-cache-expiration',
174 className: 'CacheExpiration',
175 funcName: 'constructor',
176 paramName: 'cacheName'
177 });
178
179 if (!(config.maxEntries || config.maxAgeSeconds)) {
180 throw new WorkboxError_mjs.WorkboxError('max-entries-or-age-required', {
181 moduleName: 'workbox-cache-expiration',
182 className: 'CacheExpiration',
183 funcName: 'constructor'
184 });
185 }
186
187 if (config.maxEntries) {
188 assert_mjs.assert.isType(config.maxEntries, 'number', {
189 moduleName: 'workbox-cache-expiration',
190 className: 'CacheExpiration',
191 funcName: 'constructor',
192 paramName: 'config.maxEntries'
193 });
194
195 // TODO: Assert is positive
196 }
197
198 if (config.maxAgeSeconds) {
199 assert_mjs.assert.isType(config.maxAgeSeconds, 'number', {
200 moduleName: 'workbox-cache-expiration',
201 className: 'CacheExpiration',
202 funcName: 'constructor',
203 paramName: 'config.maxAgeSeconds'
204 });
205
206 // TODO: Assert is positive
207 }
208 }
209
210 this._isRunning = false;
211 this._rerunRequested = false;
212 this._maxEntries = config.maxEntries;
213 this._maxAgeSeconds = config.maxAgeSeconds;
214 this._cacheName = cacheName;
215 this._timestampModel = new CacheTimestampsModel(cacheName);
216 }
217
218 /**
219 * Expires entries for the given cache and given criteria.
220 */
221 expireEntries() {
222 var _this = this;
223
224 return babelHelpers.asyncToGenerator(function* () {
225 if (_this._isRunning) {
226 _this._rerunRequested = true;
227 return;
228 }
229 _this._isRunning = true;
230
231 const now = Date.now();
232
233 // First, expire old entries, if maxAgeSeconds is set.
234 const oldEntries = yield _this._findOldEntries(now);
235
236 // Once that's done, check for the maximum size.
237 const extraEntries = yield _this._findExtraEntries();
238
239 // Use a Set to remove any duplicates following the concatenation, then
240 // convert back into an array.
241 const allUrls = [...new Set(oldEntries.concat(extraEntries))];
242
243 yield Promise.all([_this._deleteFromCache(allUrls), _this._deleteFromIDB(allUrls)]);
244
245 {
246 // TODO: break apart entries deleted due to expiration vs size restraints
247 if (allUrls.length > 0) {
248 logger_mjs.logger.groupCollapsed(`Expired ${allUrls.length} ` + `${allUrls.length === 1 ? 'entry' : 'entries'} and removed ` + `${allUrls.length === 1 ? 'it' : 'them'} from the ` + `'${_this._cacheName}' cache.`);
249 logger_mjs.logger.log(`Expired the following ${allUrls.length === 1 ? 'URL' : 'URLs'}:`);
250 allUrls.forEach(function (url) {
251 return logger_mjs.logger.log(` ${url}`);
252 });
253 logger_mjs.logger.groupEnd();
254 } else {
255 logger_mjs.logger.debug(`Cache expiration ran and found no entries to remove.`);
256 }
257 }
258
259 _this._isRunning = false;
260 if (_this._rerunRequested) {
261 _this._rerunRequested = false;
262 _this.expireEntries();
263 }
264 })();
265 }
266
267 /**
268 * Expires entries based on the maximum age.
269 *
270 * @param {number} expireFromTimestamp A timestamp.
271 * @return {Promise<Array<string>>} A list of the URLs that were expired.
272 *
273 * @private
274 */
275 _findOldEntries(expireFromTimestamp) {
276 var _this2 = this;
277
278 return babelHelpers.asyncToGenerator(function* () {
279 {
280 assert_mjs.assert.isType(expireFromTimestamp, 'number', {
281 moduleName: 'workbox-cache-expiration',
282 className: 'CacheExpiration',
283 funcName: '_findOldEntries',
284 paramName: 'expireFromTimestamp'
285 });
286 }
287
288 if (!_this2._maxAgeSeconds) {
289 return [];
290 }
291
292 const expireOlderThan = expireFromTimestamp - _this2._maxAgeSeconds * 1000;
293 const timestamps = yield _this2._timestampModel.getAllTimestamps();
294 const expiredUrls = [];
295 timestamps.forEach(function (timestampDetails) {
296 if (timestampDetails.timestamp < expireOlderThan) {
297 expiredUrls.push(timestampDetails.url);
298 }
299 });
300
301 return expiredUrls;
302 })();
303 }
304
305 /**
306 * @return {Promise<Array>}
307 *
308 * @private
309 */
310 _findExtraEntries() {
311 var _this3 = this;
312
313 return babelHelpers.asyncToGenerator(function* () {
314 const extraUrls = [];
315
316 if (!_this3._maxEntries) {
317 return [];
318 }
319
320 const timestamps = yield _this3._timestampModel.getAllTimestamps();
321 while (timestamps.length > _this3._maxEntries) {
322 const lastUsed = timestamps.shift();
323 extraUrls.push(lastUsed.url);
324 }
325
326 return extraUrls;
327 })();
328 }
329
330 /**
331 * @param {Array<string>} urls Array of URLs to delete from cache.
332 *
333 * @private
334 */
335 _deleteFromCache(urls) {
336 var _this4 = this;
337
338 return babelHelpers.asyncToGenerator(function* () {
339 const cache = yield caches.open(_this4._cacheName);
340 for (const url of urls) {
341 yield cache.delete(url);
342 }
343 })();
344 }
345
346 /**
347 * @param {Array<string>} urls Array of URLs to delete from IDB
348 *
349 * @private
350 */
351 _deleteFromIDB(urls) {
352 var _this5 = this;
353
354 return babelHelpers.asyncToGenerator(function* () {
355 for (const url of urls) {
356 yield _this5._timestampModel.deleteUrl(url);
357 }
358 })();
359 }
360
361 /**
362 * Update the timestamp for the given URL. This ensures the when
363 * removing entries based on maximum entries, most recently used
364 * is accurate or when expiring, the timestamp is up-to-date.
365 *
366 * @param {string} url
367 */
368 updateTimestamp(url) {
369 var _this6 = this;
370
371 return babelHelpers.asyncToGenerator(function* () {
372 {
373 assert_mjs.assert.isType(url, 'string', {
374 moduleName: 'workbox-cache-expiration',
375 className: 'CacheExpiration',
376 funcName: 'updateTimestamp',
377 paramName: 'url'
378 });
379 }
380
381 const urlObject = new URL(url, location);
382 urlObject.hash = '';
383
384 yield _this6._timestampModel.setTimestamp(urlObject.href, Date.now());
385 })();
386 }
387
388 /**
389 * Can be used to check if a URL has expired or not before it's used.
390 *
391 * This requires a look up from IndexedDB, so can be slow.
392 *
393 * Note: This method will not remove the cached entry, call
394 * `expireEntries()` to remove indexedDB and Cache entries.
395 *
396 * @param {string} url
397 * @return {boolean}
398 */
399 isURLExpired(url) {
400 var _this7 = this;
401
402 return babelHelpers.asyncToGenerator(function* () {
403 if (!_this7._maxAgeSeconds) {
404 throw new WorkboxError_mjs.WorkboxError(`expired-test-without-max-age`, {
405 methodName: 'isURLExpired',
406 paramName: 'maxAgeSeconds'
407 });
408 }
409 const urlObject = new URL(url, location);
410 urlObject.hash = '';
411
412 const timestamp = yield _this7._timestampModel.getTimestamp(urlObject.href);
413 const expireOlderThan = Date.now() - _this7._maxAgeSeconds * 1000;
414 return timestamp < expireOlderThan;
415 })();
416 }
417}
418
419/*
420 Copyright 2016 Google Inc. All Rights Reserved.
421 Licensed under the Apache License, Version 2.0 (the "License");
422 you may not use this file except in compliance with the License.
423 You may obtain a copy of the License at
424 http://www.apache.org/licenses/LICENSE-2.0
425 Unless required by applicable law or agreed to in writing, software
426 distributed under the License is distributed on an "AS IS" BASIS,
427 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
428 See the License for the specific language governing permissions and
429 limitations under the License.
430*/
431
432/**
433 * This plugin can be used in the Workbox API's to regularly enforce a
434 * limit on the age and / or the number of cached requests.
435 *
436 * Whenever a cached request is used or updated, this plugin will look
437 * at the used Cache and remove any old or extra requests.
438 *
439 * When using `maxAgeSeconds`, requests may be used *once* after expiring
440 * because the expiration clean up will not have occurred until *after* the
441 * cached request has been used. If the request has a "Date" header, then
442 * a light weight expiration check is performed and the request will not be
443 * used immediately.
444 *
445 * When using `maxEntries`, the last request to be used will be the request
446 * that is removed from the Cache.
447 *
448 * @memberof workbox.expiration
449 */
450class Plugin {
451 /**
452 * @param {Object} config
453 * @param {number} [config.maxEntries] The maximum number of entries to cache.
454 * Entries used the least will be removed as the maximum is reached.
455 * @param {number} [config.maxAgeSeconds] The maximum age of an entry before
456 * it's treated as stale and removed.
457 */
458 constructor(config = {}) {
459 {
460 if (!(config.maxEntries || config.maxAgeSeconds)) {
461 throw new WorkboxError_mjs.WorkboxError('max-entries-or-age-required', {
462 moduleName: 'workbox-cache-expiration',
463 className: 'Plugin',
464 funcName: 'constructor'
465 });
466 }
467
468 if (config.maxEntries) {
469 assert_mjs.assert.isType(config.maxEntries, 'number', {
470 moduleName: 'workbox-cache-expiration',
471 className: 'Plugin',
472 funcName: 'constructor',
473 paramName: 'config.maxEntries'
474 });
475 }
476
477 if (config.maxAgeSeconds) {
478 assert_mjs.assert.isType(config.maxAgeSeconds, 'number', {
479 moduleName: 'workbox-cache-expiration',
480 className: 'Plugin',
481 funcName: 'constructor',
482 paramName: 'config.maxAgeSeconds'
483 });
484 }
485 }
486
487 this._config = config;
488 this._maxAgeSeconds = config.maxAgeSeconds;
489 this._cacheExpirations = new Map();
490 }
491
492 /**
493 * A simple helper method to return a CacheExpiration instance for a given
494 * cache name.
495 *
496 * @param {string} cacheName
497 * @return {CacheExpiration}
498 *
499 * @private
500 */
501 _getCacheExpiration(cacheName) {
502 if (cacheName === cacheNames_mjs.cacheNames.getRuntimeName()) {
503 throw new WorkboxError_mjs.WorkboxError('expire-custom-caches-only');
504 }
505
506 let cacheExpiration = this._cacheExpirations.get(cacheName);
507 if (!cacheExpiration) {
508 cacheExpiration = new CacheExpiration(cacheName, this._config);
509 this._cacheExpirations.set(cacheName, cacheExpiration);
510 }
511 return cacheExpiration;
512 }
513
514 /**
515 * A "lifecycle" callback that will be triggered automatically by the
516 * `workbox.runtimeCaching` handlers when a `Response` is about to be returned
517 * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
518 * the handler. It allows the `Response` to be inspected for freshness and
519 * prevents it from being used if the `Response`'s `Date` header value is
520 * older than the configured `maxAgeSeconds`.
521 *
522 * @param {Object} input
523 * @param {string} input.cacheName Name of the cache the responses belong to.
524 * @param {Response} input.cachedResponse The `Response` object that's been
525 * read from a cache and whose freshness should be checked.
526 * @return {Response} Either the `cachedResponse`, if it's
527 * fresh, or `null` if the `Response` is older than `maxAgeSeconds`.
528 *
529 * @private
530 */
531 cachedResponseWillBeUsed({ cacheName, cachedResponse }) {
532 if (!cachedResponse) {
533 return null;
534 }
535
536 let isFresh = this._isResponseDateFresh(cachedResponse);
537
538 // Expire entries to ensure that even if the expiration date has
539 // expired, it'll only be used once.
540 const cacheExpiration = this._getCacheExpiration(cacheName);
541 cacheExpiration.expireEntries();
542
543 return isFresh ? cachedResponse : null;
544 }
545
546 /**
547 * @param {Response} cachedResponse
548 * @return {boolean}
549 *
550 * @private
551 */
552 _isResponseDateFresh(cachedResponse) {
553 if (!this._maxAgeSeconds) {
554 // We aren't expiring by age, so return true, it's fresh
555 return true;
556 }
557
558 // Check if the 'date' header will suffice a quick expiration check.
559 // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
560 // discussion.
561 const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
562 if (dateHeaderTimestamp === null) {
563 // Unable to parse date, so assume it's fresh.
564 return true;
565 }
566
567 // If we have a valid headerTime, then our response is fresh iff the
568 // headerTime plus maxAgeSeconds is greater than the current time.
569 const now = Date.now();
570 return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000;
571 }
572
573 /**
574 * This method will extract the data header and parse it into a useful
575 * value.
576 *
577 * @param {Response} cachedResponse
578 * @return {number}
579 *
580 * @private
581 */
582 _getDateHeaderTimestamp(cachedResponse) {
583 if (!cachedResponse.headers.has('date')) {
584 return null;
585 }
586
587 const dateHeader = cachedResponse.headers.get('date');
588 const parsedDate = new Date(dateHeader);
589 const headerTime = parsedDate.getTime();
590
591 // If the Date header was invalid for some reason, parsedDate.getTime()
592 // will return NaN.
593 if (isNaN(headerTime)) {
594 return null;
595 }
596
597 return headerTime;
598 }
599
600 /**
601 * A "lifecycle" callback that will be triggered automatically by the
602 * `workbox.runtimeCaching` handlers when an entry is added to a cache.
603 *
604 * @param {Object} input
605 * @param {string} input.cacheName Name of the cache the responses belong to.
606 * @param {string} input.request The Request for the cached entry.
607 *
608 * @private
609 */
610 cacheDidUpdate({ cacheName, request }) {
611 var _this = this;
612
613 return babelHelpers.asyncToGenerator(function* () {
614 {
615 assert_mjs.assert.isType(cacheName, 'string', {
616 moduleName: 'workbox-cache-expiration',
617 className: 'Plugin',
618 funcName: 'cacheDidUpdate',
619 paramName: 'cacheName'
620 });
621 assert_mjs.assert.isInstance(request, Request, {
622 moduleName: 'workbox-cache-expiration',
623 className: 'Plugin',
624 funcName: 'cacheDidUpdate',
625 paramName: 'request'
626 });
627 }
628
629 const cacheExpiration = _this._getCacheExpiration(cacheName);
630 yield cacheExpiration.updateTimestamp(request.url);
631 yield cacheExpiration.expireEntries();
632 })();
633 }
634}
635
636/*
637 Copyright 2017 Google Inc.
638
639 Licensed under the Apache License, Version 2.0 (the "License");
640 you may not use this file except in compliance with the License.
641 You may obtain a copy of the License at
642
643 https://www.apache.org/licenses/LICENSE-2.0
644
645 Unless required by applicable law or agreed to in writing, software
646 distributed under the License is distributed on an "AS IS" BASIS,
647 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
648 See the License for the specific language governing permissions and
649 limitations under the License.
650*/
651
652/*
653 Copyright 2017 Google Inc.
654
655 Licensed under the Apache License, Version 2.0 (the "License");
656 you may not use this file except in compliance with the License.
657 You may obtain a copy of the License at
658
659 https://www.apache.org/licenses/LICENSE-2.0
660
661 Unless required by applicable law or agreed to in writing, software
662 distributed under the License is distributed on an "AS IS" BASIS,
663 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
664 See the License for the specific language governing permissions and
665 limitations under the License.
666*/
667
668exports.CacheExpiration = CacheExpiration;
669exports.Plugin = Plugin;
670
671return exports;
672
673}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private));
674
675//# sourceMappingURL=workbox-cache-expiration.dev.js.map