NetworkFirst.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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 { cacheNames } from 'workbox-core/_private/cacheNames.js';
  9. import { cacheWrapper } from 'workbox-core/_private/cacheWrapper.js';
  10. import { fetchWrapper } from 'workbox-core/_private/fetchWrapper.js';
  11. import { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';
  12. import { logger } from 'workbox-core/_private/logger.js';
  13. import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
  14. import { messages } from './utils/messages.js';
  15. import { cacheOkAndOpaquePlugin } from './plugins/cacheOkAndOpaquePlugin.js';
  16. import './_version.js';
  17. /**
  18. * An implementation of a
  19. * [network first]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#network-falling-back-to-cache}
  20. * request strategy.
  21. *
  22. * By default, this strategy will cache responses with a 200 status code as
  23. * well as [opaque responses]{@link https://developers.google.com/web/tools/workbox/guides/handle-third-party-requests}.
  24. * Opaque responses are are cross-origin requests where the response doesn't
  25. * support [CORS]{@link https://enable-cors.org/}.
  26. *
  27. * If the network request fails, and there is no cache match, this will throw
  28. * a `WorkboxError` exception.
  29. *
  30. * @memberof module:workbox-strategies
  31. */
  32. class NetworkFirst {
  33. /**
  34. * @param {Object} options
  35. * @param {string} options.cacheName Cache name to store and retrieve
  36. * requests. Defaults to cache names provided by
  37. * [workbox-core]{@link module:workbox-core.cacheNames}.
  38. * @param {Array<Object>} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}
  39. * to use in conjunction with this caching strategy.
  40. * @param {Object} options.fetchOptions Values passed along to the
  41. * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters)
  42. * of all fetch() requests made by this strategy.
  43. * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions)
  44. * @param {number} options.networkTimeoutSeconds If set, any network requests
  45. * that fail to respond within the timeout will fallback to the cache.
  46. *
  47. * This option can be used to combat
  48. * "[lie-fi]{@link https://developers.google.com/web/fundamentals/performance/poor-connectivity/#lie-fi}"
  49. * scenarios.
  50. */
  51. constructor(options = {}) {
  52. this._cacheName = cacheNames.getRuntimeName(options.cacheName);
  53. if (options.plugins) {
  54. const isUsingCacheWillUpdate = options.plugins.some((plugin) => !!plugin.cacheWillUpdate);
  55. this._plugins = isUsingCacheWillUpdate ?
  56. options.plugins : [cacheOkAndOpaquePlugin, ...options.plugins];
  57. }
  58. else {
  59. // No plugins passed in, use the default plugin.
  60. this._plugins = [cacheOkAndOpaquePlugin];
  61. }
  62. this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
  63. if (process.env.NODE_ENV !== 'production') {
  64. if (this._networkTimeoutSeconds) {
  65. assert.isType(this._networkTimeoutSeconds, 'number', {
  66. moduleName: 'workbox-strategies',
  67. className: 'NetworkFirst',
  68. funcName: 'constructor',
  69. paramName: 'networkTimeoutSeconds',
  70. });
  71. }
  72. }
  73. this._fetchOptions = options.fetchOptions;
  74. this._matchOptions = options.matchOptions;
  75. }
  76. /**
  77. * This method will perform a request strategy and follows an API that
  78. * will work with the
  79. * [Workbox Router]{@link module:workbox-routing.Router}.
  80. *
  81. * @param {Object} options
  82. * @param {Request|string} options.request A request to run this strategy for.
  83. * @param {Event} [options.event] The event that triggered the request.
  84. * @return {Promise<Response>}
  85. */
  86. async handle({ event, request }) {
  87. const logs = [];
  88. if (typeof request === 'string') {
  89. request = new Request(request);
  90. }
  91. if (process.env.NODE_ENV !== 'production') {
  92. assert.isInstance(request, Request, {
  93. moduleName: 'workbox-strategies',
  94. className: 'NetworkFirst',
  95. funcName: 'handle',
  96. paramName: 'makeRequest',
  97. });
  98. }
  99. const promises = [];
  100. let timeoutId;
  101. if (this._networkTimeoutSeconds) {
  102. const { id, promise } = this._getTimeoutPromise({ request, event, logs });
  103. timeoutId = id;
  104. promises.push(promise);
  105. }
  106. const networkPromise = this._getNetworkPromise({ timeoutId, request, event, logs });
  107. promises.push(networkPromise);
  108. // Promise.race() will resolve as soon as the first promise resolves.
  109. let response = await Promise.race(promises);
  110. // If Promise.race() resolved with null, it might be due to a network
  111. // timeout + a cache miss. If that were to happen, we'd rather wait until
  112. // the networkPromise resolves instead of returning null.
  113. // Note that it's fine to await an already-resolved promise, so we don't
  114. // have to check to see if it's still "in flight".
  115. if (!response) {
  116. response = await networkPromise;
  117. }
  118. if (process.env.NODE_ENV !== 'production') {
  119. logger.groupCollapsed(messages.strategyStart('NetworkFirst', request));
  120. for (const log of logs) {
  121. logger.log(log);
  122. }
  123. messages.printFinalResponse(response);
  124. logger.groupEnd();
  125. }
  126. if (!response) {
  127. throw new WorkboxError('no-response', { url: request.url });
  128. }
  129. return response;
  130. }
  131. /**
  132. * @param {Object} options
  133. * @param {Request} options.request
  134. * @param {Array} options.logs A reference to the logs array
  135. * @param {Event} [options.event]
  136. * @return {Promise<Response>}
  137. *
  138. * @private
  139. */
  140. _getTimeoutPromise({ request, logs, event }) {
  141. let timeoutId;
  142. const timeoutPromise = new Promise((resolve) => {
  143. const onNetworkTimeout = async () => {
  144. if (process.env.NODE_ENV !== 'production') {
  145. logs.push(`Timing out the network response at ` +
  146. `${this._networkTimeoutSeconds} seconds.`);
  147. }
  148. resolve(await this._respondFromCache({ request, event }));
  149. };
  150. timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000);
  151. });
  152. return {
  153. promise: timeoutPromise,
  154. id: timeoutId,
  155. };
  156. }
  157. /**
  158. * @param {Object} options
  159. * @param {number|undefined} options.timeoutId
  160. * @param {Request} options.request
  161. * @param {Array} options.logs A reference to the logs Array.
  162. * @param {Event} [options.event]
  163. * @return {Promise<Response>}
  164. *
  165. * @private
  166. */
  167. async _getNetworkPromise({ timeoutId, request, logs, event }) {
  168. let error;
  169. let response;
  170. try {
  171. response = await fetchWrapper.fetch({
  172. request,
  173. event,
  174. fetchOptions: this._fetchOptions,
  175. plugins: this._plugins,
  176. });
  177. }
  178. catch (err) {
  179. error = err;
  180. }
  181. if (timeoutId) {
  182. clearTimeout(timeoutId);
  183. }
  184. if (process.env.NODE_ENV !== 'production') {
  185. if (response) {
  186. logs.push(`Got response from network.`);
  187. }
  188. else {
  189. logs.push(`Unable to get a response from the network. Will respond ` +
  190. `with a cached response.`);
  191. }
  192. }
  193. if (error || !response) {
  194. response = await this._respondFromCache({ request, event });
  195. if (process.env.NODE_ENV !== 'production') {
  196. if (response) {
  197. logs.push(`Found a cached response in the '${this._cacheName}'` +
  198. ` cache.`);
  199. }
  200. else {
  201. logs.push(`No response found in the '${this._cacheName}' cache.`);
  202. }
  203. }
  204. }
  205. else {
  206. // Keep the service worker alive while we put the request in the cache
  207. const responseClone = response.clone();
  208. const cachePut = cacheWrapper.put({
  209. cacheName: this._cacheName,
  210. request,
  211. response: responseClone,
  212. event,
  213. plugins: this._plugins,
  214. });
  215. if (event) {
  216. try {
  217. // The event has been responded to so we can keep the SW alive to
  218. // respond to the request
  219. event.waitUntil(cachePut);
  220. }
  221. catch (err) {
  222. if (process.env.NODE_ENV !== 'production') {
  223. logger.warn(`Unable to ensure service worker stays alive when ` +
  224. `updating cache for '${getFriendlyURL(request.url)}'.`);
  225. }
  226. }
  227. }
  228. }
  229. return response;
  230. }
  231. /**
  232. * Used if the network timeouts or fails to make the request.
  233. *
  234. * @param {Object} options
  235. * @param {Request} request The request to match in the cache
  236. * @param {Event} [options.event]
  237. * @return {Promise<Object>}
  238. *
  239. * @private
  240. */
  241. _respondFromCache({ event, request }) {
  242. return cacheWrapper.match({
  243. cacheName: this._cacheName,
  244. request,
  245. event,
  246. matchOptions: this._matchOptions,
  247. plugins: this._plugins,
  248. });
  249. }
  250. }
  251. export { NetworkFirst };