@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.81

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 (316) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +5091 -941
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +61 -52
  7. package/skills/breadcrumbs/SKILL.md +250 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +167 -0
  14. package/skills/handler-use/SKILL.md +362 -0
  15. package/skills/hooks/SKILL.md +340 -72
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/intercept/SKILL.md +151 -8
  18. package/skills/layout/SKILL.md +122 -3
  19. package/skills/links/SKILL.md +92 -31
  20. package/skills/loader/SKILL.md +404 -44
  21. package/skills/middleware/SKILL.md +205 -37
  22. package/skills/migrate-nextjs/SKILL.md +560 -0
  23. package/skills/migrate-react-router/SKILL.md +765 -0
  24. package/skills/mime-routes/SKILL.md +128 -0
  25. package/skills/parallel/SKILL.md +263 -1
  26. package/skills/prerender/SKILL.md +685 -0
  27. package/skills/rango/SKILL.md +87 -16
  28. package/skills/response-routes/SKILL.md +411 -0
  29. package/skills/route/SKILL.md +281 -14
  30. package/skills/router-setup/SKILL.md +210 -32
  31. package/skills/tailwind/SKILL.md +129 -0
  32. package/skills/theme/SKILL.md +9 -8
  33. package/skills/typesafety/SKILL.md +328 -89
  34. package/skills/use-cache/SKILL.md +324 -0
  35. package/src/__internal.ts +102 -4
  36. package/src/bin/rango.ts +321 -0
  37. package/src/browser/action-coordinator.ts +97 -0
  38. package/src/browser/action-response-classifier.ts +99 -0
  39. package/src/browser/app-version.ts +14 -0
  40. package/src/browser/event-controller.ts +92 -64
  41. package/src/browser/history-state.ts +80 -0
  42. package/src/browser/intercept-utils.ts +52 -0
  43. package/src/browser/link-interceptor.ts +24 -4
  44. package/src/browser/logging.ts +55 -0
  45. package/src/browser/merge-segment-loaders.ts +20 -12
  46. package/src/browser/navigation-bridge.ts +317 -560
  47. package/src/browser/navigation-client.ts +206 -68
  48. package/src/browser/navigation-store.ts +73 -55
  49. package/src/browser/navigation-transaction.ts +297 -0
  50. package/src/browser/network-error-handler.ts +61 -0
  51. package/src/browser/partial-update.ts +343 -316
  52. package/src/browser/prefetch/cache.ts +216 -0
  53. package/src/browser/prefetch/fetch.ts +206 -0
  54. package/src/browser/prefetch/observer.ts +65 -0
  55. package/src/browser/prefetch/policy.ts +48 -0
  56. package/src/browser/prefetch/queue.ts +160 -0
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +112 -0
  59. package/src/browser/react/Link.tsx +253 -74
  60. package/src/browser/react/NavigationProvider.tsx +91 -11
  61. package/src/browser/react/context.ts +11 -0
  62. package/src/browser/react/filter-segment-order.ts +11 -0
  63. package/src/browser/react/index.ts +12 -12
  64. package/src/browser/react/location-state-shared.ts +95 -53
  65. package/src/browser/react/location-state.ts +60 -15
  66. package/src/browser/react/mount-context.ts +6 -1
  67. package/src/browser/react/nonce-context.ts +23 -0
  68. package/src/browser/react/shallow-equal.ts +27 -0
  69. package/src/browser/react/use-action.ts +29 -51
  70. package/src/browser/react/use-client-cache.ts +5 -3
  71. package/src/browser/react/use-handle.ts +30 -126
  72. package/src/browser/react/use-href.tsx +2 -2
  73. package/src/browser/react/use-link-status.ts +6 -5
  74. package/src/browser/react/use-navigation.ts +44 -65
  75. package/src/browser/react/use-params.ts +75 -0
  76. package/src/browser/react/use-pathname.ts +47 -0
  77. package/src/browser/react/use-router.ts +76 -0
  78. package/src/browser/react/use-search-params.ts +56 -0
  79. package/src/browser/react/use-segments.ts +80 -97
  80. package/src/browser/response-adapter.ts +73 -0
  81. package/src/browser/rsc-router.tsx +214 -58
  82. package/src/browser/scroll-restoration.ts +127 -52
  83. package/src/browser/segment-reconciler.ts +243 -0
  84. package/src/browser/segment-structure-assert.ts +16 -0
  85. package/src/browser/server-action-bridge.ts +510 -603
  86. package/src/browser/shallow.ts +6 -1
  87. package/src/browser/types.ts +141 -48
  88. package/src/browser/validate-redirect-origin.ts +29 -0
  89. package/src/build/generate-manifest.ts +235 -24
  90. package/src/build/generate-route-types.ts +39 -0
  91. package/src/build/index.ts +13 -0
  92. package/src/build/route-trie.ts +291 -0
  93. package/src/build/route-types/ast-helpers.ts +25 -0
  94. package/src/build/route-types/ast-route-extraction.ts +98 -0
  95. package/src/build/route-types/codegen.ts +102 -0
  96. package/src/build/route-types/include-resolution.ts +418 -0
  97. package/src/build/route-types/param-extraction.ts +48 -0
  98. package/src/build/route-types/per-module-writer.ts +128 -0
  99. package/src/build/route-types/router-processing.ts +618 -0
  100. package/src/build/route-types/scan-filter.ts +85 -0
  101. package/src/build/runtime-discovery.ts +231 -0
  102. package/src/cache/background-task.ts +34 -0
  103. package/src/cache/cache-key-utils.ts +44 -0
  104. package/src/cache/cache-policy.ts +125 -0
  105. package/src/cache/cache-runtime.ts +342 -0
  106. package/src/cache/cache-scope.ts +167 -309
  107. package/src/cache/cf/cf-cache-store.ts +571 -17
  108. package/src/cache/cf/index.ts +13 -3
  109. package/src/cache/document-cache.ts +116 -77
  110. package/src/cache/handle-capture.ts +81 -0
  111. package/src/cache/handle-snapshot.ts +41 -0
  112. package/src/cache/index.ts +1 -15
  113. package/src/cache/memory-segment-store.ts +191 -13
  114. package/src/cache/profile-registry.ts +73 -0
  115. package/src/cache/read-through-swr.ts +134 -0
  116. package/src/cache/segment-codec.ts +256 -0
  117. package/src/cache/taint.ts +153 -0
  118. package/src/cache/types.ts +72 -122
  119. package/src/client.rsc.tsx +3 -1
  120. package/src/client.tsx +135 -301
  121. package/src/component-utils.ts +4 -4
  122. package/src/components/DefaultDocument.tsx +5 -1
  123. package/src/context-var.ts +156 -0
  124. package/src/debug.ts +19 -9
  125. package/src/errors.ts +108 -2
  126. package/src/handle.ts +55 -29
  127. package/src/handles/MetaTags.tsx +73 -20
  128. package/src/handles/breadcrumbs.ts +66 -0
  129. package/src/handles/index.ts +1 -0
  130. package/src/handles/meta.ts +30 -13
  131. package/src/host/cookie-handler.ts +21 -15
  132. package/src/host/errors.ts +8 -8
  133. package/src/host/index.ts +4 -7
  134. package/src/host/pattern-matcher.ts +27 -27
  135. package/src/host/router.ts +61 -39
  136. package/src/host/testing.ts +8 -8
  137. package/src/host/types.ts +15 -7
  138. package/src/host/utils.ts +1 -1
  139. package/src/href-client.ts +119 -29
  140. package/src/index.rsc.ts +155 -19
  141. package/src/index.ts +251 -30
  142. package/src/internal-debug.ts +11 -0
  143. package/src/loader.rsc.ts +26 -157
  144. package/src/loader.ts +27 -10
  145. package/src/network-error-thrower.tsx +3 -1
  146. package/src/outlet-provider.tsx +45 -0
  147. package/src/prerender/param-hash.ts +37 -0
  148. package/src/prerender/store.ts +186 -0
  149. package/src/prerender.ts +524 -0
  150. package/src/reverse.ts +354 -0
  151. package/src/root-error-boundary.tsx +41 -29
  152. package/src/route-content-wrapper.tsx +7 -4
  153. package/src/route-definition/dsl-helpers.ts +1121 -0
  154. package/src/route-definition/helper-factories.ts +200 -0
  155. package/src/route-definition/helpers-types.ts +478 -0
  156. package/src/route-definition/index.ts +55 -0
  157. package/src/route-definition/redirect.ts +101 -0
  158. package/src/route-definition/resolve-handler-use.ts +149 -0
  159. package/src/route-definition.ts +1 -1428
  160. package/src/route-map-builder.ts +217 -123
  161. package/src/route-name.ts +53 -0
  162. package/src/route-types.ts +77 -8
  163. package/src/router/content-negotiation.ts +215 -0
  164. package/src/router/debug-manifest.ts +72 -0
  165. package/src/router/error-handling.ts +9 -9
  166. package/src/router/find-match.ts +160 -0
  167. package/src/router/handler-context.ts +438 -86
  168. package/src/router/intercept-resolution.ts +402 -0
  169. package/src/router/lazy-includes.ts +237 -0
  170. package/src/router/loader-resolution.ts +356 -128
  171. package/src/router/logging.ts +251 -0
  172. package/src/router/manifest.ts +163 -35
  173. package/src/router/match-api.ts +555 -0
  174. package/src/router/match-context.ts +5 -3
  175. package/src/router/match-handlers.ts +440 -0
  176. package/src/router/match-middleware/background-revalidation.ts +108 -93
  177. package/src/router/match-middleware/cache-lookup.ts +460 -10
  178. package/src/router/match-middleware/cache-store.ts +98 -26
  179. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  180. package/src/router/match-middleware/segment-resolution.ts +80 -6
  181. package/src/router/match-pipelines.ts +10 -45
  182. package/src/router/match-result.ts +135 -35
  183. package/src/router/metrics.ts +240 -15
  184. package/src/router/middleware-cookies.ts +55 -0
  185. package/src/router/middleware-types.ts +220 -0
  186. package/src/router/middleware.ts +324 -369
  187. package/src/router/navigation-snapshot.ts +182 -0
  188. package/src/router/pattern-matching.ts +211 -43
  189. package/src/router/prerender-match.ts +502 -0
  190. package/src/router/preview-match.ts +98 -0
  191. package/src/router/request-classification.ts +310 -0
  192. package/src/router/revalidation.ts +137 -38
  193. package/src/router/route-snapshot.ts +245 -0
  194. package/src/router/router-context.ts +41 -21
  195. package/src/router/router-interfaces.ts +484 -0
  196. package/src/router/router-options.ts +618 -0
  197. package/src/router/router-registry.ts +24 -0
  198. package/src/router/segment-resolution/fresh.ts +748 -0
  199. package/src/router/segment-resolution/helpers.ts +268 -0
  200. package/src/router/segment-resolution/loader-cache.ts +199 -0
  201. package/src/router/segment-resolution/revalidation.ts +1379 -0
  202. package/src/router/segment-resolution/static-store.ts +67 -0
  203. package/src/router/segment-resolution.ts +21 -0
  204. package/src/router/segment-wrappers.ts +291 -0
  205. package/src/router/telemetry-otel.ts +299 -0
  206. package/src/router/telemetry.ts +300 -0
  207. package/src/router/timeout.ts +148 -0
  208. package/src/router/trie-matching.ts +239 -0
  209. package/src/router/types.ts +78 -3
  210. package/src/router.ts +740 -4252
  211. package/src/rsc/handler-context.ts +45 -0
  212. package/src/rsc/handler.ts +907 -797
  213. package/src/rsc/helpers.ts +140 -6
  214. package/src/rsc/index.ts +0 -20
  215. package/src/rsc/loader-fetch.ts +229 -0
  216. package/src/rsc/manifest-init.ts +90 -0
  217. package/src/rsc/nonce.ts +14 -0
  218. package/src/rsc/origin-guard.ts +141 -0
  219. package/src/rsc/progressive-enhancement.ts +393 -0
  220. package/src/rsc/response-error.ts +37 -0
  221. package/src/rsc/response-route-handler.ts +347 -0
  222. package/src/rsc/rsc-rendering.ts +246 -0
  223. package/src/rsc/runtime-warnings.ts +42 -0
  224. package/src/rsc/server-action.ts +358 -0
  225. package/src/rsc/ssr-setup.ts +128 -0
  226. package/src/rsc/types.ts +46 -11
  227. package/src/search-params.ts +230 -0
  228. package/src/segment-content-promise.ts +67 -0
  229. package/src/segment-loader-promise.ts +122 -0
  230. package/src/segment-system.tsx +134 -36
  231. package/src/server/context.ts +341 -61
  232. package/src/server/cookie-store.ts +190 -0
  233. package/src/server/fetchable-loader-store.ts +37 -0
  234. package/src/server/handle-store.ts +113 -15
  235. package/src/server/loader-registry.ts +24 -64
  236. package/src/server/request-context.ts +607 -81
  237. package/src/server.ts +35 -130
  238. package/src/ssr/index.tsx +103 -30
  239. package/src/static-handler.ts +126 -0
  240. package/src/theme/ThemeProvider.tsx +21 -15
  241. package/src/theme/ThemeScript.tsx +5 -5
  242. package/src/theme/constants.ts +5 -2
  243. package/src/theme/index.ts +4 -14
  244. package/src/theme/theme-context.ts +4 -30
  245. package/src/theme/theme-script.ts +21 -18
  246. package/src/types/boundaries.ts +158 -0
  247. package/src/types/cache-types.ts +198 -0
  248. package/src/types/error-types.ts +192 -0
  249. package/src/types/global-namespace.ts +100 -0
  250. package/src/types/handler-context.ts +791 -0
  251. package/src/types/index.ts +88 -0
  252. package/src/types/loader-types.ts +210 -0
  253. package/src/types/route-config.ts +170 -0
  254. package/src/types/route-entry.ts +120 -0
  255. package/src/types/segments.ts +150 -0
  256. package/src/types.ts +1 -1623
  257. package/src/urls/include-helper.ts +207 -0
  258. package/src/urls/index.ts +53 -0
  259. package/src/urls/path-helper-types.ts +372 -0
  260. package/src/urls/path-helper.ts +364 -0
  261. package/src/urls/pattern-types.ts +107 -0
  262. package/src/urls/response-types.ts +116 -0
  263. package/src/urls/type-extraction.ts +372 -0
  264. package/src/urls/urls-function.ts +98 -0
  265. package/src/urls.ts +1 -802
  266. package/src/use-loader.tsx +161 -81
  267. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  268. package/src/vite/discovery/discover-routers.ts +348 -0
  269. package/src/vite/discovery/prerender-collection.ts +439 -0
  270. package/src/vite/discovery/route-types-writer.ts +258 -0
  271. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  272. package/src/vite/discovery/state.ts +117 -0
  273. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  274. package/src/vite/index.ts +15 -1133
  275. package/src/vite/plugin-types.ts +103 -0
  276. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  277. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  278. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  279. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  280. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  281. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  282. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  283. package/src/vite/plugins/expose-id-utils.ts +299 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  290. package/src/vite/plugins/performance-tracks.ts +88 -0
  291. package/src/vite/plugins/refresh-cmd.ts +127 -0
  292. package/src/vite/plugins/use-cache-transform.ts +323 -0
  293. package/src/vite/plugins/version-injector.ts +83 -0
  294. package/src/vite/plugins/version-plugin.ts +266 -0
  295. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +462 -0
  298. package/src/vite/router-discovery.ts +977 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  304. package/src/vite/utils/prerender-utils.ts +221 -0
  305. package/src/vite/utils/shared-utils.ts +170 -0
  306. package/CLAUDE.md +0 -43
  307. package/src/browser/lru-cache.ts +0 -69
  308. package/src/browser/request-controller.ts +0 -164
  309. package/src/cache/memory-store.ts +0 -253
  310. package/src/href-context.ts +0 -33
  311. package/src/href.ts +0 -255
  312. package/src/server/route-manifest-cache.ts +0 -173
  313. package/src/vite/expose-handle-id.ts +0 -209
  314. package/src/vite/expose-loader-id.ts +0 -426
  315. package/src/vite/expose-location-state-id.ts +0 -177
  316. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -13,13 +13,41 @@
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
14
  import type { CookieOptions } from "../router/middleware.js";
