@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. 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,13 +41,20 @@ 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
50
  import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
41
- import { fireAndForgetWaitUntil } from "../types/request-scope.js";
42
- import { THEME_COOKIE } from "../theme/constants.js";
51
+ import type { TransitionWhenFn } from "../types/segments.js";
52
+ import type { ResolvedTracing } from "../router/tracing.js";
53
+ import {
54
+ THEME_COOKIE,
55
+ isValidTheme,
56
+ warnInvalidTheme,
57
+ } from "../theme/constants.js";
43
58
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
44
59
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
45
60
  import { isInsideCacheScope } from "./context.js";
@@ -47,7 +62,12 @@ import {
47
62
  createReverseFunction,
48
63
  stripInternalParams,
49
64
  } from "../router/handler-context.js";
50
- import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
65
+ import {
66
+ getGlobalRouteMap,
67
+ isRouteRootScoped,
68
+ getSearchSchema,
69
+ } from "../route-map-builder.js";
70
+ import { parseSearchParams } from "../search-params.js";
51
71
  import { invariant } from "../errors.js";
52
72
  import { isAutoGeneratedRouteName } from "../route-name.js";
53
73
 
@@ -102,6 +122,10 @@ export interface RequestContext<
102
122
  setStatus(status: number): void;
103
123
  /** @internal Set status bypassing cache-exec guard (for framework error handling) */
104
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;
105
129
 
106
130
  /**
107
131
  * Access loader data or push handle data.
@@ -137,9 +161,94 @@ export interface RequestContext<
137
161
  /** @internal Handle store for tracking handle data across segments */
138
162
  _handleStore: HandleStore;
139
163
 
164
+ /**
165
+ * @internal transition({ when }) predicates for segments matched this request,
166
+ * keyed by segment id. Collected during resolution (the function is stripped
167
+ * from the serialized segment config), then evaluated post-handler in
168
+ * rsc-rendering — outside any cache scope — to drop the transition of any
169
+ * segment whose predicate returns false.
170
+ */
171
+ _transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;
172
+
140
173
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
141
174
  _cacheStore?: SegmentCacheStore;
142
175
 
176
+ /**
177
+ * @internal PPR shell-capture ACTIVE marker. True ONLY inside the background
178
+ * capture task's derived request context (built by shell-capture.ts). This is
179
+ * the switch every capture-specific behavior reads: loader masking
180
+ * (loader-mask.ts isShellCaptureActive / fresh.ts emitStreaming) and the
181
+ * cookies()/headers() capture guard (cookie-store.ts
182
+ * assertNotInsideShellCapture). The foreground render never sets it, so the
183
+ * served response is byte-identical to axis 1. The capture descriptor itself
184
+ * (key/ttl/swr/tags/store) is NOT threaded through the request context — the
185
+ * integrated PPR serve path (rsc/shell-serve.ts + rsc-rendering.ts) builds it
186
+ * locally and passes it to scheduleShellCapture directly.
187
+ */
188
+ _shellCaptureRun?: boolean;
189
+
190
+ /**
191
+ * @internal Bake-lane loader containers collected DURING a shell capture:
192
+ * segment-key -> the loader's (pre-wrap) result promise. Populated by
193
+ * resolveLoaderData for loaders on entries with no renderable loading() (the
194
+ * bake lane — they execute at capture instead of being masked; see
195
+ * docs/design/loader-container-bake.md). Drained by captureAndStoreShell
196
+ * after the shell quiesces: settled containers are promise-elided,
197
+ * Flight-serialized, and pinned into the snapshot's loader family; a
198
+ * REJECTED container refuses the capture (error UI must never bake into the
199
+ * shared shell). Own property of the capture's derived context only.
200
+ */
201
+ _shellCaptureLoaderRecords?: Map<string, Promise<unknown>>;
202
+
203
+ /**
204
+ * @internal Loader-family snapshot seed for a shell HIT's tail render:
205
+ * segment-key -> the capture's elided container (already Flight-deserialized
206
+ * by serveShellHit). resolveLoaderData overlays it onto the fresh run's
207
+ * container (recorded paths pinned, hole-marker paths keep the fresh nested
208
+ * promises) so the payload's baked bytes match the frozen prelude. Own
209
+ * property of the HIT tail's derived context only.
210
+ */
211
+ _shellLoaderSeed?: Map<string, unknown>;
212
+
213
+ /**
214
+ * @internal Set (to the offending fn name) by the cookies()/headers()
215
+ * capture guard when it throws DURING a capture render. Load-bearing for the
216
+ * bake lane: a guard throw inside an executing loader is swallowed by
217
+ * wrapLoaderPromise into per-loader error UI, which would otherwise bake
218
+ * silently into the shared shell — the capture checks this flag after the
219
+ * render and refuses instead. Deterministic, so the capture does not retry.
220
+ */
221
+ _shellCaptureGuardTripped?: string;
222
+
223
+ /**
224
+ * @internal The loader $$id whose BODY was executing when the capture guard
225
+ * tripped (read off the loader-body ALS scope at trip time), or undefined
226
+ * when the read came from handler/render code. Only used to make the
227
+ * once-per-key refusal warning name the real source — the old text
228
+ * hardcoded "a bake-lane loader", which misattributed handler-land reads
229
+ * and sent users debugging the wrong lane (issue #672, secondary).
230
+ */
231
+ _shellCaptureGuardTrippedLoaderId?: string;
232
+
233
+ /**
234
+ * @internal Handler-owned registry of explicit per-scope stores from
235
+ * cache({ store }). Created once per createRSCHandler() and threaded into
236
+ * every request context, so it accumulates every explicit store the handler
237
+ * resolves. updateTag()/revalidateTag() iterate this set plus _cacheStore to
238
+ * reach every store that may hold tagged entries. The app-level store is not
239
+ * added here (it is always reachable via _cacheStore).
240
+ */
241
+ _explicitTaggedStores?: Set<SegmentCacheStore>;
242
+
243
+ /**
244
+ * @internal Union of every cache tag resolved while producing this request's
245
+ * response (from cache({ tags }), runtime cacheTag(), and loader cache tags).
246
+ * Populated at the tag-resolution sites via recordRequestTags(). Read by the
247
+ * document cache middleware so a full-page entry is tagged with everything its
248
+ * content used and can therefore be invalidated by updateTag()/revalidateTag().
249
+ */
250
+ _requestTags: Set<string>;
251
+
143
252
  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
