@rangojs/router 0.27.0 → 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 (212) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/event-controller.d.ts +2 -1
  4. package/dist/types/browser/history-state.d.ts +3 -9
  5. package/dist/types/browser/navigation-store.d.ts +8 -6
  6. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  7. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  8. package/dist/types/browser/types.d.ts +2 -1
  9. package/dist/types/browser/wire-protocol.d.ts +25 -0
  10. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  11. package/dist/types/cache/cache-scope.d.ts +34 -22
  12. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  13. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  14. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  15. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  16. package/dist/types/cache/segment-codec.d.ts +14 -0
  17. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  18. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  19. package/dist/types/cache/taint.d.ts +5 -3
  20. package/dist/types/cache/types.d.ts +7 -3
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  22. package/dist/types/prefetch-markers.d.ts +5 -14
  23. package/dist/types/prerender/store.d.ts +31 -0
  24. package/dist/types/prerender/warm-request.d.ts +3 -3
  25. package/dist/types/prerender.d.ts +2 -2
  26. package/dist/types/route-name.d.ts +5 -0
  27. package/dist/types/router/error-handling.d.ts +4 -4
  28. package/dist/types/router/instrument.d.ts +4 -3
  29. package/dist/types/router/intercept-resolution.d.ts +25 -9
  30. package/dist/types/router/match-context.d.ts +4 -11
  31. package/dist/types/router/match-handlers.d.ts +2 -6
  32. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  33. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  34. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  35. package/dist/types/router/prerender-match.d.ts +21 -6
  36. package/dist/types/router/preview-match.d.ts +2 -7
  37. package/dist/types/router/request-classification.d.ts +5 -0
  38. package/dist/types/router/router-context.d.ts +7 -46
  39. package/dist/types/router/router-interfaces.d.ts +8 -14
  40. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  41. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  42. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  43. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  44. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  45. package/dist/types/router/segment-resolution.d.ts +1 -2
  46. package/dist/types/router/segment-wrappers.d.ts +11 -48
  47. package/dist/types/router/telemetry.d.ts +2 -0
  48. package/dist/types/router/types.d.ts +10 -0
  49. package/dist/types/rsc/full-payload.d.ts +2 -2
  50. package/dist/types/rsc/handler-context.d.ts +6 -6
  51. package/dist/types/rsc/json-route-result.d.ts +5 -9
  52. package/dist/types/rsc/request-stages.d.ts +203 -0
  53. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  54. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  55. package/dist/types/rsc/routine-plan.d.ts +1 -1
  56. package/dist/types/rsc/server-action.d.ts +2 -2
  57. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  58. package/dist/types/rsc/shell-capture.d.ts +132 -15
  59. package/dist/types/rsc/shell-serve.d.ts +37 -12
  60. package/dist/types/server/context.d.ts +9 -8
  61. package/dist/types/server/handle-store.d.ts +16 -0
  62. package/dist/types/server/render-pass.d.ts +72 -0
  63. package/dist/types/server/request-context.d.ts +24 -104
  64. package/dist/types/server/rewrite-trail.d.ts +2 -2
  65. package/dist/types/testing/dispatch.d.ts +35 -18
  66. package/dist/types/types/cache-types.d.ts +6 -3
  67. package/dist/types/types/error-types.d.ts +9 -1
  68. package/dist/types/types/handler-context.d.ts +9 -1
  69. package/dist/types/types/segments.d.ts +1 -1
  70. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  71. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  72. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  73. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  74. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  75. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  76. package/dist/types/vite/discovery/state.d.ts +12 -0
  77. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  78. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  79. package/dist/types/vite/router-discovery.d.ts +8 -47
  80. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  81. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  82. package/dist/types/wire-params.d.ts +34 -0
  83. package/dist/vite/index.js +3809 -3710
  84. package/package.json +1 -1
  85. package/skills/cache-guide/SKILL.md +8 -5
  86. package/skills/caching/SKILL.md +37 -18
  87. package/skills/document-cache/SKILL.md +13 -1
  88. package/skills/handler-use/SKILL.md +1 -1
  89. package/skills/intercept/SKILL.md +17 -7
  90. package/skills/middleware/SKILL.md +18 -0
  91. package/skills/ppr/SKILL.md +18 -3
  92. package/skills/prerender/SKILL.md +39 -15
  93. package/skills/rango/SKILL.md +5 -1
  94. package/skills/server-actions/SKILL.md +8 -1
  95. package/skills/testing/response-routes.md +6 -4
  96. package/skills/use-cache/SKILL.md +46 -8
  97. package/src/__internal.ts +7 -1
  98. package/src/browser/connection-warmup.ts +3 -1
  99. package/src/browser/event-controller.ts +5 -3
  100. package/src/browser/history-state.ts +6 -12
  101. package/src/browser/navigation-bridge.ts +14 -13
  102. package/src/browser/navigation-client.ts +18 -25
  103. package/src/browser/navigation-store.ts +25 -27
  104. package/src/browser/navigation-transaction.ts +4 -1
  105. package/src/browser/prefetch/cache.ts +2 -1
  106. package/src/browser/prefetch/fetch.ts +8 -16
  107. package/src/browser/prefetch/queue.ts +4 -4
  108. package/src/browser/react/NavigationProvider.tsx +2 -1
  109. package/src/browser/react/filter-segment-order.ts +2 -2
  110. package/src/browser/rsc-router.tsx +1 -0
  111. package/src/browser/segment-reconciler.ts +66 -53
  112. package/src/browser/server-action-bridge.ts +50 -57
  113. package/src/browser/types.ts +2 -1
  114. package/src/browser/wire-protocol.ts +41 -0
  115. package/src/cache/cache-key-utils.ts +11 -5
  116. package/src/cache/cache-runtime.ts +110 -50
  117. package/src/cache/cache-scope.ts +110 -64
  118. package/src/cache/cf/cf-cache-constants.ts +2 -2
  119. package/src/cache/cf/cf-cache-store.ts +486 -874
  120. package/src/cache/cf/cf-kv-utils.ts +1 -1
  121. package/src/cache/document-cache.ts +34 -13
  122. package/src/cache/handle-snapshot.ts +19 -7
  123. package/src/cache/segment-codec.ts +55 -0
  124. package/src/cache/shell-snapshot.ts +2 -2
  125. package/src/cache/tag-invalidation.ts +21 -2
  126. package/src/cache/taint.ts +5 -3
  127. package/src/cache/types.ts +7 -3
  128. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  129. package/src/handles/pushed-by.ts +1 -1
  130. package/src/prefetch-markers.ts +5 -16
  131. package/src/prerender/store.ts +76 -0
  132. package/src/prerender/warm-request.ts +8 -4
  133. package/src/prerender.ts +2 -2
  134. package/src/route-name.ts +8 -0
  135. package/src/router/error-handling.ts +4 -4
  136. package/src/router/instrument.ts +4 -3
  137. package/src/router/intercept-resolution.ts +77 -75
  138. package/src/router/loader-resolution.ts +5 -4
  139. package/src/router/match-api.ts +4 -5
  140. package/src/router/match-context.ts +20 -12
  141. package/src/router/match-handlers.ts +8 -20
  142. package/src/router/match-middleware/background-revalidation.ts +41 -53
  143. package/src/router/match-middleware/cache-lookup.ts +675 -726
  144. package/src/router/match-middleware/cache-store.ts +4 -8
  145. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  146. package/src/router/match-middleware/segment-resolution.ts +34 -61
  147. package/src/router/middleware.ts +69 -11
  148. package/src/router/navigation-snapshot.ts +4 -4
  149. package/src/router/prerender-match.ts +100 -44
  150. package/src/router/preview-match.ts +6 -41
  151. package/src/router/request-classification.ts +13 -7
  152. package/src/router/route-snapshot.ts +2 -5
  153. package/src/router/router-context.ts +10 -95
  154. package/src/router/router-interfaces.ts +8 -14
  155. package/src/router/segment-resolution/loader-cache.ts +14 -9
  156. package/src/router/segment-resolution/loader-mask.ts +5 -5
  157. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  158. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  159. package/src/router/segment-resolution/revalidation.ts +897 -776
  160. package/src/router/segment-resolution.ts +4 -13
  161. package/src/router/segment-wrappers.ts +51 -269
  162. package/src/router/telemetry.ts +5 -0
  163. package/src/router/types.ts +10 -0
  164. package/src/router.ts +12 -13
  165. package/src/rsc/full-payload.ts +6 -5
  166. package/src/rsc/handler-context.ts +8 -14
  167. package/src/rsc/handler.ts +126 -507
  168. package/src/rsc/json-route-result.ts +5 -9
  169. package/src/rsc/loader-fetch.ts +6 -5
  170. package/src/rsc/progressive-enhancement.ts +5 -0
  171. package/src/rsc/request-stages.ts +757 -0
  172. package/src/rsc/response-cache-serve.ts +10 -14
  173. package/src/rsc/response-route-handler.ts +13 -5
  174. package/src/rsc/rewrite-passes.ts +2 -1
  175. package/src/rsc/routine-plan.ts +1 -1
  176. package/src/rsc/rsc-rendering.ts +164 -349
  177. package/src/rsc/server-action.ts +87 -113
  178. package/src/rsc/shell-capture-constants.ts +1 -1
  179. package/src/rsc/shell-capture.ts +212 -169
  180. package/src/rsc/shell-serve.ts +121 -18
  181. package/src/rsc/ssr-setup.ts +5 -4
  182. package/src/segment-system.tsx +3 -3
  183. package/src/server/context.ts +22 -25
  184. package/src/server/handle-store.ts +55 -0
  185. package/src/server/render-pass.ts +291 -0
  186. package/src/server/request-context.ts +26 -115
  187. package/src/server/rewrite-trail.ts +6 -4
  188. package/src/ssr/index.tsx +1 -1
  189. package/src/testing/dispatch.ts +223 -710
  190. package/src/testing/e2e/page-helpers.ts +2 -2
  191. package/src/testing/serve-shell-request.ts +23 -36
  192. package/src/types/cache-types.ts +6 -3
  193. package/src/types/error-types.ts +9 -1
  194. package/src/types/handler-context.ts +12 -1
  195. package/src/types/segments.ts +1 -1
  196. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  197. package/src/vite/discovery/build-env.ts +148 -0
  198. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  199. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  200. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  201. package/src/vite/discovery/discover-routers.ts +1 -1
  202. package/src/vite/discovery/discovery-errors.ts +1 -1
  203. package/src/vite/discovery/prerender-collection.ts +24 -39
  204. package/src/vite/discovery/state.ts +15 -0
  205. package/src/vite/plugins/version-plugin.ts +1 -1
  206. package/src/vite/plugins/virtual-entries.ts +1 -1
  207. package/src/vite/router-discovery.ts +29 -2428
  208. package/src/vite/temp-rsc-server.ts +223 -0
  209. package/src/vite/utils/prerender-utils.ts +47 -0
  210. package/src/wire-params.ts +35 -0
  211. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  212. package/src/router/segment-resolution/fresh.ts +0 -945
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -320,11 +320,14 @@ domains, and the router id keeps two routers on one store apart when both
320
320
  serve the same host and path (see `/host-router`, "Shared cache store"); the search part is sorted, excludes the router's internal params, and
