@rangojs/router 0.27.0 → 0.29.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 (243) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/action-fence.d.ts +2 -1
  4. package/dist/types/browser/event-controller.d.ts +8 -2
  5. package/dist/types/browser/history-state.d.ts +3 -9
  6. package/dist/types/browser/navigation-store.d.ts +8 -6
  7. package/dist/types/browser/partial-update.d.ts +11 -0
  8. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  9. package/dist/types/browser/react/use-action.d.ts +7 -2
  10. package/dist/types/browser/react/use-handle.d.ts +19 -16
  11. package/dist/types/browser/react/use-link-status.d.ts +5 -2
  12. package/dist/types/browser/segment-reconciler.d.ts +32 -4
  13. package/dist/types/browser/types.d.ts +24 -5
  14. package/dist/types/browser/wire-protocol.d.ts +26 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  16. package/dist/types/cache/cache-scope.d.ts +34 -22
  17. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  19. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  20. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  21. package/dist/types/cache/segment-codec.d.ts +14 -0
  22. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  23. package/dist/types/cache/taint.d.ts +5 -3
  24. package/dist/types/cache/types.d.ts +7 -3
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  26. package/dist/types/prefetch-markers.d.ts +5 -14
  27. package/dist/types/prerender/store.d.ts +31 -0
  28. package/dist/types/prerender/warm-request.d.ts +3 -3
  29. package/dist/types/prerender.d.ts +2 -2
  30. package/dist/types/route-name.d.ts +5 -0
  31. package/dist/types/router/error-handling.d.ts +11 -5
  32. package/dist/types/router/instrument.d.ts +4 -3
  33. package/dist/types/router/intercept-resolution.d.ts +25 -9
  34. package/dist/types/router/match-context.d.ts +6 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  38. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  39. package/dist/types/router/navigation-snapshot.d.ts +7 -0
  40. package/dist/types/router/prerender-match.d.ts +21 -6
  41. package/dist/types/router/preview-match.d.ts +2 -7
  42. package/dist/types/router/request-classification.d.ts +5 -0
  43. package/dist/types/router/router-context.d.ts +7 -46
  44. package/dist/types/router/router-interfaces.d.ts +8 -14
  45. package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
  46. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  47. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  48. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  49. package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
  52. package/dist/types/router/segment-resolution.d.ts +1 -2
  53. package/dist/types/router/segment-wrappers.d.ts +11 -48
  54. package/dist/types/router/telemetry.d.ts +2 -0
  55. package/dist/types/router/types.d.ts +11 -1
  56. package/dist/types/rsc/full-payload.d.ts +2 -2
  57. package/dist/types/rsc/handler-context.d.ts +6 -6
  58. package/dist/types/rsc/json-route-result.d.ts +5 -9
  59. package/dist/types/rsc/request-stages.d.ts +203 -0
  60. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  61. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  62. package/dist/types/rsc/routine-plan.d.ts +1 -1
  63. package/dist/types/rsc/server-action.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  65. package/dist/types/rsc/shell-capture.d.ts +132 -15
  66. package/dist/types/rsc/shell-serve.d.ts +37 -12
  67. package/dist/types/server/context.d.ts +9 -8
  68. package/dist/types/server/handle-store.d.ts +16 -0
  69. package/dist/types/server/render-pass.d.ts +72 -0
  70. package/dist/types/server/request-context.d.ts +53 -104
  71. package/dist/types/server/rewrite-trail.d.ts +2 -2
  72. package/dist/types/testing/dispatch.d.ts +35 -18
  73. package/dist/types/testing/render-route.d.ts +43 -8
  74. package/dist/types/testing/serve-shell-request.d.ts +8 -1
  75. package/dist/types/types/cache-types.d.ts +6 -3
  76. package/dist/types/types/error-types.d.ts +30 -3
  77. package/dist/types/types/handler-context.d.ts +9 -1
  78. package/dist/types/types/segments.d.ts +1 -1
  79. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  80. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  81. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  82. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  83. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  84. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  85. package/dist/types/vite/discovery/state.d.ts +12 -0
  86. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  87. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  88. package/dist/types/vite/router-discovery.d.ts +8 -47
  89. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  90. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  91. package/dist/types/wire-params.d.ts +39 -0
  92. package/dist/vite/index.js +3822 -3727
  93. package/package.json +1 -1
  94. package/skills/cache-guide/SKILL.md +8 -5
  95. package/skills/caching/SKILL.md +37 -18
  96. package/skills/client-urls/SKILL.md +4 -2
  97. package/skills/document-cache/SKILL.md +13 -1
  98. package/skills/handler-use/SKILL.md +1 -1
  99. package/skills/hooks/SKILL.md +1 -1
  100. package/skills/hooks/data.md +8 -1
  101. package/skills/hooks/handle-and-actions.md +33 -2
  102. package/skills/hooks/navigation.md +121 -15
  103. package/skills/hooks/state.md +6 -4
  104. package/skills/hooks/urls.md +16 -5
  105. package/skills/intercept/SKILL.md +23 -7
  106. package/skills/middleware/SKILL.md +18 -0
  107. package/skills/observability/SKILL.md +30 -0
  108. package/skills/ppr/SKILL.md +18 -3
  109. package/skills/prerender/SKILL.md +39 -15
  110. package/skills/rango/SKILL.md +5 -1
  111. package/skills/server-actions/SKILL.md +15 -1
  112. package/skills/testing/client-components.md +25 -18
  113. package/skills/testing/response-routes.md +6 -4
  114. package/skills/testing/serve-shell-request.md +7 -7
  115. package/skills/use-cache/SKILL.md +46 -8
  116. package/skills/view-transitions/SKILL.md +1 -1
  117. package/src/__internal.ts +7 -1
  118. package/src/browser/action-fence.ts +2 -1
  119. package/src/browser/connection-warmup.ts +3 -1
  120. package/src/browser/event-controller.ts +37 -13
  121. package/src/browser/history-state.ts +6 -12
  122. package/src/browser/navigation-bridge.ts +59 -26
  123. package/src/browser/navigation-client.ts +20 -25
  124. package/src/browser/navigation-store.ts +25 -27
  125. package/src/browser/navigation-transaction.ts +4 -1
  126. package/src/browser/network-error-handler.ts +4 -0
  127. package/src/browser/partial-update.ts +33 -6
  128. package/src/browser/prefetch/cache.ts +2 -1
  129. package/src/browser/prefetch/fetch.ts +8 -16
  130. package/src/browser/prefetch/queue.ts +4 -4
  131. package/src/browser/react/NavigationProvider.tsx +2 -1
  132. package/src/browser/react/filter-segment-order.ts +2 -2
  133. package/src/browser/react/use-action.ts +24 -48
  134. package/src/browser/react/use-handle.ts +22 -18
  135. package/src/browser/react/use-link-status.ts +48 -60
  136. package/src/browser/react/use-navigation.ts +92 -41
  137. package/src/browser/rsc-router.tsx +1 -0
  138. package/src/browser/segment-reconciler.ts +109 -53
  139. package/src/browser/server-action-bridge.ts +81 -61
  140. package/src/browser/types.ts +25 -5
  141. package/src/browser/wire-protocol.ts +43 -0
  142. package/src/cache/cache-key-utils.ts +11 -5
  143. package/src/cache/cache-runtime.ts +132 -75
  144. package/src/cache/cache-scope.ts +110 -64
  145. package/src/cache/cf/cf-cache-constants.ts +2 -2
  146. package/src/cache/cf/cf-cache-store.ts +486 -874
  147. package/src/cache/cf/cf-kv-utils.ts +1 -1
  148. package/src/cache/document-cache.ts +64 -46
  149. package/src/cache/handle-snapshot.ts +19 -7
  150. package/src/cache/read-through-swr.ts +20 -18
  151. package/src/cache/segment-codec.ts +55 -0
  152. package/src/cache/shell-snapshot.ts +2 -2
  153. package/src/cache/tag-invalidation.ts +6 -2
  154. package/src/cache/taint.ts +5 -3
  155. package/src/cache/types.ts +7 -3
  156. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  157. package/src/handles/pushed-by.ts +1 -1
  158. package/src/prefetch-markers.ts +5 -16
  159. package/src/prerender/store.ts +76 -0
  160. package/src/prerender/warm-request.ts +8 -4
  161. package/src/prerender.ts +2 -2
  162. package/src/route-name.ts +8 -0
  163. package/src/router/error-handling.ts +22 -10
  164. package/src/router/instrument.ts +4 -3
  165. package/src/router/intercept-resolution.ts +82 -75
  166. package/src/router/loader-resolution.ts +5 -4
  167. package/src/router/match-api.ts +5 -5
  168. package/src/router/match-context.ts +23 -12
  169. package/src/router/match-handlers.ts +8 -20
  170. package/src/router/match-middleware/background-revalidation.ts +46 -57
  171. package/src/router/match-middleware/cache-lookup.ts +677 -726
  172. package/src/router/match-middleware/cache-store.ts +4 -8
  173. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  174. package/src/router/match-middleware/segment-resolution.ts +34 -61
  175. package/src/router/middleware.ts +69 -11
  176. package/src/router/navigation-snapshot.ts +14 -4
  177. package/src/router/prerender-match.ts +100 -44
  178. package/src/router/preview-match.ts +6 -41
  179. package/src/router/request-classification.ts +13 -7
  180. package/src/router/route-snapshot.ts +2 -5
  181. package/src/router/router-context.ts +10 -95
  182. package/src/router/router-interfaces.ts +8 -14
  183. package/src/router/segment-resolution/helpers.ts +49 -13
  184. package/src/router/segment-resolution/loader-cache.ts +15 -9
  185. package/src/router/segment-resolution/loader-mask.ts +5 -5
  186. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  187. package/src/router/segment-resolution/resolution-pass.ts +139 -0
  188. package/src/router/segment-resolution/revalidation.ts +939 -789
  189. package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
  190. package/src/router/segment-resolution.ts +4 -13
  191. package/src/router/segment-wrappers.ts +51 -269
  192. package/src/router/telemetry.ts +5 -0
  193. package/src/router/types.ts +11 -1
  194. package/src/router.ts +19 -17
  195. package/src/rsc/full-payload.ts +6 -5
  196. package/src/rsc/handler-context.ts +8 -14
  197. package/src/rsc/handler.ts +126 -507
  198. package/src/rsc/json-route-result.ts +5 -9
  199. package/src/rsc/loader-fetch.ts +6 -5
  200. package/src/rsc/progressive-enhancement.ts +5 -0
  201. package/src/rsc/request-stages.ts +759 -0
  202. package/src/rsc/response-cache-serve.ts +10 -14
  203. package/src/rsc/response-route-handler.ts +13 -5
  204. package/src/rsc/rewrite-passes.ts +2 -1
  205. package/src/rsc/routine-plan.ts +1 -1
  206. package/src/rsc/rsc-rendering.ts +164 -349
  207. package/src/rsc/server-action.ts +87 -113
  208. package/src/rsc/shell-capture-constants.ts +1 -1
  209. package/src/rsc/shell-capture.ts +275 -227
  210. package/src/rsc/shell-serve.ts +121 -18
  211. package/src/rsc/ssr-setup.ts +5 -4
  212. package/src/segment-system.tsx +3 -3
  213. package/src/server/context.ts +22 -25
  214. package/src/server/handle-store.ts +55 -0
  215. package/src/server/render-pass.ts +291 -0
  216. package/src/server/request-context.ts +75 -115
  217. package/src/server/rewrite-trail.ts +6 -4
  218. package/src/ssr/index.tsx +1 -1
  219. package/src/testing/dispatch.ts +226 -710
  220. package/src/testing/e2e/page-helpers.ts +2 -2
  221. package/src/testing/render-route.tsx +199 -13
  222. package/src/testing/serve-shell-request.ts +36 -40
  223. package/src/types/cache-types.ts +6 -3
  224. package/src/types/error-types.ts +30 -3
  225. package/src/types/handler-context.ts +12 -1
  226. package/src/types/segments.ts +1 -1
  227. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  228. package/src/vite/discovery/build-env.ts +148 -0
  229. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  230. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  231. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  232. package/src/vite/discovery/discover-routers.ts +1 -1
  233. package/src/vite/discovery/discovery-errors.ts +1 -1
  234. package/src/vite/discovery/prerender-collection.ts +24 -39
  235. package/src/vite/discovery/state.ts +15 -0
  236. package/src/vite/plugins/version-plugin.ts +1 -1
  237. package/src/vite/plugins/virtual-entries.ts +1 -1
  238. package/src/vite/router-discovery.ts +29 -2428
  239. package/src/vite/temp-rsc-server.ts +223 -0
  240. package/src/vite/utils/prerender-utils.ts +47 -0
  241. package/src/wire-params.ts +40 -0
  242. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  243. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -277,6 +277,36 @@ Events include `request.start/end/error`, `loader.start/end/error`,
