/*
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 {WorkboxError} from './WorkboxError.mjs';
import {assert} from './assert.mjs';
import {getFriendlyURL} from './getFriendlyURL.mjs';
import {logger} from './logger.mjs';
import {executeQuotaErrorCallbacks} from './executeQuotaErrorCallbacks.mjs';
import {pluginEvents} from '../models/pluginEvents.mjs';
import {pluginUtils} from '../utils/pluginUtils.mjs';
import '../_version.mjs';
/**
* Wrapper around cache.put().
*
* Will call `cacheDidUpdate` on plugins if the cache was updated, using
* `matchOptions` when determining what the old entry is.
*
* @param {Object} options
* @param {string} options.cacheName
* @param {Request} options.request
* @param {Response} options.response
* @param {Event} [options.event]
* @param {Array<Object>} [options.plugins=[]]
* @param {Object} [options.matchOptions]
*
* @private
* @memberof module:workbox-core
*/
const putWrapper = async ({
cacheName,
request,
response,
event,
plugins = [],
matchOptions,
} = {}) => {
if (process.env.NODE_ENV !== 'production') {
if (request.method && request.method !== 'GET') {
throw new WorkboxError('attempt-to-cache-non-get-request', {
url: getFriendlyURL(request.url),
method: request.method,
});
}
}
const effectiveRequest = await _getEffectiveRequest({
plugins, request, mode: 'write'});
if (!response) {
if (process.env.NODE_ENV !== 'production') {
logger.error(`Cannot cache non-existent response for ` +
`'${getFriendlyURL(effectiveRequest.url)}'.`);
}
throw new WorkboxError('cache-put-with-no-response', {
url: getFriendlyURL(effectiveRequest.url),
});
}
let responseToCache = await _isResponseSafeToCache({
event,
plugins,
response,
request: effectiveRequest,
});
if (!responseToCache) {
if (process.env.NODE_ENV !== 'production') {
logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' will ` +
`not be cached.`, responseToCache);
}
return;
}
const cache = await caches.open(cacheName);
const updatePlugins = pluginUtils.filter(
plugins, pluginEvents.CACHE_DID_UPDATE);
let oldResponse = updatePlugins.length > 0 ?
await matchWrapper({cacheName, matchOptions, request: effectiveRequest}) :
null;
if (process.env.NODE_ENV !== 'production') {
logger.debug(`Updating the '${cacheName}' cache with a new Response for ` +
`${getFriendlyURL(effectiveRequest.url)}.`);
}
try {
await cache.put(effectiveRequest, responseToCache);
} catch (error) {
// See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError
if (error.name === 'QuotaExceededError') {
await executeQuotaErrorCallbacks();
}
throw error;
}
for (let plugin of updatePlugins) {
await plugin[pluginEvents.CACHE_DID_UPDATE].call(plugin, {
cacheName,
event,
oldResponse,
newResponse: responseToCache,
request: effectiveRequest,
});
}
};
/**
* This is a wrapper around cache.match().
*
* @param {Object} options
* @param {string} options.cacheName Name of the cache to match against.
* @param {Request} options.request The Request that will be used to look up
* cache entries.
* @param {Event} [options.event] The event that propted the action.
* @param {Object} [options.matchOptions] Options passed to cache.match().
* @param {Array<Object>} [options.plugins=[]] Array of plugins.
* @return {Response} A cached response if available.
*
* @private
* @memberof module:workbox-core
*/
const matchWrapper = async ({
cacheName,
request,
event,
matchOptions,
plugins = [],
}) => {
const cache = await caches.open(cacheName);
const effectiveRequest = await _getEffectiveRequest({
plugins, request, mode: 'read'});
let cachedResponse = await cache.match(effectiveRequest, matchOptions);
if (process.env.NODE_ENV !== 'production') {
if (cachedResponse) {
logger.debug(`Found a cached response in '${cacheName}'.`);
} else {
logger.debug(`No cached response found in '${cacheName}'.`);
}
}
for (const plugin of plugins) {
if (pluginEvents.CACHED_RESPONSE_WILL_BE_USED in plugin) {
cachedResponse = await plugin[pluginEvents.CACHED_RESPONSE_WILL_BE_USED]
.call(plugin, {
cacheName,
event,
matchOptions,
cachedResponse,
request: effectiveRequest,
});
if (process.env.NODE_ENV !== 'production') {
if (cachedResponse) {
assert.isInstance(cachedResponse, Response, {
moduleName: 'Plugin',
funcName: pluginEvents.CACHED_RESPONSE_WILL_BE_USED,
isReturnValueProblem: true,
});
}
}
}
}
return cachedResponse;
};
/**
* This method will call cacheWillUpdate on the available plugins (or use
* status === 200) to determine if the Response is safe and valid to cache.
*
* @param {Object} options
* @param {Request} options.request
* @param {Response} options.response
* @param {Event} [options.event]
* @param {Array<Object>} [options.plugins=[]]
* @return {Promise<Response>}
*
* @private
* @memberof module:workbox-core
*/
const _isResponseSafeToCache = async ({request, response, event, plugins}) => {
let responseToCache = response;
let pluginsUsed = false;
for (let plugin of plugins) {
if (pluginEvents.CACHE_WILL_UPDATE in plugin) {
pluginsUsed = true;
responseToCache = await plugin[pluginEvents.CACHE_WILL_UPDATE]
.call(plugin, {
request,
response: responseToCache,
event,
});
if (process.env.NODE_ENV !== 'production') {
if (responseToCache) {
assert.isInstance(responseToCache, Response, {
moduleName: 'Plugin',
funcName: pluginEvents.CACHE_WILL_UPDATE,
isReturnValueProblem: true,
});
}
}
if (!responseToCache) {
break;
}
}
}
if (!pluginsUsed) {
if (process.env.NODE_ENV !== 'production') {
if (!responseToCache.status === 200) {
if (responseToCache.status === 0) {
logger.warn(`The response for '${request.url}' is an opaque ` +
`response. The caching strategy that you're using will not ` +
`cache opaque responses by default.`);
} else {
logger.debug(`The response for '${request.url}' returned ` +
`a status code of '${response.status}' and won't be cached as a ` +
`result.`);
}
}
}
responseToCache = responseToCache.status === 200 ? responseToCache : null;
}
return responseToCache ? responseToCache : null;
};
/**
* Checks the list of plugins for the cacheKeyWillBeUsed callback, and
* executes any of those callbacks found in sequence. The final `Request` object
* returned by the last plugin is treated as the cache key for cache reads
* and/or writes.
*
* @param {Object} options
* @param {Request} options.request
* @param {string} options.mode
* @param {Array<Object>} [options.plugins=[]]
* @return {Promise<Request>}
*
* @private
* @memberof module:workbox-core
*/
const _getEffectiveRequest = async ({request, mode, plugins}) => {
const cacheKeyWillBeUsedPlugins = pluginUtils.filter(
plugins, pluginEvents.CACHE_KEY_WILL_BE_USED);
let effectiveRequest = request;
for (const plugin of cacheKeyWillBeUsedPlugins) {
effectiveRequest = await plugin[pluginEvents.CACHE_KEY_WILL_BE_USED].call(
plugin, {mode, request: effectiveRequest});
if (typeof effectiveRequest === 'string') {
effectiveRequest = new Request(effectiveRequest);
}
if (process.env.NODE_ENV !== 'production') {
assert.isInstance(effectiveRequest, Request, {
moduleName: 'Plugin',
funcName: pluginEvents.CACHE_KEY_WILL_BE_USED,
isReturnValueProblem: true,
});
}
}
return effectiveRequest;
};
export const cacheWrapper = {
put: putWrapper,
match: matchWrapper,
};