@serwist/strategies 9.0.0-preview.2 → 9.0.0-preview.20

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.js CHANGED
@@ -1,723 +1 @@
1
- import { assert, Deferred, logger, getFriendlyURL, SerwistError, timeout, cacheMatchIgnoreParams, executeQuotaErrorCallbacks, privateCacheNames } from '@serwist/core/internal';
2
-
3
- function toRequest(input) {
4
- return typeof input === "string" ? new Request(input) : input;
5
- }
6
- class StrategyHandler {
7
- request;
8
- url;
9
- event;
10
- params;
11
- _cacheKeys = {};
12
- _strategy;
13
- _extendLifetimePromises;
14
- _handlerDeferred;
15
- _plugins;
16
- _pluginStateMap;
17
- constructor(strategy, options){
18
- if (process.env.NODE_ENV !== "production") {
19
- assert.isInstance(options.event, ExtendableEvent, {
20
- moduleName: "@serwist/strategies",
21
- className: "StrategyHandler",
22
- funcName: "constructor",
23
- paramName: "options.event"
24
- });
25
- }
26
- Object.assign(this, options);
27
- this.event = options.event;
28
- this._strategy = strategy;
29
- this._handlerDeferred = new Deferred();
30
- this._extendLifetimePromises = [];
31
- this._plugins = [
32
- ...strategy.plugins
33
- ];
34
- this._pluginStateMap = new Map();
35
- for (const plugin of this._plugins){
36
- this._pluginStateMap.set(plugin, {});
37
- }
38
- this.event.waitUntil(this._handlerDeferred.promise);
39
- }
40
- async fetch(input) {
41
- const { event } = this;
42
- let request = toRequest(input);
43
- if (request.mode === "navigate" && event instanceof FetchEvent && event.preloadResponse) {
44
- const possiblePreloadResponse = await event.preloadResponse;
45
- if (possiblePreloadResponse) {
46
- if (process.env.NODE_ENV !== "production") {
47
- logger.log(`Using a preloaded navigation response for '${getFriendlyURL(request.url)}'`);
48
- }
49
- return possiblePreloadResponse;
50
- }
51
- }
52
- const originalRequest = this.hasCallback("fetchDidFail") ? request.clone() : null;
53
- try {
54
- for (const cb of this.iterateCallbacks("requestWillFetch")){
55
- request = await cb({
56
- request: request.clone(),
57
- event
58
- });
59
- }
60
- } catch (err) {
61
- if (err instanceof Error) {
62
- throw new SerwistError("plugin-error-request-will-fetch", {
63
- thrownErrorMessage: err.message
64
- });
65
- }
66
- }
67
- const pluginFilteredRequest = request.clone();
68
- try {
69
- let fetchResponse;
70
- fetchResponse = await fetch(request, request.mode === "navigate" ? undefined : this._strategy.fetchOptions);
71
- if (process.env.NODE_ENV !== "production") {
72
- logger.debug(`Network request for '${getFriendlyURL(request.url)}' returned a response with status '${fetchResponse.status}'.`);
73
- }
74
- for (const callback of this.iterateCallbacks("fetchDidSucceed")){
75
- fetchResponse = await callback({
76
- event,
77
- request: pluginFilteredRequest,
78
- response: fetchResponse
79
- });
80
- }
81
- return fetchResponse;
82
- } catch (error) {
83
- if (process.env.NODE_ENV !== "production") {
84
- logger.log(`Network request for '${getFriendlyURL(request.url)}' threw an error.`, error);
85
- }
86
- if (originalRequest) {
87
- await this.runCallbacks("fetchDidFail", {
88
- error: error,
89
- event,
90
- originalRequest: originalRequest.clone(),
91
- request: pluginFilteredRequest.clone()
92
- });
93
- }
94
- throw error;
95
- }
96
- }
97
- async fetchAndCachePut(input) {
98
- const response = await this.fetch(input);
99
- const responseClone = response.clone();
100
- void this.waitUntil(this.cachePut(input, responseClone));
101
- return response;
102
- }
103
- async cacheMatch(key) {
104
- const request = toRequest(key);
105
- let cachedResponse;
106
- const { cacheName, matchOptions } = this._strategy;
107
- const effectiveRequest = await this.getCacheKey(request, "read");
108
- const multiMatchOptions = {
109
- ...matchOptions,
110
- ...{
111
- cacheName
112
- }
113
- };
114
- cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);
115
- if (process.env.NODE_ENV !== "production") {
116
- if (cachedResponse) {
117
- logger.debug(`Found a cached response in '${cacheName}'.`);
118
- } else {
119
- logger.debug(`No cached response found in '${cacheName}'.`);
120
- }
121
- }
122
- for (const callback of this.iterateCallbacks("cachedResponseWillBeUsed")){
123
- cachedResponse = await callback({
124
- cacheName,
125
- matchOptions,
126
- cachedResponse,
127
- request: effectiveRequest,
128
- event: this.event
129
- }) || undefined;
130
- }
131
- return cachedResponse;
132
- }
133
- async cachePut(key, response) {
134
- const request = toRequest(key);
135
- await timeout(0);
136
- const effectiveRequest = await this.getCacheKey(request, "write");
137
- if (process.env.NODE_ENV !== "production") {
138
- if (effectiveRequest.method && effectiveRequest.method !== "GET") {
139
- throw new SerwistError("attempt-to-cache-non-get-request", {
140
- url: getFriendlyURL(effectiveRequest.url),
141
- method: effectiveRequest.method
142
- });
143
- }
144
- const vary = response.headers.get("Vary");
145
- if (vary) {
146
- logger.debug(`The response for ${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.`);
147
- }
148
- }
149
- if (!response) {
150
- if (process.env.NODE_ENV !== "production") {
151
- logger.error(`Cannot cache non-existent response for '${getFriendlyURL(effectiveRequest.url)}'.`);
152
- }
153
- throw new SerwistError("cache-put-with-no-response", {
154
- url: getFriendlyURL(effectiveRequest.url)
155
- });
156
- }
157
- const responseToCache = await this._ensureResponseSafeToCache(response);
158
- if (!responseToCache) {
159
- if (process.env.NODE_ENV !== "production") {
160
- logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' will not be cached.`, responseToCache);
161
- }
162
- return false;
163
- }
164
- const { cacheName, matchOptions } = this._strategy;
165
- const cache = await self.caches.open(cacheName);
166
- const hasCacheUpdateCallback = this.hasCallback("cacheDidUpdate");
167
- const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams(cache, effectiveRequest.clone(), [
168
- "__WB_REVISION__"
169
- ], matchOptions) : null;
170
- if (process.env.NODE_ENV !== "production") {
171
- logger.debug(`Updating the '${cacheName}' cache with a new Response ` + `for ${getFriendlyURL(effectiveRequest.url)}.`);
172
- }
173
- try {
174
- await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);
175
- } catch (error) {
176
- if (error instanceof Error) {
177
- if (error.name === "QuotaExceededError") {
178
- await executeQuotaErrorCallbacks();
179
- }
180
- throw error;
181
- }
182
- }
183
- for (const callback of this.iterateCallbacks("cacheDidUpdate")){
184
- await callback({
185
- cacheName,
186
- oldResponse,
187
- newResponse: responseToCache.clone(),
188
- request: effectiveRequest,
189
- event: this.event
190
- });
191
- }
192
- return true;
193
- }
194
- async getCacheKey(request, mode) {
195
- const key = `${request.url} | ${mode}`;
196
- if (!this._cacheKeys[key]) {
197
- let effectiveRequest = request;
198
- for (const callback of this.iterateCallbacks("cacheKeyWillBeUsed")){
199
- effectiveRequest = toRequest(await callback({
200
- mode,
201
- request: effectiveRequest,
202
- event: this.event,
203
- params: this.params
204
- }));
205
- }
206
- this._cacheKeys[key] = effectiveRequest;
207
- }
208
- return this._cacheKeys[key];
209
- }
210
- hasCallback(name) {
211
- for (const plugin of this._strategy.plugins){
212
- if (name in plugin) {
213
- return true;
214
- }
215
- }
216
- return false;
217
- }
218
- async runCallbacks(name, param) {
219
- for (const callback of this.iterateCallbacks(name)){
220
- await callback(param);
221
- }
222
- }
223
- *iterateCallbacks(name) {
224
- for (const plugin of this._strategy.plugins){
225
- if (typeof plugin[name] === "function") {
226
- const state = this._pluginStateMap.get(plugin);
227
- const statefulCallback = (param)=>{
228
- const statefulParam = {
229
- ...param,
230
- state
231
- };
232
- return plugin[name](statefulParam);
233
- };
234
- yield statefulCallback;
235
- }
236
- }
237
- }
238
- waitUntil(promise) {
239
- this._extendLifetimePromises.push(promise);
240
- return promise;
241
- }
242
- async doneWaiting() {
243
- let promise = undefined;
244
- while(promise = this._extendLifetimePromises.shift()){
245
- await promise;
246
- }
247
- }
248
- destroy() {
249
- this._handlerDeferred.resolve(null);
250
- }
251
- async _ensureResponseSafeToCache(response) {
252
- let responseToCache = response;
253
- let pluginsUsed = false;
254
- for (const callback of this.iterateCallbacks("cacheWillUpdate")){
255
- responseToCache = await callback({
256
- request: this.request,
257
- response: responseToCache,
258
- event: this.event
259
- }) || undefined;
260
- pluginsUsed = true;
261
- if (!responseToCache) {
262
- break;
263
- }
264
- }
265
- if (!pluginsUsed) {
266
- if (responseToCache && responseToCache.status !== 200) {
267
- responseToCache = undefined;
268
- }
269
- if (process.env.NODE_ENV !== "production") {
270
- if (responseToCache) {
271
- if (responseToCache.status !== 200) {
272
- if (responseToCache.status === 0) {
273
- 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.`);
274
- } else {
275
- logger.debug(`The response for '${this.request.url}' returned a status code of '${response.status}' and won't be cached as a result.`);
276
- }
277
- }
278
- }
279
- }
280
- }
281
- return responseToCache;
282
- }
283
- }
284
-
285
- class Strategy {
286
- cacheName;
287
- plugins;
288
- fetchOptions;
289
- matchOptions;
290
- constructor(options = {}){
291
- this.cacheName = privateCacheNames.getRuntimeName(options.cacheName);
292
- this.plugins = options.plugins || [];
293
- this.fetchOptions = options.fetchOptions;
294
- this.matchOptions = options.matchOptions;
295
- }
296
- handle(options) {
297
- const [responseDone] = this.handleAll(options);
298
- return responseDone;
299
- }
300
- handleAll(options) {
301
- if (options instanceof FetchEvent) {
302
- options = {
303
- event: options,
304
- request: options.request
305
- };
306
- }
307
- const event = options.event;
308
- const request = typeof options.request === "string" ? new Request(options.request) : options.request;
309
- const params = "params" in options ? options.params : undefined;
310
- const handler = new StrategyHandler(this, {
311
- event,
312
- request,
313
- params
314
- });
315
- const responseDone = this._getResponse(handler, request, event);
316
- const handlerDone = this._awaitComplete(responseDone, handler, request, event);
317
- return [
318
- responseDone,
319
- handlerDone
320
- ];
321
- }
322
- async _getResponse(handler, request, event) {
323
- await handler.runCallbacks("handlerWillStart", {
324
- event,
325
- request
326
- });
327
- let response = undefined;
328
- try {
329
- response = await this._handle(request, handler);
330
- if (response === undefined || response.type === "error") {
331
- throw new SerwistError("no-response", {
332
- url: request.url
333
- });
334
- }
335
- } catch (error) {
336
- if (error instanceof Error) {
337
- for (const callback of handler.iterateCallbacks("handlerDidError")){
338
- response = await callback({
339
- error,
340
- event,
341
- request
342
- });
343
- if (response !== undefined) {
344
- break;
345
- }
346
- }
347
- }
348
- if (!response) {
349
- throw error;
350
- }
351
- if (process.env.NODE_ENV !== "production") {
352
- throw logger.log(`While responding to '${getFriendlyURL(request.url)}', an ${error instanceof Error ? error.toString() : ""} error occurred. Using a fallback response provided by a handlerDidError plugin.`);
353
- }
354
- }
355
- for (const callback of handler.iterateCallbacks("handlerWillRespond")){
356
- response = await callback({
357
- event,
358
- request,
359
- response
360
- });
361
- }
362
- return response;
363
- }
364
- async _awaitComplete(responseDone, handler, request, event) {
365
- let response = undefined;
366
- let error = undefined;
367
- try {
368
- response = await responseDone;
369
- } catch (error) {}
370
- try {
371
- await handler.runCallbacks("handlerDidRespond", {
372
- event,
373
- request,
374
- response
375
- });
376
- await handler.doneWaiting();
377
- } catch (waitUntilError) {
378
- if (waitUntilError instanceof Error) {
379
- error = waitUntilError;
380
- }
381
- }
382
- await handler.runCallbacks("handlerDidComplete", {
383
- event,
384
- request,
385
- response,
386
- error
387
- });
388
- handler.destroy();
389
- if (error) {
390
- throw error;
391
- }
392
- }
393
- }
394
-
395
- const messages = {
396
- strategyStart: (strategyName, request)=>`Using ${strategyName} to respond to '${getFriendlyURL(request.url)}'`,
397
- printFinalResponse: (response)=>{
398
- if (response) {
399
- logger.groupCollapsed("View the final response here.");
400
- logger.log(response || "[No response returned]");
401
- logger.groupEnd();
402
- }
403
- }
404
- };
405
-
406
- class CacheFirst extends Strategy {
407
- async _handle(request, handler) {
408
- const logs = [];
409
- if (process.env.NODE_ENV !== "production") {
410
- assert.isInstance(request, Request, {
411
- moduleName: "@serwist/strategies",
412
- className: this.constructor.name,
413
- funcName: "makeRequest",
414
- paramName: "request"
415
- });
416
- }
417
- let response = await handler.cacheMatch(request);
418
- let error = undefined;
419
- if (!response) {
420
- if (process.env.NODE_ENV !== "production") {
421
- logs.push(`No response found in the '${this.cacheName}' cache. Will respond with a network request.`);
422
- }
423
- try {
424
- response = await handler.fetchAndCachePut(request);
425
- } catch (err) {
426
- if (err instanceof Error) {
427
- error = err;
428
- }
429
- }
430
- if (process.env.NODE_ENV !== "production") {
431
- if (response) {
432
- logs.push("Got response from network.");
433
- } else {
434
- logs.push("Unable to get a response from the network.");
435
- }
436
- }
437
- } else {
438
- if (process.env.NODE_ENV !== "production") {
439
- logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
440
- }
441
- }
442
- if (process.env.NODE_ENV !== "production") {
443
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
444
- for (const log of logs){
445
- logger.log(log);
446
- }
447
- messages.printFinalResponse(response);
448
- logger.groupEnd();
449
- }
450
- if (!response) {
451
- throw new SerwistError("no-response", {
452
- url: request.url,
453
- error
454
- });
455
- }
456
- return response;
457
- }
458
- }
459
-
460
- class CacheOnly extends Strategy {
461
- async _handle(request, handler) {
462
- if (process.env.NODE_ENV !== "production") {
463
- assert.isInstance(request, Request, {
464
- moduleName: "@serwist/strategies",
465
- className: this.constructor.name,
466
- funcName: "makeRequest",
467
- paramName: "request"
468
- });
469
- }
470
- const response = await handler.cacheMatch(request);
471
- if (process.env.NODE_ENV !== "production") {
472
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
473
- if (response) {
474
- logger.log(`Found a cached response in the '${this.cacheName}' cache.`);
475
- messages.printFinalResponse(response);
476
- } else {
477
- logger.log(`No response found in the '${this.cacheName}' cache.`);
478
- }
479
- logger.groupEnd();
480
- }
481
- if (!response) {
482
- throw new SerwistError("no-response", {
483
- url: request.url
484
- });
485
- }
486
- return response;
487
- }
488
- }
489
-
490
- const cacheOkAndOpaquePlugin = {
491
- cacheWillUpdate: async ({ response })=>{
492
- if (response.status === 200 || response.status === 0) {
493
- return response;
494
- }
495
- return null;
496
- }
497
- };
498
-
499
- class NetworkFirst extends Strategy {
500
- _networkTimeoutSeconds;
501
- constructor(options = {}){
502
- super(options);
503
- if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
504
- this.plugins.unshift(cacheOkAndOpaquePlugin);
505
- }
506
- this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
507
- if (process.env.NODE_ENV !== "production") {
508
- if (this._networkTimeoutSeconds) {
509
- assert.isType(this._networkTimeoutSeconds, "number", {
510
- moduleName: "@serwist/strategies",
511
- className: this.constructor.name,
512
- funcName: "constructor",
513
- paramName: "networkTimeoutSeconds"
514
- });
515
- }
516
- }
517
- }
518
- async _handle(request, handler) {
519
- const logs = [];
520
- if (process.env.NODE_ENV !== "production") {
521
- assert.isInstance(request, Request, {
522
- moduleName: "@serwist/strategies",
523
- className: this.constructor.name,
524
- funcName: "handle",
525
- paramName: "makeRequest"
526
- });
527
- }
528
- const promises = [];
529
- let timeoutId;
530
- if (this._networkTimeoutSeconds) {
531
- const { id, promise } = this._getTimeoutPromise({
532
- request,
533
- logs,
534
- handler
535
- });
536
- timeoutId = id;
537
- promises.push(promise);
538
- }
539
- const networkPromise = this._getNetworkPromise({
540
- timeoutId,
541
- request,
542
- logs,
543
- handler
544
- });
545
- promises.push(networkPromise);
546
- const response = await handler.waitUntil((async ()=>{
547
- return await handler.waitUntil(Promise.race(promises)) || await networkPromise;
548
- })());
549
- if (process.env.NODE_ENV !== "production") {
550
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
551
- for (const log of logs){
552
- logger.log(log);
553
- }
554
- messages.printFinalResponse(response);
555
- logger.groupEnd();
556
- }
557
- if (!response) {
558
- throw new SerwistError("no-response", {
559
- url: request.url
560
- });
561
- }
562
- return response;
563
- }
564
- _getTimeoutPromise({ request, logs, handler }) {
565
- let timeoutId;
566
- const timeoutPromise = new Promise((resolve)=>{
567
- const onNetworkTimeout = async ()=>{
568
- if (process.env.NODE_ENV !== "production") {
569
- logs.push(`Timing out the network response at ${this._networkTimeoutSeconds} seconds.`);
570
- }
571
- resolve(await handler.cacheMatch(request));
572
- };
573
- timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000);
574
- });
575
- return {
576
- promise: timeoutPromise,
577
- id: timeoutId
578
- };
579
- }
580
- async _getNetworkPromise({ timeoutId, request, logs, handler }) {
581
- let error = undefined;
582
- let response = undefined;
583
- try {
584
- response = await handler.fetchAndCachePut(request);
585
- } catch (fetchError) {
586
- if (fetchError instanceof Error) {
587
- error = fetchError;
588
- }
589
- }
590
- if (timeoutId) {
591
- clearTimeout(timeoutId);
592
- }
593
- if (process.env.NODE_ENV !== "production") {
594
- if (response) {
595
- logs.push("Got response from network.");
596
- } else {
597
- logs.push("Unable to get a response from the network. Will respond " + "with a cached response.");
598
- }
599
- }
600
- if (error || !response) {
601
- response = await handler.cacheMatch(request);
602
- if (process.env.NODE_ENV !== "production") {
603
- if (response) {
604
- logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
605
- } else {
606
- logs.push(`No response found in the '${this.cacheName}' cache.`);
607
- }
608
- }
609
- }
610
- return response;
611
- }
612
- }
613
-
614
- class NetworkOnly extends Strategy {
615
- _networkTimeoutSeconds;
616
- constructor(options = {}){
617
- super(options);
618
- this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
619
- }
620
- async _handle(request, handler) {
621
- if (process.env.NODE_ENV !== "production") {
622
- assert.isInstance(request, Request, {
623
- moduleName: "@serwist/strategies",
624
- className: this.constructor.name,
625
- funcName: "_handle",
626
- paramName: "request"
627
- });
628
- }
629
- let error = undefined;
630
- let response;
631
- try {
632
- const promises = [
633
- handler.fetch(request)
634
- ];
635
- if (this._networkTimeoutSeconds) {
636
- const timeoutPromise = timeout(this._networkTimeoutSeconds * 1000);
637
- promises.push(timeoutPromise);
638
- }
639
- response = await Promise.race(promises);
640
- if (!response) {
641
- throw new Error(`Timed out the network response after ${this._networkTimeoutSeconds} seconds.`);
642
- }
643
- } catch (err) {
644
- if (err instanceof Error) {
645
- error = err;
646
- }
647
- }
648
- if (process.env.NODE_ENV !== "production") {
649
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
650
- if (response) {
651
- logger.log("Got response from network.");
652
- } else {
653
- logger.log("Unable to get a response from the network.");
654
- }
655
- messages.printFinalResponse(response);
656
- logger.groupEnd();
657
- }
658
- if (!response) {
659
- throw new SerwistError("no-response", {
660
- url: request.url,
661
- error
662
- });
663
- }
664
- return response;
665
- }
666
- }
667
-
668
- class StaleWhileRevalidate extends Strategy {
669
- constructor(options = {}){
670
- super(options);
671
- if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
672
- this.plugins.unshift(cacheOkAndOpaquePlugin);
673
- }
674
- }
675
- async _handle(request, handler) {
676
- const logs = [];
677
- if (process.env.NODE_ENV !== "production") {
678
- assert.isInstance(request, Request, {
679
- moduleName: "@serwist/strategies",
680
- className: this.constructor.name,
681
- funcName: "handle",
682
- paramName: "request"
683
- });
684
- }
685
- const fetchAndCachePromise = handler.fetchAndCachePut(request).catch(()=>{});
686
- void handler.waitUntil(fetchAndCachePromise);
687
- let response = await handler.cacheMatch(request);
688
- let error = undefined;
689
- if (response) {
690
- if (process.env.NODE_ENV !== "production") {
691
- logs.push(`Found a cached response in the '${this.cacheName}' cache. Will update with the network response in the background.`);
692
- }
693
- } else {
694
- if (process.env.NODE_ENV !== "production") {
695
- logs.push(`No response found in the '${this.cacheName}' cache. Will wait for the network response.`);
696
- }
697
- try {
698
- response = await fetchAndCachePromise;
699
- } catch (err) {
700
- if (err instanceof Error) {
701
- error = err;
702
- }
703
- }
704
- }
705
- if (process.env.NODE_ENV !== "production") {
706
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
707
- for (const log of logs){
708
- logger.log(log);
709
- }
710
- messages.printFinalResponse(response);
711
- logger.groupEnd();
712
- }
713
- if (!response) {
714
- throw new SerwistError("no-response", {
715
- url: request.url,
716
- error
717
- });
718
- }
719
- return response;
720
- }
721
- }
722
-
723
- export { CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate, Strategy, StrategyHandler };
1
+ export { CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate, Strategy, StrategyHandler } from '@serwist/sw/strategies';