277
277
  `handler.error`, `cache.decision`, `revalidation.decision`, `request.timeout`,
278
278
  and `request.origin-rejected`.
279
279
 
280
+ ## Errors reported to onError
281
+
282
+ `createRouter({ onError })` sees every server error, including errors from
283
+ work no visitor waits on. A stale `cache()` record, document cache entry, PPR
284
+ shell, `"use cache"` function or cached loader is served stale and refreshed
285
+ beside the request. That refresh's error carries `background: true`, with the
286
+ starting request's `request` and `url`, and `handledByBoundary: false`: the
287
+ failed render is not stored, so no one sees its fallback. The stale entry
288
+ keeps serving.
289
+
290
+ ```typescript
291
+ const router = createRouter({
292
+ onError: ({ error, phase, background, routeKey, duration }) => {
293
+ errorTracker.capture(error, {
294
+ // A failed refresh is not a failed page view.
295
+ level: background ? "warning" : "error",
296
+ tags: { phase, route: routeKey },
297
+ extra: { duration },
298
+ });
299
+ },
300
+ });
301
+ ```
302
+
303
+ `duration` is the time from the request's start to the error; for a
304
+ `background` error, from the start of the request that started the refresh.
305
+ `segmentId` and `segmentType` name the segment that threw: an intercept's or
306
+ a parallel slot's error reports the slot (`segmentType: "parallel"`), an
307
+ orphan layout's error the orphan layout, even when the owning layout's
308
+ boundary renders the fallback.
309
+
280
310
  ## Debugging revalidation and stale data
