BroadcastCacheUpdate.mjs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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.mjs';
  8. import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.mjs';
  9. import {logger} from 'workbox-core/_private/logger.mjs';
  10. import {Deferred} from 'workbox-core/_private/Deferred.mjs';
  11. import {responsesAreSame} from './responsesAreSame.mjs';
  12. import {broadcastUpdate} from './broadcastUpdate.mjs';
  13. import {DEFAULT_HEADERS_TO_CHECK, DEFAULT_BROADCAST_CHANNEL_NAME,
  14. DEFAULT_DEFER_NOTIFICATION_TIMEOUT} from './utils/constants.mjs';
  15. import './_version.mjs';
  16. /**
  17. * Uses the [Broadcast Channel API]{@link https://developers.google.com/web/updates/2016/09/broadcastchannel}
  18. * to notify interested parties when a cached response has been updated.
  19. * In browsers that do not support the Broadcast Channel API, the instance
  20. * falls back to sending the update via `postMessage()` to all window clients.
  21. *
  22. * For efficiency's sake, the underlying response bodies are not compared;
  23. * only specific response headers are checked.
  24. *
  25. * @memberof workbox.broadcastUpdate
  26. */
  27. class BroadcastCacheUpdate {
  28. /**
  29. * Construct a BroadcastCacheUpdate instance with a specific `channelName` to
  30. * broadcast messages on
  31. *
  32. * @param {Object} options
  33. * @param {Array<string>}
  34. * [options.headersToCheck=['content-length', 'etag', 'last-modified']]
  35. * A list of headers that will be used to determine whether the responses
  36. * differ.
  37. * @param {string} [options.channelName='workbox'] The name that will be used
  38. *. when creating the `BroadcastChannel`, which defaults to 'workbox' (the
  39. * channel name used by the `workbox-window` package).
  40. * @param {string} [options.deferNoticationTimeout=10000] The amount of time
  41. * to wait for a ready message from the window on navigation requests
  42. * before sending the update.
  43. */
  44. constructor({headersToCheck, channelName, deferNoticationTimeout} = {}) {
  45. this._headersToCheck = headersToCheck || DEFAULT_HEADERS_TO_CHECK;
  46. this._channelName = channelName || DEFAULT_BROADCAST_CHANNEL_NAME;
  47. this._deferNoticationTimeout =
  48. deferNoticationTimeout || DEFAULT_DEFER_NOTIFICATION_TIMEOUT;
  49. if (process.env.NODE_ENV !== 'production') {
  50. assert.isType(this._channelName, 'string', {
  51. moduleName: 'workbox-broadcast-update',
  52. className: 'BroadcastCacheUpdate',
  53. funcName: 'constructor',
  54. paramName: 'channelName',
  55. });
  56. assert.isArray(this._headersToCheck, {
  57. moduleName: 'workbox-broadcast-update',
  58. className: 'BroadcastCacheUpdate',
  59. funcName: 'constructor',
  60. paramName: 'headersToCheck',
  61. });
  62. }
  63. this._initWindowReadyDeferreds();
  64. }
  65. /**
  66. * Compare two [Responses](https://developer.mozilla.org/en-US/docs/Web/API/Response)
  67. * and send a message via the
  68. * {@link https://developers.google.com/web/updates/2016/09/broadcastchannel|Broadcast Channel API}
  69. * if they differ.
  70. *
  71. * Neither of the Responses can be {@link http://stackoverflow.com/questions/39109789|opaque}.
  72. *
  73. * @param {Object} options
  74. * @param {Response} options.oldResponse Cached response to compare.
  75. * @param {Response} options.newResponse Possibly updated response to compare.
  76. * @param {string} options.url The URL of the request.
  77. * @param {string} options.cacheName Name of the cache the responses belong
  78. * to. This is included in the broadcast message.
  79. * @param {Event} [options.event] event An optional event that triggered
  80. * this possible cache update.
  81. * @return {Promise} Resolves once the update is sent.
  82. */
  83. notifyIfUpdated({oldResponse, newResponse, url, cacheName, event}) {
  84. if (!responsesAreSame(oldResponse, newResponse, this._headersToCheck)) {
  85. if (process.env.NODE_ENV !== 'production') {
  86. logger.log(`Newer response found (and cached) for:`, url);
  87. }
  88. const sendUpdate = async () => {
  89. // In the case of a navigation request, the requesting page will likely
  90. // not have loaded its JavaScript in time to recevied the update
  91. // notification, so we defer it until ready (or we timeout waiting).
  92. if (event && event.request && event.request.mode === 'navigate') {
  93. if (process.env.NODE_ENV !== 'production') {
  94. logger.debug(`Original request was a navigation request, ` +
  95. `waiting for a ready message from the window`, event.request);
  96. }
  97. await this._windowReadyOrTimeout(event);
  98. }
  99. await this._broadcastUpdate({
  100. channel: this._getChannel(),
  101. cacheName,
  102. url,
  103. });
  104. };
  105. // Send the update and ensure the SW stays alive until it's sent.
  106. const done = sendUpdate();
  107. if (event) {
  108. try {
  109. event.waitUntil(done);
  110. } catch (error) {
  111. if (process.env.NODE_ENV !== 'production') {
  112. logger.warn(`Unable to ensure service worker stays alive ` +
  113. `when broadcasting cache update for ` +
  114. `${getFriendlyURL(event.request.url)}'.`);
  115. }
  116. }
  117. }
  118. return done;
  119. }
  120. }
  121. /**
  122. * NOTE: this is exposed on the instance primarily so it can be spied on
  123. * in tests.
  124. *
  125. * @param {Object} opts
  126. * @private
  127. */
  128. async _broadcastUpdate(opts) {
  129. await broadcastUpdate(opts);
  130. }
  131. /**
  132. * @return {BroadcastChannel|undefined} The BroadcastChannel instance used for
  133. * broadcasting updates, or undefined if the browser doesn't support the
  134. * Broadcast Channel API.
  135. *
  136. * @private
  137. */
  138. _getChannel() {
  139. if (('BroadcastChannel' in self) && !this._channel) {
  140. this._channel = new BroadcastChannel(this._channelName);
  141. }
  142. return this._channel;
  143. }
  144. /**
  145. * Waits for a message from the window indicating that it's capable of
  146. * receiving broadcasts. By default, this will only wait for the amount of
  147. * time specified via the `deferNoticationTimeout` option.
  148. *
  149. * @param {Event} event The navigation fetch event.
  150. * @return {Promise}
  151. * @private
  152. */
  153. _windowReadyOrTimeout(event) {
  154. if (!this._navigationEventsDeferreds.has(event)) {
  155. const deferred = new Deferred();
  156. // Set the deferred on the `_navigationEventsDeferreds` map so it will
  157. // be resolved when the next ready message event comes.
  158. this._navigationEventsDeferreds.set(event, deferred);
  159. // But don't wait too long for the message since it may never come.
  160. const timeout = setTimeout(() => {
  161. if (process.env.NODE_ENV !== 'production') {
  162. logger.debug(`Timed out after ${this._deferNoticationTimeout}` +
  163. `ms waiting for message from window`);
  164. }
  165. deferred.resolve();
  166. }, this._deferNoticationTimeout);
  167. // Ensure the timeout is cleared if the deferred promise is resolved.
  168. deferred.promise.then(() => clearTimeout(timeout));
  169. }
  170. return this._navigationEventsDeferreds.get(event).promise;
  171. }
  172. /**
  173. * Creates a mapping between navigation fetch events and deferreds, and adds
  174. * a listener for message events from the window. When message events arrive,
  175. * all deferreds in the mapping are resolved.
  176. *
  177. * Note: it would be easier if we could only resolve the deferred of
  178. * navigation fetch event whose client ID matched the source ID of the
  179. * message event, but currently client IDs are not exposed on navigation
  180. * fetch events: https://www.chromestatus.com/feature/4846038800138240
  181. *
  182. * @private
  183. */
  184. _initWindowReadyDeferreds() {
  185. // A mapping between navigation events and their deferreds.
  186. this._navigationEventsDeferreds = new Map();
  187. // The message listener needs to be added in the initial run of the
  188. // service worker, but since we don't actually need to be listening for
  189. // messages until the cache updates, we only invoke the callback if set.
  190. self.addEventListener('message', (event) => {
  191. if (event.data.type === 'WINDOW_READY' &&
  192. event.data.meta === 'workbox-window' &&
  193. this._navigationEventsDeferreds.size > 0) {
  194. if (process.env.NODE_ENV !== 'production') {
  195. logger.debug(`Received WINDOW_READY event: `, event);
  196. }
  197. // Resolve any pending deferreds.
  198. for (const deferred of this._navigationEventsDeferreds.values()) {
  199. deferred.resolve();
  200. }
  201. this._navigationEventsDeferreds.clear();
  202. }
  203. });
  204. }
  205. }
  206. export {BroadcastCacheUpdate};