@swoff/cli 0.3.8 → 0.3.10

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 (228) hide show
  1. package/README.md +47 -33
  2. package/dist/__tests__/assemble-sw.test.js +14 -53
  3. package/dist/__tests__/assemble-sw.test.js.map +1 -1
  4. package/dist/__tests__/cli-integration.test.js +119 -11
  5. package/dist/__tests__/cli-integration.test.js.map +1 -1
  6. package/dist/__tests__/config-types.test.js +44 -17
  7. package/dist/__tests__/config-types.test.js.map +1 -1
  8. package/dist/__tests__/file-generators.test.js +3 -3
  9. package/dist/__tests__/file-generators.test.js.map +1 -1
  10. package/dist/__tests__/glob-matcher.test.js +86 -0
  11. package/dist/__tests__/glob-matcher.test.js.map +1 -0
  12. package/dist/__tests__/invalidation-tags.test.js +207 -23
  13. package/dist/__tests__/invalidation-tags.test.js.map +1 -1
  14. package/dist/__tests__/loader.test.js +16 -12
  15. package/dist/__tests__/loader.test.js.map +1 -1
  16. package/dist/__tests__/validator.test.js +140 -100
  17. package/dist/__tests__/validator.test.js.map +1 -1
  18. package/dist/index.js +39 -34
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/cli/help.js +12 -12
  21. package/dist/lib/cli/help.js.map +1 -1
  22. package/dist/lib/commands/add.js +84 -36
  23. package/dist/lib/commands/add.js.map +1 -1
  24. package/dist/lib/commands/assets.js +37 -0
  25. package/dist/lib/commands/assets.js.map +1 -0
  26. package/dist/lib/commands/clean.js +32 -30
  27. package/dist/lib/commands/clean.js.map +1 -1
  28. package/dist/lib/commands/generate-assets.js +44 -0
  29. package/dist/lib/commands/generate-assets.js.map +1 -0
  30. package/dist/lib/commands/generate-guide.js +22 -29
  31. package/dist/lib/commands/generate-guide.js.map +1 -1
  32. package/dist/lib/commands/generate.js +17 -24
  33. package/dist/lib/commands/generate.js.map +1 -1
  34. package/dist/lib/commands/info.js +41 -48
  35. package/dist/lib/commands/info.js.map +1 -1
  36. package/dist/lib/commands/init.js +27 -13
  37. package/dist/lib/commands/init.js.map +1 -1
  38. package/dist/lib/commands/validate.js +4 -4
  39. package/dist/lib/commands/validate.js.map +1 -1
  40. package/dist/lib/config/loader.js +25 -23
  41. package/dist/lib/config/loader.js.map +1 -1
  42. package/dist/lib/config/validator.js +169 -91
  43. package/dist/lib/config/validator.js.map +1 -1
  44. package/dist/lib/generators/asset-generator/dependency-resolver.js +76 -0
  45. package/dist/lib/generators/asset-generator/dependency-resolver.js.map +1 -0
  46. package/dist/lib/generators/asset-generator/generate.js +111 -0
  47. package/dist/lib/generators/asset-generator/generate.js.map +1 -0
  48. package/dist/lib/generators/asset-generator/guide.js +43 -0
  49. package/dist/lib/generators/asset-generator/guide.js.map +1 -0
  50. package/dist/lib/generators/asset-generator/html-patcher.js +65 -0
  51. package/dist/lib/generators/asset-generator/html-patcher.js.map +1 -0
  52. package/dist/lib/generators/asset-generator/ico-encoder.js +29 -0
  53. package/dist/lib/generators/asset-generator/ico-encoder.js.map +1 -0
  54. package/dist/lib/generators/asset-generator/manifest-patcher.js +23 -0
  55. package/dist/lib/generators/asset-generator/manifest-patcher.js.map +1 -0
  56. package/dist/lib/generators/asset-generator/maskable.js +15 -0
  57. package/dist/lib/generators/asset-generator/maskable.js.map +1 -0
  58. package/dist/lib/generators/asset-generator/rasterize.js +32 -0
  59. package/dist/lib/generators/asset-generator/rasterize.js.map +1 -0
  60. package/dist/lib/generators/asset-generator/sizes.js +25 -0
  61. package/dist/lib/generators/asset-generator/sizes.js.map +1 -0
  62. package/dist/lib/generators/file-generators/api-config.js +6 -0
  63. package/dist/lib/generators/file-generators/api-config.js.map +1 -0
  64. package/dist/lib/generators/file-generators/auth-state.js +2 -34
  65. package/dist/lib/generators/file-generators/auth-state.js.map +1 -1
  66. package/dist/lib/generators/file-generators/auth-store.js +2 -250
  67. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  68. package/dist/lib/generators/file-generators/auth-user.js +3 -81
  69. package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
  70. package/dist/lib/generators/file-generators/background-sync.js +2 -57
  71. package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
  72. package/dist/lib/generators/file-generators/cache.js +2 -38
  73. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  74. package/dist/lib/generators/file-generators/client-injector.js +2 -91
  75. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  76. package/dist/lib/generators/file-generators/fetch-state.js +6 -0
  77. package/dist/lib/generators/file-generators/fetch-state.js.map +1 -0
  78. package/dist/lib/generators/file-generators/fetch-wrapper.js +3 -267
  79. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  80. package/dist/lib/generators/file-generators/generate-framework-adapters.js +56 -0
  81. package/dist/lib/generators/file-generators/generate-framework-adapters.js.map +1 -0
  82. package/dist/lib/generators/file-generators/generate-hooks.js +4 -3
  83. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  84. package/dist/lib/generators/file-generators/gql-wrapper.js +3 -145
  85. package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -1
  86. package/dist/lib/generators/file-generators/guide-generator.js +24 -630
  87. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  88. package/dist/lib/generators/file-generators/invalidation-tags.js +18 -86
  89. package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
  90. package/dist/lib/generators/file-generators/manifest.js +15 -5
  91. package/dist/lib/generators/file-generators/manifest.js.map +1 -1
  92. package/dist/lib/generators/file-generators/mutation-queue.js +3 -285
  93. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  94. package/dist/lib/generators/file-generators/mutation-state.js +2 -117
  95. package/dist/lib/generators/file-generators/mutation-state.js.map +1 -1
  96. package/dist/lib/generators/file-generators/push.js +6 -139
  97. package/dist/lib/generators/file-generators/push.js.map +1 -1
  98. package/dist/lib/generators/file-generators/pwa-install.js +11 -84
  99. package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
  100. package/dist/lib/generators/file-generators/quick-readme.js +8 -125
  101. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
  102. package/dist/lib/generators/file-generators/reset.js +6 -0
  103. package/dist/lib/generators/file-generators/reset.js.map +1 -0
  104. package/dist/lib/generators/file-generators/server-push.js +3 -124
  105. package/dist/lib/generators/file-generators/server-push.js.map +1 -1
  106. package/dist/lib/generators/file-generators/sw-generator-build.js +8 -9
  107. package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
  108. package/dist/lib/generators/file-generators/sw-injector.js +10 -230
  109. package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
  110. package/dist/lib/generators/file-generators/type-definitions.js +2 -114
  111. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
  112. package/dist/lib/generators/sw-generator.js +10 -20
  113. package/dist/lib/generators/sw-generator.js.map +1 -1
  114. package/dist/lib/generators/sw-sections/activate-handler.js +25 -3
  115. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  116. package/dist/lib/generators/sw-sections/assemble-sw.js +20 -16
  117. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  118. package/dist/lib/generators/sw-sections/background-sync-handler.js +68 -19
  119. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
  120. package/dist/lib/generators/sw-sections/config-header.js +2 -2
  121. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  122. package/dist/lib/generators/sw-sections/fetch-handler.js +506 -144
  123. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  124. package/dist/lib/generators/sw-sections/glob-matcher-code.js.map +1 -0
  125. package/dist/lib/generators/sw-sections/message-handler.js +55 -15
  126. package/dist/lib/generators/sw-sections/message-handler.js.map +1 -1
  127. package/dist/lib/generators/sw-sections/server-push-handler.js +12 -0
  128. package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -1
  129. package/dist/lib/generators/sw-sections/sw-push.js +67 -0
  130. package/dist/lib/generators/sw-sections/sw-push.js.map +1 -0
  131. package/dist/lib/generators/sw-sections/tag-management.js +65 -23
  132. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  133. package/dist/lib/generators/swoff-files-generator.js +66 -45
  134. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  135. package/dist/lib/shared/config-types.js +182 -71
  136. package/dist/lib/shared/config-types.js.map +1 -1
  137. package/dist/lib/shared/glob-matcher.js +77 -0
  138. package/dist/lib/shared/glob-matcher.js.map +1 -0
  139. package/dist/lib/utils/tty-status.js +21 -0
  140. package/dist/lib/utils/tty-status.js.map +1 -0
  141. package/dist/runtime/api-config.js +18 -0
  142. package/dist/runtime/api-config.js.map +1 -0
  143. package/dist/runtime/auth-state.js +35 -0
  144. package/dist/runtime/auth-state.js.map +1 -0
  145. package/dist/runtime/auth-store.js +273 -0
  146. package/dist/runtime/auth-store.js.map +1 -0
  147. package/dist/runtime/auth-user.js +168 -0
  148. package/dist/runtime/auth-user.js.map +1 -0
  149. package/dist/runtime/background-sync.js +59 -0
  150. package/dist/runtime/background-sync.js.map +1 -0
  151. package/dist/runtime/cache.js +30 -0
  152. package/dist/runtime/cache.js.map +1 -0
  153. package/dist/runtime/client-injector.js +136 -0
  154. package/dist/runtime/client-injector.js.map +1 -0
  155. package/dist/runtime/fetch-state.js +42 -0
  156. package/dist/runtime/fetch-state.js.map +1 -0
  157. package/dist/runtime/fetch-wrapper.js +355 -0
  158. package/dist/runtime/fetch-wrapper.js.map +1 -0
  159. package/dist/runtime/gql-wrapper.js +135 -0
  160. package/dist/runtime/gql-wrapper.js.map +1 -0
  161. package/dist/runtime/invalidation-tags.js +260 -0
  162. package/dist/runtime/invalidation-tags.js.map +1 -0
  163. package/dist/runtime/mutation-queue.js +304 -0
  164. package/dist/runtime/mutation-queue.js.map +1 -0
  165. package/dist/runtime/mutation-state.js +117 -0
  166. package/dist/runtime/mutation-state.js.map +1 -0
  167. package/dist/runtime/push.js +138 -0
  168. package/dist/runtime/push.js.map +1 -0
  169. package/dist/runtime/pwa-index.js +11 -0
  170. package/dist/runtime/pwa-index.js.map +1 -0
  171. package/dist/runtime/pwa-injector.js +44 -0
  172. package/dist/runtime/pwa-injector.js.map +1 -0
  173. package/dist/runtime/pwa-install.js +83 -0
  174. package/dist/runtime/pwa-install.js.map +1 -0
  175. package/dist/runtime/pwa-prompt.js +54 -0
  176. package/dist/runtime/pwa-prompt.js.map +1 -0
  177. package/dist/runtime/reset.js +148 -0
  178. package/dist/runtime/reset.js.map +1 -0
  179. package/dist/runtime/server-push.js +148 -0
  180. package/dist/runtime/server-push.js.map +1 -0
  181. package/dist/runtime/sw-injector.js +236 -0
  182. package/dist/runtime/sw-injector.js.map +1 -0
  183. package/dist/runtime/type-definitions.js +138 -0
  184. package/dist/runtime/type-definitions.js.map +1 -0
  185. package/dist/runtime/utils.js +27 -0
  186. package/dist/runtime/utils.js.map +1 -0
  187. package/package.json +3 -1
  188. package/templates/react/useAuth.jsx +106 -0
  189. package/templates/react/useAuth.tsx +118 -0
  190. package/templates/{hooks → react}/useBackgroundSync.jsx +6 -4
  191. package/templates/{hooks → react}/useBackgroundSync.tsx +6 -4
  192. package/templates/{hooks → react}/useCacheInvalidation.jsx +2 -2
  193. package/templates/react/useCacheInvalidation.tsx +52 -0
  194. package/templates/react/useCachedFetch.jsx +140 -0
  195. package/templates/react/useCachedFetch.tsx +160 -0
  196. package/templates/react/useIsFetching.jsx +32 -0
  197. package/templates/react/useIsFetching.tsx +32 -0
  198. package/templates/react/useMutation.jsx +141 -0
  199. package/templates/react/useMutation.tsx +180 -0
  200. package/templates/react/useMutationQueue.jsx +69 -0
  201. package/templates/react/useMutationQueue.tsx +83 -0
  202. package/templates/react/useMutationState.jsx +13 -0
  203. package/templates/react/useMutationState.tsx +25 -0
  204. package/templates/react/useNetworkStatus.jsx +61 -0
  205. package/templates/react/useNetworkStatus.tsx +76 -0
  206. package/templates/react/usePrefetch.jsx +32 -0
  207. package/templates/react/usePrefetch.tsx +42 -0
  208. package/templates/{hooks → react}/usePushSubscription.jsx +7 -7
  209. package/templates/{hooks → react}/usePushSubscription.tsx +7 -7
  210. package/templates/{hooks → react}/useSWUpdate.jsx +3 -3
  211. package/templates/{hooks → react}/useSWUpdate.tsx +4 -4
  212. package/templates/react/useSwoffReset.jsx +32 -0
  213. package/templates/react/useSwoffReset.tsx +39 -0
  214. package/templates/hooks/useAuth.jsx +0 -30
  215. package/templates/hooks/useAuth.tsx +0 -30
  216. package/templates/hooks/useCacheInvalidation.tsx +0 -19
  217. package/templates/hooks/useCachedFetch.jsx +0 -94
  218. package/templates/hooks/useCachedFetch.tsx +0 -112
  219. package/templates/hooks/useMutation.jsx +0 -76
  220. package/templates/hooks/useMutation.tsx +0 -97
  221. package/templates/hooks/useMutationQueue.jsx +0 -42
  222. package/templates/hooks/useMutationQueue.tsx +0 -49
  223. package/templates/hooks/useMutationState.jsx +0 -27
  224. package/templates/hooks/useMutationState.tsx +0 -41
  225. package/templates/hooks/useNetworkStatus.jsx +0 -19
  226. package/templates/hooks/useNetworkStatus.tsx +0 -19
  227. package/templates/hooks/usePrefetch.jsx +0 -10
  228. package/templates/hooks/usePrefetch.tsx +0 -25