144
253
  _cacheProfiles?: Record<
145
254
  string,
@@ -168,6 +277,19 @@ export interface RequestContext<
168
277
  /** @internal Registered onResponse callbacks */
169
278
  _onResponseCallbacks: Array<(response: Response) => Response>;
170
279
 
280
+ /**
281
+ * @internal Promises of the background tasks scheduled via this context's
282
+ * waitUntil (deferred cache writes, revalidations, consumer tasks). The PPR
283
+ * shell capture drains this list BEFORE its match/render as an ORDERING EDGE:
284
+ * every foreground deferred cache write is scheduled here before the capture
285
+ * task is, so settling the list first guarantees the capture's cache reads
286
+ * observe the foreground's generation instead of racing it (see
287
+ * shell-capture.ts). Tasks whose scheduling fn carries
288
+ * UNTRACKED_BACKGROUND_TASK are not tracked (the capture task itself —
289
+ * tracking it would make that drain await its own promise).
290
+ */
291
+ _pendingBackgroundTasks?: Array<Promise<unknown>>;
292
+
171
293
  /**
172
294
  * Current theme setting (only available when theme is enabled in router config)
173
295
  *
@@ -249,6 +371,30 @@ export interface RequestContext<
249
371
  /** @internal Previous route key (from the navigation source), used for revalidation */
250
372
  _prevRouteKey?: string;
251
373
 
374
+ /**
375
+ * @internal Navigation/action source data the transition({ when }) gate reads
376
+ * to build its ShouldRevalidateFn-shaped predicate context. currentUrl/Params
377
+ * come from the navigation snapshot (set at match time); action* are stashed
378
+ * at the action-bearing gate call sites. All undefined when there is no source
379
+ * (initial full load) or no action (plain navigation).
380
+ */
381
+ _gateCurrentUrl?: URL;
382
+ _gateCurrentParams?: Record<string, string>;
383
+ _gateActionId?: string;
384
+ _gateActionUrl?: URL;
385
+ _gateActionResult?: unknown;
386
+ _gateFormData?: FormData;
387
+
388
+ /**
389
+ * @internal True while the post-action revalidation render is running (set by
390
+ * revalidateAfterAction). The "use cache" runtime reads this to prefer
391
+ * freshness over a fast stale response during an action: a stale entry
392
+ * re-executes in the foreground (so the action response reflects the refreshed
393
+ * value) with only the store write deferred, instead of serving stale and
394
+ * revalidating in the background. A plain navigation (flag unset) keeps SWR.
395
+ */
396
+ _inActionRevalidation?: boolean;
397
+
252
398
  /**
253
399
  * @internal Render barrier for experimental `rendered()` API.
254
400
  * Resolves when all non-loader segments have settled and handle data
@@ -273,7 +419,9 @@ export interface RequestContext<
273
419
 
274
420
  /**
275
421
  * @internal Set to true when the matched entry tree contains any `loading()`
276
- * entries (streaming). Used by rendered() to fail fast.
422
+ * entries (streaming). On a streaming tree rendered() waits for the streaming
423
+ * handlers to settle (via handleStore.settled) before resolving, and the
424
+ * deadlock guard state is kept live until that wait completes.
277
425
  */
278
426
  _treeHasStreaming?: boolean;
279
427
 
@@ -297,6 +445,18 @@ export interface RequestContext<
297
445
  */
298
446
  _renderBarrierHandleSnapshot?: HandleData;
299
447
 
