@serwist/strategies 8.4.4 → 9.0.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs DELETED
@@ -1,1075 +0,0 @@
1
- 'use strict';
2
-
3
- var internal = require('@serwist/core/internal');
4
-
5
- function toRequest(input) {
6
- return typeof input === "string" ? new Request(input) : input;
7
- }
8
- /**
9
- * A class created every time a Strategy instance instance calls `Strategy.handle` or
10
- * `Strategy.handleAll` that wraps all fetch and cache actions around plugin callbacks
11
- * and keeps track of when the strategy is "done" (i.e. all added `event.waitUntil()` promises
12
- * have resolved).
13
- */ class StrategyHandler {
14
- /**
15
- * The request the strategy is performing (passed to the strategy's
16
- * `handle()` or `handleAll()` method).
17
- */ request;
18
- /**
19
- * A `URL` instance of `request.url` (if passed to the strategy's
20
- * `handle()` or `handleAll()` method).
21
- * Note: the `url` param will be present if the strategy was invoked
22
- * from a `@serwist/routing.Route` object.
23
- */ url;
24
- /**
25
- * The event associated with this request.
26
- */ event;
27
- /**
28
- * A `param` value (if passed to the strategy's
29
- * `handle()` or `handleAll()` method).
30
- * Note: the `param` param will be present if the strategy was invoked
31
- * from a `@serwist/routing.Route` object and the `@serwist/strategies.matchCallback`
32
- * returned a truthy value (it will be that value).
33
- */ params;
34
- _cacheKeys = {};
35
- _strategy;
36
- _extendLifetimePromises;
37
- _handlerDeferred;
38
- _plugins;
39
- _pluginStateMap;
40
- /**
41
- * Creates a new instance associated with the passed strategy and event
42
- * that's handling the request.
43
- *
44
- * The constructor also initializes the state that will be passed to each of
45
- * the plugins handling this request.
46
- *
47
- * @param strategy
48
- * @param options
49
- */ constructor(strategy, options){
50
- if (process.env.NODE_ENV !== "production") {
51
- internal.assert.isInstance(options.event, ExtendableEvent, {
52
- moduleName: "@serwist/strategies",
53
- className: "StrategyHandler",
54
- funcName: "constructor",
55
- paramName: "options.event"
56
- });
57
- }
58
- Object.assign(this, options);
59
- this.event = options.event;
60
- this._strategy = strategy;
61
- this._handlerDeferred = new internal.Deferred();
62
- this._extendLifetimePromises = [];
63
- // Copy the plugins list (since it's mutable on the strategy),
64
- // so any mutations don't affect this handler instance.
65
- this._plugins = [
66
- ...strategy.plugins
67
- ];
68
- this._pluginStateMap = new Map();
69
- for (const plugin of this._plugins){
70
- this._pluginStateMap.set(plugin, {});
71
- }
72
- this.event.waitUntil(this._handlerDeferred.promise);
73
- }
74
- /**
75
- * Fetches a given request (and invokes any applicable plugin callback
76
- * methods) using the `fetchOptions` (for non-navigation requests) and
77
- * `plugins` defined on the `Strategy` object.
78
- *
79
- * The following plugin lifecycle methods are invoked when using this method:
80
- * - `requestWillFetch()`
81
- * - `fetchDidSucceed()`
82
- * - `fetchDidFail()`
83
- *
84
- * @param input The URL or request to fetch.
85
- * @returns
86
- */ async fetch(input) {
87
- const { event } = this;
88
- let request = toRequest(input);
89
- if (request.mode === "navigate" && event instanceof FetchEvent && event.preloadResponse) {
90
- const possiblePreloadResponse = await event.preloadResponse;
91
- if (possiblePreloadResponse) {
92
- if (process.env.NODE_ENV !== "production") {
93
- internal.logger.log(`Using a preloaded navigation response for '${internal.getFriendlyURL(request.url)}'`);
94
- }
95
- return possiblePreloadResponse;
96
- }
97
- }
98
- // If there is a fetchDidFail plugin, we need to save a clone of the
99
- // original request before it's either modified by a requestWillFetch
100
- // plugin or before the original request's body is consumed via fetch().
101
- const originalRequest = this.hasCallback("fetchDidFail") ? request.clone() : null;
102
- try {
103
- for (const cb of this.iterateCallbacks("requestWillFetch")){
104
- request = await cb({
105
- request: request.clone(),
106
- event
107
- });
108
- }
109
- } catch (err) {
110
- if (err instanceof Error) {
111
- throw new internal.SerwistError("plugin-error-request-will-fetch", {
112
- thrownErrorMessage: err.message
113
- });
114
- }
115
- }
116
- // The request can be altered by plugins with `requestWillFetch` making
117
- // the original request (most likely from a `fetch` event) different
118
- // from the Request we make. Pass both to `fetchDidFail` to aid debugging.
119
- const pluginFilteredRequest = request.clone();
120
- try {
121
- let fetchResponse;
122
- // See https://github.com/GoogleChrome/workbox/issues/1796
123
- fetchResponse = await fetch(request, request.mode === "navigate" ? undefined : this._strategy.fetchOptions);
124
- if (process.env.NODE_ENV !== "production") {
125
- internal.logger.debug(`Network request for '${internal.getFriendlyURL(request.url)}' returned a response with status '${fetchResponse.status}'.`);
126
- }
127
- for (const callback of this.iterateCallbacks("fetchDidSucceed")){
128
- fetchResponse = await callback({
129
- event,
130
- request: pluginFilteredRequest,
131
- response: fetchResponse
132
- });
133
- }
134
- return fetchResponse;
135
- } catch (error) {
136
- if (process.env.NODE_ENV !== "production") {
137
- internal.logger.log(`Network request for '${internal.getFriendlyURL(request.url)}' threw an error.`, error);
138
- }
139
- // `originalRequest` will only exist if a `fetchDidFail` callback
140
- // is being used (see above).
141
- if (originalRequest) {
142
- await this.runCallbacks("fetchDidFail", {
143
- error: error,
144
- event,
145
- originalRequest: originalRequest.clone(),
146
- request: pluginFilteredRequest.clone()
147
- });
148
- }
149
- throw error;
150
- }
151
- }
152
- /**
153
- * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on
154
- * the response generated by `this.fetch()`.
155
- *
156
- * The call to `this.cachePut()` automatically invokes `this.waitUntil()`,
157
- * so you do not have to manually call `waitUntil()` on the event.
158
- *
159
- * @param input The request or URL to fetch and cache.
160
- * @returns
161
- */ async fetchAndCachePut(input) {
162
- const response = await this.fetch(input);
163
- const responseClone = response.clone();
164
- void this.waitUntil(this.cachePut(input, responseClone));
165
- return response;
166
- }
167
- /**
168
- * Matches a request from the cache (and invokes any applicable plugin
169
- * callback methods) using the `cacheName`, `matchOptions`, and `plugins`
170
- * defined on the strategy object.
171
- *
172
- * The following plugin lifecycle methods are invoked when using this method:
173
- * - cacheKeyWillByUsed()
174
- * - cachedResponseWillByUsed()
175
- *
176
- * @param key The Request or URL to use as the cache key.
177
- * @returns A matching response, if found.
178
- */ async cacheMatch(key) {
179
- const request = toRequest(key);
180
- let cachedResponse;
181
- const { cacheName, matchOptions } = this._strategy;
182
- const effectiveRequest = await this.getCacheKey(request, "read");
183
- const multiMatchOptions = {
184
- ...matchOptions,
185
- ...{
186
- cacheName
187
- }
188
- };
189
- cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);
190
- if (process.env.NODE_ENV !== "production") {
191
- if (cachedResponse) {
192
- internal.logger.debug(`Found a cached response in '${cacheName}'.`);
193
- } else {
194
- internal.logger.debug(`No cached response found in '${cacheName}'.`);
195
- }
196
- }
197
- for (const callback of this.iterateCallbacks("cachedResponseWillBeUsed")){
198
- cachedResponse = await callback({
199
- cacheName,
200
- matchOptions,
201
- cachedResponse,
202
- request: effectiveRequest,
203
- event: this.event
204
- }) || undefined;
205
- }
206
- return cachedResponse;
207
- }
208
- /**
209
- * Puts a request/response pair in the cache (and invokes any applicable
210
- * plugin callback methods) using the `cacheName` and `plugins` defined on
211
- * the strategy object.
212
- *
213
- * The following plugin lifecycle methods are invoked when using this method:
214
- * - cacheKeyWillByUsed()
215
- * - cacheWillUpdate()
216
- * - cacheDidUpdate()
217
- *
218
- * @param key The request or URL to use as the cache key.
219
- * @param response The response to cache.
220
- * @returns `false` if a cacheWillUpdate caused the response
221
- * not be cached, and `true` otherwise.
222
- */ async cachePut(key, response) {
223
- const request = toRequest(key);
224
- // Run in the next task to avoid blocking other cache reads.
225
- // https://github.com/w3c/ServiceWorker/issues/1397
226
- await internal.timeout(0);
227
- const effectiveRequest = await this.getCacheKey(request, "write");
228
- if (process.env.NODE_ENV !== "production") {
229
- if (effectiveRequest.method && effectiveRequest.method !== "GET") {
230
- throw new internal.SerwistError("attempt-to-cache-non-get-request", {
231
- url: internal.getFriendlyURL(effectiveRequest.url),
232
- method: effectiveRequest.method
233
- });
234
- }
235
- // See https://github.com/GoogleChrome/workbox/issues/2818
236
- const vary = response.headers.get("Vary");
237
- if (vary) {
238
- internal.logger.debug(`The response for ${internal.getFriendlyURL(effectiveRequest.url)} has a 'Vary: ${vary}' header. Consider setting the {ignoreVary: true} option on your strategy to ensure cache matching and deletion works as expected.`);
239
- }
240
- }
241
- if (!response) {
242
- if (process.env.NODE_ENV !== "production") {
243
- internal.logger.error(`Cannot cache non-existent response for '${internal.getFriendlyURL(effectiveRequest.url)}'.`);
244
- }
245
- throw new internal.SerwistError("cache-put-with-no-response", {
246
- url: internal.getFriendlyURL(effectiveRequest.url)
247
- });
248
- }
249
- const responseToCache = await this._ensureResponseSafeToCache(response);
250
- if (!responseToCache) {
251
- if (process.env.NODE_ENV !== "production") {
252
- internal.logger.debug(`Response '${internal.getFriendlyURL(effectiveRequest.url)}' will not be cached.`, responseToCache);
253
- }
254
- return false;
255
- }
256
- const { cacheName, matchOptions } = this._strategy;
257
- const cache = await self.caches.open(cacheName);
258
- const hasCacheUpdateCallback = this.hasCallback("cacheDidUpdate");
259
- const oldResponse = hasCacheUpdateCallback ? await internal.cacheMatchIgnoreParams(// TODO(philipwalton): the `__WB_REVISION__` param is a precaching
260
- // feature. Consider into ways to only add this behavior if using
261
- // precaching.
262
- cache, effectiveRequest.clone(), [
263
- "__WB_REVISION__"
264
- ], matchOptions) : null;
265
- if (process.env.NODE_ENV !== "production") {
266
- internal.logger.debug(`Updating the '${cacheName}' cache with a new Response ` + `for ${internal.getFriendlyURL(effectiveRequest.url)}.`);
267
- }
268
- try {
269
- await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);
270
- } catch (error) {
271
- if (error instanceof Error) {
272
- // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError
273
- if (error.name === "QuotaExceededError") {
274
- await internal.executeQuotaErrorCallbacks();
275
- }
276
- throw error;
277
- }
278
- }
279
- for (const callback of this.iterateCallbacks("cacheDidUpdate")){
280
- await callback({
281
- cacheName,
282
- oldResponse,
283
- newResponse: responseToCache.clone(),
284
- request: effectiveRequest,
285
- event: this.event
286
- });
287
- }
288
- return true;
289
- }
290
- /**
291
- * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and
292
- * executes any of those callbacks found in sequence. The final `Request`
293
- * object returned by the last plugin is treated as the cache key for cache
294
- * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have
295
- * been registered, the passed request is returned unmodified
296
- *
297
- * @param request
298
- * @param mode
299
- * @returns
300
- */ async getCacheKey(request, mode) {
301
- const key = `${request.url} | ${mode}`;
302
- if (!this._cacheKeys[key]) {
303
- let effectiveRequest = request;
304
- for (const callback of this.iterateCallbacks("cacheKeyWillBeUsed")){
305
- effectiveRequest = toRequest(await callback({
306
- mode,
307
- request: effectiveRequest,
308
- event: this.event,
309
- // params has a type any can't change right now.
310
- params: this.params
311
- }));
312
- }
313
- this._cacheKeys[key] = effectiveRequest;
314
- }
315
- return this._cacheKeys[key];
316
- }
317
- /**
318
- * Returns true if the strategy has at least one plugin with the given
319
- * callback.
320
- *
321
- * @param name The name of the callback to check for.
322
- * @returns
323
- */ hasCallback(name) {
324
- for (const plugin of this._strategy.plugins){
325
- if (name in plugin) {
326
- return true;
327
- }
328
- }
329
- return false;
330
- }
331
- /**
332
- * Runs all plugin callbacks matching the given name, in order, passing the
333
- * given param object (merged ith the current plugin state) as the only
334
- * argument.
335
- *
336
- * Note: since this method runs all plugins, it's not suitable for cases
337
- * where the return value of a callback needs to be applied prior to calling
338
- * the next callback. See `@serwist/strategies.iterateCallbacks` for how to handle that case.
339
- *
340
- * @param name The name of the callback to run within each plugin.
341
- * @param param The object to pass as the first (and only) param when executing each callback. This object will be merged with the
342
- * current plugin state prior to callback execution.
343
- */ async runCallbacks(name, param) {
344
- for (const callback of this.iterateCallbacks(name)){
345
- // TODO(philipwalton): not sure why `any` is needed. It seems like
346
- // this should work with `as SerwistPluginCallbackParam[C]`.
347
- await callback(param);
348
- }
349
- }
350
- /**
351
- * Accepts a callback and returns an iterable of matching plugin callbacks,
352
- * where each callback is wrapped with the current handler state (i.e. when
353
- * you call each callback, whatever object parameter you pass it will
354
- * be merged with the plugin's current state).
355
- *
356
- * @param name The name fo the callback to run
357
- * @returns
358
- */ *iterateCallbacks(name) {
359
- for (const plugin of this._strategy.plugins){
360
- if (typeof plugin[name] === "function") {
361
- const state = this._pluginStateMap.get(plugin);
362
- const statefulCallback = (param)=>{
363
- const statefulParam = {
364
- ...param,
365
- state
366
- };
367
- // TODO(philipwalton): not sure why `any` is needed. It seems like
368
- // this should work with `as WorkboxPluginCallbackParam[C]`.
369
- return plugin[name](statefulParam);
370
- };
371
- yield statefulCallback;
372
- }
373
- }
374
- }
375
- /**
376
- * Adds a promise to the
377
- * [extend lifetime promises](https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises)
378
- * of the event event associated with the request being handled (usually a `FetchEvent`).
379
- *
380
- * Note: you can await
381
- * `@serwist/strategies.StrategyHandler.doneWaiting`
382
- * to know when all added promises have settled.
383
- *
384
- * @param promise A promise to add to the extend lifetime promises of
385
- * the event that triggered the request.
386
- */ waitUntil(promise) {
387
- this._extendLifetimePromises.push(promise);
388
- return promise;
389
- }
390
- /**
391
- * Returns a promise that resolves once all promises passed to
392
- * `@serwist/strategies.StrategyHandler.waitUntil` have settled.
393
- *
394
- * Note: any work done after `doneWaiting()` settles should be manually
395
- * passed to an event's `waitUntil()` method (not this handler's
396
- * `waitUntil()` method), otherwise the service worker thread my be killed
397
- * prior to your work completing.
398
- */ async doneWaiting() {
399
- let promise = undefined;
400
- while(promise = this._extendLifetimePromises.shift()){
401
- await promise;
402
- }
403
- }
404
- /**
405
- * Stops running the strategy and immediately resolves any pending
406
- * `waitUntil()` promises.
407
- */ destroy() {
408
- this._handlerDeferred.resolve(null);
409
- }
410
- /**
411
- * This method will call `cacheWillUpdate` on the available plugins (or use
412
- * status === 200) to determine if the response is safe and valid to cache.
413
- *
414
- * @param response
415
- * @returns
416
- * @private
417
- */ async _ensureResponseSafeToCache(response) {
418
- let responseToCache = response;
419
- let pluginsUsed = false;
420
- for (const callback of this.iterateCallbacks("cacheWillUpdate")){
421
- responseToCache = await callback({
422
- request: this.request,
423
- response: responseToCache,
424
- event: this.event
425
- }) || undefined;
426
- pluginsUsed = true;
427
- if (!responseToCache) {
428
- break;
429
- }
430
- }
431
- if (!pluginsUsed) {
432
- if (responseToCache && responseToCache.status !== 200) {
433
- responseToCache = undefined;
434
- }
435
- if (process.env.NODE_ENV !== "production") {
436
- if (responseToCache) {
437
- if (responseToCache.status !== 200) {
438
- if (responseToCache.status === 0) {
439
- internal.logger.warn(`The response for '${this.request.url}' is an opaque response. The caching strategy that you're using will not cache opaque responses by default.`);
440
- } else {
441
- internal.logger.debug(`The response for '${this.request.url}' returned a status code of '${response.status}' and won't be cached as a result.`);
442
- }
443
- }
444
- }
445
- }
446
- }
447
- return responseToCache;
448
- }
449
- }
450
-
451
- /**
452
- * Classes extending the `Strategy` based class should implement this method,
453
- * and leverage `@serwist/strategies`'s `StrategyHandler` arg to perform all
454
- * fetching and cache logic, which will ensure all relevant cache, cache options,
455
- * fetch options and plugins are used (per the current strategy instance).
456
- */ class Strategy {
457
- cacheName;
458
- plugins;
459
- fetchOptions;
460
- matchOptions;
461
- /**
462
- * Creates a new instance of the strategy and sets all documented option
463
- * properties as public instance properties.
464
- *
465
- * Note: if a custom strategy class extends the base Strategy class and does
466
- * not need more than these properties, it does not need to define its own
467
- * constructor.
468
- *
469
- * @param options
470
- */ constructor(options = {}){
471
- this.cacheName = internal.privateCacheNames.getRuntimeName(options.cacheName);
472
- this.plugins = options.plugins || [];
473
- this.fetchOptions = options.fetchOptions;
474
- this.matchOptions = options.matchOptions;
475
- }
476
- /**
477
- * Perform a request strategy and returns a `Promise` that will resolve with
478
- * a `Response`, invoking all relevant plugin callbacks.
479
- *
480
- * When a strategy instance is registered with a `@serwist/routing` Route, this method is automatically
481
- * called when the route matches.
482
- *
483
- * Alternatively, this method can be used in a standalone `FetchEvent`
484
- * listener by passing it to `event.respondWith()`.
485
- *
486
- * @param options A `FetchEvent` or an object with the properties listed below.
487
- * @param options.request A request to run this strategy for.
488
- * @param options.event The event associated with the request.
489
- * @param options.url
490
- * @param options.params
491
- */ handle(options) {
492
- const [responseDone] = this.handleAll(options);
493
- return responseDone;
494
- }
495
- /**
496
- * Similar to `@serwist/strategies`'s `Strategy.handle`, but
497
- * instead of just returning a `Promise` that resolves to a `Response` it
498
- * it will return an tuple of `[response, done]` promises, where the former
499
- * (`response`) is equivalent to what `handle()` returns, and the latter is a
500
- * Promise that will resolve once any promises that were added to
501
- * `event.waitUntil()` as part of performing the strategy have completed.
502
- *
503
- * You can await the `done` promise to ensure any extra work performed by
504
- * the strategy (usually caching responses) completes successfully.
505
- *
506
- * @param options A `FetchEvent` or `HandlerCallbackOptions` object.
507
- * @returns A tuple of [response, done] promises that can be used to determine when the response resolves as
508
- * well as when the handler has completed all its work.
509
- */ handleAll(options) {
510
- // Allow for flexible options to be passed.
511
- if (options instanceof FetchEvent) {
512
- options = {
513
- event: options,
514
- request: options.request
515
- };
516
- }
517
- const event = options.event;
518
- const request = typeof options.request === "string" ? new Request(options.request) : options.request;
519
- const params = "params" in options ? options.params : undefined;
520
- const handler = new StrategyHandler(this, {
521
- event,
522
- request,
523
- params
524
- });
525
- const responseDone = this._getResponse(handler, request, event);
526
- const handlerDone = this._awaitComplete(responseDone, handler, request, event);
527
- // Return an array of promises, suitable for use with Promise.all().
528
- return [
529
- responseDone,
530
- handlerDone
531
- ];
532
- }
533
- async _getResponse(handler, request, event) {
534
- await handler.runCallbacks("handlerWillStart", {
535
- event,
536
- request
537
- });
538
- let response = undefined;
539
- try {
540
- response = await this._handle(request, handler);
541
- // The "official" Strategy subclasses all throw this error automatically,
542
- // but in case a third-party Strategy doesn't, ensure that we have a
543
- // consistent failure when there's no response or an error response.
544
- if (response === undefined || response.type === "error") {
545
- throw new internal.SerwistError("no-response", {
546
- url: request.url
547
- });
548
- }
549
- } catch (error) {
550
- if (error instanceof Error) {
551
- for (const callback of handler.iterateCallbacks("handlerDidError")){
552
- response = await callback({
553
- error,
554
- event,
555
- request
556
- });
557
- if (response !== undefined) {
558
- break;
559
- }
560
- }
561
- }
562
- if (!response) {
563
- throw error;
564
- }
565
- if (process.env.NODE_ENV !== "production") {
566
- throw internal.logger.log(`While responding to '${internal.getFriendlyURL(request.url)}', an ${error instanceof Error ? error.toString() : ""} error occurred. Using a fallback response provided by a handlerDidError plugin.`);
567
- }
568
- }
569
- for (const callback of handler.iterateCallbacks("handlerWillRespond")){
570
- response = await callback({
571
- event,
572
- request,
573
- response
574
- });
575
- }
576
- return response;
577
- }
578
- async _awaitComplete(responseDone, handler, request, event) {
579
- let response = undefined;
580
- let error = undefined;
581
- try {
582
- response = await responseDone;
583
- } catch (error) {
584
- // Ignore errors, as response errors should be caught via the `response`
585
- // promise above. The `done` promise will only throw for errors in
586
- // promises passed to `handler.waitUntil()`.
587
- }
588
- try {
589
- await handler.runCallbacks("handlerDidRespond", {
590
- event,
591
- request,
592
- response
593
- });
594
- await handler.doneWaiting();
595
- } catch (waitUntilError) {
596
- if (waitUntilError instanceof Error) {
597
- error = waitUntilError;
598
- }
599
- }
600
- await handler.runCallbacks("handlerDidComplete", {
601
- event,
602
- request,
603
- response,
604
- error
605
- });
606
- handler.destroy();
607
- if (error) {
608
- throw error;
609
- }
610
- }
611
- }
612
-
613
- const messages = {
614
- strategyStart: (strategyName, request)=>`Using ${strategyName} to respond to '${internal.getFriendlyURL(request.url)}'`,
615
- printFinalResponse: (response)=>{
616
- if (response) {
617
- internal.logger.groupCollapsed("View the final response here.");
618
- internal.logger.log(response || "[No response returned]");
619
- internal.logger.groupEnd();
620
- }
621
- }
622
- };
623
-
624
- /**
625
- * An implementation of a [cache first](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#cache-first-falling-back-to-network)
626
- * request strategy.
627
- *
628
- * A cache first strategy is useful for assets that have been revisioned,
629
- * such as URLs like `/styles/example.a8f5f1.css`, since they
630
- * can be cached for long periods of time.
631
- *
632
- * If the network request fails, and there is no cache match, this will throw
633
- * a `SerwistError` exception.
634
- */ class CacheFirst extends Strategy {
635
- /**
636
- * @private
637
- * @param request A request to run this strategy for.
638
- * @param handler The event that triggered the request.
639
- * @returns
640
- */ async _handle(request, handler) {
641
- const logs = [];
642
- if (process.env.NODE_ENV !== "production") {
643
- internal.assert.isInstance(request, Request, {
644
- moduleName: "@serwist/strategies",
645
- className: this.constructor.name,
646
- funcName: "makeRequest",
647
- paramName: "request"
648
- });
649
- }
650
- let response = await handler.cacheMatch(request);
651
- let error = undefined;
652
- if (!response) {
653
- if (process.env.NODE_ENV !== "production") {
654
- logs.push(`No response found in the '${this.cacheName}' cache. Will respond with a network request.`);
655
- }
656
- try {
657
- response = await handler.fetchAndCachePut(request);
658
- } catch (err) {
659
- if (err instanceof Error) {
660
- error = err;
661
- }
662
- }
663
- if (process.env.NODE_ENV !== "production") {
664
- if (response) {
665
- logs.push("Got response from network.");
666
- } else {
667
- logs.push("Unable to get a response from the network.");
668
- }
669
- }
670
- } else {
671
- if (process.env.NODE_ENV !== "production") {
672
- logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
673
- }
674
- }
675
- if (process.env.NODE_ENV !== "production") {
676
- internal.logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
677
- for (const log of logs){
678
- internal.logger.log(log);
679
- }
680
- messages.printFinalResponse(response);
681
- internal.logger.groupEnd();
682
- }
683
- if (!response) {
684
- throw new internal.SerwistError("no-response", {
685
- url: request.url,
686
- error
687
- });
688
- }
689
- return response;
690
- }
691
- }
692
-
693
- /**
694
- * An implementation of a [cache only](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#cache-only)
695
- * request strategy.
696
- *
697
- * This class is useful if you want to take advantage of any Serwist plugin.
698
- *
699
- * If there is no cache match, this will throw a `SerwistError` exception.
700
- */ class CacheOnly extends Strategy {
701
- /**
702
- * @private
703
- * @param request A request to run this strategy for.
704
- * @param handler The event that triggered the request.
705
- * @returns
706
- */ async _handle(request, handler) {
707
- if (process.env.NODE_ENV !== "production") {
708
- internal.assert.isInstance(request, Request, {
709
- moduleName: "@serwist/strategies",
710
- className: this.constructor.name,
711
- funcName: "makeRequest",
712
- paramName: "request"
713
- });
714
- }
715
- const response = await handler.cacheMatch(request);
716
- if (process.env.NODE_ENV !== "production") {
717
- internal.logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
718
- if (response) {
719
- internal.logger.log(`Found a cached response in the '${this.cacheName}' cache.`);
720
- messages.printFinalResponse(response);
721
- } else {
722
- internal.logger.log(`No response found in the '${this.cacheName}' cache.`);
723
- }
724
- internal.logger.groupEnd();
725
- }
726
- if (!response) {
727
- throw new internal.SerwistError("no-response", {
728
- url: request.url
729
- });
730
- }
731
- return response;
732
- }
733
- }
734
-
735
- /*
736
- Copyright 2018 Google LLC
737
-
738
- Use of this source code is governed by an MIT-style
739
- license that can be found in the LICENSE file or at
740
- https://opensource.org/licenses/MIT.
741
- */ const cacheOkAndOpaquePlugin = {
742
- /**
743
- * Returns a valid response (to allow caching) if the status is 200 (OK) or
744
- * 0 (opaque).
745
- *
746
- * @param options
747
- * @returns
748
- * @private
749
- */ cacheWillUpdate: async ({ response })=>{
750
- if (response.status === 200 || response.status === 0) {
751
- return response;
752
- }
753
- return null;
754
- }
755
- };
756
-
757
- /**
758
- * An implementation of a [network first](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#network-first-falling-back-to-cache)
759
- * request strategy.
760
- *
761
- * By default, this strategy will cache responses with a 200 status code as
762
- * well as [opaque responses](https://developer.chrome.com/docs/workbox/caching-resources-during-runtime/#opaque-responses).
763
- * Opaque responses are are cross-origin requests where the response doesn't
764
- * support [CORS](https://enable-cors.org/).
765
- *
766
- * If the network request fails, and there is no cache match, this will throw
767
- * a `SerwistError` exception.
768
- */ class NetworkFirst extends Strategy {
769
- _networkTimeoutSeconds;
770
- /**
771
- * @param options
772
- * This option can be used to combat
773
- * "[lie-fi](https://developers.google.com/web/fundamentals/performance/poor-connectivity/#lie-fi)"
774
- * scenarios.
775
- */ constructor(options = {}){
776
- super(options);
777
- // If this instance contains no plugins with a 'cacheWillUpdate' callback,
778
- // prepend the `cacheOkAndOpaquePlugin` plugin to the plugins list.
779
- if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
780
- this.plugins.unshift(cacheOkAndOpaquePlugin);
781
- }
782
- this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
783
- if (process.env.NODE_ENV !== "production") {
784
- if (this._networkTimeoutSeconds) {
785
- internal.assert.isType(this._networkTimeoutSeconds, "number", {
786
- moduleName: "@serwist/strategies",
787
- className: this.constructor.name,
788
- funcName: "constructor",
789
- paramName: "networkTimeoutSeconds"
790
- });
791
- }
792
- }
793
- }
794
- /**
795
- * @private
796
- * @param request A request to run this strategy for.
797
- * @param handler The event that triggered the request.
798
- * @returns
799
- */ async _handle(request, handler) {
800
- const logs = [];
801
- if (process.env.NODE_ENV !== "production") {
802
- internal.assert.isInstance(request, Request, {
803
- moduleName: "@serwist/strategies",
804
- className: this.constructor.name,
805
- funcName: "handle",
806
- paramName: "makeRequest"
807
- });
808
- }
809
- const promises = [];
810
- let timeoutId;
811
- if (this._networkTimeoutSeconds) {
812
- const { id, promise } = this._getTimeoutPromise({
813
- request,
814
- logs,
815
- handler
816
- });
817
- timeoutId = id;
818
- promises.push(promise);
819
- }
820
- const networkPromise = this._getNetworkPromise({
821
- timeoutId,
822
- request,
823
- logs,
824
- handler
825
- });
826
- promises.push(networkPromise);
827
- const response = await handler.waitUntil((async ()=>{
828
- // Promise.race() will resolve as soon as the first promise resolves.
829
- return await handler.waitUntil(Promise.race(promises)) || // If Promise.race() resolved with null, it might be due to a network
830
- // timeout + a cache miss. If that were to happen, we'd rather wait until
831
- // the networkPromise resolves instead of returning null.
832
- // Note that it's fine to await an already-resolved promise, so we don't
833
- // have to check to see if it's still "in flight".
834
- await networkPromise;
835
- })());
836
- if (process.env.NODE_ENV !== "production") {
837
- internal.logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
838
- for (const log of logs){
839
- internal.logger.log(log);
840
- }
841
- messages.printFinalResponse(response);
842
- internal.logger.groupEnd();
843
- }
844
- if (!response) {
845
- throw new internal.SerwistError("no-response", {
846
- url: request.url
847
- });
848
- }
849
- return response;
850
- }
851
- /**
852
- * @param options
853
- * @returns
854
- * @private
855
- */ _getTimeoutPromise({ request, logs, handler }) {
856
- // biome-ignore lint/suspicious/noImplicitAnyLet: setTimeout is typed with Node.js's typings, so we can't use number | undefined here.
857
- let timeoutId;
858
- const timeoutPromise = new Promise((resolve)=>{
859
- const onNetworkTimeout = async ()=>{
860
- if (process.env.NODE_ENV !== "production") {
861
- logs.push(`Timing out the network response at ${this._networkTimeoutSeconds} seconds.`);
862
- }
863
- resolve(await handler.cacheMatch(request));
864
- };
865
- timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000);
866
- });
867
- return {
868
- promise: timeoutPromise,
869
- id: timeoutId
870
- };
871
- }
872
- /**
873
- * @param options
874
- * @param options.timeoutId
875
- * @param options.request
876
- * @param options.logs A reference to the logs Array.
877
- * @param options.event
878
- * @returns
879
- *
880
- * @private
881
- */ async _getNetworkPromise({ timeoutId, request, logs, handler }) {
882
- let error = undefined;
883
- let response = undefined;
884
- try {
885
- response = await handler.fetchAndCachePut(request);
886
- } catch (fetchError) {
887
- if (fetchError instanceof Error) {
888
- error = fetchError;
889
- }
890
- }
891
- if (timeoutId) {
892
- clearTimeout(timeoutId);
893
- }
894
- if (process.env.NODE_ENV !== "production") {
895
- if (response) {
896
- logs.push("Got response from network.");
897
- } else {
898
- logs.push("Unable to get a response from the network. Will respond " + "with a cached response.");
899
- }
900
- }
901
- if (error || !response) {
902
- response = await handler.cacheMatch(request);
903
- if (process.env.NODE_ENV !== "production") {
904
- if (response) {
905
- logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
906
- } else {
907
- logs.push(`No response found in the '${this.cacheName}' cache.`);
908
- }
909
- }
910
- }
911
- return response;
912
- }
913
- }
914
-
915
- /**
916
- * An implementation of a [network only](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#network-only)
917
- * request strategy.
918
- *
919
- * This class is useful if you want to take advantage of any Serwist plugin.
920
- *
921
- * If the network request fails, this will throw a `SerwistError` exception.
922
- */ class NetworkOnly extends Strategy {
923
- _networkTimeoutSeconds;
924
- /**
925
- * @param options
926
- */ constructor(options = {}){
927
- super(options);
928
- this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
929
- }
930
- /**
931
- * @private
932
- * @param request A request to run this strategy for.
933
- * @param handler The event that triggered the request.
934
- * @returns
935
- */ async _handle(request, handler) {
936
- if (process.env.NODE_ENV !== "production") {
937
- internal.assert.isInstance(request, Request, {
938
- moduleName: "@serwist/strategies",
939
- className: this.constructor.name,
940
- funcName: "_handle",
941
- paramName: "request"
942
- });
943
- }
944
- let error = undefined;
945
- let response;
946
- try {
947
- const promises = [
948
- handler.fetch(request)
949
- ];
950
- if (this._networkTimeoutSeconds) {
951
- const timeoutPromise = internal.timeout(this._networkTimeoutSeconds * 1000);
952
- promises.push(timeoutPromise);
953
- }
954
- response = await Promise.race(promises);
955
- if (!response) {
956
- throw new Error(`Timed out the network response after ${this._networkTimeoutSeconds} seconds.`);
957
- }
958
- } catch (err) {
959
- if (err instanceof Error) {
960
- error = err;
961
- }
962
- }
963
- if (process.env.NODE_ENV !== "production") {
964
- internal.logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
965
- if (response) {
966
- internal.logger.log("Got response from network.");
967
- } else {
968
- internal.logger.log("Unable to get a response from the network.");
969
- }
970
- messages.printFinalResponse(response);
971
- internal.logger.groupEnd();
972
- }
973
- if (!response) {
974
- throw new internal.SerwistError("no-response", {
975
- url: request.url,
976
- error
977
- });
978
- }
979
- return response;
980
- }
981
- }
982
-
983
- /**
984
- * An implementation of a
985
- * [stale-while-revalidate](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#stale-while-revalidate)
986
- * request strategy.
987
- *
988
- * Resources are requested from both the cache and the network in parallel.
989
- * The strategy will respond with the cached version if available, otherwise
990
- * wait for the network response. The cache is updated with the network response
991
- * with each successful request.
992
- *
993
- * By default, this strategy will cache responses with a 200 status code as
994
- * well as [opaque responses](https://developer.chrome.com/docs/workbox/caching-resources-during-runtime/#opaque-responses).
995
- * Opaque responses are cross-origin requests where the response doesn't
996
- * support [CORS](https://enable-cors.org/).
997
- *
998
- * If the network request fails, and there is no cache match, this will throw
999
- * a `SerwistError` exception.
1000
- */ class StaleWhileRevalidate extends Strategy {
1001
- /**
1002
- * @param options
1003
- */ constructor(options = {}){
1004
- super(options);
1005
- // If this instance contains no plugins with a 'cacheWillUpdate' callback,
1006
- // prepend the `cacheOkAndOpaquePlugin` plugin to the plugins list.
1007
- if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
1008
- this.plugins.unshift(cacheOkAndOpaquePlugin);
1009
- }
1010
- }
1011
- /**
1012
- * @private
1013
- * @param request A request to run this strategy for.
1014
- * @param handler The event that triggered the request.
1015
- * @returns
1016
- */ async _handle(request, handler) {
1017
- const logs = [];
1018
- if (process.env.NODE_ENV !== "production") {
1019
- internal.assert.isInstance(request, Request, {
1020
- moduleName: "@serwist/strategies",
1021
- className: this.constructor.name,
1022
- funcName: "handle",
1023
- paramName: "request"
1024
- });
1025
- }
1026
- const fetchAndCachePromise = handler.fetchAndCachePut(request).catch(()=>{
1027
- // Swallow this error because a 'no-response' error will be thrown in
1028
- // main handler return flow. This will be in the `waitUntil()` flow.
1029
- });
1030
- void handler.waitUntil(fetchAndCachePromise);
1031
- let response = await handler.cacheMatch(request);
1032
- let error = undefined;
1033
- if (response) {
1034
- if (process.env.NODE_ENV !== "production") {
1035
- logs.push(`Found a cached response in the '${this.cacheName}' cache. Will update with the network response in the background.`);
1036
- }
1037
- } else {
1038
- if (process.env.NODE_ENV !== "production") {
1039
- logs.push(`No response found in the '${this.cacheName}' cache. Will wait for the network response.`);
1040
- }
1041
- try {
1042
- // NOTE(philipwalton): Really annoying that we have to type cast here.
1043
- // https://github.com/microsoft/TypeScript/issues/20006
1044
- response = await fetchAndCachePromise;
1045
- } catch (err) {
1046
- if (err instanceof Error) {
1047
- error = err;
1048
- }
1049
- }
1050
- }
1051
- if (process.env.NODE_ENV !== "production") {
1052
- internal.logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
1053
- for (const log of logs){
1054
- internal.logger.log(log);
1055
- }
1056
- messages.printFinalResponse(response);
1057
- internal.logger.groupEnd();
1058
- }
1059
- if (!response) {
1060
- throw new internal.SerwistError("no-response", {
1061
- url: request.url,
1062
- error
1063
- });
1064
- }
1065
- return response;
1066
- }
1067
- }
1068
-
1069
- exports.CacheFirst = CacheFirst;
1070
- exports.CacheOnly = CacheOnly;
1071
- exports.NetworkFirst = NetworkFirst;
1072
- exports.NetworkOnly = NetworkOnly;
1073
- exports.StaleWhileRevalidate = StaleWhileRevalidate;
1074
- exports.Strategy = Strategy;
1075
- exports.StrategyHandler = StrategyHandler;