serwist 10.0.0-preview.1 → 10.0.0-preview.2

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.
Files changed (136) hide show
  1. package/dist/NavigationRoute.d.ts.map +1 -1
  2. package/dist/RegExpRoute.d.ts.map +1 -1
  3. package/dist/Route.d.ts.map +1 -1
  4. package/dist/Serwist.d.ts +5 -5
  5. package/dist/Serwist.d.ts.map +1 -1
  6. package/dist/chunks/{resultingClientExists.js → waitUntil.js} +117 -117
  7. package/dist/copyResponse.d.ts.map +1 -1
  8. package/dist/index.d.ts +11 -37
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.internal.d.ts +16 -16
  11. package/dist/index.internal.d.ts.map +1 -1
  12. package/dist/index.internal.js +2 -2
  13. package/dist/index.js +1400 -1387
  14. package/dist/lib/backgroundSync/StorableRequest.d.ts.map +1 -1
  15. package/dist/lib/backgroundSync/index.d.ts +6 -0
  16. package/dist/lib/backgroundSync/index.d.ts.map +1 -0
  17. package/dist/lib/broadcastUpdate/BroadcastCacheUpdate.d.ts.map +1 -1
  18. package/dist/lib/broadcastUpdate/index.d.ts +6 -0
  19. package/dist/lib/broadcastUpdate/index.d.ts.map +1 -0
  20. package/dist/lib/broadcastUpdate/responsesAreSame.d.ts.map +1 -1
  21. package/dist/lib/cacheableResponse/index.d.ts +4 -0
  22. package/dist/lib/cacheableResponse/index.d.ts.map +1 -0
  23. package/dist/{controllers → lib/controllers}/PrecacheController/PrecacheCacheKeyPlugin.d.ts +1 -1
  24. package/dist/lib/controllers/PrecacheController/PrecacheCacheKeyPlugin.d.ts.map +1 -0
  25. package/dist/{controllers → lib/controllers}/PrecacheController/PrecacheController.d.ts +9 -5
  26. package/dist/lib/controllers/PrecacheController/PrecacheController.d.ts.map +1 -0
  27. package/dist/{controllers → lib/controllers}/PrecacheController/PrecacheInstallReportPlugin.d.ts +1 -1
  28. package/dist/lib/controllers/PrecacheController/PrecacheInstallReportPlugin.d.ts.map +1 -0
  29. package/dist/{controllers → lib/controllers}/PrecacheController/PrecacheRoute.d.ts +1 -1
  30. package/dist/lib/controllers/PrecacheController/PrecacheRoute.d.ts.map +1 -0
  31. package/dist/{controllers → lib/controllers}/PrecacheController/PrecacheStrategy.d.ts +4 -4
  32. package/dist/lib/controllers/PrecacheController/PrecacheStrategy.d.ts.map +1 -0
  33. package/dist/{controllers → lib/controllers}/PrecacheController/parsePrecacheOptions.d.ts +3 -3
  34. package/dist/lib/controllers/PrecacheController/parsePrecacheOptions.d.ts.map +1 -0
  35. package/dist/{controllers → lib/controllers}/RuntimeCacheController.d.ts +10 -5
  36. package/dist/lib/controllers/RuntimeCacheController.d.ts.map +1 -0
  37. package/dist/lib/controllers/index.d.ts +4 -0
  38. package/dist/lib/controllers/index.d.ts.map +1 -0
  39. package/dist/lib/expiration/ExpirationPlugin.d.ts.map +1 -1
  40. package/dist/lib/expiration/index.d.ts +4 -0
  41. package/dist/lib/expiration/index.d.ts.map +1 -0
  42. package/dist/lib/googleAnalytics/index.d.ts +3 -0
  43. package/dist/lib/googleAnalytics/index.d.ts.map +1 -0
  44. package/dist/lib/googleAnalytics/initializeGoogleAnalytics.d.ts.map +1 -1
  45. package/dist/lib/precaching/PrecacheFallbackPlugin.d.ts +12 -6
  46. package/dist/lib/precaching/PrecacheFallbackPlugin.d.ts.map +1 -1
  47. package/dist/lib/precaching/index.d.ts +3 -0
  48. package/dist/lib/precaching/index.d.ts.map +1 -0
  49. package/dist/lib/rangeRequests/createPartialResponse.d.ts.map +1 -1
  50. package/dist/lib/rangeRequests/index.d.ts +3 -0
  51. package/dist/lib/rangeRequests/index.d.ts.map +1 -0
  52. package/dist/lib/rangeRequests/utils/calculateEffectiveBoundaries.d.ts.map +1 -1
  53. package/dist/lib/rangeRequests/utils/parseRangeHeader.d.ts.map +1 -1
  54. package/dist/lib/strategies/Strategy.d.ts.map +1 -1
  55. package/dist/lib/strategies/StrategyHandler.d.ts.map +1 -1
  56. package/dist/lib/strategies/index.d.ts +11 -0
  57. package/dist/lib/strategies/index.d.ts.map +1 -0
  58. package/dist/navigationPreload.d.ts.map +1 -1
  59. package/dist/registerQuotaErrorCallback.d.ts.map +1 -1
  60. package/dist/setCacheNameDetails.d.ts +1 -1
  61. package/dist/setCacheNameDetails.d.ts.map +1 -1
  62. package/dist/types.d.ts +24 -5
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/utils/SerwistError.d.ts +1 -1
  65. package/dist/utils/SerwistError.d.ts.map +1 -1
  66. package/dist/utils/cleanupOutdatedCaches.d.ts.map +1 -1
  67. package/dist/utils/createCacheKey.d.ts.map +1 -1
  68. package/dist/utils/deleteOutdatedCaches.d.ts.map +1 -1
  69. package/dist/utils/getFriendlyURL.d.ts.map +1 -1
  70. package/dist/utils/normalizeHandler.d.ts.map +1 -1
  71. package/dist/utils/parseRoute.d.ts.map +1 -1
  72. package/dist/utils/printCleanupDetails.d.ts.map +1 -1
  73. package/dist/utils/printInstallDetails.d.ts.map +1 -1
  74. package/dist/utils/removeIgnoredSearchParams.d.ts.map +1 -1
  75. package/dist/utils/waitUntil.d.ts.map +1 -1
  76. package/package.json +28 -5
  77. package/src/NavigationRoute.ts +2 -2
  78. package/src/RegExpRoute.ts +2 -2
  79. package/src/Route.ts +2 -2
  80. package/src/Serwist.ts +23 -19
  81. package/src/cacheNames.ts +1 -1
  82. package/src/copyResponse.ts +2 -2
  83. package/src/index.internal.ts +16 -16
  84. package/src/index.ts +68 -96
  85. package/src/lib/backgroundSync/BackgroundSyncQueue.ts +4 -4
  86. package/src/lib/backgroundSync/BackgroundSyncQueueStore.ts +1 -1
  87. package/src/lib/backgroundSync/StorableRequest.ts +1 -1
  88. package/src/lib/backgroundSync/index.ts +5 -0
  89. package/src/lib/broadcastUpdate/BroadcastCacheUpdate.ts +4 -4
  90. package/src/lib/broadcastUpdate/index.ts +5 -0
  91. package/src/lib/broadcastUpdate/responsesAreSame.ts +2 -2
  92. package/src/lib/cacheableResponse/CacheableResponse.ts +4 -4
  93. package/src/lib/cacheableResponse/index.ts +3 -0
  94. package/src/{controllers → lib/controllers}/PrecacheController/PrecacheCacheKeyPlugin.ts +1 -1
  95. package/src/{controllers → lib/controllers}/PrecacheController/PrecacheController.ts +19 -16
  96. package/src/{controllers → lib/controllers}/PrecacheController/PrecacheInstallReportPlugin.ts +1 -1
  97. package/src/{controllers → lib/controllers}/PrecacheController/PrecacheRoute.ts +5 -5
  98. package/src/{controllers → lib/controllers}/PrecacheController/PrecacheStrategy.ts +10 -10
  99. package/src/{controllers → lib/controllers}/PrecacheController/parsePrecacheOptions.ts +3 -3
  100. package/src/{controllers → lib/controllers}/RuntimeCacheController.ts +6 -6
  101. package/src/lib/controllers/index.ts +3 -0
  102. package/src/lib/expiration/CacheExpiration.ts +3 -3
  103. package/src/lib/expiration/ExpirationPlugin.ts +6 -6
  104. package/src/lib/expiration/index.ts +3 -0
  105. package/src/lib/googleAnalytics/index.ts +2 -0
  106. package/src/lib/googleAnalytics/initializeGoogleAnalytics.ts +5 -5
  107. package/src/lib/precaching/PrecacheFallbackPlugin.ts +18 -7
  108. package/src/lib/precaching/index.ts +2 -0
  109. package/src/lib/rangeRequests/createPartialResponse.ts +3 -3
  110. package/src/lib/rangeRequests/index.ts +2 -0
  111. package/src/lib/rangeRequests/utils/calculateEffectiveBoundaries.ts +2 -2
  112. package/src/lib/rangeRequests/utils/parseRangeHeader.ts +2 -2
  113. package/src/lib/strategies/CacheFirst.ts +3 -3
  114. package/src/lib/strategies/CacheOnly.ts +3 -3
  115. package/src/lib/strategies/NetworkFirst.ts +3 -3
  116. package/src/lib/strategies/NetworkOnly.ts +4 -4
  117. package/src/lib/strategies/StaleWhileRevalidate.ts +3 -3
  118. package/src/lib/strategies/Strategy.ts +4 -4
  119. package/src/lib/strategies/StrategyHandler.ts +9 -9
  120. package/src/lib/strategies/index.ts +10 -0
  121. package/src/lib/strategies/utils/messages.ts +2 -2
  122. package/src/models/messages/messages.ts +3 -3
  123. package/src/navigationPreload.ts +1 -1
  124. package/src/registerQuotaErrorCallback.ts +2 -2
  125. package/src/setCacheNameDetails.ts +4 -4
  126. package/src/types.ts +36 -5
  127. package/src/utils/SerwistError.ts +2 -2
  128. package/src/utils/cacheNames.ts +1 -1
  129. package/src/utils/executeQuotaErrorCallbacks.ts +1 -1
  130. package/dist/controllers/PrecacheController/PrecacheCacheKeyPlugin.d.ts.map +0 -1
  131. package/dist/controllers/PrecacheController/PrecacheController.d.ts.map +0 -1
  132. package/dist/controllers/PrecacheController/PrecacheInstallReportPlugin.d.ts.map +0 -1
  133. package/dist/controllers/PrecacheController/PrecacheRoute.d.ts.map +0 -1
  134. package/dist/controllers/PrecacheController/PrecacheStrategy.d.ts.map +0 -1
  135. package/dist/controllers/PrecacheController/parsePrecacheOptions.d.ts.map +0 -1
  136. package/dist/controllers/RuntimeCacheController.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,1094 +1,1198 @@
1
- import { S as SerwistError, c as canConstructResponseFromBodyStream, f as finalAssertExports, D as Deferred, l as logger, g as getFriendlyURL, t as timeout, a as cacheMatchIgnoreParams, e as executeQuotaErrorCallbacks, b as cacheNames$1, d as clientsClaim, w as waitUntil, r as resultingClientExists, q as quotaErrorCallbacks } from './chunks/resultingClientExists.js';
1
+ import { f as finalAssertExports, l as logger, S as SerwistError, g as getFriendlyURL, c as canConstructResponseFromBodyStream, D as Deferred, t as timeout, a as cacheMatchIgnoreParams, e as executeQuotaErrorCallbacks, b as cacheNames$1, d as clientsClaim, w as waitUntil, q as quotaErrorCallbacks, r as resultingClientExists } from './chunks/waitUntil.js';
2
2
  import { parallel } from '@serwist/utils';
3
3
  import { openDB, deleteDB } from 'idb';
4
4
 
