vendor: 5,248 bytes, lines 1-128
1this.workbox = this.workbox || {}; 2this.workbox.expiration = (function (exports, assert_js, dontWaitFor_js, logger_js, WorkboxError_js, cacheNames_js, getFriendlyURL_js, registerQuotaErrorCallback_js) { 3 'use strict'; 4 5 function _extends() { 6 return _extends = Object.assign ? Object.assign.bind() : function (n) { 7 for (var e = 1; e < arguments.length; e++) { 8 var t = arguments[e]; 9 for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); 10 } 11 return n; 12 }, _extends.apply(null, arguments); 13 } 14 15 const instanceOfAny = (object, constructors) => constructors.some(c => object instanceof c); 16 let idbProxyableTypes; 17 let cursorAdvanceMethods; 18 // This is a function to prevent it throwing up in node environments. 19 function getIdbProxyableTypes() { 20 return idbProxyableTypes || (idbProxyableTypes = [IDBDatabase, IDBObjectStore, IDBIndex, IDBCursor, IDBTransaction]); 21 } 22 // This is a function to prevent it throwing up in node environments. 23 function getCursorAdvanceMethods() { 24 return cursorAdvanceMethods || (cursorAdvanceMethods = [IDBCursor.prototype.advance, IDBCursor.prototype.continue, IDBCursor.prototype.continuePrimaryKey]); 25 } 26 const cursorRequestMap = new WeakMap(); 27 const transactionDoneMap = new WeakMap(); 28 const transactionStoreNamesMap = new WeakMap(); 29 const transformCache = new WeakMap(); 30 const reverseTransformCache = new WeakMap(); 31 function promisifyRequest(request) { 32 const promise = new Promise((resolve, reject) => { 33 const unlisten = () => { 34 request.removeEventListener('success', success); 35 request.removeEventListener('error', error); 36 }; 37 const success = () => { 38 resolve(wrap(request.result)); 39 unlisten(); 40 }; 41 const error = () => { 42 reject(request.error); 43 unlisten(); 44 }; 45 request.addEventListener('success', success); 46 request.addEventListener('error', error); 47 }); 48 promise.then(value => { 49 // Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval 50 // (see wrapFunction). 51 if (value instanceof IDBCursor) { 52 cursorRequestMap.set(value, request); 53 } 54 // Catching to avoid "Uncaught Promise exceptions" 55 }).catch(() => {}); 56 // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This 57 // is because we create many promises from a single IDBRequest. 58 reverseTransformCache.set(promise, request); 59 return promise; 60 } 61 function cacheDonePromiseForTransaction(tx) { 62 // Early bail if we've already created a done promise for this transaction. 63 if (transactionDoneMap.has(tx)) return; 64 const done = new Promise((resolve, reject) => { 65 const unlisten = () => { 66 tx.removeEventListener('complete', complete); 67 tx.removeEventListener('error', error); 68 tx.removeEventListener('abort', error); 69 }; 70 const complete = () => { 71 resolve(); 72 unlisten(); 73 }; 74 const error = () => { 75 reject(tx.error || new DOMException('AbortError', 'AbortError')); 76 unlisten(); 77 }; 78 tx.addEventListener('complete', complete); 79 tx.addEventListener('error', error); 80 tx.addEventListener('abort', error); 81 }); 82 // Cache it for later retrieval. 83 transactionDoneMap.set(tx, done); 84 } 85 let idbProxyTraps = { 86 get(target, prop, receiver) { 87 if (target instanceof IDBTransaction) { 88 // Special handling for transaction.done. 89 if (prop === 'done') return transactionDoneMap.get(target); 90 // Polyfill for objectStoreNames because of Edge. 91 if (prop === 'objectStoreNames') { 92 return target.objectStoreNames || transactionStoreNamesMap.get(target); 93 } 94 // Make tx.store return the only store in the transaction, or undefined if there are many. 95 if (prop === 'store') { 96 return receiver.objectStoreNames[1] ? undefined : receiver.objectStore(receiver.objectStoreNames[0]); 97 } 98 } 99 // Else transform whatever we get back. 100 return wrap(target[prop]); 101 }, 102 set(target, prop, value) { 103 target[prop] = value; 104 return true; 105 }, 106 has(target, prop) { 107 if (target instanceof IDBTransaction && (prop === 'done' || prop === 'store')) { 108 return true; 109 } 110 return prop in target; 111 } 112 }; 113 function replaceTraps(callback) { 114 idbProxyTraps = callback(idbProxyTraps); 115 } 116 function wrapFunction(func) { 117 // Due to expected object equality (which is enforced by the caching in `wrap`), we 118 // only create one new func per func. 119 // Edge doesn't support objectStoreNames (booo), so we polyfill it here. 120 if (func === IDBDatabase.prototype.transaction && !('objectStoreNames' in IDBTransaction.prototype)) { 121 return function (storeNames, ...args) { 122 const tx = func.call(unwrap(this), storeNames, ...args); 123 transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]); 124 return wrap(tx); 125 }; 126 } 127 // Cursor methods are special, as the behaviour is a little more different to standard IDB. In 128 // IDB, you advance the cursor and wait for a new 'success' on the ID
128BRequest that gave you the 129 // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense 130 // with real promises, so each advance methods returns a new promise for the cursor object, or 131 // undefined if the end of the cursor has been reached. 132 if (getCursorAdvanceMethods().includes(func)) { 133 return function (...args) { 134 // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use 135 // the original object. 136 func.apply(unwrap(this), args); 137 return wrap(cursorRequestMap.get(this)); 138 }; 139 } 140 return function (...args) { 141 // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use 142 // the original object. 143 return wrap(func.apply(unwrap(this), args)); 144 }; 145 } 146 function transformCachableValue(value) { 147 if (typeof value === 'function') return wrapFunction(value); 148 // This doesn't return, it just creates a 'done' promise for the transaction, 149 // which is later returned for transaction.done (see idbObjectHandler). 150 if (value instanceof IDBTransaction) cacheDonePromiseForTransaction(value); 151 if (instanceOfAny(value, getIdbProxyableTypes())) return new Proxy(value, idbProxyTraps); 152 // Return the same value back if we're not going to transform it. 153 return value; 154 } 155 function wrap(value) { 156 // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because 157 // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached. 158 if (value instanceof IDBRequest) return promisifyRequest(value); 159 // If we've already transformed this value before, reuse the transformed value. 160 // This is faster, but it also provides object equality. 161 if (transformCache.has(value)) return transformCache.get(value); 162 const newValue = transformCachableValue(value); 163 // Not all types are transformed. 164 // These may be primitive types, so they can't be WeakMap keys. 165 if (newValue !== value) { 166 transformCache.set(value, newValue); 167 reverseTransformCache.set(newValue, value); 168 } 169 return newValue; 170 } 171 const unwrap = value => reverseTransformCache.get(value); 172 173 /** 174 * Open a database. 175 * 176 * @param name Name of the database. 177 * @param version Schema version. 178 * @param callbacks Additional callbacks. 179 */ 180 function openDB(name, version, { 181 blocked, 182 upgrade, 183 blocking, 184 terminated 185 } = {}) { 186 const request = indexedDB.open(name, version); 187 const openPromise = wrap(request); 188 if (upgrade) { 189 request.addEventListener('upgradeneeded', event => { 190 upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction)); 191 }); 192 } 193 if (blocked) request.addEventListener('blocked', () => blocked()); 194 openPromise.then(db => { 195 if (terminated) db.addEventListener('close', () => terminated()); 196 if (blocking) db.addEventListener('versionchange', () => blocking()); 197 }).catch(() => {}); 198 return openPromise; 199 } 200 /** 201 * Delete a database. 202 * 203 * @param name Name of the database. 204 */ 205 function deleteDB(name, { 206 blocked 207 } = {}) { 208 const request = indexedDB.deleteDatabase(name); 209 if (blocked) request.addEventListener('blocked', () => blocked()); 210 return wrap(request).then(() => undefined); 211 } 212 const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count']; 213 const writeMethods = ['put', 'add', 'delete', 'clear']; 214 const cachedMethods = new Map(); 215 function getMethod(target, prop) { 216 if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === 'string')) { 217 return; 218 } 219 if (cachedMethods.get(prop)) return cachedMethods.get(prop); 220 const targetFuncName = prop.replace(/FromIndex$/, ''); 221 const useIndex = prop !== targetFuncName; 222 const isWrite = writeMethods.includes(targetFuncName); 223 if ( 224 // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. 225 !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))) { 226 return; 227 } 228 const method = async function (storeName, ...args) { 229 // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :( 230 const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly'); 231 let target = tx.store; 232 if (useIndex) target = target.index(args.shift()); 233 // Must reject if op rejects. 234 // If it's a write operation, must reject if tx.done rejects. 235 // Must reject with op rejection first. 236 // Must resolve with op value. 237 // Must handle both promises (no unhandled rejections) 238 return (await Promise.all([target[targetFuncName](...args), isWrite && tx.done]))[0]; 239 }; 240 cachedMethods.set(prop, method); 241 return method; 242 } 243 replaceTraps(oldTraps => _extends({}, oldTraps, { 244 get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver), 245 has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop) 246 })); 247 248 // @ts-ignore 249 try { 250 self['workbox:expiration:7.4.0'] && _(); 251 } catch (e) {} 252 253 /* 254 Copyright 2018 Google LLC 255 256 Use of this source code is governed by an MIT-style 257 license that can be found in the LICENSE file or at 258 https://opensource.org/licenses/MIT. 259 */ 260 const DB_NAME = 'workbox-expiration'; 261 const CACHE_OBJECT_STORE = 'cache-entries'; 262 const normalizeURL = unNormalizedUrl => { 263 const url = new URL(unNormalizedUrl, location.href); 264 url.hash = ''; 265 return url.href; 266 }; 267 /** 268 * Returns the timestamp model. 269 * 270 * @private 271 */ 272 class CacheTimestampsModel { 273 /** 274 * 275 * @param {string} cacheName 276 * 277 * @private 278 */ 279 constructor(cacheName) { 280 this._db = null; 281 this._cacheName = cacheName; 282 } 283 /** 284 * Performs an upgrade of indexedDB. 285 * 286 * @param {IDBPDatabase<CacheDbSchema>} db 287 * 288 * @private 289 */ 290 _upgradeDb(db) { 291 // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we 292 // have to use the `id` keyPath here and create our own values (a 293 // concatenation of `url + cacheName`) instead of simply using 294 // `keyPath: ['url', 'cacheName']`, which is supported in other browsers. 295 const objStore = db.createObjectStore(CACHE_OBJECT_STORE, { 296 keyPath: 'id' 297 });
298 // TODO(philipwalton): once we don't have to support EdgeHTML, we can 299 // create a single index with the keyPath `['cacheName', 'timestamp']` 300 // instead of doing both these indexes. 301 objStore.createIndex('cacheName', 'cacheName', { 302 unique: false 303 }); 304 objStore.createIndex('timestamp', 'timestamp', { 305 unique: false 306 }); 307 } 308 /** 309 * Performs an upgrade of indexedDB and deletes deprecated DBs. 310 * 311 * @param {IDBPDatabase<CacheDbSchema>} db 312 * 313 * @private 314 */ 315 _upgradeDbAndDeleteOldDbs(db) { 316 this._upgradeDb(db); 317 if (this._cacheName) { 318 void deleteDB(this._cacheName); 319 } 320 } 321 /** 322 * @param {string} url 323 * @param {number} timestamp 324 * 325 * @private 326 */ 327 async setTimestamp(url, timestamp) { 328 url = normalizeURL(url); 329 const entry = { 330 url, 331 timestamp, 332 cacheName: this._cacheName, 333 // Creating an ID from the URL and cache name won't be necessary once 334 // Edge switches to Chromium and all browsers we support work with 335 // array keyPaths. 336 id: this._getId(url) 337 }; 338 const db = await this.getDb(); 339 const tx = db.transaction(CACHE_OBJECT_STORE, 'readwrite', { 340 durability: 'relaxed' 341 }); 342 await tx.store.put(entry); 343 await tx.done; 344 } 345 /** 346 * Returns the timestamp stored for a given URL. 347 * 348 * @param {string} url 349 * @return {number | undefined} 350 * 351 * @private 352 */ 353 async getTimestamp(url) { 354 const db = await this.getDb(); 355 const entry = await db.get(CACHE_OBJECT_STORE, this._getId(url)); 356 return entry === null || entry === void 0 ? void 0 : entry.timestamp; 357 } 358 /** 359 * Iterates through all the entries in the object store (from newest to 360 * oldest) and removes entries once either `maxCount` is reached or the 361 * entry's timestamp is less than `minTimestamp`. 362 * 363 * @param {number} minTimestamp 364 * @param {number} maxCount 365 * @return {Array<string>} 366 * 367 * @private 368 */ 369 async expireEntries(minTimestamp, maxCount) { 370 const db = await this.getDb(); 371 let cursor = await db.transaction(CACHE_OBJECT_STORE).store.index('timestamp').openCursor(null, 'prev'); 372 const entriesToDelete = []; 373 let entriesNotDeletedCount = 0; 374 while (cursor) { 375 const result = cursor.value; 376 // TODO(philipwalton): once we can use a multi-key index, we 377 // won't have to check `cacheName` here. 378 if (result.cacheName === this._cacheName) { 379 // Delete an entry if it's older than the max age or 380 // if we already have the max number allowed. 381 if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) { 382 // TODO(philipwalton): we should be able to delete the 383 // entry right here, but doing so causes an iteration 384 // bug in Safari stable (fixed in TP). Instead we can 385 // store the keys of the entries to delete, and then 386 // delete the separate transactions. 387 // https://github.com/GoogleChrome/workbox/issues/1978 388 // cursor.delete(); 389 // We only need to return the URL, not the whole entry. 390 entriesToDelete.push(cursor.value); 391 } else { 392 entriesNotDeletedCount++; 393 } 394 } 395 cursor = await cursor.continue(); 396 } 397 // TODO(philipwalton): once the Safari bug in the following issue is fixed, 398 // we should be able to remove this loop and do the entry deletion in the 399 // cursor loop above: 400 // https://github.com/GoogleChrome/workbox/issues/1978 401 const urlsDeleted = []; 402 for (const entry of entriesToDelete) { 403 await db.delete(CACHE_OBJECT_STORE, entry.id); 404 urlsDeleted.push(entry.url); 405 } 406 return urlsDeleted; 407 } 408 /** 409 * Takes a URL and returns an ID that will be unique in the object store. 410 * 411 * @param {string} url 412 * @return {string} 413 * 414 * @private 415 */ 416 _getId(url) { 417 // Creating an ID from the URL and cache name won't be necessary once 418 // Edge switches to Chromium and all browsers we support work with 419 // array keyPaths. 420 return this._cacheName + '|' + normalizeURL(url); 421 } 422 /** 423 * Returns an open connection to the database. 424 * 425 * @private 426 */ 427 async getDb() { 428 if (!this._db) { 429 this._db = await openDB(DB_NAME, 1, { 430 upgrade: this._upgradeDbAndDeleteOldDbs.bind(this) 431 }); 432 } 433 return this._db; 434 } 435 } 436 437 /* 438 Copyright 2018 Google LLC 439 440 Use of this source code is governed by an MIT-style 441 license that can be found in the LICENSE file or at 442 https://opensource.org/licenses/MIT. 443 */ 444 /** 445 * The `CacheExpiration` class allows you define an expiration and / or 446 * limit on the number of responses stored in a 447 * [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache). 448 * 449 * @memberof workbox-expiration 450 */ 451 class CacheExpiration { 452 /** 453 * To construct a new CacheExpiration instance you must provide at least 454 * one of the `config` properties. 455 * 456 * @param {string} cacheName Name of the cache to apply restrictions to. 457 * @param {Object} config 458 * @param {number} [config.maxEntries] The maximum number of entries to cache. 459 * Entries used the least will be removed as the maximum is reached. 460 * @param {number} [config.maxAgeSeconds] The maximum age of an entry before 461 * it's treated as stale and removed. 462 * @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters) 463 * that will be used when calling `delete()` on the cache. 464 */ 465 constructor(cacheName, config = {}) { 466 this._isRunning = false;
467 this._rerunRequested = false; 468 { 469 assert_js.assert.isType(cacheName, 'string', { 470 moduleName: 'workbox-expiration', 471 className: 'CacheExpiration', 472 funcName: 'constructor', 473 paramName: 'cacheName' 474 }); 475 if (!(config.maxEntries || config.maxAgeSeconds)) { 476 throw new WorkboxError_js.WorkboxError('max-entries-or-age-required', { 477 moduleName: 'workbox-expiration', 478 className: 'CacheExpiration', 479 funcName: 'constructor' 480 }); 481 } 482 if (config.maxEntries) { 483 assert_js.assert.isType(config.maxEntries, 'number', { 484 moduleName: 'workbox-expiration', 485 className: 'CacheExpiration', 486 funcName: 'constructor', 487 paramName: 'config.maxEntries' 488 }); 489 } 490 if (config.maxAgeSeconds) { 491 assert_js.assert.isType(config.maxAgeSeconds, 'number', { 492 moduleName: 'workbox-expiration', 493 className: 'CacheExpiration', 494 funcName: 'constructor', 495 paramName: 'config.maxAgeSeconds' 496 }); 497 } 498 } 499 this._maxEntries = config.maxEntries; 500 this._maxAgeSeconds = config.maxAgeSeconds; 501 this._matchOptions = config.matchOptions; 502 this._cacheName = cacheName; 503 this._timestampModel = new CacheTimestampsModel(cacheName); 504 } 505 /** 506 * Expires entries for the given cache and given criteria. 507 */ 508 async expireEntries() { 509 if (this._isRunning) { 510 this._rerunRequested = true; 511 return; 512 } 513 this._isRunning = true; 514 const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0; 515 const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries); 516 // Delete URLs from the cache 517 const cache = await self.caches.open(this._cacheName); 518 for (const url of urlsExpired) { 519 await cache.delete(url, this._matchOptions); 520 } 521 { 522 if (urlsExpired.length > 0) { 523 logger_js.logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` + `${urlsExpired.length === 1 ? 'it' : 'them'} from the ` + `'${this._cacheName}' cache.`); 524 logger_js.logger.log(`Expired the following ${urlsExpired.length === 1 ? 'URL' : 'URLs'}:`); 525 urlsExpired.forEach(url => logger_js.logger.log(` ${url}`)); 526 logger_js.logger.groupEnd(); 527 } else { 528 logger_js.logger.debug(`Cache expiration ran and found no entries to remove.`); 529 } 530 } 531 this._isRunning = false; 532 if (this._rerunRequested) { 533 this._rerunRequested = false; 534 dontWaitFor_js.dontWaitFor(this.expireEntries()); 535 } 536 } 537 /** 538 * Update the timestamp for the given URL. This ensures the when 539 * removing entries based on maximum entries, most recently used 540 * is accurate or when expiring, the timestamp is up-to-date. 541 * 542 * @param {string} url 543 */ 544 async updateTimestamp(url) { 545 { 546 assert_js.assert.isType(url, 'string', { 547 moduleName: 'workbox-expiration', 548 className: 'CacheExpiration', 549 funcName: 'updateTimestamp', 550 paramName: 'url' 551 }); 552 } 553 await this._timestampModel.setTimestamp(url, Date.now()); 554 } 555 /** 556 * Can be used to check if a URL has expired or not before it's used. 557 * 558 * This requires a look up from IndexedDB, so can be slow. 559 * 560 * Note: This method will not remove the cached entry, call 561 * `expireEntries()` to remove indexedDB and Cache entries. 562 * 563 * @param {string} url 564 * @return {boolean} 565 */ 566 async isURLExpired(url) { 567 if (!this._maxAgeSeconds) { 568 { 569 throw new WorkboxError_js.WorkboxError(`expired-test-without-max-age`, { 570 methodName: 'isURLExpired', 571 paramName: 'maxAgeSeconds' 572 }); 573 } 574 } else { 575 const timestamp = await this._timestampModel.getTimestamp(url); 576 const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000; 577 return timestamp !== undefined ? timestamp < expireOlderThan : true; 578 } 579 } 580 /** 581 * Removes the IndexedDB object store used to keep track of cache expiration 582 * metadata. 583 */ 584 async delete() { 585 // Make sure we don't attempt another rerun if we're called in the middle of 586 // a cache expiration. 587 this._rerunRequested = false;
588 await this._timestampModel.expireEntries(Infinity); // Expires all. 589 } 590 } 591 592 /* 593 Copyright 2018 Google LLC 594 595 Use of this source code is governed by an MIT-style 596 license that can be found in the LICENSE file or at 597 https://opensource.org/licenses/MIT. 598 */ 599 /** 600 * This plugin can be used in a `workbox-strategy` to regularly enforce a 601 * limit on the age and / or the number of cached requests. 602 * 603 * It can only be used with `workbox-strategy` instances that have a 604 * [custom `cacheName` property set](/web/tools/workbox/guides/configure-workbox#custom_cache_names_in_strategies). 605 * In other words, it can't be used to expire entries in strategy that uses the 606 * default runtime cache name. 607 * 608 * Whenever a cached response is used or updated, this plugin will look 609 * at the associated cache and remove any old or extra responses. 610 * 611 * When using `maxAgeSeconds`, responses may be used *once* after expiring 612 * because the expiration clean up will not have occurred until *after* the 613 * cached response has been used. If the response has a "Date" header, then 614 * a light weight expiration check is performed and the response will not be 615 * used immediately. 616 * 617 * When using `maxEntries`, the entry least-recently requested will be removed 618 * from the cache first. 619 * 620 * @memberof workbox-expiration 621 */ 622 class ExpirationPlugin { 623 /** 624 * @param {ExpirationPluginOptions} config 625 * @param {number} [config.maxEntries] The maximum number of entries to cache. 626 * Entries used the least will be removed as the maximum is reached. 627 * @param {number} [config.maxAgeSeconds] The maximum age of an entry before 628 * it's treated as stale and removed. 629 * @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters) 630 * that will be used when calling `delete()` on the cache. 631 * @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to 632 * automatic deletion if the available storage quota has been exceeded. 633 */ 634 constructor(config = {}) { 635 /** 636 * A "lifecycle" callback that will be triggered automatically by the 637 * `workbox-strategies` handlers when a `Response` is about to be returned 638 * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to 639 * the handler. It allows the `Response` to be inspected for freshness and 640 * prevents it from being used if the `Response`'s `Date` header value is 641 * older than the configured `maxAgeSeconds`. 642 * 643 * @param {Object} options 644 * @param {string} options.cacheName Name of the cache the response is in. 645 * @param {Response} options.cachedResponse The `Response` object that's been 646 * read from a cache and whose freshness should be checked. 647 * @return {Response} Either the `cachedResponse`, if it's 648 * fresh, or `null` if the `Response` is older than `maxAgeSeconds`. 649 * 650 * @private 651 */ 652 this.cachedResponseWillBeUsed = async ({ 653 event, 654 request, 655 cacheName, 656 cachedResponse 657 }) => { 658 if (!cachedResponse) { 659 return null; 660 } 661 const isFresh = this._isResponseDateFresh(cachedResponse); 662 // Expire entries to ensure that even if the expiration date has 663 // expired, it'll only be used once. 664 const cacheExpiration = this._getCacheExpiration(cacheName); 665 dontWaitFor_js.dontWaitFor(cacheExpiration.expireEntries()); 666 // Update the metadata for the request URL to the current timestamp, 667 // but don't `await` it as we don't want to block the response. 668 const updateTimestampDone = cacheExpiration.updateTimestamp(request.url); 669 if (event) { 670 try { 671 event.waitUntil(updateTimestampDone); 672 } catch (error) { 673 { 674 // The event may not be a fetch event; only log the URL if it is. 675 if ('request' in event) { 676 logger_js.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache entry for ` + `'${getFriendlyURL_js.getFriendlyURL(event.request.url)}'.`); 677 } 678 } 679 } 680 } 681 return isFresh ? cachedResponse : null; 682 }; 683 /** 684 * A "lifecycle" callback that will be triggered automatically by the 685 * `workbox-strategies` handlers when an entry is added to a cache. 686 * 687 * @param {Object} options 688 * @param {string} options.cacheName Name of the cache that was updated. 689 * @param {string} options.request The Request for the cached entry. 690 * 691 * @private 692 */ 693 this.cacheDidUpdate = async ({ 694 cacheName, 695 request 696 }) => { 697 { 698 assert_js.assert.isType(cacheName, 'string', { 699 moduleName: 'workbox-expiration', 700 className: 'Plugin', 701 funcName: 'cacheDidUpdate', 702 paramName: 'cacheName' 703 }); 704 assert_js.assert.isInstance(request, Request, { 705 moduleName: 'workbox-expiration', 706 className: 'Plugin', 707 funcName: 'cacheDidUpdate', 708 paramName: 'request' 709 }); 710 } 711 const cacheExpiration = this._getCacheExpiration(cacheName); 712 await cacheExpiration.updateTimestamp(request.url); 713 await cacheExpiration.expireEntries(); 714 }; 715 { 716 if (!(config.maxEntries || config.maxAgeSeconds)) { 717 throw new WorkboxError_js.WorkboxError('max-entries-or-age-required', { 718 moduleName: 'workbox-expiration', 719 className: 'Plugin', 720 funcName: 'constructor' 721 }); 722 } 723 if (config.maxEntries) { 724 assert_js.assert.isType(config.maxEntries, 'number', { 725 moduleName: 'workbox-expiration', 726 className: 'Plugin', 727 funcName: 'constructor', 728 paramName: 'config.maxEntries' 729 }); 730 } 731 if (config.maxAgeSeconds) { 732 assert_js.assert.isType(config.maxAgeSeconds, 'number', { 733 moduleName: 'workbox-expiration', 734 className: 'Plugin', 735 funcName: 'constructor', 736 paramName: 'config.maxAgeSeconds' 737 }); 738 } 739 } 740 this._config = config; 741 this._maxAgeSeconds = config.maxAgeSeconds; 742 this._cacheExpirations = new Map(); 743 if (config.purgeOnQuotaError) { 744 registerQuotaErrorCallback_js.registerQuotaErrorCallback(() =>
744 this.deleteCacheAndMetadata()); 745 } 746 } 747 /** 748 * A simple helper method to return a CacheExpiration instance for a given 749 * cache name. 750 * 751 * @param {string} cacheName 752 * @return {CacheExpiration} 753 * 754 * @private 755 */ 756 _getCacheExpiration(cacheName) { 757 if (cacheName === cacheNames_js.cacheNames.getRuntimeName()) { 758 throw new WorkboxError_js.WorkboxError('expire-custom-caches-only'); 759 } 760 let cacheExpiration = this._cacheExpirations.get(cacheName); 761 if (!cacheExpiration) { 762 cacheExpiration = new CacheExpiration(cacheName, this._config); 763 this._cacheExpirations.set(cacheName, cacheExpiration); 764 } 765 return cacheExpiration; 766 } 767 /** 768 * @param {Response} cachedResponse 769 * @return {boolean} 770 * 771 * @private 772 */ 773 _isResponseDateFresh(cachedResponse) { 774 if (!this._maxAgeSeconds) { 775 // We aren't expiring by age, so return true, it's fresh 776 return true; 777 } 778 // Check if the 'date' header will suffice a quick expiration check. 779 // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for 780 // discussion. 781 const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse); 782 if (dateHeaderTimestamp === null) { 783 // Unable to parse date, so assume it's fresh. 784 return true; 785 } 786 // If we have a valid headerTime, then our response is fresh iff the 787 // headerTime plus maxAgeSeconds is greater than the current time. 788 const now = Date.now(); 789 return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000; 790 } 791 /** 792 * This method will extract the data header and parse it into a useful 793 * value. 794 * 795 * @param {Response} cachedResponse 796 * @return {number|null} 797 * 798 * @private 799 */ 800 _getDateHeaderTimestamp(cachedResponse) { 801 if (!cachedResponse.headers.has('date')) { 802 return null; 803 } 804 const dateHeader = cachedResponse.headers.get('date'); 805 const parsedDate = new Date(dateHeader); 806 const headerTime = parsedDate.getTime(); 807 // If the Date header was invalid for some reason, parsedDate.getTime() 808 // will return NaN. 809 if (isNaN(headerTime)) { 810 return null; 811 } 812 return headerTime; 813 } 814 /** 815 * This is a helper method that performs two operations: 816 * 817 * - Deletes *all* the underlying Cache instances associated with this plugin 818 * instance, by calling caches.delete() on your behalf. 819 * - Deletes the metadata from IndexedDB used to keep track of expiration 820 * details for each Cache instance. 821 * 822 * When using cache expiration, calling this method is preferable to calling 823 * `caches.delete()` directly, since this will ensure that the IndexedDB 824 * metadata is also cleanly removed and open IndexedDB instances are deleted. 825 * 826 * Note that if you're *not* using cache expiration for a given cache, calling 827 * `caches.delete()` and passing in the cache's name should be sufficient. 828 * There is no Workbox-specific method needed for cleanup in that case. 829 */ 830 async deleteCacheAndMetadata() { 831 // Do this one at a time instead of all at once via `Promise.all()` to 832 // reduce the chance of inconsistency if a promise rejects. 833 for (const [cacheName, cacheExpiration] of this._cacheExpirations) { 834 await self.caches.delete(cacheName); 835 await cacheExpiration.delete(); 836 } 837 // Reset this._cacheExpirations to its initial state. 838 this._cacheExpirations = new Map(); 839 } 840 } 841 842 exports.CacheExpiration = CacheExpiration; 843 exports.ExpirationPlugin = ExpirationPlugin; 844 845 return exports; 846 847})({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core); 848//# sourceMappingURL=workbox-expiration.dev.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.