0.2.0 - Mid migration

This commit is contained in:
Daniel Mason 2022-04-25 14:47:15 +12:00
parent 139e6a915e
commit 7e38fdbd7d
42393 changed files with 5358157 additions and 62 deletions

View file

@ -0,0 +1,62 @@
import './_version.js';
interface CacheExpirationConfig {
maxEntries?: number;
maxAgeSeconds?: number;
}
/**
* The `CacheExpiration` class allows you define an expiration and / or
* limit on the number of responses stored in a
* [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
*
* @memberof module:workbox-expiration
*/
declare class CacheExpiration {
private _isRunning;
private _rerunRequested;
private readonly _maxEntries?;
private readonly _maxAgeSeconds?;
private readonly _cacheName;
private readonly _timestampModel;
/**
* To construct a new CacheExpiration instance you must provide at least
* one of the `config` properties.
*
* @param {string} cacheName Name of the cache to apply restrictions to.
* @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's treated as stale and removed.
*/
constructor(cacheName: string, config?: CacheExpirationConfig);
/**
* Expires entries for the given cache and given criteria.
*/
expireEntries(): Promise<void>;
/**
* Update the timestamp for the given URL. This ensures the when
* removing entries based on maximum entries, most recently used
* is accurate or when expiring, the timestamp is up-to-date.
*
* @param {string} url
*/
updateTimestamp(url: string): Promise<void>;
/**
* Can be used to check if a URL has expired or not before it's used.
*
* This requires a look up from IndexedDB, so can be slow.
*
* Note: This method will not remove the cached entry, call
* `expireEntries()` to remove indexedDB and Cache entries.
*
* @param {string} url
* @return {boolean}
*/
isURLExpired(url: string): Promise<boolean>;
/**
* Removes the IndexedDB object store used to keep track of cache expiration
* metadata.
*/
delete(): Promise<void>;
}
export { CacheExpiration };

168
web/node_modules/workbox-expiration/CacheExpiration.js generated vendored Normal file
View file

@ -0,0 +1,168 @@
/*
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 'workbox-core/_private/assert.js';
import { dontWaitFor } from 'workbox-core/_private/dontWaitFor.js';
import { logger } from 'workbox-core/_private/logger.js';
import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
import { CacheTimestampsModel } from './models/CacheTimestampsModel.js';
import './_version.js';
/**
* The `CacheExpiration` class allows you define an expiration and / or
* limit on the number of responses stored in a
* [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
*
* @memberof module:workbox-expiration
*/
class CacheExpiration {
/**
* To construct a new CacheExpiration instance you must provide at least
* one of the `config` properties.
*
* @param {string} cacheName Name of the cache to apply restrictions to.
* @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's treated as stale and removed.
*/
constructor(cacheName, config = {}) {
this._isRunning = false;
this._rerunRequested = false;
if (process.env.NODE_ENV !== 'production') {
assert.isType(cacheName, 'string', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'cacheName',
});
if (!(config.maxEntries || config.maxAgeSeconds)) {
throw new WorkboxError('max-entries-or-age-required', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
});
}
if (config.maxEntries) {
assert.isType(config.maxEntries, 'number', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'config.maxEntries',
});
// TODO: Assert is positive
}
if (config.maxAgeSeconds) {
assert.isType(config.maxAgeSeconds, 'number', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'config.maxAgeSeconds',
});
// TODO: Assert is positive
}
}
this._maxEntries = config.maxEntries;
this._maxAgeSeconds = config.maxAgeSeconds;
this._cacheName = cacheName;
this._timestampModel = new CacheTimestampsModel(cacheName);
}
/**
* Expires entries for the given cache and given criteria.
*/
async expireEntries() {
if (this._isRunning) {
this._rerunRequested = true;
return;
}
this._isRunning = true;
const minTimestamp = this._maxAgeSeconds ?
Date.now() - (this._maxAgeSeconds * 1000) : 0;
const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries);
// Delete URLs from the cache
const cache = await self.caches.open(this._cacheName);
for (const url of urlsExpired) {
await cache.delete(url);
}
if (process.env.NODE_ENV !== 'production') {
if (urlsExpired.length > 0) {
logger.groupCollapsed(`Expired ${urlsExpired.length} ` +
`${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` +
`${urlsExpired.length === 1 ? 'it' : 'them'} from the ` +
`'${this._cacheName}' cache.`);
logger.log(`Expired the following ${urlsExpired.length === 1 ?
'URL' : 'URLs'}:`);
urlsExpired.forEach((url) => logger.log(` ${url}`));
logger.groupEnd();
}
else {
logger.debug(`Cache expiration ran and found no entries to remove.`);
}
}
this._isRunning = false;
if (this._rerunRequested) {
this._rerunRequested = false;
dontWaitFor(this.expireEntries());
}
}
/**
* Update the timestamp for the given URL. This ensures the when
* removing entries based on maximum entries, most recently used
* is accurate or when expiring, the timestamp is up-to-date.
*
* @param {string} url
*/
async updateTimestamp(url) {
if (process.env.NODE_ENV !== 'production') {
assert.isType(url, 'string', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'updateTimestamp',
paramName: 'url',
});
}
await this._timestampModel.setTimestamp(url, Date.now());
}
/**
* Can be used to check if a URL has expired or not before it's used.
*
* This requires a look up from IndexedDB, so can be slow.
*
* Note: This method will not remove the cached entry, call
* `expireEntries()` to remove indexedDB and Cache entries.
*
* @param {string} url
* @return {boolean}
*/
async isURLExpired(url) {
if (!this._maxAgeSeconds) {
if (process.env.NODE_ENV !== 'production') {
throw new WorkboxError(`expired-test-without-max-age`, {
methodName: 'isURLExpired',
paramName: 'maxAgeSeconds',
});
}
return false;
}
else {
const timestamp = await this._timestampModel.getTimestamp(url);
const expireOlderThan = Date.now() - (this._maxAgeSeconds * 1000);
return (timestamp < expireOlderThan);
}
}
/**
* Removes the IndexedDB object store used to keep track of cache expiration
* metadata.
*/
async delete() {
// Make sure we don't attempt another rerun if we're called in the middle of
// a cache expiration.
this._rerunRequested = false;
await this._timestampModel.expireEntries(Infinity); // Expires all.
}
}
export { CacheExpiration };

View file

@ -0,0 +1 @@
export * from './CacheExpiration.js';

View file

