NetworkFirst.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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 'workbox-core/_private/assert.js';
  8. import { logger } from 'workbox-core/_private/logger.js';
  9. import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
  10. import { cacheOkAndOpaquePlugin } from './plugins/cacheOkAndOpaquePlugin.js';
  11. import { Strategy } from './Strategy.js';
  12. import { messages } from './utils/messages.js';
  13. import './_version.js';
  14. /**
  15. * An implementation of a
  16. * [network first](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#network-first-falling-back-to-cache)
  17. * request strategy.
  18. *
  19. * By default, this strategy will cache responses with a 200 status code as
  20. * well as [opaque responses](https://developer.chrome.com/docs/workbox/caching-resources-during-runtime/#opaque-responses).
  21. * Opaque responses are are cross-origin requests where the response doesn't
  22. * support [CORS](https://enable-cors.org/).
  23. *
  24. * If the network request fails, and there is no cache match, this will throw
  25. * a `WorkboxError` exception.
  26. *
  27. * @extends workbox-strategies.Strategy
  28. * @memberof workbox-strategies
  29. */
  30. class NetworkFirst extends Strategy {
  31. /**
  32. * @param {Object} [options]
  33. * @param {string} [options.cacheName] Cache name to store and retrieve
  34. * requests. Defaults to cache names provided by
  35. * {@link workbox-core.cacheNames}.
  36. * @param {Array<Object>} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}
  37. * to use in conjunction with this caching strategy.
  38. * @param {Object} [options.fetchOptions] Values passed along to the
  39. * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters)
  40. * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796)
  41. * `fetch()` requests made by this strategy.
  42. * @param {Object} [options.matchOptions] [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions)
  43. * @param {number} [options.networkTimeoutSeconds] If set, any network requests
  44. * that fail to respond within the timeout will fallback to the cache.
  45. *
  46. * This option can be used to combat
  47. * "[lie-fi]{@link https://developers.google.com/web/fundamentals/performance/poor-connectivity/#lie-fi}"
  48. * scenarios.
  49. */
  50. constructor(options = {}) {
  51. super(options);
  52. // If this instance contains no plugins with a 'cacheWillUpdate' callback,
  53. // prepend the `cacheOkAndOpaquePlugin` plugin to the plugins list.
  54. if (!this.plugins.some((p) => 'cacheWillUpdate' in p)) {
  55. this.plugins.unshift(cacheOkAndOpaquePlugin);
  56. }
  57. this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
  58. if (process.env.NODE_ENV !== 'production') {
  59. if (this._networkTimeoutSeconds) {
  60. assert.isType(this._networkTimeoutSeconds, 'number', {
  61. moduleName: 'workbox-strategies',
  62. className: this.constructor.name,
  63. funcName: 'constructor',
  64. paramName: 'networkTimeoutSeconds',
  65. });
  66. }
  67. }
  68. }
  69. /**
  70. * @private
  71. * @param {Request|string} request A request to run this strategy for.
  72. * @param {workbox-strategies.StrategyHandler} handler The event that
  73. * triggered the request.
  74. * @return {Promise<Response>}
  75. */
  76. async _handle(request, handler) {
  77. const logs = [];
  78. if (process.env.NODE_ENV !== 'production') {
  79. assert.isInstance(request, Request, {
  80. moduleName: 'workbox-strategies',
  81. className: this.constructor.name,
  82. funcName: 'handle',
  83. paramName: 'makeRequest',
  84. });
  85. }
  86. const promises = [];
  87. let timeoutId;
  88. if (this._networkTimeoutSeconds) {
  89. const { id, promise } = this._getTimeoutPromise({ request, logs, handler });
  90. timeoutId = id;
  91. promises.push(promise);
  92. }
  93. const networkPromise = this._getNetworkPromise({
  94. timeoutId,
  95. request,
  96. logs,
  97. handler,
  98. });
  99. promises.push(networkPromise);
  100. const response = await handler.waitUntil((async () => {
  101. // Promise.race() will resolve as soon as the first promise resolves.
  102. return ((await handler.waitUntil(Promise.race(promises))) ||
  103. // If Promise.race() resolved with null, it might be due to a network
  104. // timeout + a cache miss. If that were to happen, we'd rather wait until
  105. // the networkPromise resolves instead of returning null.
  106. // Note that it's fine to await an already-resolved promise, so we don't
  107. // have to check to see if it's still "in flight".
  108. (await networkPromise));
  109. })());
  110. if (process.env.NODE_ENV !== 'production') {
  111. logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
  112. for (const log of logs) {
  113. logger.log(log);
  114. }
  115. messages.printFinalResponse(response);
  116. logger.groupEnd();
  117. }
  118. if (!response) {
  119. throw new WorkboxError('no-response', { url: request.url });
  120. }
  121. return response;
  122. }
  123. /**
  124. * @param {Object} options
  125. * @param {Request} options.request
  126. * @param {Array} options.logs A reference to the logs array
  127. * @param {Event} options.event
  128. * @return {Promise<Response>}
  129. *
  130. * @private
  131. */
  132. _getTimeoutPromise({ request, logs, handler, }) {
  133. let timeoutId;
  134. const timeoutPromise = new Promise((resolve) => {
  135. const onNetworkTimeout = async () => {
  136. if (process.env.NODE_ENV !== 'production') {
  137. logs.push(`Timing out the network response at ` +
  138. `${this._networkTimeoutSeconds} seconds.`);
  139. }
  140. resolve(await handler.cacheMatch(request));
  141. };
  142. timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000);
  143. });
  144. return {
  145. promise: timeoutPromise,
  146. id: timeoutId,
  147. };
  148. }
  149. /**
  150. * @param {Object} options
  151. * @param {number|undefined} options.timeoutId
  152. * @param {Request} options.request
  153. * @param {Array} options.logs A reference to the logs Array.
  154. * @param {Event} options.event
  155. * @return {Promise<Response>}
  156. *
  157. * @private
  158. */
  159. async _getNetworkPromise({ timeoutId, request, logs, handler, }) {
  160. let error;
  161. let response;
  162. try {
  163. response = await handler.fetchAndCachePut(request);
  164. }
  165. catch (fetchError) {
  166. if (fetchError instanceof Error) {
  167. error = fetchError;
  168. }
  169. }
  170. if (timeoutId) {
  171. clearTimeout(timeoutId);
  172. }
  173. if (process.env.NODE_ENV !== 'production') {
  174. if (response) {
  175. logs.push(`Got response from network.`);
  176. }
  177. else {
  178. logs.push(`Unable to get a response from the network. Will respond ` +
  179. `with a cached response.`);
  180. }
  181. }
  182. if (error || !response) {
  183. response = await handler.cacheMatch(request);
  184. if (process.env.NODE_ENV !== 'production') {
  185. if (response) {
  186. logs.push(`Found a cached response in the '${this.cacheName}'` + ` cache.`);
  187. }
  188. else {
  189. logs.push(`No response found in the '${this.cacheName}' cache.`);
  190. }
  191. }
  192. }
  193. return response;
  194. }
  195. }
  196. export { NetworkFirst };