@rangojs/router 0.26.6 → 0.28.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 (234) 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-coordinator.d.ts +0 -4
  4. package/dist/types/browser/action-fence.d.ts +11 -3
  5. package/dist/types/browser/event-controller.d.ts +20 -1
  6. package/dist/types/browser/history-state.d.ts +7 -9
  7. package/dist/types/browser/intercept-utils.d.ts +23 -0
  8. package/dist/types/browser/navigation-store.d.ts +8 -6
  9. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  10. package/dist/types/browser/rsc-router.d.ts +1 -1
  11. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  12. package/dist/types/browser/types.d.ts +2 -1
  13. package/dist/types/browser/wire-protocol.d.ts +25 -0
  14. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  15. package/dist/types/cache/cache-scope.d.ts +34 -22
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  18. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  19. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  20. package/dist/types/cache/segment-codec.d.ts +14 -0
  21. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  22. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  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 +4 -6
  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 +4 -4
  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 +12 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +35 -87
  38. package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
  39. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  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/revalidation.d.ts +6 -1
  44. package/dist/types/router/router-context.d.ts +9 -63
  45. package/dist/types/router/router-interfaces.d.ts +8 -14
  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 +93 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution.d.ts +1 -2
  52. package/dist/types/router/segment-wrappers.d.ts +11 -48
  53. package/dist/types/router/telemetry.d.ts +2 -0
  54. package/dist/types/router/types.d.ts +10 -0
  55. package/dist/types/rsc/full-payload.d.ts +2 -2
  56. package/dist/types/rsc/handler-context.d.ts +6 -6
  57. package/dist/types/rsc/json-route-result.d.ts +5 -9
  58. package/dist/types/rsc/request-stages.d.ts +203 -0
  59. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  60. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  61. package/dist/types/rsc/routine-plan.d.ts +1 -1
  62. package/dist/types/rsc/server-action.d.ts +2 -2
  63. package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture.d.ts +132 -15
  65. package/dist/types/rsc/shell-serve.d.ts +37 -12
  66. package/dist/types/server/context.d.ts +9 -8
  67. package/dist/types/server/handle-store.d.ts +16 -0
  68. package/dist/types/server/render-pass.d.ts +72 -0
  69. package/dist/types/server/request-context.d.ts +24 -104
  70. package/dist/types/server/rewrite-trail.d.ts +2 -2
  71. package/dist/types/testing/dispatch.d.ts +35 -18
  72. package/dist/types/testing/serve-shell-request.d.ts +7 -0
  73. package/dist/types/types/cache-types.d.ts +6 -3
  74. package/dist/types/types/error-types.d.ts +9 -1
  75. package/dist/types/types/handler-context.d.ts +18 -7
  76. package/dist/types/types/segments.d.ts +1 -1
  77. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  78. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  79. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  80. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  81. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  82. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  83. package/dist/types/vite/discovery/state.d.ts +12 -0
  84. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  85. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  86. package/dist/types/vite/router-discovery.d.ts +8 -47
  87. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  88. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  89. package/dist/types/wire-params.d.ts +34 -0
  90. package/dist/vite/index.js +3809 -3710
  91. package/package.json +1 -1
  92. package/skills/cache-guide/SKILL.md +8 -5
  93. package/skills/caching/SKILL.md +37 -18
  94. package/skills/client-urls/SKILL.md +2 -1
  95. package/skills/document-cache/SKILL.md +18 -1
  96. package/skills/handler-use/SKILL.md +1 -1
  97. package/skills/intercept/SKILL.md +53 -13
  98. package/skills/layout/SKILL.md +11 -7
  99. package/skills/loader/SKILL.md +30 -13
  100. package/skills/middleware/SKILL.md +19 -1
  101. package/skills/migrate-nextjs/SKILL.md +5 -4
  102. package/skills/parallel/SKILL.md +5 -4
  103. package/skills/ppr/SKILL.md +19 -3
  104. package/skills/prerender/SKILL.md +39 -15
  105. package/skills/rango/SKILL.md +23 -11
  106. package/skills/server-actions/SKILL.md +51 -6
  107. package/skills/testing/predicates.md +1 -1
  108. package/skills/testing/response-routes.md +6 -4
  109. package/skills/testing/serve-shell-request.md +7 -7
  110. package/skills/use-cache/SKILL.md +46 -8
  111. package/skills/ux-architecture/SKILL.md +3 -2
  112. package/src/__internal.ts +7 -1
  113. package/src/browser/action-coordinator.ts +2 -13
  114. package/src/browser/action-fence.ts +11 -3
  115. package/src/browser/connection-warmup.ts +3 -1
  116. package/src/browser/event-controller.ts +42 -3
  117. package/src/browser/history-state.ts +15 -13
  118. package/src/browser/intercept-utils.ts +51 -0
  119. package/src/browser/navigation-bridge.ts +34 -30
  120. package/src/browser/navigation-client.ts +21 -33
  121. package/src/browser/navigation-store.ts +25 -27
  122. package/src/browser/navigation-transaction.ts +7 -11
  123. package/src/browser/partial-update.ts +8 -0
  124. package/src/browser/prefetch/cache.ts +2 -1
  125. package/src/browser/prefetch/fetch.ts +8 -16
  126. package/src/browser/prefetch/queue.ts +4 -4
  127. package/src/browser/react/NavigationProvider.tsx +2 -1
  128. package/src/browser/react/filter-segment-order.ts +2 -2
  129. package/src/browser/rsc-router.tsx +17 -2
  130. package/src/browser/segment-reconciler.ts +66 -53
  131. package/src/browser/server-action-bridge.ts +265 -127
  132. package/src/browser/types.ts +2 -1
  133. package/src/browser/wire-protocol.ts +41 -0
  134. package/src/cache/cache-key-utils.ts +11 -5
  135. package/src/cache/cache-runtime.ts +110 -50
  136. package/src/cache/cache-scope.ts +110 -64
  137. package/src/cache/cf/cf-cache-constants.ts +2 -2
  138. package/src/cache/cf/cf-cache-store.ts +486 -874
  139. package/src/cache/cf/cf-kv-utils.ts +1 -1
  140. package/src/cache/document-cache.ts +42 -15
  141. package/src/cache/handle-snapshot.ts +19 -7
  142. package/src/cache/segment-codec.ts +55 -0
  143. package/src/cache/shell-snapshot.ts +2 -2
  144. package/src/cache/tag-invalidation.ts +21 -2
  145. package/src/cache/taint.ts +5 -3
  146. package/src/cache/types.ts +7 -3
  147. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  148. package/src/handles/pushed-by.ts +1 -1
  149. package/src/prefetch-markers.ts +4 -7
  150. package/src/prerender/store.ts +76 -0
  151. package/src/prerender/warm-request.ts +8 -4
  152. package/src/prerender.ts +2 -2
  153. package/src/route-name.ts +8 -0
  154. package/src/router/error-handling.ts +4 -4
  155. package/src/router/instrument.ts +4 -3
  156. package/src/router/intercept-resolution.ts +77 -75
  157. package/src/router/loader-resolution.ts +5 -4
  158. package/src/router/match-api.ts +4 -5
  159. package/src/router/match-context.ts +33 -12
  160. package/src/router/match-handlers.ts +8 -20
  161. package/src/router/match-middleware/background-revalidation.ts +41 -53
  162. package/src/router/match-middleware/cache-lookup.ts +680 -720
  163. package/src/router/match-middleware/cache-store.ts +14 -12
  164. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  165. package/src/router/match-middleware/segment-resolution.ts +34 -61
  166. package/src/router/match-result.ts +1 -20
  167. package/src/router/middleware.ts +69 -11
  168. package/src/router/navigation-snapshot.ts +4 -3
  169. package/src/router/prerender-match.ts +100 -44
  170. package/src/router/preview-match.ts +6 -41
  171. package/src/router/request-classification.ts +13 -7
  172. package/src/router/revalidation.ts +33 -7
  173. package/src/router/route-snapshot.ts +2 -5
  174. package/src/router/router-context.ts +14 -117
  175. package/src/router/router-interfaces.ts +8 -14
  176. package/src/router/segment-resolution/loader-cache.ts +14 -9
  177. package/src/router/segment-resolution/loader-mask.ts +5 -5
  178. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  179. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  180. package/src/router/segment-resolution/revalidation.ts +897 -776
  181. package/src/router/segment-resolution.ts +4 -13
  182. package/src/router/segment-wrappers.ts +51 -269
  183. package/src/router/telemetry.ts +5 -0
  184. package/src/router/types.ts +10 -0
  185. package/src/router.ts +12 -13
  186. package/src/rsc/full-payload.ts +6 -5
  187. package/src/rsc/handler-context.ts +8 -14
  188. package/src/rsc/handler.ts +126 -507
  189. package/src/rsc/json-route-result.ts +5 -9
  190. package/src/rsc/loader-fetch.ts +6 -5
  191. package/src/rsc/progressive-enhancement.ts +5 -0
  192. package/src/rsc/request-stages.ts +757 -0
  193. package/src/rsc/response-cache-serve.ts +10 -14
  194. package/src/rsc/response-route-handler.ts +13 -5
  195. package/src/rsc/rewrite-passes.ts +2 -1
  196. package/src/rsc/routine-plan.ts +1 -1
  197. package/src/rsc/rsc-rendering.ts +172 -349
  198. package/src/rsc/server-action.ts +87 -113
  199. package/src/rsc/shell-capture-constants.ts +3 -1
  200. package/src/rsc/shell-capture.ts +212 -169
  201. package/src/rsc/shell-serve.ts +121 -18
  202. package/src/rsc/ssr-setup.ts +5 -4
  203. package/src/segment-system.tsx +3 -3
  204. package/src/server/context.ts +22 -25
  205. package/src/server/handle-store.ts +55 -0
  206. package/src/server/render-pass.ts +291 -0
  207. package/src/server/request-context.ts +26 -115
  208. package/src/server/rewrite-trail.ts +6 -4
  209. package/src/ssr/index.tsx +1 -1
  210. package/src/ssr/ssr-root.tsx +1 -0
  211. package/src/testing/dispatch.ts +223 -710
  212. package/src/testing/e2e/page-helpers.ts +2 -2
  213. package/src/testing/serve-shell-request.ts +37 -32
  214. package/src/types/cache-types.ts +6 -3
  215. package/src/types/error-types.ts +9 -1
  216. package/src/types/handler-context.ts +21 -7
  217. package/src/types/segments.ts +1 -1
  218. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  219. package/src/vite/discovery/build-env.ts +148 -0
  220. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  221. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  222. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  223. package/src/vite/discovery/discover-routers.ts +1 -1
  224. package/src/vite/discovery/discovery-errors.ts +1 -1
  225. package/src/vite/discovery/prerender-collection.ts +24 -39
  226. package/src/vite/discovery/state.ts +15 -0
  227. package/src/vite/plugins/version-plugin.ts +1 -1
  228. package/src/vite/plugins/virtual-entries.ts +1 -1
  229. package/src/vite/router-discovery.ts +29 -2428
  230. package/src/vite/temp-rsc-server.ts +223 -0
  231. package/src/vite/utils/prerender-utils.ts +47 -0
  232. package/src/wire-params.ts +35 -0
  233. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  234. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -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
 
