@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918

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