1 | this.workbox = this.workbox || {};
|
2 | this.workbox.broadcastUpdate = (function (exports, assert_mjs, getFriendlyURL_mjs, logger_mjs, Deferred_mjs, WorkboxError_mjs) {
|
3 | ;
|
4 |
|
5 | try {
|
6 | self['workbox:broadcast-update:4.1.1'] && _();
|
7 | } catch (e) {} // eslint-disable-line
|
8 |
|
9 | /*
|
10 | Copyright 2018 Google LLC
|
11 |
|
12 | Use of this source code is governed by an MIT-style
|
13 | license that can be found in the LICENSE file or at
|
14 | https://opensource.org/licenses/MIT.
|
15 | */
|
16 | /**
|
17 | * Given two `Response's`, compares several header values to see if they are
|
18 | * the same or not.
|
19 | *
|
20 | * @param {Response} firstResponse
|
21 | * @param {Response} secondResponse
|
22 | * @param {Array<string>} headersToCheck
|
23 | * @return {boolean}
|
24 | *
|
25 | * @memberof workbox.broadcastUpdate
|
26 | * @private
|
27 | */
|
28 |
|
29 | const responsesAreSame = (firstResponse, secondResponse, headersToCheck) => {
|
30 | {
|
31 | if (!(firstResponse instanceof Response && secondResponse instanceof Response)) {
|
32 | throw new WorkboxError_mjs.WorkboxError('invalid-responses-are-same-args');
|
33 | }
|
34 | }
|
35 |
|
36 | const atLeastOneHeaderAvailable = headersToCheck.some(header => {
|
37 | return firstResponse.headers.has(header) && secondResponse.headers.has(header);
|
38 | });
|
39 |
|
40 | if (!atLeastOneHeaderAvailable) {
|
41 | {
|
42 | logger_mjs.logger.warn(`Unable to determine where the response has been updated ` + `because none of the headers that would be checked are present.`);
|
43 | logger_mjs.logger.debug(`Attempting to compare the following: `, firstResponse, secondResponse, headersToCheck);
|
44 | } // Just return true, indicating the that responses are the same, since we
|
45 | // can't determine otherwise.
|
46 |
|
47 |
|
48 | return true;
|
49 | }
|
50 |
|
51 | return headersToCheck.every(header => {
|
52 | const headerStateComparison = firstResponse.headers.has(header) === secondResponse.headers.has(header);
|
53 | const headerValueComparison = firstResponse.headers.get(header) === secondResponse.headers.get(header);
|
54 | return headerStateComparison && headerValueComparison;
|
55 | });
|
56 | };
|
57 |
|
58 | /*
|
59 | Copyright 2018 Google LLC
|
60 |
|
61 | Use of this source code is governed by an MIT-style
|
62 | license that can be found in the LICENSE file or at
|
63 | https://opensource.org/licenses/MIT.
|
64 | */
|
65 | const CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED';
|
66 | const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update';
|
67 | const DEFAULT_BROADCAST_CHANNEL_NAME = 'workbox';
|
68 | const DEFAULT_DEFER_NOTIFICATION_TIMEOUT = 10000;
|
69 | const DEFAULT_HEADERS_TO_CHECK = ['content-length', 'etag', 'last-modified'];
|
70 |
|
71 | /*
|
72 | Copyright 2018 Google LLC
|
73 |
|
74 | Use of this source code is governed by an MIT-style
|
75 | license that can be found in the LICENSE file or at
|
76 | https://opensource.org/licenses/MIT.
|
77 | */
|
78 | /**
|
79 | * You would not normally call this method directly; it's called automatically
|
80 | * by an instance of the {@link BroadcastCacheUpdate} class. It's exposed here
|
81 | * for the benefit of developers who would rather not use the full
|
82 | * `BroadcastCacheUpdate` implementation.
|
83 | *
|
84 | * Calling this will dispatch a message on the provided
|
85 | * {@link https://developers.google.com/web/updates/2016/09/broadcastchannel|Broadcast Channel}
|
86 | * to notify interested subscribers about a change to a cached resource.
|
87 | *
|
88 | * The message that's posted has a formation inspired by the
|
89 | * [Flux standard action](https://github.com/acdlite/flux-standard-action#introduction)
|
90 | * format like so:
|
91 | *
|
92 | * ```
|
93 | * {
|
94 | * type: 'CACHE_UPDATED',
|
95 | * meta: 'workbox-broadcast-update',
|
96 | * payload: {
|
97 | * cacheName: 'the-cache-name',
|
98 | * updatedURL: 'https://example.com/'
|
99 | * }
|
100 | * }
|
101 | * ```
|
102 | *
|
103 | * (Usage of [Flux](https://facebook.github.io/flux/) itself is not at
|
104 | * all required.)
|
105 | *
|
106 | * @param {Object} options
|
107 | * @param {string} options.cacheName The name of the cache in which the updated
|
108 | * `Response` was stored.
|
109 | * @param {string} options.url The URL associated with the updated `Response`.
|
110 | * @param {BroadcastChannel} [options.channel] The `BroadcastChannel` to use.
|
111 | * If no channel is set or the browser doesn't support the BroadcastChannel
|
112 | * api, then an attempt will be made to `postMessage` each window client.
|
113 | *
|
114 | * @memberof workbox.broadcastUpdate
|
115 | */
|
116 |
|
117 | const broadcastUpdate = async ({
|
118 | channel,
|
119 | cacheName,
|
120 | url
|
121 | }) => {
|
122 | {
|
123 | assert_mjs.assert.isType(cacheName, 'string', {
|
124 | moduleName: 'workbox-broadcast-update',
|
125 | className: '~',
|
126 | funcName: 'broadcastUpdate',
|
127 | paramName: 'cacheName'
|
128 | });
|
129 | assert_mjs.assert.isType(url, 'string', {
|
130 | moduleName: 'workbox-broadcast-update',
|
131 | className: '~',
|
132 | funcName: 'broadcastUpdate',
|
133 | paramName: 'url'
|
134 | });
|
135 | }
|
136 |
|
137 | const data = {
|
138 | type: CACHE_UPDATED_MESSAGE_TYPE,
|
139 | meta: CACHE_UPDATED_MESSAGE_META,
|
140 | payload: {
|
141 | cacheName: cacheName,
|
142 | updatedURL: url
|
143 | }
|
144 | };
|
145 |
|
146 | if (channel) {
|
147 | channel.postMessage(data);
|
148 | } else {
|
149 | const windows = await clients.matchAll({
|
150 | type: 'window'
|
151 | });
|
152 |
|
153 | for (const win of windows) {
|
154 | win.postMessage(data);
|
155 | }
|
156 | }
|
157 | };
|
158 |
|
159 | /*
|
160 | Copyright 2018 Google LLC
|
161 |
|
162 | Use of this source code is governed by an MIT-style
|
163 | license that can be found in the LICENSE file or at
|
164 | https://opensource.org/licenses/MIT.
|
165 | */
|
166 | /**
|
167 | * Uses the [Broadcast Channel API]{@link https://developers.google.com/web/updates/2016/09/broadcastchannel}
|
168 | * to notify interested parties when a cached response has been updated.
|
169 | * In browsers that do not support the Broadcast Channel API, the instance
|
170 | * falls back to sending the update via `postMessage()` to all window clients.
|
171 | *
|
172 | * For efficiency's sake, the underlying response bodies are not compared;
|
173 | * only specific response headers are checked.
|
174 | *
|
175 | * @memberof workbox.broadcastUpdate
|
176 | */
|
177 |
|
178 | class BroadcastCacheUpdate {
|
179 | /**
|
180 | * Construct a BroadcastCacheUpdate instance with a specific `channelName` to
|
181 | * broadcast messages on
|
182 | *
|
183 | * @param {Object} options
|
184 | * @param {Array<string>}
|
185 | * [options.headersToCheck=['content-length', 'etag', 'last-modified']]
|
186 | * A list of headers that will be used to determine whether the responses
|
187 | * differ.
|
188 | * @param {string} [options.channelName='workbox'] The name that will be used
|
189 | *. when creating the `BroadcastChannel`, which defaults to 'workbox' (the
|
190 | * channel name used by the `workbox-window` package).
|
191 | * @param {string} [options.deferNoticationTimeout=10000] The amount of time
|
192 | * to wait for a ready message from the window on navigation requests
|
193 | * before sending the update.
|
194 | */
|
195 | constructor({
|
196 | headersToCheck,
|
197 | channelName,
|
198 | deferNoticationTimeout
|
199 | } = {}) {
|
200 | this._headersToCheck = headersToCheck || DEFAULT_HEADERS_TO_CHECK;
|
201 | this._channelName = channelName || DEFAULT_BROADCAST_CHANNEL_NAME;
|
202 | this._deferNoticationTimeout = deferNoticationTimeout || DEFAULT_DEFER_NOTIFICATION_TIMEOUT;
|
203 |
|
204 | {
|
205 | assert_mjs.assert.isType(this._channelName, 'string', {
|
206 | moduleName: 'workbox-broadcast-update',
|
207 | className: 'BroadcastCacheUpdate',
|
208 | funcName: 'constructor',
|
209 | paramName: 'channelName'
|
210 | });
|
211 | assert_mjs.assert.isArray(this._headersToCheck, {
|
212 | moduleName: 'workbox-broadcast-update',
|
213 | className: 'BroadcastCacheUpdate',
|
214 | funcName: 'constructor',
|
215 | paramName: 'headersToCheck'
|
216 | });
|
217 | }
|
218 |
|
219 | this._initWindowReadyDeferreds();
|
220 | }
|
221 | /**
|
222 | * Compare two [Responses](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
223 | * and send a message via the
|
224 | * {@link https://developers.google.com/web/updates/2016/09/broadcastchannel|Broadcast Channel API}
|
225 | * if they differ.
|
226 | *
|
227 | * Neither of the Responses can be {@link http://stackoverflow.com/questions/39109789|opaque}.
|
228 | *
|
229 | * @param {Object} options
|
230 | * @param {Response} options.oldResponse Cached response to compare.
|
231 | * @param {Response} options.newResponse Possibly updated response to compare.
|
232 | * @param {string} options.url The URL of the request.
|
233 | * @param {string} options.cacheName Name of the cache the responses belong
|
234 | * to. This is included in the broadcast message.
|
235 | * @param {Event} [options.event] event An optional event that triggered
|
236 | * this possible cache update.
|
237 | * @return {Promise} Resolves once the update is sent.
|
238 | */
|
239 |
|
240 |
|
241 | notifyIfUpdated({
|
242 | oldResponse,
|
243 | newResponse,
|
244 | url,
|
245 | cacheName,
|
246 | event
|
247 | }) {
|
248 | if (!responsesAreSame(oldResponse, newResponse, this._headersToCheck)) {
|
249 | {
|
250 | logger_mjs.logger.log(`Newer response found (and cached) for:`, url);
|
251 | }
|
252 |
|
253 | const sendUpdate = async () => {
|
254 | // In the case of a navigation request, the requesting page will likely
|
255 | // not have loaded its JavaScript in time to recevied the update
|
256 | // notification, so we defer it until ready (or we timeout waiting).
|
257 | if (event && event.request && event.request.mode === 'navigate') {
|
258 | {
|
259 | logger_mjs.logger.debug(`Original request was a navigation request, ` + `waiting for a ready message from the window`, event.request);
|
260 | }
|
261 |
|
262 | await this._windowReadyOrTimeout(event);
|
263 | }
|
264 |
|
265 | await broadcastUpdate({
|
266 | channel: this._getChannel(),
|
267 | cacheName,
|
268 | url
|
269 | });
|
270 | }; // Send the update and ensure the SW stays alive until it's sent.
|
271 |
|
272 |
|
273 | const done = sendUpdate();
|
274 |
|
275 | if (event) {
|
276 | try {
|
277 | event.waitUntil(done);
|
278 | } catch (error) {
|
279 | {
|
280 | logger_mjs.logger.warn(`Unable to ensure service worker stays alive ` + `when broadcasting cache update for ` + `${getFriendlyURL_mjs.getFriendlyURL(event.request.url)}'.`);
|
281 | }
|
282 | }
|
283 | }
|
284 |
|
285 | return done;
|
286 | }
|
287 | }
|
288 | /**
|
289 | * @return {BroadcastChannel|undefined} The BroadcastChannel instance used for
|
290 | * broadcasting updates, or undefined if the browser doesn't support the
|
291 | * Broadcast Channel API.
|
292 | *
|
293 | * @private
|
294 | */
|
295 |
|
296 |
|
297 | _getChannel() {
|
298 | if ('BroadcastChannel' in self && !this._channel) {
|
299 | this._channel = new BroadcastChannel(this._channelName);
|
300 | }
|
301 |
|
302 | return this._channel;
|
303 | }
|
304 | /**
|
305 | * Waits for a message from the window indicating that it's capable of
|
306 | * receiving broadcasts. By default, this will only wait for the amount of
|
307 | * time specified via the `deferNoticationTimeout` option.
|
308 | *
|
309 | * @param {Event} event The navigation fetch event.
|
310 | * @return {Promise}
|
311 | * @private
|
312 | */
|
313 |
|
314 |
|
315 | _windowReadyOrTimeout(event) {
|
316 | if (!this._navigationEventsDeferreds.has(event)) {
|
317 | const deferred = new Deferred_mjs.Deferred(); // Set the deferred on the `_navigationEventsDeferreds` map so it will
|
318 | // be resolved when the next ready message event comes.
|
319 |
|
320 | this._navigationEventsDeferreds.set(event, deferred); // But don't wait too long for the message since it may never come.
|
321 |
|
322 |
|
323 | const timeout = setTimeout(() => {
|
324 | {
|
325 | logger_mjs.logger.debug(`Timed out after ${this._deferNoticationTimeout}` + `ms waiting for message from window`);
|
326 | }
|
327 |
|
328 | deferred.resolve();
|
329 | }, this._deferNoticationTimeout); // Ensure the timeout is cleared if the deferred promise is resolved.
|
330 |
|
331 | deferred.promise.then(() => clearTimeout(timeout));
|
332 | }
|
333 |
|
334 | return this._navigationEventsDeferreds.get(event).promise;
|
335 | }
|
336 | /**
|
337 | * Creates a mapping between navigation fetch events and deferreds, and adds
|
338 | * a listener for message events from the window. When message events arrive,
|
339 | * all deferreds in the mapping are resolved.
|
340 | *
|
341 | * Note: it would be easier if we could only resolve the deferred of
|
342 | * navigation fetch event whose client ID matched the source ID of the
|
343 | * message event, but currently client IDs are not exposed on navigation
|
344 | * fetch events: https://www.chromestatus.com/feature/4846038800138240
|
345 | */
|
346 |
|
347 |
|
348 | _initWindowReadyDeferreds() {
|
349 | // A mapping between navigation events and their deferreds.
|
350 | this._navigationEventsDeferreds = new Map(); // The message listener needs to be added in the initial run of the
|
351 | // service worker, but since we don't actually need to be listening for
|
352 | // messages until the cache updates, we only invoke the callback if set.
|
353 |
|
354 | self.addEventListener('message', event => {
|
355 | if (event.data.type === 'WINDOW_READY' && event.data.meta === 'workbox-window' && this._navigationEventsDeferreds.size > 0) {
|
356 | {
|
357 | logger_mjs.logger.debug(`Received WINDOW_READY event: `, event);
|
358 | } // Resolve any pending deferreds.
|
359 |
|
360 |
|
361 | for (const deferred of this._navigationEventsDeferreds.values()) {
|
362 | deferred.resolve();
|
363 | }
|
364 |
|
365 | this._navigationEventsDeferreds.clear();
|
366 | }
|
367 | });
|
368 | }
|
369 |
|
370 | }
|
371 |
|
372 | /*
|
373 | Copyright 2018 Google LLC
|
374 |
|
375 | Use of this source code is governed by an MIT-style
|
376 | license that can be found in the LICENSE file or at
|
377 | https://opensource.org/licenses/MIT.
|
378 | */
|
379 | /**
|
380 | * This plugin will automatically broadcast a message whenever a cached response
|
381 | * is updated.
|
382 | *
|
383 | * @memberof workbox.broadcastUpdate
|
384 | */
|
385 |
|
386 | class Plugin {
|
387 | /**
|
388 | * Construct a BroadcastCacheUpdate instance with the passed options and
|
389 | * calls its `notifyIfUpdated()` method whenever the plugin's
|
390 | * `cacheDidUpdate` callback is invoked.
|
391 | *
|
392 | * @param {Object} options
|
393 | * @param {Array<string>}
|
394 | * [options.headersToCheck=['content-length', 'etag', 'last-modified']]
|
395 | * A list of headers that will be used to determine whether the responses
|
396 | * differ.
|
397 | * @param {string} [options.channelName='workbox'] The name that will be used
|
398 | *. when creating the `BroadcastChannel`, which defaults to 'workbox' (the
|
399 | * channel name used by the `workbox-window` package).
|
400 | * @param {string} [options.deferNoticationTimeout=10000] The amount of time
|
401 | * to wait for a ready message from the window on navigation requests
|
402 | * before sending the update.
|
403 | */
|
404 | constructor(options) {
|
405 | this._broadcastUpdate = new BroadcastCacheUpdate(options);
|
406 | }
|
407 | /**
|
408 | * A "lifecycle" callback that will be triggered automatically by the
|
409 | * `workbox-sw` and `workbox-runtime-caching` handlers when an entry is
|
410 | * added to a cache.
|
411 | *
|
412 | * @private
|
413 | * @param {Object} options The input object to this function.
|
414 | * @param {string} options.cacheName Name of the cache being updated.
|
415 | * @param {Response} [options.oldResponse] The previous cached value, if any.
|
416 | * @param {Response} options.newResponse The new value in the cache.
|
417 | * @param {Request} options.request The request that triggered the udpate.
|
418 | * @param {Request} [options.event] The event that triggered the update.
|
419 | */
|
420 |
|
421 |
|
422 | cacheDidUpdate({
|
423 | cacheName,
|
424 | oldResponse,
|
425 | newResponse,
|
426 | request,
|
427 | event
|
428 | }) {
|
429 | {
|
430 | assert_mjs.assert.isType(cacheName, 'string', {
|
431 | moduleName: 'workbox-broadcast-update',
|
432 | className: 'Plugin',
|
433 | funcName: 'cacheDidUpdate',
|
434 | paramName: 'cacheName'
|
435 | });
|
436 | assert_mjs.assert.isInstance(newResponse, Response, {
|
437 | moduleName: 'workbox-broadcast-update',
|
438 | className: 'Plugin',
|
439 | funcName: 'cacheDidUpdate',
|
440 | paramName: 'newResponse'
|
441 | });
|
442 | assert_mjs.assert.isInstance(request, Request, {
|
443 | moduleName: 'workbox-broadcast-update',
|
444 | className: 'Plugin',
|
445 | funcName: 'cacheDidUpdate',
|
446 | paramName: 'request'
|
447 | });
|
448 | }
|
449 |
|
450 | if (!oldResponse) {
|
451 | // Without a two responses there is nothing to compare.
|
452 | return;
|
453 | }
|
454 |
|
455 | this._broadcastUpdate.notifyIfUpdated({
|
456 | cacheName,
|
457 | oldResponse,
|
458 | newResponse,
|
459 | event,
|
460 | url: request.url
|
461 | });
|
462 | }
|
463 |
|
464 | }
|
465 |
|
466 | /*
|
467 | Copyright 2018 Google LLC
|
468 |
|
469 | Use of this source code is governed by an MIT-style
|
470 | license that can be found in the LICENSE file or at
|
471 | https://opensource.org/licenses/MIT.
|
472 | */
|
473 |
|
474 | exports.BroadcastCacheUpdate = BroadcastCacheUpdate;
|
475 | exports.Plugin = Plugin;
|
476 | exports.broadcastUpdate = broadcastUpdate;
|
477 | exports.responsesAreSame = responsesAreSame;
|
478 |
|
479 | return exports;
|
480 |
|
481 | }({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private));
|
482 |
|