@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -867
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1133
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- /// <reference path="../../vite/version.d.ts" />
1
+ /// <reference path="../../vite/plugins/version.d.ts" />
2
2
 
3
3
  // Extend CacheStorage with Cloudflare's default cache property
4
4
  declare global {
@@ -25,12 +25,19 @@ import type {
25
25
  CachedEntryData,
26
26
  CacheDefaults,
27
27
  CacheGetResult,
28
+ CacheItemResult,
29
+ CacheItemOptions,
28
30
  } from "../types.js";
29
31
  import {
30
- getRequestContext,
32
+ _getRequestContext,
31
33
  type RequestContext,
32
34
  } from "../../server/request-context.js";
33
35
  import { VERSION } from "@rangojs/router:version";
36
+ import {
37
+ resolveTtl,
38
+ resolveSwrWindow,
39
+ DEFAULT_FUNCTION_TTL,
40
+ } from "../cache-policy.js";
34
41
 
35
42
  // ============================================================================
36
43
  // Constants
@@ -124,7 +131,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
124
131
  * @example Using cookies for locale-aware caching
125
132
  * ```typescript
126
133
  * keyGenerator: (ctx, defaultKey) => {
127
- * const locale = ctx.cookie('locale') || 'en';
134
+ * const locale = cookies().get('locale')?.value || 'en';
128
135
  * return `${locale}:${defaultKey}`;
129
136
  * }
130
137
  * ```
@@ -176,13 +183,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
176
183
 
177
184
  /**
178
185
  * Derive base URL from request hostname via requestContext.
179
- * Uses internal fallback for dev/preview environments.
186
+ * Uses internal fallback for dev/preview environments and untrusted hostnames.
180
187
  * @internal
181
188
  */
182
189
  private deriveBaseUrl(): string {
183
190
  const fallback = "https://rsc-cache.internal.com/";
184
191
 
185
- const ctx = getRequestContext();
192
+ const ctx = _getRequestContext();
186
193
  if (!ctx?.request) {
187
194
  return fallback;
188
195
  }
@@ -201,6 +208,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
201
208
  return fallback;
202
209
  }
203
210
 
211
+ // Validate hostname: must be a valid domain (alphanumeric, hyphens, dots)
212
+ // to prevent host header injection into cache keys
213
+ if (!/^[a-zA-Z0-9.-]+$/.test(hostname) || hostname.length > 253) {
214
+ return fallback;
215
+ }
216
+
204
217
  // Use actual hostname for production
205
218
  return `https://${hostname}/`;
206
219
  } catch {
@@ -291,7 +304,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
291
304
  const request = this.keyToRequest(key);
292
305
 
293
306
  // Extended TTL covers SWR window
294
- const swrWindow = swr ?? this.defaults?.swr ?? 0;
307
+ const swrWindow = resolveSwrWindow(swr, this.defaults);
295
308
  const totalTtl = ttl + swrWindow;
296
309
  const staleAt = Date.now() + ttl * 1000;
297
310
 
@@ -381,7 +394,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
381
394
  const request = this.keyToRequest(`doc:${key}`);
382
395
 
383
396
  // Extended TTL covers SWR window
384
- const swrWindow = swr ?? this.defaults?.swr ?? 0;
397
+ const swrWindow = resolveSwrWindow(swr, this.defaults);
385
398
  const totalTtl = ttl + swrWindow;
386
399
  const staleAt = Date.now() + ttl * 1000;
387
400
 
@@ -412,6 +425,105 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
412
425
  }
413
426
  }
414
427
 