@@ -6,43 +6,337 @@
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
- * Each tier also carries: staleTime, maxCacheEntries, maxCacheAge
10
- * Per-request values override route values, which override globals.
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
- * 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)
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", staleTime, maxCacheEntries, maxCacheAge, navigationPreload, navigationMode, spaEntry } = swConfig;
21
- const navMode = navigationMode ?? "spa";
22
- const spaPath = spaEntry ?? "/index.html";
23
- const staleVersionCode = tagInvalidation ? `
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 || {};
27
+ const staleVersionCode = tagInvalidation
28
+ ? `
24
29
  cleanStaleVersions();
25
30
  if (staleVersions.has(cacheKey(request))) {
26
- event.waitUntil(refreshCache(request).then(() => staleVersions.delete(cacheKey(request))));
27
- }` : "";
28
- const tagCode = tagInvalidation ? `
31
+ queueRefresh(cacheKey(request), new URL(request.url).href);
32
+ }`
33
+ : "";
34
+ const tagCode = tagInvalidation
35
+ ? `
29
36
  const tagsHeader = request.headers.get("X-SW-Cache-Tags");
30
37
  if (tagsHeader) {
31
38
  const cacheKeyUrl = cacheKey(request);
32
39
  const actualUrl = new URL(request.url).href;
33
40
  const tags = tagsHeader.split(",").map((t) => t.trim());
34
41
  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};`;
42
+ }`
43
+ : "";
44
+ const reactGlobalDefaults = {
45
+ staleTime: globalStaleTime,
46
+ refetchInterval: globalRefetchInterval,
47
+ refetchOnReconnect: globalRefetchOnReconnect,
48
+ refetchOnFocus: globalRefetchOnFocus,
49
+ };
50
+ const reactivePatterns = Object.entries(strategies)
51
+ .filter(([, entry]) => {
52
+ const resolved = typeof entry === "string" ? { strategy: entry } : entry;
53
+ return resolved.strategy === "reactive";
54
+ })
55
+ .map(([pattern, entry]) => {
56
+ const resolved = typeof entry === "string" ? { strategy: entry } : entry;
57
+ return {
58
+ pattern,
59
+ staleTime: resolved.staleTime ?? reactGlobalDefaults.staleTime,
60
+ refetchInterval: resolved.refetchInterval ?? reactGlobalDefaults.refetchInterval,
61
+ refetchOnReconnect: resolved.refetchOnReconnect ?? reactGlobalDefaults.refetchOnReconnect,
62
+ refetchOnFocus: resolved.refetchOnFocus ?? reactGlobalDefaults.refetchOnFocus,
63
+ };
64
+ });
65
+ const trimDecl = `const REFETCH_BATCH_SIZE = ${refetchBatchSize};
66
+ const REFETCH_BATCH_DELAY_MS = ${refetchBatchDelayMs};
67
+ const REFRESH_MAX_RETRIES = ${refetchMaxRetries};
68
+ const REFRESH_RETRY_DELAY_MS = ${refetchRetryDelayMs};`;
69
+ const GLOB_CODE = "\n" +
70
+ "// --- Glob Pattern Matching ---\n" +
71
+ "\n" +
72
+ "function escapeGlobMeta(s) {\n" +
73
+ ' return s.replace(/[.+^${}()|[\\]\\\\]/g, "\\\\$$&");\n' +
74
+ "}\n" +
75
+ "\n" +
76
+ "function globPartRe(part) {\n" +
77
+ ' for (var o = "", i = 0; i < part.length; i++) {\n' +
78
+ " var c = part[i];\n" +
79
+ ' if (c === "*") { o += "[^/]*"; }\n' +
80
+ ' else if (c === "?") { o += "[^/]"; }\n' +
81
+ ' else if (c === "{") {\n' +
82
+ ' var cl = part.indexOf("}", i);\n' +
83
+ ' if (cl === -1) { o += "\\\\" + c; }\n' +
84
+ " else {\n" +
85
+ ' o += "(?:" + part.slice(i + 1, cl).split(",").map(function(s) { return escapeGlobMeta(s.trim()); }).join("|") + ")";\n' +
86
+ " i = cl;\n" +
87
+ " }\n" +
88
+ ' } else { o += "\\\\" + c; }\n' +
89
+ " }\n" +
90
+ " return o;\n" +
91
+ "}\n" +
92
+ "\n" +
93
+ "function matchGlob(path, pattern) {\n" +
94
+ ' if (pattern.charAt(0) === "!") return !matchGlob(path, pattern.slice(1));\n' +
95
+ ' var parts = pattern.split("/").filter(Boolean);\n' +
96
+ ' var pps = path.split("/").filter(Boolean);\n' +
97
+ " var pi = 0, ppi = 0;\n" +
98
+ " while (pi < parts.length && ppi < pps.length) {\n" +
99
+ " var part = parts[pi];\n" +
100
+ ' if (part === "**") {\n' +
101
+ " if (pi === parts.length - 1) return true;\n" +
102
+ " var nxt = parts[pi + 1];\n" +
103
+ " var found = -1;\n" +
104
+ " for (var j = ppi; j < pps.length; j++) {\n" +
105
+ ' if (nxt.indexOf("*") > -1 || nxt.indexOf("?") > -1 || nxt.indexOf("{") > -1) {\n' +
106
+ ' if (new RegExp("^" + globPartRe(nxt) + "$").test(pps[j])) { found = j; break; }\n' +
107
+ " } else if (nxt === pps[j]) { found = j; break; }\n" +
108
+ " }\n" +
109
+ " if (found === -1) return false;\n" +
110
+ " ppi = found;\n" +
111
+ " pi++;\n" +
112
+ " continue;\n" +
113
+ " }\n" +
114
+ ' if (part.indexOf("*") > -1 || part.indexOf("?") > -1 || part.indexOf("{") > -1) {\n' +
115
+ ' if (!new RegExp("^" + globPartRe(part) + "$").test(pps[ppi])) return false;\n' +
116
+ " } else if (part !== pps[ppi]) return false;\n" +
117
+ " pi++;\n" +
118
+ " ppi++;\n" +
119
+ " }\n" +
120
+ " return pi === parts.length && ppi === pps.length;\n" +
121
+ "}\n" +
122
+ "\n";
39
123
  return `${trimDecl}
124
+ // --- Batch Refresh Queue ---
125
+
126
+ const _refreshQueue = new Map();
127
+ let _refreshQueuePromise = null;
128
+
129
+ function queueRefresh(cacheKeyUrl, actualUrl${tagInvalidation ? ", tags" : ""}${tagInvalidation ? ", method, body, contentType" : ""}) {
130
+ // Use Map keyed by cacheKey for proper deduplication${tagInvalidation
131
+ ? `
132
+ // Don't let a tagless (SWR/reactive) refresh override an invalidation-triggered entry with tags
133
+ if (_refreshQueue.has(cacheKeyUrl) && !tags) return;`
134
+ : ""}
135
+ _refreshQueue.set(cacheKeyUrl, { cacheKey: cacheKeyUrl, actualUrl: actualUrl || cacheKeyUrl, retryCount: 0${tagInvalidation ? ", tags: tags || null, method: method || null, body: body || null, contentType: contentType || null" : ""} });
136
+ if (!_refreshQueuePromise) {
137
+ _refreshQueuePromise = _processRefreshQueue();
138
+ }
139
+ return _refreshQueuePromise;
140
+ }
141
+
142
+ async function resolveActualUrl(entry) {
143
+ if (entry.actualUrl) return entry.actualUrl;
144
+ const url = entry.cacheKey;
145
+ if (url.includes("/__swc/")) {
146
+ // Virtual cache key — look up the real URL from tag registry
147
+ try {
148
+ const db = await openTagDB();
149
+ const tx = db.transaction(TAG_STORE_NAME, "readonly");
150
+ const store = tx.objectStore(TAG_STORE_NAME);
151
+ const stored = await new Promise((resolve) => {
152
+ const req = store.get(url);
153
+ req.onsuccess = () => resolve(req.result);
154
+ req.onerror = () => resolve(null);
155
+ });
156
+ db.close();
157
+ if (stored?.actualUrl) return stored.actualUrl;
158
+ } catch {}
159
+ }
160
+ return url;
161
+ }
162
+
163
+ async function _processRefreshQueue() {
164
+ try {
165
+ while (_refreshQueue.size > 0) {
166
+ const batch = [];
167
+ for (const [cacheKey, entry] of _refreshQueue) {
168
+ if (batch.length >= REFETCH_BATCH_SIZE) break;
169
+ if (entry.nextRetryAt && entry.nextRetryAt > Date.now()) continue;
170
+ batch.push(entry);
171
+ _refreshQueue.delete(cacheKey);
172
+ }
173
+ if (batch.length === 0) break;
174
+
175
+ const clients = await self.clients.matchAll();
176
+
177
+ await Promise.allSettled(batch.map(async (entry) => {
178
+ const fetchUrl = await resolveActualUrl(entry);
179
+ try {
180
+ const fetchOpts = {};
181
+ if (entry.method && entry.method !== "GET" && entry.method !== "HEAD") {
182
+ fetchOpts.method = entry.method;
183
+ if (entry.body) fetchOpts.body = entry.body;
184
+ if (entry.contentType) fetchOpts.headers = { "Content-Type": entry.contentType };
185
+ }
186
+ const response = await fetch(fetchUrl, fetchOpts);
187
+ if (response.ok) {
188
+ const request = new Request(entry.cacheKey);
189
+ await storeRuntime(request, response);${tagInvalidation
190
+ ? `
191
+ if (entry.tags && typeof cacheTagUrl !== "undefined") {
192
+ await cacheTagUrl(entry.cacheKey, fetchUrl, entry.tags, entry.method, entry.body, entry.contentType);
193
+ }`
194
+ : ""}
195
+ // Clean up stale version tracking on successful refresh
196
+ if (typeof staleVersions !== "undefined" && staleVersions.has(entry.cacheKey)) {
197
+ staleVersions.delete(entry.cacheKey);
198
+ }
199
+ for (const client of clients) {
200
+ client.postMessage({ type: "CACHE_UPDATED", url: fetchUrl });
201
+ }
202
+ }
203
+ } catch {
204
+ // Refresh failed — retry with exponential backoff
205
+ if (entry.retryCount < REFRESH_MAX_RETRIES) {
206
+ entry.retryCount++;
207
+ const delay = REFRESH_RETRY_DELAY_MS * Math.pow(2, entry.retryCount - 1);
208
+ entry.nextRetryAt = Date.now() + delay;
209
+ _refreshQueue.set(entry.cacheKey, entry);
210
+ setTimeout(() => {
211
+ if (!_refreshQueuePromise) {
212
+ _refreshQueuePromise = _processRefreshQueue();
213
+ }
214
+ }, delay);
215
+ }
216
+ }
217
+ }));
218
+
219
+ if (_refreshQueue.size > 0 && REFETCH_BATCH_DELAY_MS > 0) {
220
+ await new Promise(r => setTimeout(r, REFETCH_BATCH_DELAY_MS));
221
+ }
222
+ }
223
+ } finally {
224
+ _refreshQueuePromise = null;
225
+ }
226
+ }
227
+
228
+ // --- Reactive Patterns (extracted for interval/focus/reconnect) ---
229
+
230
+ const REACTIVE_PATTERNS = ${JSON.stringify(reactivePatterns)};
231
+
232
+ function findReactiveConfig(url) {
233
+ const path = new URL(url).pathname;
234
+ for (const cfg of REACTIVE_PATTERNS) {
235
+ if (matchGlob(path, cfg.pattern)) return cfg;
236
+ }
237
+ return null;
238
+ }
239
+
240
+ function shouldReactiveRefresh(cached, config) {
241
+ if (!config.staleTime && config.staleTime !== 0) return true;
242
+ if (config.staleTime === 0) return true;
243
+ return isStale(cached, config.staleTime);
244
+ }
245
+
246
+ // --- Reactive Interval Timers ---
247
+
248
+ ${(() => {
249
+ const patternsWithInterval = reactivePatterns.filter((p) => !!p.refetchInterval && p.refetchInterval > 0);
250
+ if (patternsWithInterval.length === 0)
251
+ return "";
252
+ return `// Start intervals for reactive patterns with refetchInterval
253
+ (async () => {
254
+ ${patternsWithInterval
255
+ .map((p) => ` setInterval(async () => {
256
+ const cache = await caches.open(CACHE_NAME_RUNTIME);
257
+ const keys = await cache.keys();
258
+ for (const request of keys) {
259
+ const url = new URL(request.url);
260
+ if (url.pathname.startsWith("/__swc/")) continue;
261
+ if (!matchGlob(url.pathname, "${p.pattern}")) continue;
262
+ const config = findReactiveConfig(url.href);
263
+ if (!config) continue;
264
+ const cached = await cache.match(request);
265
+ if (cached && shouldReactiveRefresh(cached, config)) {
266
+ queueRefresh(request.url, url.href);
267
+ }
268
+ }
269
+ }, ${p.refetchInterval * 1000});`)
270
+ .join("\n")}
271
+ })();
272
+ `;
273
+ })()}
274
+ async function handleOnline() {
275
+ const clients = await self.clients.matchAll();
276
+ if (clients.length === 0) return;
277
+
278
+ // Step 1: Retry stale version entries (failed refetches after invalidation)
279
+ if (typeof staleVersions !== "undefined") {
280
+ const staleUrls = [...staleVersions.keys()];
281
+ for (const url of staleUrls) {
282
+ queueRefresh(url, url);
283
+ }
284
+ }
285
+
286
+ // Step 2: Refresh reactive entries with refetchOnReconnect
287
+ const cache = await caches.open(CACHE_NAME_RUNTIME);
288
+ const keys = await cache.keys();
289
+ for (const request of keys) {
290
+ const url = new URL(request.url);
291
+ if (url.pathname.startsWith("/__swc/")) continue;
292
+ const config = findReactiveConfig(url.href);
293
+ if (!config || !config.refetchOnReconnect) continue;
294
+ const cached = await cache.match(request);
295
+ if (cached && shouldReactiveRefresh(cached, config)) {
296
+ queueRefresh(request.url, url.href);
297
+ }
298
+ }
299
+ }
300
+
301
+ async function handleOnFocus() {
302
+ const cache = await caches.open(CACHE_NAME_RUNTIME);
303
+ const keys = await cache.keys();
304
+ for (const request of keys) {
305
+ const url = new URL(request.url);
306
+ if (url.pathname.startsWith("/__swc/")) continue;
307
+ const config = findReactiveConfig(url.href);
308
+ if (!config || !config.refetchOnFocus) continue;
309
+ const cached = await cache.match(request);
310
+ if (cached && shouldReactiveRefresh(cached, config)) {
311
+ queueRefresh(request.url, url.href);
312
+ }
313
+ }
314
+ }
315
+
40
316
  // --- Cache Key ---
41
317
 
42
318
  function cacheKey(request) {
43
319
  const key = request.headers.get("X-SW-Cache-Key");
44
320
  if (key) return new URL("/__swc/" + key, self.location.origin).href;
45
- return new URL(request.url).href;
321
+ const url = new URL(request.url);${normalizeCacheKey
322
+ ? `
323
+ // Sort query params alphabetically for consistent cache keys
324
+ if (url.search) {
325
+ const params = new URLSearchParams(url.search);
326
+ params.sort();
327
+ url.search = params.toString();
328
+ }`
329
+ : ""}${ignoreQueryParams && ignoreQueryParams.length > 0
330
+ ? `
331
+ // Strip configured query params from cache key
332
+ const ignore = ${JSON.stringify(ignoreQueryParams)};
333
+ if (url.search) {
334
+ const params = new URLSearchParams(url.search);
335
+ for (const key of ignore) params.delete(key);
336
+ url.search = params.toString();
337
+ }`
338
+ : ""}
339
+ return url.href;
46
340
  }
47
341
 
48
342
  // --- Cache Helpers ---
@@ -70,7 +364,23 @@ async function storeRuntime(request, response) {
70
364
  }
71
365
 
72
366
  async function cacheResponse(request, response) {
73
- await storeRuntime(request, response);${tagCode}
367
+ await storeRuntime(request, response);
368
+ const tagsHeader = request.headers.get("X-SW-Cache-Tags");
369
+ if (tagsHeader) {
370
+ const cacheKeyUrl = cacheKey(request);
371
+ const actualUrl = new URL(request.url).href;
372
+ const tags = tagsHeader.split(",").map((t) => t.trim());
373
+ const method = request.method;
374
+ let body = null;
375
+ let contentType = null;
376
+ if (method !== "GET" && method !== "HEAD") {
377
+ contentType = request.headers.get("Content-Type");
378
+ try {
379
+ body = await request.clone().text();
380
+ } catch {}
381
+ }
382
+ await cacheTagUrl(cacheKeyUrl, actualUrl, tags, method, body, contentType);
383
+ }
74
384
  }
75
385
 
76
386
  async function fromSpaFallback(request) {
@@ -93,13 +403,13 @@ function markFromCache(response) {
93
403
  }
94
404
 
95
405
  function isStale(response, staleTimeSeconds) {
96
- if (!staleTimeSeconds || staleTimeSeconds <= 0) return false;
406
+ if (staleTimeSeconds == null || staleTimeSeconds <= 0) return false;
97
407
  const cachedAt = response.headers.get("X-SW-Cached-At");
98
408
  if (!cachedAt) return false;
99
409
  return Date.now() - Number(cachedAt) > staleTimeSeconds * 1000;
100
410
  }
101
411
 
102
- // --- Strategy Selection (3-tier config resolution) ---
412
+ ${GLOB_CODE}// --- Strategy Selection (3-tier config resolution) ---
103
413
 
104
414
  function resolveStrategyEntry(entry) {
105
415
  return typeof entry === "string" ? { strategy: entry } : entry;
@@ -108,60 +418,157 @@ function resolveStrategyEntry(entry) {
108
418
  function determineCacheStrategy(request, customStrategies, globalDefaults) {
109
419
  const override = request.headers.get("X-SW-Strategy");
110
420
  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
- };
421
+ const cfg = { strategy: override };
422
+ const hStale = request.headers.get("X-SW-Stale-Time");
423
+ if (hStale !== null) cfg.staleTime = Number(hStale);
424
+ const hFocus = request.headers.get("X-SW-Refetch-On-Focus");
425
+ if (hFocus !== null) cfg.refetchOnFocus = hFocus !== "false";
426
+ const hReconnect = request.headers.get("X-SW-Refetch-On-Reconnect");
427
+ if (hReconnect !== null) cfg.refetchOnReconnect = hReconnect !== "false";
428
+ return cfg;
117
429
  }
118
430
  const path = new URL(request.url).pathname;
119
431
  for (const [pattern, entry] of Object.entries(customStrategies)) {
120
- if (path.startsWith(pattern.replace("*", ""))) {
432
+ if (matchGlob(path, pattern)) {
121
433
  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
- };
434
+ return { strategy: resolved.strategy };
128
435
  }
129
436
  }
130
- return {
131
- strategy: globalDefaults.defaultStrategy,
132
- staleTime: globalDefaults.staleTime,
133
- maxCacheEntries: globalDefaults.maxCacheEntries,
134
- maxCacheAge: globalDefaults.maxCacheAge,
135
- };
437
+ return { strategy: globalDefaults.defaultStrategy };
438
+ }
439
+
440
+ function determineCacheStrategyForUrl(url, customStrategies, globalDefaults) {
441
+ const path = new URL(url).pathname;
442
+ for (const [pattern, entry] of Object.entries(customStrategies)) {
443
+ if (matchGlob(path, pattern)) {
444
+ const resolved = resolveStrategyEntry(entry);
445
+ return { strategy: resolved.strategy };
446
+ }
447
+ }
448
+ return { strategy: globalDefaults.defaultStrategy };
136
449
  }
137
450
 
138
451
  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));
452
+ const { strategy } = config;
453
+ if (strategy === "reactive") {
454
+ const reactiveCfg = findReactiveConfig(new URL(request.url).href);
455
+ const staleTime = config.staleTime !== undefined ? config.staleTime : reactiveCfg?.staleTime;
456
+ event.respondWith(reactiveStrategy(event, request, staleTime));
457
+ } else if (strategy === "stale-while-revalidate") {
458
+ event.respondWith(staleWhileRevalidate(event, request));
142
459
  } else if (strategy === "network-first") {
143
- event.respondWith(networkFirst(event, request, staleTime, maxCacheEntries, maxCacheAge));
460
+ event.respondWith(networkFirst(event, request));
144
461
  } else if (strategy === "cache-only") {
145
- event.respondWith(cacheOnly(event, request, staleTime, maxCacheEntries, maxCacheAge));
462
+ event.respondWith(cacheOnly(event, request));
146
463
  } else if (strategy === "network-only") {
147
464
  event.respondWith(networkOnly(event, request));
148
465
  } else {
149
- event.respondWith(cacheFirst(event, request, staleTime, maxCacheEntries, maxCacheAge));
466
+ event.respondWith(cacheFirst(event, request));
467
+ }
468
+ }
469
+
470
+ ${mutationQueueEnabled
471
+ ? `// --- Mutation Queue IDB Helpers ---
472
+
473
+ const MUTATION_DB_NAME = "swoff-queue";
474
+ const MUTATION_STORE_NAME = "mutations";
475
+ // Bump this when adding new indexes/stores for schema migration
476
+ const MUTATION_DB_VERSION = 1;
477
+
478
+ function openMutationQueueDB() {
479
+ return new Promise((resolve, reject) => {
480
+ const request = indexedDB.open(MUTATION_DB_NAME, MUTATION_DB_VERSION);
481
+ request.onupgradeneeded = (e) => {
482
+ const db = e.target.result;
483
+ if (!db.objectStoreNames.contains(MUTATION_STORE_NAME)) {
484
+ const store = db.createObjectStore(MUTATION_STORE_NAME, { keyPath: "id" });
485
+ store.createIndex("by-timestamp", "timestamp");
486
+ }
487
+ };
488
+ request.onsuccess = (e) => resolve(e.target.result);
489
+ request.onerror = (e) => reject(e.target.error);
490
+ });
491
+ }
492
+
493
+ async function storeMutationInSW(request) {
494
+ let body, bodyType;
495
+ try {
496
+ body = await request.clone().json();
497
+ bodyType = "json";
498
+ } catch {
499
+ body = await request.clone().text();
500
+ bodyType = "text";
501
+ }
502
+
503
+ const tagsHeader = request.headers.get("X-SW-Invalidate-Tags");
504
+ const tags = tagsHeader ? tagsHeader.split(",").map(function(t) { return t.trim(); }) : [];
505
+
506
+ const db = await openMutationQueueDB();
507
+ const tx = db.transaction(MUTATION_STORE_NAME, "readwrite");
508
+ tx.objectStore(MUTATION_STORE_NAME).add({
509
+ id: crypto.randomUUID(),
510
+ method: request.method,
511
+ url: new URL(request.url).href,
512
+ body,
513
+ bodyType,
514
+ headers: request.headers.get("Content-Type") ? { "Content-Type": request.headers.get("Content-Type") } : {},
515
+ timestamp: Date.now(),
516
+ retryCount: 0,
517
+ nextRetryAt: 0,
518
+ tags,
519
+ });
520
+ await new Promise(function(resolve, reject) {
521
+ tx.oncomplete = function() { resolve(); };
522
+ tx.onerror = function() { reject(tx.error); };
523
+ });
524
+ }
525
+
526
+ async function handleMutation(event) {
527
+ const request = event.request;
528
+ if (request.headers.get("X-SW-No-Queue") === "true") {
529
+ return fetch(request.clone());
530
+ }
531
+ try {
532
+ return await fetch(request.clone());
533
+ } catch {
534
+ await storeMutationInSW(request);
535
+ // Notify open clients that a mutation was stored so they can try to process
536
+ const clients = await self.clients.matchAll();
537
+ clients.forEach(function(client) {
538
+ client.postMessage({ type: "MUTATION_STORED" });
539
+ });
540
+ const queuedBody = JSON.stringify({ queued: true });
541
+ return new Response(queuedBody, {
542
+ status: 202,
543
+ statusText: "Accepted",
544
+ headers: { "Content-Type": "application/json", "X-SW-Mutation-Queued": "true" },
545
+ });
150
546
  }
151
547
  }
