@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125

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 (260) hide show
  1. package/dist/bin/rango.js +10 -6
  2. package/dist/testing/vitest.js +82 -0
  3. package/dist/vite/index.js +55 -48
  4. package/package.json +61 -21
  5. package/skills/caching/SKILL.md +2 -1
  6. package/skills/hooks/SKILL.md +40 -29
  7. package/skills/host-router/SKILL.md +16 -2
  8. package/skills/intercept/SKILL.md +4 -2
  9. package/skills/layout/SKILL.md +11 -6
  10. package/skills/loader/SKILL.md +6 -2
  11. package/skills/middleware/SKILL.md +4 -2
  12. package/skills/migrate-nextjs/SKILL.md +3 -1
  13. package/skills/parallel/SKILL.md +9 -4
  14. package/skills/rango/SKILL.md +12 -0
  15. package/skills/route/SKILL.md +10 -2
  16. package/skills/testing/SKILL.md +129 -0
  17. package/skills/testing/bindings.md +89 -0
  18. package/skills/testing/cache-prerender.md +98 -0
  19. package/skills/testing/client-components.md +122 -0
  20. package/skills/testing/e2e-parity.md +125 -0
  21. package/skills/testing/flight.md +89 -0
  22. package/skills/testing/handles.md +129 -0
  23. package/skills/testing/loader.md +128 -0
  24. package/skills/testing/middleware.md +99 -0
  25. package/skills/testing/render-handler.md +118 -0
  26. package/skills/testing/response-routes.md +95 -0
  27. package/skills/testing/reverse-and-types.md +84 -0
  28. package/skills/testing/server-actions.md +107 -0
  29. package/skills/testing/server-tree.md +128 -0
  30. package/skills/testing/setup.md +120 -0
  31. package/src/__internal.ts +0 -65
  32. package/src/browser/action-coordinator.ts +1 -1
  33. package/src/browser/action-fence.ts +47 -0
  34. package/src/browser/cookie-name.ts +140 -0
  35. package/src/browser/event-controller.ts +1 -83
  36. package/src/browser/invalidate-client-cache.ts +52 -0
  37. package/src/browser/navigation-bridge.ts +14 -1
  38. package/src/browser/navigation-client.ts +14 -1
  39. package/src/browser/navigation-store-handle.ts +38 -0
  40. package/src/browser/navigation-store.ts +26 -51
  41. package/src/browser/navigation-transaction.ts +0 -32
  42. package/src/browser/partial-update.ts +1 -83
  43. package/src/browser/prefetch/cache.ts +6 -45
  44. package/src/browser/prefetch/fetch.ts +7 -0
  45. package/src/browser/prefetch/queue.ts +6 -3
  46. package/src/browser/rango-state.ts +157 -99
  47. package/src/browser/react/Link.tsx +0 -2
  48. package/src/browser/react/NavigationProvider.tsx +2 -1
  49. package/src/browser/react/ScrollRestoration.tsx +10 -6
  50. package/src/browser/react/filter-segment-order.ts +0 -2
  51. package/src/browser/react/index.ts +0 -51
  52. package/src/browser/react/location-state-shared.ts +0 -13
  53. package/src/browser/react/location-state.ts +0 -1
  54. package/src/browser/react/use-action.ts +6 -15
  55. package/src/browser/react/use-handle.ts +0 -5
  56. package/src/browser/react/use-link-status.ts +0 -4
  57. package/src/browser/react/use-navigation.ts +0 -3
  58. package/src/browser/react/use-params.ts +0 -2
  59. package/src/browser/react/use-search-params.ts +0 -5
  60. package/src/browser/react/use-segments.ts +0 -13
  61. package/src/browser/rsc-router.tsx +12 -4
  62. package/src/browser/server-action-bridge.ts +77 -15
  63. package/src/browser/types.ts +7 -2
  64. package/src/browser/validate-redirect-origin.ts +4 -5
  65. package/src/build/route-trie.ts +3 -0
  66. package/src/build/route-types/param-extraction.ts +6 -3
  67. package/src/build/route-types/router-processing.ts +0 -8
  68. package/src/cache/cache-policy.ts +0 -54
  69. package/src/cache/cache-runtime.ts +27 -24
  70. package/src/cache/cache-scope.ts +0 -27
  71. package/src/cache/cache-tag.ts +0 -37
  72. package/src/cache/cf/cf-cache-store.ts +94 -46
  73. package/src/cache/cf/index.ts +0 -24
  74. package/src/cache/document-cache.ts +11 -36
  75. package/src/cache/handle-snapshot.ts +0 -40
  76. package/src/cache/index.ts +0 -27
  77. package/src/cache/memory-segment-store.ts +2 -48
  78. package/src/cache/profile-registry.ts +7 -3
  79. package/src/cache/read-through-swr.ts +41 -11
  80. package/src/cache/segment-codec.ts +0 -16
  81. package/src/cache/types.ts +0 -98
  82. package/src/client.rsc.tsx +1 -22
  83. package/src/client.tsx +14 -38
  84. package/src/component-utils.ts +19 -0
  85. package/src/deps/ssr.ts +0 -1
  86. package/src/handle.ts +28 -18
  87. package/src/handles/MetaTags.tsx +0 -14
  88. package/src/handles/meta.ts +0 -39
  89. package/src/host/cookie-handler.ts +0 -36
  90. package/src/host/errors.ts +0 -24
  91. package/src/host/index.ts +6 -0
  92. package/src/host/pattern-matcher.ts +7 -50
  93. package/src/host/router.ts +1 -65
  94. package/src/host/testing.ts +40 -27
  95. package/src/host/types.ts +6 -2
  96. package/src/href-client.ts +0 -4
  97. package/src/index.rsc.ts +42 -3
  98. package/src/index.ts +31 -1
  99. package/src/internal-debug.ts +2 -4
  100. package/src/loader.rsc.ts +19 -9
  101. package/src/loader.ts +12 -4
  102. package/src/network-error-thrower.tsx +1 -6
  103. package/src/outlet-provider.tsx +1 -5
  104. package/src/prerender/param-hash.ts +10 -11
  105. package/src/prerender/store.ts +23 -30
  106. package/src/prerender.ts +58 -3
  107. package/src/root-error-boundary.tsx +1 -19
  108. package/src/route-content-wrapper.tsx +1 -44
  109. package/src/route-definition/dsl-helpers.ts +7 -19
  110. package/src/route-definition/helpers-types.ts +3 -3
  111. package/src/route-definition/redirect.ts +11 -1
  112. package/src/route-map-builder.ts +0 -16
  113. package/src/router/basename.ts +14 -0
  114. package/src/router/content-negotiation.ts +0 -13
  115. package/src/router/error-handling.ts +12 -16
  116. package/src/router/find-match.ts +4 -30
  117. package/src/router/intercept-resolution.ts +10 -1
  118. package/src/router/lazy-includes.ts +1 -57
  119. package/src/router/loader-resolution.ts +3 -2
  120. package/src/router/logging.ts +0 -6
  121. package/src/router/manifest.ts +1 -25
  122. package/src/router/match-api.ts +0 -20
  123. package/src/router/match-context.ts +0 -22
  124. package/src/router/match-handlers.ts +57 -58
  125. package/src/router/match-middleware/background-revalidation.ts +0 -7
  126. package/src/router/match-middleware/cache-lookup.ts +1 -54
  127. package/src/router/match-middleware/cache-store.ts +0 -31
  128. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  129. package/src/router/match-middleware/segment-resolution.ts +0 -21
  130. package/src/router/match-pipelines.ts +1 -42
  131. package/src/router/match-result.ts +1 -52
  132. package/src/router/metrics.ts +0 -34
  133. package/src/router/middleware-cookies.ts +0 -13
  134. package/src/router/middleware-types.ts +0 -115
  135. package/src/router/middleware.ts +7 -30
  136. package/src/router/navigation-snapshot.ts +0 -51
  137. package/src/router/params-util.ts +23 -0
  138. package/src/router/pattern-matching.ts +1 -33
  139. package/src/router/prerender-match.ts +33 -45
  140. package/src/router/request-classification.ts +1 -38
  141. package/src/router/revalidation.ts +5 -58
  142. package/src/router/router-context.ts +0 -26
  143. package/src/router/router-interfaces.ts +7 -0
  144. package/src/router/router-options.ts +30 -0
  145. package/src/router/segment-resolution/fresh.ts +25 -57
  146. package/src/router/segment-resolution/helpers.ts +34 -0
  147. package/src/router/segment-resolution/loader-cache.ts +10 -13
  148. package/src/router/segment-resolution/revalidation.ts +5 -42
  149. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  150. package/src/router/segment-resolution.ts +4 -1
  151. package/src/router/state-cookie-name.ts +33 -0
  152. package/src/router/telemetry-otel.ts +0 -20
  153. package/src/router/telemetry.ts +96 -19
  154. package/src/router/timeout.ts +0 -20
  155. package/src/router/trie-matching.ts +63 -40
  156. package/src/router/types.ts +1 -63
  157. package/src/router/url-params.ts +0 -5
  158. package/src/router.ts +40 -9
  159. package/src/rsc/handler.ts +14 -2
  160. package/src/rsc/helpers.ts +34 -0
  161. package/src/rsc/origin-guard.ts +0 -12
  162. package/src/rsc/progressive-enhancement.ts +4 -1
  163. package/src/rsc/rsc-rendering.ts +4 -7
  164. package/src/rsc/runtime-warnings.ts +14 -0
  165. package/src/rsc/server-action.ts +30 -28
  166. package/src/rsc/types.ts +2 -1
  167. package/src/runtime-env.ts +18 -0
  168. package/src/search-params.ts +0 -16
  169. package/src/segment-loader-promise.ts +14 -2
  170. package/src/segment-system.tsx +79 -88
  171. package/src/server/cookie-store.ts +52 -1
  172. package/src/server/handle-store.ts +7 -24
  173. package/src/server/loader-registry.ts +5 -24
  174. package/src/server/request-context.ts +74 -77
  175. package/src/ssr/index.tsx +14 -14
  176. package/src/static-handler.ts +10 -13
  177. package/src/testing/cache-status.ts +119 -0
  178. package/src/testing/collect-handle.ts +40 -0
  179. package/src/testing/dispatch.ts +581 -0
  180. package/src/testing/dom.entry.ts +22 -0
  181. package/src/testing/e2e/fixture.ts +188 -0
  182. package/src/testing/e2e/index.ts +127 -0
  183. package/src/testing/e2e/matchers.ts +35 -0
  184. package/src/testing/e2e/page-helpers.ts +272 -0
  185. package/src/testing/e2e/parity.ts +387 -0
  186. package/src/testing/e2e/server.ts +195 -0
  187. package/src/testing/flight-matchers.ts +97 -0
  188. package/src/testing/flight-normalize.ts +11 -0
  189. package/src/testing/flight-runtime.d.ts +57 -0
  190. package/src/testing/flight-tree.ts +682 -0
  191. package/src/testing/flight.entry.ts +52 -0
  192. package/src/testing/flight.ts +186 -0
  193. package/src/testing/generated-routes.ts +183 -0
  194. package/src/testing/index.ts +98 -0
  195. package/src/testing/internal/context.ts +348 -0
  196. package/src/testing/internal/flight-client-globals.ts +30 -0
  197. package/src/testing/internal/seed-vars.ts +54 -0
  198. package/src/testing/render-handler.ts +311 -0
  199. package/src/testing/render-route.tsx +504 -0
  200. package/src/testing/run-loader.ts +378 -0
  201. package/src/testing/run-middleware.ts +205 -0
  202. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  203. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  204. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  205. package/src/testing/vitest-stubs/version.ts +5 -0
  206. package/src/testing/vitest.ts +305 -0
  207. package/src/theme/ThemeProvider.tsx +0 -52
  208. package/src/theme/ThemeScript.tsx +0 -6
  209. package/src/theme/constants.ts +0 -12
  210. package/src/theme/index.ts +0 -7
  211. package/src/theme/theme-context.ts +1 -5
  212. package/src/theme/theme-script.ts +0 -14
  213. package/src/theme/use-theme.ts +0 -3
  214. package/src/types/boundaries.ts +0 -35
  215. package/src/types/error-types.ts +25 -89
  216. package/src/types/global-namespace.ts +15 -15
  217. package/src/types/handler-context.ts +16 -13
  218. package/src/types/index.ts +0 -10
  219. package/src/types/request-scope.ts +0 -19
  220. package/src/types/route-config.ts +6 -50
  221. package/src/types/route-entry.ts +0 -6
  222. package/src/types/segments.ts +0 -13
  223. package/src/urls/include-helper.ts +0 -4
  224. package/src/urls/index.ts +0 -6
  225. package/src/urls/path-helper-types.ts +2 -2
  226. package/src/urls/path-helper.ts +0 -54
  227. package/src/urls/urls-function.ts +0 -13
  228. package/src/use-loader.tsx +0 -186
  229. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  230. package/src/vite/discovery/discover-routers.ts +6 -7
  231. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  232. package/src/vite/plugin-types.ts +3 -1
  233. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  234. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  235. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  236. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  237. package/src/vite/plugins/expose-action-id.ts +2 -73
  238. package/src/vite/plugins/expose-id-utils.ts +0 -55
  239. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  240. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  241. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  242. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  243. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  244. package/src/vite/plugins/performance-tracks.ts +0 -3
  245. package/src/vite/plugins/use-cache-transform.ts +0 -36
  246. package/src/vite/plugins/version-injector.ts +0 -20
  247. package/src/vite/plugins/version-plugin.ts +1 -49
  248. package/src/vite/plugins/virtual-entries.ts +0 -15
  249. package/src/vite/rango.ts +1 -108
  250. package/src/vite/router-discovery.ts +2 -1
  251. package/src/vite/utils/ast-handler-extract.ts +0 -16
  252. package/src/vite/utils/bundle-analysis.ts +6 -13
  253. package/src/vite/utils/client-chunks.ts +0 -6
  254. package/src/vite/utils/forward-user-plugins.ts +0 -22
  255. package/src/vite/utils/manifest-utils.ts +0 -4
  256. package/src/vite/utils/package-resolution.ts +1 -73
  257. package/src/vite/utils/prerender-utils.ts +0 -35
  258. package/src/vite/utils/shared-utils.ts +3 -35
  259. package/src/browser/react/use-client-cache.ts +0 -58
  260. package/src/browser/shallow.ts +0 -40
