@rangojs/router 0.0.0-experimental.31 → 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 +121 -205
  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 +192 -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 +64 -25
  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 +348 -128
  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)
@@ -95,6 +108,12 @@ export interface RequestContext<
95
108
  header(name: string, value: string): void;
96
109
  /** Set the response status code */
97
110
  setStatus(status: number): void;
111
+ /** @internal Set status bypassing cache-exec guard (for framework error handling) */
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;
98
117
 
99
118
  /**
100
119
  * Access loader data or push handle data.
@@ -133,26 +152,31 @@ export interface RequestContext<
133
152
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
134
153
  _cacheStore?: SegmentCacheStore;
135
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
+
136
174
  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
137
175
  _cacheProfiles?: Record<
138
176
  string,
139
177
  import("../cache/profile-registry.js").CacheProfile
140
178
  >;
141
179
 
142
- /**
143
- * Schedule work to run after the response is sent.
144
- * On Cloudflare Workers, uses ctx.waitUntil().
145
- * On Node.js, runs as fire-and-forget.
146
- *
147
- * @example
148
- * ```typescript
149
- * ctx.waitUntil(async () => {
150
- * await cacheStore.set(key, data, ttl);
151
- * });
152
- * ```
153
- */
154
- waitUntil(fn: () => Promise<void>): void;
155
-
156
180
  /**
157
181
  * Register a callback to run when the response is created.
158
182
  * Callbacks are sync and receive the response. They can:
@@ -256,6 +280,68 @@ export interface RequestContext<
256
280
  /** @internal Previous route key (from the navigation source), used for revalidation */
257
281
  _prevRouteKey?: string;
258
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
+
259
345
  /** @internal Per-request error dedup set for onError reporting */
260
346
  _reportedErrors: WeakSet<object>;
261
347
 
@@ -263,15 +349,37 @@ export interface RequestContext<
263
349
  * @internal Report a non-fatal background error through the router's
264
350
  * onError callback. Wired by the RSC handler / router during request
265
351
  * creation. Cache-runtime and other subsystems call this to surface
