@swoff/cli 0.3.6 → 0.3.8

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 (109) hide show
  1. package/README.md +43 -326
  2. package/bin/swoff +8 -14
  3. package/dist/__tests__/assemble-sw.test.js +6 -6
  4. package/dist/__tests__/assemble-sw.test.js.map +1 -1
  5. package/dist/__tests__/cli-integration.test.js +0 -1
  6. package/dist/__tests__/cli-integration.test.js.map +1 -1
  7. package/dist/__tests__/config-types.test.js +9 -5
  8. package/dist/__tests__/config-types.test.js.map +1 -1
  9. package/dist/__tests__/file-generators.test.js +12 -23
  10. package/dist/__tests__/file-generators.test.js.map +1 -1
  11. package/dist/__tests__/loader.test.js +0 -1
  12. package/dist/__tests__/loader.test.js.map +1 -1
  13. package/dist/__tests__/validator.test.js +50 -2
  14. package/dist/__tests__/validator.test.js.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib/commands/add.js +7 -6
  18. package/dist/lib/commands/add.js.map +1 -1
  19. package/dist/lib/commands/generate-guide.js +36 -9
  20. package/dist/lib/commands/generate-guide.js.map +1 -1
  21. package/dist/lib/commands/info.js +45 -8
  22. package/dist/lib/commands/info.js.map +1 -1
  23. package/dist/lib/commands/init.js +6 -2
  24. package/dist/lib/commands/init.js.map +1 -1
  25. package/dist/lib/config/validator.js +85 -4
  26. package/dist/lib/config/validator.js.map +1 -1
  27. package/dist/lib/generators/file-generators/auth-fetch.js +3 -3
  28. package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
  29. package/dist/lib/generators/file-generators/auth-store.js +114 -5
  30. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  31. package/dist/lib/generators/file-generators/auth-user.js +2 -2
  32. package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
  33. package/dist/lib/generators/file-generators/cache.js +1 -3
  34. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  35. package/dist/lib/generators/file-generators/client-injector.js +5 -17
  36. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  37. package/dist/lib/generators/file-generators/fetch-wrapper.js +86 -20
  38. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  39. package/dist/lib/generators/file-generators/generate-hooks.js +28 -210
  40. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  41. package/dist/lib/generators/file-generators/gql-wrapper.js +152 -0
  42. package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
  43. package/dist/lib/generators/file-generators/guide-generator.js +357 -36
  44. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  45. package/dist/lib/generators/file-generators/mutation-queue.js +130 -34
  46. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  47. package/dist/lib/generators/file-generators/mutation-state.js +126 -0
  48. package/dist/lib/generators/file-generators/mutation-state.js.map +1 -0
  49. package/dist/lib/generators/file-generators/push.js +143 -0
  50. package/dist/lib/generators/file-generators/push.js.map +1 -0
  51. package/dist/lib/generators/file-generators/quick-readme.js +71 -11
  52. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
  53. package/dist/lib/generators/file-generators/server-push.js +136 -0
  54. package/dist/lib/generators/file-generators/server-push.js.map +1 -0
  55. package/dist/lib/generators/file-generators/sw-generator-build.js +1 -1
  56. package/dist/lib/generators/file-generators/sw-injector.js +1 -1
  57. package/dist/lib/generators/file-generators/sw-push.js +67 -0
  58. package/dist/lib/generators/file-generators/sw-push.js.map +1 -0
  59. package/dist/lib/generators/file-generators/sw-template.js +2 -0
  60. package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
  61. package/dist/lib/generators/file-generators/type-definitions.js +39 -11
  62. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
  63. package/dist/lib/generators/sw-generator.js +8 -9
  64. package/dist/lib/generators/sw-generator.js.map +1 -1
  65. package/dist/lib/generators/sw-sections/activate-handler.js +6 -2
  66. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  67. package/dist/lib/generators/sw-sections/assemble-sw.js +11 -2
  68. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  69. package/dist/lib/generators/sw-sections/background-sync-handler.js +22 -8
  70. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
  71. package/dist/lib/generators/sw-sections/config-header.js +1 -1
  72. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  73. package/dist/lib/generators/sw-sections/default-template.js +2 -30
  74. package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
  75. package/dist/lib/generators/sw-sections/fetch-handler.js +230 -120
  76. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  77. package/dist/lib/generators/sw-sections/install-handler.js +12 -10
  78. package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
  79. package/dist/lib/generators/sw-sections/server-push-handler.js +114 -0
  80. package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -0
  81. package/dist/lib/generators/sw-sections/tag-management.js +17 -5
  82. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  83. package/dist/lib/generators/swoff-files-generator.js +16 -10
  84. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  85. package/dist/lib/shared/config-types.js +44 -4
  86. package/dist/lib/shared/config-types.js.map +1 -1
  87. package/package.json +3 -2
  88. package/templates/hooks/useAuth.jsx +30 -0
  89. package/templates/hooks/useAuth.tsx +30 -0
  90. package/templates/hooks/useBackgroundSync.jsx +31 -0
  91. package/templates/hooks/useBackgroundSync.tsx +31 -0
  92. package/templates/hooks/useCacheInvalidation.jsx +19 -0
  93. package/templates/hooks/useCacheInvalidation.tsx +19 -0
  94. package/templates/hooks/useCachedFetch.jsx +94 -0
  95. package/templates/hooks/useCachedFetch.tsx +112 -0
  96. package/templates/hooks/useMutation.jsx +76 -0
  97. package/templates/hooks/useMutation.tsx +97 -0
  98. package/templates/hooks/useMutationQueue.jsx +42 -0
  99. package/templates/hooks/useMutationQueue.tsx +49 -0
  100. package/templates/hooks/useMutationState.jsx +27 -0
  101. package/templates/hooks/useMutationState.tsx +41 -0
  102. package/templates/hooks/useNetworkStatus.jsx +19 -0
  103. package/templates/hooks/useNetworkStatus.tsx +19 -0
  104. package/templates/hooks/usePrefetch.jsx +10 -0
  105. package/templates/hooks/usePrefetch.tsx +25 -0
  106. package/templates/hooks/usePushSubscription.jsx +67 -0
  107. package/templates/hooks/usePushSubscription.tsx +67 -0
  108. package/templates/hooks/useSWUpdate.jsx +76 -0
  109. package/templates/hooks/useSWUpdate.tsx +76 -0