@@ -40,6 +40,10 @@ import {
40
40
  type RequestContext,
41
41
  } from "../../server/request-context.js";
42
42
  import { VERSION } from "@rangojs/router:version";
43
+ import {
44
+ isPerClientSignalHeader,
45
+ stripPerClientSignals,
46
+ } from "../../browser/cookie-name.js";
43
47
  import {
44
48
  resolveTtl,
45
49
  resolveSwrWindow,
@@ -214,6 +218,19 @@ function getTagMarkerInflight(
214
218
  return inflight;
215
219
  }
216
220
 
221
+ /**
222
+ * Per-request memo of the derived cache-key base URL.
223
+ *
224
+ * deriveBaseUrl() is a pure function of the live request URL, but keyToRequest
225
+ * calls it on EVERY cache operation (each segment/item get/set/delete, each
226
+ * KV->L1 promote, each tag-marker read), so a page composed of many cached
227
+ * entries re-parses the same request.url and re-runs the host validation tens
228
+ * of times. Keying by the request-context object collapses that to one derive
229
+ * per request. Keyed by ctx alone (not by store) because the derived value
230
+ * depends only on the request URL, not on which store asked.
231
+ */
232
+ const derivedBaseUrlMemo = new WeakMap<object, string>();
233
+
217
234
  /** KV key byte-length ceiling. Cloudflare KV rejects keys larger than this. */
218
235
  const KV_MAX_KEY_BYTES = 512;
219
236
 
@@ -319,10 +336,9 @@ function remainingCacheControl(headers: Headers, now: number): string {
319
336
  // Types
320
337
  // ============================================================================
321
338
 
322
- // Re-exported from the canonical home so cf-cache-store consumers keep
323
- // importing `ExecutionContext` from this module without a second interface
324
- // drifting over time.
325
- export type { ExecutionContext } from "../../types/request-scope.js";
339
+ // Imported from the canonical home (also publicly exported from src/index.ts /
340
+ // src/index.rsc.ts) so this module shares the one interface rather than
341
+ // declaring a second that could drift.
326
342
  import type { ExecutionContext } from "../../types/request-scope.js";
327
343
 
328
344
  /**
@@ -713,12 +729,6 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
713
729
  ) => string | Promise<string>;
714
730
  }
715
731
 
716
- /**
717
- * Cache status values for the x-edge-cache-status header.
718
- * @internal
719
- */
720
- export type CacheStatus = "HIT" | "REVALIDATING";
721
-
722
732
  // ============================================================================
723
733
  // CFCacheStore Implementation
724
734
  // ============================================================================
@@ -806,13 +816,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
806
816
  // tagCacheTtl gates the L1 marker cache via `> 0`. A non-finite value (NaN
807
817
  // from `Number(env.UNSET)`) is not null/undefined, so `?? 0` would let it
808
818
  // through and silently disable the cache while reading as "configured".
809
- // Coerce any non-finite/non-positive value to the documented 0 = disabled.
810
- this.tagCacheTtl =
811
- typeof options.tagCacheTtl === "number" &&
812
- Number.isFinite(options.tagCacheTtl) &&
813
- options.tagCacheTtl > 0
814
- ? options.tagCacheTtl
815
- : 0;
819
+ // finiteBudget coerces non-finite/null/undefined to 0; the `> 0` guard then
820
+ // collapses a finite non-positive value to the documented 0 = disabled.
821
+ const tagCacheTtl = finiteBudget(options.tagCacheTtl, 0);
822
+ this.tagCacheTtl = tagCacheTtl > 0 ? tagCacheTtl : 0;
816
823
 
817
824
  // Read-side tag invalidation requires KV: isGloballyInvalidated() compares an
818
825
  // entry's taggedAt against the per-tag KV marker and short-circuits to "not
@@ -905,31 +912,43 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
905
912
  return fallback;
906
913
  }
907
914
 
908
- try {
909
- const url = new URL(ctx.request.url);
910
- const hostname = url.hostname;
915
+ // The result is deterministic per request, but keyToRequest calls this on
916
+ // every cache operation; memoize per request context (see derivedBaseUrlMemo).
917
+ const memoized = derivedBaseUrlMemo.get(ctx);
918
+ if (memoized !== undefined) {
919
+ return memoized;
920
+ }
911
921
 
912
- // Use fallback for dev/preview environments
913
- if (
914
- hostname === "localhost" ||
915
- hostname === "127.0.0.1" ||
916
- hostname.endsWith(".workers.dev") ||
917
- hostname.endsWith(".pages.dev")
918
- ) {
919
- return fallback;
920
- }
922
+ const derived = ((): string => {
923
+ try {
924
+ const url = new URL(ctx.request.url);
925
+ const hostname = url.hostname;
926
+
927
+ // Use fallback for dev/preview environments
928
+ if (
929
+ hostname === "localhost" ||
930
+ hostname === "127.0.0.1" ||
931
+ hostname.endsWith(".workers.dev") ||
932
+ hostname.endsWith(".pages.dev")
933
+ ) {
934
+ return fallback;
935
+ }
921
936
 
922
- // Validate hostname: must be a valid domain (alphanumeric, hyphens, dots)
923
- // to prevent host header injection into cache keys
924
- if (!/^[a-zA-Z0-9.-]+$/.test(hostname) || hostname.length > 253) {
937
+ // Validate hostname: must be a valid domain (alphanumeric, hyphens, dots)
938
+ // to prevent host header injection into cache keys
939
+ if (!/^[a-zA-Z0-9.-]+$/.test(hostname) || hostname.length > 253) {
940
+ return fallback;
941
+ }
942
+
943
+ // Use actual hostname for production
944
+ return `https://${hostname}/`;
945
+ } catch {
925
946
  return fallback;
926
947
  }
948
+ })();
927
949
 
928
- // Use actual hostname for production
929
- return `https://${hostname}/`;
930
- } catch {
931
- return fallback;
932
- }
950
+ derivedBaseUrlMemo.set(ctx, derived);
951
+ return derived;
933
952
  }
934
953
 
935
954
  /**
@@ -1611,6 +1630,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1611
1630
  headers.delete(CACHE_STATUS_HEADER);
1612
1631
  headers.delete(CACHE_TAGS_HEADER);
1613
1632
  headers.delete(CACHE_TAGGED_AT_HEADER);
1633
+ // Finding #3 (read side): strip per-client signals a pre-fix or
1634
+ // pinned-version L1 entry may carry. See the read-side note in the design doc.
1635
+ stripPerClientSignals(headers);
1614
1636
  return new Response(response.body, {
1615
1637
  status: response.status,
1616
1638
  statusText: response.statusText,
@@ -1651,6 +1673,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1651
1673
  // replaced with a long max-age so the CF Cache API holds the entry across
1652
1674
  // the SWR window; getResponse restores the original before serving.
1653
1675
  const headers = new Headers(response.headers);
1676
+ // Finding #3: never persist a per-client signal in the shared L1 entry
1677
+ // (the platform's Set-Cookie rejection is unverified and ignores the
1678
+ // directive anyway). See stripPerClientSignals.
1679
+ stripPerClientSignals(headers);
1654
1680
  const originalCacheControl = response.headers.get("Cache-Control");
1655
1681
  if (originalCacheControl !== null) {
1656
1682
  headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
@@ -1658,10 +1684,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1658
1684
  headers.set("Cache-Control", `public, max-age=${totalTtl}`);
1659
1685
  headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
1660
1686
  // Internal tag headers (stripped by toClientResponse before serving).
1661
- const tagHeaders = this.tagHeaderEntries(tags, taggedAt);
1662
- for (const [name, value] of Object.entries(tagHeaders)) {
1663
- headers.set(name, value);
1664
- }
1687
+ this.setTagHeaders(headers, tags, taggedAt);
1665
1688
 
1666
1689
  const toCache = new Response(l1Body, {
1667
1690
  status: response.status,
@@ -1688,8 +1711,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1688
1711
  // L2: persist to KV (KV requires expirationTtl >= 60s)
1689
1712
  if (this.kv && this.waitUntil && totalTtl >= 60) {
1690
1713
  const kvKey = this.toKVKey(`doc:${key}`);
1714
+ // Finding #3: never persist a per-client signal in the KV envelope.
1691
1715
  const headersArray: [string, string][] = [];
1692
- response.headers.forEach((v, k) => headersArray.push([k, v]));
1716
+ response.headers.forEach((v, k) => {
1717
+ if (isPerClientSignalHeader(k)) return;
1718
+ headersArray.push([k, v]);
1719
+ });
1693
1720
  // Read body as ArrayBuffer and encode to base64 to preserve binary payloads
1694
1721
  const bodyBuf = kvBody
1695
1722
  ? await new Response(kvBody).arrayBuffer()
@@ -2149,6 +2176,24 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2149
2176
  };
2150
2177
  }
2151
2178
 
2179
+ /**
2180
+ * Merge the internal tag headers onto an existing Headers instance. The
2181
+ * from-scratch paths spread tagHeaderEntries() into an object-literal init;
2182
+ * the document put/promote paths build a Headers first, so they .set() each
2183
+ * entry instead.
2184
+ */
2185
+ private setTagHeaders(
2186
+ headers: Headers,
2187
+ tags: string[] | undefined,
2188
+ taggedAt: number | undefined,
2189
+ ): void {
2190
+ for (const [name, value] of Object.entries(
2191
+ this.tagHeaderEntries(tags, taggedAt),
2192
+ )) {
2193
+ headers.set(name, value);
2194
+ }
2195
+ }
2196
+
2152
2197
  /** Read an entry's tags/taggedAt back from its headers. */
2153
2198
  private readTagInfo(headers: Headers): {
2154
2199
  tags?: string[];
@@ -2848,6 +2893,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2848
2893
  // so evict it and miss rather than re-failing every read until TTL.
2849
2894
  let response: Response;
2850
2895
  try {
2896
+ // Finding #3 (read side): strip per-client signals a stale envelope may
2897
+ // carry. Inside the try so a malformed `hd` evicts (not throws through);
2898
+ // mutates `hd` in place so promoteResponseToL1 re-seeds from it too.
2899
+ envelope.hd = envelope.hd.filter(
2900
+ ([name]) => !isPerClientSignalHeader(name),
2901
+ );
2851
2902
  const bodyBuffer = base64ToBuffer(envelope.b);
2852
2903
  const headers = new Headers(envelope.hd);
2853
2904
  response = new Response(bodyBuffer, {
@@ -2903,10 +2954,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2903
2954
  // Re-attach the internal tag headers (envelope.hd is client-facing
2904
2955
  // and intentionally excludes them) so the promoted entry stays
2905
2956
  // invalidatable.
2906
- const tagHeaders = this.tagHeaderEntries(envelope.t, envelope.ta);
2907
- for (const [name, value] of Object.entries(tagHeaders)) {
2908
- headers.set(name, value);
2909
- }
2957
+ this.setTagHeaders(headers, envelope.t, envelope.ta);
2910
2958
 
2911
2959
  const bodyBuffer = base64ToBuffer(envelope.b);
2912
2960
  const response = new Response(bodyBuffer, {
@@ -1,15 +1,3 @@
1
- /**
2
- * Cloudflare Cache Store Exports
3
- *
4
- * Main export:
5
- * - CFCacheStore - Production cache store using Cloudflare's Cache API
6
- *
7
- * Header constants (for inspection/debugging):
8
- * - CACHE_STALE_AT_HEADER - Header containing staleness timestamp
9
- * - CACHE_STATUS_HEADER - Header containing HIT/REVALIDATING status
10
- */
11
-
12
- // Public API
13
1
  export {
14
2
  CFCacheStore,
15
3
  type CFCacheStoreOptions,
@@ -18,26 +6,14 @@ export {
18
6
  type KVNamespace,
19
7
  } from "./cf-cache-store.js";
20
8
 
21
- // Header constants for debugging and inspection. The tag headers
22
- // (x-edge-cache-tags / x-edge-cache-tagged-at) are intentionally NOT re-exported:
23
- // they are an internal encoding detail of the store's tag-invalidation check, not
24
- // a consumer-inspectable contract.
25
9
  export {
26
10
  CACHE_STALE_AT_HEADER,
27
11
  CACHE_STATUS_HEADER,
28
12
  CACHE_REVALIDATING_AT_HEADER,
29
13
  } from "./cf-cache-store.js";
30
14
 
31
- // Default latency-budget values, exported so the CFCacheStoreOptions JSDoc
32
- // {@link}s resolve and consumers can derive margins from the defaults.
33
15
  export {
34
16
  EDGE_LOOKUP_TIMEOUT_MS,
35
17
  EDGE_READ_TIMEOUT_MS,
36
18
  KV_READ_TIMEOUT_MS,
37
19
  } from "./cf-cache-store.js";
38
-
39
- // Internal exports (re-exported for backwards compatibility, marked @internal in source)
40
- export {
41
- type CacheStatus,
42
- MAX_REVALIDATION_INTERVAL,
43
- } from "./cf-cache-store.js";
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
15
+ import { hasPerClientSignal } from "../browser/cookie-name.js";
15
16
  import {
16
17
  getRequestContext,
17
18
  type RequestContext,
@@ -21,36 +22,8 @@ import { sortedSearchString } from "./cache-key-utils.js";
21
22
  import { runBackground } from "./background-task.js";
22
23
  import { reportCacheError } from "./cache-error.js";
23
24
 
24
- // ============================================================================
25
- // Constants
26
- // ============================================================================
27
-
28
- /** Header indicating cache status for debugging */
29
25
  const CACHE_STATUS_HEADER = "x-document-cache-status";
30
26
 
31
- /**
32
- * Snapshot the request-scoped tag union for a document cache write. The full-page
33
- * entry is tagged with every cache tag its content resolved (runtime cacheTag(),
34
- * "use cache" profile tags, and loader cache tags) so updateTag()/revalidateTag()
35
- * can invalidate it. Returns undefined when no tags were used, keeping untagged
36
- * document entries header-free.
37
- *
38
- * This is a plain synchronous snapshot. The CALLER must drain the rendered body
39
- * first (see the cache-write closures): runtime cacheTag()/"use cache" and loader
40
- * tags are recorded synchronously as each value resolves during render, including
41
- * Suspense-streamed ones that resolve AFTER the handler-settlement barrier - so
42
- * the correct barrier is the stream draining (render complete), not _handleStore.
43
- *
44
- * Caveat: this applies only to the segment cache WRITE path. When a segment is
45
- * cached for the first time, its cache({ tags }) DSL tags are recorded inside the
46
- * deferred cacheRoute waitUntil, which can still run after this snapshot; a
47
- * document that combines whole-page document caching with first-write segment-DSL
48
- * tags may miss those (the segment cache entry itself is still correctly tagged
49
- * and invalidated). On a segment-cache HIT the entry's tags are recorded
50
- * synchronously during lookupRoute, before this snapshot, so they are captured.
51
- * Runtime cacheTag()/"use cache" and loader tags are always captured once the
52
- * body drains.
53
- */
54
27
  function collectRequestTags(
55
28
  requestCtx: RequestContext | undefined,
56
29
  ): string[] | undefined {
@@ -58,11 +31,6 @@ function collectRequestTags(
58
31
  return tags && tags.size > 0 ? [...tags] : undefined;
59
32
  }
60
33
 
61
- /**
62
- * Simple hash function for segment IDs.
63
- * Creates a short, deterministic hash to differentiate cache keys
64
- * based on which segments the client already has.
65
- */
66
34
  function hashSegmentIds(segmentIds: string): string {
67
35
  if (!segmentIds) return "";
68
36
 
@@ -71,12 +39,9 @@ function hashSegmentIds(segmentIds: string): string {
71
39
  const char = segmentIds.charCodeAt(i);
72
40
  hash = ((hash << 5) - hash + char) | 0;
73
41
  }
74
- // Convert to base36 for shorter string, take absolute value
75
42
  return Math.abs(hash).toString(36);
76
43
  }
77
44
 
78
- // ============================================================================
79
- // Cache Control Parsing
80
45
  // ============================================================================
81
46
 
82
47
  interface CacheDirectives {
@@ -121,6 +86,16 @@ function shouldCacheResponse(response: Response): CacheDirectives | null {
121
86
  return null;
122
87
  }
123
88
 
89
+ // Never cache a per-client signal into a SHARED response store. A Set-Cookie
90
+ // (e.g. a rango state rotation from invalidateClientCache(), or any cookie a
91
+ // loader set) would be replayed to every client on a hit — pinning them to
92
+ // one value and even rolling a rotated client back to a prior one. The
93
+ // x-rango-keep-cache directive header is the mirror image: a replayed "keep"
94
+ // would suppress invalidation for every replayed client. Refuse both.
95
+ if (hasPerClientSignal(response.headers)) {
96
+ return null;
97
+ }
98
+
124
99
  const cacheControl = response.headers.get("Cache-Control");
125
100
  return parseCacheControl(cacheControl);
126
101
  }
@@ -11,23 +11,10 @@ import type { HandleStore } from "../server/handle-store.js";
11
11
  import type { SegmentHandleData } from "./types.js";
12
12
  import { serializeResult, deserializeResult } from "./segment-codec.js";
13
13
 
14
- /**
15
- * Bound on the background cache-write encode of handle data. A pushed handle
16
- * value can be a Promise (request-context push-a-promise) or a Promise<ReactNode>
17
- * (Breadcrumbs content), which the Flight encoder awaits while draining. The
18
- * encode runs in waitUntil/runBackground, so a never-resolving handle value
19
- * would otherwise pin a background slot indefinitely; on timeout the entry's
20
- * handles coalesce to empty rather than hanging or poisoning the whole write.
21
- */
22
14
  const HANDLE_ENCODE_TIMEOUT_MS = 5000;
23
15
 
24
16
  type HandleRecord = Record<string, SegmentHandleData>;
25
17
 
26
- // captureHandles builds a per-segment map keyed by every cached segment id, even
27
- // segments that pushed nothing (their entry is an empty object). "No handle data"
28
- // means no segment has any handle, in which case we skip the Flight encode and
29
- // store an empty string — so the common handle-free route pays neither an encode
30
- // on write nor a decode on every cache hit.
31
18
  function hasHandleData(handles: HandleRecord): boolean {
32
19
  for (const segId in handles) {
33
20
  for (const _ in handles[segId]) return true;
@@ -55,42 +42,15 @@ function withTimeout<T>(p: Promise<T>, ms: number, onTimeout: T): Promise<T> {
55
42
  ]);
56
43
  }
57
44
 
58
- /**
59
- * Encode a captured handle map to a string for cache storage.
60
- *
61
- * Handle values can be Promises or React elements (e.g. Breadcrumbs `content`).
62
- * JSON.stringify destroys those (Promise -> {}, ReactNode non-representable), so
63
- * persisting the raw map silently corrupts non-scalar handle values on stores
64
- * that serialize to JSON (the Cloudflare cache). Routing the map through the same
65
- * RSC-Flight codec the segments/value already use awaits Promises and serializes
66
- * React elements, so the stored field is a lossless, JSON-safe string. The
67
- * in-memory store keeps the same string by reference, so both backends replay
68
- * identical decoded values.
69
- */
70
45
  export async function encodeHandles(handles: HandleRecord): Promise<string> {
71
- // No handle was pushed anywhere — store an empty marker (decoded as "skip").
72
46
  if (!hasHandleData(handles)) return "";
73
47
  return encodeHandleValue(handles);
74
48
  }
75
49
 
76
- /**
77
- * Decode a stored handle string back to a handle map. Returns null on any
78
- * decode failure (e.g. a cross-version entry read under a pinned static
79
- * version), so the caller can skip handle restore without discarding the
80
- * otherwise-valid cached segments alongside it.
81
- */
82
50
  export function decodeHandles(encoded: string): Promise<HandleRecord | null> {
83
51
  return decodeHandleValue<HandleRecord>(encoded);
84
52
  }
85
53
 
86
- /**
87
- * Encode an arbitrary handle-data value to a Flight string. Used directly by the
88
- * prerender/static pipeline, whose static path holds a single segment's
89
- * `SegmentHandleData` (not a segId-keyed map). Bounded by the same timeout as
90
- * encodeHandles; failure/timeout coalesces to "". The caller owns the empty
91
- * check (an empty value still encodes to a non-empty Flight string, so skip the
92
- * call when there is nothing to store).
93
- */
94
54
  export async function encodeHandleValue(value: unknown): Promise<string> {
95
55
  const encoded = await withTimeout(
96
56
  serializeResult(value),
@@ -1,36 +1,15 @@
1
- /**
2
- * Cache Store
3
- *
4
- * Server-side caching for RSC segments and loader data.
5
- *
6
- * Main exports for users:
7
- * - SegmentCacheStore - Interface for implementing custom cache stores
8
- * - MemorySegmentCacheStore - In-memory cache for development/testing
9
- * - CFCacheStore - Cloudflare edge cache store for production
10
- * - CacheScope / createCacheScope - Request-scoped cache provider
11
- */
12
-
13
- // Segment cache store types and implementations
14
1
  export type {
15
2
  SegmentCacheStore,
16
- SegmentCacheProvider,
17
3
  CachedEntryData,
18
- CachedEntryResult,
19
4
  CacheGetResult,
20
- // The getItem()/setItem() signature types on SegmentCacheStore. Exported
21
- // alongside CacheGetResult so a consumer implementing a custom store can name
22
- // every type its interface methods use, not just the segment-read result.
23
5
  CacheItemResult,
24
6
  CacheItemOptions,
25
7
  SerializedSegmentData,
26
8
  SegmentHandleData,
27
- CacheConfig,
28
- CacheConfigOrFactory,
29
9
  } from "./types.js";
30
10
 
31
11
  export { MemorySegmentCacheStore } from "./memory-segment-store.js";
32
12
 
33
- // Cloudflare cache store
34
13
  export {
35
14
  CFCacheStore,
36
15
  type CFCacheStoreOptions,
@@ -45,17 +24,11 @@ export {
45
24
  KV_READ_TIMEOUT_MS,
46
25
  } from "./cf/index.js";
47
26
 
48
- // Cache scope
49
27
  export { CacheScope, createCacheScope } from "./cache-scope.js";
50
28
 
51
- // Document-level cache middleware
52
29
  export {
53
30
  createDocumentCacheMiddleware,
54
31
  type DocumentCacheOptions,
55
32
  } from "./document-cache.js";
56
33
 
57
- // Cache error reporting. CacheErrorCategory is the discriminator surfaced to a
58
- // router's onError callback as `metadata.category` for the `cache` phase, so
59
- // consumers can branch on the failure kind (e.g. distinguish a transient
60
- // cache-read outage from cache-corrupt self-heal).
61
34
  export type { CacheErrorCategory } from "./cache-error.js";
@@ -14,6 +14,7 @@ import type {
14
14
  CacheItemOptions,
15
15
  } from "./types.js";
16
16
  import type { RequestContext } from "../server/request-context.js";
17
+ import { isPerClientSignalHeader } from "../browser/cookie-name.js";
17
18
  import {
18
19
  resolveTtl,
19
20
  resolveSwrWindow,
@@ -58,8 +59,6 @@ interface CachedResponseEntry {
58
59
 
59
60
  interface CachedItemEntry {
60
61
  value: string;
61
- /** RSC-encoded handle data (see handle-snapshot.ts encodeHandles). Stored as
62
- * the encoded string by reference, identical to the JSON-serializing stores. */
63
62
  handles?: string;
64
63
  expiresAt: number;
65
64
  staleAt: number;
@@ -170,8 +169,6 @@ export class MemorySegmentCacheStore<
170
169
 
171
170
  constructor(options?: MemorySegmentCacheStoreOptions<TEnv>) {
172
171
  if (options?.name != null) {
173
- // Named stores use the globalThis registry so data survives HMR.
174
- // Each name gets its own isolated Map.
175
172
  this.cache = getNamedMap<CachedEntryData>(
176
173
  CACHE_REGISTRY_KEY,
177
174
  options.name,
@@ -193,7 +190,6 @@ export class MemorySegmentCacheStore<
193
190
  options.name,
194
191
  );
195
192
  } else {
196
- // Unnamed stores get a plain instance-level Map (no globalThis sharing).
197
193
  this.cache = new Map<string, CachedEntryData>();
198
194
  this.responseCache = new Map<string, CachedResponseEntry>();
199
195
  this.itemCache = new Map<string, CachedItemEntry>();
@@ -228,15 +224,11 @@ export class MemorySegmentCacheStore<
228
224
  ttl: number,
229
225
  _swr?: number,
230
226
  ): Promise<void> {
231
- // Note: Memory store doesn't implement SWR - entries just expire at TTL
232
- // For SWR support, use CFCacheStore or similar distributed cache
233
227
  const entry: CachedEntryData = {
234
228
  ...data,
235
229
  expiresAt: Date.now() + ttl * 1000,
236
230
  };
237
231
  const prefixedKey = `seg:${key}`;
238
- // Always drop stale tag mappings before writing so an overwrite with
239
- // different (or no) tags cannot leave the previous tags pointing here.
240
232
  this.unregisterTags(prefixedKey);
241
233
  this.cache.set(key, entry);
242
234
  if (data.tags && data.tags.length > 0) {
@@ -288,12 +280,10 @@ export class MemorySegmentCacheStore<
288
280
  tags?: string[],
289
281
  ): Promise<void> {
290
282
  try {
291
- // arrayBuffer() can reject (e.g. an already-consumed body). A write
292
- // failure must degrade to a no-op (entry simply not cached), never throw
293
- // up and fail the request.
294
283
  const body = await response.clone().arrayBuffer();
295
284
  const headers: [string, string][] = [];
296
285
  response.headers.forEach((value, name) => {
286
+ if (isPerClientSignalHeader(name)) return;
297
287
  headers.push([name, value]);
298
288
  });
299
289
 
@@ -363,19 +353,11 @@ export class MemorySegmentCacheStore<
363
353
  }
364
354
  }
365
355
 
366
- /**
367
- * Invalidate every cache entry (segment, response, item) tagged with any of
368
- * `tags`. Entries are dropped immediately; the next read is a miss and
369
- * re-renders fresh. This is the store-level primitive both updateTag() and
370
- * revalidateTag() delegate to. (In-process, so there is nothing to batch
371
- * beyond looping the tags.)
372
- */
373
356
  async invalidateTags(tags: string[]): Promise<void> {
374
357
  for (const tag of tags) {
375
358
  const keys = this.tagIndex.get(tag);
376
359
  if (!keys || keys.size === 0) continue;
377
360
 
378
- // Snapshot the keys before mutating the index inside the loop.
379
361
  const prefixedKeys = [...keys];
380
362
 
381
363
  for (const prefixedKey of prefixedKeys) {
@@ -391,18 +373,11 @@ export class MemorySegmentCacheStore<
391
373
  this.itemCache.delete(rawKey);
392
374
  }
393
375
 
394
- // Drop this key from every tag set it belonged to, not just `tag`.
395
376
  this.unregisterTags(prefixedKey);
396
377
  }
397
378
  }
398
379
  }
399
380
 
400
- /**
401
- * Register `tags` for a prefixed cache key in both the forward
402
- * (tag -> keys) and reverse (key -> tags) indexes.
403
- * Callers must call unregisterTags() first to clear stale mappings.
404
- * @internal
405
- */
406
381
  private registerTags(tags: string[], prefixedKey: string): void {
407
382
  let tagSet = this.keyTags.get(prefixedKey);
408
383
  if (!tagSet) {
@@ -420,11 +395,6 @@ export class MemorySegmentCacheStore<
420
395
  }
421
396
  }
422
397
 
423
- /**
424
- * Remove a prefixed cache key from every tag set it belongs to.
425
- * Uses the reverse index so this is O(tags-per-key), not O(total-tags).
426
- * @internal
427
- */
428
398
  private unregisterTags(prefixedKey: string): void {
429
399
  const tagSet = this.keyTags.get(prefixedKey);
430
400
  if (!tagSet) return;
@@ -440,10 +410,6 @@ export class MemorySegmentCacheStore<
440
410
  this.keyTags.delete(prefixedKey);
441
411
  }
442
412
 
443
- /**
444
- * Get cache statistics for debugging purposes.
445
- * @internal
446
- */
447
413
  getStats(): { size: number; keys: string[] } {
448
414
  return {
449
415
  size: this.cache.size,
@@ -451,18 +417,6 @@ export class MemorySegmentCacheStore<
451
417
  };
452
418
  }
453
419
 
454
- /**
455
- * Reset the global cache registry.
456
- * Useful for test isolation - call this in beforeEach to ensure
457
- * tests don't share cache state via globalThis.
458
- *
459
- * @example
460
- * ```typescript
461
- * beforeEach(() => {
462
- * MemorySegmentCacheStore.resetGlobalCache();
463
- * });
464
- * ```
465
- */
466
420
  static resetGlobalCache(): void {
467
421
  delete (globalThis as any)[CACHE_REGISTRY_KEY];
468
422
  delete (globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY];
@@ -64,9 +64,13 @@ export function setCacheProfiles(profiles: Record<string, CacheProfile>): void {
64
64
  }
65
65
 
66
66
  /**
67
- * Get a cache profile by name from the global registry.
68
- * Used only at DSL-time (cache("profileName") inside urls() evaluation).
69
- * Runtime code uses request-scoped profiles instead.
67
+ * Read a profile out of the global registry by name.
68
+ *
69
+ * There is currently no production reader: the "use cache: <profile>" runtime
70
+ * path resolves from the request-scoped _cacheProfiles map (see
71
+ * cache-runtime.ts), and the route DSL has no cache("profileName") form (see
72
+ * dsl-helpers.ts). This accessor exists so tests can assert what
73
+ * setCacheProfiles() wrote into the global registry.
70
74
  */
71
75
  export function getCacheProfile(name: string): CacheProfile | undefined {
72
76
  return _profiles[name];