@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7

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 (402) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +440 -133
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3373 -1176
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +68 -14
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +64 -2
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +224 -32
  13. package/skills/caching/SKILL.md +279 -17
  14. package/skills/composability/SKILL.md +27 -3
  15. package/skills/css/SKILL.md +76 -0
  16. package/skills/debug-manifest/SKILL.md +4 -2
  17. package/skills/document-cache/SKILL.md +78 -55
  18. package/skills/handler-use/SKILL.md +364 -0
  19. package/skills/hooks/SKILL.md +250 -30
  20. package/skills/host-router/SKILL.md +83 -23
  21. package/skills/i18n/SKILL.md +276 -0
  22. package/skills/intercept/SKILL.md +87 -18
  23. package/skills/layout/SKILL.md +35 -9
  24. package/skills/links/SKILL.md +249 -17
  25. package/skills/loader/SKILL.md +235 -9
  26. package/skills/middleware/SKILL.md +52 -13
  27. package/skills/migrate-nextjs/SKILL.md +584 -0
  28. package/skills/migrate-react-router/SKILL.md +771 -0
  29. package/skills/mime-routes/SKILL.md +28 -1
  30. package/skills/observability/SKILL.md +172 -0
  31. package/skills/parallel/SKILL.md +77 -7
  32. package/skills/prerender/SKILL.md +172 -125
  33. package/skills/rango/SKILL.md +251 -22
  34. package/skills/react-compiler/SKILL.md +168 -0
  35. package/skills/response-routes/SKILL.md +123 -48
  36. package/skills/route/SKILL.md +70 -5
  37. package/skills/router-setup/SKILL.md +65 -8
  38. package/skills/scripts/SKILL.md +179 -0
  39. package/skills/server-actions/SKILL.md +775 -0
  40. package/skills/streams-and-websockets/SKILL.md +283 -0
  41. package/skills/tailwind/SKILL.md +27 -3
  42. package/skills/testing/SKILL.md +130 -0
  43. package/skills/testing/bindings.md +103 -0
  44. package/skills/testing/cache-prerender.md +127 -0
  45. package/skills/testing/client-components.md +124 -0
  46. package/skills/testing/e2e-parity.md +125 -0
  47. package/skills/testing/flight.md +91 -0
  48. package/skills/testing/handles.md +129 -0
  49. package/skills/testing/loader.md +128 -0
  50. package/skills/testing/middleware.md +99 -0
  51. package/skills/testing/render-handler.md +122 -0
  52. package/skills/testing/response-routes.md +95 -0
  53. package/skills/testing/reverse-and-types.md +84 -0
  54. package/skills/testing/server-actions.md +107 -0
  55. package/skills/testing/server-tree.md +128 -0
  56. package/skills/testing/setup.md +123 -0
  57. package/skills/typesafety/SKILL.md +322 -29
  58. package/skills/use-cache/SKILL.md +57 -14
  59. package/skills/view-transitions/SKILL.md +337 -0
  60. package/src/__augment-tests__/augment.ts +81 -0
  61. package/src/__augment-tests__/augmented.check.ts +116 -0
  62. package/src/__internal.ts +1 -66
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +192 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/navigation-bridge.ts +131 -30
  74. package/src/browser/navigation-client.ts +186 -100
  75. package/src/browser/navigation-store-handle.ts +38 -0
  76. package/src/browser/navigation-store.ts +157 -74
  77. package/src/browser/navigation-transaction.ts +9 -59
  78. package/src/browser/network-error-handler.ts +34 -7
  79. package/src/browser/partial-update.ts +165 -112
  80. package/src/browser/prefetch/cache.ts +205 -62
  81. package/src/browser/prefetch/fetch.ts +347 -39
  82. package/src/browser/prefetch/queue.ts +42 -8
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +102 -15
  85. package/src/browser/react/NavigationProvider.tsx +295 -119
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  89. package/src/browser/react/filter-segment-order.ts +66 -7
  90. package/src/browser/react/index.ts +0 -48
  91. package/src/browser/react/location-state-shared.ts +178 -8
  92. package/src/browser/react/location-state.ts +39 -14
  93. package/src/browser/react/use-action.ts +6 -15
  94. package/src/browser/react/use-handle.ts +23 -69
  95. package/src/browser/react/use-href.tsx +8 -1
  96. package/src/browser/react/use-link-status.ts +33 -8
  97. package/src/browser/react/use-navigation.ts +32 -7
  98. package/src/browser/react/use-params.ts +20 -10
  99. package/src/browser/react/use-reverse.ts +106 -0
  100. package/src/browser/react/use-router.ts +46 -11
  101. package/src/browser/react/use-search-params.ts +0 -5
  102. package/src/browser/react/use-segments.ts +11 -21
  103. package/src/browser/response-adapter.ts +99 -8
  104. package/src/browser/rsc-router.tsx +114 -24
  105. package/src/browser/scroll-restoration.ts +37 -22
  106. package/src/browser/segment-reconciler.ts +36 -14
  107. package/src/browser/segment-structure-assert.ts +2 -2
  108. package/src/browser/server-action-bridge.ts +222 -72
  109. package/src/browser/types.ts +102 -12
  110. package/src/browser/validate-redirect-origin.ts +43 -16
  111. package/src/build/collect-fallback-refs.ts +107 -0
  112. package/src/build/generate-manifest.ts +65 -40
  113. package/src/build/generate-route-types.ts +5 -1
  114. package/src/build/index.ts +8 -2
  115. package/src/build/prefix-tree-utils.ts +123 -0
  116. package/src/build/route-trie.ts +165 -36
  117. package/src/build/route-types/ast-route-extraction.ts +15 -8
  118. package/src/build/route-types/codegen.ts +16 -5
  119. package/src/build/route-types/include-resolution.ts +125 -24
  120. package/src/build/route-types/param-extraction.ts +6 -3
  121. package/src/build/route-types/per-module-writer.ts +22 -6
  122. package/src/build/route-types/router-processing.ts +260 -94
  123. package/src/build/route-types/scan-filter.ts +9 -2
  124. package/src/build/route-types/source-scan.ts +216 -0
  125. package/src/build/runtime-discovery.ts +9 -20
  126. package/src/cache/cache-error.ts +104 -0
  127. package/src/cache/cache-key-utils.ts +29 -13
  128. package/src/cache/cache-policy.ts +108 -34
  129. package/src/cache/cache-runtime.ts +224 -41
  130. package/src/cache/cache-scope.ts +188 -82
  131. package/src/cache/cache-tag.ts +103 -0
  132. package/src/cache/cf/cf-base64.ts +33 -0
  133. package/src/cache/cf/cf-cache-constants.ts +127 -0
  134. package/src/cache/cf/cf-cache-store.ts +1989 -378
  135. package/src/cache/cf/cf-cache-types.ts +349 -0
  136. package/src/cache/cf/cf-kv-utils.ts +46 -0
  137. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  138. package/src/cache/cf/index.ts +6 -16
  139. package/src/cache/document-cache.ts +89 -21
  140. package/src/cache/handle-snapshot.ts +70 -0
  141. package/src/cache/index.ts +10 -20
  142. package/src/cache/memory-segment-store.ts +136 -37
  143. package/src/cache/profile-registry.ts +46 -31
  144. package/src/cache/read-through-swr.ts +56 -12
  145. package/src/cache/segment-codec.ts +9 -17
  146. package/src/cache/tag-invalidation.ts +230 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +18 -6
  155. package/src/decode-loader-results.ts +52 -0
  156. package/src/defer.ts +196 -0
  157. package/src/deps/ssr.ts +0 -1
  158. package/src/encode-kv.ts +49 -0
  159. package/src/errors.ts +30 -4
  160. package/src/escape-script.ts +52 -0
  161. package/src/handle.ts +70 -22
  162. package/src/handles/MetaTags.tsx +62 -19
  163. package/src/handles/Scripts.tsx +183 -0
  164. package/src/handles/breadcrumbs.ts +37 -8
  165. package/src/handles/is-thenable.ts +19 -0
  166. package/src/handles/meta.ts +51 -40
  167. package/src/handles/script.ts +244 -0
  168. package/src/host/cookie-handler.ts +9 -60
  169. package/src/host/errors.ts +0 -24
  170. package/src/host/index.ts +8 -2
  171. package/src/host/pattern-matcher.ts +23 -52
  172. package/src/host/router.ts +107 -99
  173. package/src/host/testing.ts +40 -27
  174. package/src/host/types.ts +37 -4
  175. package/src/host/utils.ts +1 -1
  176. package/src/href-client.ts +137 -22
  177. package/src/index.rsc.ts +99 -13
  178. package/src/index.ts +139 -19
  179. package/src/internal-debug.ts +11 -10
  180. package/src/loader-store.ts +500 -0
  181. package/src/loader.rsc.ts +20 -13
  182. package/src/loader.ts +12 -11
  183. package/src/missing-id-error.ts +68 -0
  184. package/src/outlet-context.ts +1 -1
  185. package/src/outlet-provider.tsx +1 -5
  186. package/src/prerender/param-hash.ts +16 -16
  187. package/src/prerender/store.ts +37 -41
  188. package/src/prerender.ts +198 -82
  189. package/src/redirect-origin.ts +100 -0
  190. package/src/regex-escape.ts +8 -0
  191. package/src/render-error-thrower.tsx +20 -0
  192. package/src/response-utils.ts +62 -0
  193. package/src/reverse.ts +65 -15
  194. package/src/root-error-boundary.tsx +1 -19
  195. package/src/route-content-wrapper.tsx +19 -77
  196. package/src/route-definition/dsl-helpers.ts +461 -304
  197. package/src/route-definition/helper-factories.ts +28 -140
  198. package/src/route-definition/helpers-types.ts +143 -69
  199. package/src/route-definition/index.ts +4 -2
  200. package/src/route-definition/redirect.ts +51 -10
  201. package/src/route-definition/resolve-handler-use.ts +160 -0
  202. package/src/route-definition/use-item-types.ts +29 -0
  203. package/src/route-map-builder.ts +0 -16
  204. package/src/route-types.ts +37 -46
  205. package/src/router/basename.ts +14 -0
  206. package/src/router/content-negotiation.ts +164 -17
  207. package/src/router/error-handling.ts +45 -18
  208. package/src/router/find-match.ts +44 -23
  209. package/src/router/handler-context.ts +52 -31
  210. package/src/router/instrument.ts +350 -0
  211. package/src/router/intercept-resolution.ts +48 -24
  212. package/src/router/lazy-includes.ts +15 -52
  213. package/src/router/loader-resolution.ts +268 -56
  214. package/src/router/logging.ts +0 -6
  215. package/src/router/manifest.ts +40 -42
  216. package/src/router/match-api.ts +124 -204
  217. package/src/router/match-context.ts +0 -22
  218. package/src/router/match-handlers.ts +58 -58
  219. package/src/router/match-middleware/background-revalidation.ts +40 -24
  220. package/src/router/match-middleware/cache-lookup.ts +170 -276
  221. package/src/router/match-middleware/cache-store.ts +64 -52
  222. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  223. package/src/router/match-middleware/segment-resolution.ts +45 -14
  224. package/src/router/match-pipelines.ts +1 -42
  225. package/src/router/match-result.ts +87 -39
  226. package/src/router/metrics.ts +0 -34
  227. package/src/router/middleware-types.ts +7 -140
  228. package/src/router/middleware.ts +266 -169
  229. package/src/router/navigation-snapshot.ts +131 -0
  230. package/src/router/params-util.ts +23 -0
  231. package/src/router/pattern-matching.ts +132 -90
  232. package/src/router/prefetch-cache-ttl.ts +51 -0
  233. package/src/router/prerender-match.ts +195 -56
  234. package/src/router/preview-match.ts +32 -102
  235. package/src/router/request-classification.ts +276 -0
  236. package/src/router/revalidation.ts +123 -73
  237. package/src/router/route-snapshot.ts +244 -0
  238. package/src/router/router-context.ts +3 -28
  239. package/src/router/router-interfaces.ts +115 -35
  240. package/src/router/router-options.ts +172 -15
  241. package/src/router/router-registry.ts +2 -5
  242. package/src/router/segment-resolution/fresh.ts +162 -84
  243. package/src/router/segment-resolution/helpers.ts +86 -6
  244. package/src/router/segment-resolution/loader-cache.ts +76 -39
  245. package/src/router/segment-resolution/revalidation.ts +351 -321
  246. package/src/router/segment-resolution/static-store.ts +19 -5
  247. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  248. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  249. package/src/router/segment-resolution.ts +5 -1
  250. package/src/router/segment-wrappers.ts +6 -5
  251. package/src/router/state-cookie-name.ts +33 -0
  252. package/src/router/substitute-pattern-params.ts +56 -0
  253. package/src/router/telemetry-otel.ts +161 -199
  254. package/src/router/telemetry.ts +96 -19
  255. package/src/router/timeout.ts +0 -20
  256. package/src/router/tracing.ts +206 -0
  257. package/src/router/trie-matching.ts +163 -59
  258. package/src/router/types.ts +9 -63
  259. package/src/router/url-params.ts +44 -0
  260. package/src/router.ts +157 -54
  261. package/src/rsc/handler-context.ts +3 -2
  262. package/src/rsc/handler.ts +655 -529
  263. package/src/rsc/helpers.ts +168 -46
  264. package/src/rsc/index.ts +2 -5
  265. package/src/rsc/json-route-result.ts +38 -0
  266. package/src/rsc/loader-fetch.ts +122 -31
  267. package/src/rsc/manifest-init.ts +33 -42
  268. package/src/rsc/origin-guard.ts +39 -25
  269. package/src/rsc/progressive-enhancement.ts +131 -14
  270. package/src/rsc/redirect-guard.ts +99 -0
  271. package/src/rsc/response-cache-serve.ts +238 -0
  272. package/src/rsc/response-error.ts +79 -12
  273. package/src/rsc/response-route-handler.ts +99 -189
  274. package/src/rsc/rsc-rendering.ts +109 -74
  275. package/src/rsc/runtime-warnings.ts +23 -10
  276. package/src/rsc/server-action.ts +287 -115
  277. package/src/rsc/ssr-setup.ts +18 -2
  278. package/src/rsc/transition-gate.ts +89 -0
  279. package/src/rsc/types.ts +29 -9
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +236 -202
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +224 -52
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +38 -46
  291. package/src/server/request-context.ts +401 -173
  292. package/src/ssr/index.tsx +24 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +105 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +357 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/run-transition-when.ts +164 -0
  320. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  321. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  322. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  323. package/src/testing/vitest-stubs/version.ts +5 -0
  324. package/src/testing/vitest.ts +305 -0
  325. package/src/theme/ThemeProvider.tsx +20 -58
  326. package/src/theme/ThemeScript.tsx +7 -9
  327. package/src/theme/constants.ts +52 -13
  328. package/src/theme/index.ts +0 -7
  329. package/src/theme/theme-context.ts +1 -5
  330. package/src/theme/theme-script.ts +22 -21
  331. package/src/theme/use-theme.ts +0 -3
  332. package/src/types/boundaries.ts +0 -35
  333. package/src/types/cache-types.ts +17 -8
  334. package/src/types/error-types.ts +30 -90
  335. package/src/types/global-namespace.ts +54 -41
  336. package/src/types/handler-context.ts +125 -71
  337. package/src/types/index.ts +3 -10
  338. package/src/types/loader-types.ts +40 -11
  339. package/src/types/request-scope.ts +112 -0
  340. package/src/types/route-config.ts +6 -50
  341. package/src/types/route-entry.ts +12 -7
  342. package/src/types/segments.ts +136 -15
  343. package/src/urls/include-helper.ts +33 -70
  344. package/src/urls/index.ts +1 -11
  345. package/src/urls/path-helper-types.ts +68 -18
  346. package/src/urls/path-helper.ts +57 -111
  347. package/src/urls/pattern-types.ts +48 -19
  348. package/src/urls/response-types.ts +25 -22
  349. package/src/urls/type-extraction.ts +58 -139
  350. package/src/urls/urls-function.ts +1 -19
  351. package/src/use-loader.tsx +346 -89
  352. package/src/vite/debug.ts +185 -0
  353. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  354. package/src/vite/discovery/discover-routers.ts +130 -85
  355. package/src/vite/discovery/discovery-errors.ts +194 -0
  356. package/src/vite/discovery/gate-state.ts +171 -0
  357. package/src/vite/discovery/prerender-collection.ts +214 -132
  358. package/src/vite/discovery/route-types-writer.ts +40 -84
  359. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  360. package/src/vite/discovery/state.ts +57 -4
  361. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  362. package/src/vite/index.ts +6 -0
  363. package/src/vite/inject-client-debug.ts +36 -0
  364. package/src/vite/plugin-types.ts +178 -5
  365. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  366. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  367. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  369. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  370. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  371. package/src/vite/plugins/expose-action-id.ts +48 -95
  372. package/src/vite/plugins/expose-id-utils.ts +96 -51
  373. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  374. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  375. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  376. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  377. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  378. package/src/vite/plugins/performance-tracks.ts +64 -170
  379. package/src/vite/plugins/refresh-cmd.ts +89 -27
  380. package/src/vite/plugins/use-cache-transform.ts +73 -83
  381. package/src/vite/plugins/version-injector.ts +40 -29
  382. package/src/vite/plugins/version-plugin.ts +37 -40
  383. package/src/vite/plugins/virtual-entries.ts +39 -25
  384. package/src/vite/rango.ts +118 -114
  385. package/src/vite/router-discovery.ts +941 -142
  386. package/src/vite/utils/ast-handler-extract.ts +26 -35
  387. package/src/vite/utils/banner.ts +1 -1
  388. package/src/vite/utils/bundle-analysis.ts +10 -15
  389. package/src/vite/utils/client-chunks.ts +184 -0
  390. package/src/vite/utils/directive-prologue.ts +40 -0
  391. package/src/vite/utils/forward-user-plugins.ts +171 -0
  392. package/src/vite/utils/manifest-utils.ts +4 -59
  393. package/src/vite/utils/package-resolution.ts +20 -52
  394. package/src/vite/utils/prerender-utils.ts +81 -34
  395. package/src/vite/utils/shared-utils.ts +92 -42
  396. package/src/browser/action-response-classifier.ts +0 -99
  397. package/src/browser/debug-channel.ts +0 -93
  398. package/src/browser/react/use-client-cache.ts +0 -58
  399. package/src/browser/shallow.ts +0 -40
  400. package/src/handles/index.ts +0 -7
  401. package/src/network-error-thrower.tsx +0 -23
  402. package/src/router/middleware-cookies.ts +0 -55