15
15
  import type { LoaderDefinition, LoaderContext } from "../types.js";
16
+ import type { ScopedReverseFunction } from "../reverse.js";
17
+ import type {
18
+ DefaultEnv,
19
+ DefaultReverseRouteMap,
20
+ DefaultRouteName,
21
+ } from "../types/global-namespace.js";
16
22
  import type { Handle } from "../handle.js";
17
- import { createHandleStore, type HandleStore } from "./handle-store.js";
23
+ import {
24
+ type ContextVar,
25
+ contextGet,
26
+ contextSet,
27
+ isNonCacheable,
28
+ } from "../context-var.js";
29
+ import {
30
+ createHandleStore,
31
+ buildHandleSnapshot,
32
+ type HandleStore,
33
+ type HandleData,
34
+ } from "./handle-store.js";
18
35
  import { isHandle } from "../handle.js";
19
- import { track } from "./context.js";
36
+ import { track, type MetricsStore } from "./context.js";
37
+ import { getFetchableLoader } from "./fetchable-loader-store.js";
20
38
  import type { SegmentCacheStore } from "../cache/types.js";
21
39
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
22
40
  import { THEME_COOKIE } from "../theme/constants.js";
41
+ import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
42
+ import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
43
+ import { isInsideCacheScope } from "./context.js";
44
+ import {
45
+ createReverseFunction,
46
+ stripInternalParams,
47
+ } from "../router/handler-context.js";
48
+ import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
49
+ import { invariant } from "../errors.js";
50
+ import { isAutoGeneratedRouteName } from "../route-name.js";
23
51
 
