@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126

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 (235) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/rango.js +3 -4
  3. package/dist/vite/index.js +315 -68
  4. package/package.json +19 -18
  5. package/skills/breadcrumbs/SKILL.md +60 -0
  6. package/skills/hooks/SKILL.md +2 -2
  7. package/skills/route/SKILL.md +6 -0
  8. package/skills/server-actions/SKILL.md +25 -1
  9. package/skills/testing/SKILL.md +17 -17
  10. package/skills/testing/cache-prerender.md +29 -3
  11. package/skills/testing/flight.md +13 -10
  12. package/skills/testing/render-handler.md +3 -0
  13. package/skills/testing/server-tree.md +1 -1
  14. package/skills/testing/setup.md +1 -1
  15. package/src/__internal.ts +0 -65
  16. package/src/browser/action-coordinator.ts +1 -1
  17. package/src/browser/action-fence.ts +10 -0
  18. package/src/browser/event-controller.ts +1 -83
  19. package/src/browser/navigation-store-handle.ts +3 -4
  20. package/src/browser/navigation-store.ts +0 -39
  21. package/src/browser/navigation-transaction.ts +0 -32
  22. package/src/browser/partial-update.ts +23 -84
  23. package/src/browser/prefetch/cache.ts +6 -45
  24. package/src/browser/prefetch/queue.ts +6 -3
  25. package/src/browser/rango-state.ts +2 -23
  26. package/src/browser/react/Link.tsx +0 -2
  27. package/src/browser/react/NavigationProvider.tsx +2 -1
  28. package/src/browser/react/ScrollRestoration.tsx +10 -6
  29. package/src/browser/react/filter-segment-order.ts +0 -2
  30. package/src/browser/react/index.ts +0 -45
  31. package/src/browser/react/location-state-shared.ts +0 -13
  32. package/src/browser/react/location-state.ts +0 -1
  33. package/src/browser/react/use-action.ts +6 -15
  34. package/src/browser/react/use-handle.ts +0 -5
  35. package/src/browser/react/use-link-status.ts +0 -4
  36. package/src/browser/react/use-navigation.ts +0 -3
  37. package/src/browser/react/use-params.ts +0 -2
  38. package/src/browser/react/use-router.ts +2 -1
  39. package/src/browser/react/use-search-params.ts +0 -5
  40. package/src/browser/react/use-segments.ts +0 -13
  41. package/src/browser/rsc-router.tsx +10 -3
  42. package/src/browser/server-action-bridge.ts +51 -3
  43. package/src/browser/types.ts +23 -5
  44. package/src/browser/validate-redirect-origin.ts +43 -16
  45. package/src/build/index.ts +8 -9
  46. package/src/build/route-trie.ts +46 -11
  47. package/src/build/route-types/param-extraction.ts +6 -3
  48. package/src/build/route-types/router-processing.ts +0 -8
  49. package/src/cache/cache-policy.ts +0 -54
  50. package/src/cache/cache-runtime.ts +48 -24
  51. package/src/cache/cache-scope.ts +0 -27
  52. package/src/cache/cache-tag.ts +0 -37
  53. package/src/cache/cf/cf-cache-store.ts +72 -45
  54. package/src/cache/cf/index.ts +0 -24
  55. package/src/cache/document-cache.ts +10 -36
  56. package/src/cache/handle-snapshot.ts +0 -40
  57. package/src/cache/index.ts +0 -27
  58. package/src/cache/memory-segment-store.ts +0 -52
  59. package/src/cache/profile-registry.ts +6 -30
  60. package/src/cache/read-through-swr.ts +41 -11
  61. package/src/cache/segment-codec.ts +0 -16
  62. package/src/cache/types.ts +0 -98
  63. package/src/client.rsc.tsx +4 -22
  64. package/src/client.tsx +19 -32
  65. package/src/context-var.ts +12 -0
  66. package/src/defer.ts +196 -0
  67. package/src/deps/ssr.ts +0 -1
  68. package/src/handle.ts +2 -12
  69. package/src/handles/MetaTags.tsx +0 -14
  70. package/src/handles/breadcrumbs.ts +16 -5
  71. package/src/handles/meta.ts +0 -39
  72. package/src/host/cookie-handler.ts +0 -36
  73. package/src/host/errors.ts +0 -24
  74. package/src/host/index.ts +6 -0
  75. package/src/host/pattern-matcher.ts +7 -50
  76. package/src/host/router.ts +1 -65
  77. package/src/host/testing.ts +0 -16
  78. package/src/host/types.ts +6 -2
  79. package/src/href-client.ts +0 -4
  80. package/src/index.rsc.ts +27 -2
  81. package/src/index.ts +7 -0
  82. package/src/internal-debug.ts +2 -4
  83. package/src/loader.rsc.ts +4 -15
  84. package/src/loader.ts +3 -9
  85. package/src/network-error-thrower.tsx +1 -6
  86. package/src/outlet-provider.tsx +1 -5
  87. package/src/prerender/param-hash.ts +10 -11
  88. package/src/prerender/store.ts +23 -30
  89. package/src/prerender.ts +34 -0
  90. package/src/redirect-origin.ts +100 -0
  91. package/src/root-error-boundary.tsx +1 -19
  92. package/src/route-content-wrapper.tsx +1 -44
  93. package/src/route-definition/dsl-helpers.ts +7 -19
  94. package/src/route-definition/helpers-types.ts +3 -3
  95. package/src/route-definition/redirect.ts +43 -9
  96. package/src/route-definition/resolve-handler-use.ts +6 -0
  97. package/src/route-map-builder.ts +0 -16
  98. package/src/router/content-negotiation.ts +0 -13
  99. package/src/router/error-handling.ts +12 -16
  100. package/src/router/find-match.ts +4 -31
  101. package/src/router/intercept-resolution.ts +10 -1
  102. package/src/router/lazy-includes.ts +1 -57
  103. package/src/router/loader-resolution.ts +25 -23
  104. package/src/router/logging.ts +0 -6
  105. package/src/router/manifest.ts +1 -25
  106. package/src/router/match-api.ts +0 -20
  107. package/src/router/match-context.ts +0 -22
  108. package/src/router/match-handlers.ts +0 -43
  109. package/src/router/match-middleware/background-revalidation.ts +0 -7
  110. package/src/router/match-middleware/cache-lookup.ts +96 -179
  111. package/src/router/match-middleware/cache-store.ts +0 -31
  112. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  113. package/src/router/match-middleware/segment-resolution.ts +0 -22
  114. package/src/router/match-pipelines.ts +1 -42
  115. package/src/router/match-result.ts +1 -52
  116. package/src/router/metrics.ts +0 -34
  117. package/src/router/middleware-types.ts +0 -116
  118. package/src/router/middleware.ts +77 -60
  119. package/src/router/navigation-snapshot.ts +0 -51
  120. package/src/router/params-util.ts +23 -0
  121. package/src/router/pattern-matching.ts +5 -56
  122. package/src/router/prerender-match.ts +56 -51
  123. package/src/router/request-classification.ts +1 -38
  124. package/src/router/revalidation.ts +14 -62
  125. package/src/router/route-snapshot.ts +0 -1
  126. package/src/router/router-context.ts +0 -27
  127. package/src/router/router-interfaces.ts +10 -0
  128. package/src/router/segment-resolution/fresh.ts +25 -57
  129. package/src/router/segment-resolution/helpers.ts +34 -0
  130. package/src/router/segment-resolution/loader-cache.ts +35 -23
  131. package/src/router/segment-resolution/revalidation.ts +188 -283
  132. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  133. package/src/router/segment-resolution.ts +4 -1
  134. package/src/router/segment-wrappers.ts +0 -3
  135. package/src/router/telemetry-otel.ts +0 -20
  136. package/src/router/telemetry.ts +0 -22
  137. package/src/router/timeout.ts +0 -20
  138. package/src/router/trie-matching.ts +66 -45
  139. package/src/router/types.ts +1 -63
  140. package/src/router/url-params.ts +0 -5
  141. package/src/router.ts +8 -11
  142. package/src/rsc/handler-context.ts +1 -0
  143. package/src/rsc/handler.ts +20 -4
  144. package/src/rsc/helpers.ts +71 -3
  145. package/src/rsc/json-route-result.ts +38 -0
  146. package/src/rsc/origin-guard.ts +9 -15
  147. package/src/rsc/progressive-enhancement.ts +10 -1
  148. package/src/rsc/redirect-guard.ts +99 -0
  149. package/src/rsc/response-route-handler.ts +23 -18
  150. package/src/rsc/rsc-rendering.ts +2 -7
  151. package/src/rsc/runtime-warnings.ts +14 -0
  152. package/src/rsc/server-action.ts +34 -29
  153. package/src/rsc/types.ts +6 -3
  154. package/src/search-params.ts +0 -16
  155. package/src/segment-loader-promise.ts +14 -2
  156. package/src/segment-system.tsx +79 -88
  157. package/src/server/handle-store.ts +7 -24
  158. package/src/server/loader-registry.ts +5 -24
  159. package/src/server/request-context.ts +29 -92
  160. package/src/ssr/index.tsx +14 -14
  161. package/src/static-handler.ts +2 -27
  162. package/src/testing/cache-status.ts +44 -48
  163. package/src/testing/collect-handle.ts +1 -24
  164. package/src/testing/dispatch.ts +43 -6
  165. package/src/testing/e2e/index.ts +1 -22
  166. package/src/testing/e2e/matchers.ts +0 -16
  167. package/src/testing/flight-matchers.ts +0 -13
  168. package/src/testing/flight-normalize.ts +3 -30
  169. package/src/testing/flight.ts +46 -48
  170. package/src/testing/generated-routes.ts +1 -41
  171. package/src/testing/index.ts +1 -21
  172. package/src/testing/internal/context.ts +3 -45
  173. package/src/testing/internal/seed-vars.ts +0 -26
  174. package/src/testing/render-handler.ts +31 -61
  175. package/src/testing/render-route.tsx +75 -103
  176. package/src/testing/run-loader.ts +0 -96
  177. package/src/testing/run-middleware.ts +0 -26
  178. package/src/theme/ThemeProvider.tsx +0 -52
  179. package/src/theme/ThemeScript.tsx +0 -6
  180. package/src/theme/constants.ts +0 -12
  181. package/src/theme/index.ts +0 -7
  182. package/src/theme/theme-context.ts +1 -5
  183. package/src/theme/theme-script.ts +0 -14
  184. package/src/theme/use-theme.ts +0 -3
  185. package/src/types/boundaries.ts +0 -35
  186. package/src/types/error-types.ts +25 -89
  187. package/src/types/global-namespace.ts +4 -14
  188. package/src/types/handler-context.ts +28 -9
  189. package/src/types/index.ts +0 -10
  190. package/src/types/request-scope.ts +0 -19
  191. package/src/types/route-config.ts +6 -50
  192. package/src/types/route-entry.ts +0 -6
  193. package/src/types/segments.ts +0 -13
  194. package/src/urls/include-helper.ts +0 -4
  195. package/src/urls/index.ts +0 -6
  196. package/src/urls/path-helper-types.ts +2 -2
  197. package/src/urls/path-helper.ts +0 -54
  198. package/src/urls/urls-function.ts +0 -13
  199. package/src/use-loader.tsx +0 -186
  200. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  201. package/src/vite/discovery/discover-routers.ts +28 -18
  202. package/src/vite/discovery/prerender-collection.ts +2 -4
  203. package/src/vite/discovery/state.ts +5 -0
  204. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  205. package/src/vite/plugin-types.ts +35 -9
  206. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  207. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  208. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  209. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  210. package/src/vite/plugins/expose-action-id.ts +2 -73
  211. package/src/vite/plugins/expose-id-utils.ts +0 -55
  212. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  213. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  214. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  215. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  216. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  217. package/src/vite/plugins/performance-tracks.ts +0 -3
  218. package/src/vite/plugins/refresh-cmd.ts +1 -1
  219. package/src/vite/plugins/use-cache-transform.ts +21 -46
  220. package/src/vite/plugins/version-injector.ts +0 -20
  221. package/src/vite/plugins/version-plugin.ts +1 -49
  222. package/src/vite/plugins/virtual-entries.ts +0 -15
  223. package/src/vite/rango.ts +2 -108
  224. package/src/vite/router-discovery.ts +9 -1
  225. package/src/vite/utils/ast-handler-extract.ts +0 -16
  226. package/src/vite/utils/bundle-analysis.ts +6 -13
  227. package/src/vite/utils/client-chunks.ts +0 -6
  228. package/src/vite/utils/forward-user-plugins.ts +0 -22
  229. package/src/vite/utils/manifest-utils.ts +0 -4
  230. package/src/vite/utils/package-resolution.ts +1 -73
  231. package/src/vite/utils/prerender-utils.ts +0 -35
  232. package/src/vite/utils/shared-utils.ts +3 -35
  233. package/src/browser/shallow.ts +0 -40
  234. package/src/handles/index.ts +0 -7
  235. package/src/router/middleware-cookies.ts +0 -55