448
+ /**
449
+ * @internal The deadlock guard window is closed (no further handler-awaits-
450
+ * loader cycle is possible). For non-streaming trees this is set when the
451
+ * barrier resolves. For streaming trees the window stays open until
452
+ * handleStore.settled — rendered() keeps waiting past the barrier and a
453
+ * loading() handler can still resume and await a still-waiting loader — so it
454
+ * is set only after settled. The guard (loader-resolution `setupLoaderAccess`)
455
+ * reads this instead of `_renderBarrierSegmentOrder` so it does not go blind
456
+ * during the streaming settle wait.
457
+ */
458
+ _renderBarrierGuardClosed?: boolean;
459
+
300
460
  /** @internal Per-request error dedup set for onError reporting */
301
461
  _reportedErrors: WeakSet<object>;
302
462
 
@@ -304,9 +464,13 @@ export interface RequestContext<
304
464
  * @internal Report a non-fatal background error through the router's
305
465
  * onError callback. Wired by the RSC handler / router during request
306
466
  * creation. Cache-runtime and other subsystems call this to surface
307
- * errors without failing the response.
467
+ * errors without failing the response. `category` is surfaced to consumers as
468
+ * `metadata.category` on the onError context (phase `cache`).
308
469
  */
309
- _reportBackgroundError?: (error: unknown, category: string) => void;
470
+ _reportBackgroundError?: (
471
+ error: unknown,
472
+ category: CacheErrorCategory,
473
+ ) => void;
310
474
 
311
475
  /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
312
476
  _debugPerformance?: boolean;
@@ -314,6 +478,18 @@ export interface RequestContext<
314
478
  /** @internal Request-scoped performance metrics store */
315
479
  _metricsStore?: MetricsStore;
316
480
 
481
+ /**
482
+ * @internal True request entry timestamp (performance.now() at handler entry).
483
+ * Set once at request-context creation (rsc/handler.ts) so a metrics store
484
+ * created MID-request — ctx.debugPerformance() or the getMetricsStore wrapper —
485
+ * anchors its timeline to the real request start instead of the opt-in moment,
486
+ * keeping phases that began before the opt-in at their true (non-negative) offset.
487
+ */
488
+ _handlerStart?: number;
489
+
490
+ /** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
491
+ _tracing?: ResolvedTracing;
492
+
317
493
  /** @internal Router basename for this request (used by redirect()) */
318
494
  _basename?: string;
319
495
 
@@ -349,13 +525,25 @@ export type PublicRequestContext<
349
525
  | "setCookie"
350
526
  | "deleteCookie"
351
527
  | "_handleStore"
528
+ | "_transitionWhen"
352
529
  | "_cacheStore"
530
+ | "_shellCaptureRun"
531
+ | "_shellCaptureGuardTrippedLoaderId"
532
+ | "_explicitTaggedStores"
533
+ | "_requestTags"
353
534
  | "_cacheProfiles"
354
535
  | "_onResponseCallbacks"
355
536
  | "_themeConfig"
356
537
  | "_locationState"
357
538
  | "_routeName"
358
539
  | "_prevRouteKey"
540
+ | "_gateCurrentUrl"
541
+ | "_gateCurrentParams"
542
+ | "_gateActionId"
543
+ | "_gateActionUrl"
544
+ | "_gateActionResult"
545
+ | "_gateFormData"
546
+ | "_inActionRevalidation"
359
547
  | "_reportedErrors"
360
548
  | "_renderBarrier"
361
549
  | "_resolveRenderBarrier"
@@ -364,17 +552,32 @@ export type PublicRequestContext<
364
552
  | "_renderBarrierWaiters"
365
553
  | "_handlerLoaderDeps"
366
554
  | "_renderBarrierHandleSnapshot"
555
+ | "_renderBarrierGuardClosed"
367
556
  | "_reportBackgroundError"
368
557
  | "_debugPerformance"
369
558
  | "_metricsStore"
559
+ | "_handlerStart"
370
560
  | "_basename"
371
561
  | "_setStatus"
562
+ | "_rotateStateCookie"
563
+ | "_setKeepCacheDirective"
372
564
  | "_variables"
373
565
  | "_classifiedRoute"
374
566
  | "_cacheSignal"
375
567
  | "res"
376
568
  >;
377
569
 
570
+ /**
571
+ * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
572
+ * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
573
+ * the capture's pre-render write barrier settles that list, so tracking the
574
+ * capture itself would make the drain wait on its own (still-running) promise.
575
+ * @internal
576
+ */
577
+ export const UNTRACKED_BACKGROUND_TASK: unique symbol = Symbol.for(
578
+ "rango.untrackedBackgroundTask",
579
+ );
580
+
378
581
  // AsyncLocalStorage instance for request context
379
582
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
380
583
 
