cacheWrapper.mjs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. /*
  2. Copyright 2018 Google LLC
  3. Use of this source code is governed by an MIT-style
  4. license that can be found in the LICENSE file or at
  5. https://opensource.org/licenses/MIT.
  6. */
  7. import {WorkboxError} from './WorkboxError.mjs';
  8. import {assert} from './assert.mjs';
  9. import {getFriendlyURL} from './getFriendlyURL.mjs';
  10. import {logger} from './logger.mjs';
  11. import {executeQuotaErrorCallbacks} from './executeQuotaErrorCallbacks.mjs';
  12. import {pluginEvents} from '../models/pluginEvents.mjs';
  13. import {pluginUtils} from '../utils/pluginUtils.mjs';
  14. import '../_version.mjs';
  15. /**
  16. * Wrapper around cache.put().
  17. *
  18. * Will call `cacheDidUpdate` on plugins if the cache was updated, using
  19. * `matchOptions` when determining what the old entry is.
  20. *
  21. * @param {Object} options
  22. * @param {string} options.cacheName
  23. * @param {Request} options.request
  24. * @param {Response} options.response
  25. * @param {Event} [options.event]
  26. * @param {Array<Object>} [options.plugins=[]]
  27. * @param {Object} [options.matchOptions]
  28. *
  29. * @private
  30. * @memberof module:workbox-core
  31. */
  32. const putWrapper = async ({
  33. cacheName,
  34. request,
  35. response,
  36. event,
  37. plugins = [],
  38. matchOptions,
  39. } = {}) => {
  40. if (process.env.NODE_ENV !== 'production') {
  41. if (request.method && request.method !== 'GET') {
  42. throw new WorkboxError('attempt-to-cache-non-get-request', {
  43. url: getFriendlyURL(request.url),
  44. method: request.method,
  45. });
  46. }
  47. }
  48. const effectiveRequest = await _getEffectiveRequest({
  49. plugins, request, mode: 'write'});
  50. if (!response) {
  51. if (process.env.NODE_ENV !== 'production') {
  52. logger.error(`Cannot cache non-existent response for ` +
  53. `'${getFriendlyURL(effectiveRequest.url)}'.`);
  54. }
  55. throw new WorkboxError('cache-put-with-no-response', {
  56. url: getFriendlyURL(effectiveRequest.url),
  57. });
  58. }
  59. let responseToCache = await _isResponseSafeToCache({
  60. event,
  61. plugins,
  62. response,
  63. request: effectiveRequest,
  64. });
  65. if (!responseToCache) {
  66. if (process.env.NODE_ENV !== 'production') {
  67. logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' will ` +
  68. `not be cached.`, responseToCache);
  69. }
  70. return;
  71. }
  72. const cache = await caches.open(cacheName);
  73. const updatePlugins = pluginUtils.filter(
  74. plugins, pluginEvents.CACHE_DID_UPDATE);
  75. let oldResponse = updatePlugins.length > 0 ?
  76. await matchWrapper({cacheName, matchOptions, request: effectiveRequest}) :
  77. null;
  78. if (process.env.NODE_ENV !== 'production') {
  79. logger.debug(`Updating the '${cacheName}' cache with a new Response for ` +
  80. `${getFriendlyURL(effectiveRequest.url)}.`);
  81. }
  82. try {
  83. await cache.put(effectiveRequest, responseToCache);
  84. } catch (error) {
  85. // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError
  86. if (error.name === 'QuotaExceededError') {
  87. await executeQuotaErrorCallbacks();
  88. }
  89. throw error;
  90. }
  91. for (let plugin of updatePlugins) {
  92. await plugin[pluginEvents.CACHE_DID_UPDATE].call(plugin, {
  93. cacheName,
  94. event,
  95. oldResponse,
  96. newResponse: responseToCache,
  97. request: effectiveRequest,
  98. });
  99. }
  100. };
  101. /**
  102. * This is a wrapper around cache.match().
  103. *
  104. * @param {Object} options
  105. * @param {string} options.cacheName Name of the cache to match against.
  106. * @param {Request} options.request The Request that will be used to look up
  107. * cache entries.
  108. * @param {Event} [options.event] The event that propted the action.
  109. * @param {Object} [options.matchOptions] Options passed to cache.match().
  110. * @param {Array<Object>} [options.plugins=[]] Array of plugins.
  111. * @return {Response} A cached response if available.
  112. *
  113. * @private
  114. * @memberof module:workbox-core
  115. */
  116. const matchWrapper = async ({
  117. cacheName,
  118. request,
  119. event,
  120. matchOptions,
  121. plugins = [],
  122. }) => {
  123. const cache = await caches.open(cacheName);
  124. const effectiveRequest = await _getEffectiveRequest({
  125. plugins, request, mode: 'read'});
  126. let cachedResponse = await cache.match(effectiveRequest, matchOptions);
  127. if (process.env.NODE_ENV !== 'production') {
  128. if (cachedResponse) {
  129. logger.debug(`Found a cached response in '${cacheName}'.`);
  130. } else {
  131. logger.debug(`No cached response found in '${cacheName}'.`);
  132. }
  133. }
  134. for (const plugin of plugins) {
  135. if (pluginEvents.CACHED_RESPONSE_WILL_BE_USED in plugin) {
  136. cachedResponse = await plugin[pluginEvents.CACHED_RESPONSE_WILL_BE_USED]
  137. .call(plugin, {
  138. cacheName,
  139. event,
  140. matchOptions,
  141. cachedResponse,
  142. request: effectiveRequest,
  143. });
  144. if (process.env.NODE_ENV !== 'production') {
  145. if (cachedResponse) {
  146. assert.isInstance(cachedResponse, Response, {
  147. moduleName: 'Plugin',
  148. funcName: pluginEvents.CACHED_RESPONSE_WILL_BE_USED,
  149. isReturnValueProblem: true,
  150. });
  151. }
  152. }
  153. }
  154. }
  155. return cachedResponse;
  156. };
  157. /**
  158. * This method will call cacheWillUpdate on the available plugins (or use
  159. * status === 200) to determine if the Response is safe and valid to cache.
  160. *
  161. * @param {Object} options
  162. * @param {Request} options.request
  163. * @param {Response} options.response
  164. * @param {Event} [options.event]
  165. * @param {Array<Object>} [options.plugins=[]]
  166. * @return {Promise<Response>}
  167. *
  168. * @private
  169. * @memberof module:workbox-core
  170. */
  171. const _isResponseSafeToCache = async ({request, response, event, plugins}) => {
  172. let responseToCache = response;
  173. let pluginsUsed = false;
  174. for (let plugin of plugins) {
  175. if (pluginEvents.CACHE_WILL_UPDATE in plugin) {
  176. pluginsUsed = true;
  177. responseToCache = await plugin[pluginEvents.CACHE_WILL_UPDATE]
  178. .call(plugin, {
  179. request,
  180. response: responseToCache,
  181. event,
  182. });
  183. if (process.env.NODE_ENV !== 'production') {
  184. if (responseToCache) {
  185. assert.isInstance(responseToCache, Response, {
  186. moduleName: 'Plugin',
  187. funcName: pluginEvents.CACHE_WILL_UPDATE,
  188. isReturnValueProblem: true,
  189. });
  190. }
  191. }
  192. if (!responseToCache) {
  193. break;
  194. }
  195. }
  196. }
  197. if (!pluginsUsed) {
  198. if (process.env.NODE_ENV !== 'production') {
  199. if (!responseToCache.status === 200) {
  200. if (responseToCache.status === 0) {
  201. logger.warn(`The response for '${request.url}' is an opaque ` +
  202. `response. The caching strategy that you're using will not ` +
  203. `cache opaque responses by default.`);
  204. } else {
  205. logger.debug(`The response for '${request.url}' returned ` +
  206. `a status code of '${response.status}' and won't be cached as a ` +
  207. `result.`);
  208. }
  209. }
  210. }
  211. responseToCache = responseToCache.status === 200 ? responseToCache : null;
  212. }
  213. return responseToCache ? responseToCache : null;
  214. };
  215. /**
  216. * Checks the list of plugins for the cacheKeyWillBeUsed callback, and
  217. * executes any of those callbacks found in sequence. The final `Request` object
  218. * returned by the last plugin is treated as the cache key for cache reads
  219. * and/or writes.
  220. *
  221. * @param {Object} options
  222. * @param {Request} options.request
  223. * @param {string} options.mode
  224. * @param {Array<Object>} [options.plugins=[]]
  225. * @return {Promise<Request>}
  226. *
  227. * @private
  228. * @memberof module:workbox-core
  229. */
  230. const _getEffectiveRequest = async ({request, mode, plugins}) => {
  231. const cacheKeyWillBeUsedPlugins = pluginUtils.filter(
  232. plugins, pluginEvents.CACHE_KEY_WILL_BE_USED);
  233. let effectiveRequest = request;
  234. for (const plugin of cacheKeyWillBeUsedPlugins) {
  235. effectiveRequest = await plugin[pluginEvents.CACHE_KEY_WILL_BE_USED].call(
  236. plugin, {mode, request: effectiveRequest});
  237. if (typeof effectiveRequest === 'string') {
  238. effectiveRequest = new Request(effectiveRequest);
  239. }
  240. if (process.env.NODE_ENV !== 'production') {
  241. assert.isInstance(effectiveRequest, Request, {
  242. moduleName: 'Plugin',
  243. funcName: pluginEvents.CACHE_KEY_WILL_BE_USED,
  244. isReturnValueProblem: true,
  245. });
  246. }
  247. }
  248. return effectiveRequest;
  249. };
  250. export const cacheWrapper = {
  251. put: putWrapper,
  252. match: matchWrapper,
  253. };