@swoff/cli 0.3.13 → 0.3.14

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 (224) hide show
  1. package/README.md +28 -32
  2. package/dist/index.js +2 -13
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/cli/help.js +3 -8
  5. package/dist/lib/cli/help.js.map +1 -1
  6. package/dist/lib/commands/add.js +29 -87
  7. package/dist/lib/commands/add.js.map +1 -1
  8. package/dist/lib/commands/generate-guide.js +16 -30
  9. package/dist/lib/commands/generate-guide.js.map +1 -1
  10. package/dist/lib/commands/generate.js +31 -6
  11. package/dist/lib/commands/generate.js.map +1 -1
  12. package/dist/lib/commands/init.js +188 -11
  13. package/dist/lib/commands/init.js.map +1 -1
  14. package/dist/lib/commands/validate.js +2 -4
  15. package/dist/lib/commands/validate.js.map +1 -1
  16. package/dist/lib/config/loader.js +34 -25
  17. package/dist/lib/config/loader.js.map +1 -1
  18. package/dist/lib/config/validator.js +202 -96
  19. package/dist/lib/config/validator.js.map +1 -1
  20. package/dist/lib/generators/file-generators/api-config.js +1 -1
  21. package/dist/lib/generators/file-generators/api-config.js.map +1 -1
  22. package/dist/lib/generators/file-generators/auth-adapter.js +13 -0
  23. package/dist/lib/generators/file-generators/auth-adapter.js.map +1 -0
  24. package/dist/lib/generators/file-generators/auth-check.js +40 -0
  25. package/dist/lib/generators/file-generators/auth-check.js.map +1 -0
  26. package/dist/lib/generators/file-generators/auth-store.js +2 -5
  27. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  28. package/dist/lib/generators/file-generators/background-sync.js +1 -1
  29. package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
  30. package/dist/lib/generators/file-generators/cache.js +2 -1
  31. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  32. package/dist/lib/generators/file-generators/client-injector-bundle.js +15 -0
  33. package/dist/lib/generators/file-generators/client-injector-bundle.js.map +1 -0
  34. package/dist/lib/generators/file-generators/client-injector.js +2 -1
  35. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  36. package/dist/lib/generators/file-generators/connectivity-manager.js +12 -0
  37. package/dist/lib/generators/file-generators/connectivity-manager.js.map +1 -0
  38. package/dist/lib/generators/file-generators/connectivity.js +12 -0
  39. package/dist/lib/generators/file-generators/connectivity.js.map +1 -0
  40. package/dist/lib/generators/file-generators/context.js +7 -0
  41. package/dist/lib/generators/file-generators/context.js.map +1 -1
  42. package/dist/lib/generators/file-generators/fetch-wrapper.js +1 -1
  43. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  44. package/dist/lib/generators/file-generators/generate-framework-adapters.js +29 -20
  45. package/dist/lib/generators/file-generators/generate-framework-adapters.js.map +1 -1
  46. package/dist/lib/generators/file-generators/gql-wrapper.js +2 -2
  47. package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -1
  48. package/dist/lib/generators/file-generators/guide-generator.js +51 -1
  49. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  50. package/dist/lib/generators/file-generators/invalidation-tags.js +2 -3
  51. package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
  52. package/dist/lib/generators/file-generators/mutation-queue.js +2 -2
  53. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  54. package/dist/lib/generators/file-generators/mutation-state.js +1 -1
  55. package/dist/lib/generators/file-generators/mutation-state.js.map +1 -1
  56. package/dist/lib/generators/file-generators/notification-generator.js +6 -0
  57. package/dist/lib/generators/file-generators/notification-generator.js.map +1 -0
  58. package/dist/lib/generators/file-generators/open-db.js +6 -0
  59. package/dist/lib/generators/file-generators/open-db.js.map +1 -0
  60. package/dist/lib/generators/file-generators/push.js +1 -2
  61. package/dist/lib/generators/file-generators/push.js.map +1 -1
  62. package/dist/lib/generators/file-generators/pwa-install.js +2 -6
  63. package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
  64. package/dist/lib/generators/file-generators/server-push.js +1 -1
  65. package/dist/lib/generators/file-generators/server-push.js.map +1 -1
  66. package/dist/lib/generators/file-generators/storage-generator.js +6 -0
  67. package/dist/lib/generators/file-generators/storage-generator.js.map +1 -0
  68. package/dist/lib/generators/file-generators/storage-notify.js +6 -0
  69. package/dist/lib/generators/file-generators/storage-notify.js.map +1 -0
  70. package/dist/lib/generators/file-generators/storage.js +6 -0
  71. package/dist/lib/generators/file-generators/storage.js.map +1 -0
  72. package/dist/lib/generators/file-generators/sw-generator-build.js +96 -45
  73. package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
  74. package/dist/lib/generators/file-generators/sw-injector.js +0 -4
  75. package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
  76. package/dist/lib/generators/file-generators/sw-template.js +8 -9
  77. package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
  78. package/dist/lib/generators/file-generators/sw-version-gen.js +32 -0
  79. package/dist/lib/generators/file-generators/sw-version-gen.js.map +1 -0
  80. package/dist/lib/generators/file-generators/swoff-api-bundle.js +51 -0
  81. package/dist/lib/generators/file-generators/swoff-api-bundle.js.map +1 -0
  82. package/dist/lib/generators/sw-build-utils.js +69 -0
  83. package/dist/lib/generators/sw-build-utils.js.map +1 -0
  84. package/dist/lib/generators/sw-generator.js +23 -34
  85. package/dist/lib/generators/sw-generator.js.map +1 -1
  86. package/dist/lib/generators/sw-sections/activate-handler.js +74 -23
  87. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  88. package/dist/lib/generators/sw-sections/assemble-sw.js +15 -94
  89. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  90. package/dist/lib/generators/sw-sections/background-sync-handler.js +74 -72
  91. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
  92. package/dist/lib/generators/sw-sections/batch-refresh-queue.js +102 -0
  93. package/dist/lib/generators/sw-sections/batch-refresh-queue.js.map +1 -0
  94. package/dist/lib/generators/sw-sections/config-header.js +2 -3
  95. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  96. package/dist/lib/generators/sw-sections/default-template.js +59 -6
  97. package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
  98. package/dist/lib/generators/sw-sections/fetch-handler.js +621 -495
  99. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  100. package/dist/lib/generators/sw-sections/install-handler.js +40 -23
  101. package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
  102. package/dist/lib/generators/sw-sections/message-handler.js +25 -8
  103. package/dist/lib/generators/sw-sections/message-handler.js.map +1 -1
  104. package/dist/lib/generators/sw-sections/server-push-handler.js +10 -14
  105. package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -1
  106. package/dist/lib/generators/sw-sections/shared.js +68 -0
  107. package/dist/lib/generators/sw-sections/shared.js.map +1 -0
  108. package/dist/lib/generators/sw-sections/sw-push.js +2 -6
  109. package/dist/lib/generators/sw-sections/sw-push.js.map +1 -1
  110. package/dist/lib/generators/sw-sections/tag-management.js +55 -86
  111. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  112. package/dist/lib/generators/swoff-files-generator.js +183 -59
  113. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  114. package/dist/lib/shared/config-types.js +67 -63
  115. package/dist/lib/shared/config-types.js.map +1 -1
  116. package/dist/lib/shared/feature-registry.js +198 -0
  117. package/dist/lib/shared/feature-registry.js.map +1 -0
  118. package/dist/lib/utils/detect-framework.js +52 -9
  119. package/dist/lib/utils/detect-framework.js.map +1 -1
  120. package/dist/runtime/api-config.js +2 -2
  121. package/dist/runtime/api-config.js.map +1 -1
  122. package/dist/runtime/auth-adapter.js +179 -0
  123. package/dist/runtime/auth-adapter.js.map +1 -0
  124. package/dist/runtime/auth-state.js +16 -6
  125. package/dist/runtime/auth-state.js.map +1 -1
  126. package/dist/runtime/auth-store.js +219 -213
  127. package/dist/runtime/auth-store.js.map +1 -1
  128. package/dist/runtime/auth-user.js +5 -19
  129. package/dist/runtime/auth-user.js.map +1 -1
  130. package/dist/runtime/background-sync.js +5 -2
  131. package/dist/runtime/background-sync.js.map +1 -1
  132. package/dist/runtime/cache.js +91 -3
  133. package/dist/runtime/cache.js.map +1 -1
  134. package/dist/runtime/client-injector-bundle.js +314 -0
  135. package/dist/runtime/client-injector-bundle.js.map +1 -0
  136. package/dist/runtime/client-injector.js +173 -30
  137. package/dist/runtime/client-injector.js.map +1 -1
  138. package/dist/runtime/connectivity-manager.js +83 -0
  139. package/dist/runtime/connectivity-manager.js.map +1 -0
  140. package/dist/runtime/connectivity.js +83 -0
  141. package/dist/runtime/connectivity.js.map +1 -0
  142. package/dist/runtime/fetch-wrapper.js +26 -59
  143. package/dist/runtime/fetch-wrapper.js.map +1 -1
  144. package/dist/runtime/gql-wrapper.js +15 -9
  145. package/dist/runtime/gql-wrapper.js.map +1 -1
  146. package/dist/runtime/invalidation-tags.js +12 -89
  147. package/dist/runtime/invalidation-tags.js.map +1 -1
  148. package/dist/runtime/mutation-queue.js +83 -31
  149. package/dist/runtime/mutation-queue.js.map +1 -1
  150. package/dist/runtime/mutation-state.js +3 -3
  151. package/dist/runtime/mutation-state.js.map +1 -1
  152. package/dist/runtime/notification.js +65 -0
  153. package/dist/runtime/notification.js.map +1 -0
  154. package/dist/runtime/open-db.js +41 -0
  155. package/dist/runtime/open-db.js.map +1 -0
  156. package/dist/runtime/push.js +15 -27
  157. package/dist/runtime/push.js.map +1 -1
  158. package/dist/runtime/pwa-injector.js +3 -32
  159. package/dist/runtime/pwa-injector.js.map +1 -1
  160. package/dist/runtime/pwa-prompt.js +35 -3
  161. package/dist/runtime/pwa-prompt.js.map +1 -1
  162. package/dist/runtime/reset.js +39 -42
  163. package/dist/runtime/reset.js.map +1 -1
  164. package/dist/runtime/server-push.js +23 -13
  165. package/dist/runtime/server-push.js.map +1 -1
  166. package/dist/runtime/storage.js +32 -0
  167. package/dist/runtime/storage.js.map +1 -0
  168. package/dist/runtime/sw-injector.js +19 -178
  169. package/dist/runtime/sw-injector.js.map +1 -1
  170. package/dist/runtime/sw-version.js +13 -0
  171. package/dist/runtime/sw-version.js.map +1 -0
  172. package/dist/runtime/swoff-api-bundle.js +1491 -0
  173. package/dist/runtime/swoff-api-bundle.js.map +1 -0
  174. package/dist/runtime/type-definitions.js +8 -7
  175. package/dist/runtime/type-definitions.js.map +1 -1
  176. package/dist/runtime/utils.js +4 -0
  177. package/dist/runtime/utils.js.map +1 -1
  178. package/dist/swoff-nkdhhmw8. +18 -0
  179. package/dist/swoff.js +5 -0
  180. package/package.json +1 -1
  181. package/templates/react/useAuth.jsx +7 -4
  182. package/templates/react/useAuth.tsx +14 -14
  183. package/templates/react/useBackgroundSync.jsx +5 -4
  184. package/templates/react/useBackgroundSync.tsx +5 -4
  185. package/templates/react/useCachedFetch.jsx +8 -0
  186. package/templates/react/useCachedFetch.tsx +8 -0
  187. package/templates/react/useMutation.jsx +60 -56
  188. package/templates/react/useMutation.tsx +65 -64
  189. package/templates/react/useMutationQueue.jsx +5 -1
  190. package/templates/react/useMutationQueue.tsx +8 -2
  191. package/templates/react/useMutationState.jsx +3 -4
  192. package/templates/react/useMutationState.tsx +4 -8
  193. package/templates/react/useNetworkStatus.jsx +57 -17
  194. package/templates/react/useNetworkStatus.tsx +42 -20
  195. package/templates/react/useOfflineAnalytics.jsx +42 -0
  196. package/templates/react/useOfflineAnalytics.tsx +50 -0
  197. package/templates/react/usePwaInstall.jsx +26 -0
  198. package/templates/react/usePwaInstall.tsx +26 -0
  199. package/templates/react/useSWUpdate.jsx +7 -52
  200. package/templates/react/useSWUpdate.tsx +12 -51
  201. package/templates/react/useStorageEstimate.jsx +56 -0
  202. package/templates/react/useStorageEstimate.tsx +67 -0
  203. package/dist/__tests__/assemble-sw.test.js +0 -145
  204. package/dist/__tests__/assemble-sw.test.js.map +0 -1
  205. package/dist/__tests__/cli-integration.test.js +0 -231
  206. package/dist/__tests__/cli-integration.test.js.map +0 -1
  207. package/dist/__tests__/config-types.test.js +0 -124
  208. package/dist/__tests__/config-types.test.js.map +0 -1
  209. package/dist/__tests__/detect-language.test.js +0 -51
  210. package/dist/__tests__/detect-language.test.js.map +0 -1
  211. package/dist/__tests__/file-generators.test.js +0 -228
  212. package/dist/__tests__/file-generators.test.js.map +0 -1
  213. package/dist/__tests__/glob-matcher.test.js +0 -86
  214. package/dist/__tests__/glob-matcher.test.js.map +0 -1
  215. package/dist/__tests__/invalidation-tags.test.js +0 -283
  216. package/dist/__tests__/invalidation-tags.test.js.map +0 -1
  217. package/dist/__tests__/loader.test.js +0 -77
  218. package/dist/__tests__/loader.test.js.map +0 -1
  219. package/dist/__tests__/validator.test.js +0 -340
  220. package/dist/__tests__/validator.test.js.map +0 -1
  221. package/templates/react/useCacheInvalidation.jsx +0 -19
  222. package/templates/react/useCacheInvalidation.tsx +0 -52
  223. package/templates/react/useIsFetching.jsx +0 -32
  224. package/templates/react/useIsFetching.tsx +0 -32