@@ -423,6 +626,7 @@ export function _getRequestContext<TEnv = DefaultEnv>():
423
626
  export function setRequestContextParams(
424
627
  params: Record<string, string>,
425
628
  routeName?: string,
629
+ routeMap?: Record<string, string>,
426
630
  ): void {
427
631
  const ctx = requestContextStorage.getStore();
428
632
  if (ctx) {
@@ -435,9 +639,13 @@ export function setRequestContextParams(
435
639
  : undefined
436
640
  ) as DefaultRouteName | undefined;
437
641
  }
438
- // Update reverse with scoped resolution now that route is known
642
+ // Update reverse with scoped resolution now that route is known. Production
643
+ // omits routeMap and uses the global map (routes are registered globally);
644
+ // the testing primitives (renderToFlightString/renderServerTree) pass a
645
+ // scoped routeMap so `ctx.reverse` is not order-dependent on whatever router
646
+ // registered last.
439
647
  ctx.reverse = createReverseFunction(
440
- getGlobalRouteMap(),
648
+ routeMap ?? getGlobalRouteMap(),
441
649
  routeName,
442
650
  params,
443
651
  routeName ? isRouteRootScoped(routeName) : undefined,
@@ -453,11 +661,17 @@ export function setRequestContextParams(
453
661
  */
454
662
  export function setRequestContextPrevRouteKey(
455
663
  prevRouteKey: string | undefined,
664
+ currentUrl?: URL,
665
+ currentParams?: Record<string, string>,
456
666
  ): void {
457
667
  const ctx = requestContextStorage.getStore();
458
- if (ctx && prevRouteKey !== undefined) {
459
- ctx._prevRouteKey = prevRouteKey;
460
- }
668
+ if (!ctx) return;
669
+ if (prevRouteKey !== undefined) ctx._prevRouteKey = prevRouteKey;
670
+ // Source URL/params for the transition({ when }) gate (effectiveFromUrl /
671
+ // effectiveFromMatch.params from the navigation snapshot). Same write point as
672
+ // _prevRouteKey, which doubles as fromRouteName.
673
+ if (currentUrl !== undefined) ctx._gateCurrentUrl = currentUrl;
674
+ if (currentParams !== undefined) ctx._gateCurrentParams = currentParams;
461
675
  }
462
676
 
463
677
  /**
@@ -471,16 +685,6 @@ export function getLocationState(): LocationStateEntry[] | undefined {
471
685
  return ctx?._locationState;
472
686
  }
473
687
 
474
- /**
475
- * Get the current request context, throwing if not available
476
- * @deprecated Use getRequestContext() directly — it now throws if outside context
477
- */
478
- export function requireRequestContext<
479
- TEnv = DefaultEnv,
480
- >(): RequestContext<TEnv> {
481
- return getRequestContext<TEnv>();
482
- }
483
-
484
688
  export type { ExecutionContext };
485
689
 
486
690
  /**
@@ -495,6 +699,11 @@ export interface CreateRequestContextOptions<TEnv> {
495
699
  initialResponse?: Response;
496
700
  /** Optional cache store for segment caching (used by CacheScope) */
497
701
  cacheStore?: SegmentCacheStore;
702
+ /**
703
+ * Handler-owned registry of explicit per-scope stores for cross-store tag
704
+ * invalidation. Created once per handler, reused across requests.
705
+ */
706
+ explicitTaggedStores?: Set<SegmentCacheStore>;
498
707
  /** Optional cache profiles for "use cache" resolution (per-router) */
499
708
  cacheProfiles?: Record<
500
709
  string,
@@ -504,6 +713,10 @@ export interface CreateRequestContextOptions<TEnv> {
504
713
  executionContext?: ExecutionContext;
505
714
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
506
715
  themeConfig?: ResolvedThemeConfig | null;
716
+ /** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
717
+ stateCookieName?: string;
718
+ /** Build version, used as the prefix of a server-rotated rango state value. */
719
+ version?: string;
507
720
  }
508
721
 
509
722
  /**
@@ -524,15 +737,17 @@ export function createRequestContext<TEnv>(
524
737
  variables,
525
738
  initialResponse,
526
739
  cacheStore,
740
+ explicitTaggedStores,
527
741
  cacheProfiles,
528
742
  executionContext,
529
743
  themeConfig,
744
+ stateCookieName,
745
+ version: stateVersion,
530
746
  } = options;
531
747
  const cookieHeader = request.headers.get("Cookie");
748
+ let rangoStateRotated = false;
532
749
  let parsedCookies: Record<string, string> | null = null;
533
750
 
534
- // Create stub response for collecting headers/cookies.
535
- // All cookie/header mutations go here; cookie reads derive from it.
536
751
  let stubResponse = initialResponse
537
752
  ? new Response(null, {
538
753
  status: initialResponse.status,
@@ -541,11 +756,9 @@ export function createRequestContext<TEnv>(
541
756
  })
542
757
  : new Response(null, { status: 200 });
543
758
 
544
- // Create handle store and loader memoization for this request
545
759
  const handleStore = createHandleStore();
546
760
  const loaderPromises = new Map<string, Promise<any>>();
547
761
 
548
- // Lazy parse cookies from the original Cookie header
549
762
  const getParsedCookies = (): Record<string, string> => {
550
763
  if (!parsedCookies) {
551
764
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -553,7 +766,6 @@ export function createRequestContext<TEnv>(
553
766
  return parsedCookies;
554
767
  };
555
768
 
556
- // Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
557
769
  let responseCookieCache: Map<string, string | null> | null = null;
558
770
  const getResponseCookies = (): Map<string, string | null> => {
559
771
  if (!responseCookieCache) {
@@ -565,8 +777,6 @@ export function createRequestContext<TEnv>(
565
777
  responseCookieCache = null;
566
778
  };
567
779
 
568
- // Guard: throw if a response-level side effect is called inside a cache() scope.
569
- // Uses ALS to detect the scope (set during segment resolution).
570
780
  function assertNotInsideCacheScopeALS(methodName: string): void {
571
781
  if (isInsideCacheScope()) {
572
782
  throw new Error(
@@ -577,8 +787,7 @@ export function createRequestContext<TEnv>(
577
787
  }
578
788
  }
579
789
 
580
- // Effective cookie read: response stub Set-Cookie wins, then original header.
581
- // The stub IS the source of truth for same-request mutations.
790
+ // Response stub Set-Cookie wins, then original header (source of truth for mutations).
582
791
  const effectiveCookie = (name: string): string | undefined => {
583
792
  const mutations = getResponseCookies();
584
793
  if (mutations.has(name)) {
@@ -588,14 +797,11 @@ export function createRequestContext<TEnv>(
588
797
  return getParsedCookies()[name];
589
798
  };
590
799
 
591
- // Theme helpers (only used when themeConfig is provided)
592
800
  const getTheme = (): Theme | undefined => {
593
801
  if (!themeConfig) return undefined;
594
802
 
595
- // Use overlay-aware read so setTheme() in the same request is reflected
596
803
  const stored = effectiveCookie(themeConfig.storageKey);
597
804
  if (stored) {
598
- // Validate stored value
599
805
  if (stored === "system" && themeConfig.enableSystem) {
600
806
  return "system";
601
807
  }
@@ -609,15 +815,15 @@ export function createRequestContext<TEnv>(
609
815
  const setTheme = (theme: Theme): void => {
610
816
  if (!themeConfig) return;
611
817
 
612
- // Validate theme value
613
- if (theme !== "system" && !themeConfig.themes.includes(theme)) {
614
- console.warn(
615
- `[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
616
- );
818
+ // Shared guard (isValidTheme): reject any value not in the configured theme
819
+ // set, AND reject "system" when system detection is off — a cookie of
820
+ // theme=system with enableSystem:false would re-apply a bogus class="system"
821
+ // on the next SSR.
822
+ if (!isValidTheme(theme, themeConfig)) {
823
+ warnInvalidTheme(theme, themeConfig);
617
824
  return;
618
825
  }
619
826
 
620
- // Write to stub — effectiveCookie() will pick it up on next read
621
827
  stubResponse.headers.append(
622
828
  "Set-Cookie",
623
829
  serializeCookieValue(themeConfig.storageKey, theme, {
@@ -629,10 +835,8 @@ export function createRequestContext<TEnv>(
629
835
  invalidateResponseCookieCache();
630
836
  };
631
837
 
632
- // Strip internal _rsc* params so userland sees a clean URL.
633
838
  const cleanUrl = stripInternalParams(url);
634
839
 
635
- // Build the context object first (without use), then add use
636
840
  const ctx: RequestContext<TEnv> = {
637
841
  env,
638
842
  request,
@@ -718,6 +922,45 @@ export function createRequestContext<TEnv>(
718
922
  stubResponse.headers.set(name, value);
719
923
  },
720
924
 
925
+ // Rotate the rango state cookie for the responding client (the server seat
926
+ // of invalidateClientCache). Writes ONE Set-Cookie per request with the
927
+ // value {version}:{timestamp}; the `:` stays raw (the cookie-name.ts
928
+ // serializer), not the URL-encoded form serializeCookieValue would produce.
929
+ // The timestamp is strictly greater than the client's current one (inbound
930
+ // X-Rango-State), so a same-millisecond server rotation still differs from
931
+ // the client value and the divergence observer fires.
932
+ _rotateStateCookie(): void {
933
+ if (rangoStateRotated) return;
934
+ rangoStateRotated = true;
935
+ if (!stateCookieName) return;
936
+ // The client's current value, for the monotonic guard: prefer the
937
+ // X-Rango-State header (router navigation/prefetch fetches send it), but
938
+ // fall back to the request's rango state cookie — action POSTs / plain
939
+ // app fetch()s carry no router header yet DO send the cookie. Without the
940
+ // fallback, prevTs stays 0 and a same-ms mint can equal the client value,
941
+ // leaving the divergence observer silent. `|| null` so an empty header
942
+ // ('' from proxy normalization) falls through instead of short-circuiting.
943
+ // getRawCookieValue reads the cookie undecoded (the wire value
944
+ // decodeStateValue decodes exactly once) AND is the same parser the client
945
+ // mirror uses, so both seats read the same jar entry.
946
+ const prevRaw =
947
+ (request.headers.get("x-rango-state") || null) ??
948
+ getRawCookieValue(cookieHeader, stateCookieName);
949
+ const value = mintStateValue(stateVersion ?? "0", prevRaw);
950
+ stubResponse.headers.append(
951
+ "Set-Cookie",
952
+ serializeStateCookie(stateCookieName, value, url.protocol === "https:"),
953
+ );
954
+ invalidateResponseCookieCache();
955
+ },
956
+
957
+ // Set the keepClientCache() directive header. The action bridge reads it on
958
+ // the response and suppresses its automatic invalidation. `.set` makes this
959
+ // idempotent (one header regardless of call count).
960
+ _setKeepCacheDirective(): void {
961
+ stubResponse.headers.set(KEEP_CACHE_HEADER, "1");
962
+ },
963
+
721
964
  setStatus(status: number): void {
722
965
  assertNotInsideCacheExec(ctx, "setStatus");
723
966
  assertNotInsideCacheScopeALS("setStatus");
@@ -734,26 +977,49 @@ export function createRequestContext<TEnv>(
734
977
  });
735
978
  },
736
979
 
737
- // Placeholder - will be replaced below
738
980
  use: null as any,
739
981
 
740
982
  method: request.method,
741
983
 
742
984
  _handleStore: handleStore,
985
+ _transitionWhen: [],
743
986
  _cacheStore: cacheStore,
987
+ _explicitTaggedStores: explicitTaggedStores,
988
+ _requestTags: new Set<string>(),
744
989
  _cacheProfiles: cacheProfiles,
745
990
 
746
991
  waitUntil(fn: () => Promise<void>): void {
992
+ // Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
993
+ // non-async callback becomes a rejected promise handed to the host's
994
+ // waitUntil (logged as a background failure), instead of escaping into
995
+ // the request flow. Mirrors fireAndForgetWaitUntil's deferral.
996
+ const task = Promise.resolve().then(fn);
997
+ // Track the task promise so the PPR shell capture can settle the
998
+ // foreground's deferred cache writes before its own match/render (the
999
+ // ordering edge; see _pendingBackgroundTasks). The capture task itself
1000
+ // opts out via the marker — the drain must never await its own promise.
1001
+ if (
1002
+ !(fn as { [UNTRACKED_BACKGROUND_TASK]?: boolean })[
1003
+ UNTRACKED_BACKGROUND_TASK
1004
+ ]
1005
+ ) {
1006
+ ctx._pendingBackgroundTasks?.push(task);
1007
+ }
747
1008
  if (executionContext?.waitUntil) {
748
- executionContext.waitUntil(fn());
1009
+ executionContext.waitUntil(task);
749
1010
  } else {
750
- fireAndForgetWaitUntil(fn);
1011
+ // Node/dev fallback: fire-and-forget with error logging (the same
1012
+ // policy fireAndForgetWaitUntil applies).
1013
+ task.catch((err) =>
1014
+ console.error("[waitUntil] Background task failed:", err),
1015
+ );
751
1016
  }
752
1017
  },
753
1018
 
754
1019
  executionContext,
755
1020
 
756
1021
  _onResponseCallbacks: [],
1022
+ _pendingBackgroundTasks: [],
757
1023
 
758
1024
  onResponse(callback: (response: Response) => Response): void {
759
1025
  assertNotInsideCacheExec(ctx, "onResponse");
@@ -761,7 +1027,6 @@ export function createRequestContext<TEnv>(
761
1027
  this._onResponseCallbacks.push(callback);
762
1028
  },
763
1029
 
764
- // Theme properties (only set when themeConfig is provided)
765
1030
  get theme() {
766
1031
  return themeConfig ? getTheme() : undefined;
767
1032
  },
@@ -784,20 +1049,19 @@ export function createRequestContext<TEnv>(
784
1049
 
785
1050
  _reportedErrors: new WeakSet<object>(),
786
1051
  _metricsStore: undefined,
1052
+ _handlerStart: undefined,
787
1053
 
788
- // Render barrier: deferred promise resolved after non-loader segments settle.
789
- _renderBarrier: null as any, // set below
790
- _resolveRenderBarrier: null as any, // set below
1054
+ _renderBarrier: null as any,
1055
+ _resolveRenderBarrier: null as any,
791
1056
  _renderBarrierSegmentOrder: undefined,
792
1057
 
793
1058
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
794
1059
  };
795
1060
 
796
- // Lazy render barrier: only allocate the Promise when a loader actually
797
- // calls rendered(). Requests that don't use rendered() pay zero cost.
1061
+ // Lazy allocation: only create Promise when a loader calls rendered().
798
1062
  let barrierResolved = false;
799
1063
  let resolveBarrier: (() => void) | undefined;
800
- ctx._renderBarrier = null as any; // lazy — created on first access
1064
+ ctx._renderBarrier = null as any;
801
1065
  ctx._resolveRenderBarrier = (
802
1066
  segments: Array<{ type: string; id: string }>,
803
1067
  ) => {
@@ -807,21 +1071,26 @@ export function createRequestContext<TEnv>(
807
1071
  .filter((s) => s.type !== "loader")
808
1072
  .map((s) => s.id);
809
1073
  ctx._renderBarrierSegmentOrder = segOrder;
810
- // Build and cache handle snapshot so loader ctx.use(handle) calls
811
- // don't rebuild it on every invocation.
812
- ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
813
- handleStore,
814
- segOrder,
815
- );
816
- ctx._renderBarrierWaiters = undefined;
817
- ctx._handlerLoaderDeps = undefined;
1074
+
1075
+ const closeGuard = () => {
1076
+ ctx._renderBarrierWaiters = undefined;
1077
+ ctx._handlerLoaderDeps = undefined;
1078
+ ctx._renderBarrierGuardClosed = true;
1079
+ };
1080
+
1081
+ if (ctx._treeHasStreaming) {
1082
+ handleStore.settled.then(closeGuard);
1083
+ } else {
1084
+ ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
1085
+ handleStore,
1086
+ segOrder,
1087
+ );
1088
+ closeGuard();
1089
+ }
818
1090
  if (resolveBarrier) resolveBarrier();
819
1091
  };
820
1092
  Object.defineProperty(ctx, "_renderBarrier", {
821
1093
  get() {
822
- // Barrier already resolved (cache/prerender hit) or first lazy access.
823
- // Either way, replace the getter with a concrete value to avoid
824
- // repeated Promise.resolve() allocations on subsequent reads.
825
1094
  const p = barrierResolved
826
1095
  ? Promise.resolve()
827
1096
  : new Promise<void>((resolve) => {
@@ -837,32 +1106,33 @@ export function createRequestContext<TEnv>(
837
1106
  configurable: true,
838
1107
  });
839
1108
 
840
- // Now create use() with access to ctx
841
1109
  ctx.use = createUseFunction({
842
1110
  handleStore,
843
1111
  loaderPromises,
844
1112
  getContext: () => ctx,
845
1113
  });
846
1114
 
847
- // Brand with taint symbol so "use cache" excludes ctx from cache keys
848
1115
  (ctx as any)[NOCACHE_SYMBOL] = true;
849
1116
  return ctx;
850
1117
  }
851
1118
 
852
- /**
853
- * Parse Set-Cookie headers from a response into effective cookie state.
854
- * Returns a map of cookie name -> value (string) or name -> null (deleted).
855
- * Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
856
- * Max-Age=0 is treated as a delete.
857
- */
858
- const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
1119
+ // Capture the Max-Age value so it can be parsed numerically. A leading zero
1120
+ // (Max-Age=05) is a non-zero lifetime, not a deletion; only a value that parses
1121
+ // to <= 0 marks a cookie for deletion. Pattern-matching a leading "0" misread
1122
+ // zero-prefixed values like 05 / 010 as deletions.
1123
+ const MAX_AGE_RE = /;\s*Max-Age\s*=\s*(-?\d+)/i;
1124
+
1125
+ function isCookieDeletion(header: string): boolean {
1126
+ const m = MAX_AGE_RE.exec(header);
1127
+ if (!m) return false;
1128
+ return Number(m[1]) <= 0;
1129
+ }
859
1130
 
860
1131
  function parseResponseCookies(response: Response): Map<string, string | null> {
861
1132
  const result = new Map<string, string | null>();
862
1133
  const setCookies = response.headers.getSetCookie();
863
1134
 
864
1135
  for (const header of setCookies) {
865
- // First segment before ';' is the name=value pair
866
1136
  const semiIdx = header.indexOf(";");
867
1137
  const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
868
1138
  const eqIdx = pair.indexOf("=");
@@ -874,49 +1144,22 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
874
1144
  name = decodeURIComponent(pair.substring(0, eqIdx).trim());
875
1145
  value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
876
1146
  } catch {
877
- // Malformed encoding — skip this entry
878
1147
  continue;
879
1148
  }
880
1149
 
881
- // Max-Age=0 means the cookie is being deleted
882
- const isDeleted = MAX_AGE_ZERO_RE.test(header);
1150
+ const isDeleted = isCookieDeletion(header);
883
1151
  result.set(name, isDeleted ? null : value);
884
1152
  }
885
1153
 
886
1154
  return result;
887
1155
  }
888
1156
 
889
- /**
890
- * Parse cookies from Cookie header
891
- */
892
- function parseCookiesFromHeader(
893
- cookieHeader: string | null,
894
- ): Record<string, string> {
895
- if (!cookieHeader) return {};
896
-
897
- const cookies: Record<string, string> = {};
898
- const pairs = cookieHeader.split(";");
899
-
900
- for (const pair of pairs) {
901
- const [name, ...rest] = pair.trim().split("=");
902
- if (name) {
903
- const raw = rest.join("=");
904
- try {
905
- cookies[name] = decodeURIComponent(raw);
906
- } catch {
907
- // Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
908
- cookies[name] = raw;
909
- }
910
- }
911
- }
1157
+ // Re-exported for unit tests and the existing import path. The implementation
1158
+ // lives in the dependency-free ./cookie-parse leaf so consumers (e.g. the host
1159
+ // dispatcher) can share it without pulling this module's request-context graph.
1160
+ export { parseCookiesFromHeader };
912
1161
 
913
- return cookies;
914
- }
915
-
916
- /**
917
- * Serialize a cookie for Set-Cookie header
918
- */
919
- function serializeCookieValue(
1162
+ export function serializeCookieValue(
920
1163
  name: string,
921
1164
  value: string,
922
1165
  options: CookieOptions = {},
@@ -943,20 +1186,12 @@ export interface CreateUseFunctionOptions<TEnv> {
943
1186
  getContext: () => RequestContext<TEnv>;
944
1187
  }
945
1188
 
946
- /**
947
- * Create the use() function for loader and handle composition.
948
- *
949
- * This is the unified implementation used by both RequestContext and HandlerContext.
950
- * - For loaders: executes and memoizes loader functions
951
- * - For handles: returns a push function to add handle data
952
- */
953
1189
  export function createUseFunction<TEnv>(
954
1190
  options: CreateUseFunctionOptions<TEnv>,
955
1191
  ): RequestContext["use"] {
956
1192
  const { handleStore, loaderPromises, getContext } = options;
957
1193
 
958
1194
  return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
959
- // Handle case: return a push function
960
1195
  if (isHandle(item)) {
961
1196
  const handle = item;
962
1197
  const ctx = getContext();
@@ -969,30 +1204,24 @@ export function createUseFunction<TEnv>(
969
1204
  );
970
1205
  }
971
1206
 
972
- // Return a push function bound to this handle and segment
973
- return (
974
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
975
- ) => {
976
- // If it's a function, call it immediately to get the promise
977
- const valueOrPromise =
978
- typeof dataOrFn === "function"
979
- ? (dataOrFn as () => Promise<unknown>)()
980
- : dataOrFn;
981
-
982
- // Push directly - promises will be serialized by RSC and streamed
983
- handleStore.push(handle.$$id, segmentId, valueOrPromise);
984
- };
1207
+ return withDefer(
1208
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
1209
+ const valueOrPromise =
1210
+ typeof dataOrFn === "function"
1211
+ ? (dataOrFn as () => Promise<unknown>)()
1212
+ : dataOrFn;
1213
+
1214
+ handleStore.push(handle.$$id, segmentId, valueOrPromise);
1215
+ },
1216
+ );
985
1217
  }
986
1218
 
987
- // Loader case
988
1219
  const loader = item as LoaderDefinition<any, any>;
989
1220
 
990
- // Return cached promise if already started
991
1221
  if (loaderPromises.has(loader.$$id)) {
992
1222
  return loaderPromises.get(loader.$$id);
993
1223
  }
994
1224
 
995
- // Get loader function - either from loader object or fetchable registry
996
1225
  let loaderFn = loader.fn;
997
1226
  if (!loaderFn) {
998
1227
  const fetchable = getFetchableLoader(loader.$$id);
@@ -1009,13 +1238,24 @@ export function createUseFunction<TEnv>(
1009
1238
 
1010
1239
  const ctx = getContext();
1011
1240
 
1012
- // Create loader context with recursive use() support
1241
+ // Build the typed ctx.search the same way the render path
1242
+ // (createHandlerContext) and the fetchable-loader path (loader-fetch.ts) do:
1243
+ // parse the route's search schema over the cleaned searchParams. The base
1244
+ // RequestContext carries no `search` field, so reading `(ctx as any).search`
1245
+ // here always yielded {} — dropping typed search for action/dispatch loaders.
1246
+ const searchSchema = ctx._routeName
1247
+ ? getSearchSchema(ctx._routeName)
1248
+ : undefined;
1249
+ const loaderSearch = searchSchema
1250
+ ? parseSearchParams(ctx.searchParams, searchSchema)
1251
+ : {};
1252
+
1013
1253
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
1014
1254
  params: ctx.params,
1015
1255
  routeParams: (ctx.params ?? {}) as Record<string, string>,
1016
1256
  request: ctx.request,
1017
1257
  searchParams: ctx.searchParams,
1018
- search: (ctx as any).search ?? {},
1258
+ search: loaderSearch,
1019
1259
  pathname: ctx.pathname,
1020
1260
  url: ctx.url,
1021
1261
  originalUrl: ctx.originalUrl,
@@ -1026,7 +1266,6 @@ export function createUseFunction<TEnv>(
1026
1266
  use: (<TDep, TDepParams = any>(
1027
1267
  dep: LoaderDefinition<TDep, TDepParams>,
1028
1268
  ): Promise<TDep> => {
1029
- // Recursive call - will start dep loader if not already started
1030
1269
  return ctx.use(dep);
1031
1270
  }) as LoaderContext["use"],
1032
1271
  method: "GET",
@@ -1045,12 +1284,14 @@ export function createUseFunction<TEnv>(
1045
1284
  },
1046
1285
  };
1047
1286
 
1048
- const doneLoader = track(`loader:${loader.$$id}`, 2);
1049
- const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
1050
- doneLoader();
1051
- });
1287
+ // Meter through the same unified phase API as the loader-resolution funnel
1288
+ // (observePhase), so a loader resolved via this base request-context ctx.use
1289
+ // co-emits the "loader:<id>" perf metric AND the "rango.loader" span — no
1290
+ // drift between the two ctx.use implementations.
1291
+ const promise = observePhase(PHASES.loader(loader.$$id), () =>
1292
+ Promise.resolve(loaderFn(loaderCtx)),
1293
+ );
1052
1294
 
1053
- // Memoize for subsequent calls
1054
1295
  loaderPromises.set(loader.$$id, promise);
1055
1296
 
1056
1297
  return promise;