text: <!DOCTYPE html>
  <html devsite>
  
  <head><meta name="robots" content="noindex">
    <meta name="project_path" value="/web/tools/workbox/_project.yaml" />
    <meta name="book_path" value="/web/tools/workbox/_book.yaml" />
    <meta name="gtm_var" data-key="docType" data-value="reference">
    <title>Source: workbox-expiration/Plugin.mjs</title>
    <link href="jsdoc.css" rel="stylesheet">
  </head>
  
  <body>
    <div id="jsdoc-body-container">
      <div id="jsdoc-content">
        <div id="jsdoc-content-container">
          <div id="jsdoc-banner" role="banner">
          </div>
          <div id="jsdoc-main" role="main">
            <header class="page-header">
              <h1>Source: workbox-expiration/Plugin.mjs</h1>
            </header>
            <article>
              <pre class="prettyprint linenums"><code>/*
    Copyright 2018 Google LLC
  
    Use of this source code is governed by an MIT-style
    license that can be found in the LICENSE file or at
    https://opensource.org/licenses/MIT.
  */
  
  import {assert} from &#x27;workbox-core/_private/assert.mjs&#x27;;
  import {cacheNames} from &#x27;workbox-core/_private/cacheNames.mjs&#x27;;
  import {getFriendlyURL} from &#x27;workbox-core/_private/getFriendlyURL.mjs&#x27;;
  import {logger} from &#x27;workbox-core/_private/logger.mjs&#x27;;
  import {WorkboxError} from &#x27;workbox-core/_private/WorkboxError.mjs&#x27;;
  import {registerQuotaErrorCallback}
    from &#x27;workbox-core/registerQuotaErrorCallback.mjs&#x27;;
  
  import {CacheExpiration} from &#x27;./CacheExpiration.mjs&#x27;;
  import &#x27;./_version.mjs&#x27;;
  
  /**
   * This plugin can be used in the Workbox APIs to regularly enforce a
   * limit on the age and / or the number of cached requests.
   *
   * Whenever a cached request is used or updated, this plugin will look
   * at the used Cache and remove any old or extra requests.
   *
   * When using &#x60;maxAgeSeconds&#x60;, requests may be used *once* after expiring
   * because the expiration clean up will not have occurred until *after* the
   * cached request has been used. If the request has a &quot;Date&quot; header, then
   * a light weight expiration check is performed and the request will not be
   * used immediately.
   *
   * When using &#x60;maxEntries&#x60;, the entry least-recently requested will be removed from the cache first.
   *
   * @memberof workbox.expiration
   */
  class Plugin {
    /**
     * @param {Object} config
     * @param {number} [config.maxEntries] The maximum number of entries to cache.
     * Entries used the least will be removed as the maximum is reached.
     * @param {number} [config.maxAgeSeconds] The maximum age of an entry before
     * it&#x27;s treated as stale and removed.
     * @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to
     * automatic deletion if the available storage quota has been exceeded.
     */
    constructor(config &#x3D; {}) {
      if (process.env.NODE_ENV !&#x3D;&#x3D; &#x27;production&#x27;) {
        if (!(config.maxEntries || config.maxAgeSeconds)) {
          throw new WorkboxError(&#x27;max-entries-or-age-required&#x27;, {
            moduleName: &#x27;workbox-expiration&#x27;,
            className: &#x27;Plugin&#x27;,
            funcName: &#x27;constructor&#x27;,
          });
        }
  
        if (config.maxEntries) {
          assert.isType(config.maxEntries, &#x27;number&#x27;, {
            moduleName: &#x27;workbox-expiration&#x27;,
            className: &#x27;Plugin&#x27;,
            funcName: &#x27;constructor&#x27;,
            paramName: &#x27;config.maxEntries&#x27;,
          });
        }
  
        if (config.maxAgeSeconds) {
          assert.isType(config.maxAgeSeconds, &#x27;number&#x27;, {
            moduleName: &#x27;workbox-expiration&#x27;,
            className: &#x27;Plugin&#x27;,
            funcName: &#x27;constructor&#x27;,
            paramName: &#x27;config.maxAgeSeconds&#x27;,
          });
        }
      }
  
      this._config &#x3D; config;
      this._maxAgeSeconds &#x3D; config.maxAgeSeconds;
      this._cacheExpirations &#x3D; new Map();
  
      if (config.purgeOnQuotaError) {
        registerQuotaErrorCallback(() &#x3D;&gt; this.deleteCacheAndMetadata());
      }
    }
  
    /**
     * A simple helper method to return a CacheExpiration instance for a given
     * cache name.
     *
     * @param {string} cacheName
     * @return {CacheExpiration}
     *
     * @private
     */
    _getCacheExpiration(cacheName) {
      if (cacheName &#x3D;&#x3D;&#x3D; cacheNames.getRuntimeName()) {
        throw new WorkboxError(&#x27;expire-custom-caches-only&#x27;);
      }
  
      let cacheExpiration &#x3D; this._cacheExpirations.get(cacheName);
      if (!cacheExpiration) {
        cacheExpiration &#x3D; new CacheExpiration(cacheName, this._config);
        this._cacheExpirations.set(cacheName, cacheExpiration);
      }
      return cacheExpiration;
    }
  
    /**
     * A &quot;lifecycle&quot; callback that will be triggered automatically by the
     * &#x60;workbox.strategies&#x60; handlers when a &#x60;Response&#x60; is about to be returned
     * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
     * the handler. It allows the &#x60;Response&#x60; to be inspected for freshness and
     * prevents it from being used if the &#x60;Response&#x60;&#x27;s &#x60;Date&#x60; header value is
     * older than the configured &#x60;maxAgeSeconds&#x60;.
     *
     * @param {Object} options
     * @param {string} options.cacheName Name of the cache the response is in.
     * @param {Response} options.cachedResponse The &#x60;Response&#x60; object that&#x27;s been
     *     read from a cache and whose freshness should be checked.
     * @return {Response} Either the &#x60;cachedResponse&#x60;, if it&#x27;s
     *     fresh, or &#x60;null&#x60; if the &#x60;Response&#x60; is older than &#x60;maxAgeSeconds&#x60;.
     *
     * @private
     */
    cachedResponseWillBeUsed({event, request, cacheName, cachedResponse}) {
      if (!cachedResponse) {
        return null;
      }
  
      let isFresh &#x3D; this._isResponseDateFresh(cachedResponse);
  
      // Expire entries to ensure that even if the expiration date has
      // expired, it&#x27;ll only be used once.
      const cacheExpiration &#x3D; this._getCacheExpiration(cacheName);
      cacheExpiration.expireEntries();
  
      // Update the metadata for the request URL to the current timestamp,
      // but don&#x27;t &#x60;await&#x60; it as we don&#x27;t want to block the response.
      const updateTimestampDone &#x3D; cacheExpiration.updateTimestamp(request.url);
      if (event) {
        try {
          event.waitUntil(updateTimestampDone);
        } catch (error) {
          if (process.env.NODE_ENV !&#x3D;&#x3D; &#x27;production&#x27;) {
            logger.warn(&#x60;Unable to ensure service worker stays alive when &#x60; +
              &#x60;updating cache entry for &#x27;${getFriendlyURL(event.request.url)}&#x27;.&#x60;);
          }
        }
      }
  
      return isFresh ? cachedResponse : null;
    }
  
    /**
     * @param {Response} cachedResponse
     * @return {boolean}
     *
     * @private
     */
    _isResponseDateFresh(cachedResponse) {
      if (!this._maxAgeSeconds) {
        // We aren&#x27;t expiring by age, so return true, it&#x27;s fresh
        return true;
      }
  
      // Check if the &#x27;date&#x27; header will suffice a quick expiration check.
      // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
      // discussion.
      const dateHeaderTimestamp &#x3D; this._getDateHeaderTimestamp(cachedResponse);
      if (dateHeaderTimestamp &#x3D;&#x3D;&#x3D; null) {
        // Unable to parse date, so assume it&#x27;s fresh.
        return true;
      }
  
      // If we have a valid headerTime, then our response is fresh iff the
      // headerTime plus maxAgeSeconds is greater than the current time.
      const now &#x3D; Date.now();
      return dateHeaderTimestamp &gt;&#x3D; now - (this._maxAgeSeconds * 1000);
    }
  
    /**
     * This method will extract the data header and parse it into a useful
     * value.
     *
     * @param {Response} cachedResponse
     * @return {number}
     *
     * @private
     */
    _getDateHeaderTimestamp(cachedResponse) {
      if (!cachedResponse.headers.has(&#x27;date&#x27;)) {
        return null;
      }
  
      const dateHeader &#x3D; cachedResponse.headers.get(&#x27;date&#x27;);
      const parsedDate &#x3D; new Date(dateHeader);
      const headerTime &#x3D; parsedDate.getTime();
  
      // If the Date header was invalid for some reason, parsedDate.getTime()
      // will return NaN.
      if (isNaN(headerTime)) {
        return null;
      }
  
      return headerTime;
    }
  
    /**
     * A &quot;lifecycle&quot; callback that will be triggered automatically by the
     * &#x60;workbox.strategies&#x60; handlers when an entry is added to a cache.
     *
     * @param {Object} options
     * @param {string} options.cacheName Name of the cache that was updated.
     * @param {string} options.request The Request for the cached entry.
     *
     * @private
     */
    async cacheDidUpdate({cacheName, request}) {
      if (process.env.NODE_ENV !&#x3D;&#x3D; &#x27;production&#x27;) {
        assert.isType(cacheName, &#x27;string&#x27;, {
          moduleName: &#x27;workbox-expiration&#x27;,
          className: &#x27;Plugin&#x27;,
          funcName: &#x27;cacheDidUpdate&#x27;,
          paramName: &#x27;cacheName&#x27;,
        });
        assert.isInstance(request, Request, {
          moduleName: &#x27;workbox-expiration&#x27;,
          className: &#x27;Plugin&#x27;,
          funcName: &#x27;cacheDidUpdate&#x27;,
          paramName: &#x27;request&#x27;,
        });
      }
  
      const cacheExpiration &#x3D; this._getCacheExpiration(cacheName);
      await cacheExpiration.updateTimestamp(request.url);
      await cacheExpiration.expireEntries();
    }
  
  
    /**
     * This is a helper method that performs two operations:
     *
     * - Deletes *all* the underlying Cache instances associated with this plugin
     * instance, by calling caches.delete() on your behalf.
     * - Deletes the metadata from IndexedDB used to keep track of expiration
     * details for each Cache instance.
     *
     * When using cache expiration, calling this method is preferable to calling
     * &#x60;caches.delete()&#x60; directly, since this will ensure that the IndexedDB
     * metadata is also cleanly removed and open IndexedDB instances are deleted.
     *
     * Note that if you&#x27;re *not* using cache expiration for a given cache, calling
     * &#x60;caches.delete()&#x60; and passing in the cache&#x27;s name should be sufficient.
     * There is no Workbox-specific method needed for cleanup in that case.
     */
    async deleteCacheAndMetadata() {
      // Do this one at a time instead of all at once via &#x60;Promise.all()&#x60; to
      // reduce the chance of inconsistency if a promise rejects.
      for (const [cacheName, cacheExpiration] of this._cacheExpirations) {
        await caches.delete(cacheName);
        await cacheExpiration.delete();
      }
  
      // Reset this._cacheExpirations to its initial state.
      this._cacheExpirations &#x3D; new Map();
    }
  }
  
  export {Plugin};
  </code></pre>
            </article>
          </div>
        </div>
        <nav id="jsdoc-toc-nav" role="navigation"></nav>
      </div>
    </div>
  </body>
  
  </html>