@ -0,0 +1,113 @@
import { WorkboxPlugin } from 'workbox-core/types.js';
import './_version.js';
/**
* 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 `maxAgeSeconds`, 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 "Date" header, then
* a light weight expiration check is performed and the request will not be
* used immediately.
*
* When using `maxEntries`, the entry least-recently requested will be removed
* from the cache first.
*
* @memberof module:workbox-expiration
*/
declare class ExpirationPlugin implements WorkboxPlugin {
private readonly _config;
private readonly _maxAgeSeconds?;
private _cacheExpirations;
/**
* @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'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?: {
maxEntries?: number;
maxAgeSeconds?: number;
purgeOnQuotaError?: boolean;
});
/**
* A simple helper method to return a CacheExpiration instance for a given
* cache name.
*
* @param {string} cacheName
* @return {CacheExpiration}
*
* @private
*/
private _getCacheExpiration;
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` handlers when a `Response` is about to be returned
* from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
* the handler. It allows the `Response` to be inspected for freshness and
* prevents it from being used if the `Response`'s `Date` header value is
* older than the configured `maxAgeSeconds`.
*
* @param {Object} options
* @param {string} options.cacheName Name of the cache the response is in.
* @param {Response} options.cachedResponse The `Response` object that's been
* read from a cache and whose freshness should be checked.
* @return {Response} Either the `cachedResponse`, if it's
* fresh, or `null` if the `Response` is older than `maxAgeSeconds`.
*
* @private
*/
cachedResponseWillBeUsed: WorkboxPlugin['cachedResponseWillBeUsed'];
/**
* @param {Response} cachedResponse
* @return {boolean}
*
* @private
*/
private _isResponseDateFresh;
/**
* This method will extract the data header and parse it into a useful
* value.
*
* @param {Response} cachedResponse
* @return {number|null}
*
* @private
*/
private _getDateHeaderTimestamp;
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` 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
*/
cacheDidUpdate: WorkboxPlugin['cacheDidUpdate'];
/**
* 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
* `caches.delete()` directly, since this will ensure that the IndexedDB
* metadata is also cleanly removed and open IndexedDB instances are deleted.
*
* Note that if you're *not* using cache expiration for a given cache, calling
* `caches.delete()` and passing in the cache's name should be sufficient.
* There is no Workbox-specific method needed for cleanup in that case.
*/
deleteCacheAndMetadata(): Promise<void>;
}
export { ExpirationPlugin };

247
web/node_modules/workbox-expiration/ExpirationPlugin.js generated vendored Normal file
View file

