serwist 9.2.2 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/PrecacheRoute.d.ts.map +1 -1
  2. package/dist/RegExpRoute.d.ts +1 -1
  3. package/dist/RegExpRoute.d.ts.map +1 -1
  4. package/dist/Serwist.d.ts +2 -4
  5. package/dist/Serwist.d.ts.map +1 -1
  6. package/dist/chunks/printInstallDetails.js +1113 -1113
  7. package/dist/chunks/waitUntil.js +83 -83
  8. package/dist/index.d.ts +9 -9
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.internal.d.ts +2 -2
  11. package/dist/index.internal.d.ts.map +1 -1
  12. package/dist/index.internal.js +1 -1
  13. package/dist/index.js +1319 -1319
  14. package/dist/index.legacy.d.ts +5 -5
  15. package/dist/index.legacy.d.ts.map +1 -1
  16. package/dist/index.legacy.js +30 -31
  17. package/dist/legacy/PrecacheController.d.ts +1 -2
  18. package/dist/legacy/PrecacheController.d.ts.map +1 -1
  19. package/dist/legacy/PrecacheRoute.d.ts.map +1 -1
  20. package/dist/legacy/Router.d.ts +1 -1
  21. package/dist/legacy/Router.d.ts.map +1 -1
  22. package/dist/legacy/fallbacks.d.ts.map +1 -1
  23. package/dist/legacy/handlePrecaching.d.ts.map +1 -1
  24. package/dist/legacy/initializeGoogleAnalytics.d.ts.map +1 -1
  25. package/dist/legacy/installSerwist.d.ts +2 -2
  26. package/dist/legacy/installSerwist.d.ts.map +1 -1
  27. package/dist/legacy/registerRoute.d.ts +1 -1
  28. package/dist/legacy/registerRoute.d.ts.map +1 -1
  29. package/dist/legacy/registerRuntimeCaching.d.ts.map +1 -1
  30. package/dist/lib/googleAnalytics/initializeGoogleAnalytics.d.ts.map +1 -1
  31. package/dist/lib/strategies/NetworkFirst.d.ts.map +1 -1
  32. package/dist/lib/strategies/PrecacheStrategy.d.ts.map +1 -1
  33. package/dist/lib/strategies/StaleWhileRevalidate.d.ts.map +1 -1
  34. package/dist/lib/strategies/StrategyHandler.d.ts.map +1 -1
  35. package/dist/setCacheNameDetails.d.ts.map +1 -1
  36. package/dist/utils/createCacheKey.d.ts.map +1 -1
  37. package/dist/utils/parseRoute.d.ts +1 -1
  38. package/dist/utils/parseRoute.d.ts.map +1 -1
  39. package/package.json +4 -4
  40. package/src/PrecacheRoute.ts +1 -2
  41. package/src/RegExpRoute.ts +1 -1
  42. package/src/Serwist.ts +12 -10
  43. package/src/copyResponse.ts +1 -1
  44. package/src/index.internal.ts +2 -2
  45. package/src/index.legacy.ts +5 -5
  46. package/src/index.ts +9 -9
  47. package/src/legacy/PrecacheController.ts +3 -4
  48. package/src/legacy/PrecacheRoute.ts +1 -2
  49. package/src/legacy/Router.ts +2 -2
  50. package/src/legacy/fallbacks.ts +1 -3
  51. package/src/legacy/handlePrecaching.ts +1 -1
  52. package/src/legacy/initializeGoogleAnalytics.ts +2 -2
  53. package/src/legacy/installSerwist.ts +3 -3
  54. package/src/legacy/matchPrecache.ts +1 -1
  55. package/src/legacy/precache.ts +1 -1
  56. package/src/legacy/registerRoute.ts +4 -3
  57. package/src/legacy/registerRuntimeCaching.ts +1 -2
  58. package/src/lib/backgroundSync/BackgroundSyncQueue.ts +1 -1
  59. package/src/lib/broadcastUpdate/responsesAreSame.ts +1 -1
  60. package/src/lib/cacheableResponse/CacheableResponse.ts +1 -1
  61. package/src/lib/expiration/CacheExpiration.ts +1 -1
  62. package/src/lib/expiration/ExpirationPlugin.ts +2 -2
  63. package/src/lib/googleAnalytics/initializeGoogleAnalytics.ts +2 -2
  64. package/src/lib/rangeRequests/createPartialResponse.ts +1 -1
  65. package/src/lib/rangeRequests/utils/calculateEffectiveBoundaries.ts +1 -1
  66. package/src/lib/rangeRequests/utils/parseRangeHeader.ts +1 -1
  67. package/src/lib/strategies/CacheFirst.ts +1 -1
  68. package/src/lib/strategies/CacheOnly.ts +1 -1
  69. package/src/lib/strategies/NetworkFirst.ts +2 -2
  70. package/src/lib/strategies/NetworkOnly.ts +1 -1
  71. package/src/lib/strategies/PrecacheStrategy.ts +2 -2
  72. package/src/lib/strategies/StaleWhileRevalidate.ts +2 -2
  73. package/src/lib/strategies/Strategy.ts +1 -1
  74. package/src/lib/strategies/StrategyHandler.ts +3 -3
  75. package/src/setCacheNameDetails.ts +1 -1
  76. package/src/utils/createCacheKey.ts +1 -2
  77. package/src/utils/parseRoute.ts +2 -2
package/dist/index.js CHANGED
@@ -1,1531 +1,1531 @@
1
- import { R as Route, g as generateURLVariations, B as BackgroundSyncPlugin, N as NetworkFirst, a as NetworkOnly, P as PrecacheStrategy, e as enableNavigationPreload, s as setCacheNameDetails, b as NavigationRoute, S as Strategy, d as disableDevLogs, c as createCacheKey, f as defaultMethod, n as normalizeHandler, p as parseRoute, h as PrecacheInstallReportPlugin, i as parallel, j as printInstallDetails, k as printCleanupDetails, m as messages, l as cacheOkAndOpaquePlugin } from './chunks/printInstallDetails.js';
1
+ import { c as cacheNames$1, S as SerwistError, l as logger, f as finalAssertExports, t as timeout, g as getFriendlyURL, q as quotaErrorCallbacks, a as clientsClaim, b as cleanupOutdatedCaches, w as waitUntil } from './chunks/waitUntil.js';
2
+ import { B as BackgroundSyncPlugin, N as NetworkFirst, R as Route, a as NetworkOnly, S as Strategy, m as messages, c as cacheOkAndOpaquePlugin, g as generateURLVariations, P as PrecacheStrategy, e as enableNavigationPreload, s as setCacheNameDetails, b as NavigationRoute, d as disableDevLogs, f as createCacheKey, h as defaultMethod, n as normalizeHandler, p as parseRoute, i as PrecacheInstallReportPlugin, j as parallel, k as printInstallDetails, l as printCleanupDetails } from './chunks/printInstallDetails.js';
2
3
  export { v as BackgroundSyncQueue, w as BackgroundSyncQueueStore, u as RegExpRoute, x as StorableRequest, t as StrategyHandler, o as copyResponse, q as disableNavigationPreload, r as isNavigationPreloadSupported } from './chunks/printInstallDetails.js';
3
- import { l as logger, g as getFriendlyURL, c as cacheNames$1, a as clientsClaim, b as cleanupOutdatedCaches, f as finalAssertExports, S as SerwistError, w as waitUntil, t as timeout, q as quotaErrorCallbacks } from './chunks/waitUntil.js';
4
4
  import { r as resultingClientExists } from './chunks/resultingClientExists.js';
5
5
  import { deleteDB, openDB } from 'idb';
6
6
 