281
311
 
282
312
  When stale UI or unexpected partial renders are the question, use all three
@@ -450,7 +450,7 @@ non-claiming read option.
450
450
 
451
451
  The first cold partial request reports `BYPASS; reason=no-entry` because no
452
452
  artifact could supply that response; its successful render schedules
453
- `scheduleShellCapture` with a navigation-only marker. The next request reports
453
+ `planShellCapture` with a navigation-only marker. The next request reports
454
454
  `HIT` when the capture produced an eligible snapshot. Navigation snapshots use
455
455
  a separate shell key, so they cannot overwrite or be served as a document shell.
456
456
  The capture runs with the stripped target document URL and loads SSR support in
@@ -509,7 +509,9 @@ In handlers it is
509
509
  too late to prevent a MISS render from already happening, but it still prevents
510
510
  the follow-up shell capture. A `ctx.dynamic()` call that only happens during the
511
511
  capture itself (for example inside a handler promise, after an await) refuses
512
- that capture: a dynamic render has no shell.
512
+ that capture: a dynamic render has no shell. A cached handler that calls it in
513
+ the background refresh of a stale `cache()` record does not: the capture
514
+ replayed the record, and the refresh renders the record only.
513
515
 
514
516
  During `Prerender` + `ppr` build-shell capture, middleware is replayed with
515
517
  `ctx.build === true`, `ctx.waitUntil()` inert, and the same `ctx.dynamic()`
@@ -1177,7 +1179,7 @@ their data should bake vs stay live. Your levers, in order of preference:
1177
1179
  3. **Guaranteed-fresh widgets: live loaders with a widget-sized boundary.**
1178
1180
  An unflagged loader read under an inline `<Suspense>` in the widget is
1179
1181
  enough. Alternatively, a parallel slot with its OWN `loading()`:
1180
- parallel-owned loaders get their own per-slot boundary (`fresh.ts` tags
1182
+ parallel-owned loaders get their own per-slot boundary (`resolveParallelSegments` tags
1181
1183
  them with the slot's loading; `segment-system.tsx` builds a per-slot
1182
1184
  LoaderBoundary), so the chrome bakes into the shell and each widget is an
1183
1185
  independent, widget-sized hole:
@@ -1633,6 +1635,19 @@ evicted by tag at all — move always-fresh data into a live-lane loader (no
1633
1635
  capture is served as before. Only a throw during the capture counts: a
1634
1636
  component that failed in the live render and succeeds when captured is
1635
1637
  stored.
1638
+ - **A handler that throws during capture refuses it too**: when a route,
1639
+ layout or parallel slot handler throws (or throws `notFound()`) on the
1640
+ capture's own run while the request that scheduled it rendered fine, for
1641
+ example on a flaky upstream, its boundary (a declared `errorBoundary()` or
1642
+ `notFoundBoundary()`, or the default error page) would become the shell.
1643
+ The capture stores nothing, logs `[ShellCache] capture`, reports to
1644
+ `onError` as `cache-write`, and backs the key off; the next capture after
1645
+ the window stores normally. A request whose own render fails answers
1646
+ 500/404 and schedules no capture, but a recapture has no visitor render
1647
+ behind it: a stale shell whose recapture fails keeps serving until its
1648
+ `ttl` + `swr` ends (a build-time shell, until a recapture stores), as a
1649
+ failed `cache()` refresh does, logging and calling `onError` once per
1650
+ backoff window. When you delete a page, invalidate its tag.
1636
1651
 
1637
1652
  ## Related
1638
1653
 
@@ -309,16 +309,16 @@ layout(DocsNav, () => [
309
309
 
310
310
  ## Interaction with DSL Items
311
311
 
312
- | DSL item | Behavior with Prerender |
313
- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
314
- | `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
315
- | `revalidate()` | Without Passthrough: no effect on the prerendered segments (re-served from the build artifact); loader revalidation still works. With Passthrough: re-renders through the live handler. |
316
- | `cache()` | Orthogonal -- prerendered segments bypass the runtime cache. Use `cache()` on parent layouts and loaders. |
317
- | `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live. |
318
- | `parallel()` | Parallel slots inside path are pre-rendered. |
319
- | `middleware()` | Skipped while collecting build-time Flight payloads (no request). For `Prerender` + `ppr`, the build-shell step replays global and route middleware with `ctx.build === true`; `ctx.dynamic()` skips that shell. At request time middleware runs normally on every request. |
320
- | `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
321
- | `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally). |
312
+ | DSL item | Behavior with Prerender |
313
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
314
+ | `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
315
+ | `revalidate()` | Without Passthrough: re-runs no prerendered handler (segments are re-served from the build artifact); when the params change, a held parent layout whose `revalidate()` asks for it gets its baked copy. Loader revalidation still works. With Passthrough: re-renders through the live handler. |
316
+ | `cache()` | Orthogonal -- prerendered segments bypass the runtime cache. Use `cache()` on parent layouts and loaders. |
317
+ | `layout()` | Child layouts inside path are pre-rendered. Parent layouts are pre-rendered too: every hit, intercept variant included, serves them from the build artifact and runs no layout handler. On a navigation a parent layout the client holds stays held, even when the params change, as on a live route. |
318
+ | `parallel()` | Parallel slots inside path are pre-rendered. A parent layout's slots are pre-rendered too: a hit serves them from the build artifact with the layout, and one the client holds stays held, as on a live route. |
319
+ | `middleware()` | Skipped while collecting build-time Flight payloads (no request). For `Prerender` + `ppr`, the build-shell step replays global and route middleware with `ctx.build === true`; `ctx.dynamic()` skips that shell. At request time middleware runs normally on every request. |
320
+ | `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
321
+ | `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside the main entry, without the route's own segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally); a navigation serves only the intercept it selected. |
322
322
 
323
323
  When Passthrough revalidation is enabled, remember that revalidation is
324
324
  still partial: opting a child segment into revalidation does not
@@ -1426,19 +1426,24 @@ export const urlpatterns = urls(({ path, include }) => [
1426
1426
  ## Interaction with intercept()
1427
1427
 
1428
1428
  When a pre-rendered route is also the target of an `intercept()`, the build system
1429
- resolves the intercept handler at build time and stores a combined entry (main
1430
- segments + intercept segments) under an `/i`-suffixed key alongside the main entry:
1429
+ resolves the intercept handler at build time and stores an intercept variant
1430
+ under an `/i`-suffixed key alongside the main entry. The variant holds what an
1431
+ intercept navigation renders: the main segments without the route's own (its
1432
+ handler, layouts and slots, which a render under an intercept skips), then the
1433
+ intercept segments, with the handle values of exactly those segments:
1431
1434
 
1432
1435
  ```
1433
1436
  prerender store keys:
1434
1437
  "blog.post/a1b2c3" -> main segments (full page)
1435
- "blog.post/a1b2c3/i" -> main segments + intercept segments (modal variant)
1438
+ "blog.post/a1b2c3/i" -> main segments without the route's own + intercept segments
1436
1439
  ```
1437
1440
 
1438
1441
  At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
1439
1442
 
1440
1443
  - **Intercept navigation**: looks up `paramHash/i` first. If found, yields
1441
- the combined entry. `handleCacheHitIntercept()` extracts intercept segments
1444
+ the variant's segments with only the slot of the intercept the navigation
1445
+ selected (`selectInterceptVariant`); a variant without that slot is a miss.
1446
+ `handleCacheHitIntercept()` extracts intercept segments
1442
1447
  (filtered by `namespace?.startsWith("intercept:")`) and sets up slots.
1443
1448
  - **Direct navigation**: looks up `paramHash` (no suffix). Standard prerender path.
1444
1449
  - **Intercept miss (no `/i` entry)**: falls through to the normal pipeline so
@@ -1447,7 +1452,26 @@ At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
1447
1452
 
1448
1453
  The `when` config selector receives an `InterceptSelectorContext` whose `from`
1449
1454
  location (the page being left) is unknown at build time. All intercepts are pre-rendered unconditionally;
1450
- `when` is evaluated at runtime by the intercept-resolution middleware.
1455
+ `when` is evaluated at runtime by the intercept-resolution middleware, and a
1456
+ navigation shows only the intercept it selected: the innermost whose `when`
1457
+ passes, as on a route that is not pre-rendered. Two intercepts on one layout
1458
+ that fill the same slot are not pre-rendered: the variant could not tell them
1459
+ apart, so they render live. Intercepts declared in the route's own layouts are
1460
+ never selected and are not pre-rendered.
1461
+
1462
+ The build warns once per route and slot for an intercept a navigation can
1463
+ select that is not pre-rendered: two intercepts that fill one slot, a handler
1464
+ or layout that returned a `Response` at build time, or an intercept that
1465
+ targets the route by its name inside an `include()` (`"item"` for the route
1466
+ `"shop.item"`, or for a route named `"item"` in an unnamed `include()`), which
1467
+ only the local-name fallback selects (name the `include()` and target the
1468
+ full name, or declare the intercept inside it with `".item"`, to pre-render
1469
+ it). That intercept renders live, and its handler runs per request without
1470
+ `BuildContext`; a route in an unnamed `include()` is named by its URL:
1471
+
1472
+ ```
1473
+ [rango] Prerender route "item": intercept @modal is not pre-rendered: two intercepts on one layout fill @modal. A navigation that selects it renders it live, and its handler runs per request without BuildContext. To pre-render it, give each intercept its own slot.
1474
+ ```
1451
1475
 
1452
1476
  ### Example: Pre-rendered route with intercept
1453
1477
 
@@ -249,7 +249,11 @@ identity".
249
249
  The predicate arg carries the action's full context, not just its identity. Match
250
250
  _which_ action with `ctx.isAction(addToCart)` (rename-safe); branch on _what it
251
251
  returned_ with `ctx.actionResult` — the value your `"use server"` function
252
- returned, for outcome-conditional revalidation. The arg also exposes `actionId`
252
+ returned, for outcome-conditional revalidation. When the action threw into an
253
+ `errorBoundary()`, a predicate above the boundary still runs, with
254
+ `actionResult` set to the thrown value: check a field's shape before reading
255
+ it (`Array.isArray(actionResult?.items)`). A `redirect()` it throws then redirects, as
256
+ after a success. The arg also exposes `actionId`
253
257
  (raw `path#export`), `actionUrl`, `formData` (form-based actions only), `method`,
254
258
  and `stale` (the `_rsc_stale` signal that an action ran in this or another tab;
255
259
  that Back/Forward revalidation takes an action's defaults, but `isAction()` is
@@ -569,6 +569,10 @@ A few details:
569
569
  until a request re-runs them.
570
570
  - It holds no segments, so every loader on the page re-runs whatever its
571
571
  `revalidate()` says, and the document cache never answers it.
572
+ - If a later action on that page threw into its `errorBoundary()`, the
573
+ revalidation keeps the boundary on screen: everything above it re-runs,
574
+ nothing at or below it. Under the router's default boundary at the root,
575
+ nothing runs. A link from the page re-runs what is at or below the boundary.
572
576
  - Going Back or Forward out of an intercept does not abort an action sent from
573
577
  the intercept. `useAction()` and your `await` get its real result.
574
578
 
@@ -704,7 +708,17 @@ is usually too aggressive.
704
708
  Throw for genuinely exceptional conditions (network failure, DB outage,
705
709
  auth violation). The nearest `errorBoundary()` above the page the action was
706
710
  called from catches them, and the boundary renders inside route middleware like
707
- a normal render.
711
+ a normal render. Above the boundary the response is the action's revalidation,
712
+ as after a success: the layouts' loaders re-run as their `revalidate()`
713
+ decides, so a cart count shows what the action wrote before it threw. A
714
+ route's `errorBoundary()` replaces the route: its handler and loaders do not
715
+ run. A layout's `errorBoundary()` replaces what the layout renders in its
716
+ outlet: the layout's own loaders, slots and orphan layouts revalidate as on a
717
+ success, and nothing inside the outlet runs. A link from the page re-runs
718
+ what the boundary replaced and everything under it; under the router's
719
+ default boundary at the root, the whole page. Back to the page later
720
+ restores it and revalidates it, which replaces the boundary with the route. An action sent from an intercept
721
+ gets the boundary alone.
708
722
 
709
723
  ```typescript
710
724
  // urls.tsx — inside the urls() callback
@@ -66,9 +66,13 @@ type RenderRouteResult = RenderResult & {
66
66
  url: string,
67
67
  options?: {
68
68
  loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
69
+ handles?: ReadonlyArray<readonly [Handle<any, any>, unknown[]]>;
69
70
  state?: HistoryState;
70
71
  replace?: boolean;
71
72
  transition?: boolean;
73
+ // the response's stream: opens a task after the call, before the
74
+ // commit, and stays open until the promise settles (isStreaming)
75
+ streamUntil?: Promise<unknown>;
72
76
  },
73
77
  ): Promise<void>;
74
78
  // re-render the current location, as router.refresh() does:
@@ -203,13 +207,14 @@ it("lists no product twice while the next page loads", async () => {
203
207
  });
204
208
  ```
205
209
 
206
- | To bring state to a reader | Do |
207
- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
208
- | a push / replace, the component's own | click its `<Link state>` or the control that calls `router.push(url, { state })`, then `waitFor` |
209
- | a push / replace whose loader the test holds | `router.navigate(url, { state, replace?, loaders: [[Loader, pendingPromise]] })` on a `transition` spec |
210
- | back/forward onto an entry | `history.replaceState(entry, "")`, then `window.dispatchEvent(new Event("popstate"))` inside `act`. The tree has one location, so no page is restored: readers take the entry as history holds it (also how a `Def.write()` becomes visible) |
211
- | a reader that mounts while a navigation is pending | hold the navigation as in the row above, then mount the reader inside `await act(async () => { ... })`: it reads the entry on screen |
212
- | a server action's state, a redirect's, an intercept | e2e |
210
+ | To bring state to a reader | Do |
211
+ | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
212
+ | a push / replace, the component's own | click its `<Link state>` or the control that calls `router.push(url, { state })`, then `waitFor` |
213
+ | a push / replace whose loader the test holds | `router.navigate(url, { state, replace?, loaders: [[Loader, pendingPromise]] })` on a `transition` spec |
214
+ | back/forward onto the mounted entry or one a navigation wrote | `window.history.back()` inside `await act(async () => { ... })`: the entry's page is restored with its state and handle values, as production's history-cache restore is |
215
+ | back/forward onto an entry the test wrote | `history.replaceState(entry, "")`, then `window.dispatchEvent(new Event("popstate"))` inside `act`: readers take the entry as history holds it (also how a `Def.write()` becomes visible) |
216
+ | a reader that mounts while a navigation is pending | hold the navigation as in the row above, then mount the reader inside `await act(async () => { ... })`: it reads the entry on screen |
217
+ | a server action's state, a redirect's, an intercept | e2e |
213
218
 
214
219
  ### Definitions with `clearOnReload`
215
220
 
@@ -390,17 +395,19 @@ it("dims the held price while the next product streams", async () => {
390
395
  });
391
396
  ```
392
397
 
393
- | Pending state | Under `renderRoute` |
394
- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
395
- | `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit. |
396
- | `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit. |
397
- | navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag. |
398
- | `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate. |
399
- | Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e. |
400
- | `useNavigation().state`, `useLinkStatus().pending` | Modeled: navigate() starts the navigation as a click does. `loading` from the call, `idle` in the commit, held with it; `pending` true for the destination's link until the commit is handed to React. |
401
- | `useAction().state` | Stays `idle`: navigate() runs no action. e2e. |
402
- | `useHandle()` on a held navigation, `navigate(url, { handles, loaders })` | Modeled: `handles` arrive on the response's handle stream, so readers keep the page being left's values while React holds the page and take the new ones with its commit. |
403
- | A back/forward restored from the history cache (its view transition, `pop`) | NOT modeled: the tree has one location and navigate() caches no entries, so a `popstate` restores nothing. Assert it at e2e. |
398
+ | Pending state | Under `renderRoute` |
399
+ | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
400
+ | `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit. |
401
+ | `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit. |
402
+ | navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag. |
403
+ | `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate. |
404
+ | Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e. |
405
+ | `useNavigation().state`, `useLinkStatus().pending` | Modeled: navigate() starts the navigation as a click does. `loading` from the call, `idle` in the commit, held with it; `pending` true for the destination's link until that commit, held with it too. |
406
+ | `useAction().state` | Stays `idle`: navigate() runs no action. e2e. |
407
+ | `useHandle()` on a held navigation, `navigate(url, { handles, loaders })` | Modeled: `handles` arrive on the response's handle stream, so readers keep the page being left's values while React holds the page and take the new ones with its commit. |
408
+ | `useNavigation().isStreaming` past the commit, `navigate(url, { streamUntil })` | Modeled: the stream opens a task after the call (`pendingUrl` clears) and ends when the promise settles or a newer navigation aborts it; settle it inside `await act(async () => ...)`. |
409
+ | A navigation started while React holds a page (a newer click, a `transition: false` commit) | Call it inside `await act(async () => { ... })`: its first update makes React retry the held page, and a synchronous `act()` drops that retry, so a later urgent commit never renders. |
410
+ | A back/forward restored from the history cache (`pop`) | Modeled for the mounted entry and one a navigate() wrote: the page and its handle values come back, decided by `transition({ when })` as kind `"pop"`. A stale entry's revalidation and a restore that fetches: e2e. |
404
411
 
405
412
  - Per-navigation seeds merge over the render-time `loaders`/`loaderData` for THAT navigation only; a later `navigate()` without `loaders` falls back to the render-time data.
406
413
  - Pass settled data as a plain value, not `Promise.resolve(value)`: React has not observed a plain promise yet, so it counts as pending and renders one `stale` frame. (A Flight chunk that has already settled carries its status and is skipped, which a plain value mirrors.)
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Layer:** integration (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** response routes (json/text/html/xml/md), redirects, 404 (see `/response-routes`, `/mime-routes`)
4
4
 
5
- `dispatch` runs the router's REAL matching (reusing `previewMatch`) and the real global + route-level middleware chain, with no RSC render — so redirects, 404s, response routes, content negotiation, and middleware short-circuits behave exactly as in production. You SEED the request and `env`; everything else (matching, middleware, header/cookie merge) is real machinery.
5
+ `dispatch` runs the production handler's REAL request stages — classify (matching + content negotiation), the terminal plans (reloads, the rewritten-action rule), and the real global + route-level middleware chain — with no RSC render, so redirects, 404s, reloads, response routes, content negotiation, and middleware short-circuits behave exactly as in production. You SEED the request and `env`; everything else (matching, middleware, header/cookie merge) is real machinery.
6
6
 
7
7
  Matching goes through the route trie, as every request in `vite dev` and production does.
8
8
 
@@ -83,8 +83,9 @@ describe("api routes via dispatch", () => {
83
83
  - trailing-slash redirects from the trie — a route with `trailingSlash: "never"` or `"always"` answers the other form with a 308 and the `Location` (query preserved). A route with no `trailingSlash` serves its URL with and without a trailing slash, with no redirect, as production does;
84
84
  - the open-redirect guard on browser-followed redirects (a cross-origin `Location` is rewritten to the basename root unless `redirect(url, { external: true })`);
85
85
  - content-negotiated routes (`Vary: Accept` appended);
86
+ - the reloads production answers inside the global middleware chain: a request whose `_rsc_v` names another deploy's document version (`setBuildVersions`, `./serve-shell-request.md`), and a `_rsc_partial` request whose `_rsc_rid` names another router (a cross-app navigation), get an `X-RSC-Reload` at the visitor's URL — on any route, a component route or an unmatched path included;
86
87
  - cached response routes (`cache({ ... })` around a `path.json` etc.): hit, SWR, and tag writes. The cache write is scheduled via `waitUntil` (a microtask in tests), so flush microtasks between the seeding `dispatch` and the one that asserts the hit;
87
- - `createRouter({ telemetry })` `request.start` / `request.end` / `request.error` events, and `createRouter({ onError })` for cache-read/write failures.
88
+ - `createRouter({ telemetry })` `request.start` / `request.end` / `request.error` events, all with one `requestId`, which a response route's `request.timeout` carries too (a rewritten pass's included), and `createRouter({ onError })` for cache-read/write failures.
88
89
 
89
90
  ## Caveats
90
91
 
@@ -94,9 +95,10 @@ describe("api routes via dispatch", () => {
94
95
  - Setup: needs the preset (alias + virtual stubs) or a Vite-RSC env (see `./setup.md`); a bare router import throws on Vite virtuals.
95
96
  - Building the trie runs route discovery: every `include()`d `urls()` handler runs once more, and every async include provider (`() => import(...)`) is called. Count provider calls or handler runs after the first `dispatch` on that router, not from router construction. A provider that rejects fails the `dispatch`, as it fails `vite dev` and the build.
96
97
  - A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`). Importing the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules) — build from a focused include (your API routes) for whole-router dispatch.
97
- - A `throw rewrite(target)` from global or route middleware re-dispatches as the router does: global middleware matched on the target's path, the target's route middleware and handler, the same request context, `x-rango-rewrite` on the response, 508 on a loop. The visitor's URL is matched before middleware runs, so it must be a response route (or a redirect or 404) too; a rewrite target that is a component route throws the same error as a direct request.
98
+ - A `throw rewrite(target)` from global or route middleware re-dispatches as the router does: global middleware matched on the target's path, the target's route middleware and handler, the same request context, `x-rango-rewrite` on the response, 508 on a loop. The visitor's URL is matched before middleware runs, so it must be a response route (or a redirect or 404) too; a rewrite target that is a component route throws the same error as a direct request. An action request (`?_rsc_action`) a rewrite moves to a response route, a redirect or an unmatched path rejects with production's error, `A rewrite of a server action or form post must target a page route; …`.
99
+ - The route's params and name are set inside the global middleware chain, as production sets them: a global middleware reads `ctx.routeName` (and `getRequestContext().params`) after `await next()`, not before it. A trailing-slash redirect sets none.
98
100
  - A `_rsc_partial` request to a response route runs global middleware first (an auth gate can still 401/redirect), then returns `X-RSC-Reload` — route-level middleware is skipped, exactly like production.
99
- - `dispatch` does NOT execute server actions (`?_rsc_action`), but it DOES run the global middleware chain on an action request — middleware can still 401/redirect it, and any 3xx redirect on a partial OR action request becomes a `204` + `X-RSC-Redirect` (fetch-safe interception), the raw `Location` dropped. Location state carried on such a redirect is dropped too (production embeds it in a Flight payload); cover that at e2e.
101
+ - `dispatch` does NOT execute server actions (`?_rsc_action`), but it DOES run the global middleware chain on an action request — middleware can still 401/redirect it. When a global middleware matches the path, any 3xx that leaves the chain on a partial OR action request (a global or route middleware's redirect, a response route's, a trailing-slash redirect) becomes a `204` + `X-RSC-Redirect` (fetch-safe interception), the raw `Location` dropped. Without one, an action request's 3xx goes out raw (through the open-redirect guard), as production sends it; a trailing-slash redirect on a partial request is a `204` either way. Location state carried on such a redirect is dropped too (production embeds it in a Flight payload); cover that at e2e.
100
102
  - `createRouter({ onError })` is NOT invoked for a thrown non-Response HANDLER error here (the typed error Response still matches production), and `cache.decision` / `loader.*` telemetry never fires (no RSC render runs). Cover both at e2e.
101
103
 
102
104
  ## See also
@@ -20,13 +20,13 @@ A `Prerender` route is served as in production: from the artifact `router.matchF
20
20
 
21
21
  ### Options — `ServeShellRequestOptions`
22
22
 
23
- | Field | Type | Meaning |
24
- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
- | `cacheStore` | `SegmentCacheStore` | The store for this request, in place of the store your `createRouter({ cache })` config returns. The rest of that config (`enabled`, `searchParams`) still applies. Omit it to use the router's own store. |
26
- | `env` | `TEnv` | Bindings, as `router.fetch(request, { env })` passes them. A function `cache` config receives them. |
27
- | `headers` | `HeadersInit` | Request headers (e.g. `cookie`). A document request defaults `accept` to `text/html`. |
28
- | `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends it as a `<Link>` prefetch (`X-Rango-Prefetch`), so `prefetch: false` work is deferred. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. `interceptSource` sends the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends with the fill of an intercept, whose `from` is that source too. `rewriteTarget` sends what the page at `from` carried as its payload's `metadata.rewriteTarget` when middleware rewrote it (`_rsc_rt`), as the browser does, so the server diffs against the route that rendered that page. `stale: true` sends a stale request (`_rsc_stale`), as the browser sends it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`), segments take an action's revalidation defaults, and it renders live, as an action does: neither the route cache (`cache()`) nor the document cache serves or stores it, and a `ppr` route's `replayStatus` is `{ outcome: "BYPASS", reason: "stale" }`. |
29
- | `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }` | Send the request a JS server action call makes from the page at `url`: a POST of `args` (encoded as the browser encodes them) with `?_rsc_action`, the `rsc-action` header, `X-RSC-Router-Client-Path` and `_rsc_segments` (the page's mounted segment ids, default none), plus the page's `rewriteTarget` as for `partial`. Registers `ref` under `id` (`<module id>#<export name>`). Not with `partial` or `form`. |
23
+ | Field | Type | Meaning |
24
+ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `cacheStore` | `SegmentCacheStore` | The store for this request, in place of the store your `createRouter({ cache })` config returns. The rest of that config (`enabled`, `searchParams`) still applies. Omit it to use the router's own store. |
26
+ | `env` | `TEnv` | Bindings, as `router.fetch(request, { env })` passes them. A function `cache` config receives them. |
27
+ | `headers` | `HeadersInit` | Request headers (e.g. `cookie`). A document request defaults `accept` to `text/html`. |
28
+ | `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean; boundary?: string }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends it as a `<Link>` prefetch (`X-Rango-Prefetch`), so `prefetch: false` work is deferred. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. `interceptSource` sends the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends with the fill of an intercept, whose `from` is that source too. `rewriteTarget` sends what the page at `from` carried as its payload's `metadata.rewriteTarget` when middleware rewrote it (`_rsc_rt`), as the browser does, so the server diffs against the route that rendered that page. `stale: true` sends a stale request (`_rsc_stale`), as the browser sends it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`), segments take an action's revalidation defaults, and it renders live, as an action does: neither the route cache (`cache()`) nor the document cache serves or stores it, and a `ppr` route's `replayStatus` is `{ outcome: "BYPASS", reason: "stale" }`. `boundary` names the error segment of a thrown action's boundary the page at `from` shows (`_rsc_boundary`), as the background revalidation an earlier action owes that page sends it, with `stale: true` and no `segments`: the response renders every segment above the boundary and nothing at or below it, claims the boundary in `matched` and sends no segment for it. The server reads it only beside `stale`. |
29
+ | `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }` | Send the request a JS server action call makes from the page at `url`: a POST of `args` (encoded as the browser encodes them) with `?_rsc_action`, the `rsc-action` header, `X-RSC-Router-Client-Path` and `_rsc_segments` (the page's mounted segment ids, default none), plus the page's `rewriteTarget` as for `partial`. Registers `ref` under `id` (`<module id>#<export name>`). Not with `partial` or `form`. |
30
30
 
31
31
  ### Returns — `ServeShellRequestResult`
32
32
 
@@ -68,10 +68,17 @@ export async function ProductCard({ id }: { id: string }) {
68
68
  A result Flight cannot encode cleanly is not cached: an async child component
69
69
  that throws, a promise in the result or in a handle push that rejects, a
70
70
  function or a class instance. Storing it would bake the failure into every hit
71
- until expiry. Instead the write is skipped and reported to `onError` (phase
72
- `"cache"`, category `cache-write`, or `stale-revalidation` for a background
73
- refresh): a miss still returns the live result, the next call runs the
74
- function again, and a stale entry keeps serving.
71
+ until expiry. Instead the write is skipped, logged and reported to `onError`
72
+ (phase `"cache"`, category `cache-write`, or `stale-revalidation` for a
73
+ background refresh): the next call runs the function again, and a stale entry
74
+ keeps serving.
75
+
76
+ A miss renders a decoded copy of what it encodes, as a hit renders the stored
77
+ entry, so a child that throws or a promise that rejects throws where the copy
78
+ renders, as it would on a hit. A value Flight cannot encode at all (a
79
+ function, a class instance) in the data the function returns is the
80
+ exception: the call returns the live value, uncached. One inside a promise in
81
+ that data rejects that promise instead, as it would on a hit.
75
82
 
76
83
  ## Named Cache Profiles
77
84
 
@@ -233,6 +240,35 @@ export const ProductLoader = createLoader(async (ctx) => getProduct(ctx));
233
240
  per-request values (cookies, headers) are not reflected in the cache key. Without
234
241
  this guard, one user's data would be served to another.
235
242
 
243
+ That includes the server components the function returns. The value is
244
+ encoded once, inside the same guard, and the page renders the decoded copy, as
245
+ on a hit, so the guard refuses there every read and write it refuses in the
246
+ body, and a refused read fails the request as it would in the body: at the
247
+ value's root the call throws, and below the root that part renders as an
248
+ error, to its nearest error boundary. Nothing is stored. The same holds for a
249
+ `ctx.use(Loader)` in the returned tree (the loader runs inside the guard, and
250
+ the request's memo of it rejects, so a later read of it in the request throws
251
+ too) and for a `React.lazy` or a memoizing thenable created in the body.
252
+
253
+ A handle value the function pushes (`ctx.use(Handle)`) is encoded inside the
254
+ guard as well, and one that reads the request is not stored. One handle case
255
+ is not covered yet: a handle value that holds a lazy component or a memoized
256
+ promise. Don't push lazy state that reads the request from a cached function.
257
+
258
+ Two memo gaps remain, where the read happened before the guard could see it:
259
+
260
+ - In the body: a `React.cache`-memoized read of `cookies()` or `headers()` (a
261
+ `getSession = cache(() => cookies()...)` auth helper) that the page called
262
+ before the cached function runs returns the memo without reading again, so
263
+ the guard never sees it and the entry stores that visitor's value.
264
+ - In the body or the returned tree: a per-request memo of your own, keyed by
265
+ `getRequestContext()`, that the page filled and the cached function or a
266
+ component it returns reads. `React.cache` itself is safe in the returned
267
+ tree, which renders in its own Flight request.
268
+
269
+ Pass the value in as an argument instead of reading a memo inside the cached
270
+ function or the tree it returns.
271
+
236
272
  The raw reads throw the same way: `ctx.request.headers` on any `ctx` passed in
237
273
  (handler, loader, middleware; it is one `Request` object, also
238
274
  `getRequestContext().request`), and `getRequestContext().cookie()` /
@@ -397,9 +433,11 @@ export const Page = Prerender(
397
433
 
398
434
  ## Performance: waitUntil
399
435
 
400
- On cache miss, the function executes and the result is serialized inline (blocking).
401
- The cache **store write** (`setItem`) is deferred to `waitUntil` and does NOT block
402
- the response.
436
+ On cache miss, the function executes and its result is encoded; the call
437
+ returns once the decoded copy's root has arrived, so a server component at the
438
+ root of the value renders before the call resolves. The rest of the encode and
439
+ the cache **store write** (`setItem`) are deferred to `waitUntil` and do NOT
440
+ block the response.
403
441
 
404
442
  On stale hit, stale data is returned immediately. Background revalidation
405
443
  (re-execute + store write) runs entirely inside `waitUntil`.
@@ -407,7 +445,7 @@ On stale hit, stale data is returned immediately. Background revalidation
407
445
  | Phase | Blocks response? |
408
446
  | ------------------------------------ | ---------------- |
409
447
  | Function execution (miss) | Yes |
410
- | Result serialization (miss) | Yes |
448
+ | Result encode, to its root (miss) | Yes |
411
449
  | Cache store write (miss) | No (waitUntil) |
412
450
  | Stale value return (stale hit) | No (immediate) |
413
451
  | Background revalidation (stale) | No (waitUntil) |
@@ -154,7 +154,7 @@ Every commit the router runs in a transition adds its `transition({ when })` kin
154
154
  - Tagging alone animates nothing: React starts a view transition only where a `<ViewTransition>` resolves a class other than `"none"` for the commit's types.
155
155
  - A commit outside a transition carries no type: a cold click that mounts new segments, a gated-off commit (`when` returned `false`), and a back/forward restored from the history cache on a route without `transition()` (it commits inside the `popstate` event). Under `transition()` that restore carries `"pop"` and `"navigation-back"`.
156
156
  - Under `transition()`, a stale entry's revalidation carries `"revalidate"` and `"action"`. An `action`-keyed map that must skip it also keys `revalidate: "none"` (`"none"` wins over any other match).
157
- - The types reach the commit that brings the page. React keeps one queue of transition types per root, and the first transition commit takes the whole queue. No router hook update takes them first: `useHandle()` and `useSegments()` commit with the page, and a `useNavigation()` value that moves while neither the state nor the URL does (`pendingUrl` as the response starts streaming, `isStreaming` as it ends) commits with the page while the reader has a held value, urgently otherwise, so it starts no view transition of its own. A transition your own code starts while React holds a navigation's page can still take them. Handle values on a held page land in the commit right after the page, by design; see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design). Use a kind map to choose an animation, never for correctness.
157
+ - The types reach the commit that brings the page. React keeps one queue of transition types per root, and the first transition commit takes the whole queue. No router hook update takes them first: `useSegments()` commits with the page and `useHandle()` right after it, and a `useNavigation()` value that moves while neither the state nor the URL does (`pendingUrl` as the response starts streaming, `isStreaming` as it ends) commits with the page while the reader has a held value, urgently otherwise, so it starts no view transition of its own. `useAction()` values commit urgently. `useLinkStatus()` commits `pending` urgently and its end with the page, and a newer navigation or an abort ends it urgently, outside a held commit; neither starts a transition. A transition your own code starts while React holds a navigation's page can still take them. Handle values on a held page land in the commit right after the page, by design; see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design). Use a kind map to choose an animation, never for correctness.
158
158
 
159
159
  ### Back and Forward
160
160
 
package/src/__internal.ts CHANGED
@@ -85,10 +85,16 @@ export type {
85
85
  */
86
86
  export type {
87
87
  RouterContext,
88
- RevalidationContext,
89
88
  InterceptResult,
90
89
  } from "./router/router-context.js";
91
90
 
91
+ /**
92
+ * @internal
93
+ * The request side of a revalidating resolution pass (RouterContext's
94
+ * revalidating resolvers take it).
95
+ */
96
+ export type { ResolutionPassInput } from "./router/segment-resolution/resolution-pass.js";
97
+
92
98
  /**
93
99
  * @internal
94
100
  * Route match context during pipeline processing.
@@ -20,7 +20,8 @@
20
20
  *
21
21
  * The drop runs the batch's follow-up: when an action of the batch did not
22
22
  * land on the page on screen, the action bridge refetches that page once,
23
- * whole, after the navigation the user started, unless an intercept is on
23
+ * whole (or whole above an action's error boundary on screen, which it
24
+ * holds), after the navigation the user started, unless an intercept is on
24
25
  * screen (server-action-bridge.ts followUp; the rules are R1-R7 in
25
26
  * docs/internal/execution-model.md).
26
27
  *
@@ -17,6 +17,8 @@
17
17
  * state regardless of listener ordering.
18
18
  */
19
19
 
20
+ import { WIRE } from "../wire-params.js";
21
+
20
22
  const IDLE_TIMEOUT = 60_000;
21
23
  const DEBOUNCE_DELAY = 150;
22
24
 
@@ -61,7 +63,7 @@ export function startConnectionWarmup(
61
63
 
62
64
  function sendWarmup() {
63
65
  coldLatch = false;
64
- env.fetch("/?_rsc_warmup", { method: "HEAD" }).catch(() => {});
66
+ env.fetch(`/?${WIRE.warmup}`, { method: "HEAD" }).catch(() => {});
65
67
  }
66
68
 
67
69
  function triggerWarmup() {