@ -0,0 +1,247 @@
/*
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 'workbox-core/_private/assert.js';
import { cacheNames } from 'workbox-core/_private/cacheNames.js';
import { dontWaitFor } from 'workbox-core/_private/dontWaitFor.js';
import { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';
import { logger } from 'workbox-core/_private/logger.js';
import { registerQuotaErrorCallback } from 'workbox-core/registerQuotaErrorCallback.js';
import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
import { CacheExpiration } from './CacheExpiration.js';
import './_version.js';
/**
* 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 `maxAgeSeconds`, 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 "Date" header, then
* a light weight expiration check is performed and the request will not be
* used immediately.
*
* When using `maxEntries`, the entry least-recently requested will be removed
* from the cache first.
*
* @memberof module:workbox-expiration
*/
class ExpirationPlugin {
/**
* @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'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 = {}) {
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` handlers when a `Response` is about to be returned
* from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
* the handler. It allows the `Response` to be inspected for freshness and
* prevents it from being used if the `Response`'s `Date` header value is
* older than the configured `maxAgeSeconds`.
*
* @param {Object} options
* @param {string} options.cacheName Name of the cache the response is in.
* @param {Response} options.cachedResponse The `Response` object that's been
* read from a cache and whose freshness should be checked.
* @return {Response} Either the `cachedResponse`, if it's
* fresh, or `null` if the `Response` is older than `maxAgeSeconds`.
*
* @private
*/
this.cachedResponseWillBeUsed = async ({ event, request, cacheName, cachedResponse }) => {
if (!cachedResponse) {
return null;
}
const isFresh = this._isResponseDateFresh(cachedResponse);
// Expire entries to ensure that even if the expiration date has
// expired, it'll only be used once.
const cacheExpiration = this._getCacheExpiration(cacheName);
dontWaitFor(cacheExpiration.expireEntries());
// Update the metadata for the request URL to the current timestamp,
// but don't `await` it as we don't want to block the response.
const updateTimestampDone = cacheExpiration.updateTimestamp(request.url);
if (event) {
try {
event.waitUntil(updateTimestampDone);
}
catch (error) {
if (process.env.NODE_ENV !== 'production') {
// The event may not be a fetch event; only log the URL if it is.
if ('request' in event) {
logger.warn(`Unable to ensure service worker stays alive when ` +
`updating cache entry for ` +
`'${getFriendlyURL(event.request.url)}'.`);
}
}
}
}
return isFresh ? cachedResponse : null;
};
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` 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
*/
this.cacheDidUpdate = async ({ cacheName, request }) => {
if (process.env.NODE_ENV !== 'production') {
assert.isType(cacheName, 'string', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'cacheDidUpdate',
paramName: 'cacheName',
});
assert.isInstance(request, Request, {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'cacheDidUpdate',
paramName: 'request',
});
}
const cacheExpiration = this._getCacheExpiration(cacheName);
await cacheExpiration.updateTimestamp(request.url);
await cacheExpiration.expireEntries();
};
if (process.env.NODE_ENV !== 'production') {
if (!(config.maxEntries || config.maxAgeSeconds)) {
throw new WorkboxError('max-entries-or-age-required', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
});
}
if (config.maxEntries) {
assert.isType(config.maxEntries, 'number', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
paramName: 'config.maxEntries',
});
}
if (config.maxAgeSeconds) {
assert.isType(config.maxAgeSeconds, 'number', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
paramName: 'config.maxAgeSeconds',
});
}
}
this._config = config;
this._maxAgeSeconds = config.maxAgeSeconds;
this._cacheExpirations = new Map();
if (config.purgeOnQuotaError) {
registerQuotaErrorCallback(() => 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 === cacheNames.getRuntimeName()) {
throw new WorkboxError('expire-custom-caches-only');
}
let cacheExpiration = this._cacheExpirations.get(cacheName);
if (!cacheExpiration) {
cacheExpiration = new CacheExpiration(cacheName, this._config);
this._cacheExpirations.set(cacheName, cacheExpiration);
}
return cacheExpiration;
}
/**
* @param {Response} cachedResponse
* @return {boolean}
*
* @private
*/
_isResponseDateFresh(cachedResponse) {
if (!this._maxAgeSeconds) {
// We aren't expiring by age, so return true, it's fresh
return true;
}
// Check if the 'date' header will suffice a quick expiration check.
// See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
// discussion.
const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
if (dateHeaderTimestamp === null) {
// Unable to parse date, so assume it'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 = Date.now();
return dateHeaderTimestamp >= now - (this._maxAgeSeconds * 1000);
}
/**
* This method will extract the data header and parse it into a useful
* value.
*
* @param {Response} cachedResponse
* @return {number|null}
*
* @private
*/
_getDateHeaderTimestamp(cachedResponse) {
if (!cachedResponse.headers.has('date')) {
return null;
}
const dateHeader = cachedResponse.headers.get('date');
const parsedDate = new Date(dateHeader);
const headerTime = parsedDate.getTime();
// If the Date header was invalid for some reason, parsedDate.getTime()
// will return NaN.
if (isNaN(headerTime)) {
return null;
}
return headerTime;
}
/**
* 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
* `caches.delete()` directly, since this will ensure that the IndexedDB
* metadata is also cleanly removed and open IndexedDB instances are deleted.
*
* Note that if you're *not* using cache expiration for a given cache, calling
* `caches.delete()` and passing in the cache'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 `Promise.all()` to
// reduce the chance of inconsistency if a promise rejects.
for (const [cacheName, cacheExpiration] of this._cacheExpirations) {
await self.caches.delete(cacheName);
await cacheExpiration.delete();
}
// Reset this._cacheExpirations to its initial state.
this._cacheExpirations = new Map();
}
}
export { ExpirationPlugin };

View file

@ -0,0 +1 @@
export * from './ExpirationPlugin.js';

19
web/node_modules/workbox-expiration/LICENSE generated vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright 2018 Google LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1
web/node_modules/workbox-expiration/README.md generated vendored Normal file
View file

@ -0,0 +1 @@
This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-expiration

0
web/node_modules/workbox-expiration/_version.d.ts generated vendored Normal file
View file

6
web/node_modules/workbox-expiration/_version.js generated vendored Normal file
View file

@ -0,0 +1,6 @@
"use strict";
// @ts-ignore
try {
self['workbox:expiration:5.1.4'] && _();
}
catch (e) { }

1
web/node_modules/workbox-expiration/_version.mjs generated vendored Normal file
View file

@ -0,0 +1 @@
try{self['workbox:expiration:5.1.4']&&_()}catch(e){}// eslint-disable-line

View file

@ -0,0 +1,649 @@
this.workbox = this.workbox || {};
this.workbox.expiration = (function (exports, assert_js, dontWaitFor_js, logger_js, WorkboxError_js, DBWrapper_js, deleteDatabase_js, cacheNames_js, getFriendlyURL_js, registerQuotaErrorCallback_js) {
'use strict';
try {
self['workbox:expiration:5.1.4'] && _();
} catch (e) {}
/*
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.
*/
const DB_NAME = 'workbox-expiration';
const OBJECT_STORE_NAME = 'cache-entries';
const normalizeURL = unNormalizedUrl => {
const url = new URL(unNormalizedUrl, location.href);
url.hash = '';
return url.href;
};
/**
* Returns the timestamp model.
*
* @private
*/
class CacheTimestampsModel {
/**
*
* @param {string} cacheName
*
* @private
*/
constructor(cacheName) {
this._cacheName = cacheName;
this._db = new DBWrapper_js.DBWrapper(DB_NAME, 1, {
onupgradeneeded: event => this._handleUpgrade(event)
});
}
/**
* Should perform an upgrade of indexedDB.
*
* @param {Event} event
*
* @private
*/
_handleUpgrade(event) {
const db = event.target.result; // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we
// have to use the `id` keyPath here and create our own values (a
// concatenation of `url + cacheName`) instead of simply using
// `keyPath: ['url', 'cacheName']`, which is supported in other browsers.
const objStore = db.createObjectStore(OBJECT_STORE_NAME, {
keyPath: 'id'
}); // TODO(philipwalton): once we don't have to support EdgeHTML, we can
// create a single index with the keyPath `['cacheName', 'timestamp']`
// instead of doing both these indexes.
objStore.createIndex('cacheName', 'cacheName', {
unique: false
});
objStore.createIndex('timestamp', 'timestamp', {
unique: false
}); // Previous versions of `workbox-expiration` used `this._cacheName`
// as the IDBDatabase name.
deleteDatabase_js.deleteDatabase(this._cacheName);
}
/**
* @param {string} url
* @param {number} timestamp
*
* @private
*/
async setTimestamp(url, timestamp) {
url = normalizeURL(url);
const entry = {
url,
timestamp,
cacheName: this._cacheName,
// Creating an ID from the URL and cache name won't be necessary once
// Edge switches to Chromium and all browsers we support work with
// array keyPaths.
id: this._getId(url)
};
await this._db.put(OBJECT_STORE_NAME, entry);
}
/**
* Returns the timestamp stored for a given URL.
*
* @param {string} url
* @return {number}
*
* @private
*/
async getTimestamp(url) {
const entry = await this._db.get(OBJECT_STORE_NAME, this._getId(url));
return entry.timestamp;
}
/**
* Iterates through all the entries in the object store (from newest to
* oldest) and removes entries once either `maxCount` is reached or the
* entry's timestamp is less than `minTimestamp`.
*
* @param {number} minTimestamp
* @param {number} maxCount
* @return {Array<string>}
*
* @private
*/
async expireEntries(minTimestamp, maxCount) {
const entriesToDelete = await this._db.transaction(OBJECT_STORE_NAME, 'readwrite', (txn, done) => {
const store = txn.objectStore(OBJECT_STORE_NAME);
const request = store.index('timestamp').openCursor(null, 'prev');
const entriesToDelete = [];
let entriesNotDeletedCount = 0;
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
const result = cursor.value; // TODO(philipwalton): once we can use a multi-key index, we
// won't have to check `cacheName` here.
if (result.cacheName === this._cacheName) {
// Delete an entry if it's older than the max age or
// if we already have the max number allowed.
if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) {
// TODO(philipwalton): we should be able to delete the
// entry right here, but doing so causes an iteration
// bug in Safari stable (fixed in TP). Instead we can
// store the keys of the entries to delete, and then
// delete the separate transactions.
// https://github.com/GoogleChrome/workbox/issues/1978
// cursor.delete();
// We only need to return the URL, not the whole entry.
entriesToDelete.push(cursor.value);
} else {
entriesNotDeletedCount++;
}
}
cursor.continue();
} else {
done(entriesToDelete);
}
};
}); // TODO(philipwalton): once the Safari bug in the following issue is fixed,
// we should be able to remove this loop and do the entry deletion in the
// cursor loop above:
// https://github.com/GoogleChrome/workbox/issues/1978
const urlsDeleted = [];
for (const entry of entriesToDelete) {
await this._db.delete(OBJECT_STORE_NAME, entry.id);
urlsDeleted.push(entry.url);
}
return urlsDeleted;
}
/**
* Takes a URL and returns an ID that will be unique in the object store.
*
* @param {string} url
* @return {string}
*
* @private
*/
_getId(url) {
// Creating an ID from the URL and cache name won't be necessary once
// Edge switches to Chromium and all browsers we support work with
// array keyPaths.
return this._cacheName + '|' + normalizeURL(url);
}
}
/*
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.
*/
/**
* The `CacheExpiration` class allows you define an expiration and / or
* limit on the number of responses stored in a
* [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
*
* @memberof module:workbox-expiration
*/
class CacheExpiration {
/**
* To construct a new CacheExpiration instance you must provide at least
* one of the `config` properties.
*
* @param {string} cacheName Name of the cache to apply restrictions to.
* @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's treated as stale and removed.
*/
constructor(cacheName, config = {}) {
this._isRunning = false;
this._rerunRequested = false;
{
assert_js.assert.isType(cacheName, 'string', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'cacheName'
});
if (!(config.maxEntries || config.maxAgeSeconds)) {
throw new WorkboxError_js.WorkboxError('max-entries-or-age-required', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor'
});
}
if (config.maxEntries) {
assert_js.assert.isType(config.maxEntries, 'number', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'config.maxEntries'
}); // TODO: Assert is positive
}
if (config.maxAgeSeconds) {
assert_js.assert.isType(config.maxAgeSeconds, 'number', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'config.maxAgeSeconds'
}); // TODO: Assert is positive
}
}
this._maxEntries = config.maxEntries;
this._maxAgeSeconds = config.maxAgeSeconds;
this._cacheName = cacheName;
this._timestampModel = new CacheTimestampsModel(cacheName);
}
/**
* Expires entries for the given cache and given criteria.
*/
async expireEntries() {
if (this._isRunning) {
this._rerunRequested = true;
return;
}
this._isRunning = true;
const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0;
const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries); // Delete URLs from the cache
const cache = await self.caches.open(this._cacheName);
for (const url of urlsExpired) {
await cache.delete(url);
}
{
if (urlsExpired.length > 0) {
logger_js.logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` + `${urlsExpired.length === 1 ? 'it' : 'them'} from the ` + `'${this._cacheName}' cache.`);
logger_js.logger.log(`Expired the following ${urlsExpired.length === 1 ? 'URL' : 'URLs'}:`);
urlsExpired.forEach(url => logger_js.logger.log(` ${url}`));
logger_js.logger.groupEnd();
} else {
logger_js.logger.debug(`Cache expiration ran and found no entries to remove.`);
}
}
this._isRunning = false;
if (this._rerunRequested) {
this._rerunRequested = false;
dontWaitFor_js.dontWaitFor(this.expireEntries());
}
}
/**
* Update the timestamp for the given URL. This ensures the when
* removing entries based on maximum entries, most recently used
* is accurate or when expiring, the timestamp is up-to-date.
*
* @param {string} url
*/
async updateTimestamp(url) {
{
assert_js.assert.isType(url, 'string', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'updateTimestamp',
paramName: 'url'
});
}
await this._timestampModel.setTimestamp(url, Date.now());
}
/**
* Can be used to check if a URL has expired or not before it's used.
*
* This requires a look up from IndexedDB, so can be slow.
*
* Note: This method will not remove the cached entry, call
* `expireEntries()` to remove indexedDB and Cache entries.
*
* @param {string} url
* @return {boolean}
*/
async isURLExpired(url) {
if (!this._maxAgeSeconds) {
{
throw new WorkboxError_js.WorkboxError(`expired-test-without-max-age`, {
methodName: 'isURLExpired',
paramName: 'maxAgeSeconds'
});
}
} else {
const timestamp = await this._timestampModel.getTimestamp(url);
const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000;
return timestamp < expireOlderThan;
}
}
/**
* Removes the IndexedDB object store used to keep track of cache expiration
* metadata.
*/
async delete() {
// Make sure we don't attempt another rerun if we're called in the middle of
// a cache expiration.
this._rerunRequested = false;
await this._timestampModel.expireEntries(Infinity); // Expires all.
}
}
/*
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.
*/
/**
* 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 `maxAgeSeconds`, 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 "Date" header, then
* a light weight expiration check is performed and the request will not be
* used immediately.
*
* When using `maxEntries`, the entry least-recently requested will be removed
* from the cache first.
*
* @memberof module:workbox-expiration
*/
class ExpirationPlugin {
/**
* @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'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 = {}) {
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` handlers when a `Response` is about to be returned
* from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
* the handler. It allows the `Response` to be inspected for freshness and
* prevents it from being used if the `Response`'s `Date` header value is
* older than the configured `maxAgeSeconds`.
*
* @param {Object} options
* @param {string} options.cacheName Name of the cache the response is in.
* @param {Response} options.cachedResponse The `Response` object that's been
* read from a cache and whose freshness should be checked.
* @return {Response} Either the `cachedResponse`, if it's
* fresh, or `null` if the `Response` is older than `maxAgeSeconds`.
*
* @private
*/
this.cachedResponseWillBeUsed = async ({
event,
request,
cacheName,
cachedResponse
}) => {
if (!cachedResponse) {
return null;
}
const isFresh = this._isResponseDateFresh(cachedResponse); // Expire entries to ensure that even if the expiration date has
// expired, it'll only be used once.
const cacheExpiration = this._getCacheExpiration(cacheName);
dontWaitFor_js.dontWaitFor(cacheExpiration.expireEntries()); // Update the metadata for the request URL to the current timestamp,
// but don't `await` it as we don't want to block the response.
const updateTimestampDone = cacheExpiration.updateTimestamp(request.url);
if (event) {
try {
event.waitUntil(updateTimestampDone);
} catch (error) {
{
// The event may not be a fetch event; only log the URL if it is.
if ('request' in event) {
logger_js.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache entry for ` + `'${getFriendlyURL_js.getFriendlyURL(event.request.url)}'.`);
}
}
}
}
return isFresh ? cachedResponse : null;
};
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` 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
*/
this.cacheDidUpdate = async ({
cacheName,
request
}) => {
{
assert_js.assert.isType(cacheName, 'string', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'cacheDidUpdate',
paramName: 'cacheName'
});
assert_js.assert.isInstance(request, Request, {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'cacheDidUpdate',
paramName: 'request'
});
}
const cacheExpiration = this._getCacheExpiration(cacheName);
await cacheExpiration.updateTimestamp(request.url);
await cacheExpiration.expireEntries();
};
{
if (!(config.maxEntries || config.maxAgeSeconds)) {
throw new WorkboxError_js.WorkboxError('max-entries-or-age-required', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor'
});
}
if (config.maxEntries) {
assert_js.assert.isType(config.maxEntries, 'number', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
paramName: 'config.maxEntries'
});
}
if (config.maxAgeSeconds) {
assert_js.assert.isType(config.maxAgeSeconds, 'number', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
paramName: 'config.maxAgeSeconds'
});
}
}
this._config = config;
this._maxAgeSeconds = config.maxAgeSeconds;
this._cacheExpirations = new Map();
if (config.purgeOnQuotaError) {
registerQuotaErrorCallback_js.registerQuotaErrorCallback(() => 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 === cacheNames_js.cacheNames.getRuntimeName()) {
throw new WorkboxError_js.WorkboxError('expire-custom-caches-only');
}
let cacheExpiration = this._cacheExpirations.get(cacheName);
if (!cacheExpiration) {
cacheExpiration = new CacheExpiration(cacheName, this._config);
this._cacheExpirations.set(cacheName, cacheExpiration);
}
return cacheExpiration;
}
/**
* @param {Response} cachedResponse
* @return {boolean}
*
* @private
*/
_isResponseDateFresh(cachedResponse) {
if (!this._maxAgeSeconds) {
// We aren't expiring by age, so return true, it's fresh
return true;
} // Check if the 'date' header will suffice a quick expiration check.
// See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
// discussion.
const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
if (dateHeaderTimestamp === null) {
// Unable to parse date, so assume it'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 = Date.now();
return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000;
}
/**
* This method will extract the data header and parse it into a useful
* value.
*
* @param {Response} cachedResponse
* @return {number|null}
*
* @private
*/
_getDateHeaderTimestamp(cachedResponse) {
if (!cachedResponse.headers.has('date')) {
return null;
}
const dateHeader = cachedResponse.headers.get('date');
const parsedDate = new Date(dateHeader);
const headerTime = parsedDate.getTime(); // If the Date header was invalid for some reason, parsedDate.getTime()
// will return NaN.
if (isNaN(headerTime)) {
return null;
}
return headerTime;
}
/**
* 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
* `caches.delete()` directly, since this will ensure that the IndexedDB
* metadata is also cleanly removed and open IndexedDB instances are deleted.
*
* Note that if you're *not* using cache expiration for a given cache, calling
* `caches.delete()` and passing in the cache'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 `Promise.all()` to
// reduce the chance of inconsistency if a promise rejects.
for (const [cacheName, cacheExpiration] of this._cacheExpirations) {
await self.caches.delete(cacheName);
await cacheExpiration.delete();
} // Reset this._cacheExpirations to its initial state.
this._cacheExpirations = new Map();
}
}
exports.CacheExpiration = CacheExpiration;
exports.ExpirationPlugin = ExpirationPlugin;
return exports;
}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core));
//# sourceMappingURL=workbox-expiration.dev.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.expiration=function(t,e,s,i,a,n,h){"use strict";try{self["workbox:expiration:5.1.4"]&&_()}catch(t){}const r=t=>{const e=new URL(t,location.href);return e.hash="",e.href};class c{constructor(t){this.t=t,this.s=new i.DBWrapper("workbox-expiration",1,{onupgradeneeded:t=>this.i(t)})}i(t){const e=t.target.result.createObjectStore("cache-entries",{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1}),a.deleteDatabase(this.t)}async setTimestamp(t,e){const s={url:t=r(t),timestamp:e,cacheName:this.t,id:this.h(t)};await this.s.put("cache-entries",s)}async getTimestamp(t){return(await this.s.get("cache-entries",this.h(t))).timestamp}async expireEntries(t,e){const s=await this.s.transaction("cache-entries","readwrite",(s,i)=>{const a=s.objectStore("cache-entries").index("timestamp").openCursor(null,"prev"),n=[];let h=0;a.onsuccess=()=>{const s=a.result;if(s){const i=s.value;i.cacheName===this.t&&(t&&i.timestamp<t||e&&h>=e?n.push(s.value):h++),s.continue()}else i(n)}}),i=[];for(const t of s)await this.s.delete("cache-entries",t.id),i.push(t.url);return i}h(t){return this.t+"|"+r(t)}}class o{constructor(t,e={}){this.o=!1,this.u=!1,this.l=e.maxEntries,this.m=e.maxAgeSeconds,this.t=t,this.p=new c(t)}async expireEntries(){if(this.o)return void(this.u=!0);this.o=!0;const t=this.m?Date.now()-1e3*this.m:0,s=await this.p.expireEntries(t,this.l),i=await self.caches.open(this.t);for(const t of s)await i.delete(t);this.o=!1,this.u&&(this.u=!1,e.dontWaitFor(this.expireEntries()))}async updateTimestamp(t){await this.p.setTimestamp(t,Date.now())}async isURLExpired(t){if(this.m){return await this.p.getTimestamp(t)<Date.now()-1e3*this.m}return!1}async delete(){this.u=!1,await this.p.expireEntries(1/0)}}return t.CacheExpiration=o,t.ExpirationPlugin=class{constructor(t={}){this.cachedResponseWillBeUsed=async({event:t,request:s,cacheName:i,cachedResponse:a})=>{if(!a)return null;const n=this.k(a),h=this.D(i);e.dontWaitFor(h.expireEntries());const r=h.updateTimestamp(s.url);if(t)try{t.waitUntil(r)}catch(t){}return n?a:null},this.cacheDidUpdate=async({cacheName:t,request:e})=>{const s=this.D(t);await s.updateTimestamp(e.url),await s.expireEntries()},this.N=t,this.m=t.maxAgeSeconds,this.g=new Map,t.purgeOnQuotaError&&h.registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata())}D(t){if(t===n.cacheNames.getRuntimeName())throw new s.WorkboxError("expire-custom-caches-only");let e=this.g.get(t);return e||(e=new o(t,this.N),this.g.set(t,e)),e}k(t){if(!this.m)return!0;const e=this._(t);if(null===e)return!0;return e>=Date.now()-1e3*this.m}_(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async deleteCacheAndMetadata(){for(const[t,e]of this.g)await self.caches.delete(t),await e.delete();this.g=new Map}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core);
//# sourceMappingURL=workbox-expiration.prod.js.map

File diff suppressed because one or more lines are too long

7
web/node_modules/workbox-expiration/index.d.ts generated vendored Normal file
View file

@ -0,0 +1,7 @@
import { CacheExpiration } from './CacheExpiration.js';
import { ExpirationPlugin } from './ExpirationPlugin.js';
import './_version.js';
/**
* @module workbox-expiration
*/
export { CacheExpiration, ExpirationPlugin, };

14
web/node_modules/workbox-expiration/index.js generated vendored Normal file
View file

@ -0,0 +1,14 @@
/*
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 { CacheExpiration } from './CacheExpiration.js';
import { ExpirationPlugin } from './ExpirationPlugin.js';
import './_version.js';
/**
* @module workbox-expiration
*/
export { CacheExpiration, ExpirationPlugin, };

1
web/node_modules/workbox-expiration/index.mjs generated vendored Normal file
View file

@ -0,0 +1 @@
export * from './index.js';

View file

@ -0,0 +1,63 @@
import '../_version.js';
/**
* Returns the timestamp model.
*
* @private
*/
declare class CacheTimestampsModel {
private readonly _cacheName;
private readonly _db;
/**
*
* @param {string} cacheName
*
* @private
*/
constructor(cacheName: string);
/**
* Should perform an upgrade of indexedDB.
*
* @param {Event} event
*
* @private
*/
private _handleUpgrade;
/**
* @param {string} url
* @param {number} timestamp
*
* @private
*/
setTimestamp(url: string, timestamp: number): Promise<void>;
/**
* Returns the timestamp stored for a given URL.
*
* @param {string} url
* @return {number}
*
* @private
*/
getTimestamp(url: string): Promise<number>;
/**
* Iterates through all the entries in the object store (from newest to
* oldest) and removes entries once either `maxCount` is reached or the
* entry's timestamp is less than `minTimestamp`.
*
* @param {number} minTimestamp
* @param {number} maxCount
* @return {Array<string>}
*
* @private
*/
expireEntries(minTimestamp: number, maxCount?: number): Promise<string[]>;
/**
* Takes a URL and returns an ID that will be unique in the object store.
*
* @param {string} url
* @return {string}
*
* @private
*/
private _getId;
}
export { CacheTimestampsModel };

View file

@ -0,0 +1,165 @@
/*
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 { DBWrapper } from 'workbox-core/_private/DBWrapper.js';
import { deleteDatabase } from 'workbox-core/_private/deleteDatabase.js';
import '../_version.js';
const DB_NAME = 'workbox-expiration';
const OBJECT_STORE_NAME = 'cache-entries';
const normalizeURL = (unNormalizedUrl) => {
const url = new URL(unNormalizedUrl, location.href);
url.hash = '';
return url.href;
};
/**
* Returns the timestamp model.
*
* @private
*/
class CacheTimestampsModel {
/**
*
* @param {string} cacheName
*
* @private
*/
constructor(cacheName) {
this._cacheName = cacheName;
this._db = new DBWrapper(DB_NAME, 1, {
onupgradeneeded: (event) => this._handleUpgrade(event),
});
}
/**
* Should perform an upgrade of indexedDB.
*
* @param {Event} event
*
* @private
*/
_handleUpgrade(event) {
const db = event.target.result;
// TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we
// have to use the `id` keyPath here and create our own values (a
// concatenation of `url + cacheName`) instead of simply using
// `keyPath: ['url', 'cacheName']`, which is supported in other browsers.
const objStore = db.createObjectStore(OBJECT_STORE_NAME, { keyPath: 'id' });
// TODO(philipwalton): once we don't have to support EdgeHTML, we can
// create a single index with the keyPath `['cacheName', 'timestamp']`
// instead of doing both these indexes.
objStore.createIndex('cacheName', 'cacheName', { unique: false });
objStore.createIndex('timestamp', 'timestamp', { unique: false });
// Previous versions of `workbox-expiration` used `this._cacheName`
// as the IDBDatabase name.
deleteDatabase(this._cacheName);
}
/**
* @param {string} url
* @param {number} timestamp
*
* @private
*/
async setTimestamp(url, timestamp) {
url = normalizeURL(url);
const entry = {
url,
timestamp,
cacheName: this._cacheName,
// Creating an ID from the URL and cache name won't be necessary once
// Edge switches to Chromium and all browsers we support work with
// array keyPaths.
id: this._getId(url),
};
await this._db.put(OBJECT_STORE_NAME, entry);
}
/**
* Returns the timestamp stored for a given URL.
*
* @param {string} url
* @return {number}
*
* @private
*/
async getTimestamp(url) {
const entry = await this._db.get(OBJECT_STORE_NAME, this._getId(url));
return entry.timestamp;
}
/**
* Iterates through all the entries in the object store (from newest to
* oldest) and removes entries once either `maxCount` is reached or the
* entry's timestamp is less than `minTimestamp`.
*
* @param {number} minTimestamp
* @param {number} maxCount
* @return {Array<string>}
*
* @private
*/
async expireEntries(minTimestamp, maxCount) {
const entriesToDelete = await this._db.transaction(OBJECT_STORE_NAME, 'readwrite', (txn, done) => {
const store = txn.objectStore(OBJECT_STORE_NAME);
const request = store.index('timestamp').openCursor(null, 'prev');
const entriesToDelete = [];
let entriesNotDeletedCount = 0;
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
const result = cursor.value;
// TODO(philipwalton): once we can use a multi-key index, we
// won't have to check `cacheName` here.
if (result.cacheName === this._cacheName) {
// Delete an entry if it's older than the max age or
// if we already have the max number allowed.
if ((minTimestamp && result.timestamp < minTimestamp) ||
(maxCount && entriesNotDeletedCount >= maxCount)) {
// TODO(philipwalton): we should be able to delete the
// entry right here, but doing so causes an iteration
// bug in Safari stable (fixed in TP). Instead we can
// store the keys of the entries to delete, and then
// delete the separate transactions.
// https://github.com/GoogleChrome/workbox/issues/1978
// cursor.delete();
// We only need to return the URL, not the whole entry.
entriesToDelete.push(cursor.value);
}
else {
entriesNotDeletedCount++;
}
}
cursor.continue();
}
else {
done(entriesToDelete);
}
};
});
// TODO(philipwalton): once the Safari bug in the following issue is fixed,
// we should be able to remove this loop and do the entry deletion in the
// cursor loop above:
// https://github.com/GoogleChrome/workbox/issues/1978
const urlsDeleted = [];
for (const entry of entriesToDelete) {
await this._db.delete(OBJECT_STORE_NAME, entry.id);
urlsDeleted.push(entry.url);
}
return urlsDeleted;
}
/**
* Takes a URL and returns an ID that will be unique in the object store.
*
* @param {string} url
* @return {string}
*
* @private
*/
_getId(url) {
// Creating an ID from the URL and cache name won't be necessary once
// Edge switches to Chromium and all browsers we support work with
// array keyPaths.
return this._cacheName + '|' + normalizeURL(url);
}
}
export { CacheTimestampsModel };

View file

@ -0,0 +1 @@
export * from './CacheTimestampsModel.js';

33
web/node_modules/workbox-expiration/package.json generated vendored Normal file
View file

@ -0,0 +1,33 @@
{
"name": "workbox-expiration",
"version": "5.1.4",
"license": "MIT",
"author": "Google's Web DevRel Team",
"description": "A service worker helper library that expires cached responses based on age or maximum number of entries.",
"repository": "googlechrome/workbox",
"bugs": "https://github.com/googlechrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"sw",
"workbox-plugin"
],
"scripts": {
"build": "gulp build-packages --package workbox-expiration",
"version": "npm run build",
"prepare": "npm run build"
},
"workbox": {
"browserNamespace": "workbox.expiration",
"packageType": "browser"
},
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"dependencies": {
"workbox-core": "^5.1.4"
},
"gitHead": "a95b6fd489c2a66574f1655b2de3acd2ece35fb3"
}