24
52
  /**
25
53
  * Unified request context available via getRequestContext()
@@ -28,46 +56,65 @@ import { THEME_COOKIE } from "../theme/constants.js";
28
56
  * Use this when you need access to request data outside of route handlers.
29
57
  */
30
58
  export interface RequestContext<
31
- TEnv = unknown,
59
+ TEnv = DefaultEnv,
32
60
  TParams = Record<string, string>,
33
61
  > {
34
62
  /** Platform bindings (Cloudflare env, etc.) */
35
63
  env: TEnv;
36
64
  /** Original HTTP request */
37
65
  request: Request;
38
- /** Parsed URL (system params like _rsc* are NOT filtered here) */
66
+ /** Parsed URL (with internal `_rsc*` params stripped) */
39
67
  url: URL;
68
+ /**
69
+ * The original request URL with all parameters intact, including
70
+ * internal `_rsc*` transport params.
71
+ */
72
+ originalUrl: URL;
40
73
  /** URL pathname */
41
74
  pathname: string;
42
- /** URL search params (system params like _rsc* are NOT filtered here) */
75
+ /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
43
76
  searchParams: URLSearchParams;
44
- /** Variables set by middleware (same as ctx.var) */
45
- var: Record<string, any>;
77
+ /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
78
+ _variables: Record<string, any>;
46
79
  /** Get a variable set by middleware */
47
- get: <K extends string>(key: K) => any;
80
+ get: {
81
+ <T>(contextVar: ContextVar<T>): T | undefined;
82
+ <K extends string>(key: K): any;
83
+ };
48
84
  /** Set a variable (shared with middleware and handlers) */
49
- set: <K extends string>(key: K, value: any) => void;
85
+ set: {
86
+ <T>(
87
+ contextVar: ContextVar<T>,
88
+ value: T,
89
+ options?: { cache?: boolean },
90
+ ): void;
91
+ <K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
92
+ };
50
93
  /**
51
94
  * Route params (populated after route matching)
52
95
  * Initially empty, then set to matched params
53
96
  */
54
97
  params: TParams;
55
- /**
56
- * Stub response for setting headers/cookies
57
- * Headers set here are merged into the final response
58
- */
59
- res: Response;
98
+ /** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
99
+ readonly res: Response;
60
100
 
61
- /** Get a cookie value from the request */
101
+ /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
62
102
  cookie(name: string): string | undefined;
63
- /** Get all cookies from the request */
103
+ /** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
64
104
  cookies(): Record<string, string>;
65
- /** Set a cookie on the response */
105
+ /** @internal Set a cookie on the response. Use cookies().set() instead. */
66
106
  setCookie(name: string, value: string, options?: CookieOptions): void;
67
- /** Delete a cookie */
68
- deleteCookie(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
107
+ /** @internal Delete a cookie. Use cookies().delete() instead. */
108
+ deleteCookie(
109
+ name: string,
110
+ options?: Pick<CookieOptions, "domain" | "path">,
111
+ ): void;
69
112
  /** Set a response header */
70
113
  header(name: string, value: string): void;
114
+ /** Set the response status code */
115
+ setStatus(status: number): void;
116
+ /** @internal Set status bypassing cache-exec guard (for framework error handling) */
117
+ _setStatus(status: number): void;
71
118
 
72
119
  /**
73
120
  * Access loader data or push handle data.
@@ -89,10 +136,12 @@ export interface RequestContext<
89
136
  * ```
90
137
  */
91
138
  use: {
92
- <T, TLoaderParams = any>(loader: LoaderDefinition<T, TLoaderParams>): Promise<T>;
93
- <TData, TAccumulated = TData[]>(handle: Handle<TData, TAccumulated>): (
94
- data: TData | Promise<TData> | (() => Promise<TData>)
95
- ) => void;
139
+ <T, TLoaderParams = any>(
140
+ loader: LoaderDefinition<T, TLoaderParams>,
141
+ ): Promise<T>;
142
+ <TData, TAccumulated = TData[]>(
143
+ handle: Handle<TData, TAccumulated>,
144
+ ): (data: TData | Promise<TData> | (() => Promise<TData>)) => void;
96
145
  };
97
146
 
98
147
  /** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
@@ -104,6 +153,12 @@ export interface RequestContext<
104
153
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
105
154
  _cacheStore?: SegmentCacheStore;
106
155
 
156
+ /** @internal Cache profiles for "use cache" profile resolution (per-router) */
157
+ _cacheProfiles?: Record<
158
+ string,
159
+ import("../cache/profile-registry.js").CacheProfile
160
+ >;
161
+
107
162
  /**
108
163
  * Schedule work to run after the response is sent.
109
164
  * On Cloudflare Workers, uses ctx.waitUntil().
@@ -177,8 +232,166 @@ export interface RequestContext<
177
232
 
178
233
  /** @internal Theme configuration (null if theme not enabled) */
179
234
  _themeConfig?: ResolvedThemeConfig | null;
235
+
236
+ /**
237
+ * Attach location state entries to the current response.
238
+ *
239
+ * For partial (SPA) requests, the state is included in the RSC payload
240
+ * metadata and merged into history.pushState on the client. For redirect
241
+ * responses, the state travels through the redirect payload so the target
242
+ * page can read it via useLocationState.
243
+ *
244
+ * Multiple calls accumulate entries.
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * ctx.setLocationState(Flash({ text: "Item saved!" }));
249
+ * ```
250
+ */
251
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
252
+
253
+ /** @internal Accumulated location state entries */
254
+ _locationState?: LocationStateEntry[];
255
+
256
+ /**
257
+ * The matched route name, if the route has an explicit name.
258
+ * Undefined before route matching or for unnamed routes.
259
+ * Includes the namespace prefix from include() (e.g., "blog.post").
260
+ */
261
+ routeName?: DefaultRouteName;
262
+
263
+ /**
264
+ * Generate URLs from route names.
265
+ * Uses the global route map. After route matching, scoped (`.name`) resolution
266
+ * works within the matched include() scope.
267
+ */
268
+ reverse: ScopedReverseFunction<
269
+ Record<string, string>,
270
+ DefaultReverseRouteMap
271
+ >;
272
+
273
+ /** @internal Route name from route matching, used for scoped reverse resolution */
274
+ _routeName?: string;
275
+
276
+ /** @internal Previous route key (from the navigation source), used for revalidation */
277
+ _prevRouteKey?: string;
278
+
279
+ /**
280
+ * @internal Render barrier for experimental `rendered()` API.
281
+ * Resolves when all non-loader segments have settled and handle data
282
+ * is available. Used by DSL loaders that call `ctx.rendered()`.
283
+ */
284
+ _renderBarrier: Promise<void>;
285
+
286
+ /**
287
+ * @internal Resolve the render barrier. Accepts resolved segments, filters
288
+ * out loaders, and captures non-loader segment IDs as the handle ordering.
289
+ * Called after segment resolution (fresh) or handle replay (cache/prerender).
290
+ */
291
+ _resolveRenderBarrier: (
292
+ segments: Array<{ type: string; id: string }>,
293
+ ) => void;
294
+
295
+ /**
296
+ * @internal Segment order at barrier resolution time, used by loader
297
+ * ctx.use(handle) to collect handle data in correct order.
298
+ */
299
+ _renderBarrierSegmentOrder?: string[];
300
+
301
+ /**
302
+ * @internal Set to true when the matched entry tree contains any `loading()`
303
+ * entries (streaming). Used by rendered() to fail fast.
304
+ */
305
+ _treeHasStreaming?: boolean;
306
+
307
+ /**
308
+ * @internal Loader IDs that have called rendered() and are waiting for the
309
+ * barrier. Used to detect deadlocks when a handler tries to await the same
310
+ * loader via ctx.use(Loader).
311
+ */
312
+ _renderBarrierWaiters?: Set<string>;
313
+
314
+ /**
315
+ * @internal Loader IDs that handlers have started awaiting via ctx.use().
316
+ * Used for bidirectional deadlock detection: if a loader later calls
317
+ * rendered() and a handler already awaits it, we can detect the deadlock.
318
+ */
319
+ _handlerLoaderDeps?: Set<string>;
320
+
321
+ /**
322
+ * @internal Cached HandleData snapshot built at barrier resolution time.
323
+ * Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
324
+ */
325
+ _renderBarrierHandleSnapshot?: HandleData;
326
+
327
+ /** @internal Per-request error dedup set for onError reporting */
328
+ _reportedErrors: WeakSet<object>;
329
+
330
+ /**
331
+ * @internal Report a non-fatal background error through the router's
332
+ * onError callback. Wired by the RSC handler / router during request
333
+ * creation. Cache-runtime and other subsystems call this to surface
334
+ * errors without failing the response.
335
+ */
336
+ _reportBackgroundError?: (error: unknown, category: string) => void;
337
+
338
+ /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
339
+ _debugPerformance?: boolean;
340
+
341
+ /** @internal Request-scoped performance metrics store */
342
+ _metricsStore?: MetricsStore;
343
+
344
+ /** @internal Router basename for this request (used by redirect()) */
345
+ _basename?: string;
346
+
347
+ /**
348
+ * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
349
+ * to avoid a second resolveRoute call. Cleared on HMR invalidation.
350
+ */
351
+ _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
180
352
  }
181
353
 
354
+ /**
355
+ * Public view of RequestContext, without internal methods and fields.
356
+ *
357
+ * This is the type exported to library consumers. Internal code should
358
+ * use the full RequestContext interface directly.
359
+ */
360
+ export type PublicRequestContext<
361
+ TEnv = DefaultEnv,
362
+ TParams = Record<string, string>,
363
+ > = Omit<
364
+ RequestContext<TEnv, TParams>,
365
+ | "cookie"
366
+ | "cookies"
367
+ | "setCookie"
368
+ | "deleteCookie"
369
+ | "_handleStore"
370
+ | "_cacheStore"
371
+ | "_cacheProfiles"
372
+ | "_onResponseCallbacks"
373
+ | "_themeConfig"
374
+ | "_locationState"
375
+ | "_routeName"
376
+ | "_prevRouteKey"
377
+ | "_reportedErrors"
378
+ | "_renderBarrier"
379
+ | "_resolveRenderBarrier"
380
+ | "_renderBarrierSegmentOrder"
381
+ | "_treeHasStreaming"
382
+ | "_renderBarrierWaiters"
383
+ | "_handlerLoaderDeps"
384
+ | "_renderBarrierHandleSnapshot"
385
+ | "_reportBackgroundError"
386
+ | "_debugPerformance"
387
+ | "_metricsStore"
388
+ | "_basename"
389
+ | "_setStatus"
390
+ | "_variables"
391
+ | "_classifiedRoute"
392
+ | "res"
393
+ >;
394
+
182
395
  // AsyncLocalStorage instance for request context
183
396
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
184
397
 
@@ -188,16 +401,33 @@ const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
188
401
  */
189
402
  export function runWithRequestContext<TEnv, T>(
190
403
  context: RequestContext<TEnv>,
191
- fn: () => T
404
+ fn: () => T,
192
405
  ): T {
193
406
  return requestContextStorage.run(context, fn);
194
407
  }
195
408
 
196
409
  /**
197
410
  * Get the current request context
198
- * Returns undefined if not running within a request context
411
+ * Throws if called outside of a request context
412
+ */
413
+ export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
414
+ const ctx = requestContextStorage.getStore() as
415
+ | RequestContext<TEnv>
416
+ | undefined;
417
+ invariant(
418
+ ctx,
419
+ "getRequestContext() called outside of a request context. " +
420
+ "This function must be called from within a route handler, loader, middleware, " +
421
+ "server action, or server component.",
422
+ );
423
+ return ctx;
424
+ }
425
+
426
+ /**
427
+ * @internal Get the request context without throwing — for internal code that
428
+ * may run outside a request context (cache stores, optional handle lookups, etc.)
199
429
  */
