workbox-expiration.dev.js 32 KB

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