@@ -87,10 +87,12 @@ are immune to partial-revalidation staleness by construction.
87
87
  the entry's children too — a hard `false` on one side of a
88
88
  producer/consumer pair desyncs it.
89
89
  4. **Cross-entry sharing** — an outer `layout()` entry feeding descendants.
90
- Outer entries do NOT revalidate on actions by default (the revalidation
91
- trace calls this `action:parent-chain-skip`), so this rung always requires
92
- a shared revalidation contract: the same named `revalidate()` function on
93
- the producer and every consumer. See `/layout` → "Revalidation Contracts".
90
+ Outer entries do NOT revalidate on actions, or on Back/Forward onto a page
91
+ an action made stale, by default (the revalidation trace calls this
92
+ `action:parent-chain-skip` / `stale:parent-chain-skip`), so this rung always
93
+ requires a shared revalidation contract: the same named `revalidate()`
94
+ function on the producer and every consumer, returning `true` on `stale`
95
+ as well as on its actions. See `/layout` → "Revalidation Contracts".
94
96
  Before writing one, check whether the producer can move down a rung.
95
97
 
96
98
  The failure mode this ladder prevents: a consumer re-runs, its producer
@@ -247,9 +249,15 @@ identity".
247
249
  The predicate arg carries the action's full context, not just its identity. Match
