@swoff/cli 0.3.7 → 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 +53 -606
- 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 +38 -18
- 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 +170 -74
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +12 -11
- 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 +52 -33
- 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 +5 -5
- 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 +37 -12
- 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 +192 -42
- 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 +51 -12
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js +127 -58
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-hooks.js +6 -2
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/gql-wrapper.js +139 -0
- package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
- package/dist/lib/generators/file-generators/guide-generator.js +6 -436
- 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 +140 -52
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-state.js +126 -0
- package/dist/lib/generators/file-generators/mutation-state.js.map +1 -0
- 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 -75
- package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
- package/dist/lib/generators/file-generators/server-push.js +146 -0
- package/dist/lib/generators/file-generators/server-push.js.map +1 -0
- 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 +26 -1
- 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 +19 -11
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/background-sync-handler.js +80 -24
- 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/default-template.js +2 -1
- package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +461 -121
- 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/server-push-handler.js +114 -0
- package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -0
- 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 +62 -21
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +12 -16
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +133 -45
- 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 +21 -7
- package/templates/hooks/useCachedFetch.tsx +23 -11
- package/templates/hooks/useMutation.jsx +76 -0
- package/templates/hooks/useMutation.tsx +109 -0
- package/templates/hooks/useMutationQueue.jsx +20 -11
- package/templates/hooks/useMutationQueue.tsx +31 -15
- package/templates/hooks/useMutationState.jsx +13 -0
- package/templates/hooks/useMutationState.tsx +25 -0
- package/templates/hooks/usePrefetch.jsx +10 -0
- package/templates/hooks/usePrefetch.tsx +25 -0
- package/templates/hooks/usePushSubscription.jsx +3 -3
- package/templates/hooks/usePushSubscription.tsx +3 -3
|
@@ -6,65 +6,314 @@
|
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
13
17
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
16
23
|
*/
|
|
17
|
-
export function generateFetchHandler(swConfig, tagInvalidation) {
|
|
18
|
-
const { defaultStrategy, strategies, cacheStrategy = "all",
|
|
19
|
-
const
|
|
20
|
-
const hasTrim = (maxCacheEntries ?? 0) > 0 || (maxCacheAge ?? 0) > 0 || hasPerRouteLimits;
|
|
21
|
-
const navMode = navigationMode ?? "spa";
|
|
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
|
-
if (staleVersions.has(request
|
|
26
|
-
|
|
29
|
+
if (staleVersions.has(cacheKey(request))) {
|
|
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");
|
|
30
34
|
if (tagsHeader) {
|
|
31
|
-
const
|
|
35
|
+
const cacheKeyUrl = cacheKey(request);
|
|
36
|
+
const actualUrl = new URL(request.url).href;
|
|
32
37
|
const tags = tagsHeader.split(",").map((t) => t.trim());
|
|
33
|
-
await cacheTagUrl(
|
|
38
|
+
await cacheTagUrl(cacheKeyUrl, actualUrl, tags);
|
|
34
39
|
}` : "";
|
|
35
|
-
const
|
|
36
|
-
|
|
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";
|
|
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
|
+
}
|
|
39
141
|
|
|
40
|
-
|
|
41
|
-
const
|
|
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
|
+
}
|
|
42
150
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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);
|
|
48
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;
|
|
49
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 ---
|
|
50
226
|
|
|
51
|
-
|
|
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);
|
|
52
235
|
const keys = await cache.keys();
|
|
53
|
-
const now = Date.now();
|
|
54
236
|
for (const request of keys) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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);
|
|
62
245
|
}
|
|
63
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
|
+
}
|
|
64
275
|
}
|
|
65
276
|
}
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
|
|
293
|
+
// --- Cache Key ---
|
|
294
|
+
|
|
295
|
+
function cacheKey(request) {
|
|
296
|
+
const key = request.headers.get("X-SW-Cache-Key");
|
|
297
|
+
if (key) return new URL("/__swc/" + key, self.location.origin).href;
|
|
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;
|
|
315
|
+
}
|
|
316
|
+
|
|
68
317
|
// --- Cache Helpers ---
|
|
69
318
|
|
|
70
319
|
async function fromPrecache(request) {
|
|
@@ -74,12 +323,19 @@ async function fromPrecache(request) {
|
|
|
74
323
|
|
|
75
324
|
async function fromRuntime(request) {
|
|
76
325
|
const cache = await caches.open(CACHE_NAME_RUNTIME);
|
|
77
|
-
return cache.match(
|
|
326
|
+
return cache.match(cacheKey(request));
|
|
78
327
|
}
|
|
79
328
|
|
|
80
329
|
async function storeRuntime(request, response) {
|
|
81
330
|
const cache = await caches.open(CACHE_NAME_RUNTIME);
|
|
82
|
-
|
|
331
|
+
const headers = new Headers(response.headers);
|
|
332
|
+
headers.set("X-SW-Cached-At", String(Date.now()));
|
|
333
|
+
const cloned = new Response(response.body, {
|
|
334
|
+
status: response.status,
|
|
335
|
+
statusText: response.statusText,
|
|
336
|
+
headers,
|
|
337
|
+
});
|
|
338
|
+
await cache.put(cacheKey(request), cloned);
|
|
83
339
|
}
|
|
84
340
|
|
|
85
341
|
async function cacheResponse(request, response) {
|
|
@@ -105,42 +361,153 @@ function markFromCache(response) {
|
|
|
105
361
|
});
|
|
106
362
|
}
|
|
107
363
|
|
|
108
|
-
|
|
364
|
+
function isStale(response, staleTimeSeconds) {
|
|
365
|
+
if (!staleTimeSeconds || staleTimeSeconds <= 0) return false;
|
|
366
|
+
const cachedAt = response.headers.get("X-SW-Cached-At");
|
|
367
|
+
if (!cachedAt) return false;
|
|
368
|
+
return Date.now() - Number(cachedAt) > staleTimeSeconds * 1000;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
${GLOB_CODE}// --- Strategy Selection (3-tier config resolution) ---
|
|
109
372
|
|
|
110
373
|
function resolveStrategyEntry(entry) {
|
|
111
374
|
return typeof entry === "string" ? { strategy: entry } : entry;
|
|
112
375
|
}
|
|
113
376
|
|
|
114
|
-
function determineCacheStrategy(request, customStrategies,
|
|
377
|
+
function determineCacheStrategy(request, customStrategies, globalDefaults) {
|
|
115
378
|
const override = request.headers.get("X-SW-Strategy");
|
|
116
|
-
if (override)
|
|
379
|
+
if (override) {
|
|
380
|
+
return { strategy: override };
|
|
381
|
+
}
|
|
117
382
|
const path = new URL(request.url).pathname;
|
|
118
383
|
for (const [pattern, entry] of Object.entries(customStrategies)) {
|
|
119
|
-
if (path
|
|
384
|
+
if (matchGlob(path, pattern)) {
|
|
385
|
+
const resolved = resolveStrategyEntry(entry);
|
|
386
|
+
return { strategy: resolved.strategy };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
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
|
+
}
|
|
120
399
|
}
|
|
121
|
-
return { strategy: defaultStrategy };
|
|
400
|
+
return { strategy: globalDefaults.defaultStrategy };
|
|
122
401
|
}
|
|
123
402
|
|
|
124
403
|
function applyStrategy(event, request, config) {
|
|
125
|
-
const { strategy
|
|
126
|
-
if (strategy === "
|
|
127
|
-
|
|
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));
|
|
128
410
|
} else if (strategy === "network-first") {
|
|
129
|
-
event.respondWith(networkFirst(event, request
|
|
411
|
+
event.respondWith(networkFirst(event, request));
|
|
130
412
|
} else if (strategy === "cache-only") {
|
|
131
|
-
event.respondWith(cacheOnly(event, request
|
|
413
|
+
event.respondWith(cacheOnly(event, request));
|
|
132
414
|
} else if (strategy === "network-only") {
|
|
133
415
|
event.respondWith(networkOnly(event, request));
|
|
134
416
|
} else {
|
|
135
|
-
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";
|
|
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
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
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
|
+
});
|
|
136
494
|
}
|
|
137
495
|
}
|
|
138
496
|
|
|
139
|
-
self.addEventListener("fetch", (event) => {
|
|
140
|
-
const { request } = event
|
|
141
|
-
if (request.method !== "GET" && request.method !== "HEAD")
|
|
497
|
+
` : ""}self.addEventListener("fetch", (event) => {
|
|
498
|
+
const { request } = event;${mutationQueueEnabled ? `
|
|
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
|
+
}
|
|
504
|
+
if (!request.headers.get("X-SW-Cache-Key")) return;
|
|
505
|
+
}` : `
|
|
506
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
507
|
+
if (!request.headers.get("X-SW-Cache-Key")) return;
|
|
508
|
+
}`}
|
|
142
509
|
${cacheStrategy === "explicit-only" ? `if (!request.headers.get("X-SW-Cache-Strategy")) return;` : ""}
|
|
143
|
-
applyStrategy(event, request, determineCacheStrategy(event.request, ${JSON.stringify(strategies)}, "${defaultStrategy}"));
|
|
510
|
+
applyStrategy(event, request, determineCacheStrategy(event.request, ${JSON.stringify(strategies)}, { defaultStrategy: "${defaultStrategy}" }));
|
|
144
511
|
});
|
|
145
512
|
|
|
146
513
|
// --- Strategies ---
|
|
@@ -155,43 +522,7 @@ async function fetchWithPreload(event, request) {
|
|
|
155
522
|
}
|
|
156
523
|
` : ""}const _fetch = ${navigationPreload ? "fetchWithPreload" : `(event, request) => fetch(request)`};
|
|
157
524
|
|
|
158
|
-
|
|
159
|
-
const GLOBAL_MAX_AGE = ${maxCacheAge ?? 0};
|
|
160
|
-
|
|
161
|
-
${hasTrim ? `
|
|
162
|
-
async function trimRuntimeCache(cacheName, maxEntries, maxAge) {
|
|
163
|
-
const _maxEntries = maxEntries ?? GLOBAL_MAX_ENTRIES;
|
|
164
|
-
const _maxAge = maxAge ?? GLOBAL_MAX_AGE;
|
|
165
|
-
const cache = await caches.open(cacheName);
|
|
166
|
-
|
|
167
|
-
if (_maxEntries > 0) {
|
|
168
|
-
const keys = await cache.keys();
|
|
169
|
-
if (keys.length >= _maxEntries) {
|
|
170
|
-
const toDelete = keys.slice(0, keys.length - _maxEntries + 1);
|
|
171
|
-
await Promise.all(toDelete.map((key) => cache.delete(key)));
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (_maxAge > 0) {
|
|
176
|
-
const keys = await cache.keys();
|
|
177
|
-
const now = Date.now();
|
|
178
|
-
for (const request of keys) {
|
|
179
|
-
const response = await cache.match(request);
|
|
180
|
-
const dateHeader = response?.headers.get("date");
|
|
181
|
-
if (dateHeader) {
|
|
182
|
-
const age = now - new Date(dateHeader).getTime();
|
|
183
|
-
if (age > _maxAge) {
|
|
184
|
-
await cache.delete(request);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
` : ""}function _trim(cacheName, maxEntries, maxAge) {
|
|
191
|
-
${hasTrim ? ` trimRuntimeCache(cacheName, maxEntries, maxAge);` : ""}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function cacheFirst(event, request, maxEntries, maxAge) {
|
|
525
|
+
async function cacheFirst(event, request) {
|
|
195
526
|
const cached = await fromRuntime(request);
|
|
196
527
|
if (cached) {${staleVersionCode}
|
|
197
528
|
return markFromCache(cached);
|
|
@@ -205,75 +536,84 @@ async function cacheFirst(event, request, maxEntries, maxAge) {
|
|
|
205
536
|
|
|
206
537
|
const response = await _fetch(event, request);
|
|
207
538
|
if (response.ok) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await cacheResponse(request, response);
|
|
211
|
-
_trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
|
|
212
|
-
})(),
|
|
213
|
-
);
|
|
539
|
+
const responseToCache = response.clone();
|
|
540
|
+
event.waitUntil(cacheResponse(request, responseToCache));
|
|
214
541
|
}
|
|
215
542
|
return response;
|
|
216
543
|
}
|
|
217
544
|
|
|
218
|
-
async function networkFirst(event, request
|
|
545
|
+
async function networkFirst(event, request) {
|
|
219
546
|
try {
|
|
220
547
|
const response = await _fetch(event, request);
|
|
221
548
|
if (response.ok) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
await cacheResponse(request, response);
|
|
225
|
-
_trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
|
|
226
|
-
})(),
|
|
227
|
-
);
|
|
549
|
+
const responseToCache = response.clone();
|
|
550
|
+
event.waitUntil(cacheResponse(request, responseToCache));
|
|
228
551
|
}
|
|
229
552
|
return response;
|
|
230
553
|
} catch {
|
|
231
554
|
const cached = await fromRuntime(request);
|
|
232
|
-
if (cached)
|
|
555
|
+
if (cached) {
|
|
556
|
+
return markFromCache(cached);
|
|
557
|
+
}
|
|
233
558
|
|
|
234
559
|
const precached = await fromPrecache(request);
|
|
235
|
-
if (precached) return precached;
|
|
560
|
+
if (precached) return markFromCache(precached);
|
|
236
561
|
|
|
237
562
|
const fallback = await fromSpaFallback(request);
|
|
238
563
|
if (fallback) return fallback;
|
|
239
564
|
|
|
240
|
-
throw new Error("
|
|
565
|
+
throw new Error("Network request failed and no cached response available");
|
|
241
566
|
}
|
|
242
567
|
}
|
|
243
568
|
|
|
244
|
-
async function staleWhileRevalidate(event, request
|
|
569
|
+
async function staleWhileRevalidate(event, request) {
|
|
245
570
|
const cached = await fromRuntime(request);
|
|
246
571
|
if (cached) {
|
|
247
|
-
event.waitUntil(
|
|
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
|
-
|
|
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));
|
|
270
598
|
}
|
|
271
|
-
|
|
272
|
-
|
|
599
|
+
return markFromCache(cached);
|
|
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);
|
|
273
612
|
}
|
|
613
|
+
return response;
|
|
274
614
|
}
|
|
275
615
|
|
|
276
|
-
async function cacheOnly(event, request
|
|
616
|
+
async function cacheOnly(event, request) {
|
|
277
617
|
const cached = await fromRuntime(request);
|
|
278
618
|
if (cached) {${staleVersionCode}
|
|
279
619
|
return markFromCache(cached);
|