@@ -1 +1 @@
1
- {"version":3,"file":"default-template.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/default-template.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,kBAAkB;IAChC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2CP,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"default-template.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/default-template.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,kBAAkB;IAChC,OAAO;;;;;;;;;;;;;;;2BAekB,CAAC;AAC5B,CAAC"}
@@ -1,211 +1,287 @@
1
1
  /**
2
2
  * Generates the SW fetch event handler with all caching strategies.
3
+ *
4
+ * Strategy dispatch tiers (highest to lowest priority):
5
+ * 1. X-SW-Strategy header — per-request override from fetchWithCache({ strategy })
6
+ * 2. URL pattern match — from features.serviceWorker.strategies in swoff.config.json
7
+ * 3. defaultStrategy — fallback from features.serviceWorker.defaultStrategy
8
+ *
9
+ * Each tier also carries: staleTime, maxCacheEntries, maxCacheAge
10
+ * Per-request values override route values, which override globals.
11
+ *
12
+ * With staleTime enabled, every strategy gains a "fresh" window:
13
+ * - cache-first: returns cached immediately, background refresh if stale
14
+ * - network-first: skips network if fresh, tries network if stale
15
+ * - stale-while-revalidate: skips refresh if fresh, refreshes if stale
16
+ * - cache-only: background refresh if stale (best effort)
17
+ * - network-only: unaffected (never caches)
3
18
  */
4
19
  export function generateFetchHandler(swConfig, tagInvalidation) {
5
- const { defaultStrategy, strategies, maxCacheEntries, maxCacheAge, navigationMode, spaEntry } = swConfig;
6
- const hasTrim = (maxCacheEntries ?? 0) > 0 || (maxCacheAge ?? 0) > 0;
20
+ const { defaultStrategy, strategies, cacheStrategy = "all", staleTime, maxCacheEntries, maxCacheAge, navigationPreload, navigationMode, spaEntry } = swConfig;
7
21
  const navMode = navigationMode ?? "spa";
8
22
  const spaPath = spaEntry ?? "/index.html";
9
- const navCode = navMode === "spa" ? `
10
- if (request.mode === "navigate") {
11
- const precache = await caches.open(CACHE_NAME);
12
- const entry = await precache.match("${spaPath}");
13
- if (entry) return entry;
14
- }` : "";
15
- const tagInvalidationCode = tagInvalidation ? `
16
- const tagsHeader = event.request.headers.get("X-SW-Cache-Tags");
17
- if (tagsHeader) {
18
- const url = new URL(event.request.url).href;
19
- const tags = tagsHeader.split(",").map((t) => t.trim());
20
- await cacheTagUrl(url, tags);
21
- }` : "";
22
23
  const staleVersionCode = tagInvalidation ? `
23
- if (staleVersions.has(request.url)) {
24
- event.waitUntil(refreshCache(runtimeCache, request).then(() => staleVersions.delete(request.url)));
24
+ cleanStaleVersions();
25
+ if (staleVersions.has(cacheKey(request))) {
26
+ event.waitUntil(refreshCache(request).then(() => staleVersions.delete(cacheKey(request))));
25
27
  }` : "";
26
- const staleTagCode = tagInvalidation ? `
27
- const tagsHeader = request.headers.get("X-SW-Cache-Tags");
28
- if (tagsHeader) {
29
- const url = new URL(request.url).href;
30
- const tags = tagsHeader.split(",").map((t) => t.trim());
31
- await cacheTagUrl(url, tags);
32
- }` : "";
33
- const trimCode = hasTrim ? ` await trimRuntimeCache(CACHE_NAME_RUNTIME);\n` : "";
34
- const trimFunction = hasTrim ? `
35
- const MAX_CACHE_ENTRIES = ${maxCacheEntries ?? 0};
36
- const MAX_CACHE_AGE = ${maxCacheAge ?? 0};
37
-
38
- async function trimRuntimeCache(cacheName) {
39
- const cache = await caches.open(cacheName);
40
-
41
- if (MAX_CACHE_ENTRIES > 0) {
42
- const keys = await cache.keys();
43
- if (keys.length >= MAX_CACHE_ENTRIES) {
44
- const toDelete = keys.slice(0, keys.length - MAX_CACHE_ENTRIES + 1);
45
- await Promise.all(toDelete.map((key) => cache.delete(key)));
46
- }
47
- }
28
+ const tagCode = tagInvalidation ? `
29
+ const tagsHeader = request.headers.get("X-SW-Cache-Tags");
30
+ if (tagsHeader) {
31
+ const cacheKeyUrl = cacheKey(request);
32
+ const actualUrl = new URL(request.url).href;
33
+ const tags = tagsHeader.split(",").map((t) => t.trim());
34
+ await cacheTagUrl(cacheKeyUrl, actualUrl, tags);
35
+ }` : "";
36
+ const trimDecl = `const GLOBAL_MAX_ENTRIES = ${maxCacheEntries ?? 0};
37
+ const GLOBAL_MAX_AGE = ${maxCacheAge ?? 0};
38
+ const GLOBAL_STALE_TIME = ${staleTime ?? 0};`;
39
+ return `${trimDecl}
40
+ // --- Cache Key ---
48
41
 
49
- if (MAX_CACHE_AGE > 0) {
50
- const keys = await cache.keys();
51
- const now = Date.now();
52
- for (const request of keys) {
53
- const response = await cache.match(request);
54
- const dateHeader = response?.headers.get("date");
55
- if (dateHeader) {
56
- const age = now - new Date(dateHeader).getTime();
57
- if (age > MAX_CACHE_AGE) {
58
- await cache.delete(request);
59
- }
60
- }
61
- }
62
- }
42
+ function cacheKey(request) {
43
+ const key = request.headers.get("X-SW-Cache-Key");
44
+ if (key) return new URL("/__swc/" + key, self.location.origin).href;
45
+ return new URL(request.url).href;
63
46
  }
64
- ` : "";
65
- return `${trimFunction}
47
+
48
+ // --- Cache Helpers ---
49
+
66
50
  async function fromPrecache(request) {
67
51
  const cache = await caches.open(CACHE_NAME);
68
- return cache.match(request);
52
+ return cache.match(new URL(request.url).pathname);
69
53
  }
70
54
 
71
- function markFromCache(response) {
55
+ async function fromRuntime(request) {
56
+ const cache = await caches.open(CACHE_NAME_RUNTIME);
57
+ return cache.match(cacheKey(request));
58
+ }
59
+
60
+ async function storeRuntime(request, response) {
61
+ const cache = await caches.open(CACHE_NAME_RUNTIME);
72
62
  const headers = new Headers(response.headers);
73
- headers.set("X-SW-From-Cache", "true");
74
- return new Response(response.body, {
63
+ headers.set("X-SW-Cached-At", String(Date.now()));
64
+ const cloned = new Response(response.body, {
75
65
  status: response.status,
76
66
  statusText: response.statusText,
77
67
  headers,
78
68
  });
69
+ await cache.put(cacheKey(request), cloned);
79
70
  }
80
71
 
81
- function isReadRequest(request) {
82
- return request.headers.get("X-SW-Cache-Strategy") === "read";
72
+ async function cacheResponse(request, response) {
73
+ await storeRuntime(request, response);${tagCode}
83
74
  }
84
75
 
85
- function determineCacheStrategy(request, customStrategies, defaultStrategy) {
86
- const path = new URL(request.url).pathname;
87
- for (const [pattern, strategy] of Object.entries(customStrategies)) {
88
- if (path.startsWith(pattern.replace("*", ""))) return strategy;
76
+ async function fromSpaFallback(request) {
77
+ if (request.mode === "navigate") {
78
+ const cache = await caches.open(CACHE_NAME);
79
+ return cache.match("${spaPath}");
89
80
  }
90
- return defaultStrategy;
91
81
  }
92
82
 
93
- self.addEventListener("fetch", (event) => {
94
- if (!isReadRequest(event.request)) return;
83
+ // --- Response Helpers ---
95
84
 
96
- const strategy = determineCacheStrategy(event.request, ${JSON.stringify(strategies)}, "${defaultStrategy}");
85
+ function markFromCache(response) {
86
+ const headers = new Headers(response.headers);
87
+ headers.set("X-SW-From-Cache", "true");
88
+ return new Response(response.body, {
89
+ status: response.status,
90
+ statusText: response.statusText,
91
+ headers,
92
+ });
93
+ }
97
94
 
98
- if (strategy === "stale-while-revalidate" || event.request.headers.get("X-SW-Stale") === "true") {
99
- event.respondWith(staleWhileRevalidate(event, event.request));
100
- return;
101
- }
95
+ function isStale(response, staleTimeSeconds) {
96
+ if (!staleTimeSeconds || staleTimeSeconds <= 0) return false;
97
+ const cachedAt = response.headers.get("X-SW-Cached-At");
98
+ if (!cachedAt) return false;
99
+ return Date.now() - Number(cachedAt) > staleTimeSeconds * 1000;
100
+ }
102
101
 
103
- if (strategy === "network-first") {
104
- event.respondWith(networkFirst(event, event.request));
105
- return;
106
- }
102
+ // --- Strategy Selection (3-tier config resolution) ---
103
+
104
+ function resolveStrategyEntry(entry) {
105
+ return typeof entry === "string" ? { strategy: entry } : entry;
106
+ }
107
107
 
108
- if (strategy === "cache-only") {
109
- event.respondWith(cacheOnly(event, event.request));
110
- return;
108
+ function determineCacheStrategy(request, customStrategies, globalDefaults) {
109
+ const override = request.headers.get("X-SW-Strategy");
110
+ if (override) {
111
+ return {
112
+ strategy: override,
113
+ staleTime: Number(request.headers.get("X-SW-Stale-Time")) || globalDefaults.staleTime,
114
+ maxCacheEntries: Number(request.headers.get("X-SW-Max-Entries")) || globalDefaults.maxCacheEntries,
115
+ maxCacheAge: Number(request.headers.get("X-SW-Max-Age")) || globalDefaults.maxCacheAge,
116
+ };
117
+ }
118
+ const path = new URL(request.url).pathname;
119
+ for (const [pattern, entry] of Object.entries(customStrategies)) {
120
+ if (path.startsWith(pattern.replace("*", ""))) {
121
+ const resolved = resolveStrategyEntry(entry);
122
+ return {
123
+ strategy: resolved.strategy,
124
+ staleTime: resolved.staleTime ?? globalDefaults.staleTime,
125
+ maxCacheEntries: resolved.maxCacheEntries ?? globalDefaults.maxCacheEntries,
126
+ maxCacheAge: resolved.maxCacheAge ?? globalDefaults.maxCacheAge,
127
+ };
128
+ }
111
129
  }
130
+ return {
131
+ strategy: globalDefaults.defaultStrategy,
132
+ staleTime: globalDefaults.staleTime,
133
+ maxCacheEntries: globalDefaults.maxCacheEntries,
134
+ maxCacheAge: globalDefaults.maxCacheAge,
135
+ };
136
+ }
112
137
 
113
- if (strategy === "network-only") {
114
- event.respondWith(networkOnly(event, event.request));
115
- return;
138
+ function applyStrategy(event, request, config) {
139
+ const { strategy, staleTime, maxCacheEntries, maxCacheAge } = config;
140
+ if (strategy === "stale-while-revalidate") {
141
+ event.respondWith(staleWhileRevalidate(event, request, staleTime, maxCacheEntries, maxCacheAge));
142
+ } else if (strategy === "network-first") {
143
+ event.respondWith(networkFirst(event, request, staleTime, maxCacheEntries, maxCacheAge));
144
+ } else if (strategy === "cache-only") {
145
+ event.respondWith(cacheOnly(event, request, staleTime, maxCacheEntries, maxCacheAge));
146
+ } else if (strategy === "network-only") {
147
+ event.respondWith(networkOnly(event, request));
148
+ } else {
149
+ event.respondWith(cacheFirst(event, request, staleTime, maxCacheEntries, maxCacheAge));
116
150
  }
151
+ }
117
152
 
118
- // cache-first (default)
119
- event.respondWith(cacheFirst(event, event.request));
153
+ self.addEventListener("fetch", (event) => {
154
+ const { request } = event;
155
+ if (request.method !== "GET" && request.method !== "HEAD") {
156
+ if (!request.headers.get("X-SW-Cache-Key")) return;
157
+ }
158
+ ${cacheStrategy === "explicit-only" ? `if (!request.headers.get("X-SW-Cache-Strategy")) return;` : ""}
159
+ applyStrategy(event, request, determineCacheStrategy(event.request, ${JSON.stringify(strategies)}, { defaultStrategy: "${defaultStrategy}", staleTime: GLOBAL_STALE_TIME, maxCacheEntries: GLOBAL_MAX_ENTRIES, maxCacheAge: GLOBAL_MAX_AGE }));
120
160
  });
121
161
 
122
- async function cacheFirst(event, request) {
123
- const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
162
+ // --- Strategies ---
163
+
164
+ ${navigationPreload ? `
165
+ async function fetchWithPreload(event, request) {
166
+ try {
167
+ const preload = await event.preloadResponse;
168
+ if (preload) return preload;
169
+ } catch {}
170
+ return fetch(request);
171
+ }
172
+ ` : ""}const _fetch = ${navigationPreload ? "fetchWithPreload" : `(event, request) => fetch(request)`};
124
173
 
125
- const cached = await runtimeCache.match(request);
174
+ ${generateTrimCode(maxCacheEntries, maxCacheAge)}
175
+ function _trim(cacheName, maxEntries, maxAge) {
176
+ ${maxCacheEntries || maxCacheAge ? ` trimRuntimeCache(cacheName, maxEntries, maxAge);` : ""}
177
+ }
178
+
179
+ async function cacheFirst(event, request, staleTime, maxEntries, maxAge) {
180
+ const cached = await fromRuntime(request);
126
181
  if (cached) {${staleVersionCode}
182
+ if (isStale(cached, staleTime)) {
183
+ event.waitUntil(refreshCache(request));
184
+ }
127
185
  return markFromCache(cached);
128
186
  }
129
187
 
130
188
  const precached = await fromPrecache(request);
131
189
  if (precached) return markFromCache(precached);
132
- ${navCode}
133
- const response = await fetch(request);
190
+
191
+ const fallback = await fromSpaFallback(request);
192
+ if (fallback) return fallback;
193
+
194
+ const response = await _fetch(event, request);
134
195
  if (response.ok) {
135
- const cloned = response.clone();
136
196
  event.waitUntil(
137
197
  (async () => {
138
- await runtimeCache.put(request, cloned);${tagInvalidationCode}
139
- ${trimCode} })(),
198
+ await cacheResponse(request, response);
199
+ _trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
200
+ })(),
140
201
  );
141
202
  }
142
203
  return response;
143
204
  }
144
205
 
145
- async function networkFirst(event, request) {
146
- const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
206
+ async function networkFirst(event, request, staleTime, maxEntries, maxAge) {
207
+ // If cached and fresh (within staleTime), skip network entirely
208
+ if (staleTime > 0) {
209
+ const cached = await fromRuntime(request);
210
+ if (cached && !isStale(cached, staleTime)) {
211
+ return markFromCache(cached);
212
+ }
213
+ }
147
214
 
148
215
  try {
149
- const response = await fetch(request);
216
+ const response = await _fetch(event, request);
150
217
  if (response.ok) {
151
- const cloned = response.clone();
152
218
  event.waitUntil(
153
219
  (async () => {
154
- await runtimeCache.put(request, cloned);${tagInvalidationCode}
155
- ${trimCode} })(),
220
+ await cacheResponse(request, response);
221
+ _trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
222
+ })(),
156
223
  );
157
224
  }
158
225
  return response;
159
226
  } catch {
160
- const cached = await runtimeCache.match(request);
227
+ const cached = await fromRuntime(request);
161
228
  if (cached) return cached;
162
229
 
163
230
  const precached = await fromPrecache(request);
164
231
  if (precached) return precached;
165
- ${navCode}
232
+
233
+ const fallback = await fromSpaFallback(request);
234
+ if (fallback) return fallback;
235
+
166
236
  throw new Error("Request failed and no cached response available");
167
237
  }
168
238
  }
169
239
 
170
- async function staleWhileRevalidate(event, request) {
171
- const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
172
- const cached = await runtimeCache.match(request);
173
-
240
+ async function staleWhileRevalidate(event, request, staleTime, maxEntries, maxAge) {
241
+ const cached = await fromRuntime(request);
174
242
  if (cached) {
175
- event.waitUntil(refreshCache(runtimeCache, request));
243
+ // Skip background refresh if still fresh
244
+ if (!isStale(cached, staleTime)) {
245
+ return markFromCache(cached);
246
+ }
247
+ event.waitUntil(refreshCache(request));
176
248
  return markFromCache(cached);
177
249
  }
178
250
 
179
251
  const precached = await fromPrecache(request);
180
252
  if (precached) {
181
- event.waitUntil(refreshCache(runtimeCache, request));
253
+ event.waitUntil(refreshCache(request));
182
254
  return markFromCache(precached);
183
255
  }
184
256
 
185
- const response = await fetch(request);
257
+ const response = await _fetch(event, request);
186
258
  if (response.ok) {
187
- await runtimeCache.put(request, response.clone());${staleTagCode}
188
- ${hasTrim ? " await trimRuntimeCache(CACHE_NAME_RUNTIME);\n" : ""} }
259
+ await cacheResponse(request, response);
260
+ _trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
261
+ }
189
262
  return response;
190
263
  }
191
264
 
192
- async function refreshCache(cache, request) {
265
+ async function refreshCache(request) {
193
266
  try {
194
- const response = await fetch(request);
267
+ const key = request.headers.get("X-SW-Cache-Key");
268
+ const fetchRequest = key ? new Request(new URL(request.url).href, { method: request.method, headers: request.headers, body: request.method !== "GET" ? request.body : undefined }) : request;
269
+ const response = await fetch(fetchRequest);
195
270
  if (response.ok) {
196
- await cache.put(request, response.clone());
271
+ await storeRuntime(request, response);
197
272
  }
198
273
  } catch {
199
274
  // Background refresh failed - stale cache remains usable
200
275
  }
201
276
  }
202
277
 
203
- async function cacheOnly(event, request) {
204
- const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
205
-
206
- const byRequest = await runtimeCache.match(request);
207
- if (byRequest) {${staleVersionCode}
208
- return markFromCache(byRequest);
278
+ async function cacheOnly(event, request, staleTime, maxEntries, maxAge) {
279
+ const cached = await fromRuntime(request);
280
+ if (cached) {${staleVersionCode}
281
+ if (isStale(cached, staleTime)) {
282
+ event.waitUntil(refreshCache(request));
283
+ }
284
+ return markFromCache(cached);
209
285
  }
210
286
 
211
287
  const precached = await fromPrecache(request);
@@ -215,7 +291,41 @@ async function cacheOnly(event, request) {
215
291
  }
216
292
 
217
293
  async function networkOnly(event, request) {
218
- return fetch(request);
294
+ return _fetch(event, request);
219
295
  }`;
220
296
  }
297
+ function generateTrimCode(maxCacheEntries, maxCacheAge) {
298
+ if (!maxCacheEntries && !maxCacheAge)
299
+ return "";
300
+ return `
301
+ async function trimRuntimeCache(cacheName, maxEntries, maxAge) {
302
+ const _maxEntries = maxEntries ?? GLOBAL_MAX_ENTRIES;
303
+ const _maxAge = maxAge ?? GLOBAL_MAX_AGE;
304
+ const cache = await caches.open(cacheName);
305
+
306
+ if (_maxEntries > 0) {
307
+ const keys = await cache.keys();
308
+ if (keys.length >= _maxEntries) {
309
+ const toDelete = keys.slice(0, keys.length - _maxEntries + 1);
310
+ await Promise.all(toDelete.map((key) => cache.delete(key)));
311
+ }
312
+ }
313
+
314
+ if (_maxAge > 0) {
315
+ const keys = await cache.keys();
316
+ const now = Date.now();
317
+ for (const request of keys) {
318
+ const response = await cache.match(request);
319
+ const dateHeader = response?.headers.get("date");
320
+ if (dateHeader) {
321
+ const age = now - new Date(dateHeader).getTime();
322
+ if (age > _maxAge) {
323
+ await cache.delete(request);
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+ `;
330
+ }
221
331
  //# sourceMappingURL=fetch-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/fetch-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,oBAAoB,CAClC,QAAqK,EACrK,eAAwB;IAExB,MAAM,EAAE,eAAe,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC;IAEzG,MAAM,OAAO,GAAG,CAAC,eAAe,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACrE,MAAM,OAAO,GAAG,cAAc,IAAI,KAAK,CAAC;IACxC,MAAM,OAAO,GAAG,QAAQ,IAAI,aAAa,CAAC;IAC1C,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC;;;0CAGI,OAAO;;IAE7C,CAAC,CAAC,CAAC,EAAE,CAAC;IAER,MAAM,mBAAmB,GAAG,eAAe,CAAC,CAAC,CAAC;;;;;;YAMpC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEhB,MAAM,gBAAgB,GAAG,eAAe,CAAC,CAAC,CAAC;;;MAGvC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEV,MAAM,YAAY,GAAG,eAAe,CAAC,CAAC,CAAC;;;;;;QAMjC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEZ,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,uDAAuD,CAAC,CAAC,CAAC,EAAE,CAAC;IAExF,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC;4BACL,eAAe,IAAI,CAAC;wBACxB,WAAW,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BvC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,GAAG,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2DA+BmC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BzF,gBAAgB;;;;;;EAM/B,OAAO;;;;;;kDAMyC,mBAAmB;EACnE,QAAQ;;;;;;;;;;;;;;;oDAe0C,mBAAmB;EACrE,QAAQ;;;;;;;;;;EAUR,OAAO;;;;;;;;;;;;;;;;;;;;;;wDAsB+C,YAAY;EAClE,OAAO,CAAC,CAAC,CAAC,qDAAqD,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;;;;;;;;;oBAmBlD,gBAAgB;;;;;;;;;;;;EAYlC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"fetch-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/fetch-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,UAAU,oBAAoB,CAClC,QAA0V,EAC1V,eAAwB;IAExB,MAAM,EAAE,eAAe,EAAE,UAAU,EAAE,aAAa,GAAG,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC;IAE9J,MAAM,OAAO,GAAG,cAAc,IAAI,KAAK,CAAC;IACxC,MAAM,OAAO,GAAG,QAAQ,IAAI,aAAa,CAAC;IAE1C,MAAM,gBAAgB,GAAG,eAAe,CAAC,CAAC,CAAC;;;;MAIvC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEV,MAAM,OAAO,GAAG,eAAe,CAAC,CAAC,CAAC;;;;;;;IAOhC,CAAC,CAAC,CAAC,EAAE,CAAC;IAER,MAAM,QAAQ,GAAG,8BAA8B,eAAe,IAAI,CAAC;yBAC5C,WAAW,IAAI,CAAC;4BACb,SAAS,IAAI,CAAC,GAAG,CAAC;IAE5C,OAAO,GAAG,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0CAkCsB,OAAO;;;;;;0BAMvB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA+E7B,aAAa,KAAK,eAAe,CAAC,CAAC,CAAC,0DAA0D,CAAC,CAAC,CAAC,EAAE;wEAC/B,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,yBAAyB,eAAe;;;;;EAKxI,iBAAiB,CAAC,CAAC,CAAC;;;;;;;;CAQrB,CAAC,CAAC,CAAC,EAAE,kBAAkB,iBAAiB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,oCAAoC;;EAEnG,gBAAgB,CAAC,eAAe,EAAE,WAAW,CAAC;;EAE9C,eAAe,IAAI,WAAW,CAAC,CAAC,CAAC,oDAAoD,CAAC,CAAC,CAAC,EAAE;;;;;iBAK3E,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmGhB,gBAAgB;;;;;;;;;;;;;;;EAe/B,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,eAAmC,EAAE,WAA+B;IAC5F,IAAI,CAAC,eAAe,IAAI,CAAC,WAAW;QAAE,OAAO,EAAE,CAAC;IAEhD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BR,CAAC;AACF,CAAC"}
@@ -8,24 +8,26 @@ self.addEventListener("install", (event) => {
8
8
  (async () => {
9
9
  const cache = await caches.open(CACHE_NAME);
10
10
  let downloaded = 0;
11
+ let attempted = 0;
11
12
  for (const asset of ASSETS_TO_CACHE) {
13
+ attempted++;
12
14
  try {
13
15
  const request = new Request(asset.url, asset.options);
14
16
  await cache.add(request);
15
17
  downloaded++;
16
- const percent = Math.round((downloaded / ASSETS_TO_CACHE.length) * 100);
17
- const clients = await self.clients.matchAll({ includeUncontrolled: true });
18
- clients.forEach((client) => {
19
- client.postMessage({
20
- type: "SW_PROGRESS",
21
- percent,
22
- downloaded,
23
- total: ASSETS_TO_CACHE.length,
24
- });
25
- });
26
18
  } catch (err) {
27
19
  console.error(\`Failed to cache \${asset.url}:\`, err);
28
20
  }
21
+ const percent = Math.round((attempted / ASSETS_TO_CACHE.length) * 100);
22
+ const clients = await self.clients.matchAll({ includeUncontrolled: true });
23
+ clients.forEach((client) => {
24
+ client.postMessage({
25
+ type: "SW_PROGRESS",
26
+ percent,
27
+ downloaded,
28
+ total: ASSETS_TO_CACHE.length,
29
+ });
30
+ });
29
31
  }
30
32
  if (AUTO_SKIP_WAITING) self.skipWaiting();
31
33
  })(),
@@ -1 +1 @@
1
- {"version":3,"file":"install-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/install-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,sBAAsB;IACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA4BL,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"install-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/install-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,sBAAsB;IACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8BL,CAAC;AACL,CAAC"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Generates the SW push event connection handler.
3
+ * Opens an SSE (fetch+stream) or WebSocket connection from the SW,
4
+ * listens for "invalidate" events, and calls invalidateByTag().
5
+ */
6
+ export function generateServerPushHandler(type, endpoint, reconnectDelayMs) {
7
+ if (type === "websocket") {
8
+ return `
9
+ // --- Server Push Events (WebSocket) ---
10
+
11
+ let pushWs = null;
12
+ let pushReconnectTimer = null;
13
+
14
+ async function connectPushEvents() {
15
+ try {
16
+ pushWs = new WebSocket("${endpoint}");
17
+ pushWs.onmessage = (event) => {
18
+ try {
19
+ const data = JSON.parse(event.data);
20
+ if (data.type === "invalidate" && data.tags) {
21
+ data.tags.forEach((tag) => invalidateByTag(tag));
22
+ }
23
+ } catch {}
24
+ };
25
+ pushWs.onclose = () => {
26
+ pushWs = null;
27
+ scheduleReconnect();
28
+ };
29
+ pushWs.onerror = () => {
30
+ pushWs?.close();
31
+ };
32
+ } catch {
33
+ scheduleReconnect();
34
+ }
35
+ }
36
+
37
+ function scheduleReconnect() {
38
+ if (pushReconnectTimer) clearTimeout(pushReconnectTimer);
39
+ pushReconnectTimer = setTimeout(connectPushEvents, ${reconnectDelayMs});
40
+ }
41
+
42
+ self.addEventListener("activate", (event) => {
43
+ event.waitUntil(connectPushEvents());
44
+ });
45
+ `;
46
+ }
47
+ // SSE: fetch + ReadableStream reader
48
+ return `
49
+ // --- Server Push Events (SSE) ---
50
+
51
+ let pushReconnectTimer = null;
52
+ let pushAbortController = null;
53
+
54
+ async function connectPushEvents() {
55
+ try {
56
+ pushAbortController = new AbortController();
57
+ const response = await fetch("${endpoint}", {
58
+ headers: { Accept: "text/event-stream" },
59
+ signal: pushAbortController.signal,
60
+ });
61
+ if (!response.ok || !response.body) {
62
+ scheduleReconnect();
63
+ return;
64
+ }
65
+ const reader = response.body.getReader();
66
+ const decoder = new TextDecoder();
67
+ let buffer = "";
68
+ let eventType = "";
69
+ let dataStr = "";
70
+
71
+ while (true) {
72
+ const { done, value } = await reader.read();
73
+ if (done) break;
74
+ buffer += decoder.decode(value, { stream: true });
75
+
76
+ const lines = buffer.split("\\n");
77
+ buffer = lines.pop() || "";
78
+
79
+ for (const line of lines) {
80
+ if (line.startsWith("event: ")) {
81
+ eventType = line.slice(7).trim();
82
+ } else if (line.startsWith("data: ")) {
83
+ dataStr = line.slice(6);
84
+ } else if (line === "") {
85
+ if (eventType === "invalidate" && dataStr) {
86
+ try {
87
+ const parsed = JSON.parse(dataStr);
88
+ if (parsed.tags) {
89
+ parsed.tags.forEach((tag) => invalidateByTag(tag));
90
+ }
91
+ } catch {}
92
+ }
93
+ eventType = "";
94
+ dataStr = "";
95
+ }
96
+ }
97
+ }
98
+ } catch {
99
+ // Connection lost or aborted
100
+ }
101
+ scheduleReconnect();
102
+ }
103
+
104
+ function scheduleReconnect() {
105
+ if (pushReconnectTimer) clearTimeout(pushReconnectTimer);
106
+ pushReconnectTimer = setTimeout(connectPushEvents, ${reconnectDelayMs});
107
+ }
108
+
109
+ self.addEventListener("activate", (event) => {
110
+ event.waitUntil(connectPushEvents());
111
+ });
112
+ `;
113
+ }
114
+ //# sourceMappingURL=server-push-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-push-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/server-push-handler.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,UAAU,yBAAyB,CACvC,IAAyB,EACzB,QAAgB,EAChB,gBAAwB;IAExB,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;QACzB,OAAO;;;;;;;;8BAQmB,QAAQ;;;;;;;;;;;;;;;;;;;;;;;uDAuBiB,gBAAgB;;;;;;CAMtE,CAAC;IACA,CAAC;IAED,qCAAqC;IACrC,OAAO;;;;;;;;;oCAS2B,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uDAiDW,gBAAgB;;;;;;CAMtE,CAAC;AACF,CAAC"}