@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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