7
- class PrecacheRoute extends Route {
8
- constructor(serwist, options){
9
- const match = ({ request })=>{
10
- const urlsToCacheKeys = serwist.getUrlsToPrecacheKeys();
11
- for (const possibleURL of generateURLVariations(request.url, options)){
12
- const cacheKey = urlsToCacheKeys.get(possibleURL);
13
- if (cacheKey) {
14
- const integrity = serwist.getIntegrityForPrecacheKey(cacheKey);
15
- return {
16
- cacheKey,
17
- integrity
18
- };
19
- }
20
- }
21
- if (process.env.NODE_ENV !== "production") {
22
- logger.debug(`Precaching did not find a match for ${getFriendlyURL(request.url)}.`);
23
- }
24
- return;
25
- };
26
- super(match, serwist.precacheStrategy);
7
+ const cacheNames = {
8
+ get googleAnalytics () {
9
+ return cacheNames$1.getGoogleAnalyticsName();
10
+ },
11
+ get precache () {
12
+ return cacheNames$1.getPrecacheName();
13
+ },
14
+ get prefix () {
15
+ return cacheNames$1.getPrefix();
16
+ },
17
+ get runtime () {
18
+ return cacheNames$1.getRuntimeName();
19
+ },
20
+ get suffix () {
21
+ return cacheNames$1.getSuffix();
27
22
  }
28
- }
23
+ };
29
24
 
30
- const QUEUE_NAME = "serwist-google-analytics";
31
- const MAX_RETENTION_TIME = 60 * 48;
32
- const GOOGLE_ANALYTICS_HOST = "www.google-analytics.com";
33
- const GTM_HOST = "www.googletagmanager.com";
34
- const ANALYTICS_JS_PATH = "/analytics.js";
35
- const GTAG_JS_PATH = "/gtag/js";
36
- const GTM_JS_PATH = "/gtm.js";
37
- const COLLECT_PATHS_REGEX = /^\/(\w+\/)?collect/;
25
+ const BROADCAST_UPDATE_MESSAGE_TYPE = "CACHE_UPDATED";
26
+ const BROADCAST_UPDATE_MESSAGE_META = "serwist-broadcast-update";
27
+ const BROADCAST_UPDATE_DEFAULT_NOTIFY = true;
28
+ const BROADCAST_UPDATE_DEFAULT_HEADERS = [
29
+ "content-length",
30
+ "etag",
31
+ "last-modified"
32
+ ];
38
33
 
39
- const createOnSyncCallback = (config)=>{
40
- return async ({ queue })=>{
41
- let entry;
42
- while(entry = await queue.shiftRequest()){
43
- const { request, timestamp } = entry;
44
- const url = new URL(request.url);
45
- try {
46
- const params = request.method === "POST" ? new URLSearchParams(await request.clone().text()) : url.searchParams;
47
- const originalHitTime = timestamp - (Number(params.get("qt")) || 0);
48
- const queueTime = Date.now() - originalHitTime;
49
- params.set("qt", String(queueTime));
50
- if (config.parameterOverrides) {
51
- for (const param of Object.keys(config.parameterOverrides)){
52
- const value = config.parameterOverrides[param];
53
- params.set(param, value);
54
- }
55
- }
56
- if (typeof config.hitFilter === "function") {
57
- config.hitFilter.call(null, params);
58
- }
59
- await fetch(new Request(url.origin + url.pathname, {
60
- body: params.toString(),
61
- method: "POST",
62
- mode: "cors",
63
- credentials: "omit",
64
- headers: {
65
- "Content-Type": "text/plain"
66
- }
67
- }));
68
- if (process.env.NODE_ENV !== "production") {
69
- logger.log(`Request for '${getFriendlyURL(url.href)}' has been replayed`);
70
- }
71
- } catch (err) {
72
- await queue.unshiftRequest(entry);
73
- if (process.env.NODE_ENV !== "production") {
74
- logger.log(`Request for '${getFriendlyURL(url.href)}' failed to replay, putting it back in the queue.`);
75
- }
76
- throw err;
77
- }
34
+ const responsesAreSame = (firstResponse, secondResponse, headersToCheck)=>{
35
+ if (process.env.NODE_ENV !== "production") {
36
+ if (!(firstResponse instanceof Response && secondResponse instanceof Response)) {
37
+ throw new SerwistError("invalid-responses-are-same-args");
78
38
  }
39
+ }
40
+ const atLeastOneHeaderAvailable = headersToCheck.some((header)=>{
41
+ return firstResponse.headers.has(header) && secondResponse.headers.has(header);
42
+ });
43
+ if (!atLeastOneHeaderAvailable) {
79
44
  if (process.env.NODE_ENV !== "production") {
80
- logger.log("All Google Analytics request successfully replayed; " + "the queue is now empty!");
45
+ logger.warn("Unable to determine where the response has been updated because none of the headers that would be checked are present.");
46
+ logger.debug("Attempting to compare the following: ", firstResponse, secondResponse, headersToCheck);
81
47
  }
82
- };
83
- };
84
- const createCollectRoutes = (bgSyncPlugin)=>{
85
- const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && COLLECT_PATHS_REGEX.test(url.pathname);
86
- const handler = new NetworkOnly({
87
- plugins: [
88
- bgSyncPlugin
89
- ]
90
- });
91
- return [
92
- new Route(match, handler, "GET"),
93
- new Route(match, handler, "POST")
94
- ];
95
- };
96
- const createAnalyticsJsRoute = (cacheName)=>{
97
- const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && url.pathname === ANALYTICS_JS_PATH;
98
- const handler = new NetworkFirst({
99
- cacheName
100
- });
101
- return new Route(match, handler, "GET");
102
- };
103
- const createGtagJsRoute = (cacheName)=>{
104
- const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTAG_JS_PATH;
105
- const handler = new NetworkFirst({
106
- cacheName
107
- });
108
- return new Route(match, handler, "GET");
109
- };
110
- const createGtmJsRoute = (cacheName)=>{
111
- const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTM_JS_PATH;
112
- const handler = new NetworkFirst({
113
- cacheName
114
- });
115
- return new Route(match, handler, "GET");
116
- };
117
- const initializeGoogleAnalytics = ({ serwist, cacheName, ...options })=>{
118
- const resolvedCacheName = cacheNames$1.getGoogleAnalyticsName(cacheName);
119
- const bgSyncPlugin = new BackgroundSyncPlugin(QUEUE_NAME, {
120
- maxRetentionTime: MAX_RETENTION_TIME,
121
- onSync: createOnSyncCallback(options)
122
- });
123
- const routes = [
124
- createGtmJsRoute(resolvedCacheName),
125
- createAnalyticsJsRoute(resolvedCacheName),
126
- createGtagJsRoute(resolvedCacheName),
127
- ...createCollectRoutes(bgSyncPlugin)
128
- ];
129
- for (const route of routes){
130
- serwist.registerRoute(route);
48
+ return true;
131
49
  }
50
+ return headersToCheck.every((header)=>{
51
+ const headerStateComparison = firstResponse.headers.has(header) === secondResponse.headers.has(header);
52
+ const headerValueComparison = firstResponse.headers.get(header) === secondResponse.headers.get(header);
53
+ return headerStateComparison && headerValueComparison;
54
+ });
132
55
  };
133
56
 
134
- class PrecacheFallbackPlugin {
135
- _fallbackUrls;
136
- _serwist;
137
- constructor({ fallbackUrls, serwist }){
138
- this._fallbackUrls = fallbackUrls;
139
- this._serwist = serwist;
140
- }
141
- async handlerDidError(param) {
142
- for (const fallback of this._fallbackUrls){
143
- if (typeof fallback === "string") {
144
- const fallbackResponse = await this._serwist.matchPrecache(fallback);
145
- if (fallbackResponse !== undefined) {
146
- return fallbackResponse;
147
- }
148
- } else if (fallback.matcher(param)) {
149
- const fallbackResponse = await this._serwist.matchPrecache(fallback.url);
150
- if (fallbackResponse !== undefined) {
151
- return fallbackResponse;
152
- }
153
- }
154
- }
155
- return undefined;
156
- }
157
- }
158
-
159
- class PrecacheCacheKeyPlugin {
160
- _precacheController;
161
- constructor({ precacheController }){
162
- this._precacheController = precacheController;
163
- }
164
- cacheKeyWillBeUsed = async ({ request, params })=>{
165
- const cacheKey = params?.cacheKey || this._precacheController.getPrecacheKeyForUrl(request.url);
166
- return cacheKey ? new Request(cacheKey, {
167
- headers: request.headers
168
- }) : request;
169
- };
170
- }
171
-
172
- const parsePrecacheOptions = (serwist, precacheOptions = {})=>{
173
- const { cacheName: precacheCacheName, plugins: precachePlugins = [], fetchOptions: precacheFetchOptions, matchOptions: precacheMatchOptions, fallbackToNetwork: precacheFallbackToNetwork, directoryIndex: precacheDirectoryIndex, ignoreURLParametersMatching: precacheIgnoreUrls, cleanURLs: precacheCleanUrls, urlManipulation: precacheUrlManipulation, cleanupOutdatedCaches, concurrency = 10, navigateFallback, navigateFallbackAllowlist, navigateFallbackDenylist } = precacheOptions ?? {};
57
+ const isSafari = typeof navigator !== "undefined" && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
58
+ const defaultPayloadGenerator = (data)=>{
174
59
  return {
175
- precacheStrategyOptions: {
176
- cacheName: cacheNames$1.getPrecacheName(precacheCacheName),
177
- plugins: [
178
- ...precachePlugins,
179
- new PrecacheCacheKeyPlugin({
180
- precacheController: serwist
181
- })
182
- ],
183
- fetchOptions: precacheFetchOptions,
184
- matchOptions: precacheMatchOptions,
185
- fallbackToNetwork: precacheFallbackToNetwork
186
- },
187
- precacheRouteOptions: {
188
- directoryIndex: precacheDirectoryIndex,
189
- ignoreURLParametersMatching: precacheIgnoreUrls,
190
- cleanURLs: precacheCleanUrls,
191
- urlManipulation: precacheUrlManipulation
192
- },
193
- precacheMiscOptions: {
194
- cleanupOutdatedCaches,
195
- concurrency,
196
- navigateFallback,
197
- navigateFallbackAllowlist,
198
- navigateFallbackDenylist
199
- }
60
+ cacheName: data.cacheName,
61
+ updatedURL: data.request.url
200
62
  };
201
63
  };
202
-
203
- class Serwist {
204
- _urlsToCacheKeys = new Map();
205
- _urlsToCacheModes = new Map();
206
- _cacheKeysToIntegrities = new Map();
207
- _concurrentPrecaching;
208
- _precacheStrategy;
209
- _routes;
210
- _defaultHandlerMap;
211
- _catchHandler;
212
- _requestRules;
213
- constructor({ precacheEntries, precacheOptions, skipWaiting = false, importScripts, navigationPreload = false, cacheId, clientsClaim: clientsClaim$1 = false, runtimeCaching, offlineAnalyticsConfig, disableDevLogs: disableDevLogs$1 = false, fallbacks, requestRules } = {}){
214
- const { precacheStrategyOptions, precacheRouteOptions, precacheMiscOptions } = parsePrecacheOptions(this, precacheOptions);
215
- this._concurrentPrecaching = precacheMiscOptions.concurrency;
216
- this._precacheStrategy = new PrecacheStrategy(precacheStrategyOptions);
217
- this._routes = new Map();
218
- this._defaultHandlerMap = new Map();
219
- this._requestRules = requestRules;
220
- this.handleInstall = this.handleInstall.bind(this);
221
- this.handleActivate = this.handleActivate.bind(this);
222
- this.handleFetch = this.handleFetch.bind(this);
223
- this.handleCache = this.handleCache.bind(this);
224
- if (!!importScripts && importScripts.length > 0) self.importScripts(...importScripts);
225
- if (navigationPreload) enableNavigationPreload();
226
- if (cacheId !== undefined) {
227
- setCacheNameDetails({
228
- prefix: cacheId
229
- });
230
- }
231
- if (skipWaiting) {
232
- self.skipWaiting();
233
- } else {
234
- self.addEventListener("message", (event)=>{
235
- if (event.data && event.data.type === "SKIP_WAITING") {
236
- self.skipWaiting();
237
- }
238
- });
239
- }
240
- if (clientsClaim$1) clientsClaim();
241
- if (!!precacheEntries && precacheEntries.length > 0) {
242
- this.addToPrecacheList(precacheEntries);
243
- }
244
- if (precacheMiscOptions.cleanupOutdatedCaches) {
245
- cleanupOutdatedCaches(precacheStrategyOptions.cacheName);
246
- }
247
- this.registerRoute(new PrecacheRoute(this, precacheRouteOptions));
248
- if (precacheMiscOptions.navigateFallback) {
249
- this.registerRoute(new NavigationRoute(this.createHandlerBoundToUrl(precacheMiscOptions.navigateFallback), {
250
- allowlist: precacheMiscOptions.navigateFallbackAllowlist,
251
- denylist: precacheMiscOptions.navigateFallbackDenylist
252
- }));
253
- }
254
- if (offlineAnalyticsConfig !== undefined) {
255
- if (typeof offlineAnalyticsConfig === "boolean") {
256
- offlineAnalyticsConfig && initializeGoogleAnalytics({
257
- serwist: this
258
- });
259
- } else {
260
- initializeGoogleAnalytics({
261
- ...offlineAnalyticsConfig,
262
- serwist: this
263
- });
264
- }
265
- }
266
- if (runtimeCaching !== undefined) {
267
- if (fallbacks !== undefined) {
268
- const fallbackPlugin = new PrecacheFallbackPlugin({
269
- fallbackUrls: fallbacks.entries,
270
- serwist: this
271
- });
272
- runtimeCaching.forEach((cacheEntry)=>{
273
- if (cacheEntry.handler instanceof Strategy && !cacheEntry.handler.plugins.some((plugin)=>"handlerDidError" in plugin)) {
274
- cacheEntry.handler.plugins.push(fallbackPlugin);
275
- }
276
- });
277
- }
278
- for (const entry of runtimeCaching){
279
- this.registerCapture(entry.matcher, entry.handler, entry.method);
280
- }
281
- }
282
- if (disableDevLogs$1) disableDevLogs();
283
- }
284
- get precacheStrategy() {
285
- return this._precacheStrategy;
286
- }
287
- get routes() {
288
- return this._routes;
289
- }
290
- addEventListeners() {
291
- self.addEventListener("install", this.handleInstall);
292
- self.addEventListener("activate", this.handleActivate);
293
- self.addEventListener("fetch", this.handleFetch);
294
- self.addEventListener("message", this.handleCache);
64
+ class BroadcastCacheUpdate {
65
+ _headersToCheck;
66
+ _generatePayload;
67
+ _notifyAllClients;
68
+ constructor({ generatePayload, headersToCheck, notifyAllClients } = {}){
69
+ this._headersToCheck = headersToCheck || BROADCAST_UPDATE_DEFAULT_HEADERS;
70
+ this._generatePayload = generatePayload || defaultPayloadGenerator;
71
+ this._notifyAllClients = notifyAllClients ?? BROADCAST_UPDATE_DEFAULT_NOTIFY;
295
72
  }
296
- addToPrecacheList(entries) {
73
+ async notifyIfUpdated(options) {
297
74
  if (process.env.NODE_ENV !== "production") {
298
- finalAssertExports.isArray(entries, {
75
+ finalAssertExports.isType(options.cacheName, "string", {
299
76
  moduleName: "serwist",
300
- className: "Serwist",
301
- funcName: "addToCacheList",
302
- paramName: "entries"
77
+ className: "BroadcastCacheUpdate",
78
+ funcName: "notifyIfUpdated",
79
+ paramName: "cacheName"
303
80
  });
304
- }
305
- const urlsToWarnAbout = [];
306
- for (const entry of entries){
307
- if (typeof entry === "string") {
308
- urlsToWarnAbout.push(entry);
309
- } else if (entry && !entry.integrity && entry.revision === undefined) {
310
- urlsToWarnAbout.push(entry.url);
311
- }
312
- const { cacheKey, url } = createCacheKey(entry);
313
- const cacheMode = typeof entry !== "string" && entry.revision ? "reload" : "default";
314
- if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) {
315
- throw new SerwistError("add-to-cache-list-conflicting-entries", {
316
- firstEntry: this._urlsToCacheKeys.get(url),
317
- secondEntry: cacheKey
318
- });
319
- }
320
- if (typeof entry !== "string" && entry.integrity) {
321
- if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) {
322
- throw new SerwistError("add-to-cache-list-conflicting-integrities", {
323
- url
324
- });
325
- }
326
- this._cacheKeysToIntegrities.set(cacheKey, entry.integrity);
327
- }
328
- this._urlsToCacheKeys.set(url, cacheKey);
329
- this._urlsToCacheModes.set(url, cacheMode);
330
- }
331
- if (urlsToWarnAbout.length > 0) {
332
- const warningMessage = `Serwist is precaching URLs without revision info: ${urlsToWarnAbout.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;
333
- if (process.env.NODE_ENV === "production") {
334
- console.warn(warningMessage);
335
- } else {
336
- logger.warn(warningMessage);
337
- }
338
- }
339
- }
340
- handleInstall(event) {
341
- void this.registerRequestRules(event);
342
- return waitUntil(event, async ()=>{
343
- const installReportPlugin = new PrecacheInstallReportPlugin();
344
- this.precacheStrategy.plugins.push(installReportPlugin);
345
- await parallel(this._concurrentPrecaching, Array.from(this._urlsToCacheKeys.entries()), async ([url, cacheKey])=>{
346
- const integrity = this._cacheKeysToIntegrities.get(cacheKey);
347
- const cacheMode = this._urlsToCacheModes.get(url);
348
- const request = new Request(url, {
349
- integrity,
350
- cache: cacheMode,
351
- credentials: "same-origin"
352
- });
353
- await Promise.all(this.precacheStrategy.handleAll({
354
- event,
355
- request,
356
- url: new URL(request.url),
357
- params: {
358
- cacheKey
359
- }
360
- }));
81
+ finalAssertExports.isInstance(options.newResponse, Response, {
82
+ moduleName: "serwist",
83
+ className: "BroadcastCacheUpdate",
84
+ funcName: "notifyIfUpdated",
85
+ paramName: "newResponse"
86
+ });
87
+ finalAssertExports.isInstance(options.request, Request, {
88
+ moduleName: "serwist",
89
+ className: "BroadcastCacheUpdate",
90
+ funcName: "notifyIfUpdated",
91
+ paramName: "request"
361
92
  });
362
- const { updatedURLs, notUpdatedURLs } = installReportPlugin;
363
- if (process.env.NODE_ENV !== "production") {
364
- printInstallDetails(updatedURLs, notUpdatedURLs);
365
- }
366
- return {
367
- updatedURLs,
368
- notUpdatedURLs
369
- };
370
- });
371
- }
372
- async registerRequestRules(event) {
373
- if (!this._requestRules) {
374
- return;
375
93
  }
376
- if (!event?.addRoutes) {
377
- if (process.env.NODE_ENV !== "production") {
378
- logger.warn("Request rules ignored as the Static Routing API is not supported in this browser. " + "See https://caniuse.com/mdn-api_installevent_addroutes for more information.");
379
- }
94
+ if (!options.oldResponse) {
380
95
  return;
381
96
  }
382
- try {
383
- if (process.env.NODE_ENV !== "production") {
384
- logger.warn("Request rules may not be supported in all browsers as the Static Routing API is experimental. " + "This feature allows bypassing the service worker for specific requests to improve performance. " + "See https://developer.mozilla.org/en-US/docs/Web/API/InstallEvent/addRoutes for more information.");
385
- }
386
- await event.addRoutes(this._requestRules);
387
- this._requestRules = undefined;
388
- } catch (error) {
389
- if (process.env.NODE_ENV !== "production") {
390
- logger.error(`Failed to register request rules: ${error instanceof Error ? error.message : String(error)}. ` + "This may occur if the browser doesn't support the Static Routing API or if the request rules are invalid.");
391
- }
392
- throw error;
393
- }
394
- }
395
- handleActivate(event) {
396
- return waitUntil(event, async ()=>{
397
- const cache = await self.caches.open(this.precacheStrategy.cacheName);
398
- const currentlyCachedRequests = await cache.keys();
399
- const expectedCacheKeys = new Set(this._urlsToCacheKeys.values());
400
- const deletedCacheRequests = [];
401
- for (const request of currentlyCachedRequests){
402
- if (!expectedCacheKeys.has(request.url)) {
403
- await cache.delete(request);
404
- deletedCacheRequests.push(request.url);
405
- }
406
- }
97
+ if (!responsesAreSame(options.oldResponse, options.newResponse, this._headersToCheck)) {
407
98
  if (process.env.NODE_ENV !== "production") {
408
- printCleanupDetails(deletedCacheRequests);
99
+ logger.log("Newer response found (and cached) for:", options.request.url);
409
100
  }
410
- return {
411
- deletedCacheRequests
101
+ const messageData = {
102
+ type: BROADCAST_UPDATE_MESSAGE_TYPE,
103
+ meta: BROADCAST_UPDATE_MESSAGE_META,
104
+ payload: this._generatePayload(options)
412
105
  };
413
- });
414
- }
415
- handleFetch(event) {
416
- const { request } = event;
417
- const responsePromise = this.handleRequest({
418
- request,
419
- event
420
- });
421
- if (responsePromise) {
422
- event.respondWith(responsePromise);
423
- }
424
- }
425
- handleCache(event) {
426
- if (event.data && event.data.type === "CACHE_URLS") {
427
- const { payload } = event.data;
428
- if (process.env.NODE_ENV !== "production") {
429
- logger.debug("Caching URLs from the window", payload.urlsToCache);
430
- }
431
- const requestPromises = Promise.all(payload.urlsToCache.map((entry)=>{
432
- let request;
433
- if (typeof entry === "string") {
434
- request = new Request(entry);
435
- } else {
436
- request = new Request(...entry);
106
+ if (options.request.mode === "navigate") {
107
+ let resultingClientId;
108
+ if (options.event instanceof FetchEvent) {
109
+ resultingClientId = options.event.resultingClientId;
437
110
  }
438
- return this.handleRequest({
439
- request,
440
- event
111
+ const resultingWin = await resultingClientExists(resultingClientId);
112
+ if (!resultingWin || isSafari) {
113
+ await timeout(3500);
114
+ }
115
+ }
116
+ if (this._notifyAllClients) {
117
+ const windows = await self.clients.matchAll({
118
+ type: "window"
441
119
  });
442
- }));
443
- event.waitUntil(requestPromises);
444
- if (event.ports?.[0]) {
445
- void requestPromises.then(()=>event.ports[0].postMessage(true));
120
+ for (const win of windows){
121
+ win.postMessage(messageData);
122
+ }
123
+ } else {
124
+ if (options.event instanceof FetchEvent) {
125
+ const client = await self.clients.get(options.event.clientId);
126
+ client?.postMessage(messageData);
127
+ }
446
128
  }
447
129
  }
448
130
  }
449
- setDefaultHandler(handler, method = defaultMethod) {
450
- this._defaultHandlerMap.set(method, normalizeHandler(handler));
451
- }
452
- setCatchHandler(handler) {
453
- this._catchHandler = normalizeHandler(handler);
131
+ }
132
+
133
+ class BroadcastUpdatePlugin {
134
+ _broadcastUpdate;
135
+ constructor(options){
136
+ this._broadcastUpdate = new BroadcastCacheUpdate(options);
454
137
  }
455
- registerCapture(capture, handler, method) {
456
- const route = parseRoute(capture, handler, method);
457
- this.registerRoute(route);
458
- return route;
138
+ cacheDidUpdate(options) {
139
+ void this._broadcastUpdate.notifyIfUpdated(options);
459
140
  }
460
- registerRoute(route) {
141
+ }
142
+
143
+ class CacheableResponse {
144
+ _statuses;
145
+ _headers;
146
+ constructor(config = {}){
461
147
  if (process.env.NODE_ENV !== "production") {
462
- finalAssertExports.isType(route, "object", {
463
- moduleName: "serwist",
464
- className: "Serwist",
465
- funcName: "registerRoute",
466
- paramName: "route"
467
- });
468
- finalAssertExports.hasMethod(route, "match", {
469
- moduleName: "serwist",
470
- className: "Serwist",
471
- funcName: "registerRoute",
472
- paramName: "route"
473
- });
474
- finalAssertExports.isType(route.handler, "object", {
475
- moduleName: "serwist",
476
- className: "Serwist",
477
- funcName: "registerRoute",
478
- paramName: "route"
479
- });
480
- finalAssertExports.hasMethod(route.handler, "handle", {
481
- moduleName: "serwist",
482
- className: "Serwist",
483
- funcName: "registerRoute",
484
- paramName: "route.handler"
485
- });
486
- finalAssertExports.isType(route.method, "string", {
487
- moduleName: "serwist",
488
- className: "Serwist",
489
- funcName: "registerRoute",
490
- paramName: "route.method"
491
- });
148
+ if (!(config.statuses || config.headers)) {
149
+ throw new SerwistError("statuses-or-headers-required", {
150
+ moduleName: "serwist",
151
+ className: "CacheableResponse",
152
+ funcName: "constructor"
153
+ });
154
+ }
155
+ if (config.statuses) {
156
+ finalAssertExports.isArray(config.statuses, {
157
+ moduleName: "serwist",
158
+ className: "CacheableResponse",
159
+ funcName: "constructor",
160
+ paramName: "config.statuses"
161
+ });
162
+ }
163
+ if (config.headers) {
164
+ finalAssertExports.isType(config.headers, "object", {
165
+ moduleName: "serwist",
166
+ className: "CacheableResponse",
167
+ funcName: "constructor",
168
+ paramName: "config.headers"
169
+ });
170
+ }
492
171
  }
493
- if (!this._routes.has(route.method)) {
494
- this._routes.set(route.method, []);
172
+ this._statuses = config.statuses;
173
+ if (config.headers) {
174
+ this._headers = new Headers(config.headers);
495
175
  }
496
- this._routes.get(route.method).push(route);
497
176
  }
498
- unregisterRoute(route) {
499
- if (!this._routes.has(route.method)) {
500
- throw new SerwistError("unregister-route-but-not-found-with-method", {
501
- method: route.method
177
+ isResponseCacheable(response) {
178
+ if (process.env.NODE_ENV !== "production") {
179
+ finalAssertExports.isInstance(response, Response, {
180
+ moduleName: "serwist",
181
+ className: "CacheableResponse",
182
+ funcName: "isResponseCacheable",
183
+ paramName: "response"
502
184
  });
503
185
  }
504
- const routeIndex = this._routes.get(route.method).indexOf(route);
505
- if (routeIndex > -1) {
506
- this._routes.get(route.method).splice(routeIndex, 1);
507
- } else {
508
- throw new SerwistError("unregister-route-route-not-registered");
186
+ let cacheable = true;
187
+ if (this._statuses) {
188
+ cacheable = this._statuses.includes(response.status);
189
+ }
190
+ if (this._headers && cacheable) {
191
+ for (const [headerName, headerValue] of this._headers.entries()){
192
+ if (response.headers.get(headerName) !== headerValue) {
193
+ cacheable = false;
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ if (process.env.NODE_ENV !== "production") {
199
+ if (!cacheable) {
200
+ logger.groupCollapsed(`The request for '${getFriendlyURL(response.url)}' returned a response that does not meet the criteria for being cached.`);
201
+ logger.groupCollapsed("View cacheability criteria here.");
202
+ logger.log(`Cacheable statuses: ${JSON.stringify(this._statuses)}`);
203
+ logger.log(`Cacheable headers: ${JSON.stringify(this._headers, null, 2)}`);
204
+ logger.groupEnd();
205
+ const logFriendlyHeaders = {};
206
+ response.headers.forEach((value, key)=>{
207
+ logFriendlyHeaders[key] = value;
208
+ });
209
+ logger.groupCollapsed("View response status and headers here.");
210
+ logger.log(`Response status: ${response.status}`);
211
+ logger.log(`Response headers: ${JSON.stringify(logFriendlyHeaders, null, 2)}`);
212
+ logger.groupEnd();
213
+ logger.groupCollapsed("View full response details here.");
214
+ logger.log(response.headers);
215
+ logger.log(response);
216
+ logger.groupEnd();
217
+ logger.groupEnd();
218
+ }
509
219
  }
220
+ return cacheable;
510
221
  }
511
- getUrlsToPrecacheKeys() {
512
- return this._urlsToCacheKeys;
222
+ }
223
+
224
+ class CacheableResponsePlugin {
225
+ _cacheableResponse;
226
+ constructor(config){
227
+ this._cacheableResponse = new CacheableResponse(config);
513
228
  }
514
- getPrecachedUrls() {
515
- return [
516
- ...this._urlsToCacheKeys.keys()
517
- ];
229
+ cacheWillUpdate = async ({ response })=>{
230
+ if (this._cacheableResponse.isResponseCacheable(response)) {
231
+ return response;
232
+ }
233
+ return null;
234
+ };
235
+ }
236
+
237
+ const DB_NAME = "serwist-expiration";
238
+ const CACHE_OBJECT_STORE = "cache-entries";
239
+ const normalizeURL = (unNormalizedUrl)=>{
240
+ const url = new URL(unNormalizedUrl, location.href);
241
+ url.hash = "";
242
+ return url.href;
243
+ };
244
+ class CacheTimestampsModel {
245
+ _cacheName;
246
+ _db = null;
247
+ constructor(cacheName){
248
+ this._cacheName = cacheName;
518
249
  }
519
- getPrecacheKeyForUrl(url) {
520
- const urlObject = new URL(url, location.href);
521
- return this._urlsToCacheKeys.get(urlObject.href);
250
+ _getId(url) {
251
+ return `${this._cacheName}|${normalizeURL(url)}`;
522
252
  }
523
- getIntegrityForPrecacheKey(cacheKey) {
524
- return this._cacheKeysToIntegrities.get(cacheKey);
253
+ _upgradeDb(db) {
254
+ const objStore = db.createObjectStore(CACHE_OBJECT_STORE, {
255
+ keyPath: "id"
256
+ });
257
+ objStore.createIndex("cacheName", "cacheName", {
258
+ unique: false
259
+ });
260
+ objStore.createIndex("timestamp", "timestamp", {
261
+ unique: false
262
+ });
525
263
  }
526
- async matchPrecache(request) {
527
- const url = request instanceof Request ? request.url : request;
528
- const cacheKey = this.getPrecacheKeyForUrl(url);
529
- if (cacheKey) {
530
- const cache = await self.caches.open(this.precacheStrategy.cacheName);
531
- return cache.match(cacheKey);
264
+ _upgradeDbAndDeleteOldDbs(db) {
265
+ this._upgradeDb(db);
266
+ if (this._cacheName) {
267
+ void deleteDB(this._cacheName);
532
268
  }
533
- return undefined;
534
269
  }
535
- createHandlerBoundToUrl(url) {
536
- const cacheKey = this.getPrecacheKeyForUrl(url);
537
- if (!cacheKey) {
538
- throw new SerwistError("non-precached-url", {
539
- url
270
+ async setTimestamp(url, timestamp) {
271
+ url = normalizeURL(url);
272
+ const entry = {
273
+ id: this._getId(url),
274
+ cacheName: this._cacheName,
275
+ url,
276
+ timestamp
277
+ };
278
+ const db = await this.getDb();
279
+ const tx = db.transaction(CACHE_OBJECT_STORE, "readwrite", {
280
+ durability: "relaxed"
281
+ });
282
+ await tx.store.put(entry);
283
+ await tx.done;
284
+ }
285
+ async getTimestamp(url) {
286
+ const db = await this.getDb();
287
+ const entry = await db.get(CACHE_OBJECT_STORE, this._getId(url));
288
+ return entry?.timestamp;
289
+ }
290
+ async expireEntries(minTimestamp, maxCount) {
291
+ const db = await this.getDb();
292
+ let cursor = await db.transaction(CACHE_OBJECT_STORE, "readwrite").store.index("timestamp").openCursor(null, "prev");
293
+ const urlsDeleted = [];
294
+ let entriesNotDeletedCount = 0;
295
+ while(cursor){
296
+ const result = cursor.value;
297
+ if (result.cacheName === this._cacheName) {
298
+ if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) {
299
+ cursor.delete();
300
+ urlsDeleted.push(result.url);
301
+ } else {
302
+ entriesNotDeletedCount++;
303
+ }
304
+ }
305
+ cursor = await cursor.continue();
306
+ }
307
+ return urlsDeleted;
308
+ }
309
+ async getDb() {
310
+ if (!this._db) {
311
+ this._db = await openDB(DB_NAME, 1, {
312
+ upgrade: this._upgradeDbAndDeleteOldDbs.bind(this)
540
313
  });
541
314
  }
542
- return (options)=>{
543
- options.request = new Request(url);
544
- options.params = {
545
- cacheKey,
546
- ...options.params
547
- };
548
- return this.precacheStrategy.handle(options);
549
- };
315
+ return this._db;
550
316
  }
551
- handleRequest({ request, event }) {
317
+ }
318
+
319
+ class CacheExpiration {
320
+ _isRunning = false;
321
+ _rerunRequested = false;
322
+ _maxEntries;
323
+ _maxAgeSeconds;
324
+ _matchOptions;
325
+ _cacheName;
326
+ _timestampModel;
327
+ constructor(cacheName, config = {}){
552
328
  if (process.env.NODE_ENV !== "production") {
553
- finalAssertExports.isInstance(request, Request, {
329
+ finalAssertExports.isType(cacheName, "string", {
554
330
  moduleName: "serwist",
555
- className: "Serwist",
556
- funcName: "handleRequest",
557
- paramName: "options.request"
331
+ className: "CacheExpiration",
332
+ funcName: "constructor",
333
+ paramName: "cacheName"
558
334
  });
559
- }
560
- const url = new URL(request.url, location.href);
561
- if (!url.protocol.startsWith("http")) {
562
- if (process.env.NODE_ENV !== "production") {
563
- logger.debug("Router only supports URLs that start with 'http'.");
335
+ if (!(config.maxEntries || config.maxAgeSeconds)) {
336
+ throw new SerwistError("max-entries-or-age-required", {
337
+ moduleName: "serwist",
338
+ className: "CacheExpiration",
339
+ funcName: "constructor"
340
+ });
564
341
  }
565
- return;
566
- }
567
- const sameOrigin = url.origin === location.origin;
568
- const { params, route } = this.findMatchingRoute({
569
- event,
570
- request,
571
- sameOrigin,
572
- url
573
- });
574
- let handler = route?.handler;
575
- const debugMessages = [];
576
- if (process.env.NODE_ENV !== "production") {
577
- if (handler) {
578
- debugMessages.push([
579
- "Found a route to handle this request:",
580
- route
581
- ]);
582
- if (params) {
583
- debugMessages.push([
584
- `Passing the following params to the route's handler:`,
585
- params
586
- ]);
587
- }
342
+ if (config.maxEntries) {
343
+ finalAssertExports.isType(config.maxEntries, "number", {
344
+ moduleName: "serwist",
345
+ className: "CacheExpiration",
346
+ funcName: "constructor",
347
+ paramName: "config.maxEntries"
348
+ });
588
349
  }
589
- }
590
- const method = request.method;
591
- if (!handler && this._defaultHandlerMap.has(method)) {
592
- if (process.env.NODE_ENV !== "production") {
593
- debugMessages.push(`Failed to find a matching route. Falling back to the default handler for ${method}.`);
350
+ if (config.maxAgeSeconds) {
351
+ finalAssertExports.isType(config.maxAgeSeconds, "number", {
352
+ moduleName: "serwist",
353
+ className: "CacheExpiration",
354
+ funcName: "constructor",
355
+ paramName: "config.maxAgeSeconds"
356
+ });
594
357
  }
595
- handler = this._defaultHandlerMap.get(method);
596
358
  }
597
- if (!handler) {
598
- if (process.env.NODE_ENV !== "production") {
599
- logger.debug(`No route found for: ${getFriendlyURL(url)}`);
600
- }
359
+ this._maxEntries = config.maxEntries;
360
+ this._maxAgeSeconds = config.maxAgeSeconds;
361
+ this._matchOptions = config.matchOptions;
362
+ this._cacheName = cacheName;
363
+ this._timestampModel = new CacheTimestampsModel(cacheName);
364
+ }
365
+ async expireEntries() {
366
+ if (this._isRunning) {
367
+ this._rerunRequested = true;
601
368
  return;
602
369
  }
370
+ this._isRunning = true;
371
+ const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0;
372
+ const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries);
373
+ const cache = await self.caches.open(this._cacheName);
374
+ for (const url of urlsExpired){
375
+ await cache.delete(url, this._matchOptions);
376
+ }
603
377
  if (process.env.NODE_ENV !== "production") {
604
- logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`);
605
- for (const msg of debugMessages){
606
- if (Array.isArray(msg)) {
607
- logger.log(...msg);
608
- } else {
609
- logger.log(msg);
378
+ if (urlsExpired.length > 0) {
379
+ logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? "entry" : "entries"} and removed ` + `${urlsExpired.length === 1 ? "it" : "them"} from the ` + `'${this._cacheName}' cache.`);
380
+ logger.log(`Expired the following ${urlsExpired.length === 1 ? "URL" : "URLs"}:`);
381
+ for (const url of urlsExpired){
382
+ logger.log(` ${url}`);
610
383
  }
384
+ logger.groupEnd();
385
+ } else {
386
+ logger.debug("Cache expiration ran and found no entries to remove.");
611
387
  }
612
- logger.groupEnd();
613
388
  }
614
- let responsePromise;
615
- try {
616
- responsePromise = handler.handle({
617
- url,
618
- request,
619
- event,
620
- params
621
- });
622
- } catch (err) {
623
- responsePromise = Promise.reject(err);
389
+ this._isRunning = false;
390
+ if (this._rerunRequested) {
391
+ this._rerunRequested = false;
392
+ void this.expireEntries();
624
393
  }
625
- const catchHandler = route?.catchHandler;
626
- if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) {
627
- responsePromise = responsePromise.catch(async (err)=>{
628
- if (catchHandler) {
629
- if (process.env.NODE_ENV !== "production") {
630
- logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`);
631
- logger.error("Error thrown by:", route);
632
- logger.error(err);
633
- logger.groupEnd();
634
- }
635
- try {
636
- return await catchHandler.handle({
637
- url,
638
- request,
639
- event,
640
- params
641
- });
642
- } catch (catchErr) {
643
- if (catchErr instanceof Error) {
644
- err = catchErr;
645
- }
646
- }
647
- }
648
- if (this._catchHandler) {
649
- if (process.env.NODE_ENV !== "production") {
650
- logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to global Catch Handler.`);
651
- logger.error("Error thrown by:", route);
652
- logger.error(err);
653
- logger.groupEnd();
654
- }
655
- return this._catchHandler.handle({
656
- url,
657
- request,
658
- event
659
- });
660
- }
661
- throw err;
394
+ }
395
+ async updateTimestamp(url) {
396
+ if (process.env.NODE_ENV !== "production") {
397
+ finalAssertExports.isType(url, "string", {
398
+ moduleName: "serwist",
399
+ className: "CacheExpiration",
400
+ funcName: "updateTimestamp",
401
+ paramName: "url"
662
402
  });
663
403
  }
664
- return responsePromise;
404
+ await this._timestampModel.setTimestamp(url, Date.now());
665
405
  }
666
- findMatchingRoute({ url, sameOrigin, request, event }) {
667
- const routes = this._routes.get(request.method) || [];
668
- for (const route of routes){
669
- let params;
670
- const matchResult = route.match({
671
- url,
672
- sameOrigin,
673
- request,
674
- event
675
- });
676
- if (matchResult) {
677
- if (process.env.NODE_ENV !== "production") {
678
- if (matchResult instanceof Promise) {
679
- logger.warn(`While routing ${getFriendlyURL(url)}, an async matchCallback function was used. Please convert the following route to use a synchronous matchCallback function:`, route);
680
- }
681
- }
682
- params = matchResult;
683
- if (Array.isArray(params) && params.length === 0) {
684
- params = undefined;
685
- } else if (matchResult.constructor === Object && Object.keys(matchResult).length === 0) {
686
- params = undefined;
687
- } else if (typeof matchResult === "boolean") {
688
- params = undefined;
689
- }
690
- return {
691
- route,
692
- params
693
- };
406
+ async isURLExpired(url) {
407
+ if (!this._maxAgeSeconds) {
408
+ if (process.env.NODE_ENV !== "production") {
409
+ throw new SerwistError("expired-test-without-max-age", {
410
+ methodName: "isURLExpired",
411
+ paramName: "maxAgeSeconds"
412
+ });
694
413
  }
414
+ return false;
695
415
  }
696
- return {};
416
+ const timestamp = await this._timestampModel.getTimestamp(url);
417
+ const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000;
418
+ return timestamp !== undefined ? timestamp < expireOlderThan : true;
419
+ }
420
+ async delete() {
421
+ this._rerunRequested = false;
422
+ await this._timestampModel.expireEntries(Number.POSITIVE_INFINITY);
697
423
  }
698
424
  }
699
425
 
700
- const cacheNames = {
701
- get googleAnalytics () {
702
- return cacheNames$1.getGoogleAnalyticsName();
703
- },
704
- get precache () {
705
- return cacheNames$1.getPrecacheName();
706
- },
707
- get prefix () {
708
- return cacheNames$1.getPrefix();
709
- },
710
- get runtime () {
711
- return cacheNames$1.getRuntimeName();
712
- },
713
- get suffix () {
714
- return cacheNames$1.getSuffix();
426
+ const registerQuotaErrorCallback = (callback)=>{
427
+ if (process.env.NODE_ENV !== "production") {
428
+ finalAssertExports.isType(callback, "function", {
429
+ moduleName: "@serwist/core",
430
+ funcName: "register",
431
+ paramName: "callback"
432
+ });
433
+ }
434
+ quotaErrorCallbacks.add(callback);
435
+ if (process.env.NODE_ENV !== "production") {
436
+ logger.log("Registered a callback to respond to quota errors.", callback);
715
437
  }
716
438
  };
717
439
 
718
- const BROADCAST_UPDATE_MESSAGE_TYPE = "CACHE_UPDATED";
719
- const BROADCAST_UPDATE_MESSAGE_META = "serwist-broadcast-update";
720
- const BROADCAST_UPDATE_DEFAULT_NOTIFY = true;
721
- const BROADCAST_UPDATE_DEFAULT_HEADERS = [
722
- "content-length",
723
- "etag",
724
- "last-modified"
725
- ];
726
-
727
- const responsesAreSame = (firstResponse, secondResponse, headersToCheck)=>{
728
- if (process.env.NODE_ENV !== "production") {
729
- if (!(firstResponse instanceof Response && secondResponse instanceof Response)) {
730
- throw new SerwistError("invalid-responses-are-same-args");
440
+ class ExpirationPlugin {
441
+ _config;
442
+ _cacheExpirations;
443
+ constructor(config = {}){
444
+ if (process.env.NODE_ENV !== "production") {
445
+ if (!(config.maxEntries || config.maxAgeSeconds)) {
446
+ throw new SerwistError("max-entries-or-age-required", {
447
+ moduleName: "serwist",
448
+ className: "ExpirationPlugin",
449
+ funcName: "constructor"
450
+ });
451
+ }
452
+ if (config.maxEntries) {
453
+ finalAssertExports.isType(config.maxEntries, "number", {
454
+ moduleName: "serwist",
455
+ className: "ExpirationPlugin",
456
+ funcName: "constructor",
457
+ paramName: "config.maxEntries"
458
+ });
459
+ }
460
+ if (config.maxAgeSeconds) {
461
+ finalAssertExports.isType(config.maxAgeSeconds, "number", {
462
+ moduleName: "serwist",
463
+ className: "ExpirationPlugin",
464
+ funcName: "constructor",
465
+ paramName: "config.maxAgeSeconds"
466
+ });
467
+ }
468
+ if (config.maxAgeFrom) {
469
+ finalAssertExports.isType(config.maxAgeFrom, "string", {
470
+ moduleName: "serwist",
471
+ className: "ExpirationPlugin",
472
+ funcName: "constructor",
473
+ paramName: "config.maxAgeFrom"
474
+ });
475
+ }
476
+ }
477
+ this._config = config;
478
+ this._cacheExpirations = new Map();
479
+ if (!this._config.maxAgeFrom) {
480
+ this._config.maxAgeFrom = "last-fetched";
481
+ }
482
+ if (this._config.purgeOnQuotaError) {
483
+ registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata());
484
+ }
485
+ }
486
+ _getCacheExpiration(cacheName) {
487
+ if (cacheName === cacheNames$1.getRuntimeName()) {
488
+ throw new SerwistError("expire-custom-caches-only");
489
+ }
490
+ let cacheExpiration = this._cacheExpirations.get(cacheName);
491
+ if (!cacheExpiration) {
492
+ cacheExpiration = new CacheExpiration(cacheName, this._config);
493
+ this._cacheExpirations.set(cacheName, cacheExpiration);
494
+ }
495
+ return cacheExpiration;
496
+ }
497
+ cachedResponseWillBeUsed({ event, cacheName, request, cachedResponse }) {
498
+ if (!cachedResponse) {
499
+ return null;
500
+ }
501
+ const isFresh = this._isResponseDateFresh(cachedResponse);
502
+ const cacheExpiration = this._getCacheExpiration(cacheName);
503
+ const isMaxAgeFromLastUsed = this._config.maxAgeFrom === "last-used";
504
+ const done = (async ()=>{
505
+ if (isMaxAgeFromLastUsed) {
506
+ await cacheExpiration.updateTimestamp(request.url);
507
+ }
508
+ await cacheExpiration.expireEntries();
509
+ })();
510
+ try {
511
+ event.waitUntil(done);
512
+ } catch {
513
+ if (process.env.NODE_ENV !== "production") {
514
+ if (event instanceof FetchEvent) {
515
+ logger.warn(`Unable to ensure service worker stays alive when updating cache entry for '${getFriendlyURL(event.request.url)}'.`);
516
+ }
517
+ }
518
+ }
519
+ return isFresh ? cachedResponse : null;
520
+ }
521
+ _isResponseDateFresh(cachedResponse) {
522
+ const isMaxAgeFromLastUsed = this._config.maxAgeFrom === "last-used";
523
+ if (isMaxAgeFromLastUsed) {
524
+ return true;
731
525
  }
732
- }
733
- const atLeastOneHeaderAvailable = headersToCheck.some((header)=>{
734
- return firstResponse.headers.has(header) && secondResponse.headers.has(header);
735
- });
736
- if (!atLeastOneHeaderAvailable) {
737
- if (process.env.NODE_ENV !== "production") {
738
- logger.warn("Unable to determine where the response has been updated because none of the headers that would be checked are present.");
739
- logger.debug("Attempting to compare the following: ", firstResponse, secondResponse, headersToCheck);
526
+ const now = Date.now();
527
+ if (!this._config.maxAgeSeconds) {
528
+ return true;
740
529
  }
741
- return true;
530
+ const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
531
+ if (dateHeaderTimestamp === null) {
532
+ return true;
533
+ }
534
+ return dateHeaderTimestamp >= now - this._config.maxAgeSeconds * 1000;
742
535
  }
743
- return headersToCheck.every((header)=>{
744
- const headerStateComparison = firstResponse.headers.has(header) === secondResponse.headers.has(header);
745
- const headerValueComparison = firstResponse.headers.get(header) === secondResponse.headers.get(header);
746
- return headerStateComparison && headerValueComparison;
747
- });
748
- };
749
-
750
- const isSafari = typeof navigator !== "undefined" && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
751
- const defaultPayloadGenerator = (data)=>{
752
- return {
753
- cacheName: data.cacheName,
754
- updatedURL: data.request.url
755
- };
756
- };
757
- class BroadcastCacheUpdate {
758
- _headersToCheck;
759
- _generatePayload;
760
- _notifyAllClients;
761
- constructor({ generatePayload, headersToCheck, notifyAllClients } = {}){
762
- this._headersToCheck = headersToCheck || BROADCAST_UPDATE_DEFAULT_HEADERS;
763
- this._generatePayload = generatePayload || defaultPayloadGenerator;
764
- this._notifyAllClients = notifyAllClients ?? BROADCAST_UPDATE_DEFAULT_NOTIFY;
536
+ _getDateHeaderTimestamp(cachedResponse) {
537
+ if (!cachedResponse.headers.has("date")) {
538
+ return null;
539
+ }
540
+ const dateHeader = cachedResponse.headers.get("date");
541
+ const parsedDate = new Date(dateHeader);
542
+ const headerTime = parsedDate.getTime();
543
+ if (Number.isNaN(headerTime)) {
544
+ return null;
545
+ }
546
+ return headerTime;
765
547
  }
766
- async notifyIfUpdated(options) {
548
+ async cacheDidUpdate({ cacheName, request }) {
767
549
  if (process.env.NODE_ENV !== "production") {
768
- finalAssertExports.isType(options.cacheName, "string", {
550
+ finalAssertExports.isType(cacheName, "string", {
769
551
  moduleName: "serwist",
770
- className: "BroadcastCacheUpdate",
771
- funcName: "notifyIfUpdated",
552
+ className: "Plugin",
553
+ funcName: "cacheDidUpdate",
772
554
  paramName: "cacheName"
773
555
  });
774
- finalAssertExports.isInstance(options.newResponse, Response, {
775
- moduleName: "serwist",
776
- className: "BroadcastCacheUpdate",
777
- funcName: "notifyIfUpdated",
778
- paramName: "newResponse"
779
- });
780
- finalAssertExports.isInstance(options.request, Request, {
556
+ finalAssertExports.isInstance(request, Request, {
781
557
  moduleName: "serwist",
782
- className: "BroadcastCacheUpdate",
783
- funcName: "notifyIfUpdated",
558
+ className: "Plugin",
559
+ funcName: "cacheDidUpdate",
784
560
  paramName: "request"
785
561
  });
786
562
  }
787
- if (!options.oldResponse) {
788
- return;
563
+ const cacheExpiration = this._getCacheExpiration(cacheName);
564
+ await cacheExpiration.updateTimestamp(request.url);
565
+ await cacheExpiration.expireEntries();
566
+ }
567
+ async deleteCacheAndMetadata() {
568
+ for (const [cacheName, cacheExpiration] of this._cacheExpirations){
569
+ await self.caches.delete(cacheName);
570
+ await cacheExpiration.delete();
789
571
  }
790
- if (!responsesAreSame(options.oldResponse, options.newResponse, this._headersToCheck)) {
791
- if (process.env.NODE_ENV !== "production") {
792
- logger.log("Newer response found (and cached) for:", options.request.url);
793
- }
794
- const messageData = {
795
- type: BROADCAST_UPDATE_MESSAGE_TYPE,
796
- meta: BROADCAST_UPDATE_MESSAGE_META,
797
- payload: this._generatePayload(options)
798
- };
799
- if (options.request.mode === "navigate") {
800
- let resultingClientId;
801
- if (options.event instanceof FetchEvent) {
802
- resultingClientId = options.event.resultingClientId;
572
+ this._cacheExpirations = new Map();
573
+ }
574
+ }
575
+
576
+ const QUEUE_NAME = "serwist-google-analytics";
577
+ const MAX_RETENTION_TIME = 60 * 48;
578
+ const GOOGLE_ANALYTICS_HOST = "www.google-analytics.com";
579
+ const GTM_HOST = "www.googletagmanager.com";
580
+ const ANALYTICS_JS_PATH = "/analytics.js";
581
+ const GTAG_JS_PATH = "/gtag/js";
582
+ const GTM_JS_PATH = "/gtm.js";
583
+ const COLLECT_PATHS_REGEX = /^\/(\w+\/)?collect/;
584
+
585
+ const createOnSyncCallback = (config)=>{
586
+ return async ({ queue })=>{
587
+ let entry;
588
+ while(entry = await queue.shiftRequest()){
589
+ const { request, timestamp } = entry;
590
+ const url = new URL(request.url);
591
+ try {
592
+ const params = request.method === "POST" ? new URLSearchParams(await request.clone().text()) : url.searchParams;
593
+ const originalHitTime = timestamp - (Number(params.get("qt")) || 0);
594
+ const queueTime = Date.now() - originalHitTime;
595
+ params.set("qt", String(queueTime));
596
+ if (config.parameterOverrides) {
597
+ for (const param of Object.keys(config.parameterOverrides)){
598
+ const value = config.parameterOverrides[param];
599
+ params.set(param, value);
600
+ }
803
601
  }
804
- const resultingWin = await resultingClientExists(resultingClientId);
805
- if (!resultingWin || isSafari) {
806
- await timeout(3500);
602
+ if (typeof config.hitFilter === "function") {
603
+ config.hitFilter.call(null, params);
604
+ }
605
+ await fetch(new Request(url.origin + url.pathname, {
606
+ body: params.toString(),
607
+ method: "POST",
608
+ mode: "cors",
609
+ credentials: "omit",
610
+ headers: {
611
+ "Content-Type": "text/plain"
612
+ }
613
+ }));
614
+ if (process.env.NODE_ENV !== "production") {
615
+ logger.log(`Request for '${getFriendlyURL(url.href)}' has been replayed`);
616
+ }
617
+ } catch (err) {
618
+ await queue.unshiftRequest(entry);
619
+ if (process.env.NODE_ENV !== "production") {
620
+ logger.log(`Request for '${getFriendlyURL(url.href)}' failed to replay, putting it back in the queue.`);
807
621
  }
622
+ throw err;
808
623
  }
809
- if (this._notifyAllClients) {
810
- const windows = await self.clients.matchAll({
811
- type: "window"
812
- });
813
- for (const win of windows){
814
- win.postMessage(messageData);
624
+ }
625
+ if (process.env.NODE_ENV !== "production") {
626
+ logger.log("All Google Analytics request successfully replayed; " + "the queue is now empty!");
627
+ }
628
+ };
629
+ };
630
+ const createCollectRoutes = (bgSyncPlugin)=>{
631
+ const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && COLLECT_PATHS_REGEX.test(url.pathname);
632
+ const handler = new NetworkOnly({
633
+ plugins: [
634
+ bgSyncPlugin
635
+ ]
636
+ });
637
+ return [
638
+ new Route(match, handler, "GET"),
639
+ new Route(match, handler, "POST")
640
+ ];
641
+ };
642
+ const createAnalyticsJsRoute = (cacheName)=>{
643
+ const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && url.pathname === ANALYTICS_JS_PATH;
644
+ const handler = new NetworkFirst({
645
+ cacheName
646
+ });
647
+ return new Route(match, handler, "GET");
648
+ };
649
+ const createGtagJsRoute = (cacheName)=>{
650
+ const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTAG_JS_PATH;
651
+ const handler = new NetworkFirst({
652
+ cacheName
653
+ });
654
+ return new Route(match, handler, "GET");
655
+ };
656
+ const createGtmJsRoute = (cacheName)=>{
657
+ const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTM_JS_PATH;
658
+ const handler = new NetworkFirst({
659
+ cacheName
660
+ });
661
+ return new Route(match, handler, "GET");
662
+ };
663
+ const initializeGoogleAnalytics = ({ serwist, cacheName, ...options })=>{
664
+ const resolvedCacheName = cacheNames$1.getGoogleAnalyticsName(cacheName);
665
+ const bgSyncPlugin = new BackgroundSyncPlugin(QUEUE_NAME, {
666
+ maxRetentionTime: MAX_RETENTION_TIME,
667
+ onSync: createOnSyncCallback(options)
668
+ });
669
+ const routes = [
670
+ createGtmJsRoute(resolvedCacheName),
671
+ createAnalyticsJsRoute(resolvedCacheName),
672
+ createGtagJsRoute(resolvedCacheName),
673
+ ...createCollectRoutes(bgSyncPlugin)
674
+ ];
675
+ for (const route of routes){
676
+ serwist.registerRoute(route);
677
+ }
678
+ };
679
+
680
+ class PrecacheFallbackPlugin {
681
+ _fallbackUrls;
682
+ _serwist;
683
+ constructor({ fallbackUrls, serwist }){
684
+ this._fallbackUrls = fallbackUrls;
685
+ this._serwist = serwist;
686
+ }
687
+ async handlerDidError(param) {
688
+ for (const fallback of this._fallbackUrls){
689
+ if (typeof fallback === "string") {
690
+ const fallbackResponse = await this._serwist.matchPrecache(fallback);
691
+ if (fallbackResponse !== undefined) {
692
+ return fallbackResponse;
815
693
  }
816
- } else {
817
- if (options.event instanceof FetchEvent) {
818
- const client = await self.clients.get(options.event.clientId);
819
- client?.postMessage(messageData);
694
+ } else if (fallback.matcher(param)) {
695
+ const fallbackResponse = await this._serwist.matchPrecache(fallback.url);
696
+ if (fallbackResponse !== undefined) {
697
+ return fallbackResponse;
820
698
  }
821
699
  }
822
700
  }
701
+ return undefined;
702
+ }
703
+ }
704
+
705
+ const calculateEffectiveBoundaries = (blob, start, end)=>{
706
+ if (process.env.NODE_ENV !== "production") {
707
+ finalAssertExports.isInstance(blob, Blob, {
708
+ moduleName: "@serwist/range-requests",
709
+ funcName: "calculateEffectiveBoundaries",
710
+ paramName: "blob"
711
+ });
712
+ }
713
+ const blobSize = blob.size;
714
+ if (end && end > blobSize || start && start < 0) {
715
+ throw new SerwistError("range-not-satisfiable", {
716
+ size: blobSize,
717
+ end,
718
+ start
719
+ });
720
+ }
721
+ let effectiveStart;
722
+ let effectiveEnd;
723
+ if (start !== undefined && end !== undefined) {
724
+ effectiveStart = start;
725
+ effectiveEnd = end + 1;
726
+ } else if (start !== undefined && end === undefined) {
727
+ effectiveStart = start;
728
+ effectiveEnd = blobSize;
729
+ } else if (end !== undefined && start === undefined) {
730
+ effectiveStart = blobSize - end;
731
+ effectiveEnd = blobSize;
823
732
  }
824
- }
733
+ return {
734
+ start: effectiveStart,
735
+ end: effectiveEnd
736
+ };
737
+ };
825
738
 
826
- class BroadcastUpdatePlugin {
827
- _broadcastUpdate;
828
- constructor(options){
829
- this._broadcastUpdate = new BroadcastCacheUpdate(options);
739
+ const parseRangeHeader = (rangeHeader)=>{
740
+ if (process.env.NODE_ENV !== "production") {
741
+ finalAssertExports.isType(rangeHeader, "string", {
742
+ moduleName: "@serwist/range-requests",
743
+ funcName: "parseRangeHeader",
744
+ paramName: "rangeHeader"
745
+ });
830
746
  }
831
- cacheDidUpdate(options) {
832
- void this._broadcastUpdate.notifyIfUpdated(options);
747
+ const normalizedRangeHeader = rangeHeader.trim().toLowerCase();
748
+ if (!normalizedRangeHeader.startsWith("bytes=")) {
749
+ throw new SerwistError("unit-must-be-bytes", {
750
+ normalizedRangeHeader
751
+ });
833
752
  }
834
- }
835
-
836
- class CacheableResponse {
837
- _statuses;
838
- _headers;
839
- constructor(config = {}){
840
- if (process.env.NODE_ENV !== "production") {
841
- if (!(config.statuses || config.headers)) {
842
- throw new SerwistError("statuses-or-headers-required", {
843
- moduleName: "serwist",
844
- className: "CacheableResponse",
845
- funcName: "constructor"
846
- });
847
- }
848
- if (config.statuses) {
849
- finalAssertExports.isArray(config.statuses, {
850
- moduleName: "serwist",
851
- className: "CacheableResponse",
852
- funcName: "constructor",
853
- paramName: "config.statuses"
854
- });
855
- }
856
- if (config.headers) {
857
- finalAssertExports.isType(config.headers, "object", {
858
- moduleName: "serwist",
859
- className: "CacheableResponse",
860
- funcName: "constructor",
861
- paramName: "config.headers"
862
- });
863
- }
864
- }
865
- this._statuses = config.statuses;
866
- if (config.headers) {
867
- this._headers = new Headers(config.headers);
868
- }
753
+ if (normalizedRangeHeader.includes(",")) {
754
+ throw new SerwistError("single-range-only", {
755
+ normalizedRangeHeader
756
+ });
869
757
  }
870
- isResponseCacheable(response) {
758
+ const rangeParts = /(\d*)-(\d*)/.exec(normalizedRangeHeader);
759
+ if (!rangeParts || !(rangeParts[1] || rangeParts[2])) {
760
+ throw new SerwistError("invalid-range-values", {
761
+ normalizedRangeHeader
762
+ });
763
+ }
764
+ return {
765
+ start: rangeParts[1] === "" ? undefined : Number(rangeParts[1]),
766
+ end: rangeParts[2] === "" ? undefined : Number(rangeParts[2])
767
+ };
768
+ };
769
+
770
+ const createPartialResponse = async (request, originalResponse)=>{
771
+ try {
871
772
  if (process.env.NODE_ENV !== "production") {
872
- finalAssertExports.isInstance(response, Response, {
873
- moduleName: "serwist",
874
- className: "CacheableResponse",
875
- funcName: "isResponseCacheable",
876
- paramName: "response"
773
+ finalAssertExports.isInstance(request, Request, {
774
+ moduleName: "@serwist/range-requests",
775
+ funcName: "createPartialResponse",
776
+ paramName: "request"
777
+ });
778
+ finalAssertExports.isInstance(originalResponse, Response, {
779
+ moduleName: "@serwist/range-requests",
780
+ funcName: "createPartialResponse",
781
+ paramName: "originalResponse"
877
782
  });
878
783
  }
879
- let cacheable = true;
880
- if (this._statuses) {
881
- cacheable = this._statuses.includes(response.status);
784
+ if (originalResponse.status === 206) {
785
+ return originalResponse;
882
786
  }
883
- if (this._headers && cacheable) {
884
- for (const [headerName, headerValue] of this._headers.entries()){
885
- if (response.headers.get(headerName) !== headerValue) {
886
- cacheable = false;
887
- break;
888
- }
889
- }
787
+ const rangeHeader = request.headers.get("range");
788
+ if (!rangeHeader) {
789
+ throw new SerwistError("no-range-header");
890
790
  }
791
+ const boundaries = parseRangeHeader(rangeHeader);
792
+ const originalBlob = await originalResponse.blob();
793
+ const effectiveBoundaries = calculateEffectiveBoundaries(originalBlob, boundaries.start, boundaries.end);
794
+ const slicedBlob = originalBlob.slice(effectiveBoundaries.start, effectiveBoundaries.end);
795
+ const slicedBlobSize = slicedBlob.size;
796
+ const slicedResponse = new Response(slicedBlob, {
797
+ status: 206,
798
+ statusText: "Partial Content",
799
+ headers: originalResponse.headers
800
+ });
801
+ slicedResponse.headers.set("Content-Length", String(slicedBlobSize));
802
+ slicedResponse.headers.set("Content-Range", `bytes ${effectiveBoundaries.start}-${effectiveBoundaries.end - 1}/` + `${originalBlob.size}`);
803
+ return slicedResponse;
804
+ } catch (error) {
891
805
  if (process.env.NODE_ENV !== "production") {
892
- if (!cacheable) {
893
- logger.groupCollapsed(`The request for '${getFriendlyURL(response.url)}' returned a response that does not meet the criteria for being cached.`);
894
- logger.groupCollapsed("View cacheability criteria here.");
895
- logger.log(`Cacheable statuses: ${JSON.stringify(this._statuses)}`);
896
- logger.log(`Cacheable headers: ${JSON.stringify(this._headers, null, 2)}`);
897
- logger.groupEnd();
898
- const logFriendlyHeaders = {};
899
- response.headers.forEach((value, key)=>{
900
- logFriendlyHeaders[key] = value;
901
- });
902
- logger.groupCollapsed("View response status and headers here.");
903
- logger.log(`Response status: ${response.status}`);
904
- logger.log(`Response headers: ${JSON.stringify(logFriendlyHeaders, null, 2)}`);
905
- logger.groupEnd();
906
- logger.groupCollapsed("View full response details here.");
907
- logger.log(response.headers);
908
- logger.log(response);
909
- logger.groupEnd();
910
- logger.groupEnd();
911
- }
806
+ logger.warn("Unable to construct a partial response; returning a " + "416 Range Not Satisfiable response instead.");
807
+ logger.groupCollapsed("View details here.");
808
+ logger.log(error);
809
+ logger.log(request);
810
+ logger.log(originalResponse);
811
+ logger.groupEnd();
912
812
  }
913
- return cacheable;
813
+ return new Response("", {
814
+ status: 416,
815
+ statusText: "Range Not Satisfiable"
816
+ });
914
817
  }
915
- }
818
+ };
916
819
 
917
- class CacheableResponsePlugin {
918
- _cacheableResponse;
919
- constructor(config){
920
- this._cacheableResponse = new CacheableResponse(config);
921
- }
922
- cacheWillUpdate = async ({ response })=>{
923
- if (this._cacheableResponse.isResponseCacheable(response)) {
924
- return response;
820
+ class RangeRequestsPlugin {
821
+ cachedResponseWillBeUsed = async ({ request, cachedResponse })=>{
822
+ if (cachedResponse && request.headers.has("range")) {
823
+ return await createPartialResponse(request, cachedResponse);
925
824
  }
926
- return null;
825
+ return cachedResponse;
927
826
  };
928
827
  }
929
828
 
930
- const DB_NAME = "serwist-expiration";
931
- const CACHE_OBJECT_STORE = "cache-entries";
932
- const normalizeURL = (unNormalizedUrl)=>{
933
- const url = new URL(unNormalizedUrl, location.href);
934
- url.hash = "";
935
- return url.href;
936
- };
937
- class CacheTimestampsModel {
938
- _cacheName;
939
- _db = null;
940
- constructor(cacheName){
941
- this._cacheName = cacheName;
942
- }
943
- _getId(url) {
944
- return `${this._cacheName}|${normalizeURL(url)}`;
945
- }
946
- _upgradeDb(db) {
947
- const objStore = db.createObjectStore(CACHE_OBJECT_STORE, {
948
- keyPath: "id"
949
- });
950
- objStore.createIndex("cacheName", "cacheName", {
951
- unique: false
952
- });
953
- objStore.createIndex("timestamp", "timestamp", {
954
- unique: false
955
- });
956
- }
957
- _upgradeDbAndDeleteOldDbs(db) {
958
- this._upgradeDb(db);
959
- if (this._cacheName) {
960
- void deleteDB(this._cacheName);
829
+ class CacheFirst extends Strategy {
830
+ async _handle(request, handler) {
831
+ const logs = [];
832
+ if (process.env.NODE_ENV !== "production") {
833
+ finalAssertExports.isInstance(request, Request, {
834
+ moduleName: "serwist",
835
+ className: this.constructor.name,
836
+ funcName: "makeRequest",
837
+ paramName: "request"
838
+ });
961
839
  }
962
- }
963
- async setTimestamp(url, timestamp) {
964
- url = normalizeURL(url);
965
- const entry = {
966
- id: this._getId(url),
967
- cacheName: this._cacheName,
968
- url,
969
- timestamp
970
- };
971
- const db = await this.getDb();
972
- const tx = db.transaction(CACHE_OBJECT_STORE, "readwrite", {
973
- durability: "relaxed"
974
- });
975
- await tx.store.put(entry);
976
- await tx.done;
977
- }
978
- async getTimestamp(url) {
979
- const db = await this.getDb();
980
- const entry = await db.get(CACHE_OBJECT_STORE, this._getId(url));
981
- return entry?.timestamp;
982
- }
983
- async expireEntries(minTimestamp, maxCount) {
984
- const db = await this.getDb();
985
- let cursor = await db.transaction(CACHE_OBJECT_STORE, "readwrite").store.index("timestamp").openCursor(null, "prev");
986
- const urlsDeleted = [];
987
- let entriesNotDeletedCount = 0;
988
- while(cursor){
989
- const result = cursor.value;
990
- if (result.cacheName === this._cacheName) {
991
- if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) {
992
- cursor.delete();
993
- urlsDeleted.push(result.url);
840
+ let response = await handler.cacheMatch(request);
841
+ let error;
842
+ if (!response) {
843
+ if (process.env.NODE_ENV !== "production") {
844
+ logs.push(`No response found in the '${this.cacheName}' cache. Will respond with a network request.`);
845
+ }
846
+ try {
847
+ response = await handler.fetchAndCachePut(request);
848
+ } catch (err) {
849
+ if (err instanceof Error) {
850
+ error = err;
851
+ }
852
+ }
853
+ if (process.env.NODE_ENV !== "production") {
854
+ if (response) {
855
+ logs.push("Got response from network.");
994
856
  } else {
995
- entriesNotDeletedCount++;
857
+ logs.push("Unable to get a response from the network.");
996
858
  }
997
859
  }
998
- cursor = await cursor.continue();
860
+ } else {
861
+ if (process.env.NODE_ENV !== "production") {
862
+ logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
863
+ }
999
864
  }
1000
- return urlsDeleted;
865
+ if (process.env.NODE_ENV !== "production") {
866
+ logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
867
+ for (const log of logs){
868
+ logger.log(log);
869
+ }
870
+ messages.printFinalResponse(response);
871
+ logger.groupEnd();
872
+ }
873
+ if (!response) {
874
+ throw new SerwistError("no-response", {
875
+ url: request.url,
876
+ error
877
+ });
878
+ }
879
+ return response;
1001
880
  }
1002
- async getDb() {
1003
- if (!this._db) {
1004
- this._db = await openDB(DB_NAME, 1, {
1005
- upgrade: this._upgradeDbAndDeleteOldDbs.bind(this)
881
+ }
882
+
883
+ class CacheOnly extends Strategy {
884
+ async _handle(request, handler) {
885
+ if (process.env.NODE_ENV !== "production") {
886
+ finalAssertExports.isInstance(request, Request, {
887
+ moduleName: "serwist",
888
+ className: this.constructor.name,
889
+ funcName: "makeRequest",
890
+ paramName: "request"
1006
891
  });
1007
892
  }
1008
- return this._db;
893
+ const response = await handler.cacheMatch(request);
894
+ if (process.env.NODE_ENV !== "production") {
895
+ logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
896
+ if (response) {
897
+ logger.log(`Found a cached response in the '${this.cacheName}' cache.`);
898
+ messages.printFinalResponse(response);
899
+ } else {
900
+ logger.log(`No response found in the '${this.cacheName}' cache.`);
901
+ }
902
+ logger.groupEnd();
903
+ }
904
+ if (!response) {
905
+ throw new SerwistError("no-response", {
906
+ url: request.url
907
+ });
908
+ }
909
+ return response;
1009
910
  }
1010
911
  }
1011
912
 
1012
- class CacheExpiration {
1013
- _isRunning = false;
1014
- _rerunRequested = false;
1015
- _maxEntries;
1016
- _maxAgeSeconds;
1017
- _matchOptions;
1018
- _cacheName;
1019
- _timestampModel;
1020
- constructor(cacheName, config = {}){
913
+ class StaleWhileRevalidate extends Strategy {
914
+ constructor(options = {}){
915
+ super(options);
916
+ if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
917
+ this.plugins.unshift(cacheOkAndOpaquePlugin);
918
+ }
919
+ }
920
+ async _handle(request, handler) {
921
+ const logs = [];
1021
922
  if (process.env.NODE_ENV !== "production") {
1022
- finalAssertExports.isType(cacheName, "string", {
923
+ finalAssertExports.isInstance(request, Request, {
1023
924
  moduleName: "serwist",
1024
- className: "CacheExpiration",
1025
- funcName: "constructor",
1026
- paramName: "cacheName"
925
+ className: this.constructor.name,
926
+ funcName: "handle",
927
+ paramName: "request"
1027
928
  });
1028
- if (!(config.maxEntries || config.maxAgeSeconds)) {
1029
- throw new SerwistError("max-entries-or-age-required", {
1030
- moduleName: "serwist",
1031
- className: "CacheExpiration",
1032
- funcName: "constructor"
1033
- });
929
+ }
930
+ const fetchAndCachePromise = handler.fetchAndCachePut(request).catch(()=>{});
931
+ void handler.waitUntil(fetchAndCachePromise);
932
+ let response = await handler.cacheMatch(request);
933
+ let error;
934
+ if (response) {
935
+ if (process.env.NODE_ENV !== "production") {
936
+ logs.push(`Found a cached response in the '${this.cacheName}' cache. Will update with the network response in the background.`);
1034
937
  }
1035
- if (config.maxEntries) {
1036
- finalAssertExports.isType(config.maxEntries, "number", {
1037
- moduleName: "serwist",
1038
- className: "CacheExpiration",
1039
- funcName: "constructor",
1040
- paramName: "config.maxEntries"
1041
- });
938
+ } else {
939
+ if (process.env.NODE_ENV !== "production") {
940
+ logs.push(`No response found in the '${this.cacheName}' cache. Will wait for the network response.`);
1042
941
  }
1043
- if (config.maxAgeSeconds) {
1044
- finalAssertExports.isType(config.maxAgeSeconds, "number", {
1045
- moduleName: "serwist",
1046
- className: "CacheExpiration",
1047
- funcName: "constructor",
1048
- paramName: "config.maxAgeSeconds"
1049
- });
942
+ try {
943
+ response = await fetchAndCachePromise;
944
+ } catch (err) {
945
+ if (err instanceof Error) {
946
+ error = err;
947
+ }
1050
948
  }
1051
949
  }
1052
- this._maxEntries = config.maxEntries;
1053
- this._maxAgeSeconds = config.maxAgeSeconds;
1054
- this._matchOptions = config.matchOptions;
1055
- this._cacheName = cacheName;
1056
- this._timestampModel = new CacheTimestampsModel(cacheName);
950
+ if (process.env.NODE_ENV !== "production") {
951
+ logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
952
+ for (const log of logs){
953
+ logger.log(log);
954
+ }
955
+ messages.printFinalResponse(response);
956
+ logger.groupEnd();
957
+ }
958
+ if (!response) {
959
+ throw new SerwistError("no-response", {
960
+ url: request.url,
961
+ error
962
+ });
963
+ }
964
+ return response;
1057
965
  }
1058
- async expireEntries() {
1059
- if (this._isRunning) {
1060
- this._rerunRequested = true;
966
+ }
967
+
968
+ class PrecacheRoute extends Route {
969
+ constructor(serwist, options){
970
+ const match = ({ request })=>{
971
+ const urlsToCacheKeys = serwist.getUrlsToPrecacheKeys();
972
+ for (const possibleURL of generateURLVariations(request.url, options)){
973
+ const cacheKey = urlsToCacheKeys.get(possibleURL);
974
+ if (cacheKey) {
975
+ const integrity = serwist.getIntegrityForPrecacheKey(cacheKey);
976
+ return {
977
+ cacheKey,
978
+ integrity
979
+ };
980
+ }
981
+ }
982
+ if (process.env.NODE_ENV !== "production") {
983
+ logger.debug(`Precaching did not find a match for ${getFriendlyURL(request.url)}.`);
984
+ }
1061
985
  return;
986
+ };
987
+ super(match, serwist.precacheStrategy);
988
+ }
989
+ }
990
+
991
+ class PrecacheCacheKeyPlugin {
992
+ _precacheController;
993
+ constructor({ precacheController }){
994
+ this._precacheController = precacheController;
995
+ }
996
+ cacheKeyWillBeUsed = async ({ request, params })=>{
997
+ const cacheKey = params?.cacheKey || this._precacheController.getPrecacheKeyForUrl(request.url);
998
+ return cacheKey ? new Request(cacheKey, {
999
+ headers: request.headers
1000
+ }) : request;
1001
+ };
1002
+ }
1003
+
1004
+ const parsePrecacheOptions = (serwist, precacheOptions = {})=>{
1005
+ const { cacheName: precacheCacheName, plugins: precachePlugins = [], fetchOptions: precacheFetchOptions, matchOptions: precacheMatchOptions, fallbackToNetwork: precacheFallbackToNetwork, directoryIndex: precacheDirectoryIndex, ignoreURLParametersMatching: precacheIgnoreUrls, cleanURLs: precacheCleanUrls, urlManipulation: precacheUrlManipulation, cleanupOutdatedCaches, concurrency = 10, navigateFallback, navigateFallbackAllowlist, navigateFallbackDenylist } = precacheOptions ?? {};
1006
+ return {
1007
+ precacheStrategyOptions: {
1008
+ cacheName: cacheNames$1.getPrecacheName(precacheCacheName),
1009
+ plugins: [
1010
+ ...precachePlugins,
1011
+ new PrecacheCacheKeyPlugin({
1012
+ precacheController: serwist
1013
+ })
1014
+ ],
1015
+ fetchOptions: precacheFetchOptions,
1016
+ matchOptions: precacheMatchOptions,
1017
+ fallbackToNetwork: precacheFallbackToNetwork
1018
+ },
1019
+ precacheRouteOptions: {
1020
+ directoryIndex: precacheDirectoryIndex,
1021
+ ignoreURLParametersMatching: precacheIgnoreUrls,
1022
+ cleanURLs: precacheCleanUrls,
1023
+ urlManipulation: precacheUrlManipulation
1024
+ },
1025
+ precacheMiscOptions: {
1026
+ cleanupOutdatedCaches,
1027
+ concurrency,
1028
+ navigateFallback,
1029
+ navigateFallbackAllowlist,
1030
+ navigateFallbackDenylist
1062
1031
  }
1063
- this._isRunning = true;
1064
- const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0;
1065
- const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries);
1066
- const cache = await self.caches.open(this._cacheName);
1067
- for (const url of urlsExpired){
1068
- await cache.delete(url, this._matchOptions);
1032
+ };
1033
+ };
1034
+
1035
+ class Serwist {
1036
+ _urlsToCacheKeys = new Map();
1037
+ _urlsToCacheModes = new Map();
1038
+ _cacheKeysToIntegrities = new Map();
1039
+ _concurrentPrecaching;
1040
+ _precacheStrategy;
1041
+ _routes;
1042
+ _defaultHandlerMap;
1043
+ _catchHandler;
1044
+ _requestRules;
1045
+ constructor({ precacheEntries, precacheOptions, skipWaiting = false, importScripts, navigationPreload = false, cacheId, clientsClaim: clientsClaim$1 = false, runtimeCaching, offlineAnalyticsConfig, disableDevLogs: disableDevLogs$1 = false, fallbacks, requestRules } = {}){
1046
+ const { precacheStrategyOptions, precacheRouteOptions, precacheMiscOptions } = parsePrecacheOptions(this, precacheOptions);
1047
+ this._concurrentPrecaching = precacheMiscOptions.concurrency;
1048
+ this._precacheStrategy = new PrecacheStrategy(precacheStrategyOptions);
1049
+ this._routes = new Map();
1050
+ this._defaultHandlerMap = new Map();
1051
+ this._requestRules = requestRules;
1052
+ this.handleInstall = this.handleInstall.bind(this);
1053
+ this.handleActivate = this.handleActivate.bind(this);
1054
+ this.handleFetch = this.handleFetch.bind(this);
1055
+ this.handleCache = this.handleCache.bind(this);
1056
+ if (!!importScripts && importScripts.length > 0) self.importScripts(...importScripts);
1057
+ if (navigationPreload) enableNavigationPreload();
1058
+ if (cacheId !== undefined) {
1059
+ setCacheNameDetails({
1060
+ prefix: cacheId
1061
+ });
1069
1062
  }
1070
- if (process.env.NODE_ENV !== "production") {
1071
- if (urlsExpired.length > 0) {
1072
- logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? "entry" : "entries"} and removed ` + `${urlsExpired.length === 1 ? "it" : "them"} from the ` + `'${this._cacheName}' cache.`);
1073
- logger.log(`Expired the following ${urlsExpired.length === 1 ? "URL" : "URLs"}:`);
1074
- for (const url of urlsExpired){
1075
- logger.log(` ${url}`);
1063
+ if (skipWaiting) {
1064
+ self.skipWaiting();
1065
+ } else {
1066
+ self.addEventListener("message", (event)=>{
1067
+ if (event.data && event.data.type === "SKIP_WAITING") {
1068
+ self.skipWaiting();
1076
1069
  }
1077
- logger.groupEnd();
1078
- } else {
1079
- logger.debug("Cache expiration ran and found no entries to remove.");
1080
- }
1070
+ });
1081
1071
  }
1082
- this._isRunning = false;
1083
- if (this._rerunRequested) {
1084
- this._rerunRequested = false;
1085
- void this.expireEntries();
1072
+ if (clientsClaim$1) clientsClaim();
1073
+ if (!!precacheEntries && precacheEntries.length > 0) {
1074
+ this.addToPrecacheList(precacheEntries);
1086
1075
  }
1087
- }
1088
- async updateTimestamp(url) {
1089
- if (process.env.NODE_ENV !== "production") {
1090
- finalAssertExports.isType(url, "string", {
1091
- moduleName: "serwist",
1092
- className: "CacheExpiration",
1093
- funcName: "updateTimestamp",
1094
- paramName: "url"
1095
- });
1076
+ if (precacheMiscOptions.cleanupOutdatedCaches) {
1077
+ cleanupOutdatedCaches(precacheStrategyOptions.cacheName);
1096
1078
  }
1097
- await this._timestampModel.setTimestamp(url, Date.now());
1098
- }
1099
- async isURLExpired(url) {
1100
- if (!this._maxAgeSeconds) {
1101
- if (process.env.NODE_ENV !== "production") {
1102
- throw new SerwistError("expired-test-without-max-age", {
1103
- methodName: "isURLExpired",
1104
- paramName: "maxAgeSeconds"
1079
+ this.registerRoute(new PrecacheRoute(this, precacheRouteOptions));
1080
+ if (precacheMiscOptions.navigateFallback) {
1081
+ this.registerRoute(new NavigationRoute(this.createHandlerBoundToUrl(precacheMiscOptions.navigateFallback), {
1082
+ allowlist: precacheMiscOptions.navigateFallbackAllowlist,
1083
+ denylist: precacheMiscOptions.navigateFallbackDenylist
1084
+ }));
1085
+ }
1086
+ if (offlineAnalyticsConfig !== undefined) {
1087
+ if (typeof offlineAnalyticsConfig === "boolean") {
1088
+ offlineAnalyticsConfig && initializeGoogleAnalytics({
1089
+ serwist: this
1090
+ });
1091
+ } else {
1092
+ initializeGoogleAnalytics({
1093
+ ...offlineAnalyticsConfig,
1094
+ serwist: this
1105
1095
  });
1106
1096
  }
1107
- return false;
1108
1097
  }
1109
- const timestamp = await this._timestampModel.getTimestamp(url);
1110
- const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000;
1111
- return timestamp !== undefined ? timestamp < expireOlderThan : true;
1098
+ if (runtimeCaching !== undefined) {
1099
+ if (fallbacks !== undefined) {
1100
+ const fallbackPlugin = new PrecacheFallbackPlugin({
1101
+ fallbackUrls: fallbacks.entries,
1102
+ serwist: this
1103
+ });
1104
+ runtimeCaching.forEach((cacheEntry)=>{
1105
+ if (cacheEntry.handler instanceof Strategy && !cacheEntry.handler.plugins.some((plugin)=>"handlerDidError" in plugin)) {
1106
+ cacheEntry.handler.plugins.push(fallbackPlugin);
1107
+ }
1108
+ });
1109
+ }
1110
+ for (const entry of runtimeCaching){
1111
+ this.registerCapture(entry.matcher, entry.handler, entry.method);
1112
+ }
1113
+ }
1114
+ if (disableDevLogs$1) disableDevLogs();
1112
1115
  }
1113
- async delete() {
1114
- this._rerunRequested = false;
1115
- await this._timestampModel.expireEntries(Number.POSITIVE_INFINITY);
1116
+ get precacheStrategy() {
1117
+ return this._precacheStrategy;
1116
1118
  }
1117
- }
1118
-
1119
- const registerQuotaErrorCallback = (callback)=>{
1120
- if (process.env.NODE_ENV !== "production") {
1121
- finalAssertExports.isType(callback, "function", {
1122
- moduleName: "@serwist/core",
1123
- funcName: "register",
1124
- paramName: "callback"
1125
- });
1119
+ get routes() {
1120
+ return this._routes;
1126
1121
  }
1127
- quotaErrorCallbacks.add(callback);
1128
- if (process.env.NODE_ENV !== "production") {
1129
- logger.log("Registered a callback to respond to quota errors.", callback);
1122
+ addEventListeners() {
1123
+ self.addEventListener("install", this.handleInstall);
1124
+ self.addEventListener("activate", this.handleActivate);
1125
+ self.addEventListener("fetch", this.handleFetch);
1126
+ self.addEventListener("message", this.handleCache);
1130
1127
  }
1131
- };
1132
-
1133
- class ExpirationPlugin {
1134
- _config;
1135
- _cacheExpirations;
1136
- constructor(config = {}){
1128
+ addToPrecacheList(entries) {
1137
1129
  if (process.env.NODE_ENV !== "production") {
1138
- if (!(config.maxEntries || config.maxAgeSeconds)) {
1139
- throw new SerwistError("max-entries-or-age-required", {
1140
- moduleName: "serwist",
1141
- className: "ExpirationPlugin",
1142
- funcName: "constructor"
1143
- });
1130
+ finalAssertExports.isArray(entries, {
1131
+ moduleName: "serwist",
1132
+ className: "Serwist",
1133
+ funcName: "addToCacheList",
1134
+ paramName: "entries"
1135
+ });
1136
+ }
1137
+ const urlsToWarnAbout = [];
1138
+ for (const entry of entries){
1139
+ if (typeof entry === "string") {
1140
+ urlsToWarnAbout.push(entry);
1141
+ } else if (entry && !entry.integrity && entry.revision === undefined) {
1142
+ urlsToWarnAbout.push(entry.url);
1144
1143
  }
1145
- if (config.maxEntries) {
1146
- finalAssertExports.isType(config.maxEntries, "number", {
1147
- moduleName: "serwist",
1148
- className: "ExpirationPlugin",
1149
- funcName: "constructor",
1150
- paramName: "config.maxEntries"
1144
+ const { cacheKey, url } = createCacheKey(entry);
1145
+ const cacheMode = typeof entry !== "string" && entry.revision ? "reload" : "default";
1146
+ if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) {
1147
+ throw new SerwistError("add-to-cache-list-conflicting-entries", {
1148
+ firstEntry: this._urlsToCacheKeys.get(url),
1149
+ secondEntry: cacheKey
1151
1150
  });
1152
1151
  }
1153
- if (config.maxAgeSeconds) {
1154
- finalAssertExports.isType(config.maxAgeSeconds, "number", {
1155
- moduleName: "serwist",
1156
- className: "ExpirationPlugin",
1157
- funcName: "constructor",
1158
- paramName: "config.maxAgeSeconds"
1159
- });
1152
+ if (typeof entry !== "string" && entry.integrity) {
1153
+ if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) {
1154
+ throw new SerwistError("add-to-cache-list-conflicting-integrities", {
1155
+ url
1156
+ });
1157
+ }
1158
+ this._cacheKeysToIntegrities.set(cacheKey, entry.integrity);
1160
1159
  }
1161
- if (config.maxAgeFrom) {
1162
- finalAssertExports.isType(config.maxAgeFrom, "string", {
1163
- moduleName: "serwist",
1164
- className: "ExpirationPlugin",
1165
- funcName: "constructor",
1166
- paramName: "config.maxAgeFrom"
1160
+ this._urlsToCacheKeys.set(url, cacheKey);
1161
+ this._urlsToCacheModes.set(url, cacheMode);
1162
+ }
1163
+ if (urlsToWarnAbout.length > 0) {
1164
+ const warningMessage = `Serwist is precaching URLs without revision info: ${urlsToWarnAbout.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;
1165
+ if (process.env.NODE_ENV === "production") {
1166
+ console.warn(warningMessage);
1167
+ } else {
1168
+ logger.warn(warningMessage);
1169
+ }
1170
+ }
1171
+ }
1172
+ handleInstall(event) {
1173
+ void this.registerRequestRules(event);
1174
+ return waitUntil(event, async ()=>{
1175
+ const installReportPlugin = new PrecacheInstallReportPlugin();
1176
+ this.precacheStrategy.plugins.push(installReportPlugin);
1177
+ await parallel(this._concurrentPrecaching, Array.from(this._urlsToCacheKeys.entries()), async ([url, cacheKey])=>{
1178
+ const integrity = this._cacheKeysToIntegrities.get(cacheKey);
1179
+ const cacheMode = this._urlsToCacheModes.get(url);
1180
+ const request = new Request(url, {
1181
+ integrity,
1182
+ cache: cacheMode,
1183
+ credentials: "same-origin"
1167
1184
  });
1185
+ await Promise.all(this.precacheStrategy.handleAll({
1186
+ event,
1187
+ request,
1188
+ url: new URL(request.url),
1189
+ params: {
1190
+ cacheKey
1191
+ }
1192
+ }));
1193
+ });
1194
+ const { updatedURLs, notUpdatedURLs } = installReportPlugin;
1195
+ if (process.env.NODE_ENV !== "production") {
1196
+ printInstallDetails(updatedURLs, notUpdatedURLs);
1168
1197
  }
1198
+ return {
1199
+ updatedURLs,
1200
+ notUpdatedURLs
1201
+ };
1202
+ });
1203
+ }
1204
+ async registerRequestRules(event) {
1205
+ if (!this._requestRules) {
1206
+ return;
1169
1207
  }
1170
- this._config = config;
1171
- this._cacheExpirations = new Map();
1172
- if (!this._config.maxAgeFrom) {
1173
- this._config.maxAgeFrom = "last-fetched";
1208
+ if (!event?.addRoutes) {
1209
+ if (process.env.NODE_ENV !== "production") {
1210
+ logger.warn("Request rules ignored as the Static Routing API is not supported in this browser. " + "See https://caniuse.com/mdn-api_installevent_addroutes for more information.");
1211
+ }
1212
+ return;
1174
1213
  }
1175
- if (this._config.purgeOnQuotaError) {
1176
- registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata());
1214
+ try {
1215
+ if (process.env.NODE_ENV !== "production") {
1216
+ logger.warn("Request rules may not be supported in all browsers as the Static Routing API is experimental. " + "This feature allows bypassing the service worker for specific requests to improve performance. " + "See https://developer.mozilla.org/en-US/docs/Web/API/InstallEvent/addRoutes for more information.");
1217
+ }
1218
+ await event.addRoutes(this._requestRules);
1219
+ this._requestRules = undefined;
1220
+ } catch (error) {
1221
+ if (process.env.NODE_ENV !== "production") {
1222
+ logger.error(`Failed to register request rules: ${error instanceof Error ? error.message : String(error)}. ` + "This may occur if the browser doesn't support the Static Routing API or if the request rules are invalid.");
1223
+ }
1224
+ throw error;
1177
1225
  }
1178
1226
  }
1179
- _getCacheExpiration(cacheName) {
1180
- if (cacheName === cacheNames$1.getRuntimeName()) {
1181
- throw new SerwistError("expire-custom-caches-only");
1182
- }
1183
- let cacheExpiration = this._cacheExpirations.get(cacheName);
1184
- if (!cacheExpiration) {
1185
- cacheExpiration = new CacheExpiration(cacheName, this._config);
1186
- this._cacheExpirations.set(cacheName, cacheExpiration);
1187
- }
1188
- return cacheExpiration;
1227
+ handleActivate(event) {
1228
+ return waitUntil(event, async ()=>{
1229
+ const cache = await self.caches.open(this.precacheStrategy.cacheName);
1230
+ const currentlyCachedRequests = await cache.keys();
1231
+ const expectedCacheKeys = new Set(this._urlsToCacheKeys.values());
1232
+ const deletedCacheRequests = [];
1233
+ for (const request of currentlyCachedRequests){
1234
+ if (!expectedCacheKeys.has(request.url)) {
1235
+ await cache.delete(request);
1236
+ deletedCacheRequests.push(request.url);
1237
+ }
1238
+ }
1239
+ if (process.env.NODE_ENV !== "production") {
1240
+ printCleanupDetails(deletedCacheRequests);
1241
+ }
1242
+ return {
1243
+ deletedCacheRequests
1244
+ };
1245
+ });
1189
1246
  }
1190
- cachedResponseWillBeUsed({ event, cacheName, request, cachedResponse }) {
1191
- if (!cachedResponse) {
1192
- return null;
1247
+ handleFetch(event) {
1248
+ const { request } = event;
1249
+ const responsePromise = this.handleRequest({
1250
+ request,
1251
+ event
1252
+ });
1253
+ if (responsePromise) {
1254
+ event.respondWith(responsePromise);
1193
1255
  }
1194
- const isFresh = this._isResponseDateFresh(cachedResponse);
1195
- const cacheExpiration = this._getCacheExpiration(cacheName);
1196
- const isMaxAgeFromLastUsed = this._config.maxAgeFrom === "last-used";
1197
- const done = (async ()=>{
1198
- if (isMaxAgeFromLastUsed) {
1199
- await cacheExpiration.updateTimestamp(request.url);
1200
- }
1201
- await cacheExpiration.expireEntries();
1202
- })();
1203
- try {
1204
- event.waitUntil(done);
1205
- } catch {
1256
+ }
1257
+ handleCache(event) {
1258
+ if (event.data && event.data.type === "CACHE_URLS") {
1259
+ const { payload } = event.data;
1206
1260
  if (process.env.NODE_ENV !== "production") {
1207
- if (event instanceof FetchEvent) {
1208
- logger.warn(`Unable to ensure service worker stays alive when updating cache entry for '${getFriendlyURL(event.request.url)}'.`);
1261
+ logger.debug("Caching URLs from the window", payload.urlsToCache);
1262
+ }
1263
+ const requestPromises = Promise.all(payload.urlsToCache.map((entry)=>{
1264
+ let request;
1265
+ if (typeof entry === "string") {
1266
+ request = new Request(entry);
1267
+ } else {
1268
+ request = new Request(...entry);
1209
1269
  }
1270
+ return this.handleRequest({
1271
+ request,
1272
+ event
1273
+ });
1274
+ }));
1275
+ event.waitUntil(requestPromises);
1276
+ if (event.ports?.[0]) {
1277
+ void requestPromises.then(()=>event.ports[0].postMessage(true));
1210
1278
  }
1211
1279
  }
1212
- return isFresh ? cachedResponse : null;
1213
1280
  }
1214
- _isResponseDateFresh(cachedResponse) {
1215
- const isMaxAgeFromLastUsed = this._config.maxAgeFrom === "last-used";
1216
- if (isMaxAgeFromLastUsed) {
1217
- return true;
1218
- }
1219
- const now = Date.now();
1220
- if (!this._config.maxAgeSeconds) {
1221
- return true;
1222
- }
1223
- const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
1224
- if (dateHeaderTimestamp === null) {
1225
- return true;
1226
- }
1227
- return dateHeaderTimestamp >= now - this._config.maxAgeSeconds * 1000;
1281
+ setDefaultHandler(handler, method = defaultMethod) {
1282
+ this._defaultHandlerMap.set(method, normalizeHandler(handler));
1228
1283
  }
1229
- _getDateHeaderTimestamp(cachedResponse) {
1230
- if (!cachedResponse.headers.has("date")) {
1231
- return null;
1232
- }
1233
- const dateHeader = cachedResponse.headers.get("date");
1234
- const parsedDate = new Date(dateHeader);
1235
- const headerTime = parsedDate.getTime();
1236
- if (Number.isNaN(headerTime)) {
1237
- return null;
1238
- }
1239
- return headerTime;
1284
+ setCatchHandler(handler) {
1285
+ this._catchHandler = normalizeHandler(handler);
1240
1286
  }
1241
- async cacheDidUpdate({ cacheName, request }) {
1287
+ registerCapture(capture, handler, method) {
1288
+ const route = parseRoute(capture, handler, method);
1289
+ this.registerRoute(route);
1290
+ return route;
1291
+ }
1292
+ registerRoute(route) {
1242
1293
  if (process.env.NODE_ENV !== "production") {
1243
- finalAssertExports.isType(cacheName, "string", {
1294
+ finalAssertExports.isType(route, "object", {
1244
1295
  moduleName: "serwist",
1245
- className: "Plugin",
1246
- funcName: "cacheDidUpdate",
1247
- paramName: "cacheName"
1296
+ className: "Serwist",
1297
+ funcName: "registerRoute",
1298
+ paramName: "route"
1248
1299
  });
1249
- finalAssertExports.isInstance(request, Request, {
1300
+ finalAssertExports.hasMethod(route, "match", {
1250
1301
  moduleName: "serwist",
1251
- className: "Plugin",
1252
- funcName: "cacheDidUpdate",
1253
- paramName: "request"
1302
+ className: "Serwist",
1303
+ funcName: "registerRoute",
1304
+ paramName: "route"
1305
+ });
1306
+ finalAssertExports.isType(route.handler, "object", {
1307
+ moduleName: "serwist",
1308
+ className: "Serwist",
1309
+ funcName: "registerRoute",
1310
+ paramName: "route"
1311
+ });
1312
+ finalAssertExports.hasMethod(route.handler, "handle", {
1313
+ moduleName: "serwist",
1314
+ className: "Serwist",
1315
+ funcName: "registerRoute",
1316
+ paramName: "route.handler"
1317
+ });
1318
+ finalAssertExports.isType(route.method, "string", {
1319
+ moduleName: "serwist",
1320
+ className: "Serwist",
1321
+ funcName: "registerRoute",
1322
+ paramName: "route.method"
1254
1323
  });
1255
1324
  }
1256
- const cacheExpiration = this._getCacheExpiration(cacheName);
1257
- await cacheExpiration.updateTimestamp(request.url);
1258
- await cacheExpiration.expireEntries();
1259
- }
1260
- async deleteCacheAndMetadata() {
1261
- for (const [cacheName, cacheExpiration] of this._cacheExpirations){
1262
- await self.caches.delete(cacheName);
1263
- await cacheExpiration.delete();
1325
+ if (!this._routes.has(route.method)) {
1326
+ this._routes.set(route.method, []);
1264
1327
  }
1265
- this._cacheExpirations = new Map();
1266
- }
1267
- }
1268
-
1269
- const calculateEffectiveBoundaries = (blob, start, end)=>{
1270
- if (process.env.NODE_ENV !== "production") {
1271
- finalAssertExports.isInstance(blob, Blob, {
1272
- moduleName: "@serwist/range-requests",
1273
- funcName: "calculateEffectiveBoundaries",
1274
- paramName: "blob"
1275
- });
1276
- }
1277
- const blobSize = blob.size;
1278
- if (end && end > blobSize || start && start < 0) {
1279
- throw new SerwistError("range-not-satisfiable", {
1280
- size: blobSize,
1281
- end,
1282
- start
1283
- });
1328
+ this._routes.get(route.method).push(route);
1284
1329
  }
1285
- let effectiveStart;
1286
- let effectiveEnd;
1287
- if (start !== undefined && end !== undefined) {
1288
- effectiveStart = start;
1289
- effectiveEnd = end + 1;
1290
- } else if (start !== undefined && end === undefined) {
1291
- effectiveStart = start;
1292
- effectiveEnd = blobSize;
1293
- } else if (end !== undefined && start === undefined) {
1294
- effectiveStart = blobSize - end;
1295
- effectiveEnd = blobSize;
1330
+ unregisterRoute(route) {
1331
+ if (!this._routes.has(route.method)) {
1332
+ throw new SerwistError("unregister-route-but-not-found-with-method", {
1333
+ method: route.method
1334
+ });
1335
+ }
1336
+ const routeIndex = this._routes.get(route.method).indexOf(route);
1337
+ if (routeIndex > -1) {
1338
+ this._routes.get(route.method).splice(routeIndex, 1);
1339
+ } else {
1340
+ throw new SerwistError("unregister-route-route-not-registered");
1341
+ }
1296
1342
  }
1297
- return {
1298
- start: effectiveStart,
1299
- end: effectiveEnd
1300
- };
1301
- };
1302
-
1303
- const parseRangeHeader = (rangeHeader)=>{
1304
- if (process.env.NODE_ENV !== "production") {
1305
- finalAssertExports.isType(rangeHeader, "string", {
1306
- moduleName: "@serwist/range-requests",
1307
- funcName: "parseRangeHeader",
1308
- paramName: "rangeHeader"
1309
- });
1343
+ getUrlsToPrecacheKeys() {
1344
+ return this._urlsToCacheKeys;
1310
1345
  }
1311
- const normalizedRangeHeader = rangeHeader.trim().toLowerCase();
1312
- if (!normalizedRangeHeader.startsWith("bytes=")) {
1313
- throw new SerwistError("unit-must-be-bytes", {
1314
- normalizedRangeHeader
1315
- });
1346
+ getPrecachedUrls() {
1347
+ return [
1348
+ ...this._urlsToCacheKeys.keys()
1349
+ ];
1316
1350
  }
1317
- if (normalizedRangeHeader.includes(",")) {
1318
- throw new SerwistError("single-range-only", {
1319
- normalizedRangeHeader
1320
- });
1351
+ getPrecacheKeyForUrl(url) {
1352
+ const urlObject = new URL(url, location.href);
1353
+ return this._urlsToCacheKeys.get(urlObject.href);
1321
1354
  }
1322
- const rangeParts = /(\d*)-(\d*)/.exec(normalizedRangeHeader);
1323
- if (!rangeParts || !(rangeParts[1] || rangeParts[2])) {
1324
- throw new SerwistError("invalid-range-values", {
1325
- normalizedRangeHeader
1326
- });
1355
+ getIntegrityForPrecacheKey(cacheKey) {
1356
+ return this._cacheKeysToIntegrities.get(cacheKey);
1327
1357
  }
1328
- return {
1329
- start: rangeParts[1] === "" ? undefined : Number(rangeParts[1]),
1330
- end: rangeParts[2] === "" ? undefined : Number(rangeParts[2])
1331
- };
1332
- };
1333
-
1334
- const createPartialResponse = async (request, originalResponse)=>{
1335
- try {
1336
- if (process.env.NODE_ENV !== "production") {
1337
- finalAssertExports.isInstance(request, Request, {
1338
- moduleName: "@serwist/range-requests",
1339
- funcName: "createPartialResponse",
1340
- paramName: "request"
1341
- });
1342
- finalAssertExports.isInstance(originalResponse, Response, {
1343
- moduleName: "@serwist/range-requests",
1344
- funcName: "createPartialResponse",
1345
- paramName: "originalResponse"
1346
- });
1347
- }
1348
- if (originalResponse.status === 206) {
1349
- return originalResponse;
1350
- }
1351
- const rangeHeader = request.headers.get("range");
1352
- if (!rangeHeader) {
1353
- throw new SerwistError("no-range-header");
1354
- }
1355
- const boundaries = parseRangeHeader(rangeHeader);
1356
- const originalBlob = await originalResponse.blob();
1357
- const effectiveBoundaries = calculateEffectiveBoundaries(originalBlob, boundaries.start, boundaries.end);
1358
- const slicedBlob = originalBlob.slice(effectiveBoundaries.start, effectiveBoundaries.end);
1359
- const slicedBlobSize = slicedBlob.size;
1360
- const slicedResponse = new Response(slicedBlob, {
1361
- status: 206,
1362
- statusText: "Partial Content",
1363
- headers: originalResponse.headers
1364
- });
1365
- slicedResponse.headers.set("Content-Length", String(slicedBlobSize));
1366
- slicedResponse.headers.set("Content-Range", `bytes ${effectiveBoundaries.start}-${effectiveBoundaries.end - 1}/` + `${originalBlob.size}`);
1367
- return slicedResponse;
1368
- } catch (error) {
1369
- if (process.env.NODE_ENV !== "production") {
1370
- logger.warn("Unable to construct a partial response; returning a " + "416 Range Not Satisfiable response instead.");
1371
- logger.groupCollapsed("View details here.");
1372
- logger.log(error);
1373
- logger.log(request);
1374
- logger.log(originalResponse);
1375
- logger.groupEnd();
1376
- }
1377
- return new Response("", {
1378
- status: 416,
1379
- statusText: "Range Not Satisfiable"
1380
- });
1358
+ async matchPrecache(request) {
1359
+ const url = request instanceof Request ? request.url : request;
1360
+ const cacheKey = this.getPrecacheKeyForUrl(url);
1361
+ if (cacheKey) {
1362
+ const cache = await self.caches.open(this.precacheStrategy.cacheName);
1363
+ return cache.match(cacheKey);
1364
+ }
1365
+ return undefined;
1381
1366
  }
1382
- };
1383
-
1384
- class RangeRequestsPlugin {
1385
- cachedResponseWillBeUsed = async ({ request, cachedResponse })=>{
1386
- if (cachedResponse && request.headers.has("range")) {
1387
- return await createPartialResponse(request, cachedResponse);
1367
+ createHandlerBoundToUrl(url) {
1368
+ const cacheKey = this.getPrecacheKeyForUrl(url);
1369
+ if (!cacheKey) {
1370
+ throw new SerwistError("non-precached-url", {
1371
+ url
1372
+ });
1388
1373
  }
1389
- return cachedResponse;
1390
- };
1391
- }
1392
-
1393
- class CacheFirst extends Strategy {
1394
- async _handle(request, handler) {
1395
- const logs = [];
1374
+ return (options)=>{
1375
+ options.request = new Request(url);
1376
+ options.params = {
1377
+ cacheKey,
1378
+ ...options.params
1379
+ };
1380
+ return this.precacheStrategy.handle(options);
1381
+ };
1382
+ }
1383
+ handleRequest({ request, event }) {
1396
1384
  if (process.env.NODE_ENV !== "production") {
1397
1385
  finalAssertExports.isInstance(request, Request, {
1398
1386
  moduleName: "serwist",
1399
- className: this.constructor.name,
1400
- funcName: "makeRequest",
1401
- paramName: "request"
1387
+ className: "Serwist",
1388
+ funcName: "handleRequest",
1389
+ paramName: "options.request"
1402
1390
  });
1403
1391
  }
1404
- let response = await handler.cacheMatch(request);
1405
- let error;
1406
- if (!response) {
1392
+ const url = new URL(request.url, location.href);
1393
+ if (!url.protocol.startsWith("http")) {
1407
1394
  if (process.env.NODE_ENV !== "production") {
1408
- logs.push(`No response found in the '${this.cacheName}' cache. Will respond with a network request.`);
1395
+ logger.debug("Router only supports URLs that start with 'http'.");
1409
1396
  }
1410
- try {
1411
- response = await handler.fetchAndCachePut(request);
1412
- } catch (err) {
1413
- if (err instanceof Error) {
1414
- error = err;
1397
+ return;
1398
+ }
1399
+ const sameOrigin = url.origin === location.origin;
1400
+ const { params, route } = this.findMatchingRoute({
1401
+ event,
1402
+ request,
1403
+ sameOrigin,
1404
+ url
1405
+ });
1406
+ let handler = route?.handler;
1407
+ const debugMessages = [];
1408
+ if (process.env.NODE_ENV !== "production") {
1409
+ if (handler) {
1410
+ debugMessages.push([
1411
+ "Found a route to handle this request:",
1412
+ route
1413
+ ]);
1414
+ if (params) {
1415
+ debugMessages.push([
1416
+ `Passing the following params to the route's handler:`,
1417
+ params
1418
+ ]);
1415
1419
  }
1416
1420
  }
1421
+ }
1422
+ const method = request.method;
1423
+ if (!handler && this._defaultHandlerMap.has(method)) {
1417
1424
  if (process.env.NODE_ENV !== "production") {
1418
- if (response) {
1419
- logs.push("Got response from network.");
1420
- } else {
1421
- logs.push("Unable to get a response from the network.");
1422
- }
1425
+ debugMessages.push(`Failed to find a matching route. Falling back to the default handler for ${method}.`);
1423
1426
  }
1424
- } else {
1427
+ handler = this._defaultHandlerMap.get(method);
1428
+ }
1429
+ if (!handler) {
1425
1430
  if (process.env.NODE_ENV !== "production") {
1426
- logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
1431
+ logger.debug(`No route found for: ${getFriendlyURL(url)}`);
1427
1432
  }
1433
+ return;
1428
1434
  }
1429
1435
  if (process.env.NODE_ENV !== "production") {
1430
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
1431
- for (const log of logs){
1432
- logger.log(log);
1436
+ logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`);
1437
+ for (const msg of debugMessages){
1438
+ if (Array.isArray(msg)) {
1439
+ logger.log(...msg);
1440
+ } else {
1441
+ logger.log(msg);
1442
+ }
1433
1443
  }
1434
- messages.printFinalResponse(response);
1435
1444
  logger.groupEnd();
1436
1445
  }
1437
- if (!response) {
1438
- throw new SerwistError("no-response", {
1439
- url: request.url,
1440
- error
1441
- });
1442
- }
1443
- return response;
1444
- }
1445
- }
1446
-
1447
- class CacheOnly extends Strategy {
1448
- async _handle(request, handler) {
1449
- if (process.env.NODE_ENV !== "production") {
1450
- finalAssertExports.isInstance(request, Request, {
1451
- moduleName: "serwist",
1452
- className: this.constructor.name,
1453
- funcName: "makeRequest",
1454
- paramName: "request"
1446
+ let responsePromise;
1447
+ try {
1448
+ responsePromise = handler.handle({
1449
+ url,
1450
+ request,
1451
+ event,
1452
+ params
1455
1453
  });
1454
+ } catch (err) {
1455
+ responsePromise = Promise.reject(err);
1456
1456
  }
1457
- const response = await handler.cacheMatch(request);
1458
- if (process.env.NODE_ENV !== "production") {
1459
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
1460
- if (response) {
1461
- logger.log(`Found a cached response in the '${this.cacheName}' cache.`);
1462
- messages.printFinalResponse(response);
1463
- } else {
1464
- logger.log(`No response found in the '${this.cacheName}' cache.`);
1465
- }
1466
- logger.groupEnd();
1467
- }
1468
- if (!response) {
1469
- throw new SerwistError("no-response", {
1470
- url: request.url
1457
+ const catchHandler = route?.catchHandler;
1458
+ if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) {
1459
+ responsePromise = responsePromise.catch(async (err)=>{
1460
+ if (catchHandler) {
1461
+ if (process.env.NODE_ENV !== "production") {
1462
+ logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`);
1463
+ logger.error("Error thrown by:", route);
1464
+ logger.error(err);
1465
+ logger.groupEnd();
1466
+ }
1467
+ try {
1468
+ return await catchHandler.handle({
1469
+ url,
1470
+ request,
1471
+ event,
1472
+ params
1473
+ });
1474
+ } catch (catchErr) {
1475
+ if (catchErr instanceof Error) {
1476
+ err = catchErr;
1477
+ }
1478
+ }
1479
+ }
1480
+ if (this._catchHandler) {
1481
+ if (process.env.NODE_ENV !== "production") {
1482
+ logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to global Catch Handler.`);
1483
+ logger.error("Error thrown by:", route);
1484
+ logger.error(err);
1485
+ logger.groupEnd();
1486
+ }
1487
+ return this._catchHandler.handle({
1488
+ url,
1489
+ request,
1490
+ event
1491
+ });
1492
+ }
1493
+ throw err;
1471
1494
  });
1472
1495
  }
1473
- return response;
1474
- }
1475
- }
1476
-
1477
- class StaleWhileRevalidate extends Strategy {
1478
- constructor(options = {}){
1479
- super(options);
1480
- if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
1481
- this.plugins.unshift(cacheOkAndOpaquePlugin);
1482
- }
1496
+ return responsePromise;
1483
1497
  }
1484
- async _handle(request, handler) {
1485
- const logs = [];
1486
- if (process.env.NODE_ENV !== "production") {
1487
- finalAssertExports.isInstance(request, Request, {
1488
- moduleName: "serwist",
1489
- className: this.constructor.name,
1490
- funcName: "handle",
1491
- paramName: "request"
1498
+ findMatchingRoute({ url, sameOrigin, request, event }) {
1499
+ const routes = this._routes.get(request.method) || [];
1500
+ for (const route of routes){
1501
+ let params;
1502
+ const matchResult = route.match({
1503
+ url,
1504
+ sameOrigin,
1505
+ request,
1506
+ event
1492
1507
  });
1493
- }
1494
- const fetchAndCachePromise = handler.fetchAndCachePut(request).catch(()=>{});
1495
- void handler.waitUntil(fetchAndCachePromise);
1496
- let response = await handler.cacheMatch(request);
1497
- let error;
1498
- if (response) {
1499
- if (process.env.NODE_ENV !== "production") {
1500
- logs.push(`Found a cached response in the '${this.cacheName}' cache. Will update with the network response in the background.`);
1501
- }
1502
- } else {
1503
- if (process.env.NODE_ENV !== "production") {
1504
- logs.push(`No response found in the '${this.cacheName}' cache. Will wait for the network response.`);
1505
- }
1506
- try {
1507
- response = await fetchAndCachePromise;
1508
- } catch (err) {
1509
- if (err instanceof Error) {
1510
- error = err;
1508
+ if (matchResult) {
1509
+ if (process.env.NODE_ENV !== "production") {
1510
+ if (matchResult instanceof Promise) {
1511
+ logger.warn(`While routing ${getFriendlyURL(url)}, an async matchCallback function was used. Please convert the following route to use a synchronous matchCallback function:`, route);
1512
+ }
1511
1513
  }
1514
+ params = matchResult;
1515
+ if (Array.isArray(params) && params.length === 0) {
1516
+ params = undefined;
1517
+ } else if (matchResult.constructor === Object && Object.keys(matchResult).length === 0) {
1518
+ params = undefined;
1519
+ } else if (typeof matchResult === "boolean") {
1520
+ params = undefined;
1521
+ }
1522
+ return {
1523
+ route,
1524
+ params
1525
+ };
1512
1526
  }
1513
1527
  }
1514
- if (process.env.NODE_ENV !== "production") {
1515
- logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
1516
- for (const log of logs){
1517
- logger.log(log);
1518
- }
1519
- messages.printFinalResponse(response);
1520
- logger.groupEnd();
1521
- }
1522
- if (!response) {
1523
- throw new SerwistError("no-response", {
1524
- url: request.url,
1525
- error
1526
- });
1527
- }
1528
- return response;
1528
+ return {};
1529
1529
  }
1530
1530
  }
1531
1531