266
- * 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`).
267
354
  */
268
- _reportBackgroundError?: (error: unknown, category: string) => void;
355
+ _reportBackgroundError?: (
356
+ error: unknown,
357
+ category: CacheErrorCategory,
358
+ ) => void;
269
359
 
270
360
  /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
271
361
  _debugPerformance?: boolean;
272
362
 
273
363
  /** @internal Request-scoped performance metrics store */
274
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[];
275
383
  }
276
384
 
277
385
  /**
@@ -291,6 +399,8 @@ export type PublicRequestContext<
291
399
  | "deleteCookie"
292
400
  | "_handleStore"
293
401
  | "_cacheStore"
402
+ | "_explicitTaggedStores"
403
+ | "_requestTags"
294
404
  | "_cacheProfiles"
295
405
  | "_onResponseCallbacks"
296
406
  | "_themeConfig"
@@ -298,9 +408,24 @@ export type PublicRequestContext<
298
408
  | "_routeName"
299
409
  | "_prevRouteKey"
300
410
  | "_reportedErrors"
411
+ | "_renderBarrier"
412
+ | "_resolveRenderBarrier"
413
+ | "_renderBarrierSegmentOrder"
414
+ | "_treeHasStreaming"
415
+ | "_renderBarrierWaiters"
416
+ | "_handlerLoaderDeps"
417
+ | "_renderBarrierHandleSnapshot"
418
+ | "_renderBarrierGuardClosed"
301
419
  | "_reportBackgroundError"
302
420
  | "_debugPerformance"
303
421
  | "_metricsStore"
422
+ | "_basename"
423
+ | "_setStatus"
424
+ | "_rotateStateCookie"
425
+ | "_setKeepCacheDirective"
426
+ | "_variables"
427
+ | "_classifiedRoute"
428
+ | "_cacheSignal"
304
429
  | "res"
305
430
  >;
306
431
 
@@ -352,6 +477,7 @@ export function _getRequestContext<TEnv = DefaultEnv>():
352
477
  export function setRequestContextParams(
353
478
  params: Record<string, string>,
354
479
  routeName?: string,
480
+ routeMap?: Record<string, string>,
355
481
  ): void {
356
482
  const ctx = requestContextStorage.getStore();
357
483
  if (ctx) {
@@ -364,9 +490,13 @@ export function setRequestContextParams(
364
490
  : undefined
365
491
  ) as DefaultRouteName | undefined;
366
492
  }
367
- // 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.
368
498
  ctx.reverse = createReverseFunction(
369
- getGlobalRouteMap(),
499
+ routeMap ?? getGlobalRouteMap(),
370
500
  routeName,
371
501
  params,
372
502
  routeName ? isRouteRootScoped(routeName) : undefined,
@@ -410,13 +540,7 @@ export function requireRequestContext<
410
540
  return getRequestContext<TEnv>();
411
541
  }
412
542
 
413
- /**
414
- * Cloudflare Workers ExecutionContext (subset we need)
415
- */
416
- export interface ExecutionContext {
417
- waitUntil(promise: Promise<any>): void;
418
- passThroughOnException(): void;
419
- }
543
+ export type { ExecutionContext };
420
544
 
421
545
  /**
422
546
  * Options for creating a request context
@@ -430,6 +554,11 @@ export interface CreateRequestContextOptions<TEnv> {
430
554
  initialResponse?: Response;
431
555
  /** Optional cache store for segment caching (used by CacheScope) */
432
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>;
433
562
  /** Optional cache profiles for "use cache" resolution (per-router) */
434
563
  cacheProfiles?: Record<
435
564
  string,
@@ -439,6 +568,10 @@ export interface CreateRequestContextOptions<TEnv> {
439
568
  executionContext?: ExecutionContext;
440
569
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
441
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;
442
575
  }
443
576
 
444
577
  /**
@@ -459,15 +592,17 @@ export function createRequestContext<TEnv>(
459
592
  variables,
460
593
  initialResponse,
461
594
  cacheStore,
595
+ explicitTaggedStores,
462
596
  cacheProfiles,
463
597
  executionContext,
464
598
  themeConfig,
599
+ stateCookieName,
600
+ version: stateVersion,
465
601
  } = options;
466
602
  const cookieHeader = request.headers.get("Cookie");
603
+ let rangoStateRotated = false;
467
604
  let parsedCookies: Record<string, string> | null = null;
468
605
 
469
- // Create stub response for collecting headers/cookies.
470
- // All cookie/header mutations go here; cookie reads derive from it.
471
606
  let stubResponse = initialResponse
472
607
  ? new Response(null, {
473
608
  status: initialResponse.status,
@@ -476,11 +611,9 @@ export function createRequestContext<TEnv>(
476
611
  })
477
612
  : new Response(null, { status: 200 });
478
613
 
479
- // Create handle store and loader memoization for this request
480
614
  const handleStore = createHandleStore();
481
615
  const loaderPromises = new Map<string, Promise<any>>();
482
616
 
483
- // Lazy parse cookies from the original Cookie header
484
617
  const getParsedCookies = (): Record<string, string> => {
485
618
  if (!parsedCookies) {
486
619
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -488,7 +621,6 @@ export function createRequestContext<TEnv>(
488
621
  return parsedCookies;
489
622
  };
490
623
 
491
- // Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
492
624
  let responseCookieCache: Map<string, string | null> | null = null;
493
625
  const getResponseCookies = (): Map<string, string | null> => {
494
626
  if (!responseCookieCache) {
@@ -500,8 +632,17 @@ export function createRequestContext<TEnv>(
500
632
  responseCookieCache = null;
501
633
  };
502
634
 
503
- // Effective cookie read: response stub Set-Cookie wins, then original header.
504
- // 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).
505
646
  const effectiveCookie = (name: string): string | undefined => {
506
647
  const mutations = getResponseCookies();
507
648
  if (mutations.has(name)) {
@@ -511,14 +652,11 @@ export function createRequestContext<TEnv>(
511
652
  return getParsedCookies()[name];
512
653
  };
513
654
 
514
- // Theme helpers (only used when themeConfig is provided)
515
655
  const getTheme = (): Theme | undefined => {
516
656
  if (!themeConfig) return undefined;
517
657
 
518
- // Use overlay-aware read so setTheme() in the same request is reflected
519
658
  const stored = effectiveCookie(themeConfig.storageKey);
520
659
  if (stored) {
521
- // Validate stored value
522
660
  if (stored === "system" && themeConfig.enableSystem) {
523
661
  return "system";
524
662
  }
@@ -532,7 +670,6 @@ export function createRequestContext<TEnv>(
532
670
  const setTheme = (theme: Theme): void => {
533
671
  if (!themeConfig) return;
534
672
 
535
- // Validate theme value
536
673
  if (theme !== "system" && !themeConfig.themes.includes(theme)) {
537
674
  console.warn(
538
675
  `[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
@@ -540,7 +677,6 @@ export function createRequestContext<TEnv>(
540
677
  return;
541
678
  }
542
679
 
543
- // Write to stub — effectiveCookie() will pick it up on next read
544
680
  stubResponse.headers.append(
545
681
  "Set-Cookie",
546
682
  serializeCookieValue(themeConfig.storageKey, theme, {
@@ -552,20 +688,29 @@ export function createRequestContext<TEnv>(
552
688
  invalidateResponseCookieCache();
553
689
  };
554
690
 
555
- // Build the context object first (without use), then add use
691
+ const cleanUrl = stripInternalParams(url);
692
+
556
693
  const ctx: RequestContext<TEnv> = {
557
694
  env,
558
695
  request,
559
- url,
696
+ url: cleanUrl,
560
697
  originalUrl: new URL(request.url),
561
698
  pathname: url.pathname,
562
- searchParams: url.searchParams,
563
- var: variables,
564
- get: ((keyOrVar: any) =>
565
- contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
566
- 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) => {
567
712
  assertNotInsideCacheExec(ctx, "set");
568
- contextSet(variables, keyOrVar, value);
713
+ contextSet(variables, keyOrVar, value, options);
569
714
  }) as RequestContext<TEnv>["set"],
570
715
  params: {} as Record<string, string>,
571
716
 
@@ -603,6 +748,7 @@ export function createRequestContext<TEnv>(
603
748
 
604
749
  setCookie(name: string, value: string, options?: CookieOptions): void {
605
750
  assertNotInsideCacheExec(ctx, "setCookie");
751
+ assertNotInsideCacheScopeALS("setCookie");
606
752
  stubResponse.headers.append(
607
753
  "Set-Cookie",
608
754
  serializeCookieValue(name, value, options),
@@ -615,6 +761,7 @@ export function createRequestContext<TEnv>(
615
761
  options?: Pick<CookieOptions, "domain" | "path">,
616
762
  ): void {
617
763
  assertNotInsideCacheExec(ctx, "deleteCookie");
764
+ assertNotInsideCacheScopeALS("deleteCookie");
618
765
  stubResponse.headers.append(
619
766
  "Set-Cookie",
620
767
  serializeCookieValue(name, "", { ...options, maxAge: 0 }),
@@ -624,48 +771,93 @@ export function createRequestContext<TEnv>(
624
771
 
625
772
  header(name: string, value: string): void {
626
773
  assertNotInsideCacheExec(ctx, "header");
774
+ assertNotInsideCacheScopeALS("header");
627
775
  stubResponse.headers.set(name, value);
628
776
  },
629
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
+
630
817
  setStatus(status: number): void {
631
818
  assertNotInsideCacheExec(ctx, "setStatus");
632
- // Response.status is read-only, so we must create a new Response.
633
- // Headers are passed by reference — no cookie cache invalidation needed.
819
+ assertNotInsideCacheScopeALS("setStatus");
820
+ stubResponse = new Response(null, {
821
+ status,
822
+ headers: stubResponse.headers,
823
+ });
824
+ },
825
+
826
+ _setStatus(status: number): void {
634
827
  stubResponse = new Response(null, {
635
828
  status,
636
829
  headers: stubResponse.headers,
637
830
  });
638
831
  },
639
832
 
640
- // Placeholder - will be replaced below
641
833
  use: null as any,
642
834
 
643
835
  method: request.method,
644
836
 
645
837
  _handleStore: handleStore,
646
838
  _cacheStore: cacheStore,
839
+ _explicitTaggedStores: explicitTaggedStores,
840
+ _requestTags: new Set<string>(),
647
841
  _cacheProfiles: cacheProfiles,
648
842
 
649
843
  waitUntil(fn: () => Promise<void>): void {
650
844
  if (executionContext?.waitUntil) {
651
- // Cloudflare Workers: use native waitUntil
652
845
  executionContext.waitUntil(fn());
653
846
  } else {
654
- // Node.js / dev: fire-and-forget with error logging
655
- fn().catch((err) =>
656
- console.error("[waitUntil] Background task failed:", err),
657
- );
847
+ fireAndForgetWaitUntil(fn);
658
848
  }
659
849
  },
660
850
 
851
+ executionContext,
852
+
661
853
  _onResponseCallbacks: [],
662
854
 
663
855
  onResponse(callback: (response: Response) => Response): void {
664
856
  assertNotInsideCacheExec(ctx, "onResponse");
857
+ assertNotInsideCacheScopeALS("onResponse");
665
858
  this._onResponseCallbacks.push(callback);
666
859
  },
667
860
 
668
- // Theme properties (only set when themeConfig is provided)
669
861
  get theme() {
670
862
  return themeConfig ? getTheme() : undefined;
671
863
  },
@@ -689,27 +881,71 @@ export function createRequestContext<TEnv>(
689
881
  _reportedErrors: new WeakSet<object>(),
690
882
  _metricsStore: undefined,
691
883
 
884
+ _renderBarrier: null as any,
885
+ _resolveRenderBarrier: null as any,
886
+ _renderBarrierSegmentOrder: undefined,
887
+
692
888
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
693
889
  };
694
890
 
695
- // 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
+
696
939
  ctx.use = createUseFunction({
697
940
  handleStore,
698
941
  loaderPromises,
699
942
  getContext: () => ctx,
700
943
  });
701
944
 
702
- // Brand with taint symbol so "use cache" excludes ctx from cache keys
703
945
  (ctx as any)[NOCACHE_SYMBOL] = true;
704
946
  return ctx;
705
947
  }
706
948
 
707
- /**
708
- * Parse Set-Cookie headers from a response into effective cookie state.
709
- * Returns a map of cookie name -> value (string) or name -> null (deleted).
710
- * Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
711
- * Max-Age=0 is treated as a delete.
712
- */
713
949
  const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
714
950
 
715
951
  function parseResponseCookies(response: Response): Map<string, string | null> {
@@ -717,7 +953,6 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
717
953
  const setCookies = response.headers.getSetCookie();
718
954
 
719
955
  for (const header of setCookies) {
720
- // First segment before ';' is the name=value pair
721
956
  const semiIdx = header.indexOf(";");
722
957
  const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
723
958
  const eqIdx = pair.indexOf("=");
@@ -729,11 +964,9 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
729
964
  name = decodeURIComponent(pair.substring(0, eqIdx).trim());
730
965
  value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
731
966
  } catch {
732
- // Malformed encoding — skip this entry
733
967
  continue;
734
968
  }
735
969
 
736
- // Max-Age=0 means the cookie is being deleted
737
970
  const isDeleted = MAX_AGE_ZERO_RE.test(header);
738
971
  result.set(name, isDeleted ? null : value);
739
972
  }
@@ -741,10 +974,10 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
741
974
  return result;
742
975
  }
743
976
 
744
- /**
745
- * Parse cookies from Cookie header
746
- */
747
- 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(
748
981
  cookieHeader: string | null,
749
982
  ): Record<string, string> {
750
983
  if (!cookieHeader) return {};
@@ -759,7 +992,7 @@ function parseCookiesFromHeader(
759
992
  try {
760
993
  cookies[name] = decodeURIComponent(raw);
761
994
  } catch {
762
- // Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
995
+ // Malformed percent-encoding: fall back to raw value
763
996
  cookies[name] = raw;
764
997
  }
765
998
  }
@@ -768,10 +1001,7 @@ function parseCookiesFromHeader(
768
1001
  return cookies;
769
1002
  }
770
1003
 
771
- /**
772
- * Serialize a cookie for Set-Cookie header
773
- */
774
- function serializeCookieValue(
1004
+ export function serializeCookieValue(
775
1005
  name: string,
776
1006
  value: string,
777
1007
  options: CookieOptions = {},
@@ -798,20 +1028,12 @@ export interface CreateUseFunctionOptions<TEnv> {
798
1028
  getContext: () => RequestContext<TEnv>;
799
1029
  }
800
1030
 
801
- /**
802
- * Create the use() function for loader and handle composition.
803
- *
804
- * This is the unified implementation used by both RequestContext and HandlerContext.
805
- * - For loaders: executes and memoizes loader functions
806
- * - For handles: returns a push function to add handle data
807
- */
808
1031
  export function createUseFunction<TEnv>(
809
1032
  options: CreateUseFunctionOptions<TEnv>,
810
1033
  ): RequestContext["use"] {
811
1034
  const { handleStore, loaderPromises, getContext } = options;
812
1035
 
813
1036
  return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
814
- // Handle case: return a push function
815
1037
  if (isHandle(item)) {
816
1038
  const handle = item;
817
1039
  const ctx = getContext();
@@ -824,30 +1046,24 @@ export function createUseFunction<TEnv>(
824
1046
  );
825
1047
  }
826
1048
 
827
- // Return a push function bound to this handle and segment
828
- return (
829
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
830
- ) => {
831
- // If it's a function, call it immediately to get the promise
832
- const valueOrPromise =
833
- typeof dataOrFn === "function"
834
- ? (dataOrFn as () => Promise<unknown>)()
835
- : dataOrFn;
836
-
837
- // Push directly - promises will be serialized by RSC and streamed
838
- handleStore.push(handle.$$id, segmentId, valueOrPromise);
839
- };
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
+ );
840
1059
  }
841
1060
 
842
- // Loader case
843
1061
  const loader = item as LoaderDefinition<any, any>;
844
1062
 
845
- // Return cached promise if already started
846
1063
  if (loaderPromises.has(loader.$$id)) {
847
1064
  return loaderPromises.get(loader.$$id);
848
1065
  }
849
1066
 
850
- // Get loader function - either from loader object or fetchable registry
851
1067
  let loaderFn = loader.fn;
852
1068
  if (!loaderFn) {
853
1069
  const fetchable = getFetchableLoader(loader.$$id);
@@ -864,7 +1080,6 @@ export function createUseFunction<TEnv>(
864
1080
 
865
1081
  const ctx = getContext();
866
1082
 
867
- // Create loader context with recursive use() support
868
1083
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
869
1084
  params: ctx.params,
870
1085
  routeParams: (ctx.params ?? {}) as Record<string, string>,
@@ -873,15 +1088,16 @@ export function createUseFunction<TEnv>(
873
1088
  search: (ctx as any).search ?? {},
874
1089
  pathname: ctx.pathname,
875
1090
  url: ctx.url,
1091
+ originalUrl: ctx.originalUrl,
876
1092
  env: ctx.env as any,
877
- var: ctx.var as any,
1093
+ waitUntil: ctx.waitUntil.bind(ctx),
1094
+ executionContext: ctx.executionContext,
878
1095
  get: ctx.get as any,
879
- use: <TDep, TDepParams = any>(
1096
+ use: (<TDep, TDepParams = any>(
880
1097
  dep: LoaderDefinition<TDep, TDepParams>,
881
1098
  ): Promise<TDep> => {
882
- // Recursive call - will start dep loader if not already started
883
1099
  return ctx.use(dep);
884
- },
1100
+ }) as LoaderContext["use"],
885
1101
  method: "GET",
886
1102
  body: undefined,
887
1103
  reverse: createReverseFunction(
@@ -890,15 +1106,19 @@ export function createUseFunction<TEnv>(
890
1106
  ctx.params as Record<string, string>,
891
1107
  ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
892
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
+ },
893
1115
  };
894
1116
 
895
- // Start loader execution with tracking
896
1117
  const doneLoader = track(`loader:${loader.$$id}`, 2);
897
1118
  const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
898
1119
  doneLoader();
899
1120
  });
900
1121
 
901
- // Memoize for subsequent calls
902
1122
  loaderPromises.set(loader.$$id, promise);
903
1123
 
904
1124
  return promise;