248
250
  _which_ action with `ctx.isAction(addToCart)` (rename-safe); branch on _what it
249
251
  returned_ with `ctx.actionResult` — the value your `"use server"` function
250
- 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`
251
257
  (raw `path#export`), `actionUrl`, `formData` (form-based actions only), `method`,
252
- and `stale` (the `_rsc_stale` signal that an action ran in this or another tab).
258
+ and `stale` (the `_rsc_stale` signal that an action ran in this or another tab;
259
+ that Back/Forward revalidation takes an action's defaults, but `isAction()` is
260
+ false on it).
253
261
  `actionId`, `actionUrl`, `actionResult`, and `formData` are `undefined` on plain
254
262
  navigation; `method` is `"GET"` there and `"POST"` for an action. Call
255
263
  `ctx.isAction()` with no arguments to ask "was this any action?".
@@ -258,9 +266,10 @@ navigation; `method` is `"GET"` there and `"POST"` for an action. Call
258
266
  default already does: after an action, loaders, the route, and segments inside
259
267
  the `path()` re-run; layouts and parallels above the route are skipped.
260
268
 
261
- - **Add a signal** where the default skips: `ctx.isAction(CartActions) || undefined`
262
- on a parent layout re-renders it after cart actions and defers otherwise. On a
263
- loader or route segment it changes nothing.
269
+ - **Add a signal** where the default skips:
270
+ `ctx.isAction(CartActions) || ctx.stale || undefined` on a parent layout
271
+ re-renders it after cart actions and on Back after an action, and defers
272
+ otherwise. On a loader or route segment it changes nothing.
264
273
  - **Narrow after actions** where the default re-runs:
265
274
  `ctx.isAction() ? ctx.isAction(CartActions) : undefined` re-runs only for cart
266
275
  actions and keeps the navigation default (params or search changed).
@@ -270,8 +279,11 @@ the `path()` re-run; layouts and parallels above the route are skipped.
270
279
 
271
280
  ```ts
272
281
  // on a parent layout (skipped after actions by default): also re-render when
273
- // checkout succeeded; defer to the default otherwise
274
- revalidate((ctx) => (ctx.isAction(checkout) && ctx.actionResult?.ok) || undefined),
282
+ // checkout succeeded, and on Back after an action; defer to the default otherwise
283
+ revalidate(
284
+ (ctx) =>
285
+ (ctx.isAction(checkout) && ctx.actionResult?.ok) || ctx.stale || undefined,
286
+ ),
275
287
  ```
276
288
 
277
289
  The same `actionResult` idiom works in a `clientUrls()` group's loader
@@ -428,8 +428,9 @@ several actions, or a namespace import (`import * as CartActions`). Wrap it by
428
428
  what the segment's default already does:
429
429
 
430
430
  - **Add a signal** where the default skips (a parent layout or layout-level
431
- parallel): `ctx.isAction(CartActions) || undefined` re-renders it after cart
432
- actions and defers otherwise. On a loader or route segment it changes nothing.
431
+ parallel): `ctx.isAction(CartActions) || ctx.stale || undefined` re-renders it
432
+ after cart actions and on Back after an action, and defers otherwise. On a
433
+ loader or route segment it changes nothing.
433
434
  - **Narrow after actions** where the default re-runs (loaders, the route, and
434
435
  segments inside the `path()`):
435
436
  `ctx.isAction() ? ctx.isAction(CartActions) : undefined` re-runs only for cart
@@ -508,8 +509,10 @@ consumer:
508
509
  import type { Revalidate } from "@rangojs/router";
509
510
  import * as CartActions from "./actions/cart";
510
511
 
512
+ // True on a stale Back/Forward too: it re-runs CartPage, and a producer that
513
+ // sits it out leaves the page reading undefined.
511
514
  const revalidateCart: Revalidate = (ctx) =>
512
- ctx.isAction(CartActions) || undefined;
515
+ ctx.stale || ctx.isAction(CartActions) || undefined;
513
516
 
