Strategy.ts 9.4 KB

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