@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
@@ -107,6 +107,7 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
107
107
  import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
108
108
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
109
109
  import { getContext } from "../../server/context.js";
110
+ import { executionStart } from "../../cache/tag-invalidation.js";
110
111
  import {
111
112
  createRequestContext,
112
113
  runWithRequestContext,
@@ -134,10 +135,6 @@ import {
134
135
  * cache-scope.ts), so a swap sent live pushes into this render.
135
136
  * setupLoaderAccess binds the store at setup, so it runs inside the
136
137
  * derived context too.
137
- * - _transitionWhen: fresh resolution appends transition({ when })
138
- * predicates; the foreground's gateTransitions reads the array after the
139
- * match, and a stale HIT (which replays the stored transition) must not
140
- * evaluate this render's predicates.
141
138
  * - _metricsStore: undefined, and the DSL store below has `metrics` unset
142
139
  * (track() reads that one), so nothing lands on the foreground's perf
143
140
  * timeline. ctx.Store.run closes over ctx.Store, so the derived store
@@ -156,6 +153,7 @@ export async function rerenderAndCacheRoute<TEnv>(
156
153
  cacheScope: CacheScope,
157
154
  routerCtx: RouterContext<TEnv>,
158
155
  ): Promise<number> {
156
+ const start = executionStart();
159
157
  const handleStore = routerCtx.createHandleStore();
160
158
  // Response writes land in a throwaway context nothing merges or drains. Its
161
159
  // mutators are closures over its own stub, and they keep the same guards.
@@ -172,7 +170,6 @@ export async function rerenderAndCacheRoute<TEnv>(
172
170
  }),
173
171
  {
174
172
  _handleStore: handleStore,
175
- _transitionWhen: [],
176
173
  _metricsStore: undefined,
177
174
  _onResponseCallbacks: [],
178
175
  header: sink.header,
@@ -253,6 +250,7 @@ export async function rerenderAndCacheRoute<TEnv>(
253
250
  ctx.matched.params,
254
251
  segments,
255
252
  ctx.isIntercept,
253
+ start,
256
254
  );
257
255
  return segments.length;
258
256
  });
@@ -92,12 +92,14 @@
92
92
  * - Action context (if POST)
93
93
  */
94
94
  import type { InternalHandlerContext, ResolvedSegment } from "../../types.js";
95
+ import type { OwnedPushDelivery } from "../../cache/handle-snapshot.js";
95
96
  import type { EntryData } from "../../server/context.js";
96
97
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
97
98
  import { getRouterContext, type RouterContext } from "../router-context.js";
98
99
  import { observeEvent } from "../instrument.js";
99
100
  import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
100
101
  import { treeHasStreaming } from "./segment-resolution.js";
102
+ import { liveLaneLoaderIds } from "../segment-resolution/loader-cache.js";
101
103
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
102
104
  import {
103
105
  _getRequestContext,
@@ -107,6 +109,7 @@ import {
107
109
  createShellImplicitDocScope,
108
110
  type CacheScope,
109
111
  } from "../../cache/cache-scope.js";
112
+ import { ShellRecordUnavailableError } from "../../cache/shell-snapshot.js";
110
113
  import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
111
114
  import { paramsEqual } from "../params-util.js";
112
115
 
@@ -296,8 +299,7 @@ async function* yieldFromStore<TEnv>(
296
299
  !paramsChanged &&
297
300
  ctx.clientSegmentSet.has(segment.id)
298
301
  ) {
299
- segment.component = null;
300
- segment.loading = undefined;
302
+ keepClientSegment(segment);
301
303
  }
302
304
  yield segment;
303
305
  }
@@ -404,6 +406,21 @@ async function* tryPrerenderLookup<TEnv>(
404
406
  return true;
405
407
  }
406
408
 
409
+ /**
410
+ * Keep the client's copy of a stored (runtime cache, prerender, or shell
411
+ * replay) segment this navigation does not re-render. collectMatchResult
412
+ * (match-result.ts) omits a null-component segment the client holds, as the
413
+ * live partial path omits a segment whose revalidation said no
414
+ * (segment-resolution/revalidation.ts).
415
+ *
416
+ * A transition({ when }) is no exception: it is decided in the browser for
417
+ * kept segments too, so no segment is re-sent to carry a decision (#986).
418
+ */
419
+ function keepClientSegment(segment: ResolvedSegment): void {
420
+ segment.component = null;
421
+ segment.loading = undefined;
422
+ }
423
+
407
424
  /**
408
425
  * Async generator middleware type
409
426
  */