@@ -1,76 +1,37 @@
1
- /**
2
- * Generates the SW fetch event handler with all caching strategies.
3
- *
4
- * Strategy dispatch tiers (highest to lowest priority):
5
- * 1. X-SW-Strategy header — per-request override from fetchWithCache({ strategy })
6
- * 2. URL pattern match — from features.serviceWorker.strategies in swoff.config.json
7
- * 3. defaultStrategy fallback from features.serviceWorker.defaultStrategy
8
- *
9
- * 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.
17
- *
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.
23
- */
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
- ? `
29
- cleanStaleVersions();
30
- if (staleVersions.has(cacheKey(request))) {
31
- queueRefresh(cacheKey(request), new URL(request.url).href);
32
- }`
33
- : "";
34
- const tagCode = tagInvalidation
35
- ? `
36
- const tagsHeader = request.headers.get("X-SW-Cache-Tags");
37
- if (tagsHeader) {
38
- const cacheKeyUrl = cacheKey(request);
39
- const actualUrl = new URL(request.url).href;
40
- const tags = tagsHeader.split(",").map((t) => t.trim());
41
- await cacheTagUrl(cacheKeyUrl, actualUrl, tags);
42
- }`
1
+ export function generateFetchHandler(swConfig, tagInvalidation, mutationQueueEnabled, authRoutePaths, serverPushEndpoint, debug) {
2
+ const { strategy: { default: defaultStrategy, patterns: strategies, normalizeKey: normalizeCacheKey, ignoreQueryParams, timeout: fetchTimeout = 10, maxRuntimeCacheAge: maxCacheAge, }, navigation: { mode: navMode = "spa", preload: navigationPreload, fallback: globalFallback = "", rules: navRules = [], }, } = swConfig;
3
+ const debugMode = debug === true;
4
+ const globalStaleTime = swConfig.strategy.reactive?.defaults?.staleTime;
5
+ const refetchRetry = swConfig.refetchQueue.retry;
6
+ const hasRules = navRules.length > 0;
7
+ const navModeCode = navMode === "ssr" ? '"ssr"' : navMode === "spa" ? '"spa"' : '"default"';
8
+ const fallbackCode = globalFallback ? `"${globalFallback}"` : '""';
9
+ const navRulesCode = hasRules
10
+ ? `const NAV_RULES = ${JSON.stringify(navRules.map((r) => ({
11
+ match: r.match,
12
+ ...(r.fallback ? { fallback: r.fallback } : {}),
13
+ })), null, 2)};
14
+
15
+ function matchRouteFallback(url) {
16
+ swLog("matchRouteFallback", "ENTER", url, 4);
17
+ const path = new URL(url).pathname;
18
+ for (const rule of NAV_RULES) {
19
+ if (matchGlob(path, rule.match)) {
20
+ swLog("matchRouteFallback", "MATCH rule=" + rule.match + " fallback=" + (rule.fallback || "null"), url, 4);
21
+ return rule.fallback || null;
22
+ }
23
+ }
24
+ swLog("matchRouteFallback", "NO MATCH", url, 4);
25
+ return null;
26
+ }
27
+
28
+ `
43
29
  : "";
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" +
30
+ const globCode = "\n" +
70
31
  "// --- Glob Pattern Matching ---\n" +
71
32
  "\n" +
72
33
  "function escapeGlobMeta(s) {\n" +
73
- ' return s.replace(/[.+^${}()|[\\]\\\\]/g, "\\\\$$&");\n' +
34
+ ' return s.replace(/[.+^${}()|[\\]\\\\]/g, "\\\\$&");\n' +
74
35
  "}\n" +
75
36
  "\n" +
76
37
  "function globPartRe(part) {\n" +
@@ -120,197 +81,59 @@ const REFRESH_RETRY_DELAY_MS = ${refetchRetryDelayMs};`;
120
81
  " return pi === parts.length && ppi === pps.length;\n" +
121
82
  "}\n" +
122
83
  "\n";
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
84
+ const inline503 = "function inline503Response() {\n" +
85
+ " return new Response(\n" +
86
+ ' `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Offline</title><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f5f5f5}div{text-align:center}h1{font-size:2rem;color:#333}p{color:#666}</style></head><body><div><h1>You\\\'re offline</h1><p>Please check your connection and try again.</p></div></body></html>`,\n' +
87
+ ' { status: 503, headers: { "Content-Type": "text/html", "Cache-Control": "no-store" } }\n' +
88
+ " );\n" +
89
+ "}\n";
90
+ const tagCode = tagInvalidation
190
91
  ? `
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
- }
92
+ const tagsHeader = request.headers.get("X-SW-Cache-Tags");
93
+ if (tagsHeader) {
94
+ const cacheKeyUrl = cacheKey(request);
95
+ const actualUrl = new URL(request.url).href;
96
+ const tags = tagsHeader.split(",").map((t) => t.trim());
97
+ const method = request.method;
98
+ let body = null;
99
+ let contentType = null;
100
+ if (method !== "GET") {
101
+ contentType = request.headers.get("Content-Type");
102
+ try { body = await request.clone().text(); } catch {}
222
103
  }
223
- } finally {
224
- _refreshQueuePromise = null;
225
- }
104
+ await cacheTagUrl(cacheKeyUrl, actualUrl, tags, method, body, contentType);
105
+ }`
106
+ : "";
107
+ return `// --- Mode & Strategy Configuration ---
108
+
109
+ const NAV_MODE = ${navModeCode};
110
+ const FALLBACK_PATH = ${fallbackCode};
111
+ const DEFAULT_STRATEGY = "${defaultStrategy}";
112
+ const CUSTOM_STRATEGIES = ${JSON.stringify(strategies)};
113
+ const REACTIVE_STALE_DEFAULT = ${globalStaleTime != null ? globalStaleTime : 0};
114
+ const FETCH_TIMEOUT_MS = ${fetchTimeout * 1000};
115
+ const SW_DEBUG = ${debugMode};
116
+ ${authRoutePaths.length > 0 ? `const AUTH_ROUTES = ${JSON.stringify(authRoutePaths)};` : ""}
117
+ ${serverPushEndpoint ? `const SERVER_PUSH_ENDPOINT = "${serverPushEndpoint}";` : ""}
118
+
119
+ ${navRulesCode}// --- Debug Logging ---
120
+
121
+ function swLog(fn, msg, url, indent) {
122
+ if (!SW_DEBUG) return;
123
+ var prefix = "";
124
+ for (var i = 0; i < (indent || 0); i++) prefix += " ";
125
+ console.log("[SW]" + prefix, fn + ":", msg, url || "", Date.now());
226
126
  }
227
127
 
228
- // --- Reactive Patterns (extracted for interval/focus/reconnect) ---
229
-
230
- const REACTIVE_PATTERNS = ${JSON.stringify(reactivePatterns)};
128
+ ${globCode}// --- Navigation Detection ---
231
129
 
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
- }
130
+ function isNavRequest(request) {
131
+ return (
132
+ request.mode === "navigate" ||
133
+ request.destination === "document" ||
134
+ (request.method === "GET" &&
135
+ request.headers.get("sec-fetch-mode") === "navigate")
136
+ );
314
137
  }
315
138
 
316
139
  // --- Cache Key ---
@@ -320,7 +143,6 @@ function cacheKey(request) {
320
143
  if (key) return new URL("/__swc/" + key, self.location.origin).href;
321
144
  const url = new URL(request.url);${normalizeCacheKey
322
145
  ? `
323
- // Sort query params alphabetically for consistent cache keys
324
146
  if (url.search) {
325
147
  const params = new URLSearchParams(url.search);
326
148
  params.sort();
@@ -328,10 +150,9 @@ function cacheKey(request) {
328
150
  }`
329
151
  : ""}${ignoreQueryParams && ignoreQueryParams.length > 0
330
152
  ? `
331
- // Strip configured query params from cache key
332
- const ignore = ${JSON.stringify(ignoreQueryParams)};
333
153
  if (url.search) {
334
154
  const params = new URLSearchParams(url.search);
155
+ const ignore = ${JSON.stringify(ignoreQueryParams)};
335
156
  for (const key of ignore) params.delete(key);
336
157
  url.search = params.toString();
337
158
  }`
@@ -339,60 +160,216 @@ function cacheKey(request) {
339
160
  return url.href;
340
161
  }
341
162
 
342
- // --- Cache Helpers ---
163
+ // --- Cache Lookup ---
343
164
 
344
165
  async function fromPrecache(request) {
345
- const cache = await caches.open(CACHE_NAME);
346
- return cache.match(new URL(request.url).pathname);
166
+ swLog("fromPrecache", "ENTER", request.url, 4);
167
+ const cache = await caches.open("precache");
168
+ const url = new URL(request.url);
169
+ url.search = "";
170
+ const result = await cache.match(url.href);
171
+ swLog("fromPrecache", result ? "HIT" : "MISS", request.url, 4);
172
+ return result;
347
173
  }
348
174
 
349
175
  async function fromRuntime(request) {
350
- const cache = await caches.open(CACHE_NAME_RUNTIME);
351
- return cache.match(cacheKey(request));
176
+ swLog("fromRuntime", "ENTER", request.url, 4);
177
+ const cache = await caches.open("swoff-runtime");
178
+ const result = await cache.match(cacheKey(request));
179
+ swLog("fromRuntime", result ? "HIT" : "MISS", request.url, 4);
180
+ return result;
352
181
  }
353
182
 
354
- async function storeRuntime(request, response) {
355
- const cache = await caches.open(CACHE_NAME_RUNTIME);
356
- const headers = new Headers(response.headers);
357
- headers.set("X-SW-Cached-At", String(Date.now()));
358
- const cloned = new Response(response.body, {
359
- status: response.status,
360
- statusText: response.statusText,
361
- headers,
362
- });
363
- await cache.put(cacheKey(request), cloned);
183
+ /*
184
+ * serveFromCache returns a cached Response, or null if nothing is cached.
185
+ *
186
+ * Mode-specific behaviour for navigation requests:
187
+ * SPA: bypass cache entirely return null
188
+ * SSR: precache → runtime-html → null
189
+ * Default: same as SSR
190
+ *
191
+ * Non-navigation requests:
192
+ * runtime → precache (content-type match only) → null
193
+ *
194
+ * Precache is content-type-gated for non-navigation requests to avoid
195
+ * serving HTML pages (stored with stripped extensions) for requests that
196
+ * expect a different content type (e.g. RSC payloads, JSON fetches).
197
+ */
198
+ async function serveFromCache(request) {
199
+ swLog("serveFromCache", "ENTER", request.url, 3);
200
+ if (isNavRequest(request)) {
201
+ if (NAV_MODE === "spa") {
202
+ if (FALLBACK_PATH) {
203
+ const cache = await caches.open("precache");
204
+ const match = await cache.match(FALLBACK_PATH);
205
+ if (match) {
206
+ swLog("serveFromCache", "HIT fallback-path", request.url, 3);
207
+ return match;
208
+ }
209
+ }
210
+ swLog("serveFromCache", "MISS (SPA nav)", request.url, 3);
211
+ return null;
212
+ }
213
+ const pc = await fromPrecache(request);
214
+ if (pc) {
215
+ swLog("serveFromCache", "HIT precache (nav)", request.url, 3);
216
+ return pc;
217
+ }
218
+ const htmlCache = await caches.open("swoff-runtime-html");
219
+ const htmlMatch = await htmlCache.match(cacheKey(request));
220
+ if (htmlMatch) {
221
+ swLog("serveFromCache", "HIT runtime-html", request.url, 3);
222
+ return htmlMatch;
223
+ }
224
+ swLog("serveFromCache", "MISS (SSR/Default nav)", request.url, 3);
225
+ return null;
226
+ }
227
+ const cached = await fromRuntime(request);
228
+ if (cached) {
229
+ swLog("serveFromCache", "HIT runtime", request.url, 3);
230
+ return cached;
231
+ }
232
+ const pc = await fromPrecache(request);
233
+ if (pc) {
234
+ const accept = request.headers.get("Accept") || "*/*";
235
+ const pcType = (pc.headers.get("Content-Type") || "").split(";")[0].trim();
236
+ if (accept.includes(pcType) || accept.includes("*/*") || !pcType) {
237
+ swLog("serveFromCache", "HIT precache (sub)", request.url, 3);
238
+ return pc;
239
+ }
240
+ swLog("serveFromCache", "SKIP precache (type mismatch)", request.url, 3);
241
+ }
242
+ swLog("serveFromCache", "MISS (subresource)", request.url, 3);
243
+ return null;
364
244
  }
365
245
 
366
- async function cacheResponse(request, response) {
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");
246
+ // --- Cache Store ---
247
+
248
+ /*
249
+ * cacheResponse stores a network response in the appropriate cache.
250
+ *
251
+ * If already in precache:
252
+ * - non-HTML with same content-type → update precache entry
253
+ * - otherwise → skip (no runtime duplicate)
254
+ * If not in precache:
255
+ * - HTML → runtime-html cache
256
+ * - non-HTML runtime cache
257
+ *
258
+ * Tags are recorded in all cases.
259
+ */
260
+ async function cacheResponse(response, request) {
261
+ swLog("cacheResponse", "ENTER", request.url, 3);
262
+ const key = cacheKey(request);
263
+ const ct = response.headers.get("Content-Type") || "";
264
+ var skipRuntime = false;
265
+ const precache = await caches.open("precache");
266
+ const url = new URL(key);
267
+ url.search = "";
268
+ const precached = await precache.match(url.href);
269
+
270
+ if (precached) {
271
+ if (!ct.startsWith("text/html")) {
272
+ const pct = precached.headers.get("Content-Type") || "";
273
+ if (pct.split(";")[0] === ct.split(";")[0]) {
274
+ try {
275
+ await precache.put(url.href, response.clone());
276
+ swLog("cacheResponse", "updated precache", request.url, 3);
277
+ } catch (e) {
278
+ swLog("cacheResponse", "precache update failed", request.url, 3);
279
+ }
280
+ }
281
+ }
282
+ skipRuntime = true;
283
+ }
284
+
285
+ if (!skipRuntime) {
286
+ const cacheName = ct.startsWith("text/html") ? "swoff-runtime-html" : "swoff-runtime";
287
+ const cache = await caches.open(cacheName);
288
+ const headers = new Headers(response.headers);
289
+ headers.set("X-SW-Cached-At", String(Date.now()));
290
+ const putResponse = new Response(response.body, {
291
+ status: response.status,
292
+ statusText: response.statusText,
293
+ headers,
294
+ });
295
+ try {
296
+ await cache.put(key, putResponse);
297
+ swLog("cacheResponse", "stored in " + cacheName, request.url, 3);
298
+ } catch (e) {
299
+ swLog("cacheResponse", "quota error, evicting stale entries", request.url, 3);
378
300
  try {
379
- body = await request.clone().text();
301
+ // Evict stale runtime entries and retry once
302
+ for (const name of ["swoff-runtime", "swoff-runtime-html"]) {
303
+ const c = await caches.open(name);
304
+ const keys = await c.keys();
305
+ const now = Date.now();
306
+ for (const req of keys) {
307
+ const res = await c.match(req);
308
+ const cachedAt = res ? Number(res.headers.get("X-SW-Cached-At") || 0) : 0;
309
+ if (!cachedAt || now - cachedAt > 3600000) await c.delete(req);
310
+ }
311
+ }
312
+ await cache.put(key, putResponse.clone());
313
+ swLog("cacheResponse", "stored after eviction", request.url, 3);
380
314
  } catch {}
381
315
  }
382
- await cacheTagUrl(cacheKeyUrl, actualUrl, tags, method, body, contentType);
383
- }
316
+ }${tagCode}
384
317
  }
385
318
 
386
- async function fromSpaFallback(request) {
387
- if (request.mode === "navigate") {
388
- const cache = await caches.open(CACHE_NAME);
389
- return cache.match("${spaPath}");
319
+ // --- Fallback ---
320
+
321
+ /*
322
+ * Fallback hierarchy for when a strategy cannot serve a response:
323
+ * SSR / Default: precache → runtime-html → per-route → global → inline 503
324
+ * SPA: per-route → inline 503
325
+ */
326
+ async function fallback(request) {
327
+ swLog("fallback", "ENTER", request.url, 3);
328
+ const pc = await fromPrecache(request);
329
+ if (pc) {
330
+ swLog("fallback", "HIT precache", request.url, 3);
331
+ return pc;
332
+ }
333
+ if (NAV_MODE !== "spa") {
334
+ const htmlCache = await caches.open("swoff-runtime-html");
335
+ const htmlMatch = await htmlCache.match(cacheKey(request));
336
+ if (htmlMatch) {
337
+ swLog("fallback", "HIT runtime-html", request.url, 3);
338
+ return htmlMatch;
339
+ }
340
+ }${hasRules
341
+ ? `
342
+ const routeFallbackPath = matchRouteFallback(request.url);
343
+ if (routeFallbackPath) {
344
+ const cache = await caches.open("precache");
345
+ const match = await cache.match(routeFallbackPath);
346
+ if (match) {
347
+ swLog("fallback", "HIT per-route fallback", request.url, 3);
348
+ broadcastToClients("OFFLINE_FALLBACK_ACTIVATED", { detail: { route: new URL(request.url).pathname, fallbackLevel: "route-fallback", timestamp: Date.now() } });
349
+ return match;
350
+ }
351
+ }`
352
+ : ""}
353
+ if (NAV_MODE !== "spa") {
354
+ if (FALLBACK_PATH) {
355
+ const cache = await caches.open("precache");
356
+ const match = await cache.match(FALLBACK_PATH);
357
+ if (match) {
358
+ swLog("fallback", "HIT global fallback", request.url, 3);
359
+ broadcastToClients("OFFLINE_FALLBACK_ACTIVATED", { detail: { route: new URL(request.url).pathname, fallbackLevel: "offline-page", timestamp: Date.now() } });
360
+ return match;
361
+ }
362
+ }
390
363
  }
364
+ swLog("fallback", "HIT inline 503", request.url, 3);
365
+ broadcastToClients("OFFLINE_FALLBACK_ACTIVATED", { detail: { route: new URL(request.url).pathname, fallbackLevel: "inline-503", timestamp: Date.now() } });
366
+ return inline503Response();
391
367
  }
392
368
 
393
- // --- Response Helpers ---
369
+ ${inline503}// --- Response Helpers ---
394
370
 
395
371
  function markFromCache(response) {
372
+ swLog("markFromCache", "ENTER", response.url, 4);
396
373
  const headers = new Headers(response.headers);
397
374
  headers.set("X-SW-From-Cache", "true");
398
375
  return new Response(response.body, {
@@ -403,94 +380,309 @@ function markFromCache(response) {
403
380
  }
404
381
 
405
382
  function isStale(response, staleTimeSeconds) {
406
- if (staleTimeSeconds == null || staleTimeSeconds <= 0) return false;
383
+ if (staleTimeSeconds == null) return false;
384
+ if (staleTimeSeconds === 0) return true;
385
+ if (staleTimeSeconds < 0) return false;
407
386
  const cachedAt = response.headers.get("X-SW-Cached-At");
408
387
  if (!cachedAt) return false;
409
388
  return Date.now() - Number(cachedAt) > staleTimeSeconds * 1000;
410
389
  }
411
390
 
412
- ${GLOB_CODE}// --- Strategy Selection (3-tier config resolution) ---
391
+ // --- Fetch Helpers ---
413
392
 
414
- function resolveStrategyEntry(entry) {
415
- return typeof entry === "string" ? { strategy: entry } : entry;
393
+ async function _fetchWithTimeout(_, request, timeoutMs) {
394
+ timeoutMs = timeoutMs || FETCH_TIMEOUT_MS;
395
+ swLog("_fetchWithTimeout", "ENTER timeout=" + timeoutMs, request.url, 3);
396
+ const controller = new AbortController();
397
+ const id = setTimeout(function() { controller.abort(); }, timeoutMs);
398
+ try {
399
+ const response = await fetch(request, { signal: controller.signal });
400
+ swLog("_fetchWithTimeout", "SUCCESS " + response.status, request.url, 3);
401
+ return response;
402
+ } catch (e) {
403
+ swLog("_fetchWithTimeout", "FAILED", request.url, 3);
404
+ throw e;
405
+ } finally {
406
+ clearTimeout(id);
407
+ }
416
408
  }
417
409
 
418
- function determineCacheStrategy(request, customStrategies, globalDefaults) {
410
+ ${navigationPreload
411
+ ? `
412
+ async function fetchWithPreload(event, request, timeoutMs) {
413
+ swLog("fetchWithPreload", "ENTER", request.url, 3);
414
+ try {
415
+ const preload = await Promise.race([
416
+ event.preloadResponse,
417
+ new Promise(function(_, reject) {
418
+ setTimeout(function() { reject(new Error("preload timeout")); }, (timeoutMs || FETCH_TIMEOUT_MS) / 2);
419
+ }),
420
+ ]);
421
+ if (preload) {
422
+ swLog("fetchWithPreload", "HIT preload", request.url, 3);
423
+ return preload;
424
+ }
425
+ } catch {}
426
+ swLog("fetchWithPreload", "MISS preload, fallback to fetch", request.url, 3);
427
+ return _fetchWithTimeout(event, request, timeoutMs);
428
+ }
429
+ `
430
+ : ""}const _fetch = ${navigationPreload ? "fetchWithPreload" : "_fetchWithTimeout"};
431
+
432
+ // --- Strategy Resolution ---
433
+
434
+ function determineCacheStrategy(request) {
419
435
  const override = request.headers.get("X-SW-Strategy");
420
436
  if (override) {
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;
437
+ if (override === "reactive") {
438
+ const staleTimeHeader = request.headers.get("X-SW-Stale-Time");
439
+ return {
440
+ strategy: "reactive",
441
+ timeoutMs: FETCH_TIMEOUT_MS,
442
+ staleTime: staleTimeHeader != null ? Number(staleTimeHeader) : REACTIVE_STALE_DEFAULT,
443
+ refetchInterval: Number(request.headers.get("X-SW-Refetch-Interval") || 0),
444
+ refetchOnFocus: request.headers.get("X-SW-Refetch-On-Focus") === "true",
445
+ refetchOnReconnect: request.headers.get("X-SW-Refetch-On-Reconnect") === "true",
446
+ };
447
+ }
448
+ return { strategy: override, timeoutMs: FETCH_TIMEOUT_MS };
429
449
  }
450
+
430
451
  const path = new URL(request.url).pathname;
431
- for (const [pattern, entry] of Object.entries(customStrategies)) {
452
+ for (const [pattern, entry] of Object.entries(CUSTOM_STRATEGIES)) {
432
453
  if (matchGlob(path, pattern)) {
433
- const resolved = resolveStrategyEntry(entry);
434
- return { strategy: resolved.strategy };
454
+ const resolved = typeof entry === "string" ? { strategy: entry } : entry;
455
+ const timeoutMs = (resolved.timeout != null ? resolved.timeout * 1000 : FETCH_TIMEOUT_MS);
456
+ if (resolved.strategy === "reactive") {
457
+ return {
458
+ strategy: "reactive",
459
+ timeoutMs: timeoutMs,
460
+ staleTime: resolved.staleTime ?? REACTIVE_STALE_DEFAULT,
461
+ refetchInterval: resolved.refetchInterval ?? 0,
462
+ refetchOnFocus: resolved.refetchOnFocus ?? false,
463
+ refetchOnReconnect: resolved.refetchOnReconnect ?? false,
464
+ };
465
+ }
466
+ return { strategy: resolved.strategy, timeoutMs: timeoutMs };
435
467
  }
436
468
  }
437
- return { strategy: globalDefaults.defaultStrategy };
469
+
470
+ if (DEFAULT_STRATEGY === "reactive") {
471
+ return {
472
+ strategy: "reactive",
473
+ timeoutMs: FETCH_TIMEOUT_MS,
474
+ staleTime: REACTIVE_STALE_DEFAULT,
475
+ refetchInterval: 0,
476
+ refetchOnFocus: false,
477
+ refetchOnReconnect: false,
478
+ };
479
+ }
480
+ return { strategy: DEFAULT_STRATEGY, timeoutMs: FETCH_TIMEOUT_MS };
438
481
  }
439
482
 
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 };
483
+ // --- Reactive Entry Store ---
484
+
485
+ var REACTIVE_ENTRIES = new Map();
486
+ var REACTIVE_INTERVALS = new Map();
487
+ var clearAllReactive = function() {
488
+ REACTIVE_INTERVALS.forEach(function(id) { clearInterval(id); });
489
+ REACTIVE_INTERVALS.clear();
490
+ REACTIVE_ENTRIES.clear();
491
+ swLog("clearAllReactive", "done", "", 0);
492
+ };
493
+
494
+ function registerReactiveEntry(url, actualUrl, config) {
495
+ swLog("registerReactiveEntry", "ENTER", url, 4);
496
+ if (REACTIVE_ENTRIES.has(url)) {
497
+ swLog("registerReactiveEntry", "SKIP already registered", url, 4);
498
+ return;
499
+ }
500
+ REACTIVE_ENTRIES.set(url, {
501
+ actualUrl: actualUrl,
502
+ refetchOnFocus: !!config.refetchOnFocus,
503
+ refetchOnReconnect: !!config.refetchOnReconnect,
504
+ refetchInterval: config.refetchInterval || 0,
505
+ staleTime: config.staleTime || 0,
506
+ lastRefetch: 0,
507
+ });
508
+ if ((config.refetchInterval || 0) > 0 && !REACTIVE_INTERVALS.has(url)) {
509
+ var id = setInterval(function() { refetchEntry(url); }, (config.refetchInterval || 0) * 1000);
510
+ REACTIVE_INTERVALS.set(url, id);
511
+ swLog("registerReactiveEntry", "interval=" + config.refetchInterval + "s", url, 4);
512
+ }
513
+ }
514
+
515
+ async function refetchEntry(url) {
516
+ swLog("refetchEntry", "ENTER", url, 4);
517
+ var entry = REACTIVE_ENTRIES.get(url);
518
+ var fetchUrl = entry ? entry.actualUrl || url : url;
519
+ var req = new Request(fetchUrl);
520
+ if (entry) {
521
+ var cached = await serveFromCache(req);
522
+ if (cached && !isStale(cached, entry.staleTime)) {
523
+ swLog("refetchEntry", "SKIP fresh in cache", url, 4);
524
+ entry.lastRefetch = Date.now();
525
+ return;
446
526
  }
447
527
  }
448
- return { strategy: globalDefaults.defaultStrategy };
528
+ try {
529
+ var response = await fetchWithRetry(req, REFETCH_RETRY);
530
+ if (response && response.ok) {
531
+ await cacheResponse(response.clone(), req);
532
+ if (entry) entry.lastRefetch = Date.now();
533
+ swLog("refetchEntry", "SUCCESS", url, 4);
534
+ broadcastToClients("CACHE_UPDATED", { url: fetchUrl });
535
+ }
536
+ checkAuthFailure(response);
537
+ } catch {}
449
538
  }
450
539
 
451
- function applyStrategy(event, request, config) {
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));
459
- } else if (strategy === "network-first") {
460
- event.respondWith(networkFirst(event, request));
461
- } else if (strategy === "cache-only") {
462
- event.respondWith(cacheOnly(event, request));
463
- } else if (strategy === "network-only") {
464
- event.respondWith(networkOnly(event, request));
465
- } else {
466
- event.respondWith(cacheFirst(event, request));
540
+ function handleRefetch(prop) {
541
+ swLog("handleRefetch", "ENTER prop=" + prop + " entries=" + REACTIVE_ENTRIES.size, "", 0);
542
+ REACTIVE_ENTRIES.forEach(function(config, url) {
543
+ if (config[prop]) queueRefresh(url);
544
+ });
545
+ }
546
+
547
+ async function checkAuthFailure(response) {
548
+ if (response && isAuthFailureResponse(response)) {
549
+ swLog("checkAuthFailure", "AUTH_FAILURE", response.url || "", 1);
550
+ broadcastToClients("AUTH_FAILURE");
551
+ }
552
+ }
553
+
554
+ // --- Strategies ---
555
+
556
+ /*
557
+ * Reactive: serve fresh from cache; if stale/miss → network → fallback
558
+ * Network-Only: network → fallback (cache ok responses)
559
+ * Network-First: network → cache (on fail) → fallback
560
+ * Cache-First: cache → network (on miss) → fallback
561
+ * Stale-While-Revalidate: stale cache immediately + bg network refresh → fallback
562
+ * Cache-Only: cache only → 404
563
+ */
564
+
565
+ async function reactiveStrategy(event, request, config) {
566
+ swLog("reactiveStrategy", "ENTER", request.url, 2);
567
+ const staleTime = config.staleTime != null ? config.staleTime : REACTIVE_STALE_DEFAULT;
568
+ const cached = await serveFromCache(request);
569
+ if (cached && !isStale(cached, staleTime)) return markFromCache(cached);
570
+
571
+ if (cached && isStale(cached, staleTime)) {
572
+ event.waitUntil(queueRefresh(cacheKey(request)));
573
+ return markFromCache(cached);
574
+ }
575
+
576
+ try {
577
+ const response = await _fetch(event, request, config.timeoutMs);
578
+ checkAuthFailure(response);
579
+ if (response.ok) {
580
+ event.waitUntil(cacheResponse(response.clone(), request));
581
+ return response;
582
+ }
583
+ return fallback(request);
584
+ } catch {
585
+ return fallback(request);
586
+ }
587
+ }
588
+
589
+ async function networkFirstStrategy(event, request, config) {
590
+ swLog("networkFirstStrategy", "ENTER", request.url, 2);
591
+ try {
592
+ const response = await _fetch(event, request, config.timeoutMs);
593
+ checkAuthFailure(response);
594
+ if (response.ok) {
595
+ event.waitUntil(cacheResponse(response.clone(), request));
596
+ return response;
597
+ }
598
+ const cached = await serveFromCache(request);
599
+ if (cached) return markFromCache(cached);
600
+ return fallback(request);
601
+ } catch {
602
+ const cached = await serveFromCache(request);
603
+ if (cached) return markFromCache(cached);
604
+ return fallback(request);
605
+ }
606
+ }
607
+
608
+ async function cacheFirstStrategy(event, request, config) {
609
+ swLog("cacheFirstStrategy", "ENTER", request.url, 2);
610
+ const cached = await serveFromCache(request);
611
+ if (cached) return markFromCache(cached);
612
+
613
+ try {
614
+ const response = await _fetch(event, request, config.timeoutMs);
615
+ checkAuthFailure(response);
616
+ if (response.ok) {
617
+ event.waitUntil(cacheResponse(response.clone(), request));
618
+ return response;
619
+ }
620
+ return fallback(request);
621
+ } catch {
622
+ return fallback(request);
467
623
  }
468
624
  }
469
625
 
626
+ async function staleWhileRevalidateStrategy(event, request, config) {
627
+ swLog("staleWhileRevalidateStrategy", "ENTER", request.url, 2);
628
+ const cached = await serveFromCache(request);
629
+
630
+ event.waitUntil(queueRefresh(cacheKey(request)));
631
+
632
+ if (cached) return markFromCache(cached);
633
+ try {
634
+ const response = await _fetch(event, request, config.timeoutMs);
635
+ checkAuthFailure(response);
636
+ if (response.ok) {
637
+ event.waitUntil(cacheResponse(response.clone(), request));
638
+ return response;
639
+ }
640
+ return fallback(request);
641
+ } catch {
642
+ return fallback(request);
643
+ }
644
+ }
645
+
646
+ async function cacheOnlyStrategy(event, request, _config) {
647
+ swLog("cacheOnlyStrategy", "ENTER", request.url, 2);
648
+ const cached = await serveFromCache(request);
649
+ if (cached) return markFromCache(cached);
650
+ swLog("cacheOnlyStrategy", "MISS 404", request.url, 2);
651
+ return new Response("Not in cache", { status: 404 });
652
+ }
653
+
654
+ const STRATEGY_HANDLERS = {
655
+ "reactive": reactiveStrategy,
656
+ "network-first": networkFirstStrategy,
657
+ "cache-first": cacheFirstStrategy,
658
+ "stale-while-revalidate": staleWhileRevalidateStrategy,
659
+ "cache-only": cacheOnlyStrategy,
660
+ };
661
+
662
+ async function _executeStrategy(event, request, config) {
663
+ swLog("_executeStrategy", "ENTER strategy=" + config.strategy, request.url, 1);
664
+ const handler = STRATEGY_HANDLERS[config.strategy] || cacheFirstStrategy;
665
+ try {
666
+ return await handler(event, request, config);
667
+ } catch {
668
+ swLog("_executeStrategy", "ERROR strategy=" + config.strategy, request.url, 1);
669
+ return fallback(request);
670
+ }
671
+ }
672
+
673
+ function applyStrategy(event, request, config) {
674
+ event.respondWith(_executeStrategy(event, request, config));
675
+ }
676
+
470
677
  ${mutationQueueEnabled
471
678
  ? `// --- Mutation Queue IDB Helpers ---
472
679
 
473
680
  const MUTATION_DB_NAME = "swoff-queue";
474
681
  const MUTATION_STORE_NAME = "mutations";
475
- // Bump this when adding new indexes/stores for schema migration
476
682
  const MUTATION_DB_VERSION = 1;
477
683
 
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
684
  async function storeMutationInSW(request) {
685
+ swLog("storeMutationInSW", "ENTER", request.url, 4);
494
686
  let body, bodyType;
495
687
  try {
496
688
  body = await request.clone().json();
@@ -503,9 +695,19 @@ async function storeMutationInSW(request) {
503
695
  const tagsHeader = request.headers.get("X-SW-Invalidate-Tags");
504
696
  const tags = tagsHeader ? tagsHeader.split(",").map(function(t) { return t.trim(); }) : [];
505
697
 
506
- const db = await openMutationQueueDB();
698
+ const db = await openDB(MUTATION_DB_NAME, MUTATION_DB_VERSION, function(db) {
699
+ if (!db.objectStoreNames.contains(MUTATION_STORE_NAME)) {
700
+ const store = db.createObjectStore(MUTATION_STORE_NAME, { keyPath: "id" });
701
+ store.createIndex("by-timestamp", "timestamp");
702
+ }
703
+ });${maxCacheAge && maxCacheAge > 0
704
+ ? `
705
+ // Prune expired entries in a separate transaction
706
+ await pruneStaleStore(db, MUTATION_STORE_NAME, Date.now() - MAX_RUNTIME_CACHE_AGE * 1000, "id");`
707
+ : ""}
507
708
  const tx = db.transaction(MUTATION_STORE_NAME, "readwrite");
508
- tx.objectStore(MUTATION_STORE_NAME).add({
709
+ const store = tx.objectStore(MUTATION_STORE_NAME);
710
+ store.add({
509
711
  id: crypto.randomUUID(),
510
712
  method: request.method,
511
713
  url: new URL(request.url).href,
@@ -517,26 +719,54 @@ async function storeMutationInSW(request) {
517
719
  nextRetryAt: 0,
518
720
  tags,
519
721
  });
520
- await new Promise(function(resolve, reject) {
521
- tx.oncomplete = function() { resolve(); };
522
- tx.onerror = function() { reject(tx.error); };
523
- });
722
+ try {
723
+ await new Promise(function(resolve, reject) {
724
+ tx.oncomplete = function() { resolve(); };
725
+ tx.onerror = function() { reject(tx.error); };
726
+ });
727
+ swLog("storeMutationInSW", "STORED", request.url, 4);
728
+ } finally {
729
+ db.close();
730
+ }
731
+ }
732
+
733
+ async function _fetchMutation(request) {
734
+ swLog("_fetchMutation", "ENTER", request.url, 4);
735
+ const controller = new AbortController();
736
+ const id = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
737
+ try {
738
+ const response = await fetch(request, { signal: controller.signal });
739
+ swLog("_fetchMutation", "SUCCESS " + response.status, request.url, 4);
740
+ return response;
741
+ } catch (e) {
742
+ swLog("_fetchMutation", "FAILED", request.url, 4);
743
+ throw e;
744
+ } finally {
745
+ clearTimeout(id);
746
+ }
524
747
  }
525
748
 
526
749
  async function handleMutation(event) {
750
+ swLog("handleMutation", "ENTER", event.request.url, 3);
527
751
  const request = event.request;
528
752
  if (request.headers.get("X-SW-No-Queue") === "true") {
529
- return fetch(request.clone());
753
+ swLog("handleMutation", "no-queue mode", request.url, 3);
754
+ try {
755
+ const response = await _fetchMutation(request.clone());
756
+ checkAuthFailure(response);
757
+ return response;
758
+ } catch {
759
+ throw new Error("Mutation failed (no-queue mode)");
760
+ }
530
761
  }
531
762
  try {
532
- return await fetch(request.clone());
763
+ const response = await _fetchMutation(request.clone());
764
+ checkAuthFailure(response);
765
+ return response;
533
766
  } catch {
767
+ swLog("handleMutation", "queuing mutation", request.url, 3);
534
768
  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
- });
769
+ broadcastToClients("MUTATION_STORED");
540
770
  const queuedBody = JSON.stringify({ queued: true });
541
771
  return new Response(queuedBody, {
542
772
  status: 202,
@@ -548,146 +778,42 @@ async function handleMutation(event) {
548
778
 
549
779
  `
550
780
  : ""}self.addEventListener("fetch", (event) => {
551
- const { request } = event;${mutationQueueEnabled
781
+ const { request } = event;
782
+ swLog("fetch", "INCOMING", request.url, 0);${serverPushEndpoint
783
+ ? `
784
+ if (SERVER_PUSH_ENDPOINT && new URL(request.url).pathname.includes(SERVER_PUSH_ENDPOINT)) {
785
+ swLog("fetch", "server-push-bypass", request.url, 0);
786
+ return;
787
+ }`
788
+ : ""}${authRoutePaths.length > 0
552
789
  ? `
553
- if (request.method !== "GET" && request.method !== "HEAD") {
554
- if (request.headers.get("X-SW-Cache-Strategy") === "mutation") {
790
+ if (AUTH_ROUTES.some(function(route) { return new URL(request.url).pathname.includes(route); })) {
791
+ swLog("fetch", "auth-route-bypass", request.url, 0);
792
+ return;
793
+ }`
794
+ : ""}
795
+ if (request.headers.get("X-SW-Strategy") === "network-only") {
796
+ swLog("fetch", "strategy=network-only", request.url, 0);
797
+ return;
798
+ }${mutationQueueEnabled
799
+ ? `
800
+ if (request.method !== "GET") {
801
+ if (request.headers.get("X-SW-Type") === "mutation") {
555
802
  event.respondWith(handleMutation(event));
556
803
  return;
557
804
  }
558
- if (!request.headers.get("X-SW-Cache-Key")) return;
805
+ if (!request.headers.get("X-SW-Cache-Key")) { return; }
559
806
  }`
560
807
  : `
561
- if (request.method !== "GET" && request.method !== "HEAD") {
562
- if (!request.headers.get("X-SW-Cache-Key")) return;
808
+ if (request.method !== "GET") {
809
+ if (!request.headers.get("X-SW-Cache-Key")) { return; }
563
810
  }`}
564
- ${cacheStrategy === "explicit-only" ? `if (!request.headers.get("X-SW-Cache-Strategy")) return;` : ""}
565
- applyStrategy(event, request, determineCacheStrategy(event.request, ${JSON.stringify(strategies)}, { defaultStrategy: "${defaultStrategy}" }));
566
- });
567
-
568
- // --- Strategies ---
569
-
570
- ${navigationPreload
571
- ? `
572
- async function fetchWithPreload(event, request) {
573
- try {
574
- const preload = await event.preloadResponse;
575
- if (preload) return preload;
576
- } catch {}
577
- return fetch(request);
578
- }
579
- `
580
- : ""}const _fetch = ${navigationPreload ? "fetchWithPreload" : `(event, request) => fetch(request)`};
581
-
582
- async function cacheFirst(event, request) {
583
- const cached = await fromRuntime(request);
584
- if (cached) {${staleVersionCode}
585
- return markFromCache(cached);
811
+ const cfg = determineCacheStrategy(request);
812
+ swLog("fetch", "strategy=" + cfg.strategy, request.url, 0);
813
+ if (cfg.strategy === "reactive") {
814
+ registerReactiveEntry(cacheKey(request), request.url, cfg);
586
815
  }
587
-
588
- const precached = await fromPrecache(request);
589
- if (precached) return markFromCache(precached);
590
-
591
- const fallback = await fromSpaFallback(request);
592
- if (fallback) return fallback;
593
-
594
- const reqForCache = request.clone();
595
- const response = await _fetch(event, request);
596
- if (response.ok) {
597
- const responseToCache = response.clone();
598
- event.waitUntil(cacheResponse(reqForCache, responseToCache));
599
- }
600
- return response;
601
- }
602
-
603
- async function networkFirst(event, request) {
604
- try {
605
- const reqForCache = request.clone();
606
- const response = await _fetch(event, request);
607
- if (response.ok) {
608
- const responseToCache = response.clone();
609
- event.waitUntil(cacheResponse(reqForCache, responseToCache));
610
- }
611
- return response;
612
- } catch {
613
- const cached = await fromRuntime(request);
614
- if (cached) {
615
- return markFromCache(cached);
616
- }
617
-
618
- const precached = await fromPrecache(request);
619
- if (precached) return markFromCache(precached);
620
-
621
- const fallback = await fromSpaFallback(request);
622
- if (fallback) return fallback;
623
-
624
- throw new Error("Network request failed and no cached response available");
625
- }
626
- }
627
-
628
- async function staleWhileRevalidate(event, request) {
629
- const cached = await fromRuntime(request);
630
- if (cached) {
631
- event.waitUntil(queueRefresh(cacheKey(request), new URL(request.url).href));
632
- return markFromCache(cached);
633
- }
634
-
635
- const precached = await fromPrecache(request);
636
- if (precached) {
637
- event.waitUntil(queueRefresh(cacheKey(request), new URL(request.url).href));
638
- return markFromCache(precached);
639
- }
640
-
641
- const reqForCache = request.clone();
642
- const response = await _fetch(event, request);
643
- if (response.ok) {
644
- const responseToCache = response.clone();
645
- await cacheResponse(reqForCache, responseToCache);
646
- }
647
- return response;
648
- }
649
-
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));
658
- }
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);
673
- }
674
- return response;
675
- }
676
-
677
- async function cacheOnly(event, request) {
678
- const cached = await fromRuntime(request);
679
- if (cached) {${staleVersionCode}
680
- return markFromCache(cached);
681
- }
682
-
683
- const precached = await fromPrecache(request);
684
- if (precached) return markFromCache(precached);
685
-
686
- return new Response("Not in cache", { status: 404 });
687
- }
688
-
689
- async function networkOnly(event, request) {
690
- return _fetch(event, request);
691
- }`;
816
+ applyStrategy(event, request, cfg);
817
+ });`;
692
818
  }
693
819
  //# sourceMappingURL=fetch-handler.js.map