View file

@ -0,0 +1,200 @@
/*
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 'workbox-core/_private/assert.js';
import {dontWaitFor} from 'workbox-core/_private/dontWaitFor.js';
import {logger} from 'workbox-core/_private/logger.js';
import {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
import {CacheTimestampsModel} from './models/CacheTimestampsModel.js';
import './_version.js';
interface CacheExpirationConfig {
maxEntries?: number;
maxAgeSeconds?: number;
}
/**
* The `CacheExpiration` class allows you define an expiration and / or
* limit on the number of responses stored in a
* [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
*
* @memberof module:workbox-expiration
*/
class CacheExpiration {
private _isRunning = false;
private _rerunRequested = false;
private readonly _maxEntries?: number;
private readonly _maxAgeSeconds?: number;
private readonly _cacheName: string;
private readonly _timestampModel: CacheTimestampsModel;
/**
* To construct a new CacheExpiration instance you must provide at least
* one of the `config` properties.
*
* @param {string} cacheName Name of the cache to apply restrictions to.
* @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's treated as stale and removed.
*/
constructor(cacheName: string, config: CacheExpirationConfig = {}) {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(cacheName, 'string', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'cacheName',
});
if (!(config.maxEntries || config.maxAgeSeconds)) {
throw new WorkboxError('max-entries-or-age-required', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
});
}
if (config.maxEntries) {
assert!.isType(config.maxEntries, 'number', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'config.maxEntries',
});
// TODO: Assert is positive
}
if (config.maxAgeSeconds) {
assert!.isType(config.maxAgeSeconds, 'number', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'config.maxAgeSeconds',
});
// TODO: Assert is positive
}
}
this._maxEntries = config.maxEntries;
this._maxAgeSeconds = config.maxAgeSeconds;
this._cacheName = cacheName;
this._timestampModel = new CacheTimestampsModel(cacheName);
}
/**
* Expires entries for the given cache and given criteria.
*/
async expireEntries() {
if (this._isRunning) {
this._rerunRequested = true;
return;
}
this._isRunning = true;
const minTimestamp = this._maxAgeSeconds ?
Date.now() - (this._maxAgeSeconds * 1000) : 0;
const urlsExpired = await this._timestampModel.expireEntries(
minTimestamp, this._maxEntries);
// Delete URLs from the cache
const cache = await self.caches.open(this._cacheName);
for (const url of urlsExpired) {
await cache.delete(url);
}
if (process.env.NODE_ENV !== 'production') {
if (urlsExpired.length > 0) {
logger.groupCollapsed(
`Expired ${urlsExpired.length} ` +
`${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` +
`${urlsExpired.length === 1 ? 'it' : 'them'} from the ` +
`'${this._cacheName}' cache.`);
logger.log(`Expired the following ${urlsExpired.length === 1 ?
'URL' : 'URLs'}:`);
urlsExpired.forEach((url) => logger.log(` ${url}`));
logger.groupEnd();
} else {
logger.debug(`Cache expiration ran and found no entries to remove.`);
}
}
this._isRunning = false;
if (this._rerunRequested) {
this._rerunRequested = false;
dontWaitFor(this.expireEntries());
}
}
/**
* Update the timestamp for the given URL. This ensures the when
* removing entries based on maximum entries, most recently used
* is accurate or when expiring, the timestamp is up-to-date.
*
* @param {string} url
*/
async updateTimestamp(url: string) {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(url, 'string', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'updateTimestamp',
paramName: 'url',
});
}
await this._timestampModel.setTimestamp(url, Date.now());
}
/**
* Can be used to check if a URL has expired or not before it's used.
*
* This requires a look up from IndexedDB, so can be slow.
*
* Note: This method will not remove the cached entry, call
* `expireEntries()` to remove indexedDB and Cache entries.
*
* @param {string} url
* @return {boolean}
*/
async isURLExpired(url: string): Promise<boolean> {
if (!this._maxAgeSeconds) {
if (process.env.NODE_ENV !== 'production') {
throw new WorkboxError(`expired-test-without-max-age`, {
methodName: 'isURLExpired',
paramName: 'maxAgeSeconds',
});
}
return false;
} else {
const timestamp = await this._timestampModel.getTimestamp(url);
const expireOlderThan = Date.now() - (this._maxAgeSeconds * 1000);
return (timestamp < expireOlderThan);
}
}
/**
* Removes the IndexedDB object store used to keep track of cache expiration
* metadata.
*/
async delete() {
// Make sure we don't attempt another rerun if we're called in the middle of
// a cache expiration.
this._rerunRequested = false;
await this._timestampModel.expireEntries(Infinity); // Expires all.
}
}
export {CacheExpiration};

