@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -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";
@@ -61,9 +62,10 @@ import type { RoutineTrace } from "../rsc/routine-plan.js";
61
62
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
62
63
  import {
63
64
  assertCachedHeaderWriteAllowed,
65
+ assertNonCacheableReadAllowed,
64
66
  clearPprHeaderScope,
65
- isInsideCacheScope,
66
67
  } from "./context.js";
68
+ import { assertThemeReadAllowed } from "./cookie-store.js";
67
69
  import {
68
70
  createReverseFunction,
69
71
  stripInternalParams,
@@ -165,6 +167,40 @@ export interface RequestContext<
165
167
  _rotateStateCookie(): void;
166
168
  /** @internal Set the keepClientCache() directive header on the response. */
167
169
  _setKeepCacheDirective(): void;
170
+ /**
171
+ * @internal This request carries the fresh-reads cookie: it follows an
172
+ * updateTag()/revalidateTag() by the same user, so the stores read tag
173
+ * markers and shells from the store, not from an isolate memo.
174
+ */
175
+ _freshReads?: boolean;
176
+ /**
177
+ * @internal Set the fresh-reads cookie on this response for `maxAgeMs`
178
+ * (rounded up to whole seconds; the longest stays). Called by
179
+ * updateTag()/revalidateTag().
180
+ */
181
+ _setFreshReadsCookie(maxAgeMs: number): void;
182
+ /**
183
+ * @internal The context createRequestContext() returned. A context derived
184
+ * from it with Object.create (a PPR HIT tail, a shell capture, a background
185
+ * revalidation render) inherits it, so per-request invalidation state
186
+ * (tag-invalidation.ts, the stores' masks) is keyed by the request, not by
187
+ * whichever derived object called or read (#973).
188
+ */
189
+ _requestRoot?: RequestContext<any, any>;
190
+ /**
191
+ * @internal The tags this request invalidated with
192
+ * updateTag()/revalidateTag(), each with its position in the invalidation
193
+ * order (tag-invalidation.ts currentCacheSeq). Kept on _requestRoot. A
194
+ * "use cache" execution that started before one of its own tags was
195
+ * invalidated is neither joined nor written (cache-runtime.ts, #973).
196
+ */
197
+ _tagInvalidations?: Map<string, number>;
198
+ /**
199
+ * @internal The handler has handed the response to the host (rsc/handler.ts
200
+ * `rango.response`): header and cookie writes after this are lost, as from
201
+ * a streaming loader or render.
202
+ */
203
+ _responseSent?: boolean;
168
204
 
169
205
  /**
170
206
  * Access loader data or push handle data.
@@ -197,8 +233,13 @@ export interface RequestContext<
197
233
  /** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
198
234
  method: string;
199
235
 
200
- /** @internal Handle store for tracking handle data across segments */
201
- _handleStore: HandleStore;
236
+ /**
237
+ * @internal Handle store for tracking handle data across segments.
238
+ * Readonly: a background render that needs its own store derives a context
239
+ * (rerenderAndCacheRoute, shell-capture.ts) instead of swapping this field,
240
+ * which the foreground still reads while it produces the page.
241
+ */
242
+ readonly _handleStore: HandleStore;
202
243
 
203
244
  /**
204
245
  * @internal transition({ when }) predicates for segments matched this request,
@@ -250,16 +291,58 @@ export interface RequestContext<
250
291
  /**
251
292
  * @internal Bake-lane loader containers collected DURING a shell capture:
252
293
  * segment-key -> the loader's (pre-wrap) result promise. Populated by
253
- * resolveLoaderData for loaders on entries with no renderable loading() (the
254
- * bake lane — they execute at capture instead of being masked; see
255
- * docs/design/loader-container-bake.md). Drained by captureAndStoreShell
256
- * after the shell quiesces: settled containers are promise-elided,
257
- * Flight-serialized, and pinned into the snapshot's loader family; a
258
- * REJECTED container refuses the capture (error UI must never bake into the
259
- * shared shell). Own property of the capture's derived context only.
294
+ * resolveLoaderData for bake-lane loaders (lane rule: see its JSDoc in
295
+ * loader-cache.ts). Drained by captureAndStoreShell after the shell
296
+ * quiesces: settled containers are promise-elided, Flight-serialized, and
297
+ * pinned into the snapshot's loader family; a REJECTED container refuses
298
+ * the capture (error UI must never bake into the shared shell). Own
299
+ * property of the capture's derived context only.
260
300
  */
261
301
  _shellCaptureLoaderRecords?: Map<string, Promise<unknown>>;
262
302
 
303
+ /**
304
+ * @internal Set on a capture's derived context when a loader pushed a
305
+ * value the record cannot keep (a deferred push, masked nested promises,
306
+ * or a push outside a loader body): the loader records then carry
307
+ * `runs: 1`, so a HIT still runs their bodies for those pushes.
308
+ */
309
+ _shellCaptureUnrecordedLoaderPush?: true;
310
+
311
+ /**
312
+ * @internal Record keys resolved for this request (CacheScope
313
+ * resolveKeyFrom): by the scope's `key()` (one result) or its store's
314
+ * keyGenerator (one per default key). A shell capture's derived context
315
+ * reads it through the prototype chain, so the capture reuses the
316
+ * partition its request resolved instead of running `key()` again.
317
+ */
318
+ _resolvedCacheKeys?: Map<unknown, Map<string, Promise<string>>>;
319
+
320
+ /**
321
+ * @internal The shell store key the PPR serve path resolved for this
322
+ * document request (rsc-rendering.ts shellServePlan), request partition
323
+ * included. Read by the testing helper serveShellRequest.
324
+ */
325
+ _shellKey?: string;
326
+
327
+ /**
328
+ * @internal The request carried the forced-MISS reload marker
329
+ * (shell-serve.ts SHELL_MISS_PARAM). The handler strips the marker from the
330
+ * request on entry and sets this; the shell gate renders the request like a
331
+ * cache miss.
332
+ */
333
+ _shellForcedMiss?: true;
334
+
335
+ /**
336
+ * @internal The freshness window of the route cache() records this
337
+ * request read or wrote (cache-scope.ts noteRouteRecordWindow), as ms
338
+ * timestamps: `freshUntil` is the record's `expiresAt`, `staleUntil` adds
339
+ * its scope's swr. The narrowest record wins. A shell capture caps its
340
+ * entry's ttl and ttl+swr to it (shell-capture.ts capShellWindow), so a
341
+ * shell is never fresher, or served longer, than the route cache() entry
342
+ * it replayed or wrote.
343
+ */
344
+ _routeRecordWindow?: RouteRecordWindow;
345
+
263
346
  /**
264
347
  * @internal Loader-family snapshot seed for a shell HIT's tail render:
265
348
  * segment-key -> the capture's elided container (already Flight-deserialized
@@ -267,8 +350,10 @@ export interface RequestContext<
267
350
  * overlays it onto the fresh run's container (recorded paths pinned,
268
351
  * hole-marker paths keep the fresh nested promises) so the payload's baked
269
352
  * bytes match the frozen prelude; a hole-free entry resolves pin-first
270
- * without gating on the fresh run. Own property of the HIT tail's derived
271
- * context only.
353
+ * and does not run the loader. Two owners: serveShellHit sets it on the
354
+ * HIT tail's derived context, and matchPartialWithPprReplay sets it on the
355
+ * request context of a PPR partial replay once its doc record hits, and
356
+ * restores the previous value after the match.
272
357
  */
273
358
  _shellLoaderSeed?: Map<
274
359
  string,
@@ -276,16 +361,17 @@ export interface RequestContext<
276
361
  >;
277
362
 
278
363
  /**
279
- * @internal Shell fast-path marker: makes the next eligible match treat the
280
- * whole matched route as an implicit doc-level cache() boundary (see
364
+ * @internal The doc record marker: makes the next match treat the whole
365
+ * matched route as an implicit doc-level cache() boundary (see
281
366
  * resolveShellImplicitCacheScope in cache/cache-scope.ts). Set ONLY on
282
367
  * (a) the capture's derived context — with a record-only store so the
283
368
  * capture's cacheRoute write lands in the snapshot, never the real store —
284
- * (b) a HIT tail's seeded context, and (c) an eligible normal-route partial
285
- * replay, where `store` is a request-local segment overlay. Handler-live holes
286
- * and conditional transitions decline replay.
287
- * Routes with their own cache() config are never overridden — their scope's
288
- * store/key/ttl/swr/condition semantics stay authoritative. On the
369
+ * (b) a document HIT tail's seeded context (`docTail`), and (c) an eligible
370
+ * normal-route partial replay, where `store` is a request-local segment
371
+ * overlay.
372
+ * A document HIT tail replays the record even for a route with its own
373
+ * cache() config (the gate before the commit already evaluated that
374
+ * scope's opt-outs). Elsewhere the route's scope stays authoritative. On the
289
375
  * navigation-replay serve path (`onExplicitHit` set) the marker COMPOSES with
290
376
  * such a scope instead of being ignored: the seeded doc record supplies the match
291
377
  * only when the explicit tier misses (withCacheLookup). cache(false) and a
@@ -302,12 +388,17 @@ export interface RequestContext<
302
388
  * segment record even when the triggering request is partial.
303
389
  */
304
390
  keyPrefix?: "doc";
305
- /** @internal Called only after the implicit cache hit decodes successfully. */
306
- onHit?: () => void;
391
+ /**
392
+ * @internal Called only after the implicit cache hit decodes
393
+ * successfully; the lookup awaits it before returning the hit, so work
394
+ * it starts lands before the route's loaders resolve.
395
+ */
396
+ onHit?: () => void | Promise<void>;
307
397
  /**
308
398
  * @internal Called when the seeded document record fails server-side
309
399
  * deserialization. Partial replay uses it to replace the enclosing shell
310
- * snapshot after serving the fresh fallback.
400
+ * snapshot after serving the fresh fallback. A document HIT tail does not
401
+ * set it: withCacheLookup throws ShellRecordUnavailableError instead.
311
402
  */
312
403
  onCorrupt?: () => void;
313
404
  /**
@@ -318,6 +409,21 @@ export interface RequestContext<
318
409
  * require the exact consumable record instead of "any segment record".
319
410
  */
320
411
  docKey?: string;
412
+ /**
413
+ * @internal Set on a document HIT tail (serveShellHit). The implicit doc
414
+ * scope replaces any route-derived cache() scope for the tail's match
415
+ * (resolveShellImplicitCacheScope), and a lookup that does not hit throws
416
+ * ShellRecordUnavailableError instead of resolving segments
417
+ * (withCacheLookup): a HIT never runs a handler.
418
+ */
419
+ docTail?: true;
420
+ /**
421
+ * @internal The key a document HIT tail looks the doc record up by: the
422
+ * entry's own `docKey`. Recomputing it could name another key (a store
423
+ * keyGenerator folding request data, a build-time capture's synthetic
424
+ * host) and miss the record.
425
+ */
426
+ fixedDocKey?: string;
321
427
  /**
322
428
  * @internal Set ONLY by matchPartialWithPprReplay on the navigation-replay
323
429
  * serve path. Its presence arms the explicit-scope composition in
@@ -361,43 +467,16 @@ export interface RequestContext<
361
467
  _shellFragmentPayload?: boolean;
362
468
 
363
469
  /**
364
- * @internal Handler-layer liveness observed DURING a shell capture, from
365
- * three sources: (a) the capture handle-store push wrapper (shell-capture.ts)
366
- * when a push made OUTSIDE a DSL loader scope carries a nested thenable
367
- * (masked to a never-filling hole); (b) still-pending top-level handler
368
- * pushes (liveness unknowable at the barrier); (c) a handler-invoked loader
369
- * executing during the capture (loader-resolution.ts — its consumption-lane
370
- * value would freeze on a handler-free HIT). captureAndStoreShell folds it
371
- * into ShellCacheEntry.handlerLiveHoles at the putShell barrier. Own
372
- * property of the capture's derived context only.
373
- */
374
- _shellCaptureHandleLiveness?: {
375
- holes: boolean;
376
- pendingPushes: number;
377
- handlerInvokedLoader: boolean;
378
- };
379
-
380
- /**
381
- * @internal Handle values pushed from a DSL loader scope DURING a shell
382
- * capture (identity set; populated by the capture push wrapper in
383
- * shell-capture.ts). cacheRoute threads it into captureHandles so those
384
- * values stay out of cache-write handle records — loaders re-run fresh on
385
- * every HIT, so replaying their captured (masked) values would duplicate
386
- * the fresh push and stall the Flight handle encode. Own property of the
387
- * capture's derived context only; render-time handle consumers are
388
- * unaffected (the exclusion applies only at the captureHandles call site).
389
- */
390
- _shellCaptureLoaderHandleValues?: WeakSet<object>;
391
-
392
- /**
393
- * @internal Set (to the offending fn name) by the cookies()/headers()
394
- * capture guard when it throws DURING a capture render. Load-bearing for the
470
+ * @internal Set by the capture guard (server/context.ts
471
+ * tripShellCaptureGuard) when it throws DURING a capture render: the read
472
+ * ("cookies()", "headers()", "ctx.get()", "ctx.theme") and the fix the
473
+ * refusal warning gives for it. Load-bearing for the
395
474
  * bake lane: a guard throw inside an executing loader is swallowed by
396
475
  * wrapLoaderPromise into per-loader error UI, which would otherwise bake
397
476
  * silently into the shared shell — the capture checks this flag after the
398
477
  * render and refuses instead. Deterministic, so the capture does not retry.
399
478
  */
400
- _shellCaptureGuardTripped?: string;
479
+ _shellCaptureGuardTripped?: { surface: string; fix: string };
401
480
 
402
481
  /**
403
482
  * @internal The loader $$id whose BODY was executing when the capture guard
@@ -428,6 +507,23 @@ export interface RequestContext<
428
507
  */
429
508
  _requestTags: Set<string>;
430
509
 
510
+ /**
511
+ * @internal This request can write a route cache() record: its match
512
+ * resolved a cache scope (match-api.ts). Arms the record tag owners
513
+ * (cache-tag.ts armRecordTagOwners); off, the segment scopes, loader links
514
+ * and owned tag sets are skipped entirely. A capture's or background
515
+ * re-render's derived context (Object.create) inherits it.
516
+ */
517
+ _recordTagOwners?: true;
518
+
519
+ /**
520
+ * @internal This request resolves a loader bound with its own cache()
521
+ * (match-api.ts, or the binding's funnel). Arms the per-execution loader
522
+ * tag sets (cache-tag.ts armLoaderTagSets); off, a loader execution
523
+ * allocates no set and a read links nothing. Derived contexts inherit it.
524
+ */
525
+ _recordLoaderTags?: true;
526
+
431
527
  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
432
528
  _cacheProfiles?: Record<
433
529
  string,
@@ -475,6 +571,11 @@ export interface RequestContext<
475
571
  * Returns the theme value from the cookie, or the default theme if not set.
476
572
  * This is the user's preference ("light", "dark", or "system"), not the resolved value.
477
573
  *
574
+ * The visitor's cookie, so it is guarded like `cookies()`: reading it
575
+ * refuses a ppr shell capture and throws inside a `cache()` boundary or a
576
+ * `"use cache"` function (#971). A non-enumerable getter: a spread or
577
+ * `Object.assign` copy of the context does not carry it.
578
+ *
478
579
  * @example
479
580
  * ```typescript
480
581
  * route("settings", (ctx) => {
@@ -483,7 +584,7 @@ export interface RequestContext<
483
584
  * });
484
585
  * ```
485
586
  */
486
- theme?: Theme;
587
+ readonly theme?: Theme;
487
588
 
488
589
  /**
489
590
  * Set the theme (only available when theme is enabled in router config)
@@ -507,6 +608,15 @@ export interface RequestContext<
507
608
  /** @internal Theme configuration (null if theme not enabled) */
508
609
  _themeConfig?: ResolvedThemeConfig | null;
509
610
 
611
+ /**
612
+ * @internal The theme cookie (or the default) read with no identity guard.
613
+ * The guarded reads (`theme` here, the handler and middleware `ctx.theme`)
614
+ * call it after assertThemeReadAllowed; the router's own payload read goes
615
+ * through payloadInitialTheme (rsc/full-payload.ts). Undefined without
616
+ * theme config.
617
+ */
618
+ _readTheme(): Theme | undefined;
619
+
510
620
  /**
511
621
  * Attach location state entries to the current response.
512
622
  *
@@ -650,6 +760,17 @@ export interface RequestContext<
650
760
  /** @internal Per-request error dedup set for onError reporting */
651
761
  _reportedErrors: WeakSet<object>;
652
762
 
763
+ /**
764
+ * @internal Errors this response's Flight or Fizz render reported through
765
+ * onError: a component that threw does not fail the stream, which completes
766
+ * with an error row or an errored Suspense boundary. Whole-response captures
767
+ * (document-cache.ts, captureAndStoreShell) store nothing while it is
768
+ * non-empty (issue #915). An array so a render under a derived context (the
769
+ * shell HIT tail) pushes into the request's own list through the prototype;
770
+ * deriveShellCaptureContext gives the capture its own.
771
+ */
772
+ _renderErrors?: unknown[];
773
+
653
774
  /**
654
775
  * @internal Report a non-fatal background error through the router's
655
776
  * onError callback. Wired by the RSC handler / router during request
@@ -723,6 +844,21 @@ export interface RequestContext<
723
844
  _cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
724
845
  }
725
846
 
847
+ /**
848
+ * @internal The freshness window of a route cache() record, as ms
849
+ * timestamps: fresh until `freshUntil` (its `expiresAt`), stale until
850
+ * `staleUntil` (plus its scope's swr). `ttl`, `swr` and `written` describe
851
+ * the record that sets `staleUntil`: its cache() config, and whether this
852
+ * request wrote it (cacheRoute) rather than read it.
853
+ */
854
+ export interface RouteRecordWindow {
855
+ freshUntil: number;
856
+ staleUntil: number;
857
+ ttl: number;
858
+ swr: number;
859
+ written: boolean;
860
+ }
861
+
726
862
  /**
727
863
  * Public view of RequestContext, without internal methods and fields.
728
864
  *
@@ -748,17 +884,23 @@ export type PublicRequestContext<
748
884
  | "_shellImplicitCache"
749
885
  | "_shellLoaderSeed"
750
886
  | "_shellCaptureLoaderRecords"
751
- | "_shellCaptureHandleLiveness"
752
- | "_shellCaptureLoaderHandleValues"
887
+ | "_shellCaptureUnrecordedLoaderPush"
888
+ | "_resolvedCacheKeys"
889
+ | "_shellKey"
890
+ | "_shellForcedMiss"
891
+ | "_routeRecordWindow"
753
892
  | "_shellFragmentPayload"
754
893
  | "_shellCaptureGuardTripped"
755
894
  | "_shellCaptureGuardTrippedLoaderId"
756
895
  | "_explicitTaggedStores"
757
896
  | "_requestTags"
897
+ | "_recordTagOwners"
898
+ | "_recordLoaderTags"
758
899
  | "_cacheProfiles"
759
900
  | "_onResponseCallbacks"
760
901
  | "_pendingBackgroundTasks"
761
902
  | "_themeConfig"
903
+ | "_readTheme"
762
904
  | "_locationState"
763
905
  | "_routeName"
764
906
  | "_prevRouteKey"
@@ -770,6 +912,7 @@ export type PublicRequestContext<
770
912
  | "_gateFormData"
771
913
  | "_inActionRevalidation"
772
914
  | "_reportedErrors"
915
+ | "_renderErrors"
773
916
  | "_renderBarrier"
774
917
  | "_resolveRenderBarrier"
775
918
  | "_renderBarrierSegmentOrder"
@@ -792,6 +935,11 @@ export type PublicRequestContext<
792
935
  | "_setStatus"
793
936
  | "_rotateStateCookie"
794
937
  | "_setKeepCacheDirective"
938
+ | "_freshReads"
939
+ | "_setFreshReadsCookie"
940
+ | "_requestRoot"
941
+ | "_tagInvalidations"
942
+ | "_responseSent"
795
943
  | "_variables"
796
944
  | "_classifiedRoute"
797
945
  | "_requestMode"
@@ -988,6 +1136,10 @@ export function createRequestContext<TEnv>(
988
1136
  } = options;
989
1137
  const cookieHeader = request.headers.get("Cookie");
990
1138
  let rangoStateRotated = false;
1139
+ const freshCookie = stateCookieName
1140
+ ? freshReadsCookieName(stateCookieName)
1141
+ : undefined;
1142
+ let freshCookieMaxAge = 0;
991
1143
  let parsedCookies: Record<string, string> | null = null;
992
1144
 
993
1145
  let stubResponse = initialResponse
@@ -1140,12 +1292,8 @@ export function createRequestContext<TEnv>(
1140
1292
  },
1141
1293
  _dynamic: false,
1142
1294
  get: ((keyOrVar: any) => {
1143
- if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
1144
- throw new Error(
1145
- `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
1146
- `The variable was created with { cache: false } or set with { cache: false }, ` +
1147
- `and its value would be stale on cache hit. Move the read outside the cached scope.`,
1148
- );
1295
+ if (isNonCacheable(variables, keyOrVar)) {
1296
+ assertNonCacheableReadAllowed(keyOrVar, _getRequestContext());
1149
1297
  }
1150
1298
  return contextGet(variables, keyOrVar);
1151
1299
  }) as RequestContext<TEnv>["get"],
@@ -1255,6 +1403,48 @@ export function createRequestContext<TEnv>(
1255
1403
  rawStubHeaders.set(KEEP_CACHE_HEADER, "1");
1256
1404
  },
1257
1405
 
1406
+ // The fresh-reads cookie (browser/cookie-name.ts): one Set-Cookie per
1407
+ // response, the longest Max-Age any invalidation asked for. HttpOnly (the
1408
+ // client never reads it), SameSite=Lax, Path=/. rawStubHeaders: an
1409
+ // internal writer, allowed wherever updateTag()/revalidateTag() are. A
1410
+ // shared cache never stores it (Set-Cookie is a per-client signal).
1411
+ _setFreshReadsCookie(maxAgeMs: number): void {
1412
+ if (!freshCookie) return;
1413
+ const maxAge = Math.ceil(maxAgeMs / 1000);
1414
+ // Already set for at least this long (an earlier invalidation in this
1415
+ // request): nothing to add, sent or not.
1416
+ if (maxAge <= freshCookieMaxAge) return;
1417
+ if (ctx._responseSent) {
1418
+ // A streaming loader or render: the headers already left. The
1419
+ // invalidation itself still ran; only the mutating user's bypass of
1420
+ // the stores' isolate memos is lost.
1421
+ if (process.env.NODE_ENV !== "production") {
1422
+ console.warn(
1423
+ `[rango] updateTag()/revalidateTag() ran after the response ` +
1424
+ `headers were sent (from a streaming loader or render), so the ` +
1425
+ `${freshCookie} cookie was not set: this user's next requests ` +
1426
+ `can be served from a store's isolate memo for a few seconds. ` +
1427
+ `Invalidate in a server action, a route handler, or middleware.`,
1428
+ );
1429
+ }
1430
+ return;
1431
+ }
1432
+ const serialized = (age: number) =>
1433
+ `${freshCookie}=1; Max-Age=${age}; Path=/; HttpOnly; SameSite=Lax` +
1434
+ (url.protocol === "https:" ? "; Secure" : "");
1435
+ if (freshCookieMaxAge > 0) {
1436
+ const prior = serialized(freshCookieMaxAge);
1437
+ const kept = rawStubHeaders
1438
+ .getSetCookie()
1439
+ .filter((cookie) => cookie !== prior);
1440
+ rawStubHeaders.delete("Set-Cookie");
1441
+ for (const cookie of kept) rawStubHeaders.append("Set-Cookie", cookie);
1442
+ }
1443
+ freshCookieMaxAge = maxAge;
1444
+ rawStubHeaders.append("Set-Cookie", serialized(maxAge));
1445
+ invalidateResponseCookieCache();
1446
+ },
1447
+
1258
1448
  setStatus(status: number): void {
1259
1449
  assertNotInsideCacheExec(ctx, "setStatus");
1260
1450
  assertResponseWriteAllowed("setStatus");
@@ -1276,6 +1466,9 @@ export function createRequestContext<TEnv>(
1276
1466
  _explicitTaggedStores: explicitTaggedStores,
1277
1467
  _requestTags: new Set<string>(),
1278
1468
  _cacheProfiles: cacheProfiles,
1469
+ _freshReads:
1470
+ freshCookie !== undefined &&
1471
+ getRawCookieValue(cookieHeader, freshCookie) !== null,
1279
1472
 
1280
1473
  waitUntil(fn: () => Promise<void>): void {
1281
1474
  if (ctx.build) return;
@@ -1317,9 +1510,8 @@ export function createRequestContext<TEnv>(
1317
1510
  this._onResponseCallbacks.push(callback);
1318
1511
  },
1319
1512
 
1320
- get theme() {
1321
- return themeConfig ? getTheme() : undefined;
1322
- },
1513
+ // theme: a guarded, non-enumerable getter defined after this literal.
1514
+ _readTheme: getTheme,
1323
1515
  setTheme: themeConfig
1324
1516
  ? (theme: Theme) => {
1325
1517
  assertNotInsideCacheExec(ctx, "setTheme");
@@ -1338,6 +1530,7 @@ export function createRequestContext<TEnv>(
1338
1530
  _locationState: undefined,
1339
1531
 
1340
1532
  _reportedErrors: new WeakSet<object>(),
1533
+ _renderErrors: [],
1341
1534
  _metricsStore: undefined,
1342
1535
  _renderForeground: undefined,
1343
1536
  _activeRoutine: undefined,
@@ -1359,7 +1552,39 @@ export function createRequestContext<TEnv>(
1359
1552
  getContext: () => ctx,
1360
1553
  });
1361
1554
 
1555
+ // getRequestContext().theme is the visitor's cookie, guarded like cookies()
1556
+ // (#971); before it was a plain read that a ppr shell, a cache() entry or a
1557
+ // "use cache" entry stored for every visitor. Non-enumerable like the
1558
+ // handler ctx.theme (handler-context.ts THEME_DESCRIPTOR): serializing the
1559
+ // context (React dev debug info) must not count as a read. The guard reads
1560
+ // the ambient context, as cookies() does, and falls back to the receiver.
1561
+ // _readTheme is the unguarded read: non-enumerable too, so a spread copy of
1562
+ // the context (the fetchable-loader ctx, loader-fetch.ts) cannot carry it.
1563
+ Object.defineProperties(ctx, {
1564
+ theme: {
1565
+ get(this: RequestContext<TEnv>): Theme | undefined {
1566
+ if (!themeConfig) return undefined;
1567
+ assertThemeReadAllowed(
1568
+ _getRequestContext() ?? this,
1569
+ "getRequestContext().theme",
1570
+ );
1571
+ return getTheme();
1572
+ },
1573
+ enumerable: false,
1574
+ configurable: true,
1575
+ },
1576
+ _readTheme: { enumerable: false },
1577
+ });
1578
+
1362
1579
  (ctx as any)[NOCACHE_SYMBOL] = true;
1580
+ // Non-enumerable: a self-reference would make the context circular for
1581
+ // JSON.stringify, spreads and React's dev debug-info serialization of props.
1582
+ Object.defineProperty(ctx, "_requestRoot", {
1583
+ value: ctx,
1584
+ enumerable: false,
1585
+ configurable: true,
1586
+ writable: true,
1587
+ });
1363
1588
  return ctx;
1364
1589
  }
1365
1590
 
@@ -1627,7 +1852,9 @@ export function createUseFunction<TEnv>(
1627
1852
  ? parseSearchParams(ctx.searchParams, searchSchema)
1628
1853
  : {};
1629
1854
 
1630
- const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
1855
+ const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> & {
1856
+ _routeName?: string;
1857
+ } = {
1631
1858
  params: ctx.params,
1632
1859
  routeParams: (ctx.params ?? {}) as Record<string, string>,
1633
1860
  request: ctx.request,
@@ -1666,6 +1893,7 @@ export function createUseFunction<TEnv>(
1666
1893
  ? isRouteRootScoped(ctx._routeName, ctx._routerId)
1667
1894
  : undefined,
1668
1895
  ),
1896
+ _routeName: ctx._routeName,
1669
1897
  rendered: () => {
1670
1898
  throw new Error(
1671
1899
  `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
@@ -1673,6 +1901,8 @@ export function createUseFunction<TEnv>(
1673
1901
  );
1674
1902
  },
1675
1903
  };
1904
+ // "use cache" keys it by route, like the request ctx it reads (#940).
1905
+ (loaderCtx as any)[NOCACHE_SYMBOL] = true;
1676
1906
 
1677
1907
  // Meter through the same unified phase API as the loader-resolution funnel
1678
1908
  // (observePhase), so a loader resolved via this base request-context ctx.use