@rangojs/router 0.17.0 → 0.19.0

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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -17,6 +17,7 @@ import type { CookieOptions } from "../router/middleware.js";
17
17
  import {
18
18
  KEEP_CACHE_HEADER,
19
19
  getRawCookieValue,
20
+ freshReadsCookieName,
20
21
  mintStateValue,
21
22
  serializeStateCookie,
22
23
  } from "../browser/cookie-name.js";
@@ -48,7 +49,7 @@ import { getFetchableLoader } from "./fetchable-loader-store.js";
48
49
  import type { SegmentCacheStore } from "../cache/types.js";
49
50
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
50
51
  import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
51
- import type { TransitionWhenFn } from "../types/segments.js";
52
+ import type { TransitionWhenRecord } from "../router/transition-when.js";
52
53
  import type { ResolvedTracing } from "../router/tracing.js";
53
54
  import type { RenderMode, RenderPhase } from "../router/timeout.js";
54
55
  import {
@@ -64,6 +65,12 @@ import {
64
65
  assertNonCacheableReadAllowed,
65
66
  clearPprHeaderScope,
66
67
  } from "./context.js";
68
+ import {
69
+ guardRawCookieRead,
70
+ guardRequestHeaders,
71
+ readGuardedTheme,
72
+ } from "./cookie-store.js";
73
+ import { requestHeaders } from "./request-headers.js";
67
74
  import {
68
75
  createReverseFunction,
69
76
  stripInternalParams,
@@ -74,6 +81,10 @@ import {
74
81
  getSearchSchema,
75
82
  } from "../route-map-builder.js";
76
83
  import { parseSearchParams } from "../search-params.js";
84
+ import {
85
+ executionStart,
86
+ type ExecutionStart,
87
+ } from "../cache/invalidation-order.js";
77
88
  import { invariant } from "../errors.js";
78
89
  import { isAutoGeneratedRouteName } from "../route-name.js";
79
90
 
@@ -144,10 +155,24 @@ export interface RequestContext<
144
155
  /** @internal Request-local PPR opt-out marker set by ctx.dynamic(). */
145
156
  _dynamic?: boolean;
146
157
 
147
- /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
158
+ /**
159
+ * @internal Get a cookie value (effective: request + response mutations).
160
+ * Use cookies().get() instead. Guarded like cookies() (#976).
161
+ */
148
162
  cookie(name: string): string | undefined;
149
- /** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
163
+ /**
164
+ * @internal Get all cookies (effective merged view). Use
165
+ * cookies().getAll() instead. Guarded like cookies() (#976).
166
+ */
150
167
  cookies(): Record<string, string>;
168
+ /**
169
+ * @internal `cookie()` without the identity guard: the read behind the
170
+ * cookies() store, which guards at its own call and records per read
171
+ * method. Non-enumerable, like `_readTheme`.
172
+ */
173
+ _readCookie(name: string): string | undefined;
174
+ /** @internal `cookies()` without the identity guard (see `_readCookie`). */
175
+ _readCookies(): Record<string, string>;
151
176
  /** @internal Set a cookie on the response. Use cookies().set() instead. */
152
177
  setCookie(name: string, value: string, options?: CookieOptions): void;
153
178
  /** @internal Delete a cookie. Use cookies().delete() instead. */
@@ -165,6 +190,39 @@ export interface RequestContext<
165
190
  _rotateStateCookie(): void;
166
191
  /** @internal Set the keepClientCache() directive header on the response. */
167
192
  _setKeepCacheDirective(): void;
193
+ /**
194
+ * @internal This request carries the fresh-reads cookie: it follows an
195
+ * updateTag()/revalidateTag() by the same user, so the stores read tag
196
+ * markers and shells from the store, not from an isolate memo.
197
+ */
198
+ _freshReads?: boolean;
199
+ /**
200
+ * @internal Set the fresh-reads cookie on this response for `maxAgeMs`
201
+ * (rounded up to whole seconds; the longest stays). Called by
202
+ * updateTag()/revalidateTag().
203
+ */
204
+ _setFreshReadsCookie(maxAgeMs: number): void;
205
+ /**
206
+ * @internal The context createRequestContext() returned. A context derived
207
+ * from it with Object.create (a PPR HIT tail, a shell capture, a background
208
+ * revalidation render) inherits it, so per-request invalidation state (the
209
+ * stores' masks) is keyed by the request, not by whichever derived object
210
+ * called or read (#973).
211
+ */
212
+ _requestRoot?: RequestContext<any, any>;
213
+ /**
214
+ * @internal Where the request started in the isolate's invalidation order
215
+ * (cache/invalidation-order.ts), taken when createRequestContext() ran. The
216
+ * document cache gates its write on it (#977): a middleware ahead of the
217
+ * document cache can read tagged data the document bakes.
218
+ */
219
+ _requestStart?: ExecutionStart;
220
+ /**
221
+ * @internal The handler has handed the response to the host (rsc/handler.ts
222
+ * `rango.response`): header and cookie writes after this are lost, as from
223
+ * a streaming loader or render.
224
+ */
225
+ _responseSent?: boolean;
168
226
 
169
227
  /**
170
228
  * Access loader data or push handle data.
@@ -206,16 +264,13 @@ export interface RequestContext<
206
264
  readonly _handleStore: HandleStore;
207
265
 
208
266
  /**
209
- * @internal transition({ when }) predicates for segments matched this request,
210
- * keyed by segment id. Collected during resolution (the function is stripped
211
- * from the serialized segment config), then evaluated post-handler in
212
- * rsc-rendering — outside any cache scope — to drop the transition of any
213
- * segment whose predicate returns false.
267
+ * @internal transition({ when }) client references of the matched entry
268
+ * chain, keyed by segment id. Recorded at match time from the static route
269
+ * definition (router/transition-when.ts) and attached to the payload's
270
+ * segments right before Flight (rsc/attach-transition-when.ts). The server
271
+ * never calls them, and no cache or snapshot stores them.
214
272
  */
215
- _transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;
216
-
217
- /** @internal PPR transition decisions evaluated before cache lookup/handlers. */
218
- _pprTransitionDecisions?: Map<string, boolean>;
273
+ _transitionWhenRefs?: ReadonlyMap<string, TransitionWhenRecord>;
219
274
 
220
275
  /**
221
276
  * @internal Post-match serve-source truth for the PPR replay reporter.
@@ -243,8 +298,8 @@ export interface RequestContext<
243
298
  * capture task's derived request context (built by shell-capture.ts). This is
244
299
  * the switch every capture-specific behavior reads: loader masking
245
300
  * (loader-mask.ts isShellCaptureActive / fresh.ts emitStreaming) and the
246
- * cookies()/headers() capture guard (cookie-store.ts
247
- * assertNotInsideShellCapture). The foreground render never sets it, so the
301
+ * identity-read capture guard (server/context.ts guardIdentityRead). The
302
+ * foreground render never sets it, so the
248
303
  * served response is byte-identical to axis 1. The capture descriptor itself
249
304
  * (key/ttl/swr/tags/store) is NOT threaded through the request context — the
250
305
  * integrated PPR serve path (rsc/shell-serve.ts + rsc-rendering.ts) builds it
@@ -264,6 +319,59 @@ export interface RequestContext<
264
319
  */
265
320
  _shellCaptureLoaderRecords?: Map<string, Promise<unknown>>;
266
321
 
322
+ /**
323
+ * @internal Set on a capture's derived context when a loader pushed a
324
+ * value the record cannot keep (a deferred push, masked nested promises,
325
+ * or a push outside a loader body): the loader records then carry
326
+ * `runs: 1`, so a HIT still runs their bodies for those pushes.
327
+ */
328
+ _shellCaptureUnrecordedLoaderPush?: true;
329
+
330
+ /**
331
+ * @internal The lane of each loader the capture's matched route registers
332
+ * (loader-cache.ts routeLoaderLanes), set by createMatchContextForFull
333
+ * before any loader runs. The capture's push funnel credits a push made
334
+ * under a live-lane loader's body, at any depth, to that loader
335
+ * (shell-capture.ts deriveShellCaptureContext). Own property of the
336
+ * capture's derived context only.
337
+ */
338
+ _shellCaptureLoaderLanes?: ReadonlyMap<string, "live" | "bake">;
339
+
340
+ /**
341
+ * @internal Record keys resolved for this request (CacheScope
342
+ * resolveKeyFrom): by the scope's `key()` (one result) or its store's
343
+ * keyGenerator (one per default key). A shell capture's derived context
344
+ * reads it through the prototype chain, so the capture reuses the
345
+ * partition its request resolved instead of running `key()` again.
346
+ */
347
+ _resolvedCacheKeys?: Map<unknown, Map<string, Promise<string>>>;
348
+
349
+ /**
350
+ * @internal The shell store key the PPR serve path resolved for this
351
+ * document request (rsc-rendering.ts shellServePlan), request partition
352
+ * included. Read by the testing helper serveShellRequest.
353
+ */
354
+ _shellKey?: string;
355
+
356
+ /**
357
+ * @internal The request carried the forced-MISS reload marker
358
+ * (shell-serve.ts SHELL_MISS_PARAM). The handler strips the marker from the
359
+ * request on entry and sets this; the shell gate renders the request like a
360
+ * cache miss.
361
+ */
362
+ _shellForcedMiss?: true;
363
+
364
+ /**
365
+ * @internal The freshness window of the route cache() records this
366
+ * request read or wrote (cache-scope.ts noteRouteRecordWindow), as ms
367
+ * timestamps: `freshUntil` is the record's `expiresAt`, `staleUntil` adds
368
+ * its scope's swr. The narrowest record wins. A shell capture caps its
369
+ * entry's ttl and ttl+swr to it (shell-capture.ts capShellWindow), so a
370
+ * shell is never fresher, or served longer, than the route cache() entry
371
+ * it replayed or wrote.
372
+ */
373
+ _routeRecordWindow?: RouteRecordWindow;
374
+
267
375
  /**
268
376
  * @internal Loader-family snapshot seed for a shell HIT's tail render:
269
377
  * segment-key -> the capture's elided container (already Flight-deserialized
@@ -271,8 +379,10 @@ export interface RequestContext<
271
379
  * overlays it onto the fresh run's container (recorded paths pinned,
272
380
  * hole-marker paths keep the fresh nested promises) so the payload's baked
273
381
  * bytes match the frozen prelude; a hole-free entry resolves pin-first
274
- * without gating on the fresh run. Own property of the HIT tail's derived
275
- * context only.
382
+ * and does not run the loader. Two owners: serveShellHit sets it on the
383
+ * HIT tail's derived context, and matchPartialWithPprReplay sets it on the
384
+ * request context of a PPR partial replay once its doc record hits, and
385
+ * restores the previous value after the match.
276
386
  */
277
387
  _shellLoaderSeed?: Map<
278
388
  string,
@@ -280,16 +390,17 @@ export interface RequestContext<
280
390
  >;
281
391
 
282
392
  /**
283
- * @internal Shell fast-path marker: makes the next eligible match treat the
284
- * whole matched route as an implicit doc-level cache() boundary (see
393
+ * @internal The doc record marker: makes the next match treat the whole
394
+ * matched route as an implicit doc-level cache() boundary (see
285
395
  * resolveShellImplicitCacheScope in cache/cache-scope.ts). Set ONLY on
286
396
  * (a) the capture's derived context — with a record-only store so the
287
397
  * capture's cacheRoute write lands in the snapshot, never the real store —
288
- * (b) a HIT tail's seeded context, and (c) an eligible normal-route partial
289
- * replay, where `store` is a request-local segment overlay. Handler-live holes
290
- * and conditional transitions decline replay.
291
- * Routes with their own cache() config are never overridden — their scope's
292
- * store/key/ttl/swr/condition semantics stay authoritative. On the
398
+ * (b) a document HIT tail's seeded context (`docTail`), and (c) an eligible
399
+ * normal-route partial replay, where `store` is a request-local segment
400
+ * overlay.
401
+ * A document HIT tail replays the record even for a route with its own
402
+ * cache() config (the gate before the commit already evaluated that
403
+ * scope's opt-outs). Elsewhere the route's scope stays authoritative. On the
293
404
  * navigation-replay serve path (`onExplicitHit` set) the marker COMPOSES with
294
405
  * such a scope instead of being ignored: the seeded doc record supplies the match
295
406
  * only when the explicit tier misses (withCacheLookup). cache(false) and a
@@ -306,12 +417,17 @@ export interface RequestContext<
306
417
  * segment record even when the triggering request is partial.
307
418
  */
308
419
  keyPrefix?: "doc";
309
- /** @internal Called only after the implicit cache hit decodes successfully. */
310
- onHit?: () => void;
420
+ /**
421
+ * @internal Called only after the implicit cache hit decodes
422
+ * successfully; the lookup awaits it before returning the hit, so work
423
+ * it starts lands before the route's loaders resolve.
424
+ */
425
+ onHit?: () => void | Promise<void>;
311
426
  /**
312
427
  * @internal Called when the seeded document record fails server-side
313
428
  * deserialization. Partial replay uses it to replace the enclosing shell
314
- * snapshot after serving the fresh fallback.
429
+ * snapshot after serving the fresh fallback. A document HIT tail does not
430
+ * set it: withCacheLookup throws ShellRecordUnavailableError instead.
315
431
  */
316
432
  onCorrupt?: () => void;
317
433
  /**
@@ -322,6 +438,23 @@ export interface RequestContext<
322
438
  * require the exact consumable record instead of "any segment record".
323
439
  */
324
440
  docKey?: string;
441
+ /**
442
+ * @internal Set on a document HIT tail (serveShellHit). The implicit doc
443
+ * scope replaces any route-derived cache() scope for the tail's match
444
+ * (resolveShellImplicitCacheScope), and a lookup that does not hit throws
445
+ * ShellRecordUnavailableError instead of resolving segments
446
+ * (withCacheLookup): a HIT never runs a handler. The record's
447
+ * loader-owned handle pushes restore as authoritative
448
+ * (HandleStore.pushRestored): the prelude rendered them.
449
+ */
450
+ docTail?: true;
451
+ /**
452
+ * @internal The key a document HIT tail looks the doc record up by: the
453
+ * entry's own `docKey`. Recomputing it could name another key (a store
454
+ * keyGenerator folding request data, a build-time capture's synthetic
455
+ * host) and miss the record.
456
+ */
457
+ fixedDocKey?: string;
325
458
  /**
326
459
  * @internal Set ONLY by matchPartialWithPprReplay on the navigation-replay
327
460
  * serve path. Its presence arms the explicit-scope composition in
@@ -365,31 +498,17 @@ export interface RequestContext<
365
498
  _shellFragmentPayload?: boolean;
366
499
 
367
500
  /**
368
- * @internal Handler-layer liveness observed DURING a shell capture, from
369
- * three sources: (a) the capture handle-store push wrapper (shell-capture.ts)
370
- * when a push made OUTSIDE a DSL loader scope carries a nested thenable
371
- * (masked to a never-filling hole); (b) still-pending top-level handler
372
- * pushes (liveness unknowable at the barrier); (c) a handler-invoked loader
373
- * executing during the capture (loader-resolution.ts — its consumption-lane
374
- * value would freeze on a handler-free HIT). captureAndStoreShell folds it
375
- * into ShellCacheEntry.handlerLiveHoles at the putShell barrier. Own
376
- * property of the capture's derived context only.
377
- */
378
- _shellCaptureHandleLiveness?: {
379
- holes: boolean;
380
- pendingPushes: number;
381
- handlerInvokedLoader: boolean;
382
- };
383
-
384
- /**
385
- * @internal Set (to the offending fn name) by the cookies()/headers()
386
- * capture guard when it throws DURING a capture render. Load-bearing for the
501
+ * @internal Set by the capture guard (server/context.ts
502
+ * guardIdentityRead) when it throws DURING a capture render: the read
503
+ * ("cookies()", "headers()", `ctx.get() for a non-cacheable variable`,
504
+ * "ctx.theme") and the fix the
505
+ * refusal warning gives for it. Load-bearing for the
387
506
  * bake lane: a guard throw inside an executing loader is swallowed by
388
507
  * wrapLoaderPromise into per-loader error UI, which would otherwise bake
389
508
  * silently into the shared shell — the capture checks this flag after the
390
509
  * render and refuses instead. Deterministic, so the capture does not retry.
391
510
  */
392
- _shellCaptureGuardTripped?: string;
511
+ _shellCaptureGuardTripped?: { surface: string; fix: string };
393
512
 
394
513
  /**
395
514
  * @internal The loader $$id whose BODY was executing when the capture guard
@@ -420,6 +539,23 @@ export interface RequestContext<
420
539
  */
421
540
  _requestTags: Set<string>;
422
541
 
542
+ /**
543
+ * @internal This request can write a route cache() record: its match
544
+ * resolved a cache scope (match-api.ts). Arms the record tag owners
545
+ * (cache-tag.ts armRecordTagOwners); off, the segment scopes, loader links
546
+ * and owned tag sets are skipped entirely. A capture's or background
547
+ * re-render's derived context (Object.create) inherits it.
548
+ */
549
+ _recordTagOwners?: true;
550
+
551
+ /**
552
+ * @internal This request resolves a loader bound with its own cache()
553
+ * (match-api.ts, or the binding's funnel). Arms the per-execution loader
554
+ * tag sets (cache-tag.ts armLoaderTagSets); off, a loader execution
555
+ * allocates no set and a read links nothing. Derived contexts inherit it.
556
+ */
557
+ _recordLoaderTags?: true;
558
+
423
559
  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
424
560
  _cacheProfiles?: Record<
425
561
  string,
@@ -467,6 +603,11 @@ export interface RequestContext<
467
603
  * Returns the theme value from the cookie, or the default theme if not set.
468
604
  * This is the user's preference ("light", "dark", or "system"), not the resolved value.
469
605
  *
606
+ * The visitor's cookie, so it is guarded like `cookies()`: reading it
607
+ * refuses a ppr shell capture and throws inside a `cache()` boundary or a
608
+ * `"use cache"` function (#971). A non-enumerable getter: a spread or
609
+ * `Object.assign` copy of the context does not carry it.
610
+ *
470
611
  * @example
471
612
  * ```typescript
472
613
  * route("settings", (ctx) => {
@@ -475,7 +616,7 @@ export interface RequestContext<
475
616
  * });
476
617
  * ```
477
618
  */
478
- theme?: Theme;
619
+ readonly theme?: Theme;
479
620
 
480
621
  /**
481
622
  * Set the theme (only available when theme is enabled in router config)
@@ -499,6 +640,30 @@ export interface RequestContext<
499
640
  /** @internal Theme configuration (null if theme not enabled) */
500
641
  _themeConfig?: ResolvedThemeConfig | null;
501
642
 
643
+ /**
644
+ * @internal The theme cookie (or the default) read with no identity guard.
645
+ * The guarded reads (`theme` here, the handler and middleware `ctx.theme`)
646
+ * call it after guardIdentityRead (readGuardedTheme); the router's own payload read goes
647
+ * through payloadInitialTheme (rsc/full-payload.ts), except the unmatched
648
+ * route's 404 (rsc/handler.ts), which is never stored. Undefined without
649
+ * theme config.
650
+ */
651
+ _readTheme(): Theme | undefined;
652
+
653
+ /**
654
+ * @internal Set by the document-cache middleware (cache/document-cache.ts)
655
+ * before it runs a render whose response it may store: the MISS and the
656
+ * stale refresh. payloadInitialTheme reads it (#978).
657
+ */
658
+ _documentCacheRender?: boolean;
659
+
660
+ /**
661
+ * @internal Set by payloadInitialTheme when the document payload carries
662
+ * the visitor's theme and it differs from the no-cookie default. The
663
+ * document cache does not store that response (#978).
664
+ */
665
+ _payloadVisitorTheme?: boolean;
666
+
502
667
  /**
503
668
  * Attach location state entries to the current response.
504
669
  *
@@ -542,20 +707,6 @@ export interface RequestContext<
542
707
  /** @internal Previous route key (from the navigation source), used for revalidation */
543
708
  _prevRouteKey?: string;
544
709
 
545
- /**
546
- * @internal Navigation/action source data the transition({ when }) gate reads
547
- * to build its ShouldRevalidateFn-shaped predicate context. currentUrl/Params
548
- * come from the navigation snapshot (set at match time); action* are stashed
549
- * at the action-bearing gate call sites. All undefined when there is no source
550
- * (initial full load) or no action (plain navigation).
551
- */
552
- _gateCurrentUrl?: URL;
553
- _gateCurrentParams?: Record<string, string>;
554
- _gateActionId?: string;
555
- _gateActionUrl?: URL;
556
- _gateActionResult?: unknown;
557
- _gateFormData?: FormData;
558
-
559
710
  /**
560
711
  * @internal True while the post-action revalidation render is running (set by
561
712
  * revalidateAfterAction). The "use cache" runtime reads this to prefer
@@ -726,6 +877,21 @@ export interface RequestContext<
726
877
  _cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
727
878
  }
728
879
 
880
+ /**
881
+ * @internal The freshness window of a route cache() record, as ms
882
+ * timestamps: fresh until `freshUntil` (its `expiresAt`), stale until
883
+ * `staleUntil` (plus its scope's swr). `ttl`, `swr` and `written` describe
884
+ * the record that sets `staleUntil`: its cache() config, and whether this
885
+ * request wrote it (cacheRoute) rather than read it.
886
+ */
887
+ export interface RouteRecordWindow {
888
+ freshUntil: number;
889
+ staleUntil: number;
890
+ ttl: number;
891
+ swr: number;
892
+ written: boolean;
893
+ }
894
+
729
895
  /**
730
896
  * Public view of RequestContext, without internal methods and fields.
731
897
  *
@@ -739,11 +905,12 @@ export type PublicRequestContext<
739
905
  RequestContext<TEnv, TParams>,
740
906
  | "cookie"
741
907
  | "cookies"
908
+ | "_readCookie"
909
+ | "_readCookies"
742
910
  | "setCookie"
743
911
  | "deleteCookie"
744
912
  | "_handleStore"
745
- | "_transitionWhen"
746
- | "_pprTransitionDecisions"
913
+ | "_transitionWhenRefs"
747
914
  | "_pprReplayPostMatchReason"
748
915
  | "_cacheStore"
749
916
  | "_searchParamsFilter"
@@ -751,25 +918,29 @@ export type PublicRequestContext<
751
918
  | "_shellImplicitCache"
752
919
  | "_shellLoaderSeed"
753
920
  | "_shellCaptureLoaderRecords"
754
- | "_shellCaptureHandleLiveness"
921
+ | "_shellCaptureUnrecordedLoaderPush"
922
+ | "_shellCaptureLoaderLanes"
923
+ | "_resolvedCacheKeys"
924
+ | "_shellKey"
925
+ | "_shellForcedMiss"
926
+ | "_routeRecordWindow"
755
927
  | "_shellFragmentPayload"
756
928
  | "_shellCaptureGuardTripped"
757
929
  | "_shellCaptureGuardTrippedLoaderId"
758
930
  | "_explicitTaggedStores"
759
931
  | "_requestTags"
932
+ | "_recordTagOwners"
933
+ | "_recordLoaderTags"
760
934
  | "_cacheProfiles"
761
935
  | "_onResponseCallbacks"
762
936
  | "_pendingBackgroundTasks"
763
937
  | "_themeConfig"
938
+ | "_readTheme"
939
+ | "_documentCacheRender"
940
+ | "_payloadVisitorTheme"
764
941
  | "_locationState"
765
942
  | "_routeName"
766
943
  | "_prevRouteKey"
767
- | "_gateCurrentUrl"
768
- | "_gateCurrentParams"
769
- | "_gateActionId"
770
- | "_gateActionUrl"
771
- | "_gateActionResult"
772
- | "_gateFormData"
773
944
  | "_inActionRevalidation"
774
945
  | "_reportedErrors"
775
946
  | "_renderErrors"
@@ -795,6 +966,11 @@ export type PublicRequestContext<
795
966
  | "_setStatus"
796
967
  | "_rotateStateCookie"
797
968
  | "_setKeepCacheDirective"
969
+ | "_freshReads"
970
+ | "_setFreshReadsCookie"
971
+ | "_requestRoot"
972
+ | "_requestStart"
973
+ | "_responseSent"
798
974
  | "_variables"
799
975
  | "_classifiedRoute"
800
976
  | "_requestMode"
@@ -897,17 +1073,10 @@ export function setRequestContextParams(
897
1073
  */
898
1074
  export function setRequestContextPrevRouteKey(
899
1075
  prevRouteKey: string | undefined,
900
- currentUrl?: URL,
901
- currentParams?: Record<string, string>,
902
1076
  ): void {
903
1077
  const ctx = requestContextStorage.getStore();
904
1078
  if (!ctx) return;
905
1079
  if (prevRouteKey !== undefined) ctx._prevRouteKey = prevRouteKey;
906
- // Source URL/params for the transition({ when }) gate (effectiveFromUrl /
907
- // effectiveFromMatch.params from the navigation snapshot). Same write point as
908
- // _prevRouteKey, which doubles as fromRouteName.
909
- if (currentUrl !== undefined) ctx._gateCurrentUrl = currentUrl;
910
- if (currentParams !== undefined) ctx._gateCurrentParams = currentParams;
911
1080
  }
912
1081
 
913
1082
  /**
@@ -989,8 +1158,15 @@ export function createRequestContext<TEnv>(
989
1158
  stateCookieName,
990
1159
  version: stateVersion,
991
1160
  } = options;
992
- const cookieHeader = request.headers.get("Cookie");
1161
+ // ctx.request.headers is guarded like headers() (#976); every router read
1162
+ // of this request's headers goes through requestHeaders().
1163
+ guardRequestHeaders(request);
1164
+ const cookieHeader = requestHeaders(request).get("Cookie");
993
1165
  let rangoStateRotated = false;
1166
+ const freshCookie = stateCookieName
1167
+ ? freshReadsCookieName(stateCookieName)
1168
+ : undefined;
1169
+ let freshCookieMaxAge = 0;
994
1170
  let parsedCookies: Record<string, string> | null = null;
995
1171
 
996
1172
  let stubResponse = initialResponse
@@ -1084,6 +1260,26 @@ export function createRequestContext<TEnv>(
1084
1260
  return getParsedCookies()[name];
1085
1261
  };
1086
1262
 
1263
+ // The effective jar: request cookies minus deletions plus same-request writes.
1264
+ const effectiveCookies = (): Record<string, string> => {
1265
+ const parsed = getParsedCookies();
1266
+ const mutations = getResponseCookies();
1267
+ if (mutations.size === 0) return { ...parsed };
1268
+ // Build result without delete (avoids V8 dictionary-mode de-opt)
1269
+ const deleted = new Set<string>();
1270
+ for (const [k, v] of mutations) {
1271
+ if (v === null) deleted.add(k);
1272
+ }
1273
+ const result: Record<string, string> = {};
1274
+ for (const key of Object.keys(parsed)) {
1275
+ if (!deleted.has(key)) result[key] = parsed[key];
1276
+ }
1277
+ for (const [k, v] of mutations) {
1278
+ if (v !== null) result[k] = v;
1279
+ }
1280
+ return result;
1281
+ };
1282
+
1087
1283
  const getTheme = (): Theme | undefined => {
1088
1284
  if (!themeConfig) return undefined;
1089
1285
 
@@ -1144,7 +1340,7 @@ export function createRequestContext<TEnv>(
1144
1340
  _dynamic: false,
1145
1341
  get: ((keyOrVar: any) => {
1146
1342
  if (isNonCacheable(variables, keyOrVar)) {
1147
- assertNonCacheableReadAllowed(keyOrVar);
1343
+ assertNonCacheableReadAllowed(keyOrVar, _getRequestContext());
1148
1344
  }
1149
1345
  return contextGet(variables, keyOrVar);
1150
1346
  }) as RequestContext<TEnv>["get"],
@@ -1163,29 +1359,23 @@ export function createRequestContext<TEnv>(
1163
1359
  );
1164
1360
  },
1165
1361
 
1362
+ // The visitor's cookies, guarded like cookies() (#976): before, a plain
1363
+ // read that a ppr shell, a cache() entry or a "use cache" entry stored
1364
+ // for every visitor.
1166
1365
  cookie(name: string): string | undefined {
1366
+ guardRawCookieRead(ctx, "getRequestContext().cookie()");
1167
1367
  return effectiveCookie(name);
1168
1368
  },
1169
1369
 
1170
1370
  cookies(): Record<string, string> {
1171
- const parsed = getParsedCookies();
1172
- const mutations = getResponseCookies();
1173
- if (mutations.size === 0) return { ...parsed };
1174
- // Build result without delete (avoids V8 dictionary-mode de-opt)
1175
- const deleted = new Set<string>();
1176
- for (const [k, v] of mutations) {
1177
- if (v === null) deleted.add(k);
1178
- }
1179
- const result: Record<string, string> = {};
1180
- for (const key of Object.keys(parsed)) {
1181
- if (!deleted.has(key)) result[key] = parsed[key];
1182
- }
1183
- for (const [k, v] of mutations) {
1184
- if (v !== null) result[k] = v;
1185
- }
1186
- return result;
1371
+ guardRawCookieRead(ctx, "getRequestContext().cookies()");
1372
+ return effectiveCookies();
1187
1373
  },
1188
1374
 
1375
+ // Non-enumerable, defined after this literal.
1376
+ _readCookie: effectiveCookie,
1377
+ _readCookies: effectiveCookies,
1378
+
1189
1379
  setCookie(name: string, value: string, options?: CookieOptions): void {
1190
1380
  assertNotInsideCacheExec(ctx, "setCookie");
1191
1381
  stubResponse.headers.append(
@@ -1234,7 +1424,7 @@ export function createRequestContext<TEnv>(
1234
1424
  // decodeStateValue decodes exactly once) AND is the same parser the client
1235
1425
  // mirror uses, so both seats read the same jar entry.
1236
1426
  const prevRaw =
1237
- (request.headers.get("x-rango-state") || null) ??
1427
+ (requestHeaders(request).get("x-rango-state") || null) ??
1238
1428
  getRawCookieValue(cookieHeader, stateCookieName);
1239
1429
  const value = mintStateValue(stateVersion ?? "0", prevRaw);
1240
1430
  // rawStubHeaders: guard-exempt internal writer — invalidateClientCache()
@@ -1254,6 +1444,48 @@ export function createRequestContext<TEnv>(
1254
1444
  rawStubHeaders.set(KEEP_CACHE_HEADER, "1");
1255
1445
  },
1256
1446
 
1447
+ // The fresh-reads cookie (browser/cookie-name.ts): one Set-Cookie per
1448
+ // response, the longest Max-Age any invalidation asked for. HttpOnly (the
1449
+ // client never reads it), SameSite=Lax, Path=/. rawStubHeaders: an
1450
+ // internal writer, allowed wherever updateTag()/revalidateTag() are. A
1451
+ // shared cache never stores it (Set-Cookie is a per-client signal).
1452
+ _setFreshReadsCookie(maxAgeMs: number): void {
1453
+ if (!freshCookie) return;
1454
+ const maxAge = Math.ceil(maxAgeMs / 1000);
1455
+ // Already set for at least this long (an earlier invalidation in this
1456
+ // request): nothing to add, sent or not.
1457
+ if (maxAge <= freshCookieMaxAge) return;
1458
+ if (ctx._responseSent) {
1459
+ // A streaming loader or render: the headers already left. The
1460
+ // invalidation itself still ran; only the mutating user's bypass of
1461
+ // the stores' isolate memos is lost.
1462
+ if (process.env.NODE_ENV !== "production") {
1463
+ console.warn(
1464
+ `[rango] updateTag()/revalidateTag() ran after the response ` +
1465
+ `headers were sent (from a streaming loader or render), so the ` +
1466
+ `${freshCookie} cookie was not set: this user's next requests ` +
1467
+ `can be served from a store's isolate memo for a few seconds. ` +
1468
+ `Invalidate in a server action, a route handler, or middleware.`,
1469
+ );
1470
+ }
1471
+ return;
1472
+ }
1473
+ const serialized = (age: number) =>
1474
+ `${freshCookie}=1; Max-Age=${age}; Path=/; HttpOnly; SameSite=Lax` +
1475
+ (url.protocol === "https:" ? "; Secure" : "");
1476
+ if (freshCookieMaxAge > 0) {
1477
+ const prior = serialized(freshCookieMaxAge);
1478
+ const kept = rawStubHeaders
1479
+ .getSetCookie()
1480
+ .filter((cookie) => cookie !== prior);
1481
+ rawStubHeaders.delete("Set-Cookie");
1482
+ for (const cookie of kept) rawStubHeaders.append("Set-Cookie", cookie);
1483
+ }
1484
+ freshCookieMaxAge = maxAge;
1485
+ rawStubHeaders.append("Set-Cookie", serialized(maxAge));
1486
+ invalidateResponseCookieCache();
1487
+ },
1488
+
1257
1489
  setStatus(status: number): void {
1258
1490
  assertNotInsideCacheExec(ctx, "setStatus");
1259
1491
  assertResponseWriteAllowed("setStatus");
@@ -1269,12 +1501,14 @@ export function createRequestContext<TEnv>(
1269
1501
  method: request.method,
1270
1502
 
1271
1503
  _handleStore: handleStore,
1272
- _transitionWhen: [],
1273
1504
  _cacheStore: cacheStore,
1274
1505
  _searchParamsFilter: searchParamsFilter,
1275
1506
  _explicitTaggedStores: explicitTaggedStores,
1276
1507
  _requestTags: new Set<string>(),
1277
1508
  _cacheProfiles: cacheProfiles,
1509
+ _freshReads:
1510
+ freshCookie !== undefined &&
1511
+ getRawCookieValue(cookieHeader, freshCookie) !== null,
1278
1512
 
1279
1513
  waitUntil(fn: () => Promise<void>): void {
1280
1514
  if (ctx.build) return;
@@ -1316,9 +1550,8 @@ export function createRequestContext<TEnv>(
1316
1550
  this._onResponseCallbacks.push(callback);
1317
1551
  },
1318
1552
 
1319
- get theme() {
1320
- return themeConfig ? getTheme() : undefined;
1321
- },
1553
+ // theme: a guarded, non-enumerable getter defined after this literal.
1554
+ _readTheme: getTheme,
1322
1555
  setTheme: themeConfig
1323
1556
  ? (theme: Theme) => {
1324
1557
  assertNotInsideCacheExec(ctx, "setTheme");
@@ -1359,7 +1592,38 @@ export function createRequestContext<TEnv>(
1359
1592
  getContext: () => ctx,
1360
1593
  });
1361
1594
 
1595
+ // getRequestContext().theme is the visitor's cookie, guarded like cookies()
1596
+ // (#971); before it was a plain read that a ppr shell, a cache() entry or a
1597
+ // "use cache" entry stored for every visitor. Non-enumerable like the
1598
+ // handler ctx.theme (handler-context.ts THEME_DESCRIPTOR): serializing the
1599
+ // context (React dev debug info) must not count as a read. The guard reads
1600
+ // the ambient context, as cookies() does, and falls back to the receiver.
1601
+ // _readTheme is the unguarded read: non-enumerable too, so a spread copy of
1602
+ // the context (the fetchable-loader ctx, loader-fetch.ts) cannot carry it.
1603
+ // The same holds for the unguarded cookie reads (#976).
1604
+ Object.defineProperties(ctx, {
1605
+ theme: {
1606
+ get(this: RequestContext<TEnv>): Theme | undefined {
1607
+ return readGuardedTheme(this, "getRequestContext().theme");
1608
+ },
1609
+ enumerable: false,
1610
+ configurable: true,
1611
+ },
1612
+ _readTheme: { enumerable: false },
1613
+ _readCookie: { enumerable: false },
1614
+ _readCookies: { enumerable: false },
1615
+ });
1616
+
1362
1617
  (ctx as any)[NOCACHE_SYMBOL] = true;
1618
+ // Non-enumerable: a self-reference would make the context circular for
1619
+ // JSON.stringify, spreads and React's dev debug-info serialization of props.
1620
+ Object.defineProperty(ctx, "_requestRoot", {
1621
+ value: ctx,
1622
+ enumerable: false,
1623
+ configurable: true,
1624
+ writable: true,
1625
+ });
1626
+ ctx._requestStart = executionStart();
1363
1627
  return ctx;
1364
1628
  }
1365
1629