@swoff/cli 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -33
- package/dist/__tests__/assemble-sw.test.js +8 -38
- package/dist/__tests__/assemble-sw.test.js.map +1 -1
- package/dist/__tests__/cli-integration.test.js +119 -11
- package/dist/__tests__/cli-integration.test.js.map +1 -1
- package/dist/__tests__/config-types.test.js +32 -15
- package/dist/__tests__/config-types.test.js.map +1 -1
- package/dist/__tests__/file-generators.test.js +2 -2
- package/dist/__tests__/file-generators.test.js.map +1 -1
- package/dist/__tests__/glob-matcher.test.js +86 -0
- package/dist/__tests__/glob-matcher.test.js.map +1 -0
- package/dist/__tests__/invalidation-tags.test.js +207 -23
- package/dist/__tests__/invalidation-tags.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +12 -12
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/validator.test.js +144 -97
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +11 -10
- package/dist/index.js.map +1 -1
- package/dist/lib/cli/help.js +0 -5
- package/dist/lib/cli/help.js.map +1 -1
- package/dist/lib/commands/add.js +50 -32
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/clean.js +1 -15
- package/dist/lib/commands/clean.js.map +1 -1
- package/dist/lib/commands/generate-guide.js +2 -2
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/generate.js +7 -21
- package/dist/lib/commands/generate.js.map +1 -1
- package/dist/lib/commands/info.js +8 -6
- package/dist/lib/commands/info.js.map +1 -1
- package/dist/lib/commands/init.js +2 -10
- package/dist/lib/commands/init.js.map +1 -1
- package/dist/lib/commands/validate.js +3 -3
- package/dist/lib/commands/validate.js.map +1 -1
- package/dist/lib/config/loader.js +3 -6
- package/dist/lib/config/loader.js.map +1 -1
- package/dist/lib/config/validator.js +157 -88
- package/dist/lib/config/validator.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +2 -10
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector.js +50 -11
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js +92 -64
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-hooks.js +1 -1
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/gql-wrapper.js +1 -14
- package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/guide-generator.js +6 -640
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
- package/dist/lib/generators/file-generators/invalidation-tags.js +206 -19
- package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
- package/dist/lib/generators/file-generators/manifest.js +2 -2
- package/dist/lib/generators/file-generators/manifest.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +29 -9
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
- package/dist/lib/generators/file-generators/push.js +6 -7
- package/dist/lib/generators/file-generators/push.js.map +1 -1
- package/dist/lib/generators/file-generators/quick-readme.js +8 -125
- package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
- package/dist/lib/generators/file-generators/server-push.js +11 -1
- package/dist/lib/generators/file-generators/server-push.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-generator-build.js +8 -9
- package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-injector.js +2 -1
- package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/type-definitions.js +0 -3
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
- package/dist/lib/generators/sw-generator.js +8 -18
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js +14 -11
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/background-sync-handler.js +59 -17
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/config-header.js +2 -2
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +437 -136
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/message-handler.js +55 -15
- package/dist/lib/generators/sw-sections/message-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/sw-push.js +67 -0
- package/dist/lib/generators/sw-sections/sw-push.js.map +1 -0
- package/dist/lib/generators/sw-sections/tag-management.js +60 -20
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +5 -15
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +121 -71
- package/dist/lib/shared/config-types.js.map +1 -1
- package/dist/lib/shared/glob-matcher.js +77 -0
- package/dist/lib/shared/glob-matcher.js.map +1 -0
- package/dist/lib/utils/tty-status.js +21 -0
- package/dist/lib/utils/tty-status.js.map +1 -0
- package/docs/API.md +565 -0
- package/docs/ARCHITECTURE.md +374 -0
- package/docs/CLI.md +148 -0
- package/docs/COMPARISON.md +192 -0
- package/docs/CONFIG.md +277 -0
- package/docs/ECOSYSTEM.md +33 -0
- package/package.json +2 -1
- package/templates/hooks/useAuth.jsx +2 -2
- package/templates/hooks/useAuth.tsx +2 -2
- package/templates/hooks/useCachedFetch.jsx +3 -28
- package/templates/hooks/useCachedFetch.tsx +2 -33
- package/templates/hooks/useMutation.tsx +13 -1
- package/templates/hooks/useMutationQueue.jsx +2 -1
- package/templates/hooks/useMutationQueue.tsx +2 -1
- package/templates/hooks/useMutationState.jsx +8 -22
- package/templates/hooks/useMutationState.tsx +8 -24
- package/templates/hooks/usePushSubscription.jsx +3 -3
- package/templates/hooks/usePushSubscription.tsx +3 -3
|
@@ -6,24 +6,28 @@
|
|
|
6
6
|
* 2. URL pattern match — from features.serviceWorker.strategies in swoff.config.json
|
|
7
7
|
* 3. defaultStrategy — fallback from features.serviceWorker.defaultStrategy
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Strategies:
|
|
10
|
+
* - cache-first: Serve cache if exists, fetch if missing. No bg refresh.
|
|
11
|
+
* - network-first: Try network first, fall back to cache on failure.
|
|
12
|
+
* - stale-while-revalidate: Serve cache + always refresh in background.
|
|
13
|
+
* - cache-only: Never fetch. Cache hit or 404.
|
|
14
|
+
* - network-only: Never cache. Always fetch.
|
|
15
|
+
* - reactive: Serve cache + refresh on configurable triggers (interval,
|
|
16
|
+
* onFocus, onReconnect). staleTime gates ALL triggers.
|
|
11
17
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - network-only: unaffected (never caches)
|
|
18
|
+
* staleTime, refetchInterval, refetchOnFocus, refetchOnReconnect are
|
|
19
|
+
* reactive-only fields. Non-reactive strategies ignore them.
|
|
20
|
+
*
|
|
21
|
+
* Background refreshes use a shared batch queue with rate limiting.
|
|
22
|
+
* On successful refresh, CACHE_UPDATED is posted to all clients.
|
|
18
23
|
*/
|
|
19
|
-
export function generateFetchHandler(swConfig, tagInvalidation) {
|
|
20
|
-
const { defaultStrategy, strategies, cacheStrategy = "all",
|
|
21
|
-
const
|
|
22
|
-
const spaPath = spaEntry ?? "/index.html";
|
|
24
|
+
export function generateFetchHandler(swConfig, tagInvalidation, mutationQueueEnabled) {
|
|
25
|
+
const { strategy: { default: defaultStrategy, patterns: strategies, mode: cacheStrategy = "all", normalizeKey: normalizeCacheKey, ignoreQueryParams }, navigation: { mode: navMode = "spa", preload: navigationPreload, fallback: spaPath = "/index.html" }, refetchQueue: { batchSize: refetchBatchSize = 5, batchDelayMs: refetchBatchDelayMs = 1000, maxRetries: refetchMaxRetries = 3, retryDelayMs: refetchRetryDelayMs = 1000 } } = swConfig;
|
|
26
|
+
const { staleTime: globalStaleTime, refetchInterval: globalRefetchInterval, refetchOnReconnect: globalRefetchOnReconnect, refetchOnFocus: globalRefetchOnFocus } = swConfig.strategy.reactive?.defaults || {};
|
|
23
27
|
const staleVersionCode = tagInvalidation ? `
|
|
24
28
|
cleanStaleVersions();
|
|
25
29
|
if (staleVersions.has(cacheKey(request))) {
|
|
26
|
-
|
|
30
|
+
queueRefresh(cacheKey(request), new URL(request.url).href);
|
|
27
31
|
}` : "";
|
|
28
32
|
const tagCode = tagInvalidation ? `
|
|
29
33
|
const tagsHeader = request.headers.get("X-SW-Cache-Tags");
|
|
@@ -33,16 +37,281 @@ export function generateFetchHandler(swConfig, tagInvalidation) {
|
|
|
33
37
|
const tags = tagsHeader.split(",").map((t) => t.trim());
|
|
34
38
|
await cacheTagUrl(cacheKeyUrl, actualUrl, tags);
|
|
35
39
|
}` : "";
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
const reactGlobalDefaults = {
|
|
41
|
+
staleTime: globalStaleTime,
|
|
42
|
+
refetchInterval: globalRefetchInterval,
|
|
43
|
+
refetchOnReconnect: globalRefetchOnReconnect,
|
|
44
|
+
refetchOnFocus: globalRefetchOnFocus,
|
|
45
|
+
};
|
|
46
|
+
const reactivePatterns = Object.entries(strategies)
|
|
47
|
+
.filter(([, entry]) => {
|
|
48
|
+
const resolved = typeof entry === "string" ? { strategy: entry } : entry;
|
|
49
|
+
return resolved.strategy === "reactive";
|
|
50
|
+
})
|
|
51
|
+
.map(([pattern, entry]) => {
|
|
52
|
+
const resolved = typeof entry === "string" ? { strategy: entry } : entry;
|
|
53
|
+
return {
|
|
54
|
+
pattern,
|
|
55
|
+
staleTime: resolved.staleTime ?? reactGlobalDefaults.staleTime,
|
|
56
|
+
refetchInterval: resolved.refetchInterval ?? reactGlobalDefaults.refetchInterval,
|
|
57
|
+
refetchOnReconnect: resolved.refetchOnReconnect ?? reactGlobalDefaults.refetchOnReconnect,
|
|
58
|
+
refetchOnFocus: resolved.refetchOnFocus ?? reactGlobalDefaults.refetchOnFocus,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
const trimDecl = `const REFETCH_BATCH_SIZE = ${refetchBatchSize};
|
|
62
|
+
const REFETCH_BATCH_DELAY_MS = ${refetchBatchDelayMs};
|
|
63
|
+
const REFRESH_MAX_RETRIES = ${refetchMaxRetries};
|
|
64
|
+
const REFRESH_RETRY_DELAY_MS = ${refetchRetryDelayMs};`;
|
|
65
|
+
const GLOB_CODE = "\n" +
|
|
66
|
+
"// --- Glob Pattern Matching ---\n" +
|
|
67
|
+
"// NOTE: This is an inline copy of the algorithm in shared/glob-matcher.ts.\n" +
|
|
68
|
+
"// Keep the two implementations in sync when making changes.\n" +
|
|
69
|
+
"\n" +
|
|
70
|
+
"function escapeGlobMeta(s) {\n" +
|
|
71
|
+
" return s.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$$&\");\n" +
|
|
72
|
+
"}\n" +
|
|
73
|
+
"\n" +
|
|
74
|
+
"function globPartRe(part) {\n" +
|
|
75
|
+
" for (var o = \"\", i = 0; i < part.length; i++) {\n" +
|
|
76
|
+
" var c = part[i];\n" +
|
|
77
|
+
" if (c === \"*\") { o += \"[^/]*\"; }\n" +
|
|
78
|
+
" else if (c === \"?\") { o += \"[^/]\"; }\n" +
|
|
79
|
+
" else if (c === \"{\") {\n" +
|
|
80
|
+
" var cl = part.indexOf(\"}\", i);\n" +
|
|
81
|
+
" if (cl === -1) { o += \"\\\\\" + c; }\n" +
|
|
82
|
+
" else {\n" +
|
|
83
|
+
" o += \"(?:\" + part.slice(i + 1, cl).split(\",\").map(function(s) { return escapeGlobMeta(s.trim()); }).join(\"|\") + \")\";\n" +
|
|
84
|
+
" i = cl;\n" +
|
|
85
|
+
" }\n" +
|
|
86
|
+
" } else { o += \"\\\\\" + c; }\n" +
|
|
87
|
+
" }\n" +
|
|
88
|
+
" return o;\n" +
|
|
89
|
+
"}\n" +
|
|
90
|
+
"\n" +
|
|
91
|
+
"function matchGlob(path, pattern) {\n" +
|
|
92
|
+
" if (pattern.charAt(0) === \"!\") return !matchGlob(path, pattern.slice(1));\n" +
|
|
93
|
+
" var parts = pattern.split(\"/\").filter(Boolean);\n" +
|
|
94
|
+
" var pps = path.split(\"/\").filter(Boolean);\n" +
|
|
95
|
+
" var pi = 0, ppi = 0;\n" +
|
|
96
|
+
" while (pi < parts.length && ppi < pps.length) {\n" +
|
|
97
|
+
" var part = parts[pi];\n" +
|
|
98
|
+
" if (part === \"**\") {\n" +
|
|
99
|
+
" if (pi === parts.length - 1) return true;\n" +
|
|
100
|
+
" var nxt = parts[pi + 1];\n" +
|
|
101
|
+
" var found = -1;\n" +
|
|
102
|
+
" for (var j = ppi; j < pps.length; j++) {\n" +
|
|
103
|
+
" if (nxt.indexOf(\"*\") > -1 || nxt.indexOf(\"?\") > -1 || nxt.indexOf(\"{\") > -1) {\n" +
|
|
104
|
+
" if (new RegExp(\"^\" + globPartRe(nxt) + \"$\").test(pps[j])) { found = j; break; }\n" +
|
|
105
|
+
" } else if (nxt === pps[j]) { found = j; break; }\n" +
|
|
106
|
+
" }\n" +
|
|
107
|
+
" if (found === -1) return false;\n" +
|
|
108
|
+
" ppi = found;\n" +
|
|
109
|
+
" pi++;\n" +
|
|
110
|
+
" continue;\n" +
|
|
111
|
+
" }\n" +
|
|
112
|
+
" if (part.indexOf(\"*\") > -1 || part.indexOf(\"?\") > -1 || part.indexOf(\"{\") > -1) {\n" +
|
|
113
|
+
" if (!new RegExp(\"^\" + globPartRe(part) + \"$\").test(pps[ppi])) return false;\n" +
|
|
114
|
+
" } else if (part !== pps[ppi]) return false;\n" +
|
|
115
|
+
" pi++;\n" +
|
|
116
|
+
" ppi++;\n" +
|
|
117
|
+
" }\n" +
|
|
118
|
+
" return pi === parts.length && ppi === pps.length;\n" +
|
|
119
|
+
"}\n" +
|
|
120
|
+
"\n";
|
|
39
121
|
return `${trimDecl}
|
|
122
|
+
// --- Batch Refresh Queue ---
|
|
123
|
+
|
|
124
|
+
const _refreshQueue = new Map();
|
|
125
|
+
let _refreshQueueProcessing = false;
|
|
126
|
+
let _refreshQueuePromise = null;
|
|
127
|
+
|
|
128
|
+
function queueRefresh(cacheKeyUrl, actualUrl${tagInvalidation ? ", tags" : ""}) {
|
|
129
|
+
// Use Map keyed by cacheKey for proper deduplication${tagInvalidation ? `
|
|
130
|
+
// Don't let a tagless (SWR/reactive) refresh override an invalidation-triggered entry with tags
|
|
131
|
+
if (_refreshQueue.has(cacheKeyUrl) && !tags) return;` : ""}
|
|
132
|
+
_refreshQueue.set(cacheKeyUrl, { cacheKey: cacheKeyUrl, actualUrl: actualUrl || cacheKeyUrl, retryCount: 0${tagInvalidation ? ", tags: tags || null" : ""} });
|
|
133
|
+
if (!_refreshQueuePromise) {
|
|
134
|
+
_refreshQueuePromise = _processRefreshQueue().finally(() => {
|
|
135
|
+
_refreshQueuePromise = null;
|
|
136
|
+
if (_refreshQueue.size > 0) queueRefresh();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return _refreshQueuePromise;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveActualUrl(entry) {
|
|
143
|
+
const url = entry.actualUrl || entry.cacheKey;
|
|
144
|
+
if (url.includes("/__swc/")) {
|
|
145
|
+
// Virtual cache key — use the stored actual URL from tag registry
|
|
146
|
+
return url;
|
|
147
|
+
}
|
|
148
|
+
return url;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function _processRefreshQueue() {
|
|
152
|
+
while (_refreshQueue.size > 0) {
|
|
153
|
+
const batch = [];
|
|
154
|
+
for (const [cacheKey, entry] of _refreshQueue) {
|
|
155
|
+
if (batch.length >= REFETCH_BATCH_SIZE) break;
|
|
156
|
+
batch.push(entry);
|
|
157
|
+
_refreshQueue.delete(cacheKey);
|
|
158
|
+
}
|
|
159
|
+
if (batch.length === 0) break;
|
|
160
|
+
|
|
161
|
+
const clients = await self.clients.matchAll();
|
|
162
|
+
|
|
163
|
+
await Promise.allSettled(batch.map(async (entry) => {
|
|
164
|
+
const fetchUrl = resolveActualUrl(entry);
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(fetchUrl);
|
|
167
|
+
if (response.ok) {
|
|
168
|
+
const request = new Request(entry.cacheKey);
|
|
169
|
+
await storeRuntime(request, response);${tagInvalidation ? `
|
|
170
|
+
if (entry.tags && typeof cacheTagUrl !== "undefined") {
|
|
171
|
+
await cacheTagUrl(entry.cacheKey, fetchUrl, entry.tags);
|
|
172
|
+
}` : ""}
|
|
173
|
+
// Clean up stale version tracking on successful refresh
|
|
174
|
+
if (typeof staleVersions !== "undefined" && staleVersions.has(entry.cacheKey)) {
|
|
175
|
+
staleVersions.delete(entry.cacheKey);
|
|
176
|
+
}
|
|
177
|
+
for (const client of clients) {
|
|
178
|
+
client.postMessage({ type: "CACHE_UPDATED", url: fetchUrl });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Refresh failed — retry with exponential backoff
|
|
183
|
+
if (entry.retryCount < REFRESH_MAX_RETRIES) {
|
|
184
|
+
entry.retryCount++;
|
|
185
|
+
const delay = REFRESH_RETRY_DELAY_MS * Math.pow(2, entry.retryCount - 1);
|
|
186
|
+
_refreshQueue.set(entry.cacheKey, entry);
|
|
187
|
+
// Schedule re-processing after delay
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
if (!_refreshQueuePromise) {
|
|
190
|
+
_refreshQueuePromise = _processRefreshQueue().finally(() => {
|
|
191
|
+
_refreshQueuePromise = null;
|
|
192
|
+
if (_refreshQueue.size > 0) queueRefresh();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}, delay);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
if (_refreshQueue.size > 0 && REFETCH_BATCH_DELAY_MS > 0) {
|
|
201
|
+
await new Promise(r => setTimeout(r, REFETCH_BATCH_DELAY_MS));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
_refreshQueueProcessing = false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Reactive Patterns (extracted for interval/focus/reconnect) ---
|
|
208
|
+
|
|
209
|
+
const REACTIVE_PATTERNS = ${JSON.stringify(reactivePatterns)};
|
|
210
|
+
|
|
211
|
+
function findReactiveConfig(url) {
|
|
212
|
+
const path = new URL(url).pathname;
|
|
213
|
+
for (const cfg of REACTIVE_PATTERNS) {
|
|
214
|
+
if (matchGlob(path, cfg.pattern)) return cfg;
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function shouldReactiveRefresh(cached, config) {
|
|
220
|
+
if (!config.staleTime && config.staleTime !== 0) return true;
|
|
221
|
+
if (config.staleTime === 0) return true;
|
|
222
|
+
return isStale(cached, config.staleTime);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- Reactive Interval Timers ---
|
|
226
|
+
|
|
227
|
+
${(() => {
|
|
228
|
+
const patternsWithInterval = reactivePatterns.filter((p) => !!p.refetchInterval && p.refetchInterval > 0);
|
|
229
|
+
if (patternsWithInterval.length === 0)
|
|
230
|
+
return "";
|
|
231
|
+
return `// Start intervals for reactive patterns with refetchInterval
|
|
232
|
+
(async () => {
|
|
233
|
+
${patternsWithInterval.map(p => ` setInterval(async () => {
|
|
234
|
+
const cache = await caches.open(CACHE_NAME_RUNTIME);
|
|
235
|
+
const keys = await cache.keys();
|
|
236
|
+
for (const request of keys) {
|
|
237
|
+
const url = new URL(request.url);
|
|
238
|
+
if (url.pathname.startsWith("/__swc/")) continue;
|
|
239
|
+
if (!matchGlob(url.pathname, "${p.pattern}")) continue;
|
|
240
|
+
const config = findReactiveConfig(url.href);
|
|
241
|
+
if (!config) continue;
|
|
242
|
+
const cached = await cache.match(request);
|
|
243
|
+
if (cached && shouldReactiveRefresh(cached, config)) {
|
|
244
|
+
queueRefresh(request.url, url.href);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}, ${p.refetchInterval * 1000});`).join("\n")}
|
|
248
|
+
})();
|
|
249
|
+
`;
|
|
250
|
+
})()}
|
|
251
|
+
async function handleOnline() {
|
|
252
|
+
const clients = await self.clients.matchAll();
|
|
253
|
+
if (clients.length === 0) return;
|
|
254
|
+
|
|
255
|
+
// Step 1: Retry stale version entries (failed refetches after invalidation)
|
|
256
|
+
if (typeof staleVersions !== "undefined") {
|
|
257
|
+
const staleUrls = [...staleVersions.keys()];
|
|
258
|
+
for (const url of staleUrls) {
|
|
259
|
+
queueRefresh(url, url);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Step 2: Refresh reactive entries with refetchOnReconnect
|
|
264
|
+
const cache = await caches.open(CACHE_NAME_RUNTIME);
|
|
265
|
+
const keys = await cache.keys();
|
|
266
|
+
for (const request of keys) {
|
|
267
|
+
const url = new URL(request.url);
|
|
268
|
+
if (url.pathname.startsWith("/__swc/")) continue;
|
|
269
|
+
const config = findReactiveConfig(url.href);
|
|
270
|
+
if (!config || !config.refetchOnReconnect) continue;
|
|
271
|
+
const cached = await cache.match(request);
|
|
272
|
+
if (cached && shouldReactiveRefresh(cached, config)) {
|
|
273
|
+
queueRefresh(request.url, url.href);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function handleOnFocus() {
|
|
279
|
+
const cache = await caches.open(CACHE_NAME_RUNTIME);
|
|
280
|
+
const keys = await cache.keys();
|
|
281
|
+
for (const request of keys) {
|
|
282
|
+
const url = new URL(request.url);
|
|
283
|
+
if (url.pathname.startsWith("/__swc/")) continue;
|
|
284
|
+
const config = findReactiveConfig(url.href);
|
|
285
|
+
if (!config || !config.refetchOnFocus) continue;
|
|
286
|
+
const cached = await cache.match(request);
|
|
287
|
+
if (cached && shouldReactiveRefresh(cached, config)) {
|
|
288
|
+
queueRefresh(request.url, url.href);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
40
293
|
// --- Cache Key ---
|
|
41
294
|
|
|
42
295
|
function cacheKey(request) {
|
|
43
296
|
const key = request.headers.get("X-SW-Cache-Key");
|
|
44
297
|
if (key) return new URL("/__swc/" + key, self.location.origin).href;
|
|
45
|
-
|
|
298
|
+
const url = new URL(request.url);${normalizeCacheKey ? `
|
|
299
|
+
// Sort query params alphabetically for consistent cache keys
|
|
300
|
+
if (url.search) {
|
|
301
|
+
const params = new URLSearchParams(url.search);
|
|
302
|
+
params.sort();
|
|
303
|
+
url.search = params.toString();
|
|
304
|
+
}` : ""}${ignoreQueryParams && ignoreQueryParams.length > 0
|
|
305
|
+
? `
|
|
306
|
+
// Strip configured query params from cache key
|
|
307
|
+
const ignore = ${JSON.stringify(ignoreQueryParams)};
|
|
308
|
+
if (url.search) {
|
|
309
|
+
const params = new URLSearchParams(url.search);
|
|
310
|
+
for (const key of ignore) params.delete(key);
|
|
311
|
+
url.search = params.toString();
|
|
312
|
+
}`
|
|
313
|
+
: ""}
|
|
314
|
+
return url.href;
|
|
46
315
|
}
|
|
47
316
|
|
|
48
317
|
// --- Cache Helpers ---
|
|
@@ -99,7 +368,7 @@ function isStale(response, staleTimeSeconds) {
|
|
|
99
368
|
return Date.now() - Number(cachedAt) > staleTimeSeconds * 1000;
|
|
100
369
|
}
|
|
101
370
|
|
|
102
|
-
// --- Strategy Selection (3-tier config resolution) ---
|
|
371
|
+
${GLOB_CODE}// --- Strategy Selection (3-tier config resolution) ---
|
|
103
372
|
|
|
104
373
|
function resolveStrategyEntry(entry) {
|
|
105
374
|
return typeof entry === "string" ? { strategy: entry } : entry;
|
|
@@ -108,55 +377,137 @@ function resolveStrategyEntry(entry) {
|
|
|
108
377
|
function determineCacheStrategy(request, customStrategies, globalDefaults) {
|
|
109
378
|
const override = request.headers.get("X-SW-Strategy");
|
|
110
379
|
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
|
-
};
|
|
380
|
+
return { strategy: override };
|
|
117
381
|
}
|
|
118
382
|
const path = new URL(request.url).pathname;
|
|
119
383
|
for (const [pattern, entry] of Object.entries(customStrategies)) {
|
|
120
|
-
if (path
|
|
384
|
+
if (matchGlob(path, pattern)) {
|
|
121
385
|
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
|
-
};
|
|
386
|
+
return { strategy: resolved.strategy };
|
|
128
387
|
}
|
|
129
388
|
}
|
|
130
|
-
return {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
389
|
+
return { strategy: globalDefaults.defaultStrategy };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function determineCacheStrategyForUrl(url, customStrategies, globalDefaults) {
|
|
393
|
+
const path = new URL(url).pathname;
|
|
394
|
+
for (const [pattern, entry] of Object.entries(customStrategies)) {
|
|
395
|
+
if (matchGlob(path, pattern)) {
|
|
396
|
+
const resolved = resolveStrategyEntry(entry);
|
|
397
|
+
return { strategy: resolved.strategy };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { strategy: globalDefaults.defaultStrategy };
|
|
136
401
|
}
|
|
137
402
|
|
|
138
403
|
function applyStrategy(event, request, config) {
|
|
139
|
-
const { strategy
|
|
140
|
-
if (strategy === "
|
|
141
|
-
|
|
404
|
+
const { strategy } = config;
|
|
405
|
+
if (strategy === "reactive") {
|
|
406
|
+
const reactiveCfg = findReactiveConfig(new URL(request.url).href);
|
|
407
|
+
event.respondWith(reactiveStrategy(event, request, reactiveCfg?.staleTime));
|
|
408
|
+
} else if (strategy === "stale-while-revalidate") {
|
|
409
|
+
event.respondWith(staleWhileRevalidate(event, request));
|
|
142
410
|
} else if (strategy === "network-first") {
|
|
143
|
-
event.respondWith(networkFirst(event, request
|
|
411
|
+
event.respondWith(networkFirst(event, request));
|
|
144
412
|
} else if (strategy === "cache-only") {
|
|
145
|
-
event.respondWith(cacheOnly(event, request
|
|
413
|
+
event.respondWith(cacheOnly(event, request));
|
|
146
414
|
} else if (strategy === "network-only") {
|
|
147
415
|
event.respondWith(networkOnly(event, request));
|
|
148
416
|
} else {
|
|
149
|
-
event.respondWith(cacheFirst(event, request
|
|
417
|
+
event.respondWith(cacheFirst(event, request));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
${mutationQueueEnabled ? `// --- Mutation Queue IDB Helpers ---
|
|
422
|
+
|
|
423
|
+
const MUTATION_DB_NAME = "swoff-queue";
|
|
424
|
+
const MUTATION_STORE_NAME = "mutations";
|
|
425
|
+
|
|
426
|
+
function openMutationQueueDB() {
|
|
427
|
+
return new Promise((resolve, reject) => {
|
|
428
|
+
const request = indexedDB.open(MUTATION_DB_NAME, 1);
|
|
429
|
+
request.onupgradeneeded = (e) => {
|
|
430
|
+
const db = e.target.result;
|
|
431
|
+
if (!db.objectStoreNames.contains(MUTATION_STORE_NAME)) {
|
|
432
|
+
const store = db.createObjectStore(MUTATION_STORE_NAME, { keyPath: "id" });
|
|
433
|
+
store.createIndex("by-timestamp", "timestamp");
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
request.onsuccess = (e) => resolve(e.target.result);
|
|
437
|
+
request.onerror = (e) => reject(e.target.error);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function storeMutationInSW(request) {
|
|
442
|
+
let body, bodyType;
|
|
443
|
+
try {
|
|
444
|
+
body = await request.clone().json();
|
|
445
|
+
bodyType = "json";
|
|
446
|
+
} catch {
|
|
447
|
+
body = await request.clone().text();
|
|
448
|
+
bodyType = "text";
|
|
150
449
|
}
|
|
450
|
+
|
|
451
|
+
const tagsHeader = request.headers.get("X-SW-Invalidate-Tags");
|
|
452
|
+
const tags = tagsHeader ? tagsHeader.split(",").map(function(t) { return t.trim(); }) : [];
|
|
453
|
+
|
|
454
|
+
const db = await openMutationQueueDB();
|
|
455
|
+
const tx = db.transaction(MUTATION_STORE_NAME, "readwrite");
|
|
456
|
+
tx.objectStore(MUTATION_STORE_NAME).add({
|
|
457
|
+
id: crypto.randomUUID(),
|
|
458
|
+
method: request.method,
|
|
459
|
+
url: new URL(request.url).href,
|
|
460
|
+
body,
|
|
461
|
+
bodyType,
|
|
462
|
+
headers: {},
|
|
463
|
+
timestamp: Date.now(),
|
|
464
|
+
retryCount: 0,
|
|
465
|
+
nextRetryAt: 0,
|
|
466
|
+
tags,
|
|
467
|
+
});
|
|
468
|
+
await new Promise(function(resolve, reject) {
|
|
469
|
+
tx.oncomplete = function() { resolve(); };
|
|
470
|
+
tx.onerror = function() { reject(tx.error); };
|
|
471
|
+
});
|
|
151
472
|
}
|
|
152
473
|
|
|
153
|
-
|
|
154
|
-
const
|
|
474
|
+
async function handleMutation(event) {
|
|
475
|
+
const request = event.request;
|
|
476
|
+
if (request.headers.get("X-SW-No-Queue") === "true") {
|
|
477
|
+
return fetch(request.clone());
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
return await fetch(request.clone());
|
|
481
|
+
} catch {
|
|
482
|
+
await storeMutationInSW(request);
|
|
483
|
+
// Notify open clients that a mutation was stored so they can try to process
|
|
484
|
+
const clients = await self.clients.matchAll();
|
|
485
|
+
clients.forEach(function(client) {
|
|
486
|
+
client.postMessage({ type: "MUTATION_STORED" });
|
|
487
|
+
});
|
|
488
|
+
const queuedBody = JSON.stringify({ queued: true });
|
|
489
|
+
return new Response(queuedBody, {
|
|
490
|
+
status: 202,
|
|
491
|
+
statusText: "Accepted",
|
|
492
|
+
headers: { "Content-Type": "application/json", "X-SW-Mutation-Queued": "true" },
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
` : ""}self.addEventListener("fetch", (event) => {
|
|
498
|
+
const { request } = event;${mutationQueueEnabled ? `
|
|
155
499
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
500
|
+
if (request.headers.get("X-SW-Cache-Strategy") === "mutation") {
|
|
501
|
+
event.respondWith(handleMutation(event));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
156
504
|
if (!request.headers.get("X-SW-Cache-Key")) return;
|
|
157
|
-
}
|
|
505
|
+
}` : `
|
|
506
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
507
|
+
if (!request.headers.get("X-SW-Cache-Key")) return;
|
|
508
|
+
}`}
|
|
158
509
|
${cacheStrategy === "explicit-only" ? `if (!request.headers.get("X-SW-Cache-Strategy")) return;` : ""}
|
|
159
|
-
applyStrategy(event, request, determineCacheStrategy(event.request, ${JSON.stringify(strategies)}, { defaultStrategy: "${defaultStrategy}"
|
|
510
|
+
applyStrategy(event, request, determineCacheStrategy(event.request, ${JSON.stringify(strategies)}, { defaultStrategy: "${defaultStrategy}" }));
|
|
160
511
|
});
|
|
161
512
|
|
|
162
513
|
// --- Strategies ---
|
|
@@ -171,17 +522,9 @@ async function fetchWithPreload(event, request) {
|
|
|
171
522
|
}
|
|
172
523
|
` : ""}const _fetch = ${navigationPreload ? "fetchWithPreload" : `(event, request) => fetch(request)`};
|
|
173
524
|
|
|
174
|
-
|
|
175
|
-
function _trim(cacheName, maxEntries, maxAge) {
|
|
176
|
-
${maxCacheEntries || maxCacheAge ? ` trimRuntimeCache(cacheName, maxEntries, maxAge);` : ""}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function cacheFirst(event, request, staleTime, maxEntries, maxAge) {
|
|
525
|
+
async function cacheFirst(event, request) {
|
|
180
526
|
const cached = await fromRuntime(request);
|
|
181
527
|
if (cached) {${staleVersionCode}
|
|
182
|
-
if (isStale(cached, staleTime)) {
|
|
183
|
-
event.waitUntil(refreshCache(request));
|
|
184
|
-
}
|
|
185
528
|
return markFromCache(cached);
|
|
186
529
|
}
|
|
187
530
|
|
|
@@ -193,94 +536,86 @@ async function cacheFirst(event, request, staleTime, maxEntries, maxAge) {
|
|
|
193
536
|
|
|
194
537
|
const response = await _fetch(event, request);
|
|
195
538
|
if (response.ok) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
await cacheResponse(request, response);
|
|
199
|
-
_trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
|
|
200
|
-
})(),
|
|
201
|
-
);
|
|
539
|
+
const responseToCache = response.clone();
|
|
540
|
+
event.waitUntil(cacheResponse(request, responseToCache));
|
|
202
541
|
}
|
|
203
542
|
return response;
|
|
204
543
|
}
|
|
205
544
|
|
|
206
|
-
async function networkFirst(event, request
|
|
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
|
-
}
|
|
214
|
-
|
|
545
|
+
async function networkFirst(event, request) {
|
|
215
546
|
try {
|
|
216
547
|
const response = await _fetch(event, request);
|
|
217
548
|
if (response.ok) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
await cacheResponse(request, response);
|
|
221
|
-
_trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
|
|
222
|
-
})(),
|
|
223
|
-
);
|
|
549
|
+
const responseToCache = response.clone();
|
|
550
|
+
event.waitUntil(cacheResponse(request, responseToCache));
|
|
224
551
|
}
|
|
225
552
|
return response;
|
|
226
553
|
} catch {
|
|
227
554
|
const cached = await fromRuntime(request);
|
|
228
|
-
if (cached)
|
|
555
|
+
if (cached) {
|
|
556
|
+
return markFromCache(cached);
|
|
557
|
+
}
|
|
229
558
|
|
|
230
559
|
const precached = await fromPrecache(request);
|
|
231
|
-
if (precached) return precached;
|
|
560
|
+
if (precached) return markFromCache(precached);
|
|
232
561
|
|
|
233
562
|
const fallback = await fromSpaFallback(request);
|
|
234
563
|
if (fallback) return fallback;
|
|
235
564
|
|
|
236
|
-
throw new Error("
|
|
565
|
+
throw new Error("Network request failed and no cached response available");
|
|
237
566
|
}
|
|
238
567
|
}
|
|
239
568
|
|
|
240
|
-
async function staleWhileRevalidate(event, request
|
|
569
|
+
async function staleWhileRevalidate(event, request) {
|
|
241
570
|
const cached = await fromRuntime(request);
|
|
242
571
|
if (cached) {
|
|
243
|
-
|
|
244
|
-
if (!isStale(cached, staleTime)) {
|
|
245
|
-
return markFromCache(cached);
|
|
246
|
-
}
|
|
247
|
-
event.waitUntil(refreshCache(request));
|
|
572
|
+
event.waitUntil(queueRefresh(cacheKey(request), new URL(request.url).href));
|
|
248
573
|
return markFromCache(cached);
|
|
249
574
|
}
|
|
250
575
|
|
|
251
576
|
const precached = await fromPrecache(request);
|
|
252
577
|
if (precached) {
|
|
253
|
-
event.waitUntil(
|
|
578
|
+
event.waitUntil(queueRefresh(cacheKey(request), new URL(request.url).href));
|
|
254
579
|
return markFromCache(precached);
|
|
255
580
|
}
|
|
256
581
|
|
|
257
582
|
const response = await _fetch(event, request);
|
|
258
583
|
if (response.ok) {
|
|
259
|
-
|
|
260
|
-
|
|
584
|
+
const responseToCache = response.clone();
|
|
585
|
+
await cacheResponse(request, responseToCache);
|
|
261
586
|
}
|
|
262
587
|
return response;
|
|
263
588
|
}
|
|
264
589
|
|
|
265
|
-
async function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
590
|
+
async function reactiveStrategy(event, request, staleTime) {
|
|
591
|
+
const cached = await fromRuntime(request);
|
|
592
|
+
// Per-request staleTime header overrides config-level staleTime
|
|
593
|
+
const headerStale = request.headers.get("X-SW-Stale-Time");
|
|
594
|
+
const effectiveStaleTime = headerStale !== null ? Number(headerStale) : staleTime;
|
|
595
|
+
if (cached) {${staleVersionCode}
|
|
596
|
+
if (shouldReactiveRefresh(cached, { staleTime: effectiveStaleTime })) {
|
|
597
|
+
event.waitUntil(queueRefresh(cacheKey(request), new URL(request.url).href));
|
|
272
598
|
}
|
|
273
|
-
|
|
274
|
-
// Background refresh failed - stale cache remains usable
|
|
599
|
+
return markFromCache(cached);
|
|
275
600
|
}
|
|
601
|
+
|
|
602
|
+
const precached = await fromPrecache(request);
|
|
603
|
+
if (precached) return markFromCache(precached);
|
|
604
|
+
|
|
605
|
+
const fallback = await fromSpaFallback(request);
|
|
606
|
+
if (fallback) return fallback;
|
|
607
|
+
|
|
608
|
+
const response = await _fetch(event, request);
|
|
609
|
+
if (response.ok) {
|
|
610
|
+
const responseToCache = response.clone();
|
|
611
|
+
await cacheResponse(request, responseToCache);
|
|
612
|
+
}
|
|
613
|
+
return response;
|
|
276
614
|
}
|
|
277
615
|
|
|
278
|
-
async function cacheOnly(event, request
|
|
616
|
+
async function cacheOnly(event, request) {
|
|
279
617
|
const cached = await fromRuntime(request);
|
|
280
618
|
if (cached) {${staleVersionCode}
|
|
281
|
-
if (isStale(cached, staleTime)) {
|
|
282
|
-
event.waitUntil(refreshCache(request));
|
|
283
|
-
}
|
|
284
619
|
return markFromCache(cached);
|
|
285
620
|
}
|
|
286
621
|
|
|
@@ -294,38 +629,4 @@ async function networkOnly(event, request) {
|
|
|
294
629
|
return _fetch(event, request);
|
|
295
630
|
}`;
|
|
296
631
|
}
|
|
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
|
-
}
|
|
331
632
|
//# sourceMappingURL=fetch-handler.js.map
|