@@ -18,35 +18,11 @@ import {
18
18
 
19
19
  const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
20
20
 
21
- /**
22
- * Normalize a tag for storage.
23
- *
24
- * Returns the tag unchanged if usable, or null if it is empty/whitespace-only
25
- * (dropped consistently in every environment - an empty tag matches nothing).
26
- *
27
- * Backend-specific constraints are intentionally NOT enforced here so the tag
28
- * primitive stays backend-agnostic. In particular, the CFCacheStore
29
- * encodeURIComponent's tags at serialization time so commas/spaces/non-Latin1
30
- * characters cannot corrupt the comma-delimited Cloudflare Cache-Tag header or
31
- * the HTTP marker header (it does not reject them). Keep tags short and
32
- * low-cardinality: a tag's KV marker key must stay under Cloudflare's 512-byte
33
- * limit, and a Cache-Tag value under 1024 bytes. The in-memory store has no
34
- * such limitations.
35
- *
36
- * @internal
37
- */
38
21
  export function normalizeTag(tag: string): string | null {
39
22
  if (!tag || !tag.trim()) return null;
40
23
  return tag;
41
24
  }
42
25
 
43
- /**
44
- * Normalize a tag collection: drop empty/whitespace-only tags so the WRITE path
45
- * matches the invalidate path (updateTag/revalidateTag/cacheTag all normalize).
46
- * Does not deduplicate - callers that need that wrap with a Set.
47
- *
48
- * @internal
49
- */
50
26
  export function normalizeTags(tags: Iterable<string>): string[] {
51
27
  const out: string[] = [];
52
28
  for (const tag of tags) {
@@ -89,19 +65,6 @@ export function cacheTag(...tags: string[]): void {
89
65
  }
90
66
  }
91
67
 
92
- /**
93
- * Record `tags` into the request-scoped tag set (ctx._requestTags), the union of
94
- * every cache tag resolved while producing the response. The document cache reads
95
- * this after the render settles so a full-page entry is tagged with everything its
96
- * content used, making it invalidatable by updateTag()/revalidateTag().
97
- *
98
- * Called at the tag-resolution sites: "use cache" stores (cache-runtime, both the
99
- * miss and read/hit paths), loader cache (cache-policy/loader-cache), and segment
100
- * cache() (cache-scope). Writes the field directly (not via ctx.set()) so it does
101
- * not trip the cache-scope side-effect guard, mirroring cacheTag() itself.
102
- *
103
- * @internal
104
- */
105
68
  export function recordRequestTags(
106
69
  tags: Iterable<string> | undefined,
107
70
  ctx: RequestContext | undefined = _getRequestContext(),
@@ -218,6 +218,19 @@ function getTagMarkerInflight(
218
218
  return inflight;
219
219
  }
220
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
+
221
234
  /** KV key byte-length ceiling. Cloudflare KV rejects keys larger than this. */
222
235
  const KV_MAX_KEY_BYTES = 512;
223
236
 
@@ -323,10 +336,9 @@ function remainingCacheControl(headers: Headers, now: number): string {
323
336
  // Types
324
337
  // ============================================================================
325
338
 
326
- // Re-exported from the canonical home so cf-cache-store consumers keep
327
- // importing `ExecutionContext` from this module without a second interface
328
- // drifting over time.
329
- 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.
330
342
  import type { ExecutionContext } from "../../types/request-scope.js";
331
343
 
332
344
  /**
@@ -717,12 +729,6 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
717
729
  ) => string | Promise<string>;
718
730
  }
719
731
 
720
- /**
721
- * Cache status values for the x-edge-cache-status header.
722
- * @internal
723
- */
724
- export type CacheStatus = "HIT" | "REVALIDATING";
725
-
726
732
  // ============================================================================
727
733
  // CFCacheStore Implementation
728
734
  // ============================================================================
@@ -810,13 +816,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
810
816
  // tagCacheTtl gates the L1 marker cache via `> 0`. A non-finite value (NaN
811
817
  // from `Number(env.UNSET)`) is not null/undefined, so `?? 0` would let it
812
818
  // through and silently disable the cache while reading as "configured".
813
- // Coerce any non-finite/non-positive value to the documented 0 = disabled.
814
- this.tagCacheTtl =
815
- typeof options.tagCacheTtl === "number" &&
816
- Number.isFinite(options.tagCacheTtl) &&
817
- options.tagCacheTtl > 0
818
- ? options.tagCacheTtl
819
- : 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;
820
823
 
821
824
  // Read-side tag invalidation requires KV: isGloballyInvalidated() compares an
822
825
  // entry's taggedAt against the per-tag KV marker and short-circuits to "not
@@ -909,31 +912,43 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
909
912
  return fallback;
910
913
  }
911
914
 
912
- try {
913
- const url = new URL(ctx.request.url);
914
- 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
+ }
915
921
 
916
- // Use fallback for dev/preview environments
917
- if (
918
- hostname === "localhost" ||
919
- hostname === "127.0.0.1" ||
920
- hostname.endsWith(".workers.dev") ||
921
- hostname.endsWith(".pages.dev")
922
- ) {
923
- return fallback;
924
- }
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
+ }
925
936
 
926
- // Validate hostname: must be a valid domain (alphanumeric, hyphens, dots)
927
- // to prevent host header injection into cache keys
928
- 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 {
929
946
  return fallback;
930
947
  }
948
+ })();
931
949
 
