Strategy.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /*
  2. Copyright 2020 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 { cacheNames } from 'workbox-core/_private/cacheNames.js';
  8. import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
  9. import { logger } from 'workbox-core/_private/logger.js';
  10. import { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';
  11. import { StrategyHandler } from './StrategyHandler.js';
  12. import './_version.js';
  13. /**
  14. * An abstract base class that all other strategy classes must extend from:
  15. *
  16. * @memberof workbox-strategies
  17. */
  18. class Strategy {
  19. /**
  20. * Creates a new instance of the strategy and sets all documented option
  21. * properties as public instance properties.
  22. *
  23. * Note: if a custom strategy class extends the base Strategy class and does
  24. * not need more than these properties, it does not need to define its own
  25. * constructor.
  26. *
  27. * @param {Object} [options]
  28. * @param {string} [options.cacheName] Cache name to store and retrieve
  29. * requests. Defaults to the cache names provided by
  30. * {@link workbox-core.cacheNames}.
  31. * @param {Array<Object>} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}
  32. * to use in conjunction with this caching strategy.
  33. * @param {Object} [options.fetchOptions] Values passed along to the
  34. * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters)
  35. * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796)
  36. * `fetch()` requests made by this strategy.
  37. * @param {Object} [options.matchOptions] The
  38. * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions}
  39. * for any `cache.match()` or `cache.put()` calls made by this strategy.
  40. */
  41. constructor(options = {}) {
  42. /**
  43. * Cache name to store and retrieve
  44. * requests. Defaults to the cache names provided by
  45. * {@link workbox-core.cacheNames}.
  46. *
  47. * @type {string}
  48. */
  49. this.cacheName = cacheNames.getRuntimeName(options.cacheName);
  50. /**
  51. * The list
  52. * [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}
  53. * used by this strategy.
  54. *
  55. * @type {Array<Object>}
  56. */
  57. this.plugins = options.plugins || [];
  58. /**
  59. * Values passed along to the
  60. * [`init`]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters}
  61. * of all fetch() requests made by this strategy.
  62. *
  63. * @type {Object}
  64. */
  65. this.fetchOptions = options.fetchOptions;
  66. /**
  67. * The
  68. * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions}
  69. * for any `cache.match()` or `cache.put()` calls made by this strategy.
  70. *
  71. * @type {Object}
  72. */
  73. this.matchOptions = options.matchOptions;
  74. }
  75. /**
  76. * Perform a request strategy and returns a `Promise` that will resolve with
  77. * a `Response`, invoking all relevant plugin callbacks.
  78. *
  79. * When a strategy instance is registered with a Workbox
  80. * {@link workbox-routing.Route}, this method is automatically
  81. * called when the route matches.
  82. *
  83. * Alternatively, this method can be used in a standalone `FetchEvent`
  84. * listener by passing it to `event.respondWith()`.
  85. *
  86. * @param {FetchEvent|Object} options A `FetchEvent` or an object with the
  87. * properties listed below.
  88. * @param {Request|string} options.request A request to run this strategy for.
  89. * @param {ExtendableEvent} options.event The event associated with the
  90. * request.
  91. * @param {URL} [options.url]
  92. * @param {*} [options.params]
  93. */
  94. handle(options) {
  95. const [responseDone] = this.handleAll(options);
  96. return responseDone;
  97. }
  98. /**
  99. * Similar to {@link workbox-strategies.Strategy~handle}, but
  100. * instead of just returning a `Promise` that resolves to a `Response` it
  101. * it will return an tuple of `[response, done]` promises, where the former
  102. * (`response`) is equivalent to what `handle()` returns, and the latter is a
  103. * Promise that will resolve once any promises that were added to
  104. * `event.waitUntil()` as part of performing the strategy have completed.
  105. *
  106. * You can await the `done` promise to ensure any extra work performed by
  107. * the strategy (usually caching responses) completes successfully.
  108. *
  109. * @param {FetchEvent|Object} options A `FetchEvent` or an object with the
  110. * properties listed below.
  111. * @param {Request|string} options.request A request to run this strategy for.
  112. * @param {ExtendableEvent} options.event The event associated with the
  113. * request.
  114. * @param {URL} [options.url]
  115. * @param {*} [options.params]
  116. * @return {Array<Promise>} A tuple of [response, done]
  117. * promises that can be used to determine when the response resolves as
  118. * well as when the handler has completed all its work.
  119. */
  120. handleAll(options) {
  121. // Allow for flexible options to be passed.
  122. if (options instanceof FetchEvent) {
  123. options = {
  124. event: options,
  125. request: options.request,
  126. };
  127. }
  128. const event = options.event;
  129. const request = typeof options.request === 'string'
  130. ? new Request(options.request)
  131. : options.request;
  132. const params = 'params' in options ? options.params : undefined;
  133. const handler = new StrategyHandler(this, { event, request, params });
  134. const responseDone = this._getResponse(handler, request, event);
  135. const handlerDone = this._awaitComplete(responseDone, handler, request, event);
  136. // Return an array of promises, suitable for use with Promise.all().
  137. return [responseDone, handlerDone];
  138. }
  139. async _getResponse(handler, request, event) {
  140. await handler.runCallbacks('handlerWillStart', { event, request });
  141. let response = undefined;
  142. try {
  143. response = await this._handle(request, handler);
  144. // The "official" Strategy subclasses all throw this error automatically,
  145. // but in case a third-party Strategy doesn't, ensure that we have a
  146. // consistent failure when there's no response or an error response.
  147. if (!response || response.type === 'error') {
  148. throw new WorkboxError('no-response', { url: request.url });
  149. }
  150. }
  151. catch (error) {
  152. if (error instanceof Error) {
  153. for (const callback of handler.iterateCallbacks('handlerDidError')) {
  154. response = await callback({ error, event, request });
  155. if (response) {
  156. break;
  157. }
  158. }
  159. }
  160. if (!response) {
  161. throw error;
  162. }
  163. else if (process.env.NODE_ENV !== 'production') {
  164. logger.log(`While responding to '${getFriendlyURL(request.url)}', ` +
  165. `an ${error instanceof Error ? error.toString() : ''} error occurred. Using a fallback response provided by ` +
  166. `a handlerDidError plugin.`);
  167. }
  168. }
  169. for (const callback of handler.iterateCallbacks('handlerWillRespond')) {
  170. response = await callback({ event, request, response });
  171. }
  172. return response;
  173. }
  174. async _awaitComplete(responseDone, handler, request, event) {
  175. let response;
  176. let error;
  177. try {
  178. response = await responseDone;
  179. }
  180. catch (error) {
  181. // Ignore errors, as response errors should be caught via the `response`
  182. // promise above. The `done` promise will only throw for errors in
  183. // promises passed to `handler.waitUntil()`.
  184. }
  185. try {
  186. await handler.runCallbacks('handlerDidRespond', {
  187. event,
  188. request,
  189. response,
  190. });
  191. await handler.doneWaiting();
  192. }
  193. catch (waitUntilError) {
  194. if (waitUntilError instanceof Error) {
  195. error = waitUntilError;
  196. }
  197. }
  198. await handler.runCallbacks('handlerDidComplete', {
  199. event,
  200. request,
  201. response,
  202. error: error,
  203. });
  204. handler.destroy();
  205. if (error) {
  206. throw error;
  207. }
  208. }
  209. }
  210. export { Strategy };
  211. /**
  212. * Classes extending the `Strategy` based class should implement this method,
  213. * and leverage the {@link workbox-strategies.StrategyHandler}
  214. * arg to perform all fetching and cache logic, which will ensure all relevant
  215. * cache, cache options, fetch options and plugins are used (per the current
  216. * strategy instance).
  217. *
  218. * @name _handle
  219. * @instance
  220. * @abstract
  221. * @function
  222. * @param {Request} request
  223. * @param {workbox-strategies.StrategyHandler} handler
  224. * @return {Promise<Response>}
  225. *
  226. * @memberof workbox-strategies.Strategy
  227. */