152
548
 
153
- self.addEventListener("fetch", (event) => {
154
- const { request } = event;
549
+ `
550
+ : ""}self.addEventListener("fetch", (event) => {
551
+ const { request } = event;${mutationQueueEnabled
552
+ ? `
155
553
  if (request.method !== "GET" && request.method !== "HEAD") {
554
+ if (request.headers.get("X-SW-Cache-Strategy") === "mutation") {
555
+ event.respondWith(handleMutation(event));
556
+ return;
557
+ }
156
558
  if (!request.headers.get("X-SW-Cache-Key")) return;
157
- }
559
+ }`
560
+ : `
561
+ if (request.method !== "GET" && request.method !== "HEAD") {
562
+ if (!request.headers.get("X-SW-Cache-Key")) return;
563
+ }`}
158
564
  ${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 }));
565
+ applyStrategy(event, request, determineCacheStrategy(event.request, ${JSON.stringify(strategies)}, { defaultStrategy: "${defaultStrategy}" }));
160
566
  });
161
567
 
162
568
  // --- Strategies ---
163
569
 
164
- ${navigationPreload ? `
570
+ ${navigationPreload
571
+ ? `
165
572
  async function fetchWithPreload(event, request) {
166
573
  try {
167
574
  const preload = await event.preloadResponse;
@@ -169,19 +576,12 @@ async function fetchWithPreload(event, request) {
169
576
  } catch {}
170
577
  return fetch(request);
171
578
  }
172
- ` : ""}const _fetch = ${navigationPreload ? "fetchWithPreload" : `(event, request) => fetch(request)`};
173
-
174
- ${generateTrimCode(maxCacheEntries, maxCacheAge)}
175
- function _trim(cacheName, maxEntries, maxAge) {
176
- ${maxCacheEntries || maxCacheAge ? ` trimRuntimeCache(cacheName, maxEntries, maxAge);` : ""}
177
- }
579
+ `
580
+ : ""}const _fetch = ${navigationPreload ? "fetchWithPreload" : `(event, request) => fetch(request)`};
178
581
 
179
- async function cacheFirst(event, request, staleTime, maxEntries, maxAge) {
582
+ async function cacheFirst(event, request) {
180
583
  const cached = await fromRuntime(request);
181
584
  if (cached) {${staleVersionCode}
182
- if (isStale(cached, staleTime)) {
183
- event.waitUntil(refreshCache(request));
184
- }
185
585
  return markFromCache(cached);
186
586
  }
187
587
 
@@ -191,96 +591,92 @@ async function cacheFirst(event, request, staleTime, maxEntries, maxAge) {
191
591
  const fallback = await fromSpaFallback(request);
192
592
  if (fallback) return fallback;
193
593
 
594
+ const reqForCache = request.clone();
194
595
  const response = await _fetch(event, request);
195
596
  if (response.ok) {
196
- event.waitUntil(
197
- (async () => {
198
- await cacheResponse(request, response);
199
- _trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
200
- })(),
201
- );
597
+ const responseToCache = response.clone();
598
+ event.waitUntil(cacheResponse(reqForCache, responseToCache));
202
599
  }
203
600
  return response;
204
601
  }
205
602
 
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
- }
214
-
603
+ async function networkFirst(event, request) {
215
604
  try {
605
+ const reqForCache = request.clone();
216
606
  const response = await _fetch(event, request);
217
607
  if (response.ok) {
218
- event.waitUntil(
219
- (async () => {
220
- await cacheResponse(request, response);
221
- _trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
222
- })(),
223
- );
608
+ const responseToCache = response.clone();
609
+ event.waitUntil(cacheResponse(reqForCache, responseToCache));
224
610
  }
225
611
  return response;
226
612
  } catch {
227
613
  const cached = await fromRuntime(request);
228
- if (cached) return cached;
614
+ if (cached) {
615
+ return markFromCache(cached);
616
+ }
229
617
 
230
618
  const precached = await fromPrecache(request);
231
- if (precached) return precached;
619
+ if (precached) return markFromCache(precached);
232
620
 
233
621
  const fallback = await fromSpaFallback(request);
234
622
  if (fallback) return fallback;
235
623
 
236
- throw new Error("Request failed and no cached response available");
624
+ throw new Error("Network request failed and no cached response available");
237
625
  }
238
626
  }
239
627
 
240
- async function staleWhileRevalidate(event, request, staleTime, maxEntries, maxAge) {
628
+ async function staleWhileRevalidate(event, request) {
241
629
  const cached = await fromRuntime(request);
242
630
  if (cached) {
243
- // Skip background refresh if still fresh
244
- if (!isStale(cached, staleTime)) {
245
- return markFromCache(cached);
246
- }
247
- event.waitUntil(refreshCache(request));
631
+ event.waitUntil(queueRefresh(cacheKey(request), new URL(request.url).href));
248
632
  return markFromCache(cached);
249
633
  }
250
634
 
251
635
  const precached = await fromPrecache(request);
252
636
  if (precached) {
253
- event.waitUntil(refreshCache(request));
637
+ event.waitUntil(queueRefresh(cacheKey(request), new URL(request.url).href));
254
638
  return markFromCache(precached);
255
639
  }
256
640
 
641
+ const reqForCache = request.clone();
257
642
  const response = await _fetch(event, request);
258
643
  if (response.ok) {
259
- await cacheResponse(request, response);
260
- _trim(CACHE_NAME_RUNTIME, maxEntries, maxAge);
644
+ const responseToCache = response.clone();
645
+ await cacheResponse(reqForCache, responseToCache);
261
646
  }
262
647
  return response;
263
648
  }
264
649
 
265
- async function refreshCache(request) {
266
- try {
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);
270
- if (response.ok) {
271
- await storeRuntime(request, response);
650
+ async function reactiveStrategy(event, request, staleTime) {
651
+ const cached = await fromRuntime(request);
652
+ // Per-request staleTime header overrides config-level staleTime
653
+ const headerStale = request.headers.get("X-SW-Stale-Time");
654
+ const effectiveStaleTime = headerStale !== null ? Number(headerStale) : staleTime;
655
+ if (cached) {${staleVersionCode}
656
+ if (shouldReactiveRefresh(cached, { staleTime: effectiveStaleTime })) {
657
+ event.waitUntil(queueRefresh(cacheKey(request), new URL(request.url).href));
272
658
  }
273
- } catch {
274
- // Background refresh failed - stale cache remains usable
659
+ return markFromCache(cached);
660
+ }
661
+
662
+ const precached = await fromPrecache(request);
663
+ if (precached) return markFromCache(precached);
664
+
665
+ const fallback = await fromSpaFallback(request);
666
+ if (fallback) return fallback;
667
+
668
+ const reqForCache = request.clone();
669
+ const response = await _fetch(event, request);
670
+ if (response.ok) {
671
+ const responseToCache = response.clone();
672
+ await cacheResponse(reqForCache, responseToCache);
275
673
  }
674
+ return response;
276
675
  }
277
676
 
278
- async function cacheOnly(event, request, staleTime, maxEntries, maxAge) {
677
+ async function cacheOnly(event, request) {
279
678
  const cached = await fromRuntime(request);
280
679
  if (cached) {${staleVersionCode}
281
- if (isStale(cached, staleTime)) {
282
- event.waitUntil(refreshCache(request));
283
- }
284
680
  return markFromCache(cached);
285
681
  }
286
682
 
@@ -294,38 +690,4 @@ async function networkOnly(event, request) {
294
690
  return _fetch(event, request);
295
691
  }`;
296
692
  }
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
693
  //# sourceMappingURL=fetch-handler.js.map