View file

@ -0,0 +1,291 @@
/*
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 'workbox-core/_private/assert.js';
import {cacheNames} from 'workbox-core/_private/cacheNames.js';
import {dontWaitFor} from 'workbox-core/_private/dontWaitFor.js';
import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.js';
import {logger} from 'workbox-core/_private/logger.js';
import {registerQuotaErrorCallback} from 'workbox-core/registerQuotaErrorCallback.js';
import {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
import {WorkboxPlugin} from 'workbox-core/types.js';
import {CacheExpiration} from './CacheExpiration.js';
import './_version.js';
/**
* 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 `maxAgeSeconds`, 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 "Date" header, then
* a light weight expiration check is performed and the request will not be
* used immediately.
*
* When using `maxEntries`, the entry least-recently requested will be removed
* from the cache first.
*
* @memberof module:workbox-expiration
*/
class ExpirationPlugin implements WorkboxPlugin {
private readonly _config: object;
private readonly _maxAgeSeconds?: number;
private _cacheExpirations: Map<string, CacheExpiration>;
/**
* @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'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: {
maxEntries?: number;
maxAgeSeconds?: number;
purgeOnQuotaError?: boolean;
} = {}) {
if (process.env.NODE_ENV !== 'production') {
if (!(config.maxEntries || config.maxAgeSeconds)) {
throw new WorkboxError('max-entries-or-age-required', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
});
}
if (config.maxEntries) {
assert!.isType(config.maxEntries, 'number', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
paramName: 'config.maxEntries',
});
}
if (config.maxAgeSeconds) {
assert!.isType(config.maxAgeSeconds, 'number', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
paramName: 'config.maxAgeSeconds',
});
}
}
this._config = config;
this._maxAgeSeconds = config.maxAgeSeconds;
this._cacheExpirations = new Map();
if (config.purgeOnQuotaError) {
registerQuotaErrorCallback(() => this.deleteCacheAndMetadata());
}
}
/**
* A simple helper method to return a CacheExpiration instance for a given
* cache name.
*
* @param {string} cacheName
* @return {CacheExpiration}
*
* @private
*/
private _getCacheExpiration(cacheName: string): CacheExpiration {
if (cacheName === cacheNames.getRuntimeName()) {
throw new WorkboxError('expire-custom-caches-only');
}
let cacheExpiration = this._cacheExpirations.get(cacheName);
if (!cacheExpiration) {
cacheExpiration = new CacheExpiration(cacheName, this._config);
this._cacheExpirations.set(cacheName, cacheExpiration);
}
return cacheExpiration;
}
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` handlers when a `Response` is about to be returned
* from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
* the handler. It allows the `Response` to be inspected for freshness and
* prevents it from being used if the `Response`'s `Date` header value is
* older than the configured `maxAgeSeconds`.
*
* @param {Object} options
* @param {string} options.cacheName Name of the cache the response is in.
* @param {Response} options.cachedResponse The `Response` object that's been
* read from a cache and whose freshness should be checked.
* @return {Response} Either the `cachedResponse`, if it's
* fresh, or `null` if the `Response` is older than `maxAgeSeconds`.
*
* @private
*/
cachedResponseWillBeUsed: WorkboxPlugin['cachedResponseWillBeUsed'] = async ({
event,
request,
cacheName,
cachedResponse
}) => {
if (!cachedResponse) {
return null;
}
const isFresh = this._isResponseDateFresh(cachedResponse);
// Expire entries to ensure that even if the expiration date has
// expired, it'll only be used once.
const cacheExpiration = this._getCacheExpiration(cacheName);
dontWaitFor(cacheExpiration.expireEntries());
// Update the metadata for the request URL to the current timestamp,
// but don't `await` it as we don't want to block the response.
const updateTimestampDone = cacheExpiration.updateTimestamp(request.url);
if (event) {
try {
event.waitUntil(updateTimestampDone);
} catch (error) {
if (process.env.NODE_ENV !== 'production') {
// The event may not be a fetch event; only log the URL if it is.
if ('request' in event) {
logger.warn(`Unable to ensure service worker stays alive when ` +
`updating cache entry for ` +
`'${getFriendlyURL((event as FetchEvent).request.url)}'.`);
}
}
}
}
return isFresh ? cachedResponse : null;
}
/**
* @param {Response} cachedResponse
* @return {boolean}
*
* @private
*/
private _isResponseDateFresh(cachedResponse: Response): boolean {
if (!this._maxAgeSeconds) {
// We aren't expiring by age, so return true, it's fresh
return true;
}
// Check if the 'date' header will suffice a quick expiration check.
// See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
// discussion.
const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
if (dateHeaderTimestamp === null) {
// Unable to parse date, so assume it'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 = Date.now();
return dateHeaderTimestamp >= now - (this._maxAgeSeconds * 1000);
}
/**
* This method will extract the data header and parse it into a useful
* value.
*
* @param {Response} cachedResponse
* @return {number|null}
*
* @private
*/
private _getDateHeaderTimestamp(cachedResponse: Response): number | null {
if (!cachedResponse.headers.has('date')) {
return null;
}
const dateHeader = cachedResponse.headers.get('date');
const parsedDate = new Date(dateHeader!);
const headerTime = parsedDate.getTime();
// If the Date header was invalid for some reason, parsedDate.getTime()
// will return NaN.
if (isNaN(headerTime)) {
return null;
}
return headerTime;
}
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` 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
*/
cacheDidUpdate: WorkboxPlugin['cacheDidUpdate'] = async ({
cacheName,
request
}) => {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(cacheName, 'string', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'cacheDidUpdate',
paramName: 'cacheName',
});
assert!.isInstance(request, Request, {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'cacheDidUpdate',
paramName: 'request',
});
}
const cacheExpiration = 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
* `caches.delete()` directly, since this will ensure that the IndexedDB
* metadata is also cleanly removed and open IndexedDB instances are deleted.
*
* Note that if you're *not* using cache expiration for a given cache, calling
* `caches.delete()` and passing in the cache'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 `Promise.all()` to
// reduce the chance of inconsistency if a promise rejects.
for (const [cacheName, cacheExpiration] of this._cacheExpirations) {
await self.caches.delete(cacheName);
await cacheExpiration.delete();
}
// Reset this._cacheExpirations to its initial state.
this._cacheExpirations = new Map();
}
}
export {ExpirationPlugin};

