cacheWrapper.js 9.1 KB

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