428
+ // ============================================================================
429
+ // Function Cache Methods (for "use cache" directive)
430
+ // ============================================================================
431
+
432
+ /**
433
+ * Get a cached function result by key.
434
+ * Follows the same SWR pattern as get() for segment caching.
435
+ */
436
+ async getItem(key: string): Promise<CacheItemResult | null> {
437
+ try {
438
+ const cache = await this.getCache();
439
+ const request = this.keyToRequest(`fn:${key}`);
440
+ const response = await cache.match(request);
441
+
442
+ if (!response) return null;
443
+
444
+ const staleAt = Number(
445
+ response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
446
+ );
447
+ const status = response.headers.get(CACHE_STATUS_HEADER);
448
+ const age = Number(response.headers.get("age") ?? "0");
449
+
450
+ const isStale = staleAt > 0 && Date.now() > staleAt;
451
+ const isRevalidating =
452
+ status === "REVALIDATING" && age < MAX_REVALIDATION_INTERVAL;
453
+
454
+ const data = (await response.json()) as {
455
+ value: string;
456
+ handles?: Record<string, Record<string, unknown[]>>;
457
+ };
458
+
459
+ if (!isStale || isRevalidating) {
460
+ return {
461
+ value: data.value,
462
+ handles: data.handles,
463
+ shouldRevalidate: false,
464
+ };
465
+ }
466
+
467
+ // Stale and needs revalidation — mark REVALIDATING atomically
468
+ const headers = new Headers(response.headers);
469
+ headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
470
+ await cache.put(
471
+ request,
472
+ new Response(JSON.stringify(data), { status: 200, headers }),
473
+ );
474
+
475
+ return {
476
+ value: data.value,
477
+ handles: data.handles,
478
+ shouldRevalidate: true,
479
+ };
480
+ } catch (error) {
481
+ console.error("[CFCacheStore] getItem failed:", error);
482
+ return null;
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Store a function result with TTL and optional SWR window.
488
+ */
489
+ async setItem(
490
+ key: string,
491
+ value: string,
492
+ options?: CacheItemOptions,
493
+ ): Promise<void> {
494
+ try {
495
+ const cache = await this.getCache();
496
+ const request = this.keyToRequest(`fn:${key}`);
497
+
498
+ const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
499
+ const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
500
+ const totalTtl = ttl + swrWindow;
501
+ const staleAt = Date.now() + ttl * 1000;
502
+
503
+ const body = JSON.stringify({ value, handles: options?.handles });
504
+ const response = new Response(body, {
505
+ headers: {
506
+ "Content-Type": "application/json",
507
+ "Cache-Control": `public, max-age=${totalTtl}`,
508
+ [CACHE_STALE_AT_HEADER]: String(staleAt),
509
+ [CACHE_STATUS_HEADER]: "HIT",
510
+ },
511
+ });
512
+
513
+ const putPromise = cache.put(request, response);
514
+
515
+ if (this.waitUntil) {
516
+ this.waitUntil(async () => {
517
+ await putPromise;
518
+ });
519
+ } else {
520
+ await putPromise;
521
+ }
522
+ } catch (error) {
523
+ console.error("[CFCacheStore] setItem failed:", error);
524
+ }
525
+ }
526
+
415
527
  /**
416
528
  * Convert string key to Request object for CF Cache API.
417
529
  * Includes version in URL if specified (for cache invalidation on code changes).
@@ -13,7 +13,13 @@
13
13
  export { CFCacheStore, type CFCacheStoreOptions } from "./cf-cache-store.js";
14
14
 
15
15
  // Header constants for debugging and inspection
16
- export { CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER } from "./cf-cache-store.js";
16
+ export {
17
+ CACHE_STALE_AT_HEADER,
18
+ CACHE_STATUS_HEADER,
19
+ } from "./cf-cache-store.js";
17
20
 
18
21
  // Internal exports (re-exported for backwards compatibility, marked @internal in source)
19
- export { type CacheStatus, MAX_REVALIDATION_INTERVAL } from "./cf-cache-store.js";
22
+ export {
23
+ type CacheStatus,
24
+ MAX_REVALIDATION_INTERVAL,
25
+ } from "./cf-cache-store.js";
@@ -13,6 +13,8 @@
13
13
 
14
14
  import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
15
15
  import { getRequestContext } from "../server/request-context.js";
16
+ import { sortedSearchString } from "./cache-key-utils.js";
17
+ import { runBackground } from "./background-task.js";
16
18
 
17
19
  // ============================================================================
18
20
  // Constants
@@ -110,15 +112,24 @@ function addCacheStatusHeader(
110
112
  }
111
113
 
112
114
  /**
113
- * Run onResponse callbacks registered on the request context
115
+ * Drain and run onResponse callbacks registered on the request context.
116
+ * Mirrors the drain semantics of finalizeResponse() in rsc/helpers.ts:
117
+ * callbacks are spliced out so they fire at most once per request.
114
118
  */
115
- function runOnResponseCallbacks(
119
+ function drainOnResponseCallbacks(
116
120
  response: Response,
117
- callbacks: Array<(response: Response) => Response>,
121
+ requestCtx:
122
+ | { _onResponseCallbacks: Array<(r: Response) => Response> }
123
+ | undefined,
118
124
  ): Response {
125
+ if (!requestCtx || requestCtx._onResponseCallbacks.length === 0) {
126
+ return response;
127
+ }
128
+ const callbacks = requestCtx._onResponseCallbacks;
129
+ requestCtx._onResponseCallbacks = [];
119
130
  let result = response;
120
131
  for (const callback of callbacks) {
121
- result = callback(result);
132
+ result = callback(result) ?? result;
122
133
  }
123
134
  return result;
124
135
  }
@@ -185,9 +196,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
185
196
  ): MiddlewareFn<TEnv> {
186
197
  const { skipPaths = [], keyGenerator, isEnabled, debug = false } = options;
187
198
 
188
- const log = debug
189
- ? (message: string) => console.log(message)
190
- : () => {};
199
+ const log = debug ? (message: string) => console.log(message) : () => {};
191
200
 
192
201
  return async function documentCacheMiddleware(
193
202
  ctx: MiddlewareContext<TEnv>,
@@ -195,6 +204,11 @@ export function createDocumentCacheMiddleware<TEnv = any>(
195
204
  ): Promise<Response> {
196
205
  const url = ctx.url;
197
206
 
207
+ // Only cache GET requests — mutations and other methods must not be cached
208
+ if (ctx.request.method !== "GET") {
209
+ return next();
210
+ }
211
+
198
212
  // Skip RSC action requests (mutations shouldn't be cached)
199
213
  if (url.searchParams.has("_rsc_action")) {
200
214
  return next();
@@ -231,17 +245,31 @@ export function createDocumentCacheMiddleware<TEnv = any>(
231
245
  const isPartial = url.searchParams.has("_rsc_partial");
232
246
  const typeLabel = isPartial ? "RSC" : "HTML";
233
247
 
234
- // Generate cache key
235
- // For partial requests, include hash of client segments to prevent serving
236
- // wrong cached response when navigating from different pages with different layouts
237
- const clientSegments = url.searchParams.get("_rsc_segments") || "";
238
- const segmentHash = isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
239
- const typeSuffix = isPartial ? ":rsc" : ":html";
240
- const cacheKey = keyGenerator
241
- ? keyGenerator(url) + segmentHash + typeSuffix
242
- : `${url.pathname}${segmentHash}${typeSuffix}`;
248
+ // Track whether next() has been called so the catch block knows
249
+ // whether it is safe to fall through to the handler.
250
+ let handlerCalled = false;
243
251
 
244
252
  try {
253
+ // Generate cache key inside try so a throwing keyGenerator degrades
254
+ // gracefully to the origin handler instead of rejecting the request.
255
+ // This is a deliberate fail-open-to-origin policy: the fallback is
256
+ // "serve uncached from origin", not "use a different cache key".
257
+ const clientSegments = url.searchParams.get("_rsc_segments") || "";
258
+ const segmentHash =
259
+ isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
260
+ const typeSuffix = isPartial ? ":rsc" : ":html";
261
+
262
+ let searchSuffix = "";
263
+ if (!keyGenerator) {
264
+ const sorted = sortedSearchString(url.searchParams);
265
+ if (sorted) {
266
+ searchSuffix = `?${sorted}`;
267
+ }
268
+ }
269
+
270
+ const cacheKey = keyGenerator
271
+ ? keyGenerator(url) + segmentHash + typeSuffix
272
+ : `${url.pathname}${searchSuffix}${segmentHash}${typeSuffix}`;
245
273
  // 1. Check cache
246
274
  const cached = await store.getResponse(cacheKey);
247
275
 
@@ -249,79 +277,75 @@ export function createDocumentCacheMiddleware<TEnv = any>(
249
277
  if (!cached.shouldRevalidate) {
250
278
  // Fresh hit - return immediately
251
279
  log(`[DocumentCache] HIT ${typeLabel}: ${url.pathname}`);
252
- let response = addCacheStatusHeader(cached.response, "HIT");
253
- // Run onResponse callbacks even for cache hits
254
- if (requestCtx && requestCtx._onResponseCallbacks.length > 0) {
255
- response = runOnResponseCallbacks(
256
- response,
257
- requestCtx._onResponseCallbacks,
258
- );
259
- }
260
- return response;
280
+ return drainOnResponseCallbacks(
281
+ addCacheStatusHeader(cached.response, "HIT"),
282
+ requestCtx,
283
+ );
261
284
  }
262
285
 
263
286
  // Stale hit - return cached response, revalidate in background
264
- log(`[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`);
265
-
266
- if (requestCtx) {
267
- requestCtx.waitUntil(async () => {
268
- try {
269
- const fresh = await next();
270
- const directives = shouldCacheResponse(fresh);
271
-
272
- if (directives) {
273
- await store.putResponse!(
274
- cacheKey,
275
- fresh,
276
- directives.sMaxAge!,
277
- directives.staleWhileRevalidate,
278
- );
279
- log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
280
- }
281
- } catch (error) {
282
- console.error(`[DocumentCache] Revalidation failed:`, error);
287
+ log(
288
+ `[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`,
289
+ );
290
+
291
+ runBackground(requestCtx, async () => {
292
+ try {
293
+ const fresh = await next();
294
+ const directives = shouldCacheResponse(fresh);
295
+
296
+ if (directives) {
297
+ await store.putResponse!(
298
+ cacheKey,
299
+ fresh,
300
+ directives.sMaxAge!,
301
+ directives.staleWhileRevalidate,
302
+ );
303
+ log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
283
304
  }
284
- });
285
- }
305
+ } catch (error) {
306
+ console.error(`[DocumentCache] Revalidation failed:`, error);
307
+ }
308
+ });
286
309
 
287
- let response = addCacheStatusHeader(cached.response, "STALE");
288
- // Run onResponse callbacks even for stale cache hits
289
- if (requestCtx && requestCtx._onResponseCallbacks.length > 0) {
290
- response = runOnResponseCallbacks(
291
- response,
292
- requestCtx._onResponseCallbacks,
293
- );
294
- }
295
- return response;
310
+ return drainOnResponseCallbacks(
311
+ addCacheStatusHeader(cached.response, "STALE"),
312
+ requestCtx,
313
+ );
296
314
  }
297
315
 
298
316
  // 2. Cache miss - run handler
317
+ handlerCalled = true;
299
318
  const originalResponse = await next();
300
319
 
301
320
  // 3. Cache if response has appropriate headers
302
321
  const directives = shouldCacheResponse(originalResponse);
303
322
 
304
323
  if (directives) {
305
- log(`[DocumentCache] MISS ${typeLabel}: ${url.pathname} (caching with s-maxage=${directives.sMaxAge})`);
324
+ log(
325
+ `[DocumentCache] MISS ${typeLabel}: ${url.pathname} (caching with s-maxage=${directives.sMaxAge})`,
326
+ );
327
+
328
+ // If the response has no body (e.g., 200 with empty body), skip caching
329
+ if (!originalResponse.body) {
330
+ return originalResponse;
331
+ }
306
332
 
307
333
  // Tee the body so we can return one stream and cache the other
308
- const [returnStream, cacheStream] = originalResponse.body!.tee();
334
+ const [returnStream, cacheStream] = originalResponse.body.tee();
309
335
 
310
336
  // Clone response for caching (non-blocking)
311
- if (requestCtx) {
312
- requestCtx.waitUntil(async () => {
313
- try {
314
- await store.putResponse!(
315
- cacheKey,
316
- new Response(cacheStream, originalResponse),
317
- directives.sMaxAge!,
318
- directives.staleWhileRevalidate,
319
- );
320
- } catch (error) {
321
- console.error(`[DocumentCache] Cache write failed:`, error);
322
- }
323
- });
324
- }
337
+ runBackground(requestCtx, async () => {
338
+ try {
339
+ await store.putResponse!(
340
+ cacheKey,
341
+ new Response(cacheStream, originalResponse),
342
+ directives.sMaxAge!,
343
+ directives.staleWhileRevalidate,
344
+ );
345
+ } catch (error) {
346
+ console.error(`[DocumentCache] Cache write failed:`, error);
347
+ }
348
+ });
325
349
 
326
350
  return addCacheStatusHeader(
327
351
  new Response(returnStream, originalResponse),
@@ -333,7 +357,12 @@ export function createDocumentCacheMiddleware<TEnv = any>(
333
357
  return originalResponse;
334
358
  } catch (error) {
335
359
  console.error(`[DocumentCache] Error:`, error);
336
- // On any cache error, fall through to handler
360
+ if (handlerCalled) {
361
+ // Post-handler failure (e.g. body.tee()): do not call next() again
362
+ // as that would re-run handler side effects.
363
+ throw error;
364
+ }
365
+ // Pre-handler failure (cache lookup): degrade gracefully to origin
337
366
  return next();
338
367
  }
339
368
  };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Handle Capture
3
+ *
4
+ * Captures handle pushes during cached function execution.
5
+ * Extracted from cache-runtime.ts so tests can import without
6
+ * pulling in @vitejs/plugin-rsc/rsc dependencies.
7
+ */
8
+
9
+ import type { HandleStore } from "../server/handle-store.js";
10
+ import type { SegmentHandleData } from "./types.js";
11
+
12
+ export interface HandleCapture {
13
+ data: Record<string, SegmentHandleData>;
14
+ }
15
+
16
+ /**
17
+ * Active capture tokens per HandleStore.
18
+ *
19
+ * Instead of mutating handleStore.push (which breaks when overlapping
20
+ * captures finish out of order), we install a single interceptor on
21
+ * first use and manage a set of active capture tokens. Each push fans
22
+ * out to every active token. Stopping a capture simply removes the
23
+ * token — order does not matter.
24
+ */
25
+ const activeCapturesMap = new WeakMap<HandleStore, Set<HandleCapture>>();
26
+
27
+ /**
28
+ * One-time interceptor installation. Wraps the original push so every
29
+ * call fans out to all active capture tokens. Installed once per
30
+ * HandleStore instance; subsequent startHandleCapture calls on the
31
+ * same store just add tokens to the Set.
32
+ */
33
+ function ensureInterceptorInstalled(handleStore: HandleStore): void {
34
+ if (activeCapturesMap.has(handleStore)) return;
35
+
36
+ const captures = new Set<HandleCapture>();
37
+ activeCapturesMap.set(handleStore, captures);
38
+
39
+ const originalPush = handleStore.push.bind(handleStore);
40
+ handleStore.push = (
41
+ handleName: string,
42
+ segmentId: string,
43
+ value: unknown,
44
+ ) => {
45
+ for (const capture of captures) {
46
+ if (!capture.data[segmentId]) {
47
+ capture.data[segmentId] = {};
48
+ }
49
+ if (!capture.data[segmentId][handleName]) {
50
+ capture.data[segmentId][handleName] = [];
51
+ }
52
+ capture.data[segmentId][handleName].push(value);
53
+ }
54
+ originalPush(handleName, segmentId, value);
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Start capturing handle pushes for a cached function execution.
60
+ *
61
+ * Concurrency-safe: multiple overlapping captures on the same
62
+ * HandleStore are independent. Each capture registers a token in a
63
+ * Set; stopping removes it. No ordering requirement (LIFO not needed).
64
+ */
65
+ export function startHandleCapture(handleStore: HandleStore): {
66
+ capture: HandleCapture;
67
+ stop: () => void;
68
+ } {
69
+ ensureInterceptorInstalled(handleStore);
70
+
71
+ const capture: HandleCapture = { data: {} };
72
+ const captures = activeCapturesMap.get(handleStore)!;
73
+ captures.add(capture);
74
+
75
+ return {
76
+ capture,
77
+ stop() {
78
+ captures.delete(capture);
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Handle Snapshot
3
+ *
4
+ * Capture and restore handle data for cached segments.
5
+ * Handle data (breadcrumbs, metadata from ctx.use(Handle)) is collected
6
+ * during segment resolution and stored alongside cached segments.
7
+ */
8
+
9
+ import type { ResolvedSegment } from "../types.js";
10
+ import type { HandleStore } from "../server/handle-store.js";
11
+ import type { SegmentHandleData } from "./types.js";
12
+
13
+ /**
14
+ * Capture handle data for a set of segments from the handle store.
15
+ * Used when caching segments to preserve their handle data.
16
+ */
17
+ export function captureHandles(
18
+ segments: ResolvedSegment[],
19
+ handleStore: HandleStore,
20
+ ): Record<string, SegmentHandleData> {
21
+ const handles: Record<string, SegmentHandleData> = {};
22
+ for (const seg of segments) {
23
+ handles[seg.id] = handleStore.getDataForSegment(seg.id);
24
+ }
25
+ return handles;
26
+ }
27
+
28
+ /**
29
+ * Restore handle data from a cached snapshot into the handle store.
30
+ * Used when serving cached segments to replay their handle data.
31
+ */
32
+ export function restoreHandles(
33
+ handles: Record<string, SegmentHandleData>,
34
+ handleStore: HandleStore,
35
+ ): void {
36
+ for (const [segId, segHandles] of Object.entries(handles)) {
37
+ if (Object.keys(segHandles).length > 0) {
38
+ handleStore.replaySegmentData(segId, segHandles);
39
+ }
40
+ }
41
+ }
@@ -10,21 +10,6 @@
10
10
  * - CacheScope / createCacheScope - Request-scoped cache provider
11
11
  */
12
12
 
13
- // Generic cache store types (reserved for future extensibility)
14
- // These types support caching arbitrary values like Response, Stream, etc.
15
- // Currently unused - segment caching uses SegmentCacheStore directly.
16
- export type {
17
- CacheStore,
18
- CacheEntry,
19
- CacheValue,
20
- CacheValueType,
21
- CachePutOptions,
22
- CacheMetadata,
23
- } from "./types.js";
24
-
25
- // Generic memory cache (reserved for future extensibility)
26
- export { MemoryCacheStore } from "./memory-store.js";
27
-
28
13
  // Segment cache store types and implementations
29
14
  export type {
30
15
  SegmentCacheStore,