@@ -508,6 +525,15 @@ export function withCacheLookup<TEnv>(
508
525
  }
509
526
  }
510
527
 
528
+ // A document HIT tail replays the handler layer through the implicit doc
529
+ // scope and never runs a handler: without that scope (a context that lost
530
+ // the marker before the scope resolved), fail like a lookup miss instead
531
+ // of falling through to the handlers below.
532
+ const tailMarker = pipelineReqCtx?._shellImplicitCache;
533
+ if (tailMarker?.docTail && !ctx.cacheScope?.isShellImplicitDocScope) {
534
+ throw new ShellRecordUnavailableError(tailMarker.fixedDocKey);
535
+ }
536
+
511
537
  if (ctx.isAction || !ctx.cacheScope?.enabled) {
512
538
  yield* source;
513
539
  if (ms) {
@@ -521,14 +547,24 @@ export function withCacheLookup<TEnv>(
521
547
  }
522
548
 
523
549
  // A record's loader-owned handle values claim their loader, so its own
524
- // cache() HIT does not replay them again (restoreHandles).
525
- const claimLoaderPushes = (ctx.handlerContext as InternalHandlerContext)
526
- ._claimLoaderPushes;
550
+ // cache() HIT does not replay them again (restoreHandles). A document
551
+ // HIT tail restores a bake-lane owner's values as the prelude's, unclaimed:
552
+ // they stand against any later push of that loader
553
+ // (HandleStore.pushRestored). An owner the route runs on the live lane is
554
+ // a hole, so its live run still replaces them, even inside a restored
555
+ // loader's body.
556
+ const ownedPushes: OwnedPushDelivery = {
557
+ claim: (ctx.handlerContext as InternalHandlerContext)._claimLoaderPushes,
558
+ };
559
+ if (tailMarker) {
560
+ ownedPushes.liveLane = liveLaneLoaderIds(ctx.entries);
561
+ ownedPushes.restore = tailMarker.docTail === true;
562
+ }
527
563
  const explicitLookup = await ctx.cacheScope.lookupRouteDetailed(
528
564
  ctx.pathname,
529
565
  ctx.matched.params,
530
566
  ctx.isIntercept,
531
- claimLoaderPushes,
567
+ ownedPushes,
532
568
  );
533
569
  let cacheResult =
534
570
  explicitLookup.status === "hit" ? explicitLookup.result : null;
@@ -572,7 +608,7 @@ export function withCacheLookup<TEnv>(
572
608
  ctx.pathname,
573
609
  ctx.matched.params,
574
610
  ctx.isIntercept,
575
- claimLoaderPushes,
611
+ ownedPushes,
576
612
  );
577
613
  } else if (explicitLookup.status === "bypass") {
578
614
  // condition() refused at lookup time (the gate only pre-decides the
@@ -585,6 +621,13 @@ export function withCacheLookup<TEnv>(
585
621
  }
586
622
 
587
623
  if (!cacheResult) {
624
+ // A document shell HIT tail replays the handler layer from the entry's
625
+ // doc record and must never run a handler behind the committed prelude:
626
+ // a record that did not hit (it failed to decode, or the entry lost it)
627
+ // ends the tail here; serveShellHit degrades the response.
628
+ if (replayMarker?.docTail) {
629
+ throw new ShellRecordUnavailableError(replayMarker.fixedDocKey);
630
+ }
588
631
  yield* source;
589
632
  if (ms) {
590
633
  ms.metrics.push({
@@ -658,7 +701,6 @@ export function withCacheLookup<TEnv>(
658
701
  ...liveMatchedIds,
659
702
  ...cacheResult.segments.map((s) => s.id),
660
703
  ];
661
- const pprTransitionDecisions = pipelineReqCtx?._pprTransitionDecisions;
662
704
 
663
705
  const canCheckSegmentRevalidation =
664
706
  !ctx.isFullMatch &&
@@ -734,13 +776,7 @@ export function withCacheLookup<TEnv>(
734
776
  reason: "cached-no-rules",
735
777
  });
736
778
  }
737
- // A PPR transition decision must reach the client even when this cached
738
- // segment otherwise needs no update. Keep its replayed component so the
739
- // partial result retains the segment for gateTransitions().
740
- if (!pprTransitionDecisions?.has(segment.id)) {
741
- segment.component = null;
742
- segment.loading = undefined;
743
- }
779
+ keepClientSegment(segment);
744
780
  yield segment;
745
781
  continue;
746
782
  }
@@ -772,10 +808,7 @@ export function withCacheLookup<TEnv>(
772
808
  shouldRevalidate,
773
809
  });
774
810
 
775
- if (!shouldRevalidate && !pprTransitionDecisions?.has(segment.id)) {
776
- segment.component = null;
777
- segment.loading = undefined;
778
- }
811
+ if (!shouldRevalidate) keepClientSegment(segment);
779
812
 
780
813
  yield segment;
781
814
  }
@@ -107,6 +107,8 @@ import {
107
107
  } from "../../server/request-context.js";
108
108
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
109
109
  import { createShellImplicitDocScope } from "../../cache/cache-scope.js";
110
+ import { recordSegmentTags } from "../../cache/cache-tag.js";
111
+ import { executionStart } from "../../cache/tag-invalidation.js";
110
112
  import { getRouterContext } from "../router-context.js";
111
113
  import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
112
114
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
@@ -127,6 +129,9 @@ export function withCacheStore<TEnv>(
127
129
  source: AsyncGenerator<ResolvedSegment>,
128
130
  ): AsyncGenerator<ResolvedSegment> {
129
131
  const ms = ctx.metricsStore;
132
+ // Before the lookup and the handlers below it run: the record's write
133
+ // gate (#977, CacheScope.cacheRoute).
134
+ const renderStart = executionStart();
130
135
 
131
136
  const allSegments: ResolvedSegment[] = [];
132
137
  for await (const segment of source) {
@@ -262,6 +267,10 @@ export function withCacheStore<TEnv>(
262
267
  ctx.matched.params,
263
268
  allSegmentsToCache,
264
269
  ctx.isIntercept,
270
+ // A capture's doc record lives only in the shell entry, which
271
+ // putShell gates by the capture start; skipping it here would
272
+ // read as a render that produced no record.
273
+ cacheScope.isShellImplicitDocScope ? undefined : renderStart,
265
274
  );
266
275
  if (INTERNAL_RANGO_DEBUG) {
267
276
  const dur = performance.now() - start;
@@ -316,9 +325,10 @@ export function withCacheStore<TEnv>(
316
325
  * store) on the no-eligible-snapshot path, so the lookup's own refusal is
317
326
  * still surfaced while the fallback has nothing to serve.
318
327
  *
319
- * Wrapped in requestCtx.waitUntil — during a capture that is the tracked-write
320
- * override, so captureAndStoreShell's settleWrites awaits the record before
321
- * draining the snapshot.
328
+ * Registered through requestCtx.onResponse, which a capture fires only after
329
+ * the handler pushes settle (settleCaptureRecord), and wrapped in
330
+ * requestCtx.waitUntil — during a capture that is the tracked-write override,
331
+ * so settleWrites awaits the record before the capture reads it.
322
332
  */
323
333
  function recordShellCaptureDocRecord<TEnv>(
324
334
  ctx: MatchContext<TEnv>,
@@ -336,12 +346,28 @@ function recordShellCaptureDocRecord<TEnv>(
336
346
  if (!scope.allowsCache("write")) return;
337
347
 
338
348
  const docScope = createShellImplicitDocScope(marker);
339
- requestCtx.waitUntil(() =>
340
- docScope.cacheRoute(
341
- ctx.pathname,
342
- ctx.matched.params,
343
- segments,
344
- ctx.isIntercept,
345
- ),
349
+ // The route scope's cache({ tags }), and its enclosing scopes' (#974),
350
+ // describe these segments too: on its HIT they arrive with the replayed
351
+ // record (recordSegmentTags in lookupRouteDetailed); on a fresh render they
352
+ // are recorded here, so the doc record, and the shell taking its tags,
353
+ // carries them either way.
354
+ recordSegmentTags(
355
+ segments.map((s) => s.id),
356
+ scope.resolveTags(requestCtx),
357
+ requestCtx,
346
358
  );
359
+ // Through onResponse, like the implicit scope's own write: the capture
360
+ // fires these callbacks (settleCaptureRecord) only after the handler
361
+ // pushes settled, so the record's handle encode never races them.
362
+ requestCtx.onResponse((response) => {
363
+ requestCtx.waitUntil(() =>
364
+ docScope.cacheRoute(
365
+ ctx.pathname,
366
+ ctx.matched.params,
367
+ segments,
368
+ ctx.isIntercept,
369
+ ),
370
+ );
371
+ return response;
372
+ });
347
373
  }
@@ -281,6 +281,7 @@ export function buildMatchResult<TEnv>(
281
281
  routeName: ctx.routeKey,
282
282
  slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
283
283
  ...(interceptTargets.length > 0 ? { interceptTargets } : {}),
284
+ ...(ctx.interceptTargeted ? { interceptTargeted: true } : {}),
284
285
  routeMiddleware:
285
286
  ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
286
287
  };
@@ -68,7 +68,12 @@ export interface MiddlewareContext<
68
68
 
69
69
  debugPerformance(): void;
70
70
 
71
- theme?: Theme;
71
+ /**
72
+ * The visitor's theme (cookie or default); undefined without theme config.
73
+ * Guarded like `cookies()`: a read inside a `"use cache"` function throws
74
+ * (#971).
75
+ */
76
+ readonly theme?: Theme;
72
77
 
73
78
  setTheme?: (theme: Theme) => void;
74
79
 
@@ -31,6 +31,8 @@ import {
31
31
  } from "../cache/taint.js";
32
32
  import { getCacheExecScope } from "../cache/cache-exec-scope.js";
33
33
  import { assertNonCacheableReadAllowed } from "../server/context.js";
34
+ import { readGuardedTheme } from "../server/cookie-store.js";
35
+ import type { Theme } from "../theme/types.js";
34
36
 
35
37
  // Re-export types consumed through this module's path.
36
38
  export type {
@@ -40,6 +42,22 @@ export type {
40
42
  MiddlewareFn,
41
43
  } from "./middleware-types.js";
42
44
 
45
+ /**
46
+ * Middleware ctx.theme: the visitor's theme cookie, guarded like the handler
47
+ * ctx.theme and cookies() (#971, #946), so a read inside a "use cache"
48
+ * function the middleware calls throws. Non-enumerable like the handler and
49
+ * request-context getters: a spread of the ctx (inside a "use cache"
50
+ * function, say) does not read it. One shared descriptor, so every ctx keeps
51
+ * the same property shape.
52
+ */
53
+ const MIDDLEWARE_THEME_DESCRIPTOR: PropertyDescriptor = {
54
+ get(): Theme | undefined {
55
+ return readGuardedTheme(_getRequestContext(), "ctx.theme");
56
+ },
57
+ enumerable: false,
58
+ configurable: true,
59
+ };
60
+
43
61
  const MIDDLEWARE_METRIC_DEPTH = 1;
44
62
  const POST_METRIC_MIN_DURATION_MS = 0.01;
45
63
 
@@ -320,10 +338,7 @@ export function createMiddlewareContext<TEnv>(
320
338
  responseHolder.response.headers.set(name, value);
321
339
  },
322
340
 
323
- get theme(): MiddlewareContext<TEnv>["theme"] {
324
- return _getRequestContext()?.theme;
325
- },
326
-
341
+ // theme: MIDDLEWARE_THEME_DESCRIPTOR, defined after this literal.
327
342
  get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
328
343
  return _getRequestContext()?.setTheme;
329
344
  },
@@ -361,6 +376,7 @@ export function createMiddlewareContext<TEnv>(
361
376
  }
362
377
  },
363
378
  };
379
+ Object.defineProperty(ctx, "theme", MIDDLEWARE_THEME_DESCRIPTOR);
364
380
  (ctx as any)[NOCACHE_SYMBOL] = true;
365
381
  return ctx;
366
382
  }
@@ -1,3 +1,4 @@
1
+ import { requestHeaders } from "../server/request-headers.js";
1
2
  import type { RouteMatchResult } from "./pattern-matching.js";
2
3
 
3
4
  export interface NavigationSnapshot {
@@ -40,8 +41,8 @@ export interface ResolveNavigationDeps {
40
41
  */
41
42
  export function getNavigationContextHeader(request: Request): string | null {
42
43
  return (
43
- request.headers.get("X-RSC-Router-Client-Path") ||
44
- request.headers.get("Referer")
44
+ requestHeaders(request).get("X-RSC-Router-Client-Path") ||
45
+ requestHeaders(request).get("Referer")
45
46
  );
46
47
  }
47
48
 
@@ -59,7 +60,7 @@ export function prerenderStoreShortCircuits(
59
60
  pr: true | undefined,
60
61
  request: Request,
61
62
  ): boolean {
62
- return pr === true && !request.headers.get("X-RSC-HMR");
63
+ return pr === true && !requestHeaders(request).get("X-RSC-HMR");
63
64
  }
64
65
 
65
66
  /**
@@ -71,7 +72,7 @@ export function prerenderStoreShortCircuits(
71
72
  * request would report `prerender-store` for an artifact that cannot serve.
72
73
  */
73
74
  export function getInterceptSourceHeader(request: Request): string | null {
74
- return request.headers.get("X-RSC-Router-Intercept-Source");
75
+ return requestHeaders(request).get("X-RSC-Router-Intercept-Source");
75
76
  }
76
77
 
77
78
  export async function resolveNavigation(
@@ -85,7 +86,7 @@ export async function resolveNavigation(
85
86
  const stale = url.searchParams.get("_rsc_stale") === "true";
86
87
  const previousUrl = getNavigationContextHeader(request);
87
88
  const interceptSourceUrl = getInterceptSourceHeader(request);
88
- const isHmr = !!request.headers.get("X-RSC-HMR");
89
+ const isHmr = !!requestHeaders(request).get("X-RSC-HMR");
89
90
 
90
91
  if (!previousUrl) {
91
92
  return null;
@@ -223,6 +223,8 @@ export async function matchForPrerender<TEnv = any>(
223
223
  res: stubRes,
224
224
  cookie: () => undefined,
225
225
  cookies: () => ({}),
226
+ _readCookie: () => undefined,
227
+ _readCookies: () => ({}),
226
228
  setCookie: () => {},
227
229
  deleteCookie: () => {},
228
230
  header: () => {},
@@ -230,6 +232,7 @@ export async function matchForPrerender<TEnv = any>(
230
232
  _setStatus: () => {},
231
233
  _rotateStateCookie: () => {},
232
234
  _setKeepCacheDirective: () => {},
235
+ _setFreshReadsCookie: () => {},
233
236
  use: (() => {
234
237
  throw new Error("use() not available during pre-rendering");
235
238
  }) as any,
@@ -239,6 +242,8 @@ export async function matchForPrerender<TEnv = any>(
239
242
  waitUntil: () => {},
240
243
  onResponse: () => {},
241
244
  _onResponseCallbacks: [],
245
+ // No theme at build time: no request cookie.
246
+ _readTheme: () => undefined,
242
247
  setLocationState() {},
243
248
  _locationState: undefined,
244
249
  _renderBarrier: Promise.resolve(),
@@ -523,6 +528,8 @@ export async function renderStaticSegment<TEnv = any>(
523
528
  res: stubRes,
524
529
  cookie: () => undefined,
525
530
  cookies: () => ({}),
531
+ _readCookie: () => undefined,
532
+ _readCookies: () => ({}),
526
533
  setCookie: () => {},
527
534
  deleteCookie: () => {},
528
535
  header: () => {},
@@ -530,6 +537,7 @@ export async function renderStaticSegment<TEnv = any>(
530
537
  _setStatus: () => {},
531
538
  _rotateStateCookie: () => {},
532
539
  _setKeepCacheDirective: () => {},
540
+ _setFreshReadsCookie: () => {},
533
541
  use: (() => {
534
542
  throw new Error("use() not available during static pre-rendering");
535
543
  }) as any,
@@ -539,6 +547,8 @@ export async function renderStaticSegment<TEnv = any>(
539
547
  waitUntil: () => {},
540
548
  onResponse: () => {},
541
549
  _onResponseCallbacks: [],
550
+ // No theme at build time: no request cookie.
551
+ _readTheme: () => undefined,
542
552
  setLocationState() {},
543
553
  _locationState: undefined,
544
554
  _renderBarrier: Promise.resolve(),
@@ -15,6 +15,7 @@
15
15
  * Builds on RouteSnapshot from route-snapshot.ts.
16
16
  */
17
17
 
18
+ import { requestHeaders } from "../server/request-headers.js";
18
19
  import { RouteNotFoundError } from "../errors.js";
19
20
  import type { EntryData } from "../server/context.js";
20
21
  import type { CollectedMiddleware } from "./middleware-types.js";
@@ -149,10 +150,11 @@ export async function classifyRequest<TEnv = any>(
149
150
  ): Promise<RequestPlan<TEnv>> {
150
151
  const pathname = url.pathname;
151
152
  const isAction =
152
- request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
153
+ requestHeaders(request).has("rsc-action") ||
154
+ url.searchParams.has("_rsc_action");
153
155
  const isLoaderFetch = url.searchParams.has("_rsc_loader");
154
156
  const isPartialReq = url.searchParams.has("_rsc_partial");
155
- const contentType = request.headers.get("content-type") || "";
157
+ const contentType = requestHeaders(request).get("content-type") || "";
156
158
  const isFormSubmission =
157
159
  contentType.includes("multipart/form-data") ||
158
160
  contentType.includes("application/x-www-form-urlencoded");
@@ -176,7 +178,7 @@ export async function classifyRequest<TEnv = any>(
176
178
  ) {
177
179
  let reloadUrl = stripInternalParams(url).toString();
178
180
  if (isAction) {
179
- const referer = request.headers.get("referer");
181
+ const referer = requestHeaders(request).get("referer");
180
182
  if (referer) {
181
183
  try {
182
184
  const refererUrl = new URL(referer);
@@ -248,7 +250,8 @@ export async function classifyRequest<TEnv = any>(
248
250
  }
249
251
 
250
252
  const actionId =
251
- request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
253
+ requestHeaders(request).get("rsc-action") ||
254
+ url.searchParams.get("_rsc_action");
252
255
 
253
256
  const hasVariants =
254
257
  snapshot.matched.negotiateVariants &&
@@ -518,7 +518,10 @@ export interface RangoOptions<TEnv = any> {
518
518
  *
519
519
  * Controls how long prefetch responses are kept in the client-side
520
520
  * in-memory cache and sets `Cache-Control: private, max-age=<ttl>`
521
- * on server responses for CDN/edge caching.
521
+ * on prefetch responses, so the browser's HTTP cache may reuse them
522
+ * (`private`: shared caches such as a CDN do not store them). A route an
523
+ * intercept targets gets no prefetch `Cache-Control`: whether its
524
+ * response is the modal or the full page depends on the source page.
522
525
  *
523
526
  * The cache is automatically invalidated on server actions regardless
524
527
  * of TTL, so this is primarily a staleness safety net.
@@ -37,7 +37,7 @@ import { applyViewTransitionDefault } from "./view-transition-default.js";
37
37
  import { _getRequestContext } from "../../server/request-context.js";
38
38
  import { getRouterContext } from "../router-context.js";
39
39
  import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
40
- import { observeHandler } from "../instrument.js";
40
+ import { observeSegmentHandler } from "../instrument.js";
41
41
  import {
42
42
  track,
43
43
  RangoContext,
@@ -173,9 +173,7 @@ export async function resolveLoaders<TEnv>(
173
173
  // bakes at capture regardless of the entry's loading() lane
174
174
  // (loader-cache.ts capture branch) and its HIT-tail seed
175
175
  // overlay needs the same key.
176
- bakeLane || loaderEntry.awaitBeforeFlush === true
177
- ? segmentId
178
- : null,
176
+ bakeLane || loaderEntry.bake === true ? segmentId : null,
179
177
  ),
180
178
  ),
181
179
  entry,
@@ -223,7 +221,7 @@ export async function resolveLoaders<TEnv>(
223
221
  loaderEntry,
224
222
  ctx,
225
223
  ctx.pathname,
226
- bakeLane || loaderEntry.awaitBeforeFlush === true ? segmentId : null,
224
+ bakeLane || loaderEntry.bake === true ? segmentId : null,
227
225
  ),
228
226
  ),
229
227
  entry,
@@ -326,7 +324,6 @@ export async function resolveSegment<TEnv>(
326
324
  transition: applyViewTransitionDefault(
327
325
  entry.transition,
328
326
  deps.viewTransitionDefault,
329
- entry.shortCode,
330
327
  ),
331
328
  params,
332
329
  belongsToRoute: false,
@@ -396,7 +393,7 @@ export async function resolveSegment<TEnv>(
396
393
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
397
394
  if (entry.loading) {
398
395
  const result = handleHandlerResult(
399
- observeHandler(entry.id, handler, context),
396
+ observeSegmentHandler(entry.shortCode, entry.id, handler, context),
400
397
  );
401
398
  if (result instanceof Promise) {
402
399
  warnOnStreamedResponse(result, entry.id);
@@ -420,7 +417,12 @@ export async function resolveSegment<TEnv>(
420
417
  }
421
418
  } else {
422
419
  component = handleHandlerResult(
423
- await observeHandler(entry.id, handler, context),
420
+ await observeSegmentHandler(
421
+ entry.shortCode,
422
+ entry.id,
423
+ handler,
424
+ context,
425
+ ),
424
426
  );
425
427
  doneRouteHandler();
426
428
  }
@@ -471,7 +473,6 @@ export async function resolveSegment<TEnv>(
471
473
  transition: applyViewTransitionDefault(
472
474
  entry.transition,
473
475
  deps.viewTransitionDefault,
474
- entry.shortCode,
475
476
  ),
476
477
  params,
477
478
  belongsToRoute: true,
@@ -560,7 +561,6 @@ export async function resolveOrphanLayout<TEnv>(
560
561
  transition: applyViewTransitionDefault(
561
562
  orphan.transition,
562
563
  deps.viewTransitionDefault,
563
- orphan.shortCode,
564
564
  ),
565
565
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
566
566
  });
@@ -670,7 +670,12 @@ export async function resolveParallelEntry<TEnv>(
670
670
  if (hasLoadingFallback) {
671
671
  const result =
672
672
  typeof handler === "function"
673
- ? observeHandler(`${parallelEntry.id}.${slot}`, handler, context)
673
+ ? observeSegmentHandler(
674
+ `${parentShortCode}.${slot}`,
675
+ `${parallelEntry.id}.${slot}`,
676
+ handler,
677
+ context,
678
+ )
674
679
  : handler;
675
680
  if (result instanceof Promise) {
676
681
  result.finally(doneParallelHandler).catch(() => {});
@@ -694,7 +699,8 @@ export async function resolveParallelEntry<TEnv>(
694
699
  } else {
695
700
  component =
696
701
  typeof handler === "function"
697
- ? await observeHandler(
702
+ ? await observeSegmentHandler(
703
+ `${parentShortCode}.${slot}`,
698
704
  `${parallelEntry.id}.${slot}`,
699
705
  handler,
700
706
  context,
@@ -714,7 +720,6 @@ export async function resolveParallelEntry<TEnv>(
714
720
  transition: applyViewTransitionDefault(
715
721
  parallelEntry.transition,
716
722
  deps.viewTransitionDefault,
717
- `${parentShortCode}.${slot}`,
718
723
  ),
719
724
  params,
720
725
  slot,
@@ -29,7 +29,7 @@ import type {
29
29
  import type { SegmentResolutionDeps } from "../types.js";
30
30
  import { debugLog } from "../logging.js";
31
31
  import { tryStaticLookup } from "./static-store.js";
32
- import { observeHandler } from "../instrument.js";
32
+ import { observeSegmentHandler } from "../instrument.js";
33
33
  import type { TelemetrySink } from "../telemetry.js";
34
34
  import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
35
35
 
@@ -175,7 +175,9 @@ export async function resolveLayoutComponent<TEnv>(
175
175
  // by track("handler:<id>") at the call site). handleHandlerResult stays OUTSIDE
176
176
  // the span so a handler that returns a Response (redirect control flow, which it
177
177
  // rethrows) is not recorded as a span error — mirrors the route-handler sites.
178
- return handleHandlerResult(await observeHandler(entry.id, handler, context));
178
+ return handleHandlerResult(
179
+ await observeSegmentHandler(entry.shortCode, entry.id, handler, context),
180
+ );
179
181
  }
180
182
 
181
183
  // ---------------------------------------------------------------------------