<!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>