@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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