source: frontend/node_modules/workbox-strategies/StrategyHandler.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 21.2 KB
RevLine 
[9af201e]1/*
2 Copyright 2020 Google LLC
3
4 Use of this source code is governed by an MIT-style
5 license that can be found in the LICENSE file or at
6 https://opensource.org/licenses/MIT.
7*/
8import { assert } from 'workbox-core/_private/assert.js';
9import { cacheMatchIgnoreParams } from 'workbox-core/_private/cacheMatchIgnoreParams.js';
10import { Deferred } from 'workbox-core/_private/Deferred.js';
11import { executeQuotaErrorCallbacks } from 'workbox-core/_private/executeQuotaErrorCallbacks.js';
12import { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';
13import { logger } from 'workbox-core/_private/logger.js';
14import { timeout } from 'workbox-core/_private/timeout.js';
15import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
16import './_version.js';
17function toRequest(input) {
18 return typeof input === 'string' ? new Request(input) : input;
19}
20/**
21 * A class created every time a Strategy instance instance calls
22 * {@link workbox-strategies.Strategy~handle} or
23 * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and
24 * cache actions around plugin callbacks and keeps track of when the strategy
25 * is "done" (i.e. all added `event.waitUntil()` promises have resolved).
26 *
27 * @memberof workbox-strategies
28 */
29class StrategyHandler {
30 /**
31 * Creates a new instance associated with the passed strategy and event
32 * that's handling the request.
33 *
34 * The constructor also initializes the state that will be passed to each of
35 * the plugins handling this request.
36 *
37 * @param {workbox-strategies.Strategy} strategy
38 * @param {Object} options
39 * @param {Request|string} options.request A request to run this strategy for.
40 * @param {ExtendableEvent} options.event The event associated with the
41 * request.
42 * @param {URL} [options.url]
43 * @param {*} [options.params] The return value from the
44 * {@link workbox-routing~matchCallback} (if applicable).
45 */
46 constructor(strategy, options) {
47 this._cacheKeys = {};
48 /**
49 * The request the strategy is performing (passed to the strategy's
50 * `handle()` or `handleAll()` method).
51 * @name request
52 * @instance
53 * @type {Request}
54 * @memberof workbox-strategies.StrategyHandler
55 */
56 /**
57 * The event associated with this request.
58 * @name event
59 * @instance
60 * @type {ExtendableEvent}
61 * @memberof workbox-strategies.StrategyHandler
62 */
63 /**
64 * A `URL` instance of `request.url` (if passed to the strategy's
65 * `handle()` or `handleAll()` method).
66 * Note: the `url` param will be present if the strategy was invoked
67 * from a workbox `Route` object.
68 * @name url
69 * @instance
70 * @type {URL|undefined}
71 * @memberof workbox-strategies.StrategyHandler
72 */
73 /**
74 * A `param` value (if passed to the strategy's
75 * `handle()` or `handleAll()` method).
76 * Note: the `param` param will be present if the strategy was invoked
77 * from a workbox `Route` object and the
78 * {@link workbox-routing~matchCallback} returned
79 * a truthy value (it will be that value).
80 * @name params
81 * @instance
82 * @type {*|undefined}
83 * @memberof workbox-strategies.StrategyHandler
84 */
85 if (process.env.NODE_ENV !== 'production') {
86 assert.isInstance(options.event, ExtendableEvent, {
87 moduleName: 'workbox-strategies',
88 className: 'StrategyHandler',
89 funcName: 'constructor',
90 paramName: 'options.event',
91 });
92 }
93 Object.assign(this, options);
94 this.event = options.event;
95 this._strategy = strategy;
96 this._handlerDeferred = new Deferred();
97 this._extendLifetimePromises = [];
98 // Copy the plugins list (since it's mutable on the strategy),
99 // so any mutations don't affect this handler instance.
100 this._plugins = [...strategy.plugins];
101 this._pluginStateMap = new Map();
102 for (const plugin of this._plugins) {
103 this._pluginStateMap.set(plugin, {});
104 }
105 this.event.waitUntil(this._handlerDeferred.promise);
106 }
107 /**
108 * Fetches a given request (and invokes any applicable plugin callback
109 * methods) using the `fetchOptions` (for non-navigation requests) and
110 * `plugins` defined on the `Strategy` object.
111 *
112 * The following plugin lifecycle methods are invoked when using this method:
113 * - `requestWillFetch()`
114 * - `fetchDidSucceed()`
115 * - `fetchDidFail()`
116 *
117 * @param {Request|string} input The URL or request to fetch.
118 * @return {Promise<Response>}
119 */
120 async fetch(input) {
121 const { event } = this;
122 let request = toRequest(input);
123 if (request.mode === 'navigate' &&
124 event instanceof FetchEvent &&
125 event.preloadResponse) {
126 const possiblePreloadResponse = (await event.preloadResponse);
127 if (possiblePreloadResponse) {
128 if (process.env.NODE_ENV !== 'production') {
129 logger.log(`Using a preloaded navigation response for ` +
130 `'${getFriendlyURL(request.url)}'`);
131 }
132 return possiblePreloadResponse;
133 }
134 }
135 // If there is a fetchDidFail plugin, we need to save a clone of the
136 // original request before it's either modified by a requestWillFetch
137 // plugin or before the original request's body is consumed via fetch().
138 const originalRequest = this.hasCallback('fetchDidFail')
139 ? request.clone()
140 : null;
141 try {
142 for (const cb of this.iterateCallbacks('requestWillFetch')) {
143 request = await cb({ request: request.clone(), event });
144 }
145 }
146 catch (err) {
147 if (err instanceof Error) {
148 throw new WorkboxError('plugin-error-request-will-fetch', {
149 thrownErrorMessage: err.message,
150 });
151 }
152 }
153 // The request can be altered by plugins with `requestWillFetch` making
154 // the original request (most likely from a `fetch` event) different
155 // from the Request we make. Pass both to `fetchDidFail` to aid debugging.
156 const pluginFilteredRequest = request.clone();
157 try {
158 let fetchResponse;
159 // See https://github.com/GoogleChrome/workbox/issues/1796
160 fetchResponse = await fetch(request, request.mode === 'navigate' ? undefined : this._strategy.fetchOptions);
161 if (process.env.NODE_ENV !== 'production') {
162 logger.debug(`Network request for ` +
163 `'${getFriendlyURL(request.url)}' returned a response with ` +
164 `status '${fetchResponse.status}'.`);
165 }
166 for (const callback of this.iterateCallbacks('fetchDidSucceed')) {
167 fetchResponse = await callback({
168 event,
169 request: pluginFilteredRequest,
170 response: fetchResponse,
171 });
172 }
173 return fetchResponse;
174 }
175 catch (error) {
176 if (process.env.NODE_ENV !== 'production') {
177 logger.log(`Network request for ` +
178 `'${getFriendlyURL(request.url)}' threw an error.`, error);
179 }
180 // `originalRequest` will only exist if a `fetchDidFail` callback
181 // is being used (see above).
182 if (originalRequest) {
183 await this.runCallbacks('fetchDidFail', {
184 error: error,
185 event,
186 originalRequest: originalRequest.clone(),
187 request: pluginFilteredRequest.clone(),
188 });
189 }
190 throw error;
191 }
192 }
193 /**
194 * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on
195 * the response generated by `this.fetch()`.
196 *
197 * The call to `this.cachePut()` automatically invokes `this.waitUntil()`,
198 * so you do not have to manually call `waitUntil()` on the event.
199 *
200 * @param {Request|string} input The request or URL to fetch and cache.
201 * @return {Promise<Response>}
202 */
203 async fetchAndCachePut(input) {
204 const response = await this.fetch(input);
205 const responseClone = response.clone();
206 void this.waitUntil(this.cachePut(input, responseClone));
207 return response;
208 }
209 /**
210 * Matches a request from the cache (and invokes any applicable plugin
211 * callback methods) using the `cacheName`, `matchOptions`, and `plugins`
212 * defined on the strategy object.
213 *
214 * The following plugin lifecycle methods are invoked when using this method:
215 * - cacheKeyWillByUsed()
216 * - cachedResponseWillByUsed()
217 *
218 * @param {Request|string} key The Request or URL to use as the cache key.
219 * @return {Promise<Response|undefined>} A matching response, if found.
220 */
221 async cacheMatch(key) {
222 const request = toRequest(key);
223 let cachedResponse;
224 const { cacheName, matchOptions } = this._strategy;
225 const effectiveRequest = await this.getCacheKey(request, 'read');
226 const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), { cacheName });
227 cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);
228 if (process.env.NODE_ENV !== 'production') {
229 if (cachedResponse) {
230 logger.debug(`Found a cached response in '${cacheName}'.`);
231 }
232 else {
233 logger.debug(`No cached response found in '${cacheName}'.`);
234 }
235 }
236 for (const callback of this.iterateCallbacks('cachedResponseWillBeUsed')) {
237 cachedResponse =
238 (await callback({
239 cacheName,
240 matchOptions,
241 cachedResponse,
242 request: effectiveRequest,
243 event: this.event,
244 })) || undefined;
245 }
246 return cachedResponse;
247 }
248 /**
249 * Puts a request/response pair in the cache (and invokes any applicable
250 * plugin callback methods) using the `cacheName` and `plugins` defined on
251 * the strategy object.
252 *
253 * The following plugin lifecycle methods are invoked when using this method:
254 * - cacheKeyWillByUsed()
255 * - cacheWillUpdate()
256 * - cacheDidUpdate()
257 *
258 * @param {Request|string} key The request or URL to use as the cache key.
259 * @param {Response} response The response to cache.
260 * @return {Promise<boolean>} `false` if a cacheWillUpdate caused the response
261 * not be cached, and `true` otherwise.
262 */
263 async cachePut(key, response) {
264 const request = toRequest(key);
265 // Run in the next task to avoid blocking other cache reads.
266 // https://github.com/w3c/ServiceWorker/issues/1397
267 await timeout(0);
268 const effectiveRequest = await this.getCacheKey(request, 'write');
269 if (process.env.NODE_ENV !== 'production') {
270 if (effectiveRequest.method && effectiveRequest.method !== 'GET') {
271 throw new WorkboxError('attempt-to-cache-non-get-request', {
272 url: getFriendlyURL(effectiveRequest.url),
273 method: effectiveRequest.method,
274 });
275 }
276 // See https://github.com/GoogleChrome/workbox/issues/2818
277 const vary = response.headers.get('Vary');
278 if (vary) {
279 logger.debug(`The response for ${getFriendlyURL(effectiveRequest.url)} ` +
280 `has a 'Vary: ${vary}' header. ` +
281 `Consider setting the {ignoreVary: true} option on your strategy ` +
282 `to ensure cache matching and deletion works as expected.`);
283 }
284 }
285 if (!response) {
286 if (process.env.NODE_ENV !== 'production') {
287 logger.error(`Cannot cache non-existent response for ` +
288 `'${getFriendlyURL(effectiveRequest.url)}'.`);
289 }
290 throw new WorkboxError('cache-put-with-no-response', {
291 url: getFriendlyURL(effectiveRequest.url),
292 });
293 }
294 const responseToCache = await this._ensureResponseSafeToCache(response);
295 if (!responseToCache) {
296 if (process.env.NODE_ENV !== 'production') {
297 logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` +
298 `will not be cached.`, responseToCache);
299 }
300 return false;
301 }
302 const { cacheName, matchOptions } = this._strategy;
303 const cache = await self.caches.open(cacheName);
304 const hasCacheUpdateCallback = this.hasCallback('cacheDidUpdate');
305 const oldResponse = hasCacheUpdateCallback
306 ? await cacheMatchIgnoreParams(
307 // TODO(philipwalton): the `__WB_REVISION__` param is a precaching
308 // feature. Consider into ways to only add this behavior if using
309 // precaching.
310 cache, effectiveRequest.clone(), ['__WB_REVISION__'], matchOptions)
311 : null;
312 if (process.env.NODE_ENV !== 'production') {
313 logger.debug(`Updating the '${cacheName}' cache with a new Response ` +
314 `for ${getFriendlyURL(effectiveRequest.url)}.`);
315 }
316 try {
317 await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);
318 }
319 catch (error) {
320 if (error instanceof Error) {
321 // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError
322 if (error.name === 'QuotaExceededError') {
323 await executeQuotaErrorCallbacks();
324 }
325 throw error;
326 }
327 }
328 for (const callback of this.iterateCallbacks('cacheDidUpdate')) {
329 await callback({
330 cacheName,
331 oldResponse,
332 newResponse: responseToCache.clone(),
333 request: effectiveRequest,
334 event: this.event,
335 });
336 }
337 return true;
338 }
339 /**
340 * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and
341 * executes any of those callbacks found in sequence. The final `Request`
342 * object returned by the last plugin is treated as the cache key for cache
343 * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have
344 * been registered, the passed request is returned unmodified
345 *
346 * @param {Request} request
347 * @param {string} mode
348 * @return {Promise<Request>}
349 */
350 async getCacheKey(request, mode) {
351 const key = `${request.url} | ${mode}`;
352 if (!this._cacheKeys[key]) {
353 let effectiveRequest = request;
354 for (const callback of this.iterateCallbacks('cacheKeyWillBeUsed')) {
355 effectiveRequest = toRequest(await callback({
356 mode,
357 request: effectiveRequest,
358 event: this.event,
359 // params has a type any can't change right now.
360 params: this.params, // eslint-disable-line
361 }));
362 }
363 this._cacheKeys[key] = effectiveRequest;
364 }
365 return this._cacheKeys[key];
366 }
367 /**
368 * Returns true if the strategy has at least one plugin with the given
369 * callback.
370 *
371 * @param {string} name The name of the callback to check for.
372 * @return {boolean}
373 */
374 hasCallback(name) {
375 for (const plugin of this._strategy.plugins) {
376 if (name in plugin) {
377 return true;
378 }
379 }
380 return false;
381 }
382 /**
383 * Runs all plugin callbacks matching the given name, in order, passing the
384 * given param object (merged ith the current plugin state) as the only
385 * argument.
386 *
387 * Note: since this method runs all plugins, it's not suitable for cases
388 * where the return value of a callback needs to be applied prior to calling
389 * the next callback. See
390 * {@link workbox-strategies.StrategyHandler#iterateCallbacks}
391 * below for how to handle that case.
392 *
393 * @param {string} name The name of the callback to run within each plugin.
394 * @param {Object} param The object to pass as the first (and only) param
395 * when executing each callback. This object will be merged with the
396 * current plugin state prior to callback execution.
397 */
398 async runCallbacks(name, param) {
399 for (const callback of this.iterateCallbacks(name)) {
400 // TODO(philipwalton): not sure why `any` is needed. It seems like
401 // this should work with `as WorkboxPluginCallbackParam[C]`.
402 await callback(param);
403 }
404 }
405 /**
406 * Accepts a callback and returns an iterable of matching plugin callbacks,
407 * where each callback is wrapped with the current handler state (i.e. when
408 * you call each callback, whatever object parameter you pass it will
409 * be merged with the plugin's current state).
410 *
411 * @param {string} name The name fo the callback to run
412 * @return {Array<Function>}
413 */
414 *iterateCallbacks(name) {
415 for (const plugin of this._strategy.plugins) {
416 if (typeof plugin[name] === 'function') {
417 const state = this._pluginStateMap.get(plugin);
418 const statefulCallback = (param) => {
419 const statefulParam = Object.assign(Object.assign({}, param), { state });
420 // TODO(philipwalton): not sure why `any` is needed. It seems like
421 // this should work with `as WorkboxPluginCallbackParam[C]`.
422 return plugin[name](statefulParam);
423 };
424 yield statefulCallback;
425 }
426 }
427 }
428 /**
429 * Adds a promise to the
430 * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises}
431 * of the event event associated with the request being handled (usually a
432 * `FetchEvent`).
433 *
434 * Note: you can await
435 * {@link workbox-strategies.StrategyHandler~doneWaiting}
436 * to know when all added promises have settled.
437 *
438 * @param {Promise} promise A promise to add to the extend lifetime promises
439 * of the event that triggered the request.
440 */
441 waitUntil(promise) {
442 this._extendLifetimePromises.push(promise);
443 return promise;
444 }
445 /**
446 * Returns a promise that resolves once all promises passed to
447 * {@link workbox-strategies.StrategyHandler~waitUntil}
448 * have settled.
449 *
450 * Note: any work done after `doneWaiting()` settles should be manually
451 * passed to an event's `waitUntil()` method (not this handler's
452 * `waitUntil()` method), otherwise the service worker thread my be killed
453 * prior to your work completing.
454 */
455 async doneWaiting() {
456 let promise;
457 while ((promise = this._extendLifetimePromises.shift())) {
458 await promise;
459 }
460 }
461 /**
462 * Stops running the strategy and immediately resolves any pending
463 * `waitUntil()` promises.
464 */
465 destroy() {
466 this._handlerDeferred.resolve(null);
467 }
468 /**
469 * This method will call cacheWillUpdate on the available plugins (or use
470 * status === 200) to determine if the Response is safe and valid to cache.
471 *
472 * @param {Request} options.request
473 * @param {Response} options.response
474 * @return {Promise<Response|undefined>}
475 *
476 * @private
477 */
478 async _ensureResponseSafeToCache(response) {
479 let responseToCache = response;
480 let pluginsUsed = false;
481 for (const callback of this.iterateCallbacks('cacheWillUpdate')) {
482 responseToCache =
483 (await callback({
484 request: this.request,
485 response: responseToCache,
486 event: this.event,
487 })) || undefined;
488 pluginsUsed = true;
489 if (!responseToCache) {
490 break;
491 }
492 }
493 if (!pluginsUsed) {
494 if (responseToCache && responseToCache.status !== 200) {
495 responseToCache = undefined;
496 }
497 if (process.env.NODE_ENV !== 'production') {
498 if (responseToCache) {
499 if (responseToCache.status !== 200) {
500 if (responseToCache.status === 0) {
501 logger.warn(`The response for '${this.request.url}' ` +
502 `is an opaque response. The caching strategy that you're ` +
503 `using will not cache opaque responses by default.`);
504 }
505 else {
506 logger.debug(`The response for '${this.request.url}' ` +
507 `returned a status code of '${response.status}' and won't ` +
508 `be cached as a result.`);
509 }
510 }
511 }
512 }
513 }
514 return responseToCache;
515 }
516}
517export { StrategyHandler };
Note: See TracBrowser for help on using the repository browser.