@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136

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 (404) 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 +3386 -1111
  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 +78 -25
  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 +247 -23
  13. package/skills/caching/SKILL.md +281 -11
  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 +289 -53
  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 +914 -0
  29. package/skills/mime-routes/SKILL.md +28 -1
  30. package/skills/observability/SKILL.md +172 -0
  31. package/skills/parallel/SKILL.md +144 -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 +101 -5
  37. package/skills/router-setup/SKILL.md +117 -10
  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 +332 -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 +199 -86
  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 +174 -109
  80. package/src/browser/prefetch/cache.ts +222 -69
  81. package/src/browser/prefetch/fetch.ts +347 -39
  82. package/src/browser/prefetch/queue.ts +113 -32
  83. package/src/browser/prefetch/resource-ready.ts +77 -0
  84. package/src/browser/rango-state.ts +158 -76
  85. package/src/browser/react/Link.tsx +102 -15
  86. package/src/browser/react/NavigationProvider.tsx +300 -122
  87. package/src/browser/react/ScrollRestoration.tsx +10 -6
  88. package/src/browser/react/context.ts +7 -2
  89. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  90. package/src/browser/react/filter-segment-order.ts +66 -7
  91. package/src/browser/react/index.ts +0 -48
  92. package/src/browser/react/location-state-shared.ts +178 -8
  93. package/src/browser/react/location-state.ts +39 -14
  94. package/src/browser/react/use-action.ts +6 -15
  95. package/src/browser/react/use-handle.ts +23 -69
  96. package/src/browser/react/use-href.tsx +8 -1
  97. package/src/browser/react/use-link-status.ts +33 -8
  98. package/src/browser/react/use-navigation.ts +32 -7
  99. package/src/browser/react/use-params.ts +20 -10
  100. package/src/browser/react/use-reverse.ts +106 -0
  101. package/src/browser/react/use-router.ts +46 -11
  102. package/src/browser/react/use-search-params.ts +0 -5
  103. package/src/browser/react/use-segments.ts +11 -21
  104. package/src/browser/response-adapter.ts +99 -8
  105. package/src/browser/rsc-router.tsx +125 -28
  106. package/src/browser/scroll-restoration.ts +37 -22
  107. package/src/browser/segment-reconciler.ts +36 -14
  108. package/src/browser/segment-structure-assert.ts +2 -2
  109. package/src/browser/server-action-bridge.ts +222 -61
  110. package/src/browser/types.ts +115 -12
  111. package/src/browser/validate-redirect-origin.ts +43 -16
  112. package/src/build/collect-fallback-refs.ts +107 -0
  113. package/src/build/generate-manifest.ts +65 -40
  114. package/src/build/generate-route-types.ts +5 -1
  115. package/src/build/index.ts +8 -2
  116. package/src/build/prefix-tree-utils.ts +123 -0
  117. package/src/build/route-trie.ts +165 -36
  118. package/src/build/route-types/ast-route-extraction.ts +15 -8
  119. package/src/build/route-types/codegen.ts +16 -5
  120. package/src/build/route-types/include-resolution.ts +125 -24
  121. package/src/build/route-types/param-extraction.ts +6 -3
  122. package/src/build/route-types/per-module-writer.ts +22 -6
  123. package/src/build/route-types/router-processing.ts +260 -94
  124. package/src/build/route-types/scan-filter.ts +9 -2
  125. package/src/build/route-types/source-scan.ts +216 -0
  126. package/src/build/runtime-discovery.ts +9 -20
  127. package/src/cache/cache-error.ts +104 -0
  128. package/src/cache/cache-key-utils.ts +29 -13
  129. package/src/cache/cache-policy.ts +108 -34
  130. package/src/cache/cache-runtime.ts +239 -52
  131. package/src/cache/cache-scope.ts +234 -87
  132. package/src/cache/cache-tag.ts +103 -0
  133. package/src/cache/cf/cf-base64.ts +33 -0
  134. package/src/cache/cf/cf-cache-constants.ts +127 -0
  135. package/src/cache/cf/cf-cache-store.ts +1989 -378
  136. package/src/cache/cf/cf-cache-types.ts +349 -0
  137. package/src/cache/cf/cf-kv-utils.ts +46 -0
  138. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  139. package/src/cache/cf/index.ts +6 -16
  140. package/src/cache/document-cache.ts +89 -21
  141. package/src/cache/handle-snapshot.ts +70 -0
  142. package/src/cache/index.ts +10 -20
  143. package/src/cache/memory-segment-store.ts +136 -37
  144. package/src/cache/profile-registry.ts +46 -31
  145. package/src/cache/read-through-swr.ts +56 -12
  146. package/src/cache/segment-codec.ts +9 -17
  147. package/src/cache/tag-invalidation.ts +230 -0
  148. package/src/cache/taint.ts +55 -0
  149. package/src/cache/types.ts +37 -100
  150. package/src/client.rsc.tsx +44 -21
  151. package/src/client.tsx +119 -290
  152. package/src/cloudflare/index.ts +11 -0
  153. package/src/cloudflare/tracing.ts +109 -0
  154. package/src/component-utils.ts +19 -0
  155. package/src/components/DefaultDocument.tsx +8 -2
  156. package/src/context-var.ts +84 -2
  157. package/src/decode-loader-results.ts +52 -0
  158. package/src/defer.ts +196 -0
  159. package/src/deps/ssr.ts +0 -1
  160. package/src/encode-kv.ts +49 -0
  161. package/src/errors.ts +30 -4
  162. package/src/escape-script.ts +52 -0
  163. package/src/handle.ts +70 -22
  164. package/src/handles/MetaTags.tsx +62 -19
  165. package/src/handles/Scripts.tsx +183 -0
  166. package/src/handles/breadcrumbs.ts +37 -8
  167. package/src/handles/is-thenable.ts +19 -0
  168. package/src/handles/meta.ts +51 -40
  169. package/src/handles/script.ts +244 -0
  170. package/src/host/cookie-handler.ts +9 -60
  171. package/src/host/errors.ts +0 -24
  172. package/src/host/index.ts +8 -2
  173. package/src/host/pattern-matcher.ts +23 -52
  174. package/src/host/router.ts +107 -99
  175. package/src/host/testing.ts +40 -27
  176. package/src/host/types.ts +37 -4
  177. package/src/host/utils.ts +1 -1
  178. package/src/href-client.ts +137 -22
  179. package/src/index.rsc.ts +99 -13
  180. package/src/index.ts +139 -19
  181. package/src/internal-debug.ts +11 -10
  182. package/src/loader-store.ts +500 -0
  183. package/src/loader.rsc.ts +20 -13
  184. package/src/loader.ts +12 -11
  185. package/src/missing-id-error.ts +68 -0
  186. package/src/outlet-context.ts +1 -1
  187. package/src/outlet-provider.tsx +1 -5
  188. package/src/prerender/param-hash.ts +16 -16
  189. package/src/prerender/store.ts +37 -41
  190. package/src/prerender.ts +198 -82
  191. package/src/redirect-origin.ts +100 -0
  192. package/src/regex-escape.ts +8 -0
  193. package/src/render-error-thrower.tsx +20 -0
  194. package/src/response-utils.ts +62 -0
  195. package/src/reverse.ts +65 -15
  196. package/src/root-error-boundary.tsx +1 -19
  197. package/src/route-content-wrapper.tsx +19 -77
  198. package/src/route-definition/dsl-helpers.ts +461 -304
  199. package/src/route-definition/helper-factories.ts +28 -140
  200. package/src/route-definition/helpers-types.ts +149 -74
  201. package/src/route-definition/index.ts +4 -2
  202. package/src/route-definition/redirect.ts +51 -10
  203. package/src/route-definition/resolve-handler-use.ts +160 -0
  204. package/src/route-definition/use-item-types.ts +29 -0
  205. package/src/route-map-builder.ts +0 -16
  206. package/src/route-types.ts +37 -46
  207. package/src/router/basename.ts +14 -0
  208. package/src/router/content-negotiation.ts +164 -17
  209. package/src/router/error-handling.ts +45 -18
  210. package/src/router/find-match.ts +44 -23
  211. package/src/router/handler-context.ts +83 -39
  212. package/src/router/instrument.ts +350 -0
  213. package/src/router/intercept-resolution.ts +48 -24
  214. package/src/router/lazy-includes.ts +15 -52
  215. package/src/router/loader-resolution.ts +274 -56
  216. package/src/router/logging.ts +0 -6
  217. package/src/router/manifest.ts +40 -42
  218. package/src/router/match-api.ts +124 -204
  219. package/src/router/match-context.ts +0 -22
  220. package/src/router/match-handlers.ts +58 -58
  221. package/src/router/match-middleware/background-revalidation.ts +54 -25
  222. package/src/router/match-middleware/cache-lookup.ts +205 -271
  223. package/src/router/match-middleware/cache-store.ts +81 -50
  224. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  225. package/src/router/match-middleware/segment-resolution.ts +45 -14
  226. package/src/router/match-pipelines.ts +1 -42
  227. package/src/router/match-result.ts +93 -39
  228. package/src/router/metrics.ts +5 -34
  229. package/src/router/middleware-types.ts +13 -142
  230. package/src/router/middleware.ts +268 -171
  231. package/src/router/navigation-snapshot.ts +131 -0
  232. package/src/router/params-util.ts +23 -0
  233. package/src/router/pattern-matching.ts +132 -90
  234. package/src/router/prefetch-cache-ttl.ts +51 -0
  235. package/src/router/prefetch-limits.ts +37 -0
  236. package/src/router/prerender-match.ts +195 -56
  237. package/src/router/preview-match.ts +32 -102
  238. package/src/router/request-classification.ts +276 -0
  239. package/src/router/revalidation.ts +123 -73
  240. package/src/router/route-snapshot.ts +244 -0
  241. package/src/router/router-context.ts +3 -27
  242. package/src/router/router-interfaces.ts +129 -35
  243. package/src/router/router-options.ts +202 -15
  244. package/src/router/router-registry.ts +2 -5
  245. package/src/router/segment-resolution/fresh.ts +197 -92
  246. package/src/router/segment-resolution/helpers.ts +115 -30
  247. package/src/router/segment-resolution/loader-cache.ts +76 -39
  248. package/src/router/segment-resolution/revalidation.ts +392 -338
  249. package/src/router/segment-resolution/static-store.ts +19 -5
  250. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  251. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  252. package/src/router/segment-resolution.ts +5 -1
  253. package/src/router/segment-wrappers.ts +6 -5
  254. package/src/router/state-cookie-name.ts +33 -0
  255. package/src/router/substitute-pattern-params.ts +56 -0
  256. package/src/router/telemetry-otel.ts +161 -199
  257. package/src/router/telemetry.ts +96 -19
  258. package/src/router/timeout.ts +0 -20
  259. package/src/router/tracing.ts +206 -0
  260. package/src/router/trie-matching.ts +163 -59
  261. package/src/router/types.ts +10 -63
  262. package/src/router/url-params.ts +44 -0
  263. package/src/router.ts +174 -54
  264. package/src/rsc/handler-context.ts +3 -2
  265. package/src/rsc/handler.ts +660 -518
  266. package/src/rsc/helpers.ts +168 -46
  267. package/src/rsc/index.ts +2 -5
  268. package/src/rsc/json-route-result.ts +38 -0
  269. package/src/rsc/loader-fetch.ts +127 -31
  270. package/src/rsc/manifest-init.ts +33 -42
  271. package/src/rsc/origin-guard.ts +39 -25
  272. package/src/rsc/progressive-enhancement.ts +133 -13
  273. package/src/rsc/redirect-guard.ts +99 -0
  274. package/src/rsc/response-cache-serve.ts +238 -0
  275. package/src/rsc/response-error.ts +79 -12
  276. package/src/rsc/response-route-handler.ts +99 -189
  277. package/src/rsc/rsc-rendering.ts +115 -73
  278. package/src/rsc/runtime-warnings.ts +23 -10
  279. package/src/rsc/server-action.ts +287 -113
  280. package/src/rsc/ssr-setup.ts +18 -2
  281. package/src/rsc/transition-gate.ts +89 -0
  282. package/src/rsc/types.ts +36 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +35 -30
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +269 -202
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +235 -51
  290. package/src/server/cookie-parse.ts +32 -0
  291. package/src/server/cookie-store.ts +80 -5
  292. package/src/server/handle-store.ts +40 -38
  293. package/src/server/loader-registry.ts +38 -46
  294. package/src/server/request-context.ts +442 -173
  295. package/src/ssr/index.tsx +24 -16
  296. package/src/static-handler.ts +27 -18
  297. package/src/testing/cache-status.ts +162 -0
  298. package/src/testing/collect-handle.ts +40 -0
  299. package/src/testing/dispatch.ts +701 -0
  300. package/src/testing/dom.entry.ts +22 -0
  301. package/src/testing/e2e/fixture.ts +188 -0
  302. package/src/testing/e2e/index.ts +128 -0
  303. package/src/testing/e2e/matchers.ts +35 -0
  304. package/src/testing/e2e/page-helpers.ts +272 -0
  305. package/src/testing/e2e/parity.ts +387 -0
  306. package/src/testing/e2e/server.ts +195 -0
  307. package/src/testing/flight-matchers.ts +97 -0
  308. package/src/testing/flight-normalize.ts +11 -0
  309. package/src/testing/flight-runtime.d.ts +57 -0
  310. package/src/testing/flight-tree.ts +682 -0
  311. package/src/testing/flight.entry.ts +52 -0
  312. package/src/testing/flight.ts +257 -0
  313. package/src/testing/generated-routes.ts +183 -0
  314. package/src/testing/index.ts +105 -0
  315. package/src/testing/internal/context.ts +371 -0
  316. package/src/testing/internal/flight-client-globals.ts +30 -0
  317. package/src/testing/internal/seed-vars.ts +54 -0
  318. package/src/testing/render-handler.ts +357 -0
  319. package/src/testing/render-route.tsx +581 -0
  320. package/src/testing/run-loader.ts +385 -0
  321. package/src/testing/run-middleware.ts +205 -0
  322. package/src/testing/run-transition-when.ts +164 -0
  323. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  324. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  325. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  326. package/src/testing/vitest-stubs/version.ts +5 -0
  327. package/src/testing/vitest.ts +305 -0
  328. package/src/theme/ThemeProvider.tsx +20 -58
  329. package/src/theme/ThemeScript.tsx +7 -9
  330. package/src/theme/constants.ts +52 -13
  331. package/src/theme/index.ts +0 -7
  332. package/src/theme/theme-context.ts +1 -5
  333. package/src/theme/theme-script.ts +22 -21
  334. package/src/theme/use-theme.ts +0 -3
  335. package/src/types/boundaries.ts +0 -35
  336. package/src/types/cache-types.ts +17 -8
  337. package/src/types/error-types.ts +30 -90
  338. package/src/types/global-namespace.ts +54 -41
  339. package/src/types/handler-context.ts +234 -82
  340. package/src/types/index.ts +3 -10
  341. package/src/types/loader-types.ts +44 -15
  342. package/src/types/request-scope.ts +112 -0
  343. package/src/types/route-config.ts +6 -50
  344. package/src/types/route-entry.ts +12 -7
  345. package/src/types/segments.ts +136 -15
  346. package/src/urls/include-helper.ts +33 -70
  347. package/src/urls/index.ts +1 -11
  348. package/src/urls/path-helper-types.ts +68 -18
  349. package/src/urls/path-helper.ts +57 -111
  350. package/src/urls/pattern-types.ts +48 -19
  351. package/src/urls/response-types.ts +25 -22
  352. package/src/urls/type-extraction.ts +58 -139
  353. package/src/urls/urls-function.ts +1 -19
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  357. package/src/vite/discovery/discover-routers.ts +130 -85
  358. package/src/vite/discovery/discovery-errors.ts +194 -0
  359. package/src/vite/discovery/gate-state.ts +171 -0
  360. package/src/vite/discovery/prerender-collection.ts +214 -132
  361. package/src/vite/discovery/route-types-writer.ts +40 -84
  362. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  363. package/src/vite/discovery/state.ts +57 -4
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +6 -0
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +178 -5
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  370. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  371. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  372. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  373. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  374. package/src/vite/plugins/expose-action-id.ts +48 -95
  375. package/src/vite/plugins/expose-id-utils.ts +96 -51
  376. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  377. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  378. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  379. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  380. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  381. package/src/vite/plugins/performance-tracks.ts +89 -0
  382. package/src/vite/plugins/refresh-cmd.ts +89 -27
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +40 -29
  385. package/src/vite/plugins/version-plugin.ts +37 -40
  386. package/src/vite/plugins/virtual-entries.ts +39 -25
  387. package/src/vite/rango.ts +119 -111
  388. package/src/vite/router-discovery.ts +941 -142
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +1 -1
  391. package/src/vite/utils/bundle-analysis.ts +10 -15
  392. package/src/vite/utils/client-chunks.ts +184 -0
  393. package/src/vite/utils/directive-prologue.ts +40 -0
  394. package/src/vite/utils/forward-user-plugins.ts +171 -0
  395. package/src/vite/utils/manifest-utils.ts +4 -59
  396. package/src/vite/utils/package-resolution.ts +20 -52
  397. package/src/vite/utils/prerender-utils.ts +81 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/src/browser/action-response-classifier.ts +0 -99
  400. package/src/browser/react/use-client-cache.ts +0 -58
  401. package/src/browser/shallow.ts +0 -40
  402. package/src/handles/index.ts +0 -7
  403. package/src/network-error-thrower.tsx +0 -23
  404. 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 {
@@ -20,21 +28,47 @@ import type {
20
28
  DefaultRouteName,
21
29
  } from "../types/global-namespace.js";
22
30
  import type { Handle } from "../handle.js";
23
- import { type ContextVar, contextGet, contextSet } from "../context-var.js";
24
- import { createHandleStore, type HandleStore } from "./handle-store.js";
31
+ import {
32
+ type ContextVar,
33
+ contextGet,
34
+ contextSet,
35
+ isNonCacheable,
36
+ } from "../context-var.js";
37
+ import {
38
+ createHandleStore,
39
+ buildHandleSnapshot,
40
+ type HandleStore,
41
+ type HandleData,
42
+ } from "./handle-store.js";
25
43
  import { isHandle } from "../handle.js";
26
- 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";
27
47
  import { getFetchableLoader } from "./fetchable-loader-store.js";
28
48
  import type { SegmentCacheStore } from "../cache/types.js";
29
49
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
30
- 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";
31
59
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
32
60
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
61
+ import { isInsideCacheScope } from "./context.js";
33
62
  import {
34
63
  createReverseFunction,
35
64
  stripInternalParams,
36
65
  } from "../router/handler-context.js";
37
- 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";
38
72
  import { invariant } from "../errors.js";
39
73
  import { isAutoGeneratedRouteName } from "../route-name.js";
40
74
 
@@ -47,24 +81,9 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
47
81
  export interface RequestContext<
48
82
  TEnv = DefaultEnv,
49
83
  TParams = Record<string, string>,
50
- > {
51
- /** Platform bindings (Cloudflare env, etc.) */
52
- env: TEnv;
53
- /** Original HTTP request */
54
- request: Request;
55
- /** Parsed URL (with internal `_rsc*` params stripped) */
56
- url: URL;
57
- /**
58
- * The original request URL with all parameters intact, including
59
- * internal `_rsc*` transport params.
60
- */
61
- originalUrl: URL;
62
- /** URL pathname */
63
- pathname: string;
64
- /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
65
- searchParams: URLSearchParams;
66
- /** Variables set by middleware (same as ctx.var) */
67
- var: Record<string, any>;
84
+ > extends RequestScope<TEnv> {
85
+ /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
86
+ _variables: Record<string, any>;
68
87
  /** Get a variable set by middleware */
69
88
  get: {
70
89
  <T>(contextVar: ContextVar<T>): T | undefined;
@@ -72,8 +91,12 @@ export interface RequestContext<
72
91
  };
73
92
  /** Set a variable (shared with middleware and handlers) */
74
93
  set: {
75
- <T>(contextVar: ContextVar<T>, value: T): void;
76
- <K extends string>(key: K, value: any): void;
94
+ <T>(
95
+ contextVar: ContextVar<T>,
96
+ value: T,
97
+ options?: { cache?: boolean },
98
+ ): void;
99
+ <K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
77
100
  };
78
101
  /**
79
102
  * Route params (populated after route matching)
@@ -100,6 +123,10 @@ export interface RequestContext<
100
123
  setStatus(status: number): void;
101
124
  /** @internal Set status bypassing cache-exec guard (for framework error handling) */
102
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;
103
130
 
104
131
  /**
105
132
  * Access loader data or push handle data.
@@ -135,29 +162,43 @@ export interface RequestContext<
135
162
  /** @internal Handle store for tracking handle data across segments */
136
163
  _handleStore: HandleStore;
137
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
+
138
174
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
139
175
  _cacheStore?: SegmentCacheStore;
140
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
+
141
196
  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
142
197
  _cacheProfiles?: Record<
143
198
  string,
144
199
  import("../cache/profile-registry.js").CacheProfile
145
200
  >;
146
201
 
147
- /**
148
- * Schedule work to run after the response is sent.
149
- * On Cloudflare Workers, uses ctx.waitUntil().
150
- * On Node.js, runs as fire-and-forget.
151
- *
152
- * @example
153
- * ```typescript
154
- * ctx.waitUntil(async () => {
155
- * await cacheStore.set(key, data, ttl);
156
- * });
157
- * ```
158
- */
159
- waitUntil(fn: () => Promise<void>): void;
160
-
161
202
  /**
162
203
  * Register a callback to run when the response is created.
163
204
  * Callbacks are sync and receive the response. They can:
@@ -261,6 +302,92 @@ export interface RequestContext<
261
302
  /** @internal Previous route key (from the navigation source), used for revalidation */
262
303
  _prevRouteKey?: string;
263
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
+
264
391
  /** @internal Per-request error dedup set for onError reporting */
265
392
  _reportedErrors: WeakSet<object>;
266
393
 
@@ -268,15 +395,40 @@ export interface RequestContext<
268
395
  * @internal Report a non-fatal background error through the router's
269
396
  * onError callback. Wired by the RSC handler / router during request
270
397
  * creation. Cache-runtime and other subsystems call this to surface
271
- * 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`).
272
400
  */
273
- _reportBackgroundError?: (error: unknown, category: string) => void;
401
+ _reportBackgroundError?: (
402
+ error: unknown,
403
+ category: CacheErrorCategory,
404
+ ) => void;
274
405
 
275
406
  /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
276
407
  _debugPerformance?: boolean;
277
408
 
278
409
  /** @internal Request-scoped performance metrics store */
279
410
  _metricsStore?: MetricsStore;
411
+
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[];
280
432
  }
281
433
 
282
434
  /**
@@ -295,18 +447,42 @@ export type PublicRequestContext<
295
447
  | "setCookie"
296
448
  | "deleteCookie"
297
449
  | "_handleStore"
450
+ | "_transitionWhen"
298
451
  | "_cacheStore"
452
+ | "_explicitTaggedStores"
453
+ | "_requestTags"
299
454
  | "_cacheProfiles"
300
455
  | "_onResponseCallbacks"
301
456
  | "_themeConfig"
302
457
  | "_locationState"
303
458
  | "_routeName"
304
459
  | "_prevRouteKey"
460
+ | "_gateCurrentUrl"
461
+ | "_gateCurrentParams"
462
+ | "_gateActionId"
463
+ | "_gateActionUrl"
464
+ | "_gateActionResult"
465
+ | "_gateFormData"
466
+ | "_inActionRevalidation"
305
467
  | "_reportedErrors"
468
+ | "_renderBarrier"
469
+ | "_resolveRenderBarrier"
470
+ | "_renderBarrierSegmentOrder"
471
+ | "_treeHasStreaming"
472
+ | "_renderBarrierWaiters"
473
+ | "_handlerLoaderDeps"
474
+ | "_renderBarrierHandleSnapshot"
475
+ | "_renderBarrierGuardClosed"
306
476
  | "_reportBackgroundError"
307
477
  | "_debugPerformance"
308
478
  | "_metricsStore"
479
+ | "_basename"
309
480
  | "_setStatus"
481
+ | "_rotateStateCookie"
482
+ | "_setKeepCacheDirective"
483
+ | "_variables"
484
+ | "_classifiedRoute"
485
+ | "_cacheSignal"
310
486
  | "res"
311
487
  >;
312
488
 
@@ -358,6 +534,7 @@ export function _getRequestContext<TEnv = DefaultEnv>():
358
534
  export function setRequestContextParams(
359
535
  params: Record<string, string>,
360
536
  routeName?: string,
537
+ routeMap?: Record<string, string>,
361
538
  ): void {
362
539
  const ctx = requestContextStorage.getStore();
363
540
  if (ctx) {
@@ -370,9 +547,13 @@ export function setRequestContextParams(
370
547
  : undefined
371
548
  ) as DefaultRouteName | undefined;
372
549
  }
373
- // 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.
374
555
  ctx.reverse = createReverseFunction(
375
- getGlobalRouteMap(),
556
+ routeMap ?? getGlobalRouteMap(),
376
557
  routeName,
377
558
  params,
378
559
  routeName ? isRouteRootScoped(routeName) : undefined,
@@ -388,11 +569,17 @@ export function setRequestContextParams(
388
569
  */
389
570
  export function setRequestContextPrevRouteKey(
390
571
  prevRouteKey: string | undefined,
572
+ currentUrl?: URL,
573
+ currentParams?: Record<string, string>,
391
574
  ): void {
392
575
  const ctx = requestContextStorage.getStore();
393
- if (ctx && prevRouteKey !== undefined) {
394
- ctx._prevRouteKey = prevRouteKey;
395
- }
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;
396
583
  }
397
584
 
398
585
  /**
@@ -406,23 +593,7 @@ export function getLocationState(): LocationStateEntry[] | undefined {
406
593
  return ctx?._locationState;
407
594
  }
408
595
 
409
- /**
410
- * Get the current request context, throwing if not available
411
- * @deprecated Use getRequestContext() directly — it now throws if outside context
412
- */
413
- export function requireRequestContext<
414
- TEnv = DefaultEnv,
415
- >(): RequestContext<TEnv> {
416
- return getRequestContext<TEnv>();
417
- }
418
-
419
- /**
420
- * Cloudflare Workers ExecutionContext (subset we need)
421
- */
422
- export interface ExecutionContext {
423
- waitUntil(promise: Promise<any>): void;
424
- passThroughOnException(): void;
425
- }
596
+ export type { ExecutionContext };
426
597
 
427
598
  /**
428
599
  * Options for creating a request context
@@ -436,6 +607,11 @@ export interface CreateRequestContextOptions<TEnv> {
436
607
  initialResponse?: Response;
437
608
  /** Optional cache store for segment caching (used by CacheScope) */
438
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>;
439
615
  /** Optional cache profiles for "use cache" resolution (per-router) */
440
616
  cacheProfiles?: Record<
441
617
  string,
@@ -445,6 +621,10 @@ export interface CreateRequestContextOptions<TEnv> {
445
621
  executionContext?: ExecutionContext;
446
622
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
447
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;
448
628
  }
449
629
 
450
630
  /**
@@ -465,15 +645,17 @@ export function createRequestContext<TEnv>(
465
645
  variables,
466
646
  initialResponse,
467
647
  cacheStore,
648
+ explicitTaggedStores,
468
649
  cacheProfiles,
469
650
  executionContext,
470
651
  themeConfig,
652
+ stateCookieName,
653
+ version: stateVersion,
471
654
  } = options;
472
655
  const cookieHeader = request.headers.get("Cookie");
656
+ let rangoStateRotated = false;
473
657
  let parsedCookies: Record<string, string> | null = null;
474
658
 
475
- // Create stub response for collecting headers/cookies.
476
- // All cookie/header mutations go here; cookie reads derive from it.
477
659
  let stubResponse = initialResponse
478
660
  ? new Response(null, {
479
661
  status: initialResponse.status,
@@ -482,11 +664,9 @@ export function createRequestContext<TEnv>(
482
664
  })
483
665
  : new Response(null, { status: 200 });
484
666
 
485
- // Create handle store and loader memoization for this request
486
667
  const handleStore = createHandleStore();
487
668
  const loaderPromises = new Map<string, Promise<any>>();
488
669
 
489
- // Lazy parse cookies from the original Cookie header
490
670
  const getParsedCookies = (): Record<string, string> => {
491
671
  if (!parsedCookies) {
492
672
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -494,7 +674,6 @@ export function createRequestContext<TEnv>(
494
674
  return parsedCookies;
495
675
  };
496
676
 
497
- // Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
498
677
  let responseCookieCache: Map<string, string | null> | null = null;
499
678
  const getResponseCookies = (): Map<string, string | null> => {
500
679
  if (!responseCookieCache) {
@@ -506,8 +685,17 @@ export function createRequestContext<TEnv>(
506
685
  responseCookieCache = null;
507
686
  };
508
687
 
509
- // Effective cookie read: response stub Set-Cookie wins, then original header.
510
- // The stub IS the source of truth for same-request mutations.
688
+ function assertNotInsideCacheScopeALS(methodName: string): void {
689
+ if (isInsideCacheScope()) {
690
+ throw new Error(
691
+ `ctx.${methodName}() cannot be called inside a cache() boundary. ` +
692
+ `On cache hit the handler is skipped, so this side effect would be lost. ` +
693
+ `Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
694
+ );
695
+ }
696
+ }
697
+
698
+ // Response stub Set-Cookie wins, then original header (source of truth for mutations).
511
699
  const effectiveCookie = (name: string): string | undefined => {
512
700
  const mutations = getResponseCookies();
513
701
  if (mutations.has(name)) {
@@ -517,14 +705,11 @@ export function createRequestContext<TEnv>(
517
705
  return getParsedCookies()[name];
518
706
  };
519
707
 
520
- // Theme helpers (only used when themeConfig is provided)
521
708
  const getTheme = (): Theme | undefined => {
522
709
  if (!themeConfig) return undefined;
523
710
 
524
- // Use overlay-aware read so setTheme() in the same request is reflected
525
711
  const stored = effectiveCookie(themeConfig.storageKey);
526
712
  if (stored) {
527
- // Validate stored value
528
713
  if (stored === "system" && themeConfig.enableSystem) {
529
714
  return "system";
530
715
  }
@@ -538,15 +723,15 @@ export function createRequestContext<TEnv>(
538
723
  const setTheme = (theme: Theme): void => {
539
724
  if (!themeConfig) return;
540
725
 
541
- // Validate theme value
542
- if (theme !== "system" && !themeConfig.themes.includes(theme)) {
543
- console.warn(
544
- `[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
545
- );
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);
546
732
  return;
547
733
  }
548
734
 
549
- // Write to stub — effectiveCookie() will pick it up on next read
550
735
  stubResponse.headers.append(
551
736
  "Set-Cookie",
552
737
  serializeCookieValue(themeConfig.storageKey, theme, {
@@ -558,10 +743,8 @@ export function createRequestContext<TEnv>(
558
743
  invalidateResponseCookieCache();
559
744
  };
560
745
 
561
- // Strip internal _rsc* params so userland sees a clean URL.
562
746
  const cleanUrl = stripInternalParams(url);
563
747
 
564
- // Build the context object first (without use), then add use
565
748
  const ctx: RequestContext<TEnv> = {
566
749
  env,
567
750
  request,
@@ -569,12 +752,20 @@ export function createRequestContext<TEnv>(
569
752
  originalUrl: new URL(request.url),
570
753
  pathname: url.pathname,
571
754
  searchParams: cleanUrl.searchParams,
572
- var: variables,
573
- get: ((keyOrVar: any) =>
574
- contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
575
- set: ((keyOrVar: any, value: any) => {
755
+ _variables: variables,
756
+ get: ((keyOrVar: any) => {
757
+ if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
758
+ throw new Error(
759
+ `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
760
+ `The variable was created with { cache: false } or set with { cache: false }, ` +
761
+ `and its value would be stale on cache hit. Move the read outside the cached scope.`,
762
+ );
763
+ }
764
+ return contextGet(variables, keyOrVar);
765
+ }) as RequestContext<TEnv>["get"],
766
+ set: ((keyOrVar: any, value: any, options?: any) => {
576
767
  assertNotInsideCacheExec(ctx, "set");
577
- contextSet(variables, keyOrVar, value);
768
+ contextSet(variables, keyOrVar, value, options);
578
769
  }) as RequestContext<TEnv>["set"],
579
770
  params: {} as Record<string, string>,
580
771
 
@@ -612,6 +803,7 @@ export function createRequestContext<TEnv>(
612
803
 
613
804
  setCookie(name: string, value: string, options?: CookieOptions): void {
614
805
  assertNotInsideCacheExec(ctx, "setCookie");
806
+ assertNotInsideCacheScopeALS("setCookie");
615
807
  stubResponse.headers.append(
616
808
  "Set-Cookie",
617
809
  serializeCookieValue(name, value, options),
@@ -624,6 +816,7 @@ export function createRequestContext<TEnv>(
624
816
  options?: Pick<CookieOptions, "domain" | "path">,
625
817
  ): void {
626
818
  assertNotInsideCacheExec(ctx, "deleteCookie");
819
+ assertNotInsideCacheScopeALS("deleteCookie");
627
820
  stubResponse.headers.append(
628
821
  "Set-Cookie",
629
822
  serializeCookieValue(name, "", { ...options, maxAge: 0 }),
@@ -633,11 +826,52 @@ export function createRequestContext<TEnv>(
633
826
 
634
827
  header(name: string, value: string): void {
635
828
  assertNotInsideCacheExec(ctx, "header");
829
+ assertNotInsideCacheScopeALS("header");
636
830
  stubResponse.headers.set(name, value);
637
831
  },
638
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
+
639
872
  setStatus(status: number): void {
640
873
  assertNotInsideCacheExec(ctx, "setStatus");
874
+ assertNotInsideCacheScopeALS("setStatus");
641
875
  stubResponse = new Response(null, {
642
876
  status,
643
877
  headers: stubResponse.headers,
@@ -651,35 +885,39 @@ export function createRequestContext<TEnv>(
651
885
  });
652
886
  },
653
887
 
654
- // Placeholder - will be replaced below
655
888
  use: null as any,
656
889
 
657
890
  method: request.method,
658
891
 
659
892
  _handleStore: handleStore,
893
+ _transitionWhen: [],
660
894
  _cacheStore: cacheStore,
895
+ _explicitTaggedStores: explicitTaggedStores,
896
+ _requestTags: new Set<string>(),
661
897
  _cacheProfiles: cacheProfiles,
662
898
 
663
899
  waitUntil(fn: () => Promise<void>): void {
664
900
  if (executionContext?.waitUntil) {
665
- // Cloudflare Workers: use native waitUntil
666
- 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));
667
906
  } else {
668
- // Node.js / dev: fire-and-forget with error logging
669
- fn().catch((err) =>
670
- console.error("[waitUntil] Background task failed:", err),
671
- );
907
+ fireAndForgetWaitUntil(fn);
672
908
  }
673
909
  },
674
910
 
911
+ executionContext,
912
+
675
913
  _onResponseCallbacks: [],
676
914
 
677
915
  onResponse(callback: (response: Response) => Response): void {
678
916
  assertNotInsideCacheExec(ctx, "onResponse");
917
+ assertNotInsideCacheScopeALS("onResponse");
679
918
  this._onResponseCallbacks.push(callback);
680
919
  },
681
920
 
682
- // Theme properties (only set when themeConfig is provided)
683
921
  get theme() {
684
922
  return themeConfig ? getTheme() : undefined;
685
923
  },
@@ -703,35 +941,88 @@ export function createRequestContext<TEnv>(
703
941
  _reportedErrors: new WeakSet<object>(),
704
942
  _metricsStore: undefined,
705
943
 
944
+ _renderBarrier: null as any,
945
+ _resolveRenderBarrier: null as any,
946
+ _renderBarrierSegmentOrder: undefined,
947
+
706
948
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
707
949
  };
708
950
 
709
- // 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
+
710
999
  ctx.use = createUseFunction({
711
1000
  handleStore,
712
1001
  loaderPromises,
713
1002
  getContext: () => ctx,
714
1003
  });
715
1004
 
716
- // Brand with taint symbol so "use cache" excludes ctx from cache keys
717
1005
  (ctx as any)[NOCACHE_SYMBOL] = true;
718
1006
  return ctx;
719
1007
  }
720
1008
 
721
- /**
722
- * Parse Set-Cookie headers from a response into effective cookie state.
723
- * Returns a map of cookie name -> value (string) or name -> null (deleted).
724
- * Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
725
- * Max-Age=0 is treated as a delete.
726
- */
727
- 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
+ }
728
1020
 
729
1021
  function parseResponseCookies(response: Response): Map<string, string | null> {
730
1022
  const result = new Map<string, string | null>();
731
1023
  const setCookies = response.headers.getSetCookie();
732
1024
 
733
1025
  for (const header of setCookies) {
734
- // First segment before ';' is the name=value pair
735
1026
  const semiIdx = header.indexOf(";");
736
1027
  const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
737
1028
  const eqIdx = pair.indexOf("=");
@@ -743,49 +1034,22 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
743
1034
  name = decodeURIComponent(pair.substring(0, eqIdx).trim());
744
1035
  value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
745
1036
  } catch {
746
- // Malformed encoding — skip this entry
747
1037
  continue;
748
1038
  }
749
1039
 
750
- // Max-Age=0 means the cookie is being deleted
751
- const isDeleted = MAX_AGE_ZERO_RE.test(header);
1040
+ const isDeleted = isCookieDeletion(header);
752
1041
  result.set(name, isDeleted ? null : value);
753
1042
  }
754
1043
 
755
1044
  return result;
756
1045
  }
757
1046
 
758
- /**
759
- * Parse cookies from Cookie header
760
- */
761
- function parseCookiesFromHeader(
762
- cookieHeader: string | null,
763
- ): Record<string, string> {
764
- if (!cookieHeader) return {};
765
-
766
- const cookies: Record<string, string> = {};
767
- const pairs = cookieHeader.split(";");
768
-
769
- for (const pair of pairs) {
770
- const [name, ...rest] = pair.trim().split("=");
771
- if (name) {
772
- const raw = rest.join("=");
773
- try {
774
- cookies[name] = decodeURIComponent(raw);
775
- } catch {
776
- // Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
777
- cookies[name] = raw;
778
- }
779
- }
780
- }
781
-
782
- return cookies;
783
- }
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 };
784
1051
 
785
- /**
786
- * Serialize a cookie for Set-Cookie header
787
- */
788
- function serializeCookieValue(
1052
+ export function serializeCookieValue(
789
1053
  name: string,
790
1054
  value: string,
791
1055
  options: CookieOptions = {},
@@ -812,20 +1076,12 @@ export interface CreateUseFunctionOptions<TEnv> {
812
1076
  getContext: () => RequestContext<TEnv>;
813
1077
  }
814
1078
 
815
- /**
816
- * Create the use() function for loader and handle composition.
817
- *
818
- * This is the unified implementation used by both RequestContext and HandlerContext.
819
- * - For loaders: executes and memoizes loader functions
820
- * - For handles: returns a push function to add handle data
821
- */
822
1079
  export function createUseFunction<TEnv>(
823
1080
  options: CreateUseFunctionOptions<TEnv>,
824
1081
  ): RequestContext["use"] {
825
1082
  const { handleStore, loaderPromises, getContext } = options;
826
1083
 
827
1084
  return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
828
- // Handle case: return a push function
829
1085
  if (isHandle(item)) {
830
1086
  const handle = item;
831
1087
  const ctx = getContext();
@@ -838,30 +1094,24 @@ export function createUseFunction<TEnv>(
838
1094
  );
839
1095
  }
840
1096
 
841
- // Return a push function bound to this handle and segment
842
- return (
843
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
844
- ) => {
845
- // If it's a function, call it immediately to get the promise
846
- const valueOrPromise =
847
- typeof dataOrFn === "function"
848
- ? (dataOrFn as () => Promise<unknown>)()
849
- : dataOrFn;
850
-
851
- // Push directly - promises will be serialized by RSC and streamed
852
- handleStore.push(handle.$$id, segmentId, valueOrPromise);
853
- };
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
+ );
854
1107
  }
855
1108
 
856
- // Loader case
857
1109
  const loader = item as LoaderDefinition<any, any>;
858
1110
 
859
- // Return cached promise if already started
860
1111
  if (loaderPromises.has(loader.$$id)) {
861
1112
  return loaderPromises.get(loader.$$id);
862
1113
  }
863
1114
 
864
- // Get loader function - either from loader object or fetchable registry
865
1115
  let loaderFn = loader.fn;
866
1116
  if (!loaderFn) {
867
1117
  const fetchable = getFetchableLoader(loader.$$id);
@@ -878,24 +1128,36 @@ export function createUseFunction<TEnv>(
878
1128
 
879
1129
  const ctx = getContext();
880
1130
 
881
- // 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
+
882
1143
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
883
1144
  params: ctx.params,
884
1145
  routeParams: (ctx.params ?? {}) as Record<string, string>,
885
1146
  request: ctx.request,
886
1147
  searchParams: ctx.searchParams,
887
- search: (ctx as any).search ?? {},
1148
+ search: loaderSearch,
888
1149
  pathname: ctx.pathname,
889
1150
  url: ctx.url,
1151
+ originalUrl: ctx.originalUrl,
890
1152
  env: ctx.env as any,
891
- var: ctx.var as any,
1153
+ waitUntil: ctx.waitUntil.bind(ctx),
1154
+ executionContext: ctx.executionContext,
892
1155
  get: ctx.get as any,
893
- use: <TDep, TDepParams = any>(
1156
+ use: (<TDep, TDepParams = any>(
894
1157
  dep: LoaderDefinition<TDep, TDepParams>,
895
1158
  ): Promise<TDep> => {
896
- // Recursive call - will start dep loader if not already started
897
1159
  return ctx.use(dep);
898
- },
1160
+ }) as LoaderContext["use"],
899
1161
  method: "GET",
900
1162
  body: undefined,
901
1163
  reverse: createReverseFunction(
@@ -904,15 +1166,22 @@ export function createUseFunction<TEnv>(
904
1166
  ctx.params as Record<string, string>,
905
1167
  ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
906
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
+ },
907
1175
  };
908
1176
 
909
- // Start loader execution with tracking
910
- const doneLoader = track(`loader:${loader.$$id}`, 2);
911
- const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
912
- doneLoader();
913
- });
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
+ );
914
1184
 
915
- // Memoize for subsequent calls
916
1185
  loaderPromises.set(loader.$$id, promise);
917
1186
 
918
1187
  return promise;