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