2
web/node_modules/workbox-expiration/src/_version.ts generated vendored Normal file
View file

@ -0,0 +1,2 @@
// @ts-ignore
try{self['workbox:expiration:5.1.4']&&_()}catch(e){}

21
web/node_modules/workbox-expiration/src/index.ts generated vendored Normal file
View file

@ -0,0 +1,21 @@
/*
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 {CacheExpiration} from './CacheExpiration.js';
import {ExpirationPlugin} from './ExpirationPlugin.js';
import './_version.js';
/**
* @module workbox-expiration
*/
export {
CacheExpiration,
ExpirationPlugin,
};

View file

@ -0,0 +1,199 @@
/*
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 {DBWrapper} from 'workbox-core/_private/DBWrapper.js';
import {deleteDatabase} from 'workbox-core/_private/deleteDatabase.js';
import '../_version.js';
const DB_NAME = 'workbox-expiration';
const OBJECT_STORE_NAME = 'cache-entries';
const normalizeURL = (unNormalizedUrl: string) => {
const url = new URL(unNormalizedUrl, location.href);
url.hash = '';
return url.href;
};
interface CacheTimestampsModelEntry {
id: string;
cacheName: string;
url: string;
timestamp: number;
}
/**
* Returns the timestamp model.
*
* @private
*/
class CacheTimestampsModel {
private readonly _cacheName: string;
private readonly _db: DBWrapper;
/**
*
* @param {string} cacheName
*
* @private
*/
constructor(cacheName: string) {
this._cacheName = cacheName;
this._db = new DBWrapper(DB_NAME, 1, {
onupgradeneeded: (event) => this._handleUpgrade(event),
});
}
/**
* Should perform an upgrade of indexedDB.
*
* @param {Event} event
*
* @private
*/
private _handleUpgrade(event: IDBVersionChangeEvent) {
const db = (event.target as IDBOpenDBRequest).result;
// TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we
// have to use the `id` keyPath here and create our own values (a
// concatenation of `url + cacheName`) instead of simply using
// `keyPath: ['url', 'cacheName']`, which is supported in other browsers.
const objStore = db.createObjectStore(OBJECT_STORE_NAME, {keyPath: 'id'});
// TODO(philipwalton): once we don't have to support EdgeHTML, we can
// create a single index with the keyPath `['cacheName', 'timestamp']`
// instead of doing both these indexes.
objStore.createIndex('cacheName', 'cacheName', {unique: false});
objStore.createIndex('timestamp', 'timestamp', {unique: false});
// Previous versions of `workbox-expiration` used `this._cacheName`
// as the IDBDatabase name.
deleteDatabase(this._cacheName);
}
/**
* @param {string} url
* @param {number} timestamp
*
* @private
*/
async setTimestamp(url: string, timestamp: number) {
url = normalizeURL(url);
const entry: CacheTimestampsModelEntry = {
url,
timestamp,
cacheName: this._cacheName,
// Creating an ID from the URL and cache name won't be necessary once
// Edge switches to Chromium and all browsers we support work with
// array keyPaths.
id: this._getId(url),
};
await this._db.put!(OBJECT_STORE_NAME, entry);
}
/**
* Returns the timestamp stored for a given URL.
*
* @param {string} url
* @return {number}
*
* @private
*/
async getTimestamp(url: string): Promise<number> {
const entry: CacheTimestampsModelEntry =
await this._db.get!(OBJECT_STORE_NAME, this._getId(url));
return entry.timestamp;
}
/**
* Iterates through all the entries in the object store (from newest to
* oldest) and removes entries once either `maxCount` is reached or the
* entry's timestamp is less than `minTimestamp`.
*
* @param {number} minTimestamp
* @param {number} maxCount
* @return {Array<string>}
*
* @private
*/
async expireEntries(minTimestamp: number, maxCount?: number): Promise<string[]> {
const entriesToDelete = await this._db.transaction(
OBJECT_STORE_NAME, 'readwrite', (txn, done) => {
const store = txn.objectStore(OBJECT_STORE_NAME);
const request = store.index('timestamp').openCursor(null, 'prev')
const entriesToDelete: string[] = [];
let entriesNotDeletedCount = 0;
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
const result = cursor.value;
// TODO(philipwalton): once we can use a multi-key index, we
// won't have to check `cacheName` here.
if (result.cacheName === this._cacheName) {
// Delete an entry if it's older than the max age or
// if we already have the max number allowed.
if ((minTimestamp && result.timestamp < minTimestamp) ||
(maxCount && entriesNotDeletedCount >= maxCount)) {
// TODO(philipwalton): we should be able to delete the
// entry right here, but doing so causes an iteration
// bug in Safari stable (fixed in TP). Instead we can
// store the keys of the entries to delete, and then
// delete the separate transactions.
// https://github.com/GoogleChrome/workbox/issues/1978
// cursor.delete();
// We only need to return the URL, not the whole entry.
entriesToDelete.push(cursor.value);
} else {
entriesNotDeletedCount++;
}
}
cursor.continue();
} else {
done(entriesToDelete);
}
};
});
// TODO(philipwalton): once the Safari bug in the following issue is fixed,
// we should be able to remove this loop and do the entry deletion in the
// cursor loop above:
// https://github.com/GoogleChrome/workbox/issues/1978
const urlsDeleted: string[] = [];
for (const entry of entriesToDelete) {
await this._db.delete!(OBJECT_STORE_NAME, entry.id);
urlsDeleted.push(entry.url);
}
return urlsDeleted;
}
/**
* Takes a URL and returns an ID that will be unique in the object store.
*
* @param {string} url
* @return {string}
*
* @private
*/
private _getId(url: string): string {
// Creating an ID from the URL and cache name won't be necessary once
// Edge switches to Chromium and all browsers we support work with
// array keyPaths.
return this._cacheName + '|' + normalizeURL(url);
}
}
export {CacheTimestampsModel};

14
web/node_modules/workbox-expiration/tsconfig.json generated vendored Normal file
View file

@ -0,0 +1,14 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./",
"rootDir": "./src",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"include": [
"src/**/*.ts"
],
"references": [
{ "path": "../workbox-core/" }
]
}

2577
web/node_modules/workbox-expiration/tsconfig.tsbuildinfo generated vendored Normal file

File diff suppressed because it is too large Load diff