5
- const copyResponse = async (response, modifier)=>{
6
- let origin = null;
7
- if (response.url) {
8
- const responseURL = new URL(response.url);
9
- origin = responseURL.origin;
5
+ const normalizeHandler = (handler)=>{
6
+ if (handler && typeof handler === "object") {
7
+ if (process.env.NODE_ENV !== "production") {
8
+ finalAssertExports.hasMethod(handler, "handle", {
9
+ moduleName: "serwist",
10
+ className: "Route",
11
+ funcName: "constructor",
12
+ paramName: "handler"
13
+ });
14
+ }
15
+ return handler;
10
16
  }
11
- if (origin !== self.location.origin) {
12
- throw new SerwistError("cross-origin-copy-response", {
13
- origin
17
+ if (process.env.NODE_ENV !== "production") {
18
+ finalAssertExports.isType(handler, "function", {
19
+ moduleName: "serwist",
20
+ className: "Route",
21
+ funcName: "constructor",
22
+ paramName: "handler"
14
23
  });
15
24
  }
16
- const clonedResponse = response.clone();
17
- const responseInit = {
18
- headers: new Headers(clonedResponse.headers),
19
- status: clonedResponse.status,
20
- statusText: clonedResponse.statusText
25
+ return {
26
+ handle: handler
21
27
  };
22
- const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit;
23
- const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob();
24
- return new Response(body, modifiedResponseInit);
25
28
  };
26
29
 
27
- function toRequest(input) {
28
- return typeof input === "string" ? new Request(input) : input;
30
+ const defaultMethod = "GET";
31
+ const validMethods = [
32
+ "DELETE",
33
+ "GET",
34
+ "HEAD",
35
+ "PATCH",
36
+ "POST",
37
+ "PUT"
38
+ ];
39
+
40
+ class Route {
41
+ handler;
42
+ match;
43
+ method;
44
+ catchHandler;
45
+ constructor(match, handler, method = defaultMethod){
46
+ if (process.env.NODE_ENV !== "production") {
47
+ finalAssertExports.isType(match, "function", {
48
+ moduleName: "serwist",
49
+ className: "Route",
50
+ funcName: "constructor",
51
+ paramName: "match"
52
+ });
53
+ if (method) {
54
+ finalAssertExports.isOneOf(method, validMethods, {
55
+ paramName: "method"
56
+ });
57
+ }
58
+ }
59
+ this.handler = normalizeHandler(handler);
60
+ this.match = match;
61
+ this.method = method;
62
+ }
63
+ setCatchHandler(handler) {
64
+ this.catchHandler = normalizeHandler(handler);
65
+ }
29
66
  }
30
- class StrategyHandler {
31
- event;
32
- request;
33
- url;
34
- params;
35
- _cacheKeys = {};
36
- _strategy;
37
- _handlerDeferred;
38
- _extendLifetimePromises;
39
- _plugins;
40
- _pluginStateMap;
41
- constructor(strategy, options){
67
+
68
+ class NavigationRoute extends Route {
69
+ _allowlist;
70
+ _denylist;
71
+ constructor(handler, { allowlist = [
72
+ /./
73
+ ], denylist = [] } = {}){
42
74
  if (process.env.NODE_ENV !== "production") {
43
- finalAssertExports.isInstance(options.event, ExtendableEvent, {
75
+ finalAssertExports.isArrayOfClass(allowlist, RegExp, {
44
76
  moduleName: "serwist",
45
- className: "StrategyHandler",
77
+ className: "NavigationRoute",
46
78
  funcName: "constructor",
47
- paramName: "options.event"
79
+ paramName: "options.allowlist"
48
80
  });
49
- finalAssertExports.isInstance(options.request, Request, {
81
+ finalAssertExports.isArrayOfClass(denylist, RegExp, {
50
82
  moduleName: "serwist",
51
- className: "StrategyHandler",
83
+ className: "NavigationRoute",
52
84
  funcName: "constructor",
53
- paramName: "options.request"
85
+ paramName: "options.denylist"
54
86
  });
55
87
  }
56
- this.event = options.event;
57
- this.request = options.request;
58
- if (options.url) {
59
- this.url = options.url;
60
- this.params = options.params;
61
- }
62
- this._strategy = strategy;
63
- this._handlerDeferred = new Deferred();
64
- this._extendLifetimePromises = [];
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);
88
+ super((options)=>this._match(options), handler);
89
+ this._allowlist = allowlist;
90
+ this._denylist = denylist;
73
91
  }
74
- async fetch(input) {
75
- const { event } = this;
76
- let request = toRequest(input);
77
- const preloadResponse = await this.getPreloadResponse();
78
- if (preloadResponse) {
79
- return preloadResponse;
92
+ _match({ url, request }) {
93
+ if (request && request.mode !== "navigate") {
94
+ return false;
80
95
  }
81
- const originalRequest = this.hasCallback("fetchDidFail") ? request.clone() : null;
82
- try {
83
- for (const cb of this.iterateCallbacks("requestWillFetch")){
84
- request = await cb({
85
- request: request.clone(),
86
- event
87
- });
88
- }
89
- } catch (err) {
90
- if (err instanceof Error) {
91
- throw new SerwistError("plugin-error-request-will-fetch", {
92
- thrownErrorMessage: err.message
93
- });
96
+ const pathnameAndSearch = url.pathname + url.search;
97
+ for (const regExp of this._denylist){
98
+ if (regExp.test(pathnameAndSearch)) {
99
+ if (process.env.NODE_ENV !== "production") {
100
+ logger.log(`The navigation route ${pathnameAndSearch} is not being used, since the URL matches this denylist pattern: ${regExp.toString()}`);
101
+ }
102
+ return false;
94
103
  }
95
104
  }
96
- const pluginFilteredRequest = request.clone();
97
- try {
98
- let fetchResponse;
99
- fetchResponse = await fetch(request, request.mode === "navigate" ? undefined : this._strategy.fetchOptions);
100
- if (process.env.NODE_ENV !== "production") {
101
- logger.debug(`Network request for '${getFriendlyURL(request.url)}' returned a response with status '${fetchResponse.status}'.`);
102
- }
103
- for (const callback of this.iterateCallbacks("fetchDidSucceed")){
104
- fetchResponse = await callback({
105
- event,
106
- request: pluginFilteredRequest,
107
- response: fetchResponse
108
- });
109
- }
110
- return fetchResponse;
111
- } catch (error) {
105
+ if (this._allowlist.some((regExp)=>regExp.test(pathnameAndSearch))) {
112
106
  if (process.env.NODE_ENV !== "production") {
113
- logger.log(`Network request for '${getFriendlyURL(request.url)}' threw an error.`, error);
114
- }
115
- if (originalRequest) {
116
- await this.runCallbacks("fetchDidFail", {
117
- error: error,
118
- event,
119
- originalRequest: originalRequest.clone(),
120
- request: pluginFilteredRequest.clone()
121
- });
107
+ logger.debug(`The navigation route ${pathnameAndSearch} is being used.`);
122
108
  }
123
- throw error;
109
+ return true;
124
110
  }
125
- }
126
- async fetchAndCachePut(input) {
127
- const response = await this.fetch(input);
128
- const responseClone = response.clone();
129
- void this.waitUntil(this.cachePut(input, responseClone));
130
- return response;
131
- }
132
- async cacheMatch(key) {
133
- const request = toRequest(key);
134
- let cachedResponse;
135
- const { cacheName, matchOptions } = this._strategy;
136
- const effectiveRequest = await this.getCacheKey(request, "read");
137
- const multiMatchOptions = {
138
- ...matchOptions,
139
- ...{
140
- cacheName
141
- }
142
- };
143
- cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);
144
111
  if (process.env.NODE_ENV !== "production") {
145
- if (cachedResponse) {
146
- logger.debug(`Found a cached response in '${cacheName}'.`);
147
- } else {
148
- logger.debug(`No cached response found in '${cacheName}'.`);
149
- }
150
- }
151
- for (const callback of this.iterateCallbacks("cachedResponseWillBeUsed")){
152
- cachedResponse = await callback({
153
- cacheName,
154
- matchOptions,
155
- cachedResponse,
156
- request: effectiveRequest,
157
- event: this.event
158
- }) || undefined;
112
+ logger.log(`The navigation route ${pathnameAndSearch} is not being used, since the URL being navigated to doesn't match the allowlist.`);
159
113
  }
160
- return cachedResponse;
114
+ return false;
161
115
  }
162
- async cachePut(key, response) {
163
- const request = toRequest(key);
164
- await timeout(0);
165
- const effectiveRequest = await this.getCacheKey(request, "write");
116
+ }
117
+
118
+ class RegExpRoute extends Route {
119
+ constructor(regExp, handler, method){
166
120
  if (process.env.NODE_ENV !== "production") {
167
- if (effectiveRequest.method && effectiveRequest.method !== "GET") {
168
- throw new SerwistError("attempt-to-cache-non-get-request", {
169
- url: getFriendlyURL(effectiveRequest.url),
170
- method: effectiveRequest.method
171
- });
172
- }
173
- }
174
- if (!response) {
175
- if (process.env.NODE_ENV !== "production") {
176
- logger.error(`Cannot cache non-existent response for '${getFriendlyURL(effectiveRequest.url)}'.`);
177
- }
178
- throw new SerwistError("cache-put-with-no-response", {
179
- url: getFriendlyURL(effectiveRequest.url)
121
+ finalAssertExports.isInstance(regExp, RegExp, {
122
+ moduleName: "serwist",
123
+ className: "RegExpRoute",
124
+ funcName: "constructor",
125
+ paramName: "pattern"
180
126
  });
181
127
  }
182
- const responseToCache = await this._ensureResponseSafeToCache(response);
183
- if (!responseToCache) {
184
- if (process.env.NODE_ENV !== "production") {
185
- logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' will not be cached.`, responseToCache);
128
+ const match = ({ url })=>{
129
+ const result = regExp.exec(url.href);
130
+ if (!result) {
131
+ return;
186
132
  }
187
- return false;
188
- }
189
- const { cacheName, matchOptions } = this._strategy;
190
- const cache = await self.caches.open(cacheName);
191
- if (process.env.NODE_ENV !== "production") {
192
- const vary = response.headers.get("Vary");
193
- if (vary && matchOptions?.ignoreVary !== true) {
194
- 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.`);
195
- }
196
- }
197
- const hasCacheUpdateCallback = this.hasCallback("cacheDidUpdate");
198
- const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams(cache, effectiveRequest.clone(), [
199
- "__WB_REVISION__"
200
- ], matchOptions) : null;
201
- if (process.env.NODE_ENV !== "production") {
202
- logger.debug(`Updating the '${cacheName}' cache with a new Response for ${getFriendlyURL(effectiveRequest.url)}.`);
203
- }
204
- try {
205
- await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);
206
- } catch (error) {
207
- if (error instanceof Error) {
208
- if (error.name === "QuotaExceededError") {
209
- await executeQuotaErrorCallbacks();
133
+ if (url.origin !== location.origin && result.index !== 0) {
134
+ if (process.env.NODE_ENV !== "production") {
135
+ logger.debug(`The regular expression '${regExp.toString()}' only partially matched against the cross-origin URL '${url.toString()}'. RegExpRoute's will only handle cross-origin requests if they match the entire URL.`);
210
136
  }
211
- throw error;
137
+ return;
212
138
  }
213
- }
214
- for (const callback of this.iterateCallbacks("cacheDidUpdate")){
215
- await callback({
216
- cacheName,
217
- oldResponse,
218
- newResponse: responseToCache.clone(),
219
- request: effectiveRequest,
220
- event: this.event
221
- });
222
- }
223
- return true;
139
+ return result.slice(1);
140
+ };
141
+ super(match, handler, method);
224
142
  }
225
- async getCacheKey(request, mode) {
226
- const key = `${request.url} | ${mode}`;
227
- if (!this._cacheKeys[key]) {
228
- let effectiveRequest = request;
229
- for (const callback of this.iterateCallbacks("cacheKeyWillBeUsed")){
230
- effectiveRequest = toRequest(await callback({
231
- mode,
232
- request: effectiveRequest,
233
- event: this.event,
234
- params: this.params
235
- }));
236
- }
237
- this._cacheKeys[key] = effectiveRequest;
238
- }
239
- return this._cacheKeys[key];
143
+ }
144
+
145
+ const REVISION_SEARCH_PARAM = "__WB_REVISION__";
146
+ const createCacheKey = (entry)=>{
147
+ if (!entry) {
148
+ throw new SerwistError("add-to-cache-list-unexpected-type", {
149
+ entry
150
+ });
240
151
  }
241
- hasCallback(name) {
242
- for (const plugin of this._strategy.plugins){
243
- if (name in plugin) {
244
- return true;
245
- }
246
- }
247
- return false;
152
+ if (typeof entry === "string") {
153
+ const urlObject = new URL(entry, location.href);
154
+ return {
155
+ cacheKey: urlObject.href,
156
+ url: urlObject.href
157
+ };
248
158
  }
249
- async runCallbacks(name, param) {
250
- for (const callback of this.iterateCallbacks(name)){
251
- await callback(param);
252
- }
159
+ const { revision, url } = entry;
160
+ if (!url) {
161
+ throw new SerwistError("add-to-cache-list-unexpected-type", {
162
+ entry
163
+ });
253
164
  }
254
- *iterateCallbacks(name) {
255
- for (const plugin of this._strategy.plugins){
256
- if (typeof plugin[name] === "function") {
257
- const state = this._pluginStateMap.get(plugin);
258
- const statefulCallback = (param)=>{
259
- const statefulParam = {
260
- ...param,
261
- state
262
- };
263
- return plugin[name](statefulParam);
264
- };
265
- yield statefulCallback;
266
- }
267
- }
165
+ if (!revision) {
166
+ const urlObject = new URL(url, location.href);
167
+ return {
168
+ cacheKey: urlObject.href,
169
+ url: urlObject.href
170
+ };
268
171
  }
269
- waitUntil(promise) {
270
- this._extendLifetimePromises.push(promise);
271
- return promise;
172
+ const cacheKeyURL = new URL(url, location.href);
173
+ const originalURL = new URL(url, location.href);
174
+ cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision);
175
+ return {
176
+ cacheKey: cacheKeyURL.href,
177
+ url: originalURL.href
178
+ };
179
+ };
180
+
181
+ const logGroup = (groupTitle, deletedURLs)=>{
182
+ logger.groupCollapsed(groupTitle);
183
+ for (const url of deletedURLs){
184
+ logger.log(url);
272
185
  }
273
- async doneWaiting() {
274
- let promise = undefined;
275
- while(promise = this._extendLifetimePromises.shift()){
276
- await promise;
277
- }
186
+ logger.groupEnd();
187
+ };
188
+ const printCleanupDetails = (deletedURLs)=>{
189
+ const deletionCount = deletedURLs.length;
190
+ if (deletionCount > 0) {
191
+ logger.groupCollapsed(`During precaching cleanup, ${deletionCount} cached request${deletionCount === 1 ? " was" : "s were"} deleted.`);
192
+ logGroup("Deleted Cache Requests", deletedURLs);
193
+ logger.groupEnd();
278
194
  }
279
- destroy() {
280
- this._handlerDeferred.resolve(null);
195
+ };
196
+
197
+ function _nestedGroup(groupTitle, urls) {
198
+ if (urls.length === 0) {
199
+ return;
281
200
  }
282
- async getPreloadResponse() {
283
- if (this.event instanceof FetchEvent && this.event.request.mode === "navigate" && "preloadResponse" in this.event) {
284
- try {
285
- const possiblePreloadResponse = await this.event.preloadResponse;
286
- if (possiblePreloadResponse) {
287
- if (process.env.NODE_ENV !== "production") {
288
- logger.log(`Using a preloaded navigation response for '${getFriendlyURL(this.event.request.url)}'`);
289
- }
290
- return possiblePreloadResponse;
291
- }
292
- } catch (error) {
293
- if (process.env.NODE_ENV !== "production") {
294
- logger.error(error);
295
- }
296
- return undefined;
297
- }
201
+ logger.groupCollapsed(groupTitle);
202
+ for (const url of urls){
203
+ logger.log(url);
204
+ }
205
+ logger.groupEnd();
206
+ }
207
+ const printInstallDetails = (urlsToPrecache, urlsAlreadyPrecached)=>{
208
+ const precachedCount = urlsToPrecache.length;
209
+ const alreadyPrecachedCount = urlsAlreadyPrecached.length;
210
+ if (precachedCount || alreadyPrecachedCount) {
211
+ let message = `Precaching ${precachedCount} file${precachedCount === 1 ? "" : "s"}.`;
212
+ if (alreadyPrecachedCount > 0) {
213
+ message += ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? " is" : "s are"} already cached.`;
298
214
  }
299
- return undefined;
215
+ logger.groupCollapsed(message);
216
+ _nestedGroup("View newly precached URLs.", urlsToPrecache);
217
+ _nestedGroup("View previously precached URLs.", urlsAlreadyPrecached);
218
+ logger.groupEnd();
300
219
  }
301
- async _ensureResponseSafeToCache(response) {
302
- let responseToCache = response;
303
- let pluginsUsed = false;
304
- for (const callback of this.iterateCallbacks("cacheWillUpdate")){
305
- responseToCache = await callback({
306
- request: this.request,
307
- response: responseToCache,
308
- event: this.event
309
- }) || undefined;
310
- pluginsUsed = true;
311
- if (!responseToCache) {
312
- break;
313
- }
220
+ };
221
+
222
+ class PrecacheInstallReportPlugin {
223
+ updatedURLs = [];
224
+ notUpdatedURLs = [];
225
+ handlerWillStart = async ({ request, state })=>{
226
+ if (state) {
227
+ state.originalRequest = request;
314
228
  }
315
- if (!pluginsUsed) {
316
- if (responseToCache && responseToCache.status !== 200) {
317
- if (process.env.NODE_ENV !== "production") {
318
- if (responseToCache.status === 0) {
319
- 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.`);
320
- } else {
321
- logger.debug(`The response for '${this.request.url}' returned a status code of '${response.status}' and won't be cached as a result.`);
322
- }
229
+ };
230
+ cachedResponseWillBeUsed = async ({ event, state, cachedResponse })=>{
231
+ if (event.type === "install") {
232
+ if (state?.originalRequest && state.originalRequest instanceof Request) {
233
+ const url = state.originalRequest.url;
234
+ if (cachedResponse) {
235
+ this.notUpdatedURLs.push(url);
236
+ } else {
237
+ this.updatedURLs.push(url);
323
238
  }
324
- responseToCache = undefined;
325
239
  }
326
240
  }
327
- return responseToCache;
328
- }
241
+ return cachedResponse;
242
+ };
329
243
  }
330
244
 
331
- class Strategy {
332
- cacheName;
333
- plugins;
334
- fetchOptions;
335
- matchOptions;
336
- constructor(options = {}){
337
- this.cacheName = cacheNames$1.getRuntimeName(options.cacheName);
338
- this.plugins = options.plugins || [];
339
- this.fetchOptions = options.fetchOptions;
340
- this.matchOptions = options.matchOptions;
245
+ const removeIgnoredSearchParams = (urlObject, ignoreURLParametersMatching = [])=>{
246
+ for (const paramName of [
247
+ ...urlObject.searchParams.keys()
248
+ ]){
249
+ if (ignoreURLParametersMatching.some((regExp)=>regExp.test(paramName))) {
250
+ urlObject.searchParams.delete(paramName);
251
+ }
341
252
  }
342
- handle(options) {
343
- const [responseDone] = this.handleAll(options);
344
- return responseDone;
253
+ return urlObject;
254
+ };
255
+
256
+ function* generateURLVariations(url, { directoryIndex = "index.html", ignoreURLParametersMatching = [
257
+ /^utm_/,
258
+ /^fbclid$/
259
+ ], cleanURLs = true, urlManipulation } = {}) {
260
+ const urlObject = new URL(url, location.href);
261
+ urlObject.hash = "";
262
+ yield urlObject.href;
263
+ const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching);
264
+ yield urlWithoutIgnoredParams.href;
265
+ if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith("/")) {
266
+ const directoryURL = new URL(urlWithoutIgnoredParams.href);
267
+ directoryURL.pathname += directoryIndex;
268
+ yield directoryURL.href;
345
269
  }
346
- handleAll(options) {
347
- if (options instanceof FetchEvent) {
348
- options = {
349
- event: options,
350
- request: options.request
351
- };
352
- }
353
- const event = options.event;
354
- const request = typeof options.request === "string" ? new Request(options.request) : options.request;
355
- const handler = new StrategyHandler(this, options.url ? {
356
- event,
357
- request,
358
- url: options.url,
359
- params: options.params
360
- } : {
361
- event,
362
- request
363
- });
364
- const responseDone = this._getResponse(handler, request, event);
365
- const handlerDone = this._awaitComplete(responseDone, handler, request, event);
366
- return [
367
- responseDone,
368
- handlerDone
369
- ];
270
+ if (cleanURLs) {
271
+ const cleanURL = new URL(urlWithoutIgnoredParams.href);
272
+ cleanURL.pathname += ".html";
273
+ yield cleanURL.href;
370
274
  }
371
- async _getResponse(handler, request, event) {
372
- await handler.runCallbacks("handlerWillStart", {
373
- event,
374
- request
275
+ if (urlManipulation) {
276
+ const additionalURLs = urlManipulation({
277
+ url: urlObject
375
278
  });
376
- let response = undefined;
377
- try {
378
- response = await this._handle(request, handler);
379
- if (response === undefined || response.type === "error") {
380
- throw new SerwistError("no-response", {
381
- url: request.url
382
- });
383
- }
384
- } catch (error) {
385
- if (error instanceof Error) {
386
- for (const callback of handler.iterateCallbacks("handlerDidError")){
387
- response = await callback({
388
- error,
389
- event,
390
- request
391
- });
392
- if (response !== undefined) {
393
- break;
394
- }
279
+ for (const urlToAttempt of additionalURLs){
280
+ yield urlToAttempt.href;
281
+ }
282
+ }
283
+ }
284
+
285
+ class PrecacheRoute extends Route {
286
+ constructor(controller, options){
287
+ const match = ({ request })=>{
288
+ const urlsToCacheKeys = controller.getUrlsToPrecacheKeys();
289
+ for (const possibleURL of generateURLVariations(request.url, options)){
290
+ const cacheKey = urlsToCacheKeys.get(possibleURL);
291
+ if (cacheKey) {
292
+ const integrity = controller.getIntegrityForPrecacheKey(cacheKey);
293
+ return {
294
+ cacheKey,
295
+ integrity
296
+ };
395
297
  }
396
298
  }
397
- if (!response) {
398
- throw error;
399
- }
400
299
  if (process.env.NODE_ENV !== "production") {
401
- 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.`);
300
+ logger.debug(`Precaching did not find a match for ${getFriendlyURL(request.url)}.`);
402
301
  }
403
- }
404
- for (const callback of handler.iterateCallbacks("handlerWillRespond")){
405
- response = await callback({
406
- event,
407
- request,
408
- response
409
- });
410
- }
411
- return response;
302
+ return;
303
+ };
304
+ super(match, controller.strategy);
412
305
  }
413
- async _awaitComplete(responseDone, handler, request, event) {
414
- let response = undefined;
415
- let error = undefined;
416
- try {
417
- response = await responseDone;
418
- } catch (error) {}
419
- try {
420
- await handler.runCallbacks("handlerDidRespond", {
421
- event,
422
- request,
423
- response
424
- });
425
- await handler.doneWaiting();
426
- } catch (waitUntilError) {
427
- if (waitUntilError instanceof Error) {
428
- error = waitUntilError;
429
- }
430
- }
431
- await handler.runCallbacks("handlerDidComplete", {
432
- event,
433
- request,
434
- response,
435
- error
306
+ }
307
+
308
+ const copyResponse = async (response, modifier)=>{
309
+ let origin = null;
310
+ if (response.url) {
311
+ const responseURL = new URL(response.url);
312
+ origin = responseURL.origin;
313
+ }
314
+ if (origin !== self.location.origin) {
315
+ throw new SerwistError("cross-origin-copy-response", {
316
+ origin
436
317
  });
437
- handler.destroy();
438
- if (error) {
439
- throw error;
440
- }
441
318
  }
442
- }
319
+ const clonedResponse = response.clone();
320
+ const responseInit = {
321
+ headers: new Headers(clonedResponse.headers),
322
+ status: clonedResponse.status,
323
+ statusText: clonedResponse.statusText
324
+ };
325
+ const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit;
326
+ const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob();
327
+ return new Response(body, modifiedResponseInit);
328
+ };
443
329
 
444
- class PrecacheStrategy extends Strategy {
445
- _fallbackToNetwork;
446
- static defaultPrecacheCacheabilityPlugin = {
447
- async cacheWillUpdate ({ response }) {
448
- if (!response || response.status >= 400) {
449
- return null;
450
- }
451
- return response;
330
+ function toRequest(input) {
331
+ return typeof input === "string" ? new Request(input) : input;
332
+ }
333
+ class StrategyHandler {
334
+ event;
335
+ request;
336
+ url;
337
+ params;
338
+ _cacheKeys = {};
339
+ _strategy;
340
+ _handlerDeferred;
341
+ _extendLifetimePromises;
342
+ _plugins;
343
+ _pluginStateMap;
344
+ constructor(strategy, options){
345
+ if (process.env.NODE_ENV !== "production") {
346
+ finalAssertExports.isInstance(options.event, ExtendableEvent, {
347
+ moduleName: "serwist",
348
+ className: "StrategyHandler",
349
+ funcName: "constructor",
350
+ paramName: "options.event"
351
+ });
352
+ finalAssertExports.isInstance(options.request, Request, {
353
+ moduleName: "serwist",
354
+ className: "StrategyHandler",
355
+ funcName: "constructor",
356
+ paramName: "options.request"
357
+ });
452
358
  }
453
- };
454
- static copyRedirectedCacheableResponsesPlugin = {
455
- async cacheWillUpdate ({ response }) {
456
- return response.redirected ? await copyResponse(response) : response;
359
+ this.event = options.event;
360
+ this.request = options.request;
361
+ if (options.url) {
362
+ this.url = options.url;
363
+ this.params = options.params;
457
364
  }
458
- };
459
- constructor(options = {}){
460
- options.cacheName = cacheNames$1.getPrecacheName(options.cacheName);
461
- super(options);
462
- this._fallbackToNetwork = options.fallbackToNetwork !== false;
463
- this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin);
365
+ this._strategy = strategy;
366
+ this._handlerDeferred = new Deferred();
367
+ this._extendLifetimePromises = [];
368
+ this._plugins = [
369
+ ...strategy.plugins
370
+ ];
371
+ this._pluginStateMap = new Map();
372
+ for (const plugin of this._plugins){
373
+ this._pluginStateMap.set(plugin, {});
374
+ }
375
+ this.event.waitUntil(this._handlerDeferred.promise);
464
376
  }
465
- async _handle(request, handler) {
466
- const preloadResponse = await handler.getPreloadResponse();
377
+ async fetch(input) {
378
+ const { event } = this;
379
+ let request = toRequest(input);
380
+ const preloadResponse = await this.getPreloadResponse();
467
381
  if (preloadResponse) {
468
382
  return preloadResponse;
469
383
  }
470
- const response = await handler.cacheMatch(request);
471
- if (response) {
472
- return response;
473
- }
474
- if (handler.event && handler.event.type === "install") {
475
- return await this._handleInstall(request, handler);
384
+ const originalRequest = this.hasCallback("fetchDidFail") ? request.clone() : null;
385
+ try {
386
+ for (const cb of this.iterateCallbacks("requestWillFetch")){
387
+ request = await cb({
388
+ request: request.clone(),
389
+ event
390
+ });
391
+ }
392
+ } catch (err) {
393
+ if (err instanceof Error) {
394
+ throw new SerwistError("plugin-error-request-will-fetch", {
395
+ thrownErrorMessage: err.message
396
+ });
397
+ }
476
398
  }
477
- return await this._handleFetch(request, handler);
478
- }
479
- async _handleFetch(request, handler) {
480
- let response = undefined;
481
- const params = handler.params || {};
482
- if (this._fallbackToNetwork) {
399
+ const pluginFilteredRequest = request.clone();
400
+ try {
401
+ let fetchResponse;
402
+ fetchResponse = await fetch(request, request.mode === "navigate" ? undefined : this._strategy.fetchOptions);
483
403
  if (process.env.NODE_ENV !== "production") {
484
- logger.warn(`The precached response for ${getFriendlyURL(request.url)} in ${this.cacheName} was not found. Falling back to the network.`);
404
+ logger.debug(`Network request for '${getFriendlyURL(request.url)}' returned a response with status '${fetchResponse.status}'.`);
485
405
  }
486
- const integrityInManifest = params.integrity;
487
- const integrityInRequest = request.integrity;
488
- const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest;
489
- response = await handler.fetch(new Request(request, {
490
- integrity: request.mode !== "no-cors" ? integrityInRequest || integrityInManifest : undefined
491
- }));
492
- if (integrityInManifest && noIntegrityConflict && request.mode !== "no-cors") {
493
- this._useDefaultCacheabilityPluginIfNeeded();
494
- const wasCached = await handler.cachePut(request, response.clone());
495
- if (process.env.NODE_ENV !== "production") {
496
- if (wasCached) {
497
- logger.log(`A response for ${getFriendlyURL(request.url)} was used to "repair" the precache.`);
498
- }
499
- }
406
+ for (const callback of this.iterateCallbacks("fetchDidSucceed")){
407
+ fetchResponse = await callback({
408
+ event,
409
+ request: pluginFilteredRequest,
410
+ response: fetchResponse
411
+ });
500
412
  }
501
- } else {
502
- throw new SerwistError("missing-precache-entry", {
503
- cacheName: this.cacheName,
504
- url: request.url
505
- });
506
- }
507
- if (process.env.NODE_ENV !== "production") {
508
- const cacheKey = params.cacheKey || await handler.getCacheKey(request, "read");
509
- logger.groupCollapsed(`Precaching is responding to: ${getFriendlyURL(request.url)}`);
510
- logger.log(`Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}`);
511
- logger.groupCollapsed("View request details here.");
512
- logger.log(request);
513
- logger.groupEnd();
514
- logger.groupCollapsed("View response details here.");
515
- logger.log(response);
516
- logger.groupEnd();
517
- logger.groupEnd();
413
+ return fetchResponse;
414
+ } catch (error) {
415
+ if (process.env.NODE_ENV !== "production") {
416
+ logger.log(`Network request for '${getFriendlyURL(request.url)}' threw an error.`, error);
417
+ }
418
+ if (originalRequest) {
419
+ await this.runCallbacks("fetchDidFail", {
420
+ error: error,
421
+ event,
422
+ originalRequest: originalRequest.clone(),
423
+ request: pluginFilteredRequest.clone()
424
+ });
425
+ }
426
+ throw error;
518
427
  }
519
- return response;
520
428
  }
521
- async _handleInstall(request, handler) {
522
- this._useDefaultCacheabilityPluginIfNeeded();
523
- const response = await handler.fetch(request);
524
- const wasCached = await handler.cachePut(request, response.clone());
525
- if (!wasCached) {
526
- throw new SerwistError("bad-precaching-response", {
527
- url: request.url,
528
- status: response.status
529
- });
530
- }
429
+ async fetchAndCachePut(input) {
430
+ const response = await this.fetch(input);
431
+ const responseClone = response.clone();
432
+ void this.waitUntil(this.cachePut(input, responseClone));
531
433
  return response;
532
434
  }
533
- _useDefaultCacheabilityPluginIfNeeded() {
534
- let defaultPluginIndex = null;
535
- let cacheWillUpdatePluginCount = 0;
536
- for (const [index, plugin] of this.plugins.entries()){
537
- if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) {
538
- continue;
539
- }
540
- if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) {
541
- defaultPluginIndex = index;
435
+ async cacheMatch(key) {
436
+ const request = toRequest(key);
437
+ let cachedResponse;
438
+ const { cacheName, matchOptions } = this._strategy;
439
+ const effectiveRequest = await this.getCacheKey(request, "read");
440
+ const multiMatchOptions = {
441
+ ...matchOptions,
442
+ ...{
443
+ cacheName
542
444
  }
543
- if (plugin.cacheWillUpdate) {
544
- cacheWillUpdatePluginCount++;
445
+ };
446
+ cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);
447
+ if (process.env.NODE_ENV !== "production") {
448
+ if (cachedResponse) {
449
+ logger.debug(`Found a cached response in '${cacheName}'.`);
450
+ } else {
451
+ logger.debug(`No cached response found in '${cacheName}'.`);
545
452
  }
546
453
  }
547
- if (cacheWillUpdatePluginCount === 0) {
548
- this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin);
549
- } else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) {
550
- this.plugins.splice(defaultPluginIndex, 1);
454
+ for (const callback of this.iterateCallbacks("cachedResponseWillBeUsed")){
455
+ cachedResponse = await callback({
456
+ cacheName,
457
+ matchOptions,
458
+ cachedResponse,
459
+ request: effectiveRequest,
460
+ event: this.event
461
+ }) || undefined;
551
462
  }
463
+ return cachedResponse;
552
464
  }
553
- }
554
-
555
- const defaultMethod = "GET";
556
- const validMethods = [
557
- "DELETE",
558
- "GET",
559
- "HEAD",
560
- "PATCH",
561
- "POST",
562
- "PUT"
563
- ];
564
-
565
- const normalizeHandler = (handler)=>{
566
- if (handler && typeof handler === "object") {
465
+ async cachePut(key, response) {
466
+ const request = toRequest(key);
467
+ await timeout(0);
468
+ const effectiveRequest = await this.getCacheKey(request, "write");
567
469
  if (process.env.NODE_ENV !== "production") {
568
- finalAssertExports.hasMethod(handler, "handle", {
569
- moduleName: "serwist",
570
- className: "Route",
571
- funcName: "constructor",
572
- paramName: "handler"
470
+ if (effectiveRequest.method && effectiveRequest.method !== "GET") {
471
+ throw new SerwistError("attempt-to-cache-non-get-request", {
472
+ url: getFriendlyURL(effectiveRequest.url),
473
+ method: effectiveRequest.method
474
+ });
475
+ }
476
+ }
477
+ if (!response) {
478
+ if (process.env.NODE_ENV !== "production") {
479
+ logger.error(`Cannot cache non-existent response for '${getFriendlyURL(effectiveRequest.url)}'.`);
480
+ }
481
+ throw new SerwistError("cache-put-with-no-response", {
482
+ url: getFriendlyURL(effectiveRequest.url)
573
483
  });
574
484
  }
575
- return handler;
576
- }
577
- if (process.env.NODE_ENV !== "production") {
578
- finalAssertExports.isType(handler, "function", {
579
- moduleName: "serwist",
580
- className: "Route",
581
- funcName: "constructor",
582
- paramName: "handler"
583
- });
584
- }
585
- return {
586
- handle: handler
587
- };
588
- };
589
-
590
- class Route {
591
- handler;
592
- match;
593
- method;
594
- catchHandler;
595
- constructor(match, handler, method = defaultMethod){
485
+ const responseToCache = await this._ensureResponseSafeToCache(response);
486
+ if (!responseToCache) {
487
+ if (process.env.NODE_ENV !== "production") {
488
+ logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' will not be cached.`, responseToCache);
489
+ }
490
+ return false;
491
+ }
492
+ const { cacheName, matchOptions } = this._strategy;
493
+ const cache = await self.caches.open(cacheName);
596
494
  if (process.env.NODE_ENV !== "production") {
597
- finalAssertExports.isType(match, "function", {
598
- moduleName: "serwist",
599
- className: "Route",
600
- funcName: "constructor",
601
- paramName: "match"
602
- });
603
- if (method) {
604
- finalAssertExports.isOneOf(method, validMethods, {
605
- paramName: "method"
606
- });
495
+ const vary = response.headers.get("Vary");
496
+ if (vary && matchOptions?.ignoreVary !== true) {
497
+ 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.`);
607
498
  }
608
499
  }
609
- this.handler = normalizeHandler(handler);
610
- this.match = match;
611
- this.method = method;
612
- }
613
- setCatchHandler(handler) {
614
- this.catchHandler = normalizeHandler(handler);
615
- }
616
- }
617
-
618
- const removeIgnoredSearchParams = (urlObject, ignoreURLParametersMatching = [])=>{
619
- for (const paramName of [
620
- ...urlObject.searchParams.keys()
621
- ]){
622
- if (ignoreURLParametersMatching.some((regExp)=>regExp.test(paramName))) {
623
- urlObject.searchParams.delete(paramName);
500
+ const hasCacheUpdateCallback = this.hasCallback("cacheDidUpdate");
501
+ const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams(cache, effectiveRequest.clone(), [
502
+ "__WB_REVISION__"
503
+ ], matchOptions) : null;
504
+ if (process.env.NODE_ENV !== "production") {
505
+ logger.debug(`Updating the '${cacheName}' cache with a new Response for ${getFriendlyURL(effectiveRequest.url)}.`);
506
+ }
507
+ try {
508
+ await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);
509
+ } catch (error) {
510
+ if (error instanceof Error) {
511
+ if (error.name === "QuotaExceededError") {
512
+ await executeQuotaErrorCallbacks();
513
+ }
514
+ throw error;
515
+ }
516
+ }
517
+ for (const callback of this.iterateCallbacks("cacheDidUpdate")){
518
+ await callback({
519
+ cacheName,
520
+ oldResponse,
521
+ newResponse: responseToCache.clone(),
522
+ request: effectiveRequest,
523
+ event: this.event
524
+ });
624
525
  }
526
+ return true;
625
527
  }
626
- return urlObject;
627
- };
628
-
629
- function* generateURLVariations(url, { directoryIndex = "index.html", ignoreURLParametersMatching = [
630
- /^utm_/,
631
- /^fbclid$/
632
- ], cleanURLs = true, urlManipulation } = {}) {
633
- const urlObject = new URL(url, location.href);
634
- urlObject.hash = "";
635
- yield urlObject.href;
636
- const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching);
637
- yield urlWithoutIgnoredParams.href;
638
- if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith("/")) {
639
- const directoryURL = new URL(urlWithoutIgnoredParams.href);
640
- directoryURL.pathname += directoryIndex;
641
- yield directoryURL.href;
528
+ async getCacheKey(request, mode) {
529
+ const key = `${request.url} | ${mode}`;
530
+ if (!this._cacheKeys[key]) {
531
+ let effectiveRequest = request;
532
+ for (const callback of this.iterateCallbacks("cacheKeyWillBeUsed")){
533
+ effectiveRequest = toRequest(await callback({
534
+ mode,
535
+ request: effectiveRequest,
536
+ event: this.event,
537
+ params: this.params
538
+ }));
539
+ }
540
+ this._cacheKeys[key] = effectiveRequest;
541
+ }
542
+ return this._cacheKeys[key];
642
543
  }
643
- if (cleanURLs) {
644
- const cleanURL = new URL(urlWithoutIgnoredParams.href);
645
- cleanURL.pathname += ".html";
646
- yield cleanURL.href;
544
+ hasCallback(name) {
545
+ for (const plugin of this._strategy.plugins){
546
+ if (name in plugin) {
547
+ return true;
548
+ }
549
+ }
550
+ return false;
647
551
  }
648
- if (urlManipulation) {
649
- const additionalURLs = urlManipulation({
650
- url: urlObject
651
- });
652
- for (const urlToAttempt of additionalURLs){
653
- yield urlToAttempt.href;
552
+ async runCallbacks(name, param) {
553
+ for (const callback of this.iterateCallbacks(name)){
554
+ await callback(param);
654
555
  }
655
556
  }
656
- }
657
-
658
- class PrecacheRoute extends Route {
659
- constructor(controller, options){
660
- const match = ({ request })=>{
661
- const urlsToCacheKeys = controller.getUrlsToPrecacheKeys();
662
- for (const possibleURL of generateURLVariations(request.url, options)){
663
- const cacheKey = urlsToCacheKeys.get(possibleURL);
664
- if (cacheKey) {
665
- const integrity = controller.getIntegrityForPrecacheKey(cacheKey);
666
- return {
667
- cacheKey,
668
- integrity
557
+ *iterateCallbacks(name) {
558
+ for (const plugin of this._strategy.plugins){
559
+ if (typeof plugin[name] === "function") {
560
+ const state = this._pluginStateMap.get(plugin);
561
+ const statefulCallback = (param)=>{
562
+ const statefulParam = {
563
+ ...param,
564
+ state
669
565
  };
670
- }
671
- }
672
- if (process.env.NODE_ENV !== "production") {
673
- logger.debug(`Precaching did not find a match for ${getFriendlyURL(request.url)}.`);
566
+ return plugin[name](statefulParam);
567
+ };
568
+ yield statefulCallback;
674
569
  }
675
- return;
676
- };
677
- super(match, controller.strategy);
570
+ }
678
571
  }
679
- }
680
-
681
- class PrecacheFallbackPlugin {
682
- _fallbackUrls;
683
- _precacheController;
684
- constructor({ fallbackUrls, precacheController }){
685
- this._fallbackUrls = fallbackUrls;
686
- this._precacheController = precacheController;
572
+ waitUntil(promise) {
573
+ this._extendLifetimePromises.push(promise);
574
+ return promise;
687
575
  }
688
- async handlerDidError(param) {
689
- for (const fallback of this._fallbackUrls){
690
- if (typeof fallback === "string") {
691
- const fallbackResponse = await this._precacheController.matchPrecache(fallback);
692
- if (fallbackResponse !== undefined) {
693
- return fallbackResponse;
576
+ async doneWaiting() {
577
+ let promise = undefined;
578
+ while(promise = this._extendLifetimePromises.shift()){
579
+ await promise;
580
+ }
581
+ }
582
+ destroy() {
583
+ this._handlerDeferred.resolve(null);
584
+ }
585
+ async getPreloadResponse() {
586
+ if (this.event instanceof FetchEvent && this.event.request.mode === "navigate" && "preloadResponse" in this.event) {
587
+ try {
588
+ const possiblePreloadResponse = await this.event.preloadResponse;
589
+ if (possiblePreloadResponse) {
590
+ if (process.env.NODE_ENV !== "production") {
591
+ logger.log(`Using a preloaded navigation response for '${getFriendlyURL(this.event.request.url)}'`);
592
+ }
593
+ return possiblePreloadResponse;
694
594
  }
695
- } else if (fallback.matcher(param)) {
696
- const fallbackResponse = await this._precacheController.matchPrecache(fallback.url);
697
- if (fallbackResponse !== undefined) {
698
- return fallbackResponse;
595
+ } catch (error) {
596
+ if (process.env.NODE_ENV !== "production") {
597
+ logger.error(error);
699
598
  }
599
+ return undefined;
700
600
  }
701
601
  }
702
602
  return undefined;
703
603
  }
704
- }
705
-
706
- class RuntimeCacheController {
707
- _entries;
708
- _options;
709
- constructor(entries, options = {}){
710
- this._entries = entries;
711
- this._options = options;
712
- this.init = this.init.bind(this);
713
- this.install = this.install.bind(this);
714
- }
715
- init(serwist) {
716
- if (this._options.fallbacks !== undefined) {
717
- const fallbackPlugin = new PrecacheFallbackPlugin({
718
- fallbackUrls: this._options.fallbacks.entries,
719
- precacheController: serwist.precache
720
- });
721
- this._entries.forEach((cacheEntry)=>{
722
- if (cacheEntry.handler instanceof Strategy && !cacheEntry.handler.plugins.some((plugin)=>"handlerDidError" in plugin)) {
723
- cacheEntry.handler.plugins.push(fallbackPlugin);
724
- }
725
- });
726
- }
727
- for (const entry of this._entries){
728
- serwist.registerCapture(entry.matcher, entry.handler, entry.method);
604
+ async _ensureResponseSafeToCache(response) {
605
+ let responseToCache = response;
606
+ let pluginsUsed = false;
607
+ for (const callback of this.iterateCallbacks("cacheWillUpdate")){
608
+ responseToCache = await callback({
609
+ request: this.request,
610
+ response: responseToCache,
611
+ event: this.event
612
+ }) || undefined;
613
+ pluginsUsed = true;
614
+ if (!responseToCache) {
615
+ break;
616
+ }
729
617
  }
730
- }
731
- async install(event, serwist) {
732
- const concurrency = this._options.warmOptions?.concurrency ?? 10;
733
- if (this._options.warmEntries) {
734
- await parallel(concurrency, this._options.warmEntries, async (entry)=>{
735
- const request = entry instanceof Request ? entry : new Request(typeof entry === "string" ? entry : entry.url, {
736
- integrity: typeof entry !== "string" ? entry.integrity : undefined,
737
- credentials: "same-origin"
738
- });
739
- await serwist.handleRequest({
740
- request,
741
- event
742
- });
743
- });
618
+ if (!pluginsUsed) {
619
+ if (responseToCache && responseToCache.status !== 200) {
620
+ if (process.env.NODE_ENV !== "production") {
621
+ if (responseToCache.status === 0) {
622
+ 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.`);
623
+ } else {
624
+ logger.debug(`The response for '${this.request.url}' returned a status code of '${response.status}' and won't be cached as a result.`);
625
+ }
626
+ }
627
+ responseToCache = undefined;
628
+ }
744
629
  }
745
- }
746
- warmRuntimeCache(entries) {
747
- if (!this._options.warmEntries) this._options.warmEntries = [];
748
- this._options.warmEntries.push(...entries);
630
+ return responseToCache;
749
631
  }
750
632
  }
751
633
 
752
- class NavigationRoute extends Route {
753
- _allowlist;
754
- _denylist;
755
- constructor(handler, { allowlist = [
756
- /./
757
- ], denylist = [] } = {}){
758
- if (process.env.NODE_ENV !== "production") {
759
- finalAssertExports.isArrayOfClass(allowlist, RegExp, {
760
- moduleName: "serwist",
761
- className: "NavigationRoute",
762
- funcName: "constructor",
763
- paramName: "options.allowlist"
764
- });
765
- finalAssertExports.isArrayOfClass(denylist, RegExp, {
766
- moduleName: "serwist",
767
- className: "NavigationRoute",
768
- funcName: "constructor",
769
- paramName: "options.denylist"
770
- });
771
- }
772
- super((options)=>this._match(options), handler);
773
- this._allowlist = allowlist;
774
- this._denylist = denylist;
634
+ class Strategy {
635
+ cacheName;
636
+ plugins;
637
+ fetchOptions;
638
+ matchOptions;
639
+ constructor(options = {}){
640
+ this.cacheName = cacheNames$1.getRuntimeName(options.cacheName);
641
+ this.plugins = options.plugins || [];
642
+ this.fetchOptions = options.fetchOptions;
643
+ this.matchOptions = options.matchOptions;
775
644
  }
776
- _match({ url, request }) {
777
- if (request && request.mode !== "navigate") {
778
- return false;
645
+ handle(options) {
646
+ const [responseDone] = this.handleAll(options);
647
+ return responseDone;
648
+ }
649
+ handleAll(options) {
650
+ if (options instanceof FetchEvent) {
651
+ options = {
652
+ event: options,
653
+ request: options.request
654
+ };
779
655
  }
780
- const pathnameAndSearch = url.pathname + url.search;
781
- for (const regExp of this._denylist){
782
- if (regExp.test(pathnameAndSearch)) {
783
- if (process.env.NODE_ENV !== "production") {
784
- logger.log(`The navigation route ${pathnameAndSearch} is not being used, since the URL matches this denylist pattern: ${regExp.toString()}`);
656
+ const event = options.event;
657
+ const request = typeof options.request === "string" ? new Request(options.request) : options.request;
658
+ const handler = new StrategyHandler(this, options.url ? {
659
+ event,
660
+ request,
661
+ url: options.url,
662
+ params: options.params
663
+ } : {
664
+ event,
665
+ request
666
+ });
667
+ const responseDone = this._getResponse(handler, request, event);
668
+ const handlerDone = this._awaitComplete(responseDone, handler, request, event);
669
+ return [
670
+ responseDone,
671
+ handlerDone
672
+ ];
673
+ }
674
+ async _getResponse(handler, request, event) {
675
+ await handler.runCallbacks("handlerWillStart", {
676
+ event,
677
+ request
678
+ });
679
+ let response = undefined;
680
+ try {
681
+ response = await this._handle(request, handler);
682
+ if (response === undefined || response.type === "error") {
683
+ throw new SerwistError("no-response", {
684
+ url: request.url
685
+ });
686
+ }
687
+ } catch (error) {
688
+ if (error instanceof Error) {
689
+ for (const callback of handler.iterateCallbacks("handlerDidError")){
690
+ response = await callback({
691
+ error,
692
+ event,
693
+ request
694
+ });
695
+ if (response !== undefined) {
696
+ break;
697
+ }
785
698
  }
786
- return false;
787
699
  }
788
- }
789
- if (this._allowlist.some((regExp)=>regExp.test(pathnameAndSearch))) {
700
+ if (!response) {
701
+ throw error;
702
+ }
790
703
  if (process.env.NODE_ENV !== "production") {
791
- logger.debug(`The navigation route ${pathnameAndSearch} is being used.`);
704
+ 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.`);
792
705
  }
793
- return true;
794
706
  }
795
- if (process.env.NODE_ENV !== "production") {
796
- logger.log(`The navigation route ${pathnameAndSearch} is not being used, since the URL being navigated to doesn't match the allowlist.`);
707
+ for (const callback of handler.iterateCallbacks("handlerWillRespond")){
708
+ response = await callback({
709
+ event,
710
+ request,
711
+ response
712
+ });
797
713
  }
798
- return false;
714
+ return response;
799
715
  }
800
- }
801
-
802
- class RegExpRoute extends Route {
803
- constructor(regExp, handler, method){
804
- if (process.env.NODE_ENV !== "production") {
805
- finalAssertExports.isInstance(regExp, RegExp, {
806
- moduleName: "serwist",
807
- className: "RegExpRoute",
808
- funcName: "constructor",
809
- paramName: "pattern"
716
+ async _awaitComplete(responseDone, handler, request, event) {
717
+ let response = undefined;
718
+ let error = undefined;
719
+ try {
720
+ response = await responseDone;
721
+ } catch (error) {}
722
+ try {
723
+ await handler.runCallbacks("handlerDidRespond", {
724
+ event,
725
+ request,
726
+ response
810
727
  });
811
- }
812
- const match = ({ url })=>{
813
- const result = regExp.exec(url.href);
814
- if (!result) {
815
- return;
816
- }
817
- if (url.origin !== location.origin && result.index !== 0) {
818
- if (process.env.NODE_ENV !== "production") {
819
- logger.debug(`The regular expression '${regExp.toString()}' only partially matched against the cross-origin URL '${url.toString()}'. RegExpRoute's will only handle cross-origin requests if they match the entire URL.`);
820
- }
821
- return;
728
+ await handler.doneWaiting();
729
+ } catch (waitUntilError) {
730
+ if (waitUntilError instanceof Error) {
731
+ error = waitUntilError;
822
732
  }
823
- return result.slice(1);
824
- };
825
- super(match, handler, method);
733
+ }
734
+ await handler.runCallbacks("handlerDidComplete", {
735
+ event,
736
+ request,
737
+ response,
738
+ error
739
+ });
740
+ handler.destroy();
741
+ if (error) {
742
+ throw error;
743
+ }
826
744
  }
827
745
  }
828
746
 
829
- const disableDevLogs = ()=>{
830
- self.__WB_DISABLE_DEV_LOGS = true;
831
- };
832
-
833
- const cacheOkAndOpaquePlugin = {
834
- cacheWillUpdate: async ({ response })=>{
835
- if (response.status === 200 || response.status === 0) {
747
+ class PrecacheStrategy extends Strategy {
748
+ _fallbackToNetwork;
749
+ static defaultPrecacheCacheabilityPlugin = {
750
+ async cacheWillUpdate ({ response }) {
751
+ if (!response || response.status >= 400) {
752
+ return null;
753
+ }
836
754
  return response;
837
755
  }
838
- return null;
839
- }
840
- };
841
-
842
- const messages = {
843
- strategyStart: (strategyName, request)=>`Using ${strategyName} to respond to '${getFriendlyURL(request.url)}'`,
844
- printFinalResponse: (response)=>{
845
- if (response) {
846
- logger.groupCollapsed("View the final response here.");
847
- logger.log(response || "[No response returned]");
848
- logger.groupEnd();
756
+ };
757
+ static copyRedirectedCacheableResponsesPlugin = {
758
+ async cacheWillUpdate ({ response }) {
759
+ return response.redirected ? await copyResponse(response) : response;
849
760
  }
850
- }
851
- };
852
-
853
- class NetworkFirst extends Strategy {
854
- _networkTimeoutSeconds;
761
+ };
855
762
  constructor(options = {}){
763
+ options.cacheName = cacheNames$1.getPrecacheName(options.cacheName);
856
764
  super(options);
857
- if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
858
- this.plugins.unshift(cacheOkAndOpaquePlugin);
859
- }
860
- this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
861
- if (process.env.NODE_ENV !== "production") {
862
- if (this._networkTimeoutSeconds) {
863
- finalAssertExports.isType(this._networkTimeoutSeconds, "number", {
864
- moduleName: "serwist",
865
- className: this.constructor.name,
866
- funcName: "constructor",
867
- paramName: "networkTimeoutSeconds"
868
- });
869
- }
870
- }
765
+ this._fallbackToNetwork = options.fallbackToNetwork !== false;
766
+ this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin);
871
767
  }
872
768
  async _handle(request, handler) {
873
- const logs = [];
874
- if (process.env.NODE_ENV !== "production") {
875
- finalAssertExports.isInstance(request, Request, {
876
- moduleName: "serwist",
877
- className: this.constructor.name,
878
- funcName: "handle",
879
- paramName: "makeRequest"
880
- });
769
+ const preloadResponse = await handler.getPreloadResponse();
770
+ if (preloadResponse) {
771
+ return preloadResponse;
881
772
  }
882
- const promises = [];
883
- let timeoutId;
884
- if (this._networkTimeoutSeconds) {
885
- const { id, promise } = this._getTimeoutPromise({
886
- request,
887
- logs,
888
- handler
773
+ const response = await handler.cacheMatch(request);
774
+ if (response) {
775
+ return response;
776
+ }
777
+ if (handler.event && handler.event.type === "install") {
778
+ return await this._handleInstall(request, handler);
779
+ }
780
+ return await this._handleFetch(request, handler);
781
+ }
782
+ async _handleFetch(request, handler) {
783
+ let response = undefined;
784
+ const params = handler.params || {};
785
+ if (this._fallbackToNetwork) {
786
+ if (process.env.NODE_ENV !== "production") {
787
+ logger.warn(`The precached response for ${getFriendlyURL(request.url)} in ${this.cacheName} was not found. Falling back to the network.`);
788
+ }
789
+ const integrityInManifest = params.integrity;
790
+ const integrityInRequest = request.integrity;
791
+ const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest;
792
+ response = await handler.fetch(new Request(request, {
793
+ integrity: request.mode !== "no-cors" ? integrityInRequest || integrityInManifest : undefined
794
+ }));
795
+ if (integrityInManifest && noIntegrityConflict && request.mode !== "no-cors") {
796
+ this._useDefaultCacheabilityPluginIfNeeded();
797
+ const wasCached = await handler.cachePut(request, response.clone());
798
+ if (process.env.NODE_ENV !== "production") {
799
+ if (wasCached) {
800
+ logger.log(`A response for ${getFriendlyURL(request.url)} was used to "repair" the precache.`);
801
+ }
802
+ }
803
+ }
804
+ } else {
805
+ throw new SerwistError("missing-precache-entry", {
806
+ cacheName: this.cacheName,
807
+ url: request.url
889
808
  });
890
- timeoutId = id;
891
- promises.push(promise);
892
809
  }
893
- const networkPromise = this._getNetworkPromise({
894
- timeoutId,
895
- request,
896
- logs,
897
- handler
898
- });
899
- promises.push(networkPromise);
900
- const response = await handler.waitUntil((async ()=>{
901
- return await handler.waitUntil(Promise.race(promises)) || await networkPromise;
902
- })());
903
810
  if (process.env.NODE_ENV !== "production") {
904
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
905
- for (const log of logs){
906
- logger.log(log);
907
- }
908
- messages.printFinalResponse(response);
811
+ const cacheKey = params.cacheKey || await handler.getCacheKey(request, "read");
812
+ logger.groupCollapsed(`Precaching is responding to: ${getFriendlyURL(request.url)}`);
813
+ logger.log(`Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}`);
814
+ logger.groupCollapsed("View request details here.");
815
+ logger.log(request);
816
+ logger.groupEnd();
817
+ logger.groupCollapsed("View response details here.");
818
+ logger.log(response);
819
+ logger.groupEnd();
909
820
  logger.groupEnd();
910
821
  }
911
- if (!response) {
912
- throw new SerwistError("no-response", {
913
- url: request.url
822
+ return response;
823
+ }
824
+ async _handleInstall(request, handler) {
825
+ this._useDefaultCacheabilityPluginIfNeeded();
826
+ const response = await handler.fetch(request);
827
+ const wasCached = await handler.cachePut(request, response.clone());
828
+ if (!wasCached) {
829
+ throw new SerwistError("bad-precaching-response", {
830
+ url: request.url,
831
+ status: response.status
914
832
  });
915
833
  }
916
834
  return response;
917
835
  }
918
- _getTimeoutPromise({ request, logs, handler }) {
919
- let timeoutId;
920
- const timeoutPromise = new Promise((resolve)=>{
921
- const onNetworkTimeout = async ()=>{
922
- if (process.env.NODE_ENV !== "production") {
923
- logs.push(`Timing out the network response at ${this._networkTimeoutSeconds} seconds.`);
924
- }
925
- resolve(await handler.cacheMatch(request));
926
- };
927
- timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000);
928
- });
929
- return {
930
- promise: timeoutPromise,
931
- id: timeoutId
932
- };
933
- }
934
- async _getNetworkPromise({ timeoutId, request, logs, handler }) {
935
- let error = undefined;
936
- let response = undefined;
937
- try {
938
- response = await handler.fetchAndCachePut(request);
939
- } catch (fetchError) {
940
- if (fetchError instanceof Error) {
941
- error = fetchError;
836
+ _useDefaultCacheabilityPluginIfNeeded() {
837
+ let defaultPluginIndex = null;
838
+ let cacheWillUpdatePluginCount = 0;
839
+ for (const [index, plugin] of this.plugins.entries()){
840
+ if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) {
841
+ continue;
942
842
  }
943
- }
944
- if (timeoutId) {
945
- clearTimeout(timeoutId);
946
- }
947
- if (process.env.NODE_ENV !== "production") {
948
- if (response) {
949
- logs.push("Got response from network.");
950
- } else {
951
- logs.push("Unable to get a response from the network. Will respond " + "with a cached response.");
843
+ if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) {
844
+ defaultPluginIndex = index;
952
845
  }
953
- }
954
- if (error || !response) {
955
- response = await handler.cacheMatch(request);
956
- if (process.env.NODE_ENV !== "production") {
957
- if (response) {
958
- logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
959
- } else {
960
- logs.push(`No response found in the '${this.cacheName}' cache.`);
961
- }
846
+ if (plugin.cacheWillUpdate) {
847
+ cacheWillUpdatePluginCount++;
962
848
  }
963
849
  }
964
- return response;
850
+ if (cacheWillUpdatePluginCount === 0) {
851
+ this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin);
852
+ } else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) {
853
+ this.plugins.splice(defaultPluginIndex, 1);
854
+ }
965
855
  }
966
856
  }
967
857
 
968
- class NetworkOnly extends Strategy {
969
- _networkTimeoutSeconds;
970
- constructor(options = {}){
971
- super(options);
972
- this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
858
+ class PrecacheCacheKeyPlugin {
859
+ _precacheController;
860
+ constructor({ precacheController }){
861
+ this._precacheController = precacheController;
973
862
  }
974
- async _handle(request, handler) {
863
+ cacheKeyWillBeUsed = async ({ request, params })=>{
864
+ const cacheKey = params?.cacheKey || this._precacheController.getPrecacheKeyForUrl(request.url);
865
+ return cacheKey ? new Request(cacheKey, {
866
+ headers: request.headers
867
+ }) : request;
868
+ };
869
+ }
870
+
871
+ const parsePrecacheOptions = (controller, { cacheName, plugins, fetchOptions, matchOptions, fallbackToNetwork, directoryIndex, ignoreURLParametersMatching, cleanURLs, urlManipulation, cleanupOutdatedCaches, concurrency, navigateFallback, navigateFallbackAllowlist, navigateFallbackDenylist } = {})=>({
872
+ strategyOptions: {
873
+ cacheName: cacheNames$1.getPrecacheName(cacheName),
874
+ plugins: [
875
+ ...plugins ?? [],
876
+ new PrecacheCacheKeyPlugin({
877
+ precacheController: controller
878
+ })
879
+ ],
880
+ fetchOptions,
881
+ matchOptions,
882
+ fallbackToNetwork
883
+ },
884
+ routeOptions: {
885
+ directoryIndex,
886
+ ignoreURLParametersMatching,
887
+ cleanURLs,
888
+ urlManipulation
889
+ },
890
+ controllerOptions: {
891
+ cleanupOutdatedCaches,
892
+ concurrency: concurrency ?? 10,
893
+ navigateFallback,
894
+ navigateFallbackAllowlist,
895
+ navigateFallbackDenylist
896
+ }
897
+ });
898
+
899
+ class PrecacheController {
900
+ _urlsToCacheKeys = new Map();
901
+ _urlsToCacheModes = new Map();
902
+ _cacheKeysToIntegrities = new Map();
903
+ _strategy;
904
+ _options;
905
+ _routeOptions;
906
+ constructor(entries, precacheOptions){
907
+ const { strategyOptions, routeOptions, controllerOptions } = parsePrecacheOptions(this, precacheOptions);
908
+ this.addToCacheList(entries);
909
+ this._strategy = new PrecacheStrategy(strategyOptions);
910
+ this._options = controllerOptions;
911
+ this._routeOptions = routeOptions;
912
+ }
913
+ get strategy() {
914
+ return this._strategy;
915
+ }
916
+ addToCacheList(entries) {
975
917
  if (process.env.NODE_ENV !== "production") {
976
- finalAssertExports.isInstance(request, Request, {
918
+ finalAssertExports.isArray(entries, {
977
919
  moduleName: "serwist",
978
- className: this.constructor.name,
979
- funcName: "_handle",
980
- paramName: "request"
920
+ className: "PrecacheController",
921
+ funcName: "addEntries",
922
+ paramName: "entries"
981
923
  });
982
924
  }
983
- let error = undefined;
984
- let response;
985
- try {
986
- const promises = [
987
- handler.fetch(request)
988
- ];
989
- if (this._networkTimeoutSeconds) {
990
- const timeoutPromise = timeout(this._networkTimeoutSeconds * 1000);
991
- promises.push(timeoutPromise);
925
+ const urlsToWarnAbout = [];
926
+ for (const entry of entries){
927
+ if (typeof entry === "string") {
928
+ urlsToWarnAbout.push(entry);
929
+ } else if (entry && !entry.integrity && entry.revision === undefined) {
930
+ urlsToWarnAbout.push(entry.url);
992
931
  }
993
- response = await Promise.race(promises);
994
- if (!response) {
995
- throw new Error(`Timed out the network response after ${this._networkTimeoutSeconds} seconds.`);
932
+ const { cacheKey, url } = createCacheKey(entry);
933
+ const cacheMode = typeof entry !== "string" && entry.revision ? "reload" : "default";
934
+ if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) {
935
+ throw new SerwistError("add-to-cache-list-conflicting-entries", {
936
+ firstEntry: this._urlsToCacheKeys.get(url),
937
+ secondEntry: cacheKey
938
+ });
996
939
  }
997
- } catch (err) {
998
- if (err instanceof Error) {
999
- error = err;
940
+ if (typeof entry !== "string" && entry.integrity) {
941
+ if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) {
942
+ throw new SerwistError("add-to-cache-list-conflicting-integrities", {
943
+ url
944
+ });
945
+ }
946
+ this._cacheKeysToIntegrities.set(cacheKey, entry.integrity);
1000
947
  }
1001
- }
1002
- if (process.env.NODE_ENV !== "production") {
1003
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
1004
- if (response) {
1005
- logger.log("Got response from network.");
1006
- } else {
1007
- logger.log("Unable to get a response from the network.");
948
+ this._urlsToCacheKeys.set(url, cacheKey);
949
+ this._urlsToCacheModes.set(url, cacheMode);
950
+ if (urlsToWarnAbout.length > 0) {
951
+ const warningMessage = `Serwist is precaching URLs without revision info: ${urlsToWarnAbout.join(", ")}\nThis is generally NOT safe, as you risk serving outdated assets.`;
952
+ if (process.env.NODE_ENV === "production") {
953
+ console.warn(warningMessage);
954
+ } else {
955
+ logger.warn(warningMessage);
956
+ }
1008
957
  }
1009
- messages.printFinalResponse(response);
1010
- logger.groupEnd();
1011
958
  }
1012
- if (!response) {
1013
- throw new SerwistError("no-response", {
1014
- url: request.url,
1015
- error
1016
- });
959
+ }
960
+ init({ serwist }) {
961
+ serwist.registerRoute(new PrecacheRoute(this, this._routeOptions));
962
+ if (this._options.navigateFallback) {
963
+ serwist.registerRoute(new NavigationRoute(this.createHandlerBoundToUrl(this._options.navigateFallback), {
964
+ allowlist: this._options.navigateFallbackAllowlist,
965
+ denylist: this._options.navigateFallbackDenylist
966
+ }));
1017
967
  }
1018
- return response;
1019
968
  }
1020
- }
1021
-
1022
- const BACKGROUND_SYNC_DB_VERSION = 3;
1023
- const BACKGROUND_SYNC_DB_NAME = "serwist-background-sync";
1024
- const REQUEST_OBJECT_STORE_NAME = "requests";
1025
- const QUEUE_NAME_INDEX = "queueName";
1026
- class BackgroundSyncQueueDb {
1027
- _db = null;
1028
- async addEntry(entry) {
1029
- const db = await this.getDb();
1030
- const tx = db.transaction(REQUEST_OBJECT_STORE_NAME, "readwrite", {
1031
- durability: "relaxed"
969
+ async install({ event }) {
970
+ const installReportPlugin = new PrecacheInstallReportPlugin();
971
+ this._strategy.plugins.push(installReportPlugin);
972
+ await parallel(this._options.concurrency, Array.from(this._urlsToCacheKeys.entries()), async ([url, cacheKey])=>{
973
+ const integrity = this._cacheKeysToIntegrities.get(cacheKey);
974
+ const cacheMode = this._urlsToCacheModes.get(url);
975
+ const request = new Request(url, {
976
+ integrity,
977
+ cache: cacheMode,
978
+ credentials: "same-origin"
979
+ });
980
+ await Promise.all(this._strategy.handleAll({
981
+ event,
982
+ request,
983
+ url: new URL(request.url),
984
+ params: {
985
+ cacheKey
986
+ }
987
+ }));
1032
988
  });
1033
- await tx.store.add(entry);
1034
- await tx.done;
1035
- }
1036
- async getFirstEntryId() {
1037
- const db = await this.getDb();
1038
- const cursor = await db.transaction(REQUEST_OBJECT_STORE_NAME).store.openCursor();
1039
- return cursor?.value.id;
1040
- }
1041
- async getAllEntriesByQueueName(queueName) {
1042
- const db = await this.getDb();
1043
- const results = await db.getAllFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
1044
- return results ? results : new Array();
989
+ const { updatedURLs, notUpdatedURLs } = installReportPlugin;
990
+ if (process.env.NODE_ENV !== "production") {
991
+ printInstallDetails(updatedURLs, notUpdatedURLs);
992
+ }
1045
993
  }
1046
- async getEntryCountByQueueName(queueName) {
1047
- const db = await this.getDb();
1048
- return db.countFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
994
+ async activate() {
995
+ const cache = await self.caches.open(this._strategy.cacheName);
996
+ const currentlyCachedRequests = await cache.keys();
997
+ const expectedCacheKeys = new Set(this._urlsToCacheKeys.values());
998
+ const deletedCacheRequests = [];
999
+ for (const request of currentlyCachedRequests){
1000
+ if (!expectedCacheKeys.has(request.url)) {
1001
+ await cache.delete(request);
1002
+ deletedCacheRequests.push(request.url);
1003
+ }
1004
+ }
1005
+ if (process.env.NODE_ENV !== "production") {
1006
+ printCleanupDetails(deletedCacheRequests);
1007
+ }
1049
1008
  }
1050
- async deleteEntry(id) {
1051
- const db = await this.getDb();
1052
- await db.delete(REQUEST_OBJECT_STORE_NAME, id);
1009
+ getUrlsToPrecacheKeys() {
1010
+ return this._urlsToCacheKeys;
1053
1011
  }
1054
- async getFirstEntryByQueueName(queueName) {
1055
- return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), "next");
1012
+ getPrecachedUrls() {
1013
+ return [
1014
+ ...this._urlsToCacheKeys.keys()
1015
+ ];
1056
1016
  }
1057
- async getLastEntryByQueueName(queueName) {
1058
- return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), "prev");
1017
+ getPrecacheKeyForUrl(url) {
1018
+ const urlObject = new URL(url, location.href);
1019
+ return this._urlsToCacheKeys.get(urlObject.href);
1059
1020
  }
1060
- async getEndEntryFromIndex(query, direction) {
1061
- const db = await this.getDb();
1062
- const cursor = await db.transaction(REQUEST_OBJECT_STORE_NAME).store.index(QUEUE_NAME_INDEX).openCursor(query, direction);
1063
- return cursor?.value;
1021
+ getIntegrityForPrecacheKey(cacheKey) {
1022
+ return this._cacheKeysToIntegrities.get(cacheKey);
1064
1023
  }
1065
- async getDb() {
1066
- if (!this._db) {
1067
- this._db = await openDB(BACKGROUND_SYNC_DB_NAME, BACKGROUND_SYNC_DB_VERSION, {
1068
- upgrade: this._upgradeDb
1069
- });
1024
+ async matchPrecache(request) {
1025
+ const url = request instanceof Request ? request.url : request;
1026
+ const cacheKey = this.getPrecacheKeyForUrl(url);
1027
+ if (cacheKey) {
1028
+ const cache = await self.caches.open(this._strategy.cacheName);
1029
+ return cache.match(cacheKey);
1070
1030
  }
1071
- return this._db;
1031
+ return undefined;
1072
1032
  }
1073
- _upgradeDb(db, oldVersion) {
1074
- if (oldVersion > 0 && oldVersion < BACKGROUND_SYNC_DB_VERSION) {
1075
- if (db.objectStoreNames.contains(REQUEST_OBJECT_STORE_NAME)) {
1076
- db.deleteObjectStore(REQUEST_OBJECT_STORE_NAME);
1077
- }
1033
+ createHandlerBoundToUrl(url) {
1034
+ const cacheKey = this.getPrecacheKeyForUrl(url);
1035
+ if (!cacheKey) {
1036
+ throw new SerwistError("non-precached-url", {
1037
+ url
1038
+ });
1078
1039
  }
1079
- const objStore = db.createObjectStore(REQUEST_OBJECT_STORE_NAME, {
1080
- autoIncrement: true,
1081
- keyPath: "id"
1082
- });
1083
- objStore.createIndex(QUEUE_NAME_INDEX, QUEUE_NAME_INDEX, {
1084
- unique: false
1085
- });
1040
+ return (options)=>{
1041
+ options.request = new Request(url);
1042
+ options.params = {
1043
+ cacheKey,
1044
+ ...options.params
1045
+ };
1046
+ return this._strategy.handle(options);
1047
+ };
1086
1048
  }
1087
1049
  }
1088
1050
 
1089
- class BackgroundSyncQueueStore {
1090
- _queueName;
1091
- _queueDb;
1051
+ class PrecacheFallbackPlugin {
1052
+ _fallbackUrls;
1053
+ _precacheController;
1054
+ constructor({ fallbackUrls, precacheController, serwist }){
1055
+ this._fallbackUrls = fallbackUrls;
1056
+ if (!serwist) {
1057
+ this._precacheController = precacheController;
1058
+ } else {
1059
+ this._precacheController = serwist.precache;
1060
+ }
1061
+ }
1062
+ async handlerDidError(param) {
1063
+ for (const fallback of this._fallbackUrls){
1064
+ if (typeof fallback === "string") {
1065
+ const fallbackResponse = await this._precacheController.matchPrecache(fallback);
1066
+ if (fallbackResponse !== undefined) {
1067
+ return fallbackResponse;
1068
+ }
1069
+ } else if (fallback.matcher(param)) {
1070
+ const fallbackResponse = await this._precacheController.matchPrecache(fallback.url);
1071
+ if (fallbackResponse !== undefined) {
1072
+ return fallbackResponse;
1073
+ }
1074
+ }
1075
+ }
1076
+ return undefined;
1077
+ }
1078
+ }
1079
+
1080
+ class RuntimeCacheController {
1081
+ _entries;
1082
+ _options;
1083
+ constructor(entries, options = {}){
1084
+ this._entries = entries;
1085
+ this._options = options;
1086
+ this.init = this.init.bind(this);
1087
+ this.install = this.install.bind(this);
1088
+ }
1089
+ init({ serwist }) {
1090
+ if (this._options.fallbacks !== undefined) {
1091
+ const fallbackPlugin = new PrecacheFallbackPlugin({
1092
+ fallbackUrls: this._options.fallbacks.entries,
1093
+ precacheController: serwist.precache
1094
+ });
1095
+ this._entries.forEach((cacheEntry)=>{
1096
+ if (cacheEntry.handler instanceof Strategy && !cacheEntry.handler.plugins.some((plugin)=>"handlerDidError" in plugin)) {
1097
+ cacheEntry.handler.plugins.push(fallbackPlugin);
1098
+ }
1099
+ });
1100
+ }
1101
+ for (const entry of this._entries){
1102
+ serwist.registerCapture(entry.matcher, entry.handler, entry.method);
1103
+ }
1104
+ }
1105
+ async install({ event, serwist }) {
1106
+ const concurrency = this._options.warmOptions?.concurrency ?? 10;
1107
+ if (this._options.warmEntries) {
1108
+ await parallel(concurrency, this._options.warmEntries, async (entry)=>{
1109
+ const request = entry instanceof Request ? entry : new Request(typeof entry === "string" ? entry : entry.url, {
1110
+ integrity: typeof entry !== "string" ? entry.integrity : undefined,
1111
+ credentials: "same-origin"
1112
+ });
1113
+ await serwist.handleRequest({
1114
+ request,
1115
+ event
1116
+ });
1117
+ });
1118
+ }
1119
+ }
1120
+ warmRuntimeCache(entries) {
1121
+ if (!this._options.warmEntries) this._options.warmEntries = [];
1122
+ this._options.warmEntries.push(...entries);
1123
+ }
1124
+ }
1125
+
1126
+ const BACKGROUND_SYNC_DB_VERSION = 3;
1127
+ const BACKGROUND_SYNC_DB_NAME = "serwist-background-sync";
1128
+ const REQUEST_OBJECT_STORE_NAME = "requests";
1129
+ const QUEUE_NAME_INDEX = "queueName";
1130
+ class BackgroundSyncQueueDb {
1131
+ _db = null;
1132
+ async addEntry(entry) {
1133
+ const db = await this.getDb();
1134
+ const tx = db.transaction(REQUEST_OBJECT_STORE_NAME, "readwrite", {
1135
+ durability: "relaxed"
1136
+ });
1137
+ await tx.store.add(entry);
1138
+ await tx.done;
1139
+ }
1140
+ async getFirstEntryId() {
1141
+ const db = await this.getDb();
1142
+ const cursor = await db.transaction(REQUEST_OBJECT_STORE_NAME).store.openCursor();
1143
+ return cursor?.value.id;
1144
+ }
1145
+ async getAllEntriesByQueueName(queueName) {
1146
+ const db = await this.getDb();
1147
+ const results = await db.getAllFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
1148
+ return results ? results : new Array();
1149
+ }
1150
+ async getEntryCountByQueueName(queueName) {
1151
+ const db = await this.getDb();
1152
+ return db.countFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
1153
+ }
1154
+ async deleteEntry(id) {
1155
+ const db = await this.getDb();
1156
+ await db.delete(REQUEST_OBJECT_STORE_NAME, id);
1157
+ }
1158
+ async getFirstEntryByQueueName(queueName) {
1159
+ return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), "next");
1160
+ }
1161
+ async getLastEntryByQueueName(queueName) {
1162
+ return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), "prev");
1163
+ }
1164
+ async getEndEntryFromIndex(query, direction) {
1165
+ const db = await this.getDb();
1166
+ const cursor = await db.transaction(REQUEST_OBJECT_STORE_NAME).store.index(QUEUE_NAME_INDEX).openCursor(query, direction);
1167
+ return cursor?.value;
1168
+ }
1169
+ async getDb() {
1170
+ if (!this._db) {
1171
+ this._db = await openDB(BACKGROUND_SYNC_DB_NAME, BACKGROUND_SYNC_DB_VERSION, {
1172
+ upgrade: this._upgradeDb
1173
+ });
1174
+ }
1175
+ return this._db;
1176
+ }
1177
+ _upgradeDb(db, oldVersion) {
1178
+ if (oldVersion > 0 && oldVersion < BACKGROUND_SYNC_DB_VERSION) {
1179
+ if (db.objectStoreNames.contains(REQUEST_OBJECT_STORE_NAME)) {
1180
+ db.deleteObjectStore(REQUEST_OBJECT_STORE_NAME);
1181
+ }
1182
+ }
1183
+ const objStore = db.createObjectStore(REQUEST_OBJECT_STORE_NAME, {
1184
+ autoIncrement: true,
1185
+ keyPath: "id"
1186
+ });
1187
+ objStore.createIndex(QUEUE_NAME_INDEX, QUEUE_NAME_INDEX, {
1188
+ unique: false
1189
+ });
1190
+ }
1191
+ }
1192
+
1193
+ class BackgroundSyncQueueStore {
1194
+ _queueName;
1195
+ _queueDb;
1092
1196
  constructor(queueName){
1093
1197
  this._queueName = queueName;
1094
1198
  this._queueDb = new BackgroundSyncQueueDb();
@@ -1456,515 +1560,413 @@ class BackgroundSyncPlugin {
1456
1560
  }
1457
1561
  }
1458
1562
 
1459
- const QUEUE_NAME = "serwist-google-analytics";
1460
- const MAX_RETENTION_TIME = 60 * 48;
1461
- const GOOGLE_ANALYTICS_HOST = "www.google-analytics.com";
1462
- const GTM_HOST = "www.googletagmanager.com";
1463
- const ANALYTICS_JS_PATH = "/analytics.js";
1464
- const GTAG_JS_PATH = "/gtag/js";
1465
- const GTM_JS_PATH = "/gtm.js";
1466
- const COLLECT_PATHS_REGEX = /^\/(\w+\/)?collect/;
1467
-
1468
- const createOnSyncCallback = (config)=>{
1469
- return async ({ queue })=>{
1470
- let entry = undefined;
1471
- while(entry = await queue.shiftRequest()){
1472
- const { request, timestamp } = entry;
1473
- const url = new URL(request.url);
1474
- try {
1475
- const params = request.method === "POST" ? new URLSearchParams(await request.clone().text()) : url.searchParams;
1476
- const originalHitTime = timestamp - (Number(params.get("qt")) || 0);
1477
- const queueTime = Date.now() - originalHitTime;
1478
- params.set("qt", String(queueTime));
1479
- if (config.parameterOverrides) {
1480
- for (const param of Object.keys(config.parameterOverrides)){
1481
- const value = config.parameterOverrides[param];
1482
- params.set(param, value);
1483
- }
1484
- }
1485
- if (typeof config.hitFilter === "function") {
1486
- config.hitFilter.call(null, params);
1487
- }
1488
- await fetch(new Request(url.origin + url.pathname, {
1489
- body: params.toString(),
1490
- method: "POST",
1491
- mode: "cors",
1492
- credentials: "omit",
1493
- headers: {
1494
- "Content-Type": "text/plain"
1495
- }
1496
- }));
1497
- if (process.env.NODE_ENV !== "production") {
1498
- logger.log(`Request for '${getFriendlyURL(url.href)}' has been replayed`);
1499
- }
1500
- } catch (err) {
1501
- await queue.unshiftRequest(entry);
1502
- if (process.env.NODE_ENV !== "production") {
1503
- logger.log(`Request for '${getFriendlyURL(url.href)}' failed to replay, putting it back in the queue.`);
1504
- }
1505
- throw err;
1506
- }
1507
- }
1508
- if (process.env.NODE_ENV !== "production") {
1509
- logger.log("All Google Analytics request successfully replayed; " + "the queue is now empty!");
1563
+ const cacheOkAndOpaquePlugin = {
1564
+ cacheWillUpdate: async ({ response })=>{
1565
+ if (response.status === 200 || response.status === 0) {
1566
+ return response;
1510
1567
  }
1511
- };
1512
- };
1513
- const createCollectRoutes = (bgSyncPlugin)=>{
1514
- const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && COLLECT_PATHS_REGEX.test(url.pathname);
1515
- const handler = new NetworkOnly({
1516
- plugins: [
1517
- bgSyncPlugin
1518
- ]
1519
- });
1520
- return [
1521
- new Route(match, handler, "GET"),
1522
- new Route(match, handler, "POST")
1523
- ];
1524
- };
1525
- const createAnalyticsJsRoute = (cacheName)=>{
1526
- const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && url.pathname === ANALYTICS_JS_PATH;
1527
- const handler = new NetworkFirst({
1528
- cacheName
1529
- });
1530
- return new Route(match, handler, "GET");
1531
- };
1532
- const createGtagJsRoute = (cacheName)=>{
1533
- const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTAG_JS_PATH;
1534
- const handler = new NetworkFirst({
1535
- cacheName
1536
- });
1537
- return new Route(match, handler, "GET");
1538
- };
1539
- const createGtmJsRoute = (cacheName)=>{
1540
- const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTM_JS_PATH;
1541
- const handler = new NetworkFirst({
1542
- cacheName
1543
- });
1544
- return new Route(match, handler, "GET");
1545
- };
1546
- const initializeGoogleAnalytics = ({ serwist, cacheName, ...options })=>{
1547
- const resolvedCacheName = cacheNames$1.getGoogleAnalyticsName(cacheName);
1548
- const bgSyncPlugin = new BackgroundSyncPlugin(QUEUE_NAME, {
1549
- maxRetentionTime: MAX_RETENTION_TIME,
1550
- onSync: createOnSyncCallback(options)
1551
- });
1552
- const routes = [
1553
- createGtmJsRoute(resolvedCacheName),
1554
- createAnalyticsJsRoute(resolvedCacheName),
1555
- createGtagJsRoute(resolvedCacheName),
1556
- ...createCollectRoutes(bgSyncPlugin)
1557
- ];
1558
- for (const route of routes){
1559
- serwist.registerRoute(route);
1568
+ return null;
1560
1569
  }
1561
1570
  };
1562
1571
 
1563
- const isNavigationPreloadSupported = ()=>{
1564
- return Boolean(self.registration?.navigationPreload);
1572
+ const messages = {
1573
+ strategyStart: (strategyName, request)=>`Using ${strategyName} to respond to '${getFriendlyURL(request.url)}'`,
1574
+ printFinalResponse: (response)=>{
1575
+ if (response) {
1576
+ logger.groupCollapsed("View the final response here.");
1577
+ logger.log(response || "[No response returned]");
1578
+ logger.groupEnd();
1579
+ }
1580
+ }
1565
1581
  };
1566
- const enableNavigationPreload = (headerValue)=>{
1567
- if (isNavigationPreloadSupported()) {
1568
- self.addEventListener("activate", (event)=>{
1569
- event.waitUntil(self.registration.navigationPreload.enable().then(()=>{
1570
- if (headerValue) {
1571
- void self.registration.navigationPreload.setHeaderValue(headerValue);
1572
- }
1573
- if (process.env.NODE_ENV !== "production") {
1574
- logger.log("Navigation preloading is enabled.");
1575
- }
1576
- }));
1577
- });
1578
- } else {
1579
- if (process.env.NODE_ENV !== "production") {
1580
- logger.log("Navigation preloading is not supported in this browser.");
1582
+
1583
+ class NetworkFirst extends Strategy {
1584
+ _networkTimeoutSeconds;
1585
+ constructor(options = {}){
1586
+ super(options);
1587
+ if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
1588
+ this.plugins.unshift(cacheOkAndOpaquePlugin);
1581
1589
  }
1582
- }
1583
- };
1584
- const disableNavigationPreload = ()=>{
1585
- if (isNavigationPreloadSupported()) {
1586
- self.addEventListener("activate", (event)=>{
1587
- event.waitUntil(self.registration.navigationPreload.disable().then(()=>{
1588
- if (process.env.NODE_ENV !== "production") {
1589
- logger.log("Navigation preloading is disabled.");
1590
- }
1591
- }));
1592
- });
1593
- } else {
1590
+ this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
1594
1591
  if (process.env.NODE_ENV !== "production") {
1595
- logger.log("Navigation preloading is not supported in this browser.");
1592
+ if (this._networkTimeoutSeconds) {
1593
+ finalAssertExports.isType(this._networkTimeoutSeconds, "number", {
1594
+ moduleName: "serwist",
1595
+ className: this.constructor.name,
1596
+ funcName: "constructor",
1597
+ paramName: "networkTimeoutSeconds"
1598
+ });
1599
+ }
1596
1600
  }
1597
1601
  }
1598
- };
1599
-
1600
- const setCacheNameDetails = (details)=>{
1601
- if (process.env.NODE_ENV !== "production") {
1602
- for (const key of Object.keys(details)){
1603
- finalAssertExports.isType(details[key], "string", {
1604
- moduleName: "@serwist/core",
1605
- funcName: "setCacheNameDetails",
1606
- paramName: `details.${key}`
1602
+ async _handle(request, handler) {
1603
+ const logs = [];
1604
+ if (process.env.NODE_ENV !== "production") {
1605
+ finalAssertExports.isInstance(request, Request, {
1606
+ moduleName: "serwist",
1607
+ className: this.constructor.name,
1608
+ funcName: "handle",
1609
+ paramName: "makeRequest"
1607
1610
  });
1608
1611
  }
1609
- if (details.precache?.length === 0) {
1610
- throw new SerwistError("invalid-cache-name", {
1611
- cacheNameId: "precache",
1612
- value: details.precache
1612
+ const promises = [];
1613
+ let timeoutId;
1614
+ if (this._networkTimeoutSeconds) {
1615
+ const { id, promise } = this._getTimeoutPromise({
1616
+ request,
1617
+ logs,
1618
+ handler
1613
1619
  });
1620
+ timeoutId = id;
1621
+ promises.push(promise);
1614
1622
  }
1615
- if (details.runtime?.length === 0) {
1616
- throw new SerwistError("invalid-cache-name", {
1617
- cacheNameId: "runtime",
1618
- value: details.runtime
1619
- });
1623
+ const networkPromise = this._getNetworkPromise({
1624
+ timeoutId,
1625
+ request,
1626
+ logs,
1627
+ handler
1628
+ });
1629
+ promises.push(networkPromise);
1630
+ const response = await handler.waitUntil((async ()=>{
1631
+ return await handler.waitUntil(Promise.race(promises)) || await networkPromise;
1632
+ })());
1633
+ if (process.env.NODE_ENV !== "production") {
1634
+ logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
1635
+ for (const log of logs){
1636
+ logger.log(log);
1637
+ }
1638
+ messages.printFinalResponse(response);
1639
+ logger.groupEnd();
1620
1640
  }
1621
- if (details.googleAnalytics?.length === 0) {
1622
- throw new SerwistError("invalid-cache-name", {
1623
- cacheNameId: "googleAnalytics",
1624
- value: details.googleAnalytics
1641
+ if (!response) {
1642
+ throw new SerwistError("no-response", {
1643
+ url: request.url
1625
1644
  });
1626
1645
  }
1646
+ return response;
1627
1647
  }
1628
- cacheNames$1.updateDetails(details);
1629
- };
1630
-
1631
- const parseRoute = (capture, handler, method)=>{
1632
- if (typeof capture === "string") {
1633
- const captureUrl = new URL(capture, location.href);
1634
- if (process.env.NODE_ENV !== "production") {
1635
- if (!(capture.startsWith("/") || capture.startsWith("http"))) {
1636
- throw new SerwistError("invalid-string", {
1637
- moduleName: "serwist",
1638
- funcName: "parseRoute",
1639
- paramName: "capture"
1640
- });
1641
- }
1642
- const valueToCheck = capture.startsWith("http") ? captureUrl.pathname : capture;
1643
- const wildcards = "[*:?+]";
1644
- if (new RegExp(`${wildcards}`).exec(valueToCheck)) {
1645
- logger.debug(`The '$capture' parameter contains an Express-style wildcard character (${wildcards}). Strings are now always interpreted as exact matches; use a RegExp for partial or wildcard matches.`);
1646
- }
1647
- }
1648
- const matchCallback = ({ url })=>{
1649
- if (process.env.NODE_ENV !== "production") {
1650
- if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) {
1651
- logger.debug(`${capture} only partially matches the cross-origin URL ${url.toString()}. This route will only handle cross-origin requests if they match the entire URL.`);
1648
+ _getTimeoutPromise({ request, logs, handler }) {
1649
+ let timeoutId;
1650
+ const timeoutPromise = new Promise((resolve)=>{
1651
+ const onNetworkTimeout = async ()=>{
1652
+ if (process.env.NODE_ENV !== "production") {
1653
+ logs.push(`Timing out the network response at ${this._networkTimeoutSeconds} seconds.`);
1652
1654
  }
1653
- }
1654
- return url.href === captureUrl.href;
1655
- };
1656
- return new Route(matchCallback, handler, method);
1657
- }
1658
- if (capture instanceof RegExp) {
1659
- return new RegExpRoute(capture, handler, method);
1660
- }
1661
- if (typeof capture === "function") {
1662
- return new Route(capture, handler, method);
1663
- }
1664
- if (capture instanceof Route) {
1665
- return capture;
1666
- }
1667
- throw new SerwistError("unsupported-route-type", {
1668
- moduleName: "serwist",
1669
- funcName: "parseRoute",
1670
- paramName: "capture"
1671
- });
1672
- };
1673
-
1674
- const REVISION_SEARCH_PARAM = "__WB_REVISION__";
1675
- const createCacheKey = (entry)=>{
1676
- if (!entry) {
1677
- throw new SerwistError("add-to-cache-list-unexpected-type", {
1678
- entry
1679
- });
1680
- }
1681
- if (typeof entry === "string") {
1682
- const urlObject = new URL(entry, location.href);
1683
- return {
1684
- cacheKey: urlObject.href,
1685
- url: urlObject.href
1686
- };
1687
- }
1688
- const { revision, url } = entry;
1689
- if (!url) {
1690
- throw new SerwistError("add-to-cache-list-unexpected-type", {
1691
- entry
1655
+ resolve(await handler.cacheMatch(request));
1656
+ };
1657
+ timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000);
1692
1658
  });
1693
- }
1694
- if (!revision) {
1695
- const urlObject = new URL(url, location.href);
1696
1659
  return {
1697
- cacheKey: urlObject.href,
1698
- url: urlObject.href
1660
+ promise: timeoutPromise,
1661
+ id: timeoutId
1699
1662
  };
1700
1663
  }
1701
- const cacheKeyURL = new URL(url, location.href);
1702
- const originalURL = new URL(url, location.href);
1703
- cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision);
1704
- return {
1705
- cacheKey: cacheKeyURL.href,
1706
- url: originalURL.href
1707
- };
1708
- };
1709
-
1710
- class PrecacheInstallReportPlugin {
1711
- updatedURLs = [];
1712
- notUpdatedURLs = [];
1713
- handlerWillStart = async ({ request, state })=>{
1714
- if (state) {
1715
- state.originalRequest = request;
1664
+ async _getNetworkPromise({ timeoutId, request, logs, handler }) {
1665
+ let error = undefined;
1666
+ let response = undefined;
1667
+ try {
1668
+ response = await handler.fetchAndCachePut(request);
1669
+ } catch (fetchError) {
1670
+ if (fetchError instanceof Error) {
1671
+ error = fetchError;
1672
+ }
1716
1673
  }
1717
- };
1718
- cachedResponseWillBeUsed = async ({ event, state, cachedResponse })=>{
1719
- if (event.type === "install") {
1720
- if (state?.originalRequest && state.originalRequest instanceof Request) {
1721
- const url = state.originalRequest.url;
1722
- if (cachedResponse) {
1723
- this.notUpdatedURLs.push(url);
1674
+ if (timeoutId) {
1675
+ clearTimeout(timeoutId);
1676
+ }
1677
+ if (process.env.NODE_ENV !== "production") {
1678
+ if (response) {
1679
+ logs.push("Got response from network.");
1680
+ } else {
1681
+ logs.push("Unable to get a response from the network. Will respond " + "with a cached response.");
1682
+ }
1683
+ }
1684
+ if (error || !response) {
1685
+ response = await handler.cacheMatch(request);
1686
+ if (process.env.NODE_ENV !== "production") {
1687
+ if (response) {
1688
+ logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
1724
1689
  } else {
1725
- this.updatedURLs.push(url);
1690
+ logs.push(`No response found in the '${this.cacheName}' cache.`);
1726
1691
  }
1727
1692
  }
1728
1693
  }
1729
- return cachedResponse;
1730
- };
1694
+ return response;
1695
+ }
1731
1696
  }
1732
1697
 
1733
- function _nestedGroup(groupTitle, urls) {
1734
- if (urls.length === 0) {
1735
- return;
1698
+ class NetworkOnly extends Strategy {
1699
+ _networkTimeoutSeconds;
1700
+ constructor(options = {}){
1701
+ super(options);
1702
+ this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
1736
1703
  }
1737
- logger.groupCollapsed(groupTitle);
1738
- for (const url of urls){
1739
- logger.log(url);
1704
+ async _handle(request, handler) {
1705
+ if (process.env.NODE_ENV !== "production") {
1706
+ finalAssertExports.isInstance(request, Request, {
1707
+ moduleName: "serwist",
1708
+ className: this.constructor.name,
1709
+ funcName: "_handle",
1710
+ paramName: "request"
1711
+ });
1712
+ }
1713
+ let error = undefined;
1714
+ let response;
1715
+ try {
1716
+ const promises = [
1717
+ handler.fetch(request)
1718
+ ];
1719
+ if (this._networkTimeoutSeconds) {
1720
+ const timeoutPromise = timeout(this._networkTimeoutSeconds * 1000);
1721
+ promises.push(timeoutPromise);
1722
+ }
1723
+ response = await Promise.race(promises);
1724
+ if (!response) {
1725
+ throw new Error(`Timed out the network response after ${this._networkTimeoutSeconds} seconds.`);
1726
+ }
1727
+ } catch (err) {
1728
+ if (err instanceof Error) {
1729
+ error = err;
1730
+ }
1731
+ }
1732
+ if (process.env.NODE_ENV !== "production") {
1733
+ logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
1734
+ if (response) {
1735
+ logger.log("Got response from network.");
1736
+ } else {
1737
+ logger.log("Unable to get a response from the network.");
1738
+ }
1739
+ messages.printFinalResponse(response);
1740
+ logger.groupEnd();
1741
+ }
1742
+ if (!response) {
1743
+ throw new SerwistError("no-response", {
1744
+ url: request.url,
1745
+ error
1746
+ });
1747
+ }
1748
+ return response;
1740
1749
  }
1741
- logger.groupEnd();
1742
1750
  }
1743
- const printInstallDetails = (urlsToPrecache, urlsAlreadyPrecached)=>{
1744
- const precachedCount = urlsToPrecache.length;
1745
- const alreadyPrecachedCount = urlsAlreadyPrecached.length;
1746
- if (precachedCount || alreadyPrecachedCount) {
1747
- let message = `Precaching ${precachedCount} file${precachedCount === 1 ? "" : "s"}.`;
1748
- if (alreadyPrecachedCount > 0) {
1749
- message += ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? " is" : "s are"} already cached.`;
1751
+
1752
+ const QUEUE_NAME = "serwist-google-analytics";
1753
+ const MAX_RETENTION_TIME = 60 * 48;
1754
+ const GOOGLE_ANALYTICS_HOST = "www.google-analytics.com";
1755
+ const GTM_HOST = "www.googletagmanager.com";
1756
+ const ANALYTICS_JS_PATH = "/analytics.js";
1757
+ const GTAG_JS_PATH = "/gtag/js";
1758
+ const GTM_JS_PATH = "/gtm.js";
1759
+ const COLLECT_PATHS_REGEX = /^\/(\w+\/)?collect/;
1760
+
1761
+ const createOnSyncCallback = (config)=>{
1762
+ return async ({ queue })=>{
1763
+ let entry = undefined;
1764
+ while(entry = await queue.shiftRequest()){
1765
+ const { request, timestamp } = entry;
1766
+ const url = new URL(request.url);
1767
+ try {
1768
+ const params = request.method === "POST" ? new URLSearchParams(await request.clone().text()) : url.searchParams;
1769
+ const originalHitTime = timestamp - (Number(params.get("qt")) || 0);
1770
+ const queueTime = Date.now() - originalHitTime;
1771
+ params.set("qt", String(queueTime));
1772
+ if (config.parameterOverrides) {
1773
+ for (const param of Object.keys(config.parameterOverrides)){
1774
+ const value = config.parameterOverrides[param];
1775
+ params.set(param, value);
1776
+ }
1777
+ }
1778
+ if (typeof config.hitFilter === "function") {
1779
+ config.hitFilter.call(null, params);
1780
+ }
1781
+ await fetch(new Request(url.origin + url.pathname, {
1782
+ body: params.toString(),
1783
+ method: "POST",
1784
+ mode: "cors",
1785
+ credentials: "omit",
1786
+ headers: {
1787
+ "Content-Type": "text/plain"
1788
+ }
1789
+ }));
1790
+ if (process.env.NODE_ENV !== "production") {
1791
+ logger.log(`Request for '${getFriendlyURL(url.href)}' has been replayed`);
1792
+ }
1793
+ } catch (err) {
1794
+ await queue.unshiftRequest(entry);
1795
+ if (process.env.NODE_ENV !== "production") {
1796
+ logger.log(`Request for '${getFriendlyURL(url.href)}' failed to replay, putting it back in the queue.`);
1797
+ }
1798
+ throw err;
1799
+ }
1750
1800
  }
1751
- logger.groupCollapsed(message);
1752
- _nestedGroup("View newly precached URLs.", urlsToPrecache);
1753
- _nestedGroup("View previously precached URLs.", urlsAlreadyPrecached);
1754
- logger.groupEnd();
1755
- }
1801
+ if (process.env.NODE_ENV !== "production") {
1802
+ logger.log("All Google Analytics request successfully replayed; " + "the queue is now empty!");
1803
+ }
1804
+ };
1756
1805
  };
1757
-
1758
- const logGroup = (groupTitle, deletedURLs)=>{
1759
- logger.groupCollapsed(groupTitle);
1760
- for (const url of deletedURLs){
1761
- logger.log(url);
1762
- }
1763
- logger.groupEnd();
1806
+ const createCollectRoutes = (bgSyncPlugin)=>{
1807
+ const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && COLLECT_PATHS_REGEX.test(url.pathname);
1808
+ const handler = new NetworkOnly({
1809
+ plugins: [
1810
+ bgSyncPlugin
1811
+ ]
1812
+ });
1813
+ return [
1814
+ new Route(match, handler, "GET"),
1815
+ new Route(match, handler, "POST")
1816
+ ];
1764
1817
  };
1765
- const printCleanupDetails = (deletedURLs)=>{
1766
- const deletionCount = deletedURLs.length;
1767
- if (deletionCount > 0) {
1768
- logger.groupCollapsed(`During precaching cleanup, ${deletionCount} cached request${deletionCount === 1 ? " was" : "s were"} deleted.`);
1769
- logGroup("Deleted Cache Requests", deletedURLs);
1770
- logger.groupEnd();
1771
- }
1818
+ const createAnalyticsJsRoute = (cacheName)=>{
1819
+ const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && url.pathname === ANALYTICS_JS_PATH;
1820
+ const handler = new NetworkFirst({
1821
+ cacheName
1822
+ });
1823
+ return new Route(match, handler, "GET");
1772
1824
  };
1773
-
1774
- class PrecacheCacheKeyPlugin {
1775
- _precacheController;
1776
- constructor({ precacheController }){
1777
- this._precacheController = precacheController;
1778
- }
1779
- cacheKeyWillBeUsed = async ({ request, params })=>{
1780
- const cacheKey = params?.cacheKey || this._precacheController.getPrecacheKeyForUrl(request.url);
1781
- return cacheKey ? new Request(cacheKey, {
1782
- headers: request.headers
1783
- }) : request;
1784
- };
1785
- }
1786
-
1787
- const parsePrecacheOptions = (controller, { cacheName, plugins, fetchOptions, matchOptions, fallbackToNetwork, directoryIndex, ignoreURLParametersMatching, cleanURLs, urlManipulation, cleanupOutdatedCaches, concurrency, navigateFallback, navigateFallbackAllowlist, navigateFallbackDenylist } = {})=>({
1788
- strategyOptions: {
1789
- cacheName: cacheNames$1.getPrecacheName(cacheName),
1790
- plugins: [
1791
- ...plugins ?? [],
1792
- new PrecacheCacheKeyPlugin({
1793
- precacheController: controller
1794
- })
1795
- ],
1796
- fetchOptions,
1797
- matchOptions,
1798
- fallbackToNetwork
1799
- },
1800
- routeOptions: {
1801
- directoryIndex,
1802
- ignoreURLParametersMatching,
1803
- cleanURLs,
1804
- urlManipulation
1805
- },
1806
- controllerOptions: {
1807
- cleanupOutdatedCaches,
1808
- concurrency: concurrency ?? 10,
1809
- navigateFallback,
1810
- navigateFallbackAllowlist,
1811
- navigateFallbackDenylist
1812
- }
1825
+ const createGtagJsRoute = (cacheName)=>{
1826
+ const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTAG_JS_PATH;
1827
+ const handler = new NetworkFirst({
1828
+ cacheName
1813
1829
  });
1814
-
1815
- class PrecacheController {
1816
- _urlsToCacheKeys = new Map();
1817
- _urlsToCacheModes = new Map();
1818
- _cacheKeysToIntegrities = new Map();
1819
- _strategy;
1820
- _options;
1821
- _routeOptions;
1822
- constructor(entries, precacheOptions){
1823
- const { strategyOptions, routeOptions, controllerOptions } = parsePrecacheOptions(this, precacheOptions);
1824
- this.addToCacheList(entries);
1825
- this._strategy = new PrecacheStrategy(strategyOptions);
1826
- this._options = controllerOptions;
1827
- this._routeOptions = routeOptions;
1828
- this.install = this.install.bind(this);
1829
- this.activate = this.activate.bind(this);
1830
- }
1831
- get strategy() {
1832
- return this._strategy;
1830
+ return new Route(match, handler, "GET");
1831
+ };
1832
+ const createGtmJsRoute = (cacheName)=>{
1833
+ const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTM_JS_PATH;
1834
+ const handler = new NetworkFirst({
1835
+ cacheName
1836
+ });
1837
+ return new Route(match, handler, "GET");
1838
+ };
1839
+ const initializeGoogleAnalytics = ({ serwist, cacheName, ...options })=>{
1840
+ const resolvedCacheName = cacheNames$1.getGoogleAnalyticsName(cacheName);
1841
+ const bgSyncPlugin = new BackgroundSyncPlugin(QUEUE_NAME, {
1842
+ maxRetentionTime: MAX_RETENTION_TIME,
1843
+ onSync: createOnSyncCallback(options)
1844
+ });
1845
+ const routes = [
1846
+ createGtmJsRoute(resolvedCacheName),
1847
+ createAnalyticsJsRoute(resolvedCacheName),
1848
+ createGtagJsRoute(resolvedCacheName),
1849
+ ...createCollectRoutes(bgSyncPlugin)
1850
+ ];
1851
+ for (const route of routes){
1852
+ serwist.registerRoute(route);
1833
1853
  }
1834
- addToCacheList(entries) {
1854
+ };
1855
+
1856
+ const parseRoute = (capture, handler, method)=>{
1857
+ if (typeof capture === "string") {
1858
+ const captureUrl = new URL(capture, location.href);
1835
1859
  if (process.env.NODE_ENV !== "production") {
1836
- finalAssertExports.isArray(entries, {
1837
- moduleName: "serwist",
1838
- className: "PrecacheController",
1839
- funcName: "addEntries",
1840
- paramName: "entries"
1841
- });
1842
- }
1843
- const urlsToWarnAbout = [];
1844
- for (const entry of entries){
1845
- if (typeof entry === "string") {
1846
- urlsToWarnAbout.push(entry);
1847
- } else if (entry && !entry.integrity && entry.revision === undefined) {
1848
- urlsToWarnAbout.push(entry.url);
1849
- }
1850
- const { cacheKey, url } = createCacheKey(entry);
1851
- const cacheMode = typeof entry !== "string" && entry.revision ? "reload" : "default";
1852
- if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) {
1853
- throw new SerwistError("add-to-cache-list-conflicting-entries", {
1854
- firstEntry: this._urlsToCacheKeys.get(url),
1855
- secondEntry: cacheKey
1860
+ if (!(capture.startsWith("/") || capture.startsWith("http"))) {
1861
+ throw new SerwistError("invalid-string", {
1862
+ moduleName: "serwist",
1863
+ funcName: "parseRoute",
1864
+ paramName: "capture"
1856
1865
  });
1857
1866
  }
1858
- if (typeof entry !== "string" && entry.integrity) {
1859
- if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) {
1860
- throw new SerwistError("add-to-cache-list-conflicting-integrities", {
1861
- url
1862
- });
1863
- }
1864
- this._cacheKeysToIntegrities.set(cacheKey, entry.integrity);
1867
+ const valueToCheck = capture.startsWith("http") ? captureUrl.pathname : capture;
1868
+ const wildcards = "[*:?+]";
1869
+ if (new RegExp(`${wildcards}`).exec(valueToCheck)) {
1870
+ logger.debug(`The '$capture' parameter contains an Express-style wildcard character (${wildcards}). Strings are now always interpreted as exact matches; use a RegExp for partial or wildcard matches.`);
1865
1871
  }
1866
- this._urlsToCacheKeys.set(url, cacheKey);
1867
- this._urlsToCacheModes.set(url, cacheMode);
1868
- if (urlsToWarnAbout.length > 0) {
1869
- const warningMessage = `Serwist is precaching URLs without revision info: ${urlsToWarnAbout.join(", ")}\nThis is generally NOT safe, as you risk serving outdated assets.`;
1870
- if (process.env.NODE_ENV === "production") {
1871
- console.warn(warningMessage);
1872
- } else {
1873
- logger.warn(warningMessage);
1872
+ }
1873
+ const matchCallback = ({ url })=>{
1874
+ if (process.env.NODE_ENV !== "production") {
1875
+ if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) {
1876
+ logger.debug(`${capture} only partially matches the cross-origin URL ${url.toString()}. This route will only handle cross-origin requests if they match the entire URL.`);
1874
1877
  }
1875
1878
  }
1876
- }
1879
+ return url.href === captureUrl.href;
1880
+ };
1881
+ return new Route(matchCallback, handler, method);
1877
1882
  }
1878
- init(serwist) {
1879
- serwist.registerRoute(new PrecacheRoute(this, this._routeOptions));
1880
- if (this._options.navigateFallback) {
1881
- serwist.registerRoute(new NavigationRoute(this.createHandlerBoundToUrl(this._options.navigateFallback), {
1882
- allowlist: this._options.navigateFallbackAllowlist,
1883
- denylist: this._options.navigateFallbackDenylist
1883
+ if (capture instanceof RegExp) {
1884
+ return new RegExpRoute(capture, handler, method);
1885
+ }
1886
+ if (typeof capture === "function") {
1887
+ return new Route(capture, handler, method);
1888
+ }
1889
+ if (capture instanceof Route) {
1890
+ return capture;
1891
+ }
1892
+ throw new SerwistError("unsupported-route-type", {
1893
+ moduleName: "serwist",
1894
+ funcName: "parseRoute",
1895
+ paramName: "capture"
1896
+ });
1897
+ };
1898
+
1899
+ const disableDevLogs = ()=>{
1900
+ self.__WB_DISABLE_DEV_LOGS = true;
1901
+ };
1902
+
1903
+ const isNavigationPreloadSupported = ()=>{
1904
+ return Boolean(self.registration?.navigationPreload);
1905
+ };
1906
+ const enableNavigationPreload = (headerValue)=>{
1907
+ if (isNavigationPreloadSupported()) {
1908
+ self.addEventListener("activate", (event)=>{
1909
+ event.waitUntil(self.registration.navigationPreload.enable().then(()=>{
1910
+ if (headerValue) {
1911
+ void self.registration.navigationPreload.setHeaderValue(headerValue);
1912
+ }
1913
+ if (process.env.NODE_ENV !== "production") {
1914
+ logger.log("Navigation preloading is enabled.");
1915
+ }
1884
1916
  }));
1917
+ });
1918
+ } else {
1919
+ if (process.env.NODE_ENV !== "production") {
1920
+ logger.log("Navigation preloading is not supported in this browser.");
1885
1921
  }
1886
1922
  }
1887
- async install(event) {
1888
- const installReportPlugin = new PrecacheInstallReportPlugin();
1889
- this._strategy.plugins.push(installReportPlugin);
1890
- await parallel(this._options.concurrency, Array.from(this._urlsToCacheKeys.entries()), async ([url, cacheKey])=>{
1891
- const integrity = this._cacheKeysToIntegrities.get(cacheKey);
1892
- const cacheMode = this._urlsToCacheModes.get(url);
1893
- const request = new Request(url, {
1894
- integrity,
1895
- cache: cacheMode,
1896
- credentials: "same-origin"
1897
- });
1898
- await Promise.all(this._strategy.handleAll({
1899
- event,
1900
- request,
1901
- url: new URL(request.url),
1902
- params: {
1903
- cacheKey
1923
+ };
1924
+ const disableNavigationPreload = ()=>{
1925
+ if (isNavigationPreloadSupported()) {
1926
+ self.addEventListener("activate", (event)=>{
1927
+ event.waitUntil(self.registration.navigationPreload.disable().then(()=>{
1928
+ if (process.env.NODE_ENV !== "production") {
1929
+ logger.log("Navigation preloading is disabled.");
1904
1930
  }
1905
1931
  }));
1906
1932
  });
1907
- const { updatedURLs, notUpdatedURLs } = installReportPlugin;
1933
+ } else {
1908
1934
  if (process.env.NODE_ENV !== "production") {
1909
- printInstallDetails(updatedURLs, notUpdatedURLs);
1935
+ logger.log("Navigation preloading is not supported in this browser.");
1910
1936
  }
1911
1937
  }
1912
- async activate() {
1913
- const cache = await self.caches.open(this._strategy.cacheName);
1914
- const currentlyCachedRequests = await cache.keys();
1915
- const expectedCacheKeys = new Set(this._urlsToCacheKeys.values());
1916
- const deletedCacheRequests = [];
1917
- for (const request of currentlyCachedRequests){
1918
- if (!expectedCacheKeys.has(request.url)) {
1919
- await cache.delete(request);
1920
- deletedCacheRequests.push(request.url);
1921
- }
1938
+ };
1939
+
1940
+ const setCacheNameDetails = (details)=>{
1941
+ if (process.env.NODE_ENV !== "production") {
1942
+ for (const key of Object.keys(details)){
1943
+ finalAssertExports.isType(details[key], "string", {
1944
+ moduleName: "@serwist/core",
1945
+ funcName: "setCacheNameDetails",
1946
+ paramName: `details.${key}`
1947
+ });
1922
1948
  }
1923
- if (process.env.NODE_ENV !== "production") {
1924
- printCleanupDetails(deletedCacheRequests);
1949
+ if (details.precache?.length === 0) {
1950
+ throw new SerwistError("invalid-cache-name", {
1951
+ cacheNameId: "precache",
1952
+ value: details.precache
1953
+ });
1925
1954
  }
1926
- }
1927
- getUrlsToPrecacheKeys() {
1928
- return this._urlsToCacheKeys;
1929
- }
1930
- getPrecachedUrls() {
1931
- return [
1932
- ...this._urlsToCacheKeys.keys()
1933
- ];
1934
- }
1935
- getPrecacheKeyForUrl(url) {
1936
- const urlObject = new URL(url, location.href);
1937
- return this._urlsToCacheKeys.get(urlObject.href);
1938
- }
1939
- getIntegrityForPrecacheKey(cacheKey) {
1940
- return this._cacheKeysToIntegrities.get(cacheKey);
1941
- }
1942
- async matchPrecache(request) {
1943
- const url = request instanceof Request ? request.url : request;
1944
- const cacheKey = this.getPrecacheKeyForUrl(url);
1945
- if (cacheKey) {
1946
- const cache = await self.caches.open(this._strategy.cacheName);
1947
- return cache.match(cacheKey);
1955
+ if (details.runtime?.length === 0) {
1956
+ throw new SerwistError("invalid-cache-name", {
1957
+ cacheNameId: "runtime",
1958
+ value: details.runtime
1959
+ });
1948
1960
  }
1949
- return undefined;
1950
- }
1951
- createHandlerBoundToUrl(url) {
1952
- const cacheKey = this.getPrecacheKeyForUrl(url);
1953
- if (!cacheKey) {
1954
- throw new SerwistError("non-precached-url", {
1955
- url
1961
+ if (details.googleAnalytics?.length === 0) {
1962
+ throw new SerwistError("invalid-cache-name", {
1963
+ cacheNameId: "googleAnalytics",
1964
+ value: details.googleAnalytics
1956
1965
  });
1957
1966
  }
1958
- return (options)=>{
1959
- options.request = new Request(url);
1960
- options.params = {
1961
- cacheKey,
1962
- ...options.params
1963
- };
1964
- return this._strategy.handle(options);
1965
- };
1966
1967
  }
1967
- }
1968
+ cacheNames$1.updateDetails(details);
1969
+ };
1968
1970
 
1969
1971
  class Serwist {
1970
1972
  _routes;
@@ -2021,7 +2023,9 @@ class Serwist {
2021
2023
  }
2022
2024
  }
2023
2025
  for (const callback of this.iterateControllers("init")){
2024
- callback(this);
2026
+ callback({
2027
+ serwist: this
2028
+ });
2025
2029
  }
2026
2030
  if (disableDevLogs$1) disableDevLogs();
2027
2031
  }
@@ -2029,7 +2033,10 @@ class Serwist {
2029
2033
  if (!this._controllers) return;
2030
2034
  for (const controller of this._controllers){
2031
2035
  if (typeof controller[name] === "function") {
2032
- yield controller[name];
2036
+ const callback = (param)=>{
2037
+ controller[name](param);
2038
+ };
2039
+ yield callback;
2033
2040
  }
2034
2041
  }
2035
2042
  }
@@ -2051,14 +2058,20 @@ class Serwist {
2051
2058
  handleInstall(event) {
2052
2059
  return waitUntil(event, async ()=>{
2053
2060
  for (const callback of this.iterateControllers("install")){
2054
- await callback(event, this);
2061
+ await callback({
2062
+ event,
2063
+ serwist: this
2064
+ });
2055
2065
  }
2056
2066
  });
2057
2067
  }
2058
2068
  handleActivate(event) {
2059
2069
  return waitUntil(event, async ()=>{
2060
2070
  for (const callback of this.iterateControllers("activate")){
2061
- await callback(event, this);
2071
+ await callback({
2072
+ event,
2073
+ serwist: this
2074
+ });
2062
2075
  }
2063
2076
  });
2064
2077
  }
@@ -2346,6 +2359,20 @@ const cacheNames = {
2346
2359
  }
2347
2360
  };
2348
2361
 
2362
+ const registerQuotaErrorCallback = (callback)=>{
2363
+ if (process.env.NODE_ENV !== "production") {
2364
+ finalAssertExports.isType(callback, "function", {
2365
+ moduleName: "@serwist/core",
2366
+ funcName: "register",
2367
+ paramName: "callback"
2368
+ });
2369
+ }
2370
+ quotaErrorCallbacks.add(callback);
2371
+ if (process.env.NODE_ENV !== "production") {
2372
+ logger.log("Registered a callback to respond to quota errors.", callback);
2373
+ }
2374
+ };
2375
+
2349
2376
  const BROADCAST_UPDATE_MESSAGE_TYPE = "CACHE_UPDATED";
2350
2377
  const BROADCAST_UPDATE_MESSAGE_META = "serwist-broadcast-update";
2351
2378
  const BROADCAST_UPDATE_DEFAULT_NOTIFY = true;
@@ -2747,20 +2774,6 @@ class CacheExpiration {
2747
2774
  }
2748
2775
  }
2749
2776
 
2750
- const registerQuotaErrorCallback = (callback)=>{
2751
- if (process.env.NODE_ENV !== "production") {
2752
- finalAssertExports.isType(callback, "function", {
2753
- moduleName: "@serwist/core",
2754
- funcName: "register",
2755
- paramName: "callback"
2756
- });
2757
- }
2758
- quotaErrorCallbacks.add(callback);
2759
- if (process.env.NODE_ENV !== "production") {
2760
- logger.log("Registered a callback to respond to quota errors.", callback);
2761
- }
2762
- };
2763
-
2764
2777
  class ExpirationPlugin {
2765
2778
  _config;
2766
2779
  _cacheExpirations;