932
- // Use actual hostname for production
933
- return `https://${hostname}/`;
934
- } catch {
935
- return fallback;
936
- }
950
+ derivedBaseUrlMemo.set(ctx, derived);
951
+ return derived;
937
952
  }
938
953
 
939
954
  /**
@@ -1669,10 +1684,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1669
1684
  headers.set("Cache-Control", `public, max-age=${totalTtl}`);
1670
1685
  headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
1671
1686
  // Internal tag headers (stripped by toClientResponse before serving).
1672
- const tagHeaders = this.tagHeaderEntries(tags, taggedAt);
1673
- for (const [name, value] of Object.entries(tagHeaders)) {
1674
- headers.set(name, value);
1675
- }
1687
+ this.setTagHeaders(headers, tags, taggedAt);
1676
1688
 
1677
1689
  const toCache = new Response(l1Body, {
1678
1690
  status: response.status,
@@ -2164,6 +2176,24 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2164
2176
  };
2165
2177
  }
2166
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
+
2167
2197
  /** Read an entry's tags/taggedAt back from its headers. */
2168
2198
  private readTagInfo(headers: Headers): {
2169
2199
  tags?: string[];
@@ -2924,10 +2954,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2924
2954
  // Re-attach the internal tag headers (envelope.hd is client-facing
2925
2955
  // and intentionally excludes them) so the promoted entry stays
2926
2956
  // invalidatable.
2927
- const tagHeaders = this.tagHeaderEntries(envelope.t, envelope.ta);
2928
- for (const [name, value] of Object.entries(tagHeaders)) {
2929
- headers.set(name, value);
2930
- }
2957
+ this.setTagHeaders(headers, envelope.t, envelope.ta);
2931
2958
 
2932
2959
  const bodyBuffer = base64ToBuffer(envelope.b);
2933
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";
@@ -22,36 +22,8 @@ import { sortedSearchString } from "./cache-key-utils.js";
22
22
  import { runBackground } from "./background-task.js";
23
23
  import { reportCacheError } from "./cache-error.js";
24
24
 
25
- // ============================================================================
26
- // Constants
27
- // ============================================================================
28
-
29
- /** Header indicating cache status for debugging */
30
25
  const CACHE_STATUS_HEADER = "x-document-cache-status";
31
26
 
32
- /**
33
- * Snapshot the request-scoped tag union for a document cache write. The full-page
34
- * entry is tagged with every cache tag its content resolved (runtime cacheTag(),
35
- * "use cache" profile tags, and loader cache tags) so updateTag()/revalidateTag()
36
- * can invalidate it. Returns undefined when no tags were used, keeping untagged
37
- * document entries header-free.
38
- *
39
- * This is a plain synchronous snapshot. The CALLER must drain the rendered body
40
- * first (see the cache-write closures): runtime cacheTag()/"use cache" and loader
41
- * tags are recorded synchronously as each value resolves during render, including
42
- * Suspense-streamed ones that resolve AFTER the handler-settlement barrier - so
43
- * the correct barrier is the stream draining (render complete), not _handleStore.
44
- *
45
- * Caveat: this applies only to the segment cache WRITE path. When a segment is
46
- * cached for the first time, its cache({ tags }) DSL tags are recorded inside the
47
- * deferred cacheRoute waitUntil, which can still run after this snapshot; a
48
- * document that combines whole-page document caching with first-write segment-DSL
49
- * tags may miss those (the segment cache entry itself is still correctly tagged
50
- * and invalidated). On a segment-cache HIT the entry's tags are recorded
51
- * synchronously during lookupRoute, before this snapshot, so they are captured.
52
- * Runtime cacheTag()/"use cache" and loader tags are always captured once the
53
- * body drains.
54
- */
55
27
  function collectRequestTags(
56
28
  requestCtx: RequestContext | undefined,
57
29
  ): string[] | undefined {
@@ -59,11 +31,6 @@ function collectRequestTags(
59
31
  return tags && tags.size > 0 ? [...tags] : undefined;
60
32
  }
61
33
 
62
- /**
63
- * Simple hash function for segment IDs.
64
- * Creates a short, deterministic hash to differentiate cache keys
65
- * based on which segments the client already has.
66
- */
67
34
  function hashSegmentIds(segmentIds: string): string {
68
35
  if (!segmentIds) return "";
69
36
 
@@ -72,12 +39,9 @@ function hashSegmentIds(segmentIds: string): string {
72
39
  const char = segmentIds.charCodeAt(i);
73
40
  hash = ((hash << 5) - hash + char) | 0;
74
41
  }
75
- // Convert to base36 for shorter string, take absolute value
76
42
  return Math.abs(hash).toString(36);
77
43
  }
78
44
 
79
- // ============================================================================
80
- // Cache Control Parsing
81
45
  // ============================================================================
82
46
 
83
47
  interface CacheDirectives {
@@ -91,6 +55,16 @@ interface CacheDirectives {
91
55
  function parseCacheControl(header: string | null): CacheDirectives | null {
92
56
  if (!header) return null;
93
57
 
58
+ // RFC 7234: in a SHARED cache, `private` and `no-store` forbid storage and
59
+ // MUST win over `s-maxage` even though `private, s-maxage` is contradictory.
60
+ // The document cache is a shared edge store, so refuse both regardless of any
61
+ // s-maxage / stale-while-revalidate also present. Match standalone directive
62
+ // tokens (start/end, whitespace, comma, semicolon, or `=` bounded), not a
63
+ // substring, so a value containing "private" cannot false-veto.
64
+ if (/(^|[\s,;])(private|no-store)(?=$|[\s,;=])/i.test(header)) {
65
+ return null;
66
+ }
67
+
94
68
  const directives: CacheDirectives = {};
95
69
 
96
70
  // Parse s-maxage
@@ -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";
@@ -59,8 +59,6 @@ interface CachedResponseEntry {
59
59
 
60
60
  interface CachedItemEntry {
61
61
  value: string;
62
- /** RSC-encoded handle data (see handle-snapshot.ts encodeHandles). Stored as
63
- * the encoded string by reference, identical to the JSON-serializing stores. */
64
62
  handles?: string;
65
63
  expiresAt: number;
66
64
  staleAt: number;
@@ -171,8 +169,6 @@ export class MemorySegmentCacheStore<
171
169
 
172
170
  constructor(options?: MemorySegmentCacheStoreOptions<TEnv>) {
173
171
  if (options?.name != null) {
174
- // Named stores use the globalThis registry so data survives HMR.
175
- // Each name gets its own isolated Map.
176
172
  this.cache = getNamedMap<CachedEntryData>(
177
173
  CACHE_REGISTRY_KEY,
178
174
  options.name,
@@ -194,7 +190,6 @@ export class MemorySegmentCacheStore<
194
190
  options.name,
195
191
  );
196
192
  } else {
197
- // Unnamed stores get a plain instance-level Map (no globalThis sharing).
198
193
  this.cache = new Map<string, CachedEntryData>();
199
194
  this.responseCache = new Map<string, CachedResponseEntry>();
200
195
  this.itemCache = new Map<string, CachedItemEntry>();
@@ -229,15 +224,11 @@ export class MemorySegmentCacheStore<
229
224
  ttl: number,
230
225
  _swr?: number,
231
226
  ): Promise<void> {
232
- // Note: Memory store doesn't implement SWR - entries just expire at TTL
233
- // For SWR support, use CFCacheStore or similar distributed cache
234
227
  const entry: CachedEntryData = {
235
228
  ...data,
236
229
  expiresAt: Date.now() + ttl * 1000,
237
230
  };
238
231
  const prefixedKey = `seg:${key}`;
239
- // Always drop stale tag mappings before writing so an overwrite with
240
- // different (or no) tags cannot leave the previous tags pointing here.
241
232
  this.unregisterTags(prefixedKey);
242
233
  this.cache.set(key, entry);
243
234
  if (data.tags && data.tags.length > 0) {
@@ -289,14 +280,7 @@ export class MemorySegmentCacheStore<
289
280
  tags?: string[],
290
281
  ): Promise<void> {
291
282
  try {
292
- // arrayBuffer() can reject (e.g. an already-consumed body). A write
293
- // failure must degrade to a no-op (entry simply not cached), never throw
294
- // up and fail the request.
295
283
  const body = await response.clone().arrayBuffer();
296
- // Defense-in-depth (Finding #3): never persist a per-client signal into a
297
- // shared store. The document-cache chokepoint already refuses these, but
298
- // putResponse is public and reachable directly (e.g. tag-revalidation
299
- // re-puts), so strip them here too.
300
284
  const headers: [string, string][] = [];
301
285
  response.headers.forEach((value, name) => {
302
286
  if (isPerClientSignalHeader(name)) return;
@@ -369,19 +353,11 @@ export class MemorySegmentCacheStore<
369
353
  }
370
354
  }
371
355
 
372
- /**
373
- * Invalidate every cache entry (segment, response, item) tagged with any of
374
- * `tags`. Entries are dropped immediately; the next read is a miss and
375
- * re-renders fresh. This is the store-level primitive both updateTag() and
376
- * revalidateTag() delegate to. (In-process, so there is nothing to batch
377
- * beyond looping the tags.)
378
- */
379
356
  async invalidateTags(tags: string[]): Promise<void> {
380
357
  for (const tag of tags) {
381
358
  const keys = this.tagIndex.get(tag);
382
359
  if (!keys || keys.size === 0) continue;
383
360
 
384
- // Snapshot the keys before mutating the index inside the loop.
385
361
  const prefixedKeys = [...keys];
386
362
 
387
363
  for (const prefixedKey of prefixedKeys) {
@@ -397,18 +373,11 @@ export class MemorySegmentCacheStore<
397
373
  this.itemCache.delete(rawKey);
398
374
  }
399
375
 
400
- // Drop this key from every tag set it belonged to, not just `tag`.
401
376
  this.unregisterTags(prefixedKey);
402
377
  }
403
378
  }
404
379
  }
405
380
 
406
- /**
407
- * Register `tags` for a prefixed cache key in both the forward
408
- * (tag -> keys) and reverse (key -> tags) indexes.
409
- * Callers must call unregisterTags() first to clear stale mappings.
410
- * @internal
411
- */
412
381
  private registerTags(tags: string[], prefixedKey: string): void {
413
382
  let tagSet = this.keyTags.get(prefixedKey);
414
383
  if (!tagSet) {
@@ -426,11 +395,6 @@ export class MemorySegmentCacheStore<
426
395
  }
427
396
  }
428
397
 
429
- /**
430
- * Remove a prefixed cache key from every tag set it belongs to.
431
- * Uses the reverse index so this is O(tags-per-key), not O(total-tags).
432
- * @internal
433
- */
434
398
  private unregisterTags(prefixedKey: string): void {
435
399
  const tagSet = this.keyTags.get(prefixedKey);
436
400
  if (!tagSet) return;
@@ -446,10 +410,6 @@ export class MemorySegmentCacheStore<
446
410
  this.keyTags.delete(prefixedKey);
447
411
  }
448
412
 
449
- /**
450
- * Get cache statistics for debugging purposes.
451
- * @internal
452
- */
453
413
  getStats(): { size: number; keys: string[] } {
454
414
  return {
455
415
  size: this.cache.size,
@@ -457,18 +417,6 @@ export class MemorySegmentCacheStore<
457
417
  };
458
418
  }
459
419
 
460
- /**
461
- * Reset the global cache registry.
462
- * Useful for test isolation - call this in beforeEach to ensure
463
- * tests don't share cache state via globalThis.
464
- *
465
- * @example
466
- * ```typescript
467
- * beforeEach(() => {
468
- * MemorySegmentCacheStore.resetGlobalCache();
469
- * });
470
- * ```
471
- */
472
420
  static resetGlobalCache(): void {
473
421
  delete (globalThis as any)[CACHE_REGISTRY_KEY];
474
422
  delete (globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY];
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Cache Profile Registry
2
+ * Cache profile resolution.
3
3
  *
4
- * Named cache profiles for "use cache" directive.
5
- * Profiles define TTL, SWR, and optional default tags.
6
- * Set by createRouter() at startup, read by registerCachedFunction() at runtime.
4
+ * Named cache profiles for the "use cache" directive define TTL, SWR, and
5
+ * optional default tags. createRouter() resolves the user's profiles once via
6
+ * resolveCacheProfiles() and threads the resulting map onto each request
7
+ * context; the "use cache: <profile>" runtime path reads it from there
8
+ * (request-scoped) — there is no global registry.
7
9
  */
8
10
 
9
11
  export interface CacheProfile {
@@ -17,10 +19,6 @@ export interface CacheProfile {
17
19
 
18
20
  const DEFAULT_PROFILE: CacheProfile = { ttl: 900, swr: 1800 };
19
21
 
20
- let _profiles: Record<string, CacheProfile> = {
21
- default: DEFAULT_PROFILE,
22
- };
23
-
24
22
  const PROFILE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
25
23
 
26
24
  /**
@@ -49,25 +47,3 @@ export function resolveCacheProfiles(
49
47
  }
50
48
  return merged;
51
49
  }
52
-
53
- /**
54
- * Set all cache profiles in the global registry.
55
- * Called by createRouter() at startup for DSL-time resolution
56
- * (cache("profileName") reads from this during route definition).
57
- *
58
- * WARNING: This is global mutable state. It exists only for DSL-time
59
- * reads. Runtime resolution (registerCachedFunction) uses request-scoped
60
- * profiles and does NOT read from this registry.
61
- */
62
- export function setCacheProfiles(profiles: Record<string, CacheProfile>): void {
63
- _profiles = resolveCacheProfiles(profiles);
64
- }
65
-
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.
70
- */
71
- export function getCacheProfile(name: string): CacheProfile | undefined {
72
- return _profiles[name];
73
- }