514
517
  urls(({ path, layout, loader, revalidate }) => [
515
518
  layout(CartLayout, () => [
@@ -534,6 +537,41 @@ revalidates), flushes the prefetch cache, rotates the router's state cookie,
534
537
  and tells other open tabs to do the same. This happens for every action that
535
538
  reached the server, including ones that threw. No setup is needed.
536
539
 
540
+ ### When the user navigates while the action runs
541
+
542
+ Users rarely wait for an action. They click "Add to cart" and then a link, or
543
+ go Back. That navigation is an ordinary one: a prefetched link shows its page at
544
+ once, as it would with no action in flight. That page may show data from before
545
+ the write, such as a layout's cart count the navigation kept. So once the last
546
+ action in flight settles, the router revalidates the page on screen once, in
547
+ the background: every loader on it re-runs, and the result commits with no
548
+ `loading()` fallback and no remount. `useNavigation().state` stays `"idle"`. This covers an action that failed too.
549
+
550
+ ```text
551
+ /product/1: click "Add to cart", then a link to /checkout
552
+ /checkout shows at once (layout: cart:0)
553
+ the action settles
554
+ one background revalidation (layout: cart:1)
555
+ ```
556
+
557
+ A few details:
558
+
559
+ - If the user stays on the page, nothing extra is sent. The action's own
560
+ response already re-ran that page's loaders.
561
+ - The revalidation waits for a navigation the user started and never cancels
562
+ it. Several actions in flight share one revalidation.
563
+ - A `redirect()` from the action replaces it: the redirect target renders
564
+ fresh.
565
+ - If an `intercept()` is on screen, nothing runs. Leaving the intercept with
566
+ Back restores the page underneath and revalidates it as stale (the
567
+ action marked the history cache stale). Only loaders that opt in with
568
+ `revalidate(({ stale }) => stale)` re-run there; the others keep their data
569
+ until a request re-runs them.
570
+ - It holds no segments, so every loader on the page re-runs whatever its
571
+ `revalidate()` says, and the document cache never answers it.
572
+ - Going Back or Forward out of an intercept does not abort an action sent from
573
+ the intercept. `useAction()` and your `await` get its real result.
574
+
537
575
  ### `keepClientCache()` — skip it for no-op actions
538
576
 
539
577
  Some hot actions change nothing a route renders: a debounced draft autosave, an
@@ -555,8 +593,8 @@ export async function saveDraft(formData: FormData) {
555
593
 
556
594
  What it suppresses, for this action only: the stale-marking of the history
557
595
  cache, the prefetch flush, the state-cookie rotation, the cross-tab broadcast,
558
- and the background re-fetch the client runs when the user navigated away while
559
- the action was in flight. What it does not change:
596
+ and the background revalidation the client runs when the user navigated while
597
+ the action was in flight (above). What it does not change:
560
598
 
561
599
  - The action's own response still carries the revalidation render your
562
600
  `revalidate()` rules select, and the client applies it.
@@ -666,7 +704,14 @@ is usually too aggressive.
666
704
  Throw for genuinely exceptional conditions (network failure, DB outage,
667
705
  auth violation). The nearest `errorBoundary()` above the page the action was
668
706
  called from catches them, and the boundary renders inside route middleware like
669
- a normal render.
707
+ a normal render. Above the boundary the response is the action's revalidation,
708
+ as after a success: the layouts' loaders re-run as their `revalidate()`
709
+ decides, so a cart count shows what the action wrote before it threw. A
710
+ route's `errorBoundary()` replaces the route: its handler and loaders do not
711
+ run. A layout's `errorBoundary()` replaces what the layout renders in its
712
+ outlet: the layout's own loaders, slots and orphan layouts revalidate as on a
713
+ success, and nothing inside the outlet runs. An action sent from an intercept
714
+ gets the boundary alone.
670
715
 
671
716
  ```typescript
672
717
  // urls.tsx — inside the urls() callback
@@ -80,7 +80,7 @@ Defaults model a same-URL navigation with no action, for a route-owned loader: a
80
80
  | `method` | `string` | Overrides the request method (`POST` with an `action`, otherwise `GET`). |
81
81
  | `segmentType` | `"layout" \| "route" \| "parallel" \| "loader"` | The segment type the predicate is attached to. Defaults to `"loader"`. |
82
82
  | `belongsToRoute` | `boolean` | Defaults to `true`; `false` models a layout's loader, which a navigation keeps by default. |
83
- | `stale` | `boolean` | The `stale` arg (a history-entry background revalidation). Defaults to `false`. |
83
+ | `stale` | `boolean` | The `stale` arg (a history-entry background revalidation), which takes an action's defaults. Defaults to `false`. |
84
84
  | `routeName` | `string` | `toRouteName` (and `routeName`). |
85
85
  | `fromRouteName` | `string` | `fromRouteName`. |
86
86
  | `env` | `TEnv` | Environment bindings of the seeded context. |
@@ -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 }` | 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. |
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 }` | 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`. |
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) |
@@ -228,8 +228,9 @@ them right:
228
228
 
229
229
  - A boolean is a hard answer, `undefined` defers to the default. Pick by the
230
230
  default you are changing: the ternary narrows a loader that re-runs after
231
- every action; `isAction(X) || undefined` adds a re-run where the default
232
- skips (a server layout's own handler).
231
+ every action; `isAction(X) || stale || undefined` adds a re-run, after `X`
232
+ and on Back after an action, where the default skips (a server layout's own
233
+ handler).
233
234
  - `revalidate(true)` and `revalidate(false)` are shorthands for `() => true`
234
235
  and `() => false`.
235
236
  - Never `revalidate(false)` on data keyed by a param: product to product then
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.
@@ -6,11 +6,7 @@ import type { ActionEntry } from "./event-controller.js";
6
6
  * reconciliation; this only covers successfully-reconciled partial responses.
7
7
  */
8
8
  type ActionScenario =
9
- | {
10
- type: "navigated-away";
11
- historyKeyChanged: boolean;
12
- onInterceptRoute: boolean;
13
- }
9
+ | { type: "navigated-away" }
14
10
  | { type: "hmr-missing" }
15
11
  | { type: "consolidation-needed"; segmentIds: string[] }
16
12
  | { type: "concurrent-skip"; otherFetchingCount: number }
@@ -41,8 +37,6 @@ export interface ActionOutcomeInput {
41
37
  reconciledSegmentCount: number;
42
38
  /** Number of matched segment IDs from server */
43
39
  matchedCount: number;
44
- /** Current intercept source URL (null when not on intercept route) */
45
- currentInterceptSource: string | null;
46
40
  }
47
41
 
48
42
  // Segment IDs to re-fetch when concurrent actions each revalidated different
@@ -85,12 +79,7 @@ export function classifyActionOutcome(
85
79
  input.currentPathname !== input.actionStartPathname ||
86
80
  input.currentLocationKey !== input.actionStartLocationKey
87
81
  ) {
88
- return {
89
- type: "navigated-away",
90
- historyKeyChanged:
91
- input.currentLocationKey !== input.actionStartLocationKey,
92
- onInterceptRoute: input.currentInterceptSource !== null,
93
- };
82
+ return { type: "navigated-away" };
94
83
  }
95
84
 
96
85
  if (input.reconciledSegmentCount < input.matchedCount) {
@@ -6,9 +6,11 @@
6
6
  * start. While the fence is up, the decision of whether the action invalidated
7
7
  * anything is deferred to the response:
8
8
  *
9
- * - prefetch consumption is suspended (a queued prefetch result is not served),
10
- * - a genuine navigation during the flight fetches with `cache: "no-store"` so
11
- * it cannot be served stale bytes from the browser's Vary-keyed HTTP cache,
9
+ * - a navigation during the flight is an ordinary one: a prefetch is served,
10
+ * and the page it commits may hold pre-write data until the follow-up below,
11
+ * - a navigation without a prefetch, and a prefetch, fetch with
12
+ * `cache: "no-store"` so they cannot be served stale bytes from the
13
+ * browser's Vary-keyed HTTP cache,
12
14
  * - popstate reads are treated as stale-while-revalidate.
13
15
  *
14
16
  * Nothing is wiped, rotated, or broadcast while the fence is up — that is what
@@ -16,6 +18,12 @@
16
18
  * actions compose: each action raises and lowers its own reference, and the
17
19
  * fence is down only when the count reaches zero.
18
20
  *
21
+ * The drop runs the batch's follow-up: when an action of the batch did not
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
24
+ * screen (server-action-bridge.ts followUp; the rules are R1-R7 in
25
+ * docs/internal/execution-model.md).
26
+ *
19
27
  * The refcount is a single module-level counter, not keyed by routerId.
20
28
  * Consumers (navigation-client.ts, prefetch/fetch.ts, navigation-bridge.ts)
21
29
  * read it unscoped, so an action in one router would suppress another router's
@@ -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() {