source: frontend/node_modules/workbox-strategies/src/StrategyHandler.ts

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