321
321
  honors `createRouter({ cache: { searchParams } })`.
322
322
 
323
- A custom `key` function replaces the whole default key (e.g., to key by user role
324
- or locale); it also bypasses the store's `keyGenerator` and the search-param
325
- filter. It runs once per request and may read `cookies()`. Its result is
326
- stored namespaced (`key:` plus its URI encoding), so no value it returns, raw
327
- request input included, names another route's record. A nested `cache()`
323
+ A custom `key` function replaces the URL part of the default key (e.g., to
324
+ key by user role or locale), not the request type: document loads, client
325
+ navigations and intercept navigations still keep their own records, and the
326
+ key decides which requests of one type share a record. It also bypasses the
327
+ store's `keyGenerator` and the search-param filter. It runs once per request
328
+ and may read `cookies()`. Its result is stored namespaced (`key:` plus its URI
329
+ encoding, then the request type), so no value it returns, raw request input
330
+ included, names another route's record. A nested `cache()`
328
331
  keys its records within the enclosing `key()` partition (`/caching` → "Keys
329
332
  nest"). Normalize the result to the values you serve: each distinct value is
330
333
  a record. On a `ppr` route, `key()` (and a store `keyGenerator` that returns
@@ -102,7 +102,9 @@ tainted objects" below).
102
102
 
103
103
  Handle pushes follow the same split. Handler pushes (`ctx.use(Meta)`,
104
104
  `ctx.use(Breadcrumbs)`, custom handles) are stored with the segments and
105
- replayed on a hit. Pushes from a `loader()` body are not stored: the loader
105
+ replayed on a hit. On a navigation the hit replays them only for the segments
106
+ it sends: a layout the browser keeps keeps its values on screen, as on an
107
+ uncached navigation. Pushes from a `loader()` body are not stored: the loader
106
108
  re-runs on the hit and pushes again, so each value appears once. A loader that
107
109
  runs only because a cached handler awaited it with `ctx.use(Loader)` does not
108
110
  re-run on a hit, so its pushes are stored with the handler's.
@@ -154,7 +156,7 @@ cache(
154
156
  swr: 300, // seconds a stale entry is served while it refreshes.
155
157
  // Default: store.defaults.swr, else no SWR window
156
158
  tags: ["blog"], // or (ctx) => string[]; see "Tag-Based Invalidation"
157
- key: (ctx) => `blog:${ctx.pathname}`, // replaces the default key entirely
159
+ key: (ctx) => `blog:${ctx.pathname}`, // replaces the URL part of the default key
158
160
  condition: (ctx) => !ctx.request.headers.has("x-preview"), // false = bypass
159
161
  store: blogStore, // per-boundary store; see "Custom Cache Store"
160
162
  },
@@ -164,15 +166,26 @@ cache(
164
166
  );
165
167
  ```
166
168
 
167
- - **Default key**: `{doc|partial|intercept}:{routerId}@{host}{pathname}[:params][?search]`
168
- — document requests, client navigations, and intercept navigations are cached
169
- separately, and so are two routers that share a store (see `/host-router`,
170
- "Shared cache store"). The search part honors `cache.searchParams` (below).
171
- - **`key`** is a full override: it bypasses the default key, the store's
172
- `keyGenerator`, and the search-param filter. The result is stored
173
- namespaced (`key:` plus its URI encoding), so whatever it returns, request
174
- input included, never names another route's default-keyed record or a
175
- nested partition's. A nested `cache()` keys its records within that
169
+ - **Default key**: `{doc|partial|intercept:<intercept>}:{routerId}@{host}{pathname}[:params][?search]`
170
+ — under this key, document requests, client navigations, and intercept
171
+ navigations are cached separately, each intercept that targets the route
172
+ apart (`<intercept>` names the one the navigation selected), and so are two
173
+ routers that share a store (see `/host-router`, "Shared cache store"). The
174
+ search part honors `cache.searchParams` (below).
175
+ - **`key`** replaces the URL part of the default key (host, path, params,
176
+ search): it bypasses those, the store's `keyGenerator`, and the
177
+ search-param filter. The request kind stays: document requests, client
178
+ navigations and each intercept navigation keep their own record under one
179
+ `key()` result, so `key()` decides which requests of one kind share a
180
+ record (a `key()` that ignores `:id` shares one document record across
181
+ ids), never that a document request reads a navigation's record (which
182
+ keeps the fallback of a `loading(..., { ssr: false })` that a document
183
+ leaves out) or an intercept's. The
184
+ result is stored namespaced (`key:` plus its URI encoding, then the kind:
185
+ `tier:gold` is `key:tier%3Agold|doc` for a document request and
186
+ `key:tier%3Agold|partial` for a navigation), so whatever it returns,
187
+ request input included, never names another route's default-keyed record
188
+ or a nested partition's. A nested `cache()` keys its records within that
176
189
  partition (see "Keys nest" under "Nested Cache Boundaries"). On a `ppr`
177
190
  route the same key (or, without one, the store's `keyGenerator`)
178
191
  partitions the PPR shell: each partition captures and serves its own shell
@@ -532,7 +545,7 @@ const router = createRouter({
532
545
  baked-shell manifest gate (a URL whose only params are excluded ones now
533
546
  matches the prerendered shell), and `"use cache"` ctx key normalization.
534
547
  - **`cache({ key })` override wins**: a custom key bypasses the filter along
535
- with the rest of default key generation.
548
+ with the rest of the default key's URL part.
536
549
  - **The footgun**: excluding a param promises the rendered output does not
537
550
  depend on it. If it does, the first variant is cached and served to everyone.
538
551
  That is why the default is `"all"`.
@@ -725,7 +738,9 @@ default, shared by every router on the host with that prefix; `HttpOnly`,
725
738
  `SameSite=Lax`, `Path=/`, `Secure` on https, `Max-Age` 11 s with KV and 3 s
726
739
  without at the defaults: the longest memo staleness plus 1 s). It is strictly
727
740
  functional: its value is `1`, it identifies no one, and it only sends that
728
- browser's reads past the memos. The same user's requests that carry it skip
741
+ browser's reads past the memos. A stale entry's background refresh (`cache()`,
742
+ `"use cache"`, a cached loader, the document cache) sets none: no visitor made
743
+ that invalidation. The same user's requests that carry it skip
729
744
  both memos on every isolate, so a mutating user never sees a memoized shell or
730
745
  marker from before their write. What they read past the memos is the store's
731
746
  own consistency. `VercelCacheStore`: `expireTag` is global (about 300 ms), so
@@ -1084,7 +1099,8 @@ cache({ ttl: 300, key: (ctx) => `tier:${tier(ctx)}` }, () => [
1084
1099
  path("/faq", FaqPage, { name: "faq" }),
1085
1100
  ]),
1086
1101
 
1087
- // Its own key() composes with the outer one ("key:tier%3Agold|key:v%3Ab")
1102
+ // Its own key() composes with the outer one, then the request kind
1103
+ // ("key:tier%3Agold|key:v%3Ab|doc")
1088
1104
  cache(
1089
1105
  { ttl: 60, key: (ctx) => `v:${ctx.searchParams.get("v") ?? "a"}` },
1090
1106
  () => [path("/plans", PlansPage, { name: "plans" })],
@@ -1099,10 +1115,10 @@ results first, outermost first:
1099
1115
 
1100
1116
  | Boundary the route sits in | Record key |
1101
1117
  | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1102
- | a single keyed `cache()` | Its `key()` result, namespaced (`tier:gold` is stored as `key:tier%3Agold`) |
1118
+ | a single keyed `cache()` | Its `key()` result, namespaced, then the request kind (`tier:gold` is stored as `key:tier%3Agold\|doc` for a document request) |
1103
1119
  | `cache({ ttl })`, no `key`, under a keyed one | The chain's `key()` results, then this boundary's default key (its store's `keyGenerator` result, else the default key), encoded |
1104
- | `cache({ key })` under a keyed one | The chain's `key()` results |
1105
- | deeper nesting | Composes the same way; only the innermost boundary's missing `key` adds its default key |
1120
+ | `cache({ key })` under a keyed one | The chain's `key()` results, then the request kind |
1121
+ | deeper nesting | Composes the same way; the innermost boundary adds its default key without a `key` of its own, its request kind with one |
1106
1122
  | under a `cache({ store })` on another store | Also that store's `keyGenerator` result, one part per store: empty for a store that returns the default key while another's differs, none when all return it |
1107
1123
  | `cache(false)` | No caching, as before; a `cache()` re-enabled below it stays in the partition |
1108
1124
  | no `key` anywhere | The default key, or the store's `keyGenerator` result, as before |
@@ -1134,7 +1150,10 @@ results first, outermost first:
1134
1150
  empty result there fails key resolution, so the request renders uncached,
1135
1151
  and the router warns once naming the store. On the same store the inner
1136
1152
  boundary's own default key already carries it (or its own `key()`
1137
- overrides it).
1153
+ overrides it). Keep the default key in the result
1154
+ (`` `${locale}:${defaultKey}` ``): its prefix is the request kind, and a
1155
+ result without it gives a document request, a navigation and each
1156
+ intercept navigation one record.
1138
1157
  - A response route's entry (`path.json()` and the other response routes) is
1139
1158
  keyed the same way, behind a `response:` prefix. A `ppr` route's shell is
1140
1159
  partitioned by the chain's `key()` results, namespaced the same way, since
@@ -161,6 +161,16 @@ short-circuits the middleware's downstream pipeline; global middleware that
161
161
  wraps it can still run. Route middleware, handlers, and loaders below it do not.
162
162
  An external CDN hit is different: the function never runs at all.
163
163
 
164
+ The background revalidation re-runs that downstream pipeline for the stale
165
+ entry. Its cookies, and the headers its route handlers and the middleware
166
+ after the cache write, go on its own response, never on the visitor's `STALE`
167
+ response; middleware ahead of the cache gets the `STALE` response from
168
+ `next()`, and its writes after `next()` land there. It
169
+ starts from what middleware ahead of the cache wrote to the response before
170
+ `next()`, as a `MISS` does, and its response is stored by the rules below:
171
+ its headers are kept, and a cookie it sets keeps the refreshed page out of the
172
+ cache.
173
+
164
174
  ## Cache Status Header
165
175
 
166
176
  Response includes `x-document-cache-status`:
@@ -215,7 +225,9 @@ suffixes are still appended.
215
225
  everyone). That includes a response whose request called
216
226
  `updateTag()`/`revalidateTag()` on a store that keeps per-isolate memos
217
227
  (`CFCacheStore`, `VercelCacheStore`): it carries the fresh-reads cookie
218
- (`rango-state-fresh` by default), so it is not stored.
228
+ (`rango-state-fresh` by default), so it is not stored. A stale entry's
229
+ background re-run sets no fresh-reads cookie (no visitor made that
230
+ invalidation), so its invalidation does not keep the refreshed page out.
219
231
  - Non-200 responses
220
232
  - A 200 whose render reported an error: a component that threw after the
221
233
  response started streaming, such as an async server component whose fetch
@@ -368,7 +368,7 @@ Per-slot merge order is **handler.use → shared use → slot-local use**. Slot-
368
368
 
369
369
  Other things to keep in mind about `loading()`:
370
370
 
371
- - Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading === false` handling in [segment-resolution/fresh.ts](../../src/router/segment-resolution/fresh.ts)) — use the slot descriptor form for parallel slots so the opt-out doesn't broadcast.
371
+ - Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading === false` handling in [segment-resolution/revalidation.ts](../../src/router/segment-resolution/revalidation.ts)) — use the slot descriptor form for parallel slots so the opt-out doesn't broadcast.
372
372
 
373
373
  Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every mount site to stream by default. Use the slot descriptor's `use` for any per-slot intent at a `parallel()` call.
374
374
 
@@ -370,9 +370,14 @@ below), and `cache()` onto the target route.
370
370
 
371
371
  To cache a modal, put `cache()` on the target route (or a layout above it).
372
372
  An intercept navigation is cached under that scope with its own `intercept:`
373
- key, so it never collides with the full-page render of the same URL. For
374
- finer control, use `"use cache"` in the intercept handler, or cache the
375
- intercept's loader: `loader(ProductLoader, () => [cache()])`.
373
+ key, so it never collides with the full-page render of the same URL. The key
374
+ names the intercept the navigation selected, so two intercepts that target one
375
+ route with different `when()` conditions each keep their own record. Both
376
+ hold under a `cache({ key })` too: the `key()` replaces the URL part of the
377
+ key, never the request kind, so a document request and a plain navigation
378
+ never read an intercept's record (see `/caching`). For finer control, use
379
+ `"use cache"` in the intercept handler, or cache the intercept's loader:
380
+ `loader(ProductLoader, () => [cache()])`.
376
381
 
377
382
  ## Errors in an Intercept
378
383
 
@@ -463,7 +468,9 @@ contract and direction-aware examples.
463
468
  ## Interaction with Prerender
464
469
 
465
470
  When the target route of an intercept uses `Prerender`, the intercept handler is
466
- also resolved at build time and stored alongside the main pre-rendered segments.
471
+ also resolved at build time and stored in an intercept variant next to the
472
+ route's own pre-rendered page. The variant holds what an intercept navigation
473
+ renders: the layouts above the route and the intercept, not the route's page.
467
474
  This means intercept navigations to pre-rendered routes are served from the
468
475
  prerender store without executing handler code at runtime.
469
476
 
@@ -496,13 +503,16 @@ Build-time behavior:
496
503
  - The intercept handler (`<ProductModal />`) is resolved with BuildContext
497
504
  - Result is stored under the key `"detail/paramHash/i"` (intercept variant)
498
505
  - `when` config conditions are skipped at build time (all intercepts pre-rendered unconditionally)
499
- - `when` is still evaluated at runtime by the intercept-resolution middleware
506
+ - `when` is still evaluated at runtime by the intercept-resolution middleware,
507
+ and a navigation shows only the intercept it selected
508
+ - Two intercepts on one layout that fill the same slot are not pre-rendered:
509
+ they render live
500
510
 
501
511
  Runtime behavior:
502
512
 
503
- - Intercept navigation: prerender store serves the `/i` variant (frozen handler + fresh loaders)
513
+ - Intercept navigation: prerender store serves the `/i` variant (frozen handler + fresh loaders). A layout the client holds (`ShopLayout`) stays as it is on screen, as on a live intercept, unless its `revalidate()` asks for it
504
514
  - Direct navigation: prerender store serves the main variant (full page)
505
- - If no intercept prerender entry exists, falls through to live intercept resolution
515
+ - If no intercept prerender entry exists, or it lacks the selected intercept, falls through to live intercept resolution
506
516
 
507
517
  Loaders inside the intercept always run fresh at request time, same as regular
508
518
  pre-rendered routes.
@@ -319,6 +319,24 @@ export const timing: Middleware = async (ctx, next) => {
319
319
  };
320
320
  ```
321
321
 
322
+ A middleware may answer without waiting for its `next()`, as a timeout
323
+ fallback does. The middleware ahead then see its `Response` after `next()`
324
+ and write on it. The abandoned render keeps running and builds its own
325
+ response, which nothing serves.
326
+
327
+ ```typescript
328
+ export const renderTimeout: Middleware = (ctx, next) =>
329
+ Promise.race([
330
+ next(),
331
+ new Promise<Response>((resolve) =>
332
+ setTimeout(
333
+ () => resolve(new Response("Try again", { status: 503 })),
334
+ 2000,
335
+ ),
336
+ ),
337
+ ]);
338
+ ```
339
+
322
340
  ### Typed context variables in middleware
323
341
 
324
342
  Use `createVar<T>()` for type-safe data sharing between middleware and handlers:
@@ -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
@@ -704,7 +704,14 @@ is usually too aggressive.
704
704
  Throw for genuinely exceptional conditions (network failure, DB outage,
705
705
  auth violation). The nearest `errorBoundary()` above the page the action was
706
706
  called from catches them, and the boundary renders inside route middleware like
707
- 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.
708
715
 
709
716
  ```typescript
710
717
  // urls.tsx — inside the urls() callback
@@ -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
@@ -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) |
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.
@@ -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() {
@@ -258,7 +258,8 @@ export interface EventController {
258
258
  /** Originating history entry key; scopes location-state arbitration. */
259
259
  cohort?: string,
260
260
  ): ActionHandle;
261
- abortAllActions(): void;
261
+ /** Abort every action in flight except `except` (an ActionHandle id). */
262
+ abortAllActions(except?: string): void;
262
263
 
263
264
  // State access
264
265
  getState(): DerivedNavigationState;
@@ -1108,13 +1109,14 @@ export function createEventController(
1108
1109
  };
1109
1110
  }
1110
1111
 
1111
- function abortAllActions() {
1112
+ /** Abort every action in flight except `except` (an ActionHandle id). */
1113
+ function abortAllActions(except?: string) {
1112
1114
  for (const [id, entry] of inflightActions) {
1113
1115
  // Preserve settling entries — they have already been handled by
1114
1116
  // fail()/complete() and will self-cleanup via the settlement timeout.
1115
1117
  // Clearing them here would prevent debounced notifications from
1116
1118
  // delivering the error/result state to subscribers.
1117
- if (entry.phase === "settling") continue;
1119
+ if (entry.phase === "settling" || id === except) continue;
1118
1120
  entry.abort.abort();
1119
1121
  inflightActions.delete(id);
1120
1122
  }