200
- export function getRequestContext<TEnv = unknown>():
430
+ export function _getRequestContext<TEnv = DefaultEnv>():
201
431
  | RequestContext<TEnv>
202
432
  | undefined {
203
433
  return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
@@ -205,28 +435,67 @@ export function getRequestContext<TEnv = unknown>():
205
435
 
206
436
  /**
207
437
  * Update params on the current request context
208
- * Called after route matching to populate route params
438
+ * Called after route matching to populate route params and route name
209
439
  */
210
- export function setRequestContextParams(params: Record<string, string>): void {
440
+ export function setRequestContextParams(
441
+ params: Record<string, string>,
442
+ routeName?: string,
443
+ ): void {
211
444
  const ctx = requestContextStorage.getStore();
212
445
  if (ctx) {
213
446
  ctx.params = params;
447
+ if (routeName !== undefined) {
448
+ ctx._routeName = routeName;
449
+ ctx.routeName = (
450
+ routeName && !isAutoGeneratedRouteName(routeName)
451
+ ? routeName
452
+ : undefined
453
+ ) as DefaultRouteName | undefined;
454
+ }
455
+ // Update reverse with scoped resolution now that route is known
456
+ ctx.reverse = createReverseFunction(
457
+ getGlobalRouteMap(),
458
+ routeName,
459
+ params,
460
+ routeName ? isRouteRootScoped(routeName) : undefined,
461
+ );
214
462
  }
215
463
  }
216
464
 
217
465
  /**
218
- * Get the current request context, throwing if not available
219
- * Use this when context is required (e.g., in loader actions)
466
+ * Store the previous route key on the request context.
467
+ * Called during partial-match context creation to make the navigation source
468
+ * route key available for revalidation and intercept evaluation.
469
+ * @internal
220
470
  */
221
- export function requireRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
222
- const ctx = getRequestContext<TEnv>();
223
- if (!ctx) {
224
- throw new Error(
225
- "Request context not available. This function must be called from within a server action " +
226
- "executed through the RSC handler."
227
- );
471
+ export function setRequestContextPrevRouteKey(
472
+ prevRouteKey: string | undefined,
473
+ ): void {
474
+ const ctx = requestContextStorage.getStore();
475
+ if (ctx && prevRouteKey !== undefined) {
476
+ ctx._prevRouteKey = prevRouteKey;
228
477
  }
229
- return ctx;
478
+ }
479
+
480
+ /**
481
+ * Get accumulated location state entries from the current request context.
482
+ * Returns undefined if no state has been set.
483
+ *
484
+ * @internal Used by the RSC handler to include state in payload metadata.
485
+ */
486
+ export function getLocationState(): LocationStateEntry[] | undefined {
487
+ const ctx = getRequestContext();
488
+ return ctx?._locationState;
489
+ }
490
+
491
+ /**
492
+ * Get the current request context, throwing if not available
493
+ * @deprecated Use getRequestContext() directly — it now throws if outside context
494
+ */
495
+ export function requireRequestContext<
496
+ TEnv = DefaultEnv,
497
+ >(): RequestContext<TEnv> {
498
+ return getRequestContext<TEnv>();
230
499
  }
231
500
 
232
501
  /**
@@ -245,8 +514,15 @@ export interface CreateRequestContextOptions<TEnv> {
245
514
  request: Request;
246
515
  url: URL;
247
516
  variables: Record<string, any>;
517
+ /** Optional initial response stub headers/status to seed effective cookie reads */
518
+ initialResponse?: Response;
248
519
  /** Optional cache store for segment caching (used by CacheScope) */
249
520
  cacheStore?: SegmentCacheStore;
521
+ /** Optional cache profiles for "use cache" resolution (per-router) */
522
+ cacheProfiles?: Record<
523
+ string,
524
+ import("../cache/profile-registry.js").CacheProfile
525
+ >;
250
526
  /** Optional Cloudflare execution context for waitUntil support */
251
527
  executionContext?: ExecutionContext;
252
528
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
@@ -262,20 +538,37 @@ export interface CreateRequestContextOptions<TEnv> {
262
538
  * - Passed to handlers as ctx
263
539
  */
264
540
  export function createRequestContext<TEnv>(
265
- options: CreateRequestContextOptions<TEnv>
541
+ options: CreateRequestContextOptions<TEnv>,
266
542
  ): RequestContext<TEnv> {
267
- const { env, request, url, variables, cacheStore, executionContext, themeConfig } = options;
543
+ const {
544
+ env,
545
+ request,
546
+ url,
547
+ variables,
548
+ initialResponse,
549
+ cacheStore,
550
+ cacheProfiles,
551
+ executionContext,
552
+ themeConfig,
553
+ } = options;
268
554
  const cookieHeader = request.headers.get("Cookie");
269
555
  let parsedCookies: Record<string, string> | null = null;
270
556
 
271
- // Create stub response for collecting headers/cookies
272
- const stubResponse = new Response(null, { status: 200 });
557
+ // Create stub response for collecting headers/cookies.
558
+ // All cookie/header mutations go here; cookie reads derive from it.
559
+ let stubResponse = initialResponse
560
+ ? new Response(null, {
561
+ status: initialResponse.status,
562
+ statusText: initialResponse.statusText,
563
+ headers: new Headers(initialResponse.headers),
564
+ })
565
+ : new Response(null, { status: 200 });
273
566
 
274
567
  // Create handle store and loader memoization for this request
275
568
  const handleStore = createHandleStore();
276
569
  const loaderPromises = new Map<string, Promise<any>>();
277
570
 
278
- // Lazy parse cookies
571
+ // Lazy parse cookies from the original Cookie header
279
572
  const getParsedCookies = (): Record<string, string> => {
280
573
  if (!parsedCookies) {
281
574
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -283,11 +576,47 @@ export function createRequestContext<TEnv>(
283
576
  return parsedCookies;
284
577
  };
285
578
 
579
+ // Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
580
+ let responseCookieCache: Map<string, string | null> | null = null;
581
+ const getResponseCookies = (): Map<string, string | null> => {
582
+ if (!responseCookieCache) {
583
+ responseCookieCache = parseResponseCookies(stubResponse);
584
+ }
585
+ return responseCookieCache;
586
+ };
587
+ const invalidateResponseCookieCache = () => {
588
+ responseCookieCache = null;
589
+ };
590
+
591
+ // Guard: throw if a response-level side effect is called inside a cache() scope.
592
+ // Uses ALS to detect the scope (set during segment resolution).
593
+ function assertNotInsideCacheScopeALS(methodName: string): void {
594
+ if (isInsideCacheScope()) {
595
+ throw new Error(
596
+ `ctx.${methodName}() cannot be called inside a cache() boundary. ` +
597
+ `On cache hit the handler is skipped, so this side effect would be lost. ` +
598
+ `Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
599
+ );
600
+ }
601
+ }
602
+
603
+ // Effective cookie read: response stub Set-Cookie wins, then original header.
604
+ // The stub IS the source of truth for same-request mutations.
605
+ const effectiveCookie = (name: string): string | undefined => {
606
+ const mutations = getResponseCookies();
607
+ if (mutations.has(name)) {
608
+ const v = mutations.get(name);
609
+ return v === null ? undefined : v;
610
+ }
611
+ return getParsedCookies()[name];
612
+ };
613
+
286
614
  // Theme helpers (only used when themeConfig is provided)
287
615
  const getTheme = (): Theme | undefined => {
288
616
  if (!themeConfig) return undefined;
289
617
 
290
- const stored = getParsedCookies()[themeConfig.storageKey];
618
+ // Use overlay-aware read so setTheme() in the same request is reflected
619
+ const stored = effectiveCookie(themeConfig.storageKey);
291
620
  if (stored) {
292
621
  // Validate stored value
293
622
  if (stored === "system" && themeConfig.enableSystem) {
@@ -305,65 +634,129 @@ export function createRequestContext<TEnv>(
305
634
 
306
635
  // Validate theme value
307
636
  if (theme !== "system" && !themeConfig.themes.includes(theme)) {
308
- console.warn(`[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`);
637
+ console.warn(
638
+ `[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
639
+ );
309
640
  return;
310
641
  }
311
642
 
312
- // Set cookie
643
+ // Write to stub — effectiveCookie() will pick it up on next read
313
644
  stubResponse.headers.append(
314
645
  "Set-Cookie",
315
646
  serializeCookieValue(themeConfig.storageKey, theme, {
316
647
  path: THEME_COOKIE.path,
317
648
  maxAge: THEME_COOKIE.maxAge,
318
649
  sameSite: THEME_COOKIE.sameSite,
319
- })
650
+ }),
320
651
  );
652
+ invalidateResponseCookieCache();
321
653
  };
322
654
 
655
+ // Strip internal _rsc* params so userland sees a clean URL.
656
+ const cleanUrl = stripInternalParams(url);
657
+
323
658
  // Build the context object first (without use), then add use
324
659
  const ctx: RequestContext<TEnv> = {
325
660
  env,
326
661
  request,
327
- url,
662
+ url: cleanUrl,
663
+ originalUrl: new URL(request.url),
328
664
  pathname: url.pathname,
329
- searchParams: url.searchParams,
330
- var: variables,
331
- get: <K extends string>(key: K) => variables[key],
332
- set: <K extends string>(key: K, value: any) => {
333
- variables[key] = value;
334
- },
665
+ searchParams: cleanUrl.searchParams,
666
+ _variables: variables,
667
+ get: ((keyOrVar: any) => {
668
+ if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
669
+ throw new Error(
670
+ `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
671
+ `The variable was created with { cache: false } or set with { cache: false }, ` +
672
+ `and its value would be stale on cache hit. Move the read outside the cached scope.`,
673
+ );
674
+ }
675
+ return contextGet(variables, keyOrVar);
676
+ }) as RequestContext<TEnv>["get"],
677
+ set: ((keyOrVar: any, value: any, options?: any) => {
678
+ assertNotInsideCacheExec(ctx, "set");
679
+ contextSet(variables, keyOrVar, value, options);
680
+ }) as RequestContext<TEnv>["set"],
335
681
  params: {} as Record<string, string>,
336
- res: stubResponse,
682
+
683
+ get res(): Response {
684
+ return stubResponse;
685
+ },
686
+ set res(_: Response) {
687
+ throw new Error(
688
+ "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
689
+ );
690
+ },
337
691
 
338
692
  cookie(name: string): string | undefined {
339
- return getParsedCookies()[name];
693
+ return effectiveCookie(name);
340
694
  },
341
695
 
342
696
  cookies(): Record<string, string> {
343
- return { ...getParsedCookies() };
697
+ const parsed = getParsedCookies();
698
+ const mutations = getResponseCookies();
699
+ if (mutations.size === 0) return { ...parsed };
700
+ // Build result without delete (avoids V8 dictionary-mode de-opt)
701
+ const deleted = new Set<string>();
702
+ for (const [k, v] of mutations) {
703
+ if (v === null) deleted.add(k);
704
+ }
705
+ const result: Record<string, string> = {};
706
+ for (const key of Object.keys(parsed)) {
707
+ if (!deleted.has(key)) result[key] = parsed[key];
708
+ }
709
+ for (const [k, v] of mutations) {
710
+ if (v !== null) result[k] = v;
711
+ }
712
+ return result;
344
713
  },
345
714
 
346
715
  setCookie(name: string, value: string, options?: CookieOptions): void {
716
+ assertNotInsideCacheExec(ctx, "setCookie");
717
+ assertNotInsideCacheScopeALS("setCookie");
347
718
  stubResponse.headers.append(
348
719
  "Set-Cookie",
349
- serializeCookieValue(name, value, options)
720
+ serializeCookieValue(name, value, options),
350
721
  );
722
+ invalidateResponseCookieCache();
351
723
  },
352
724
 
353
725
  deleteCookie(
354
726
  name: string,
355
- options?: Pick<CookieOptions, "domain" | "path">
727
+ options?: Pick<CookieOptions, "domain" | "path">,
356
728
  ): void {
729
+ assertNotInsideCacheExec(ctx, "deleteCookie");
730
+ assertNotInsideCacheScopeALS("deleteCookie");
357
731
  stubResponse.headers.append(
358
732
  "Set-Cookie",
359
- serializeCookieValue(name, "", { ...options, maxAge: 0 })
733
+ serializeCookieValue(name, "", { ...options, maxAge: 0 }),
360
734
  );
735
+ invalidateResponseCookieCache();
361
736
  },
362
737
 
363
738
  header(name: string, value: string): void {
739
+ assertNotInsideCacheExec(ctx, "header");
740
+ assertNotInsideCacheScopeALS("header");
364
741
  stubResponse.headers.set(name, value);
365
742
  },
366
743
 
744
+ setStatus(status: number): void {
745
+ assertNotInsideCacheExec(ctx, "setStatus");
746
+ assertNotInsideCacheScopeALS("setStatus");
747
+ stubResponse = new Response(null, {
748
+ status,
749
+ headers: stubResponse.headers,
750
+ });
751
+ },
752
+
753
+ _setStatus(status: number): void {
754
+ stubResponse = new Response(null, {
755
+ status,
756
+ headers: stubResponse.headers,
757
+ });
758
+ },
759
+
367
760
  // Placeholder - will be replaced below
368
761
  use: null as any,
369
762
 
@@ -371,6 +764,7 @@ export function createRequestContext<TEnv>(
371
764
 
372
765
  _handleStore: handleStore,
373
766
  _cacheStore: cacheStore,
767
+ _cacheProfiles: cacheProfiles,
374
768
 
375
769
  waitUntil(fn: () => Promise<void>): void {
376
770
  if (executionContext?.waitUntil) {
@@ -378,22 +772,96 @@ export function createRequestContext<TEnv>(
378
772
  executionContext.waitUntil(fn());
379
773
  } else {
380
774
  // Node.js / dev: fire-and-forget with error logging
381
- fn().catch((err) => console.error("[waitUntil] Background task failed:", err));
775
+ fn().catch((err) =>
776
+ console.error("[waitUntil] Background task failed:", err),
777
+ );
382
778
  }
383
779
  },
384
780
 
385
781
  _onResponseCallbacks: [],
386
782
 
387
783
  onResponse(callback: (response: Response) => Response): void {
784
+ assertNotInsideCacheExec(ctx, "onResponse");
785
+ assertNotInsideCacheScopeALS("onResponse");
388
786
  this._onResponseCallbacks.push(callback);
389
787
  },
390
788
 
391
789
  // Theme properties (only set when themeConfig is provided)
392
- theme: themeConfig ? getTheme() : undefined,
393
- setTheme: themeConfig ? setTheme : undefined,
790
+ get theme() {
791
+ return themeConfig ? getTheme() : undefined;
792
+ },
793
+ setTheme: themeConfig
794
+ ? (theme: Theme) => {
795
+ assertNotInsideCacheExec(ctx, "setTheme");
796
+ setTheme(theme);
797
+ }
798
+ : undefined,
394
799
  _themeConfig: themeConfig,
800
+
801
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
802
+ assertNotInsideCacheExec(ctx, "setLocationState");
803
+ const arr = Array.isArray(entries) ? entries : [entries];
804
+ this._locationState = this._locationState
805
+ ? [...this._locationState, ...arr]
806
+ : arr;
807
+ },
808
+ _locationState: undefined,
809
+
810
+ _reportedErrors: new WeakSet<object>(),
811
+ _metricsStore: undefined,
812
+
813
+ // Render barrier: deferred promise resolved after non-loader segments settle.
814
+ _renderBarrier: null as any, // set below
815
+ _resolveRenderBarrier: null as any, // set below
816
+ _renderBarrierSegmentOrder: undefined,
817
+
818
+ reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
395
819
  };
396
820
 
821
+ // Lazy render barrier: only allocate the Promise when a loader actually
822
+ // calls rendered(). Requests that don't use rendered() pay zero cost.
823
+ let barrierResolved = false;
824
+ let resolveBarrier: (() => void) | undefined;
825
+ ctx._renderBarrier = null as any; // lazy — created on first access
826
+ ctx._resolveRenderBarrier = (
827
+ segments: Array<{ type: string; id: string }>,
828
+ ) => {
829
+ if (barrierResolved) return;
830
+ barrierResolved = true;
831
+ const segOrder = segments
832
+ .filter((s) => s.type !== "loader")
833
+ .map((s) => s.id);
834
+ ctx._renderBarrierSegmentOrder = segOrder;
835
+ // Build and cache handle snapshot so loader ctx.use(handle) calls
836
+ // don't rebuild it on every invocation.
837
+ ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
838
+ handleStore,
839
+ segOrder,
840
+ );
841
+ ctx._renderBarrierWaiters = undefined;
842
+ ctx._handlerLoaderDeps = undefined;
843
+ if (resolveBarrier) resolveBarrier();
844
+ };
845
+ Object.defineProperty(ctx, "_renderBarrier", {
846
+ get() {
847
+ // Barrier already resolved (cache/prerender hit) or first lazy access.
848
+ // Either way, replace the getter with a concrete value to avoid
849
+ // repeated Promise.resolve() allocations on subsequent reads.
850
+ const p = barrierResolved
851
+ ? Promise.resolve()
852
+ : new Promise<void>((resolve) => {
853
+ resolveBarrier = resolve;
854
+ });
855
+ Object.defineProperty(ctx, "_renderBarrier", {
856
+ value: p,
857
+ writable: false,
858
+ configurable: false,
859
+ });
860
+ return p;
861
+ },
862
+ configurable: true,
863
+ });
864
+
397
865
  // Now create use() with access to ctx
398
866
  ctx.use = createUseFunction({
399
867
  handleStore,
@@ -401,14 +869,53 @@ export function createRequestContext<TEnv>(
401
869
  getContext: () => ctx,
402
870
  });
403
871
 
872
+ // Brand with taint symbol so "use cache" excludes ctx from cache keys
873
+ (ctx as any)[NOCACHE_SYMBOL] = true;
404
874
  return ctx;
405
875
  }
406
876
 
877
+ /**
878
+ * Parse Set-Cookie headers from a response into effective cookie state.
879
+ * Returns a map of cookie name -> value (string) or name -> null (deleted).
880
+ * Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
881
+ * Max-Age=0 is treated as a delete.
882
+ */
883
+ const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
884
+
885
+ function parseResponseCookies(response: Response): Map<string, string | null> {
886
+ const result = new Map<string, string | null>();
887
+ const setCookies = response.headers.getSetCookie();
888
+
889
+ for (const header of setCookies) {
890
+ // First segment before ';' is the name=value pair
891
+ const semiIdx = header.indexOf(";");
892
+ const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
893
+ const eqIdx = pair.indexOf("=");
894
+ if (eqIdx === -1) continue;
895
+
896
+ let name: string;
897
+ let value: string;
898
+ try {
899
+ name = decodeURIComponent(pair.substring(0, eqIdx).trim());
900
+ value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
901
+ } catch {
902
+ // Malformed encoding — skip this entry
903
+ continue;
904
+ }
905
+
906
+ // Max-Age=0 means the cookie is being deleted
907
+ const isDeleted = MAX_AGE_ZERO_RE.test(header);
908
+ result.set(name, isDeleted ? null : value);
909
+ }
910
+
911
+ return result;
912
+ }
913
+
407
914
  /**
408
915
  * Parse cookies from Cookie header
409
916
  */
410
917
  function parseCookiesFromHeader(
411
- cookieHeader: string | null
918
+ cookieHeader: string | null,
412
919
  ): Record<string, string> {
413
920
  if (!cookieHeader) return {};
414
921
 
@@ -418,7 +925,13 @@ function parseCookiesFromHeader(
418
925
  for (const pair of pairs) {
419
926
  const [name, ...rest] = pair.trim().split("=");
420
927
  if (name) {
421
- cookies[name] = decodeURIComponent(rest.join("="));
928
+ const raw = rest.join("=");
929
+ try {
930
+ cookies[name] = decodeURIComponent(raw);
931
+ } catch {
932
+ // Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
933
+ cookies[name] = raw;
934
+ }
422
935
  }
423
936
  }
424
937
 
@@ -431,7 +944,7 @@ function parseCookiesFromHeader(
431
944
  function serializeCookieValue(
432
945
  name: string,
433
946
  value: string,
434
- options: CookieOptions = {}
947
+ options: CookieOptions = {},
435
948
  ): string {
436
949
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
437
950
 
@@ -463,7 +976,7 @@ export interface CreateUseFunctionOptions<TEnv> {
463
976
  * - For handles: returns a push function to add handle data
464
977
  */
465
978
  export function createUseFunction<TEnv>(
466
- options: CreateUseFunctionOptions<TEnv>
979
+ options: CreateUseFunctionOptions<TEnv>,
467
980
  ): RequestContext["use"] {
468
981
  const { handleStore, loaderPromises, getContext } = options;
469
982
 
@@ -477,16 +990,19 @@ export function createUseFunction<TEnv>(
477
990
  if (!segmentId) {
478
991
  throw new Error(
479
992
  `Handle "${handle.$$id}" used outside of handler context. ` +
480
- `Handles must be used within route/layout handlers.`
993
+ `Handles must be used within route/layout handlers.`,
481
994
  );
482
995
  }
483
996
 
484
997
  // Return a push function bound to this handle and segment
485
- return (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
998
+ return (
999
+ dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
1000
+ ) => {
486
1001
  // If it's a function, call it immediately to get the promise
487
- const valueOrPromise = typeof dataOrFn === "function"
488
- ? (dataOrFn as () => Promise<unknown>)()
489
- : dataOrFn;
1002
+ const valueOrPromise =
1003
+ typeof dataOrFn === "function"
1004
+ ? (dataOrFn as () => Promise<unknown>)()
1005
+ : dataOrFn;
490
1006
 
491
1007
  // Push directly - promises will be serialized by RSC and streamed
492
1008
  handleStore.push(handle.$$id, segmentId, valueOrPromise);
@@ -504,8 +1020,6 @@ export function createUseFunction<TEnv>(
504
1020
  // Get loader function - either from loader object or fetchable registry
505
1021
  let loaderFn = loader.fn;
506
1022
  if (!loaderFn) {
507
- // Lazy import to avoid circular dependency
508
- const { getFetchableLoader } = require("../loader.rsc.js");
509
1023
  const fetchable = getFetchableLoader(loader.$$id);
510
1024
  if (fetchable) {
511
1025
  loaderFn = fetchable.fn;
@@ -514,7 +1028,7 @@ export function createUseFunction<TEnv>(
514
1028
 
515
1029
  if (!loaderFn) {
516
1030
  throw new Error(
517
- `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`
1031
+ `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`,
518
1032
  );
519
1033
  }
520
1034
 
@@ -523,25 +1037,37 @@ export function createUseFunction<TEnv>(
523
1037
  // Create loader context with recursive use() support
524
1038
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
525
1039
  params: ctx.params,
1040
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
526
1041
  request: ctx.request,
527
1042
  searchParams: ctx.searchParams,
1043
+ search: (ctx as any).search ?? {},
528
1044
  pathname: ctx.pathname,
529
1045
  url: ctx.url,
530
1046
  env: ctx.env as any,
531
- var: ctx.var as any,
532
1047
  get: ctx.get as any,
533
- use: <TDep, TDepParams = any>(
534
- dep: LoaderDefinition<TDep, TDepParams>
1048
+ use: (<TDep, TDepParams = any>(
1049
+ dep: LoaderDefinition<TDep, TDepParams>,
535
1050
  ): Promise<TDep> => {
536
1051
  // Recursive call - will start dep loader if not already started
537
1052
  return ctx.use(dep);
538
- },
1053
+ }) as LoaderContext["use"],
539
1054
  method: "GET",
540
1055
  body: undefined,
1056
+ reverse: createReverseFunction(
1057
+ getGlobalRouteMap(),
1058
+ ctx._routeName,
1059
+ ctx.params as Record<string, string>,
1060
+ ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
1061
+ ),
1062
+ rendered: () => {
1063
+ throw new Error(
1064
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
1065
+ `It cannot be used from request-context loaders or server actions.`,
1066
+ );
1067
+ },
541
1068
  };
542
1069
 
543
- // Start loader execution with tracking
544
- const doneLoader = track(`loader:${loader.$$id}`);
1070
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
545
1071
  const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
546
1072
  doneLoader();
547
1073
  });