@@ -11,7 +11,15 @@
11
11
  */
12
12
 
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
+ import { parseCookiesFromHeader } from "./cookie-parse.js";
15
+ import type { CacheErrorCategory } from "../cache/cache-error.js";
14
16
  import type { CookieOptions } from "../router/middleware.js";
17
+ import {
18
+ KEEP_CACHE_HEADER,
19
+ getRawCookieValue,
20
+ mintStateValue,
21
+ serializeStateCookie,
22
+ } from "../browser/cookie-name.js";
15
23
  import type { LoaderDefinition, LoaderContext } from "../types.js";
16
24
  import type { ScopedReverseFunction } from "../reverse.js";
17
25
  import type {
@@ -26,13 +34,28 @@ import {
26
34
  contextSet,
27
35
  isNonCacheable,
28
36
  } from "../context-var.js";
29
- import { createHandleStore, type HandleStore } from "./handle-store.js";
37
+ import {
38
+ createHandleStore,
39
+ buildHandleSnapshot,
40
+ type HandleStore,
41
+ type HandleData,
42
+ } from "./handle-store.js";
30
43
  import { isHandle } from "../handle.js";
31
- import { track, type MetricsStore } from "./context.js";
44
+ import { withDefer } from "../defer.js";
45
+ import { type MetricsStore } from "./context.js";
46
+ import { observePhase, PHASES } from "../router/instrument.js";
32
47
  import { getFetchableLoader } from "./fetchable-loader-store.js";
33
48
  import type { SegmentCacheStore } from "../cache/types.js";
34
49
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
35
- import { THEME_COOKIE } from "../theme/constants.js";
50
+ import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
51
+ import type { TransitionWhenFn } from "../types/segments.js";
52
+ import type { ResolvedTracing } from "../router/tracing.js";
53
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
54
+ import {
55
+ THEME_COOKIE,
56
+ isValidTheme,
57
+ warnInvalidTheme,
58
+ } from "../theme/constants.js";
36
59
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
37
60
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
38
61
  import { isInsideCacheScope } from "./context.js";
@@ -40,7 +63,12 @@ import {
40
63
  createReverseFunction,
41
64
  stripInternalParams,
42
65
  } from "../router/handler-context.js";
43
- import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
66
+ import {
67
+ getGlobalRouteMap,
68
+ isRouteRootScoped,
69
+ getSearchSchema,
70
+ } from "../route-map-builder.js";
71
+ import { parseSearchParams } from "../search-params.js";
44
72
  import { invariant } from "../errors.js";
45
73
  import { isAutoGeneratedRouteName } from "../route-name.js";
46
74
 
@@ -53,24 +81,9 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
53
81
  export interface RequestContext<
54
82
  TEnv = DefaultEnv,
55
83
  TParams = Record<string, string>,
56
- > {
57
- /** Platform bindings (Cloudflare env, etc.) */
58
- env: TEnv;
59
- /** Original HTTP request */
60
- request: Request;
61
- /** Parsed URL (with internal `_rsc*` params stripped) */
62
- url: URL;
63
- /**
64
- * The original request URL with all parameters intact, including
65
- * internal `_rsc*` transport params.
66
- */
67
- originalUrl: URL;
68
- /** URL pathname */
69
- pathname: string;
70
- /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
71
- searchParams: URLSearchParams;
72
- /** Variables set by middleware (same as ctx.var) */
73
- var: Record<string, any>;
84
+ > extends RequestScope<TEnv> {
85
+ /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
86
+ _variables: Record<string, any>;
74
87
  /** Get a variable set by middleware */
75
88
  get: {
76
89
  <T>(contextVar: ContextVar<T>): T | undefined;
@@ -110,6 +123,10 @@ export interface RequestContext<
110
123
  setStatus(status: number): void;
111
124
  /** @internal Set status bypassing cache-exec guard (for framework error handling) */
112
125
  _setStatus(status: number): void;
126
+ /** @internal Rotate the rango state cookie (server seat of invalidateClientCache). */
127
+ _rotateStateCookie(): void;
128
+ /** @internal Set the keepClientCache() directive header on the response. */
129
+ _setKeepCacheDirective(): void;
113
130
 
114
131
  /**
115
132
  * Access loader data or push handle data.
@@ -145,29 +162,43 @@ export interface RequestContext<
145
162
  /** @internal Handle store for tracking handle data across segments */
146
163
  _handleStore: HandleStore;
147
164
 
165
+ /**
166
+ * @internal transition({ when }) predicates for segments matched this request,
167
+ * keyed by segment id. Collected during resolution (the function is stripped
168
+ * from the serialized segment config), then evaluated post-handler in
169
+ * rsc-rendering — outside any cache scope — to drop the transition of any
170
+ * segment whose predicate returns false.
171
+ */
172
+ _transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;
173
+
148
174
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
149
175
  _cacheStore?: SegmentCacheStore;
150
176
 
177
+ /**
178
+ * @internal Handler-owned registry of explicit per-scope stores from
179
+ * cache({ store }). Created once per createRSCHandler() and threaded into
180
+ * every request context, so it accumulates every explicit store the handler
181
+ * resolves. updateTag()/revalidateTag() iterate this set plus _cacheStore to
182
+ * reach every store that may hold tagged entries. The app-level store is not
183
+ * added here (it is always reachable via _cacheStore).
184
+ */
185
+ _explicitTaggedStores?: Set<SegmentCacheStore>;
186
+
187
+ /**
188
+ * @internal Union of every cache tag resolved while producing this request's
189
+ * response (from cache({ tags }), runtime cacheTag(), and loader cache tags).
190
+ * Populated at the tag-resolution sites via recordRequestTags(). Read by the
191
+ * document cache middleware so a full-page entry is tagged with everything its
192
+ * content used and can therefore be invalidated by updateTag()/revalidateTag().
193
+ */
194
+ _requestTags: Set<string>;
195
+
151
196
  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
152
197
  _cacheProfiles?: Record<
153
198
  string,
154
199
  import("../cache/profile-registry.js").CacheProfile
155
200
  >;
156
201
 
157
- /**
158
- * Schedule work to run after the response is sent.
159
- * On Cloudflare Workers, uses ctx.waitUntil().
160
- * On Node.js, runs as fire-and-forget.
161
- *
162
- * @example
163
- * ```typescript
164
- * ctx.waitUntil(async () => {
165
- * await cacheStore.set(key, data, ttl);
166
- * });
167
- * ```
168
- */
169
- waitUntil(fn: () => Promise<void>): void;
170
-
171
202
  /**
172
203
  * Register a callback to run when the response is created.
173
204
  * Callbacks are sync and receive the response. They can:
@@ -271,6 +302,92 @@ export interface RequestContext<
271
302
  /** @internal Previous route key (from the navigation source), used for revalidation */
272
303
  _prevRouteKey?: string;
273
304
 
305
+ /**
306
+ * @internal Navigation/action source data the transition({ when }) gate reads
307
+ * to build its ShouldRevalidateFn-shaped predicate context. currentUrl/Params
308
+ * come from the navigation snapshot (set at match time); action* are stashed
309
+ * at the action-bearing gate call sites. All undefined when there is no source
310
+ * (initial full load) or no action (plain navigation).
311
+ */
312
+ _gateCurrentUrl?: URL;
313
+ _gateCurrentParams?: Record<string, string>;
314
+ _gateActionId?: string;
315
+ _gateActionUrl?: URL;
316
+ _gateActionResult?: unknown;
317
+ _gateFormData?: FormData;
318
+
319
+ /**
320
+ * @internal True while the post-action revalidation render is running (set by
321
+ * revalidateAfterAction). The "use cache" runtime reads this to prefer
322
+ * freshness over a fast stale response during an action: a stale entry
323
+ * re-executes in the foreground (so the action response reflects the refreshed
324
+ * value) with only the store write deferred, instead of serving stale and
325
+ * revalidating in the background. A plain navigation (flag unset) keeps SWR.
326
+ */
327
+ _inActionRevalidation?: boolean;
328
+
329
+ /**
330
+ * @internal Render barrier for experimental `rendered()` API.
331
+ * Resolves when all non-loader segments have settled and handle data
332
+ * is available. Used by DSL loaders that call `ctx.rendered()`.
333
+ */
334
+ _renderBarrier: Promise<void>;
335
+
336
+ /**
337
+ * @internal Resolve the render barrier. Accepts resolved segments, filters
338
+ * out loaders, and captures non-loader segment IDs as the handle ordering.
339
+ * Called after segment resolution (fresh) or handle replay (cache/prerender).
340
+ */
341
+ _resolveRenderBarrier: (
342
+ segments: Array<{ type: string; id: string }>,
343
+ ) => void;
344
+
345
+ /**
346
+ * @internal Segment order at barrier resolution time, used by loader
347
+ * ctx.use(handle) to collect handle data in correct order.
348
+ */
349
+ _renderBarrierSegmentOrder?: string[];
350
+
351
+ /**
352
+ * @internal Set to true when the matched entry tree contains any `loading()`
353
+ * entries (streaming). On a streaming tree rendered() waits for the streaming
354
+ * handlers to settle (via handleStore.settled) before resolving, and the
355
+ * deadlock guard state is kept live until that wait completes.
356
+ */
357
+ _treeHasStreaming?: boolean;
358
+
359
+ /**
360
+ * @internal Loader IDs that have called rendered() and are waiting for the
361
+ * barrier. Used to detect deadlocks when a handler tries to await the same
362
+ * loader via ctx.use(Loader).
363
+ */
364
+ _renderBarrierWaiters?: Set<string>;
365
+
366
+ /**
367
+ * @internal Loader IDs that handlers have started awaiting via ctx.use().
368
+ * Used for bidirectional deadlock detection: if a loader later calls
369
+ * rendered() and a handler already awaits it, we can detect the deadlock.
370
+ */
371
+ _handlerLoaderDeps?: Set<string>;
372
+
373
+ /**
374
+ * @internal Cached HandleData snapshot built at barrier resolution time.
375
+ * Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
376
+ */
377
+ _renderBarrierHandleSnapshot?: HandleData;
378
+
379
+ /**
380
+ * @internal The deadlock guard window is closed (no further handler-awaits-
381
+ * loader cycle is possible). For non-streaming trees this is set when the
382
+ * barrier resolves. For streaming trees the window stays open until
383
+ * handleStore.settled — rendered() keeps waiting past the barrier and a
384
+ * loading() handler can still resume and await a still-waiting loader — so it
385
+ * is set only after settled. The guard (loader-resolution `setupLoaderAccess`)
386
+ * reads this instead of `_renderBarrierSegmentOrder` so it does not go blind
387
+ * during the streaming settle wait.
388
+ */
389
+ _renderBarrierGuardClosed?: boolean;
390
+
274
391
  /** @internal Per-request error dedup set for onError reporting */
275
392
  _reportedErrors: WeakSet<object>;
276
393
 
@@ -278,9 +395,13 @@ export interface RequestContext<
278
395
  * @internal Report a non-fatal background error through the router's
279
396
  * onError callback. Wired by the RSC handler / router during request
280
397
  * creation. Cache-runtime and other subsystems call this to surface
281
- * errors without failing the response.
398
+ * errors without failing the response. `category` is surfaced to consumers as
399
+ * `metadata.category` on the onError context (phase `cache`).
282
400
  */
283
- _reportBackgroundError?: (error: unknown, category: string) => void;
401
+ _reportBackgroundError?: (
402
+ error: unknown,
403
+ category: CacheErrorCategory,
404
+ ) => void;
284
405
 
285
406
  /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
286
407
  _debugPerformance?: boolean;
@@ -288,11 +409,26 @@ export interface RequestContext<
288
409
  /** @internal Request-scoped performance metrics store */
289
410
  _metricsStore?: MetricsStore;
290
411
 
291
- /** @internal Dev-only: debug channel for React Performance Tracks */
292
- _debugChannel?: {
293
- readable: ReadableStream;
294
- writable: WritableStream;
295
- };
412
+ /** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
413
+ _tracing?: ResolvedTracing;
414
+
415
+ /** @internal Router basename for this request (used by redirect()) */
416
+ _basename?: string;
417
+
418
+ /**
419
+ * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
420
+ * to avoid a second resolveRoute call. Cleared on HMR invalidation.
421
+ */
422
+ _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
423
+
424
+ /**
425
+ * @internal Coarse route-level cache signal for the X-Rango-Cache debug
426
+ * header. Populated by match/matchPartial only when the debug cache signal
427
+ * gate is enabled (debugCacheSignal option or RANGO_TEST_SIGNALS=1). Read by
428
+ * the response-finalization path (createResponseWithMergedHeaders). Undefined
429
+ * when the gate is off, so no header is emitted.
430
+ */
431
+ _cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
296
432
  }
297
433
 
298
434
  /**
@@ -311,19 +447,42 @@ export type PublicRequestContext<
311
447
  | "setCookie"
312
448
  | "deleteCookie"
313
449
  | "_handleStore"
450
+ | "_transitionWhen"
314
451
  | "_cacheStore"
452
+ | "_explicitTaggedStores"
453
+ | "_requestTags"
315
454
  | "_cacheProfiles"
316
455
  | "_onResponseCallbacks"
317
456
  | "_themeConfig"
318
457
  | "_locationState"
319
458
  | "_routeName"
320
459
  | "_prevRouteKey"
460
+ | "_gateCurrentUrl"
461
+ | "_gateCurrentParams"
462
+ | "_gateActionId"
463
+ | "_gateActionUrl"
464
+ | "_gateActionResult"
465
+ | "_gateFormData"
466
+ | "_inActionRevalidation"
321
467
  | "_reportedErrors"
468
+ | "_renderBarrier"
469
+ | "_resolveRenderBarrier"
470
+ | "_renderBarrierSegmentOrder"
471
+ | "_treeHasStreaming"
472
+ | "_renderBarrierWaiters"
473
+ | "_handlerLoaderDeps"
474
+ | "_renderBarrierHandleSnapshot"
475
+ | "_renderBarrierGuardClosed"
322
476
  | "_reportBackgroundError"
323
477
  | "_debugPerformance"
324
478
  | "_metricsStore"
325
- | "_debugChannel"
479
+ | "_basename"
326
480
  | "_setStatus"
481
+ | "_rotateStateCookie"
482
+ | "_setKeepCacheDirective"
483
+ | "_variables"
484
+ | "_classifiedRoute"
485
+ | "_cacheSignal"
327
486
  | "res"
328
487
  >;
329
488
 
@@ -375,6 +534,7 @@ export function _getRequestContext<TEnv = DefaultEnv>():
375
534
  export function setRequestContextParams(
376
535
  params: Record<string, string>,
377
536
  routeName?: string,
537
+ routeMap?: Record<string, string>,
378
538
  ): void {
379
539
  const ctx = requestContextStorage.getStore();
380
540
  if (ctx) {
@@ -387,9 +547,13 @@ export function setRequestContextParams(
387
547
  : undefined
388
548
  ) as DefaultRouteName | undefined;
389
549
  }
390
- // Update reverse with scoped resolution now that route is known
550
+ // Update reverse with scoped resolution now that route is known. Production
551
+ // omits routeMap and uses the global map (routes are registered globally);
552
+ // the testing primitives (renderToFlightString/renderServerTree) pass a
553
+ // scoped routeMap so `ctx.reverse` is not order-dependent on whatever router
554
+ // registered last.
391
555
  ctx.reverse = createReverseFunction(
392
- getGlobalRouteMap(),
556
+ routeMap ?? getGlobalRouteMap(),
393
557
  routeName,
394
558
  params,
395
559
  routeName ? isRouteRootScoped(routeName) : undefined,
@@ -405,11 +569,17 @@ export function setRequestContextParams(
405
569
  */
406
570
  export function setRequestContextPrevRouteKey(
407
571
  prevRouteKey: string | undefined,
572
+ currentUrl?: URL,
573
+ currentParams?: Record<string, string>,
408
574
  ): void {
409
575
  const ctx = requestContextStorage.getStore();
410
- if (ctx && prevRouteKey !== undefined) {
411
- ctx._prevRouteKey = prevRouteKey;
412
- }
576
+ if (!ctx) return;
577
+ if (prevRouteKey !== undefined) ctx._prevRouteKey = prevRouteKey;
578
+ // Source URL/params for the transition({ when }) gate (effectiveFromUrl /
579
+ // effectiveFromMatch.params from the navigation snapshot). Same write point as
580
+ // _prevRouteKey, which doubles as fromRouteName.
581
+ if (currentUrl !== undefined) ctx._gateCurrentUrl = currentUrl;
582
+ if (currentParams !== undefined) ctx._gateCurrentParams = currentParams;
413
583
  }
414
584
 
415
585
  /**
@@ -423,23 +593,7 @@ export function getLocationState(): LocationStateEntry[] | undefined {
423
593
  return ctx?._locationState;
424
594
  }
425
595
 
426
- /**
427
- * Get the current request context, throwing if not available
428
- * @deprecated Use getRequestContext() directly — it now throws if outside context
429
- */
430
- export function requireRequestContext<
431
- TEnv = DefaultEnv,
432
- >(): RequestContext<TEnv> {
433
- return getRequestContext<TEnv>();
434
- }
435
-
436
- /**
437
- * Cloudflare Workers ExecutionContext (subset we need)
438
- */
439
- export interface ExecutionContext {
440
- waitUntil(promise: Promise<any>): void;
441
- passThroughOnException(): void;
442
- }
596
+ export type { ExecutionContext };
443
597
 
444
598
  /**
445
599
  * Options for creating a request context
@@ -453,6 +607,11 @@ export interface CreateRequestContextOptions<TEnv> {
453
607
  initialResponse?: Response;
454
608
  /** Optional cache store for segment caching (used by CacheScope) */
455
609
  cacheStore?: SegmentCacheStore;
610
+ /**
611
+ * Handler-owned registry of explicit per-scope stores for cross-store tag
612
+ * invalidation. Created once per handler, reused across requests.
613
+ */
614
+ explicitTaggedStores?: Set<SegmentCacheStore>;
456
615
  /** Optional cache profiles for "use cache" resolution (per-router) */
457
616
  cacheProfiles?: Record<
458
617
  string,
@@ -462,6 +621,10 @@ export interface CreateRequestContextOptions<TEnv> {
462
621
  executionContext?: ExecutionContext;
463
622
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
464
623
  themeConfig?: ResolvedThemeConfig | null;
624
+ /** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
625
+ stateCookieName?: string;
626
+ /** Build version, used as the prefix of a server-rotated rango state value. */
627
+ version?: string;
465
628
  }
466
629
 
467
630
  /**
@@ -482,15 +645,17 @@ export function createRequestContext<TEnv>(
482
645
  variables,
483
646
  initialResponse,
484
647
  cacheStore,
648
+ explicitTaggedStores,
485
649
  cacheProfiles,
486
650
  executionContext,
487
651
  themeConfig,
652
+ stateCookieName,
653
+ version: stateVersion,
488
654
  } = options;
489
655
  const cookieHeader = request.headers.get("Cookie");
656
+ let rangoStateRotated = false;
490
657
  let parsedCookies: Record<string, string> | null = null;
491
658
 
492
- // Create stub response for collecting headers/cookies.
493
- // All cookie/header mutations go here; cookie reads derive from it.
494
659
  let stubResponse = initialResponse
495
660
  ? new Response(null, {
496
661
  status: initialResponse.status,
@@ -499,11 +664,9 @@ export function createRequestContext<TEnv>(
499
664
  })
500
665
  : new Response(null, { status: 200 });
501
666
 
502
- // Create handle store and loader memoization for this request
503
667
  const handleStore = createHandleStore();
504
668
  const loaderPromises = new Map<string, Promise<any>>();
505
669
 
506
- // Lazy parse cookies from the original Cookie header
507
670
  const getParsedCookies = (): Record<string, string> => {
508
671
  if (!parsedCookies) {
509
672
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -511,7 +674,6 @@ export function createRequestContext<TEnv>(
511
674
  return parsedCookies;
512
675
  };
513
676
 
514
- // Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
515
677
  let responseCookieCache: Map<string, string | null> | null = null;
516
678
  const getResponseCookies = (): Map<string, string | null> => {
517
679
  if (!responseCookieCache) {
@@ -523,8 +685,6 @@ export function createRequestContext<TEnv>(
523
685
  responseCookieCache = null;
524
686
  };
525
687
 
526
- // Guard: throw if a response-level side effect is called inside a cache() scope.
527
- // Uses ALS to detect the scope (set during segment resolution).
528
688
  function assertNotInsideCacheScopeALS(methodName: string): void {
529
689
  if (isInsideCacheScope()) {
530
690
  throw new Error(
@@ -535,8 +695,7 @@ export function createRequestContext<TEnv>(
535
695
  }
536
696
  }
537
697
 
538
- // Effective cookie read: response stub Set-Cookie wins, then original header.
539
- // The stub IS the source of truth for same-request mutations.
698
+ // Response stub Set-Cookie wins, then original header (source of truth for mutations).
540
699
  const effectiveCookie = (name: string): string | undefined => {
541
700
  const mutations = getResponseCookies();
542
701
  if (mutations.has(name)) {
@@ -546,14 +705,11 @@ export function createRequestContext<TEnv>(
546
705
  return getParsedCookies()[name];
547
706
  };
548
707
 
549
- // Theme helpers (only used when themeConfig is provided)
550
708
  const getTheme = (): Theme | undefined => {
551
709
  if (!themeConfig) return undefined;
552
710
 
553
- // Use overlay-aware read so setTheme() in the same request is reflected
554
711
  const stored = effectiveCookie(themeConfig.storageKey);
555
712
  if (stored) {
556
- // Validate stored value
557
713
  if (stored === "system" && themeConfig.enableSystem) {
558
714
  return "system";
559
715
  }
@@ -567,15 +723,15 @@ export function createRequestContext<TEnv>(
567
723
  const setTheme = (theme: Theme): void => {
568
724
  if (!themeConfig) return;
569
725
 
570
- // Validate theme value
571
- if (theme !== "system" && !themeConfig.themes.includes(theme)) {
572
- console.warn(
573
- `[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
574
- );
726
+ // Shared guard (isValidTheme): reject any value not in the configured theme
727
+ // set, AND reject "system" when system detection is off — a cookie of
728
+ // theme=system with enableSystem:false would re-apply a bogus class="system"
729
+ // on the next SSR.
730
+ if (!isValidTheme(theme, themeConfig)) {
731
+ warnInvalidTheme(theme, themeConfig);
575
732
  return;
576
733
  }
577
734
 
578
- // Write to stub — effectiveCookie() will pick it up on next read
579
735
  stubResponse.headers.append(
580
736
  "Set-Cookie",
581
737
  serializeCookieValue(themeConfig.storageKey, theme, {
@@ -587,10 +743,8 @@ export function createRequestContext<TEnv>(
587
743
  invalidateResponseCookieCache();
588
744
  };
589
745
 
590
- // Strip internal _rsc* params so userland sees a clean URL.
591
746
  const cleanUrl = stripInternalParams(url);
592
747
 
593
- // Build the context object first (without use), then add use
594
748
  const ctx: RequestContext<TEnv> = {
595
749
  env,
596
750
  request,
@@ -598,7 +752,7 @@ export function createRequestContext<TEnv>(
598
752
  originalUrl: new URL(request.url),
599
753
  pathname: url.pathname,
600
754
  searchParams: cleanUrl.searchParams,
601
- var: variables,
755
+ _variables: variables,
602
756
  get: ((keyOrVar: any) => {
603
757
  if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
604
758
  throw new Error(
@@ -676,6 +830,45 @@ export function createRequestContext<TEnv>(
676
830
  stubResponse.headers.set(name, value);
677
831
  },
678
832
 
833
+ // Rotate the rango state cookie for the responding client (the server seat
834
+ // of invalidateClientCache). Writes ONE Set-Cookie per request with the
835
+ // value {version}:{timestamp}; the `:` stays raw (the cookie-name.ts
836
+ // serializer), not the URL-encoded form serializeCookieValue would produce.
837
+ // The timestamp is strictly greater than the client's current one (inbound
838
+ // X-Rango-State), so a same-millisecond server rotation still differs from
839
+ // the client value and the divergence observer fires.
840
+ _rotateStateCookie(): void {
841
+ if (rangoStateRotated) return;
842
+ rangoStateRotated = true;
843
+ if (!stateCookieName) return;
844
+ // The client's current value, for the monotonic guard: prefer the
845
+ // X-Rango-State header (router navigation/prefetch fetches send it), but
846
+ // fall back to the request's rango state cookie — action POSTs / plain
847
+ // app fetch()s carry no router header yet DO send the cookie. Without the
848
+ // fallback, prevTs stays 0 and a same-ms mint can equal the client value,
849
+ // leaving the divergence observer silent. `|| null` so an empty header
850
+ // ('' from proxy normalization) falls through instead of short-circuiting.
851
+ // getRawCookieValue reads the cookie undecoded (the wire value
852
+ // decodeStateValue decodes exactly once) AND is the same parser the client
853
+ // mirror uses, so both seats read the same jar entry.
854
+ const prevRaw =
855
+ (request.headers.get("x-rango-state") || null) ??
856
+ getRawCookieValue(cookieHeader, stateCookieName);
857
+ const value = mintStateValue(stateVersion ?? "0", prevRaw);
858
+ stubResponse.headers.append(
859
+ "Set-Cookie",
860
+ serializeStateCookie(stateCookieName, value, url.protocol === "https:"),
861
+ );
862
+ invalidateResponseCookieCache();
863
+ },
864
+
865
+ // Set the keepClientCache() directive header. The action bridge reads it on
866
+ // the response and suppresses its automatic invalidation. `.set` makes this
867
+ // idempotent (one header regardless of call count).
868
+ _setKeepCacheDirective(): void {
869
+ stubResponse.headers.set(KEEP_CACHE_HEADER, "1");
870
+ },
871
+
679
872
  setStatus(status: number): void {
680
873
  assertNotInsideCacheExec(ctx, "setStatus");
681
874
  assertNotInsideCacheScopeALS("setStatus");
@@ -692,27 +885,31 @@ export function createRequestContext<TEnv>(
692
885
  });
693
886
  },
694
887
 
695
- // Placeholder - will be replaced below
696
888
  use: null as any,
697
889
 
698
890
  method: request.method,
699
891
 
700
892
  _handleStore: handleStore,
893
+ _transitionWhen: [],
701
894
  _cacheStore: cacheStore,
895
+ _explicitTaggedStores: explicitTaggedStores,
896
+ _requestTags: new Set<string>(),
702
897
  _cacheProfiles: cacheProfiles,
703
898
 
704
899
  waitUntil(fn: () => Promise<void>): void {
705
900
  if (executionContext?.waitUntil) {
706
- // Cloudflare Workers: use native waitUntil
707
- executionContext.waitUntil(fn());
901
+ // Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
902
+ // non-async callback becomes a rejected promise handed to the host's
903
+ // waitUntil (logged as a background failure), instead of escaping into
904
+ // the request flow. Mirrors fireAndForgetWaitUntil's deferral.
905
+ executionContext.waitUntil(Promise.resolve().then(fn));
708
906
  } else {
709
- // Node.js / dev: fire-and-forget with error logging
710
- fn().catch((err) =>
711
- console.error("[waitUntil] Background task failed:", err),
712
- );
907
+ fireAndForgetWaitUntil(fn);
713
908
  }
714
909
  },
715
910
 
911
+ executionContext,
912
+
716
913
  _onResponseCallbacks: [],
717
914
 
718
915
  onResponse(callback: (response: Response) => Response): void {
@@ -721,7 +918,6 @@ export function createRequestContext<TEnv>(
721
918
  this._onResponseCallbacks.push(callback);
722
919
  },
723
920
 
724
- // Theme properties (only set when themeConfig is provided)
725
921
  get theme() {
726
922
  return themeConfig ? getTheme() : undefined;
727
923
  },
@@ -745,35 +941,88 @@ export function createRequestContext<TEnv>(
745
941
  _reportedErrors: new WeakSet<object>(),
746
942
  _metricsStore: undefined,
747
943
 
944
+ _renderBarrier: null as any,
945
+ _resolveRenderBarrier: null as any,
946
+ _renderBarrierSegmentOrder: undefined,
947
+
748
948
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
749
949
  };
750
950
 
751
- // Now create use() with access to ctx
951
+ // Lazy allocation: only create Promise when a loader calls rendered().
952
+ let barrierResolved = false;
953
+ let resolveBarrier: (() => void) | undefined;
954
+ ctx._renderBarrier = null as any;
955
+ ctx._resolveRenderBarrier = (
956
+ segments: Array<{ type: string; id: string }>,
957
+ ) => {
958
+ if (barrierResolved) return;
959
+ barrierResolved = true;
960
+ const segOrder = segments
961
+ .filter((s) => s.type !== "loader")
962
+ .map((s) => s.id);
963
+ ctx._renderBarrierSegmentOrder = segOrder;
964
+
965
+ const closeGuard = () => {
966
+ ctx._renderBarrierWaiters = undefined;
967
+ ctx._handlerLoaderDeps = undefined;
968
+ ctx._renderBarrierGuardClosed = true;
969
+ };
970
+
971
+ if (ctx._treeHasStreaming) {
972
+ handleStore.settled.then(closeGuard);
973
+ } else {
974
+ ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
975
+ handleStore,
976
+ segOrder,
977
+ );
978
+ closeGuard();
979
+ }
980
+ if (resolveBarrier) resolveBarrier();
981
+ };
982
+ Object.defineProperty(ctx, "_renderBarrier", {
983
+ get() {
984
+ const p = barrierResolved
985
+ ? Promise.resolve()
986
+ : new Promise<void>((resolve) => {
987
+ resolveBarrier = resolve;
988
+ });
989
+ Object.defineProperty(ctx, "_renderBarrier", {
990
+ value: p,
991
+ writable: false,
992
+ configurable: false,
993
+ });
994
+ return p;
995
+ },
996
+ configurable: true,
997
+ });
998
+
752
999
  ctx.use = createUseFunction({
753
1000
  handleStore,
754
1001
  loaderPromises,
755
1002
  getContext: () => ctx,
756
1003
  });
757
1004
 
758
- // Brand with taint symbol so "use cache" excludes ctx from cache keys
759
1005
  (ctx as any)[NOCACHE_SYMBOL] = true;
760
1006
  return ctx;
761
1007
  }
762
1008
 
763
- /**
764
- * Parse Set-Cookie headers from a response into effective cookie state.
765
- * Returns a map of cookie name -> value (string) or name -> null (deleted).
766
- * Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
767
- * Max-Age=0 is treated as a delete.
768
- */
769
- const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
1009
+ // Capture the Max-Age value so it can be parsed numerically. A leading zero
1010
+ // (Max-Age=05) is a non-zero lifetime, not a deletion; only a value that parses
1011
+ // to <= 0 marks a cookie for deletion. Pattern-matching a leading "0" misread
1012
+ // zero-prefixed values like 05 / 010 as deletions.
1013
+ const MAX_AGE_RE = /;\s*Max-Age\s*=\s*(-?\d+)/i;
1014
+
1015
+ function isCookieDeletion(header: string): boolean {
1016
+ const m = MAX_AGE_RE.exec(header);
1017
+ if (!m) return false;
1018
+ return Number(m[1]) <= 0;
1019
+ }
770
1020
 
771
1021
  function parseResponseCookies(response: Response): Map<string, string | null> {
772
1022
  const result = new Map<string, string | null>();
773
1023
  const setCookies = response.headers.getSetCookie();
774
1024
 
775
1025
  for (const header of setCookies) {
776
- // First segment before ';' is the name=value pair
777
1026
  const semiIdx = header.indexOf(";");
778
1027
  const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
779
1028
  const eqIdx = pair.indexOf("=");
@@ -785,49 +1034,22 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
785
1034
  name = decodeURIComponent(pair.substring(0, eqIdx).trim());
786
1035
  value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
787
1036
  } catch {
788
- // Malformed encoding — skip this entry
789
1037
  continue;
790
1038
  }
791
1039
 
792
- // Max-Age=0 means the cookie is being deleted
793
- const isDeleted = MAX_AGE_ZERO_RE.test(header);
1040
+ const isDeleted = isCookieDeletion(header);
794
1041
  result.set(name, isDeleted ? null : value);
795
1042
  }
796
1043
 
797
1044
  return result;
798
1045
  }
799
1046
 
800
- /**
801
- * Parse cookies from Cookie header
802
- */
803
- function parseCookiesFromHeader(
804
- cookieHeader: string | null,
805
- ): Record<string, string> {
806
- if (!cookieHeader) return {};
807
-
808
- const cookies: Record<string, string> = {};
809
- const pairs = cookieHeader.split(";");
810
-
811
- for (const pair of pairs) {
812
- const [name, ...rest] = pair.trim().split("=");
813
- if (name) {
814
- const raw = rest.join("=");
815
- try {
816
- cookies[name] = decodeURIComponent(raw);
817
- } catch {
818
- // Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
819
- cookies[name] = raw;
820
- }
821
- }
822
- }
823
-
824
- return cookies;
825
- }
1047
+ // Re-exported for unit tests and the existing import path. The implementation
1048
+ // lives in the dependency-free ./cookie-parse leaf so consumers (e.g. the host
1049
+ // dispatcher) can share it without pulling this module's request-context graph.
1050
+ export { parseCookiesFromHeader };
826
1051
 
827
- /**
828
- * Serialize a cookie for Set-Cookie header
829
- */
830
- function serializeCookieValue(
1052
+ export function serializeCookieValue(
831
1053
  name: string,
832
1054
  value: string,
833
1055
  options: CookieOptions = {},
@@ -854,20 +1076,12 @@ export interface CreateUseFunctionOptions<TEnv> {
854
1076
  getContext: () => RequestContext<TEnv>;
855
1077
  }
856
1078
 
857
- /**
858
- * Create the use() function for loader and handle composition.
859
- *
860
- * This is the unified implementation used by both RequestContext and HandlerContext.
861
- * - For loaders: executes and memoizes loader functions
862
- * - For handles: returns a push function to add handle data
863
- */
864
1079
  export function createUseFunction<TEnv>(
865
1080
  options: CreateUseFunctionOptions<TEnv>,
866
1081
  ): RequestContext["use"] {
867
1082
  const { handleStore, loaderPromises, getContext } = options;
868
1083
 
869
1084
  return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
870
- // Handle case: return a push function
871
1085
  if (isHandle(item)) {
872
1086
  const handle = item;
873
1087
  const ctx = getContext();
@@ -880,30 +1094,24 @@ export function createUseFunction<TEnv>(
880
1094
  );
881
1095
  }
882
1096
 
883
- // Return a push function bound to this handle and segment
884
- return (
885
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
886
- ) => {
887
- // If it's a function, call it immediately to get the promise
888
- const valueOrPromise =
889
- typeof dataOrFn === "function"
890
- ? (dataOrFn as () => Promise<unknown>)()
891
- : dataOrFn;
892
-
893
- // Push directly - promises will be serialized by RSC and streamed
894
- handleStore.push(handle.$$id, segmentId, valueOrPromise);
895
- };
1097
+ return withDefer(
1098
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
1099
+ const valueOrPromise =
1100
+ typeof dataOrFn === "function"
1101
+ ? (dataOrFn as () => Promise<unknown>)()
1102
+ : dataOrFn;
1103
+
1104
+ handleStore.push(handle.$$id, segmentId, valueOrPromise);
1105
+ },
1106
+ );
896
1107
  }
897
1108
 
898
- // Loader case
899
1109
  const loader = item as LoaderDefinition<any, any>;
900
1110
 
901
- // Return cached promise if already started
902
1111
  if (loaderPromises.has(loader.$$id)) {
903
1112
  return loaderPromises.get(loader.$$id);
904
1113
  }
905
1114
 
906
- // Get loader function - either from loader object or fetchable registry
907
1115
  let loaderFn = loader.fn;
908
1116
  if (!loaderFn) {
909
1117
  const fetchable = getFetchableLoader(loader.$$id);
@@ -920,24 +1128,36 @@ export function createUseFunction<TEnv>(
920
1128
 
921
1129
  const ctx = getContext();
922
1130
 
923
- // Create loader context with recursive use() support
1131
+ // Build the typed ctx.search the same way the render path
1132
+ // (createHandlerContext) and the fetchable-loader path (loader-fetch.ts) do:
1133
+ // parse the route's search schema over the cleaned searchParams. The base
1134
+ // RequestContext carries no `search` field, so reading `(ctx as any).search`
1135
+ // here always yielded {} — dropping typed search for action/dispatch loaders.
1136
+ const searchSchema = ctx._routeName
1137
+ ? getSearchSchema(ctx._routeName)
1138
+ : undefined;
1139
+ const loaderSearch = searchSchema
1140
+ ? parseSearchParams(ctx.searchParams, searchSchema)
1141
+ : {};
1142
+
924
1143
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
925
1144
  params: ctx.params,
926
1145
  routeParams: (ctx.params ?? {}) as Record<string, string>,
927
1146
  request: ctx.request,
928
1147
  searchParams: ctx.searchParams,
929
- search: (ctx as any).search ?? {},
1148
+ search: loaderSearch,
930
1149
  pathname: ctx.pathname,
931
1150
  url: ctx.url,
1151
+ originalUrl: ctx.originalUrl,
932
1152
  env: ctx.env as any,
933
- var: ctx.var as any,
1153
+ waitUntil: ctx.waitUntil.bind(ctx),
1154
+ executionContext: ctx.executionContext,
934
1155
  get: ctx.get as any,
935
- use: <TDep, TDepParams = any>(
1156
+ use: (<TDep, TDepParams = any>(
936
1157
  dep: LoaderDefinition<TDep, TDepParams>,
937
1158
  ): Promise<TDep> => {
938
- // Recursive call - will start dep loader if not already started
939
1159
  return ctx.use(dep);
940
- },
1160
+ }) as LoaderContext["use"],
941
1161
  method: "GET",
942
1162
  body: undefined,
943
1163
  reverse: createReverseFunction(
@@ -946,14 +1166,22 @@ export function createUseFunction<TEnv>(
946
1166
  ctx.params as Record<string, string>,
947
1167
  ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
948
1168
  ),
1169
+ rendered: () => {
1170
+ throw new Error(
1171
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
1172
+ `It cannot be used from request-context loaders or server actions.`,
1173
+ );
1174
+ },
949
1175
  };
950
1176
 
951
- const doneLoader = track(`loader:${loader.$$id}`, 2);
952
- const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
953
- doneLoader();
954
- });
1177
+ // Meter through the same unified phase API as the loader-resolution funnel
1178
+ // (observePhase), so a loader resolved via this base request-context ctx.use
1179
+ // co-emits the "loader:<id>" perf metric AND the "rango.loader" span — no
1180
+ // drift between the two ctx.use implementations.
1181
+ const promise = observePhase(PHASES.loader(loader.$$id), () =>
1182
+ Promise.resolve(loaderFn(loaderCtx)),
1183
+ );
955
1184
 
956
- // Memoize for subsequent calls
957
1185
  loaderPromises.set(loader.$$id, promise);
958
1186
 
959
1187
  return promise;