@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1c0bdfad

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