@rangojs/router 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/action-fence.d.ts +2 -1
  4. package/dist/types/browser/event-controller.d.ts +8 -2
  5. package/dist/types/browser/history-state.d.ts +3 -9
  6. package/dist/types/browser/navigation-store.d.ts +8 -6
  7. package/dist/types/browser/partial-update.d.ts +11 -0
  8. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  9. package/dist/types/browser/react/use-action.d.ts +7 -2
  10. package/dist/types/browser/react/use-handle.d.ts +19 -16
  11. package/dist/types/browser/react/use-link-status.d.ts +5 -2
  12. package/dist/types/browser/segment-reconciler.d.ts +32 -4
  13. package/dist/types/browser/types.d.ts +24 -5
  14. package/dist/types/browser/wire-protocol.d.ts +26 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  16. package/dist/types/cache/cache-scope.d.ts +34 -22
  17. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  19. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  20. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  21. package/dist/types/cache/segment-codec.d.ts +14 -0
  22. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  23. package/dist/types/cache/taint.d.ts +5 -3
  24. package/dist/types/cache/types.d.ts +7 -3
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  26. package/dist/types/prefetch-markers.d.ts +5 -14
  27. package/dist/types/prerender/store.d.ts +31 -0
  28. package/dist/types/prerender/warm-request.d.ts +3 -3
  29. package/dist/types/prerender.d.ts +2 -2
  30. package/dist/types/route-name.d.ts +5 -0
  31. package/dist/types/router/error-handling.d.ts +11 -5
  32. package/dist/types/router/instrument.d.ts +4 -3
  33. package/dist/types/router/intercept-resolution.d.ts +25 -9
  34. package/dist/types/router/match-context.d.ts +6 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  38. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  39. package/dist/types/router/navigation-snapshot.d.ts +7 -0
  40. package/dist/types/router/prerender-match.d.ts +21 -6
  41. package/dist/types/router/preview-match.d.ts +2 -7
  42. package/dist/types/router/request-classification.d.ts +5 -0
  43. package/dist/types/router/router-context.d.ts +7 -46
  44. package/dist/types/router/router-interfaces.d.ts +8 -14
  45. package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
  46. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  47. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  48. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  49. package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
  52. package/dist/types/router/segment-resolution.d.ts +1 -2
  53. package/dist/types/router/segment-wrappers.d.ts +11 -48
  54. package/dist/types/router/telemetry.d.ts +2 -0
  55. package/dist/types/router/types.d.ts +11 -1
  56. package/dist/types/rsc/full-payload.d.ts +2 -2
  57. package/dist/types/rsc/handler-context.d.ts +6 -6
  58. package/dist/types/rsc/json-route-result.d.ts +5 -9
  59. package/dist/types/rsc/request-stages.d.ts +203 -0
  60. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  61. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  62. package/dist/types/rsc/routine-plan.d.ts +1 -1
  63. package/dist/types/rsc/server-action.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  65. package/dist/types/rsc/shell-capture.d.ts +132 -15
  66. package/dist/types/rsc/shell-serve.d.ts +37 -12
  67. package/dist/types/server/context.d.ts +9 -8
  68. package/dist/types/server/handle-store.d.ts +16 -0
  69. package/dist/types/server/render-pass.d.ts +72 -0
  70. package/dist/types/server/request-context.d.ts +53 -104
  71. package/dist/types/server/rewrite-trail.d.ts +2 -2
  72. package/dist/types/testing/dispatch.d.ts +35 -18
  73. package/dist/types/testing/render-route.d.ts +43 -8
  74. package/dist/types/testing/serve-shell-request.d.ts +8 -1
  75. package/dist/types/types/cache-types.d.ts +6 -3
  76. package/dist/types/types/error-types.d.ts +30 -3
  77. package/dist/types/types/handler-context.d.ts +9 -1
  78. package/dist/types/types/segments.d.ts +1 -1
  79. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  80. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  81. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  82. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  83. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  84. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  85. package/dist/types/vite/discovery/state.d.ts +12 -0
  86. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  87. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  88. package/dist/types/vite/router-discovery.d.ts +8 -47
  89. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  90. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  91. package/dist/types/wire-params.d.ts +39 -0
  92. package/dist/vite/index.js +3822 -3727
  93. package/package.json +1 -1
  94. package/skills/cache-guide/SKILL.md +8 -5
  95. package/skills/caching/SKILL.md +37 -18
  96. package/skills/client-urls/SKILL.md +4 -2
  97. package/skills/document-cache/SKILL.md +13 -1
  98. package/skills/handler-use/SKILL.md +1 -1
  99. package/skills/hooks/SKILL.md +1 -1
  100. package/skills/hooks/data.md +8 -1
  101. package/skills/hooks/handle-and-actions.md +33 -2
  102. package/skills/hooks/navigation.md +121 -15
  103. package/skills/hooks/state.md +6 -4
  104. package/skills/hooks/urls.md +16 -5
  105. package/skills/intercept/SKILL.md +23 -7
  106. package/skills/middleware/SKILL.md +18 -0
  107. package/skills/observability/SKILL.md +30 -0
  108. package/skills/ppr/SKILL.md +18 -3
  109. package/skills/prerender/SKILL.md +39 -15
  110. package/skills/rango/SKILL.md +5 -1
  111. package/skills/server-actions/SKILL.md +15 -1
  112. package/skills/testing/client-components.md +25 -18
  113. package/skills/testing/response-routes.md +6 -4
  114. package/skills/testing/serve-shell-request.md +7 -7
  115. package/skills/use-cache/SKILL.md +46 -8
  116. package/skills/view-transitions/SKILL.md +1 -1
  117. package/src/__internal.ts +7 -1
  118. package/src/browser/action-fence.ts +2 -1
  119. package/src/browser/connection-warmup.ts +3 -1
  120. package/src/browser/event-controller.ts +37 -13
  121. package/src/browser/history-state.ts +6 -12
  122. package/src/browser/navigation-bridge.ts +59 -26
  123. package/src/browser/navigation-client.ts +20 -25
  124. package/src/browser/navigation-store.ts +25 -27
  125. package/src/browser/navigation-transaction.ts +4 -1
  126. package/src/browser/network-error-handler.ts +4 -0
  127. package/src/browser/partial-update.ts +33 -6
  128. package/src/browser/prefetch/cache.ts +2 -1
  129. package/src/browser/prefetch/fetch.ts +8 -16
  130. package/src/browser/prefetch/queue.ts +4 -4
  131. package/src/browser/react/NavigationProvider.tsx +2 -1
  132. package/src/browser/react/filter-segment-order.ts +2 -2
  133. package/src/browser/react/use-action.ts +24 -48
  134. package/src/browser/react/use-handle.ts +22 -18
  135. package/src/browser/react/use-link-status.ts +48 -60
  136. package/src/browser/react/use-navigation.ts +92 -41
  137. package/src/browser/rsc-router.tsx +1 -0
  138. package/src/browser/segment-reconciler.ts +109 -53
  139. package/src/browser/server-action-bridge.ts +81 -61
  140. package/src/browser/types.ts +25 -5
  141. package/src/browser/wire-protocol.ts +43 -0
  142. package/src/cache/cache-key-utils.ts +11 -5
  143. package/src/cache/cache-runtime.ts +132 -75
  144. package/src/cache/cache-scope.ts +110 -64
  145. package/src/cache/cf/cf-cache-constants.ts +2 -2
  146. package/src/cache/cf/cf-cache-store.ts +486 -874
  147. package/src/cache/cf/cf-kv-utils.ts +1 -1
  148. package/src/cache/document-cache.ts +64 -46
  149. package/src/cache/handle-snapshot.ts +19 -7
  150. package/src/cache/read-through-swr.ts +20 -18
  151. package/src/cache/segment-codec.ts +55 -0
  152. package/src/cache/shell-snapshot.ts +2 -2
  153. package/src/cache/tag-invalidation.ts +6 -2
  154. package/src/cache/taint.ts +5 -3
  155. package/src/cache/types.ts +7 -3
  156. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  157. package/src/handles/pushed-by.ts +1 -1
  158. package/src/prefetch-markers.ts +5 -16
  159. package/src/prerender/store.ts +76 -0
  160. package/src/prerender/warm-request.ts +8 -4
  161. package/src/prerender.ts +2 -2
  162. package/src/route-name.ts +8 -0
  163. package/src/router/error-handling.ts +22 -10
  164. package/src/router/instrument.ts +4 -3
  165. package/src/router/intercept-resolution.ts +82 -75
  166. package/src/router/loader-resolution.ts +5 -4
  167. package/src/router/match-api.ts +5 -5
  168. package/src/router/match-context.ts +23 -12
  169. package/src/router/match-handlers.ts +8 -20
  170. package/src/router/match-middleware/background-revalidation.ts +46 -57
  171. package/src/router/match-middleware/cache-lookup.ts +677 -726
  172. package/src/router/match-middleware/cache-store.ts +4 -8
  173. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  174. package/src/router/match-middleware/segment-resolution.ts +34 -61
  175. package/src/router/middleware.ts +69 -11
  176. package/src/router/navigation-snapshot.ts +14 -4
  177. package/src/router/prerender-match.ts +100 -44
  178. package/src/router/preview-match.ts +6 -41
  179. package/src/router/request-classification.ts +13 -7
  180. package/src/router/route-snapshot.ts +2 -5
  181. package/src/router/router-context.ts +10 -95
  182. package/src/router/router-interfaces.ts +8 -14
  183. package/src/router/segment-resolution/helpers.ts +49 -13
  184. package/src/router/segment-resolution/loader-cache.ts +15 -9
  185. package/src/router/segment-resolution/loader-mask.ts +5 -5
  186. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  187. package/src/router/segment-resolution/resolution-pass.ts +139 -0
  188. package/src/router/segment-resolution/revalidation.ts +939 -789
  189. package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
  190. package/src/router/segment-resolution.ts +4 -13
  191. package/src/router/segment-wrappers.ts +51 -269
  192. package/src/router/telemetry.ts +5 -0
  193. package/src/router/types.ts +11 -1
  194. package/src/router.ts +19 -17
  195. package/src/rsc/full-payload.ts +6 -5
  196. package/src/rsc/handler-context.ts +8 -14
  197. package/src/rsc/handler.ts +126 -507
  198. package/src/rsc/json-route-result.ts +5 -9
  199. package/src/rsc/loader-fetch.ts +6 -5
  200. package/src/rsc/progressive-enhancement.ts +5 -0
  201. package/src/rsc/request-stages.ts +759 -0
  202. package/src/rsc/response-cache-serve.ts +10 -14
  203. package/src/rsc/response-route-handler.ts +13 -5
  204. package/src/rsc/rewrite-passes.ts +2 -1
  205. package/src/rsc/routine-plan.ts +1 -1
  206. package/src/rsc/rsc-rendering.ts +164 -349
  207. package/src/rsc/server-action.ts +87 -113
  208. package/src/rsc/shell-capture-constants.ts +1 -1
  209. package/src/rsc/shell-capture.ts +275 -227
  210. package/src/rsc/shell-serve.ts +121 -18
  211. package/src/rsc/ssr-setup.ts +5 -4
  212. package/src/segment-system.tsx +3 -3
  213. package/src/server/context.ts +22 -25
  214. package/src/server/handle-store.ts +55 -0
  215. package/src/server/render-pass.ts +291 -0
  216. package/src/server/request-context.ts +75 -115
  217. package/src/server/rewrite-trail.ts +6 -4
  218. package/src/ssr/index.tsx +1 -1
  219. package/src/testing/dispatch.ts +226 -710
  220. package/src/testing/e2e/page-helpers.ts +2 -2
  221. package/src/testing/render-route.tsx +199 -13
  222. package/src/testing/serve-shell-request.ts +36 -40
  223. package/src/types/cache-types.ts +6 -3
  224. package/src/types/error-types.ts +30 -3
  225. package/src/types/handler-context.ts +12 -1
  226. package/src/types/segments.ts +1 -1
  227. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  228. package/src/vite/discovery/build-env.ts +148 -0
  229. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  230. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  231. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  232. package/src/vite/discovery/discover-routers.ts +1 -1
  233. package/src/vite/discovery/discovery-errors.ts +1 -1
  234. package/src/vite/discovery/prerender-collection.ts +24 -39
  235. package/src/vite/discovery/state.ts +15 -0
  236. package/src/vite/plugins/version-plugin.ts +1 -1
  237. package/src/vite/plugins/virtual-entries.ts +1 -1
  238. package/src/vite/router-discovery.ts +29 -2428
  239. package/src/vite/temp-rsc-server.ts +223 -0
  240. package/src/vite/utils/prerender-utils.ts +47 -0
  241. package/src/wire-params.ts +40 -0
  242. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  243. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { ConsoleMessage, Expect, Locator, Page } from "@playwright/test";
6
6
  import { PREFETCH_HEADER } from "../../prefetch-markers.js";
7
+ import { WIRE } from "../../wire-params.js";
7
8
 
8
9
  // ============================================================================
9
10
  // Standalone helpers (need only a page/locator)
@@ -177,8 +178,7 @@ export function isPrefetchRequest(req: {
177
178
 
178
179
  // Module-level matcher + handler so blockPrefetch/unblockPrefetch share
179
180
  // stable references — Playwright's unroute matches by identity.
180
- const rscPartialUrl = (url: URL): boolean =>
181
- url.searchParams.has("_rsc_partial");
181
+ const rscPartialUrl = (url: URL): boolean => url.searchParams.has(WIRE.partial);
182
182
  async function abortPrefetchRoute(
183
183
  route: import("@playwright/test").Route,
184
184
  ): Promise<void> {
@@ -26,24 +26,30 @@
26
26
  * - navigate() drives the navigation lifecycle as a click does:
27
27
  * useNavigation() reads loading from the call and idle with the commit,
28
28
  * and useLinkStatus().pending is true for a link to the destination until
29
- * the commit is handed to React. A `transition` spec plus a pending
29
+ * the commit, which React holds with it. A `transition` spec plus a pending
30
30
  * Promise seeded via navigate(url, { loaders }) commits through
31
31
  * production's commitInTransition (browser/partial-update.ts), which React
32
32
  * holds: useNavigation() stays loading and the held reader's
33
33
  * useLoader().isLoading reads true until the data lands. Without
34
34
  * `transition` the commit is urgent (production's same-structure hold is
35
- * not modeled). useAction().state stays idle: no action runs.
35
+ * not modeled). navigate(url, { streamUntil }) gives the navigation a
36
+ * response stream that stays open past the commit (isStreaming).
37
+ * useAction().state stays idle: no action runs.
36
38
  * - A spec's transition({ when }) decides every commit with production's
37
39
  * browser decision: navigate() and useRouter().push model kind "push",
38
- * useRouter().replace kind "replace", refresh() kind "revalidate".
39
- * Back/forward, action and optimistic clientUrls() swap decisions are
40
- * e2e territory.
40
+ * useRouter().replace kind "replace", refresh() kind "revalidate", and a
41
+ * back/forward that restores a page kind "pop". Action and optimistic
42
+ * clientUrls() swap decisions are e2e territory.
41
43
  * - Location state reaches readers the way it does in production:
42
44
  * NavigationProvider holds the committed entry's state next to the
43
45
  * payload, so a navigation whose loader the test holds keeps the entry
44
46
  * being left on screen with its own state. A `popstate` event is a
45
- * back/forward onto the entry history.state holds; the tree has one
46
- * location, so no page is restored with it. A server action's state, a
47
+ * back/forward onto the entry history.state holds. When it is the
48
+ * mounted entry or one a navigate() wrote, history still holds the state
49
+ * written there, and another page is on screen, its page is restored
50
+ * from the history cache, with the handle values it had, as production's
51
+ * restore is; otherwise the tree keeps its location. A stale entry's
52
+ * revalidation, a restore that fetches, a server action's state, a
47
53
  * redirect's and an intercept's are e2e territory.
48
54
  * What it DOES cover: client hooks that read NavigationProvider /
49
55
  * OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
@@ -61,6 +67,7 @@
61
67
  import { decideTransitionGatedOff } from "../browser/transition-when.js";
62
68
  import {
63
69
  StrictMode,
70
+ startTransition,
64
71
  useEffect,
65
72
  type ReactNode,
66
73
  type ComponentType,
@@ -78,10 +85,12 @@ import {
78
85
  shouldStartViewTransition,
79
86
  } from "../browser/partial-update.js";
80
87
  import type {
88
+ HandleData,
81
89
  NavigateOptions,
82
90
  NavigateOptionsInternal,
83
91
  NavigationStore,
84
92
  NavigationBridge,
93
+ NavigationUpdate,
85
94
  UpdateSubscriber,
86
95
  } from "../browser/types.js";
87
96
  import type {
@@ -101,6 +110,7 @@ import type {
101
110
  TransitionWhenKind,
102
111
  } from "../types.js";
103
112
  import {
113
+ setCurrentHistoryStateListener,
104
114
  setLocationStateVersion,
105
115
  type LocationStateDefinition,
106
116
  } from "../browser/react/location-state-shared.js";
@@ -447,11 +457,40 @@ export interface TestRouterHandle {
447
457
  * `handles`). They arrive on the response's handle stream, so `useHandle`
448
458
  * readers take them with the page's commit: while React holds the page,
449
459
  * readers keep the values of the page being left.
460
+ *
461
+ * `options.streamUntil` gives the navigation a response stream, as a real
462
+ * response has: it opens in a task after the call, before the commit (the
463
+ * response arrives, and `useNavigation().pendingUrl` clears), and stays open
464
+ * until the promise settles (a page that streams a tail past its commit:
465
+ * `isStreaming` reads true until then). Without it the navigation has no
466
+ * stream. A navigation that a newer one or a back/forward aborts before its
467
+ * response arrives opens none and commits nothing; one aborted after its
468
+ * response arrived stops counting as streaming at the abort, as in
469
+ * production, where its response streams on and is ignored.
470
+ *
471
+ * @example
472
+ * const tail = deferred();
473
+ * await router.navigate("/products/2", { streamUntil: tail.promise });
474
+ * // useNavigation(): idle, isStreaming true
475
+ * await act(async () => tail.resolve());
476
+ * // useNavigation(): idle, isStreaming false
477
+ *
478
+ * A navigation started while React holds a page goes inside
479
+ * `await act(async () => ...)`. Its first update makes React retry the held
480
+ * page, which suspends again; a synchronous act() scope drops that retry
481
+ * and React renders no later update of the same priority.
482
+ *
483
+ * @example
484
+ * await act(async () => {
485
+ * await router.navigate("/products/3", { transition: false });
486
+ * });
450
487
  */
451
488
  navigate(
452
489
  url: string,
453
490
  options?: Pick<RenderRouteOptions, "loaders" | "handles"> &
454
- Pick<NavigateOptions, "state" | "replace" | "transition">,
491
+ Pick<NavigateOptions, "state" | "replace" | "transition"> & {
492
+ streamUntil?: Promise<unknown>;
493
+ },
455
494
  ): Promise<void>;
456
495
  /**
457
496
  * Re-render the current location, as router.refresh() does: a spec's
@@ -821,7 +860,49 @@ export async function renderRoute(
821
860
  history?: Pick<NavigateOptionsInternal, "state" | "replace" | "transition">;
822
861
  kind?: TransitionWhenKind;
823
862
  transition?: boolean;
863
+ streamUntil?: Promise<unknown>;
824
864
  };
865
+
866
+ // The page each history entry shows, by the entry's `idx` stamp (the
867
+ // seeded entry has none: 0), so a back/forward can restore it as
868
+ // production restores a history-cache entry. `state` is the entry's
869
+ // history state as the router last wrote it: an entry the test wrote over
870
+ // it (no `idx` reads as 0) is not that page. `handleData` is the page's
871
+ // pushes, taken when it is left.
872
+ interface EntryPage {
873
+ url: URL;
874
+ segments: ResolvedSegment[];
875
+ params: Record<string, string>;
876
+ state: unknown;
877
+ handleData?: HandleData;
878
+ }
879
+ const entries = new Map<number, EntryPage>();
880
+ const entryIndex = (): number => {
881
+ const idx = (window.history.state as { idx?: unknown } | null)?.idx;
882
+ return typeof idx === "number" ? idx : 0;
883
+ };
884
+ let entryOnScreen = entryIndex();
885
+ entries.set(entryOnScreen, {
886
+ url,
887
+ segments: initialSegments,
888
+ params: initialMatch.params,
889
+ state: window.history.state,
890
+ });
891
+ // A write the router makes to the entry history is on (a flash read,
892
+ // Def.write() or delete()) stays that entry's state, as production's
893
+ // per-entry memory follows it (navigation-store.ts). A test's own
894
+ // history.replaceState() does not.
895
+ setCurrentHistoryStateListener(() => {
896
+ store.rememberDisplayedEntry();
897
+ const written = entries.get(entryIndex());
898
+ if (written) written.state = window.history.state;
899
+ });
900
+ /** The page being left keeps its pushes for a back/forward onto it. */
901
+ const leaveEntry = (): void => {
902
+ const left = entries.get(entryOnScreen);
903
+ if (left) left.handleData = eventController.getHandleState().data;
904
+ };
905
+ let popGeneration = 0;
825
906
  // The navigation's lifecycle, as navigation-transaction.ts drives it:
826
907
  // useNavigation() reads loading from the call, delivered before the commit
827
908
  // as a click's notification goes out while its response is in flight, and
@@ -837,6 +918,29 @@ export async function renderRoute(
837
918
  // takes it, and no act() of the caller's can overlap it.
838
919
  act(() => eventController.flushRouteState());
839
920
  try {
921
+ const streamUntil = navOptions?.streamUntil;
922
+ if (streamUntil) {
923
+ // The response arrives a task after the request, ahead of the
924
+ // commit (navigation-transaction.ts: the stream opens with it).
925
+ await act(async () => {
926
+ await new Promise((resolve) => setTimeout(resolve, 0));
927
+ });
928
+ if (handle.signal.aborted) return;
929
+ const stream = handle.startStreaming();
930
+ act(() => eventController.flushRouteState());
931
+ // A navigation that is left stops counting as streaming at the abort
932
+ // (partial-update.ts endStream); the abort's notification goes out
933
+ // with the newer navigation's.
934
+ handle.signal.addEventListener("abort", () => stream.end(), {
935
+ once: true,
936
+ });
937
+ const end = (): void =>
938
+ act(() => {
939
+ stream.end();
940
+ eventController.flushRouteState();
941
+ });
942
+ void streamUntil.then(end, end);
943
+ }
840
944
  await commitNavigation(handle, target, navOptions);
841
945
  } finally {
842
946
  // Back to idle when the commit never came (a render that threw).
@@ -922,6 +1026,7 @@ export async function renderRoute(
922
1026
  // A newer navigate() started meanwhile: this one commits nothing, as
923
1027
  // production's (browser/partial-update.ts) returns on its aborted signal.
924
1028
  if (handle.signal.aborted) return;
1029
+ leaveEntry();
925
1030
  handle.complete(nextUrl);
926
1031
  eventController.setParams(match.params);
927
1032
  store.setCurrentUrl(nextUrl.href);
@@ -934,6 +1039,13 @@ export async function renderRoute(
934
1039
  );
935
1040
  eventController.commitLocationState(historyState);
936
1041
  }
1042
+ entryOnScreen = entryIndex();
1043
+ entries.set(entryOnScreen, {
1044
+ url: nextUrl,
1045
+ segments,
1046
+ params: match.params,
1047
+ state: window.history.state,
1048
+ });
937
1049
  const emit: UpdateSubscriber = (update) => store.emitUpdate(update);
938
1050
  await act(async () => {
939
1051
  // As production's emitters name their cause (suspense-audit.ts I6).
@@ -961,17 +1073,90 @@ export async function renderRoute(
961
1073
  return navigate(current.pathname + current.search, { kind: "revalidate" });
962
1074
  };
963
1075
 
1076
+ // A back/forward onto the mounted entry or one a navigation wrote, whose
1077
+ // page is not the one on screen: its page is restored from the entry, as production's
1078
+ // history-cache restore does (navigation-bridge.ts handlePopstate). Not
1079
+ // modeled: a stale entry's revalidation and a restore that has to fetch.
1080
+ const restoreEntry = async (index: number, page: EntryPage) => {
1081
+ const generation = ++popGeneration;
1082
+ eventController.abortNavigation();
1083
+ const historyState = window.history.state;
1084
+ // transition({ when }) decides the restore (kind "pop") against the
1085
+ // entry being left.
1086
+ const gatedOff = decideTransitionGatedOff(page.segments, () => ({
1087
+ kind: "pop",
1088
+ from: {
1089
+ url: eventController.getLocation().href,
1090
+ params: eventController.getParams(),
1091
+ routeName: leaf.name,
1092
+ state: entries.get(entryOnScreen)?.state ?? null,
1093
+ },
1094
+ to: {
1095
+ url: page.url,
1096
+ params: page.params,
1097
+ routeName: leaf.name,
1098
+ state: historyState,
1099
+ },
1100
+ }));
1101
+ const root = await renderSegments(page.segments, {
1102
+ forceAwait: true,
1103
+ outletPending: options.outletPending,
1104
+ transitionGatedOff: gatedOff,
1105
+ });
1106
+ // A later back/forward owns the screen.
1107
+ if (generation !== popGeneration) return;
1108
+ leaveEntry();
1109
+ entryOnScreen = index;
1110
+ store.setCurrentUrl(page.url.href);
1111
+ store.setSegmentIds(page.segments.map((s) => s.id));
1112
+ const update: NavigationUpdate = {
1113
+ root,
1114
+ metadata: {
1115
+ ...makeMetadata(page.url.pathname, page.segments, page.params),
1116
+ isPartial: true,
1117
+ cachedHandleData: page.handleData,
1118
+ },
1119
+ };
1120
+ await act(async () => {
1121
+ // The restored entry's location and params, set where its tree is
1122
+ // handed over, so the hooks take them in that batch.
1123
+ eventController.setLocation(page.url);
1124
+ eventController.setParams(page.params);
1125
+ eventController.commitLocationState(historyState);
1126
+ auditTreeCause("popstate");
1127
+ if (!shouldStartViewTransition(page.segments, gatedOff)) {
1128
+ store.emitUpdate(update);
1129
+ } else {
1130
+ startTransition(() => {
1131
+ store.emitUpdate({ ...update, inTransition: true });
1132
+ });
1133
+ }
1134
+ });
1135
+ };
1136
+
964
1137
  let prefetchRoot: HTMLElement | undefined;
965
1138
  const bridge: NavigationBridge = {
966
1139
  navigate: (target, navigateOptions) =>
967
1140
  navigate(target, { history: navigateOptions ?? {} }),
968
1141
  refresh: () => refresh(),
969
- // Back/forward onto the entry history.state holds. The tree has one
970
- // location, so nothing is restored: a treeless commit of that entry, as
971
- // production's traversal that keeps every segment
972
- // (navigation-transaction.ts commit). Flushed, not left to the notify
973
- // timer: the dispatching test's act() has returned by then.
1142
+ // Back/forward onto the entry history.state holds. Its page is restored
1143
+ // when the mount or a navigation wrote the entry, history still holds that
1144
+ // state and another page is on screen (restoreEntry). Otherwise the tree
1145
+ // keeps its location: a treeless commit of that entry, as production's
1146
+ // traversal that keeps every segment (navigation-transaction.ts commit).
1147
+ // Flushed, not left to the notify timer: the dispatching test's act()
1148
+ // has returned by then.
974
1149
  handlePopstate: async () => {
1150
+ const index = entryIndex();
1151
+ const page = entries.get(index);
1152
+ if (
1153
+ page &&
1154
+ index !== entryOnScreen &&
1155
+ page.state === window.history.state
1156
+ ) {
1157
+ await restoreEntry(index, page);
1158
+ return;
1159
+ }
975
1160
  eventController.commitLocationState(window.history.state, true);
976
1161
  eventController.flushRouteState();
977
1162
  },
@@ -1091,6 +1276,7 @@ export async function renderRoute(
1091
1276
  loaders: navOptions?.loaders,
1092
1277
  handles: navOptions?.handles,
1093
1278
  transition: navOptions?.transition,
1279
+ streamUntil: navOptions?.streamUntil,
1094
1280
  history:
1095
1281
  navOptions?.state !== undefined || navOptions?.replace
1096
1282
  ? navOptions
@@ -53,16 +53,16 @@ import type { ExecutionContext } from "../types/request-scope.js";
53
53
  import type { HandlerCacheConfig, SSRModule } from "../rsc/types.js";
54
54
  import type { createRSCHandler } from "../rsc/handler.js";
55
55
  import { setDefaultSSRModuleLoaderForTests } from "../rsc/ssr-module-loader.js";
56
+ import {
57
+ createPrerenderStore,
58
+ prerenderManifestEntries,
59
+ } from "../prerender/store.js";
56
60
  import {
57
61
  POST_QUIESCE_TASK_HOPS,
58
62
  SHELL_CAPTURE_MAX_WAIT_MS,
59
63
  } from "../rsc/shell-capture-constants.js";
60
64
  import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../segment-fragments.js";
61
- import {
62
- FILL_PARAM,
63
- PREFETCH_HEADER,
64
- STALE_PARAM,
65
- } from "../prefetch-markers.js";
65
+ import { PREFETCH_HEADER } from "../prefetch-markers.js";
66
66
  import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
67
67
  import { withoutPushedBy } from "../handles/pushed-by.js";
68
68
  import type { HandleData } from "../server/handle-store.js";
@@ -76,6 +76,7 @@ import {
76
76
  type PprReplayStatus,
77
77
  type ShellStatus,
78
78
  } from "./shell-status.js";
79
+ import { writePartialParams } from "../browser/wire-protocol.js";
79
80
 
80
81
  /** Options for {@link serveShellRequest}. */
81
82
  export interface ServeShellRequestOptions<TEnv = any> {
@@ -114,7 +115,13 @@ export interface ServeShellRequestOptions<TEnv = any> {
114
115
  * and segments take an action's revalidation defaults; it renders live, as
115
116
  * an action does, so neither the route cache nor the document cache answers
116
117
  * or stores it, and a `ppr` route's `replayStatus` is
117
- * `{ outcome: "BYPASS", reason: "stale" }`.
118
+ * `{ outcome: "BYPASS", reason: "stale" }`. `boundary` names the error
119
+ * segment of a thrown action's boundary the page at `from` shows
120
+ * (`_rsc_boundary`), as the follow-up an earlier action owes that page
121
+ * sends it, with `stale: true` and no `segments`: the response renders
122
+ * every segment above the boundary and nothing at or below it, claims the
123
+ * boundary in `matched` and sends no segment for it. The server reads it
124
+ * only beside `stale`.
118
125
  */
119
126
  partial?:
120
127
  | true
@@ -126,6 +133,7 @@ export interface ServeShellRequestOptions<TEnv = any> {
126
133
  fill?: boolean;
127
134
  interceptSource?: string;
128
135
  stale?: boolean;
136
+ boundary?: string;
129
137
  };
130
138
  /**
131
139
  * POST this form as a submit before hydration: the no-JS request the
@@ -485,29 +493,14 @@ async function servePrerenderArtifacts(
485
493
  const { hashParams } = await import("../prerender/param-hash.js");
486
494
  baked.add(pathname);
487
495
  if (result && !result.passthrough) {
488
- const key = `${result.routeName}/${hashParams(result.params)}`;
489
- payloads.set(
490
- key,
491
- JSON.stringify({ segments: result.segments, handles: result.handles }),
492
- );
493
- // The intercept variant, keyed and shaped as the build writes it
494
- // (vite/discovery/prerender-collection.ts).
495
- if (result.interceptSegments?.length) {
496
- payloads.set(
497
- `${key}/i`,
498
- JSON.stringify({
499
- segments: [...result.segments, ...result.interceptSegments],
500
- handles: result.interceptHandles ?? "",
501
- }),
502
- );
496
+ const paramHash = hashParams(result.params);
497
+ for (const [key, value] of prerenderManifestEntries(result, paramHash)) {
498
+ payloads.set(key, value);
503
499
  }
504
500
  }
505
501
  }
506
- const [{ createPrerenderStore }, { setPrerenderStoreForTests }] =
507
- await Promise.all([
508
- import("../prerender/store.js"),
509
- import("../router/match-middleware/cache-lookup.js"),
510
- ]);
502
+ const { setPrerenderStoreForTests } =
503
+ await import("../router/match-middleware/cache-lookup.js");
511
504
  const previousLoader = globalThis.__loadPrerenderManifestModule;
512
505
  globalThis.__loadPrerenderManifestModule = async () => ({
513
506
  default: Object.fromEntries([...payloads.keys()].map((key) => [key, key])),
@@ -585,12 +578,12 @@ async function buildActionRequest(
585
578
  const { encodeReply } =
586
579
  await import("@vitejs/plugin-rsc/vendor/react-server-dom/client.edge");
587
580
  registerTestAction(action.ref, action.id, "serveShellRequest");
588
- const target = new URL(url);
589
- target.searchParams.set("_rsc_action", action.id);
590
- target.searchParams.set("_rsc_segments", (action.segments ?? []).join(","));
591
- if (action.rewriteTarget) {
592
- target.searchParams.set("_rsc_rt", action.rewriteTarget);
593
- }
581
+ const target = writePartialParams(new URL(url), {
582
+ action: action.id,
583
+ segmentIds: action.segments ?? [],
584
+ emptySegments: "send",
585
+ rewriteTarget: action.rewriteTarget,
586
+ });
594
587
  headers.set("rsc-action", action.id);
595
588
  if (!headers.has("X-RSC-Router-Client-Path")) {
596
589
  headers.set("X-RSC-Router-Client-Path", url.href);
@@ -640,20 +633,23 @@ async function buildRequest(
640
633
  fill = false,
641
634
  interceptSource,
642
635
  stale = false,
636
+ boundary,
643
637
  } = partial === true ? {} : partial;
644
638
  if (stale && prefetch) {
645
639
  throw new Error(
646
640
  "serveShellRequest: partial.stale cannot be combined with partial.prefetch",
647
641
  );
648
642
  }
649
- const target = new URL(url);
650
- target.searchParams.set("_rsc_partial", "true");
651
- target.searchParams.set("_rsc_segments", segments.join(","));
652
- if (rewriteTarget) target.searchParams.set("_rsc_rt", rewriteTarget);
653
- // As the browser sends them: browser/navigation-client.ts (fill, stale) and
654
- // browser/prefetch/fetch.ts (prefetch).
655
- if (fill) target.searchParams.set(FILL_PARAM, "1");
656
- if (stale) target.searchParams.set(STALE_PARAM, "true");
643
+ // As the browser writes them: browser/navigation-client.ts, and
644
+ // browser/prefetch/fetch.ts for the prefetch header.
645
+ const target = writePartialParams(new URL(url), {
646
+ segmentIds: segments,
647
+ emptySegments: "send",
648
+ stale,
649
+ boundary,
650
+ rewriteTarget,
651
+ fill,
652
+ });
657
653
  if (prefetch && !headers.has(PREFETCH_HEADER)) {
658
654
  headers.set(PREFETCH_HEADER, "1");
659
655
  }
@@ -137,9 +137,12 @@ export interface CacheOptions<TEnv = unknown> {
137
137
  ) => boolean;
138
138
 
139
139
  /**
140
- * Custom cache key function - FULL OVERRIDE.
141
- * Bypasses default key generation AND store's keyGenerator. The result is
142
- * stored namespaced (`key:` plus its URI encoding), so no value it returns
140
+ * Custom cache key function. Replaces the URL part of the default key
141
+ * (host, path, params, search) AND bypasses the store's keyGenerator. The
142
+ * request kind stays: a document request, a navigation and each intercept
143
+ * navigation keep their own record under one result, so the result decides
144
+ * which requests of one kind share a record. It is stored namespaced
145
+ * (`key:` plus its URI encoding, then the kind), so no value it returns
143
146
  * names another route's record; a nested cache() keys its records within
144
147
  * this partition.
145
148
  *
@@ -81,11 +81,38 @@ export interface OnErrorContext<TEnv = any> {
81
81
  actionId?: string;
82
82
  /** Environment/bindings (platform context) */
83
83
  env?: TEnv;
84
- /** Duration from request start to error (milliseconds) */
84
+ /**
85
+ * Duration from request start to error (milliseconds). For a `background`
86
+ * error it counts from the start of the request that started the work, so
87
+ * it includes that response and any wait before the work ran.
88
+ */
85
89
  duration?: number;
86
- /** Whether this is a partial/navigation request */
90
+ /**
91
+ * Whether this is a partial/navigation request, on an error the segment
92
+ * walk catches (a handler that throws, a streamed `loading()` handler that
93
+ * rejects, a DSL loader that rejects): `true` for a navigation, unset for a
94
+ * document. A `cache()` record's refresh or proactive write reports the
95
+ * request that started it; a PPR shell capture renders a document. The
96
+ * `routing` phase reports `false`.
97
+ */
87
98
  isPartial?: boolean;
88
- /** Whether an error boundary caught the error */
99
+ /**
100
+ * `true` when the error came from work whose output goes to a cache and
101
+ * never to the response of the request that started it: a stale `cache()`
102
+ * record's refresh, the write of a navigation that kept a layout mounted, a
103
+ * stale document cache entry's re-run, a PPR shell capture, or a stale
104
+ * `"use cache"` function's or cached loader's refresh, the work's own
105
+ * failure included. `request` and `url` are the starting request's (a
106
+ * shell capture's handler errors name the page it captures). A
107
+ * `router.prerender()` warm's own render is its request's: unset, while a
108
+ * shell capture it makes is `true`. Unset for every other error.
109
+ */
110
+ background?: boolean;
111
+ /**
112
+ * Whether an error boundary caught the error and its fallback reached the
113
+ * response. `false` on a `background` error: the failed render is never
114
+ * stored, so no visitor sees the fallback.
115
+ */
89
116
  handledByBoundary?: boolean;
90
117
  /** Stack trace (if available) */
91
118
  stack?: string;
@@ -462,7 +462,7 @@ export type InternalHandlerContext<
462
462
  _reverseScope?: import("../server/context.js").ReverseScope;
463
463
  /**
464
464
  * @internal The on-demand overlay holds a "removed" marker for this request
465
- * (tryPrerenderLookup, cache-lookup.ts); gateOnDemandProducer reads it. On
465
+ * (readPrerenderSource, cache-lookup.ts); gateOnDemandProducer reads it. On
466
466
  * this context, not the request's: withCacheLookup documents that the
467
467
  * request ALS can be lost mid-pipeline on workerd.
468
468
  */
@@ -510,6 +510,17 @@ export type InternalHandlerContext<
510
510
  * request is a fill. See router/segment-resolution/prefetch-deferral.ts.
511
511
  */
512
512
  _prefetchDeferral?: import("../router/segment-resolution/prefetch-deferral.js").PrefetchDeferral;
513
+ /**
514
+ * @internal DSL loaders registered and not started, by `$$id`: a prefetch
515
+ * deferred them, or a pass that skips loaders (`skipLoaders`) did not run
516
+ * them. A reader's ctx.use() starts one through its registration
517
+ * (registerDeferredLoader, startDeferredLoader in
518
+ * router/segment-resolution/prefetch-deferral.ts).
519
+ */
520
+ _deferredLoaders?: Map<
521
+ string,
522
+ import("../router/segment-resolution/prefetch-deferral.js").DeferredLoader
523
+ >;
513
524
  /**
514
525
  * @internal Whether a loader was started in this request: a run of it (a
515
526
  * handler's or another loader's ctx.use()) or its cache() binding. A
@@ -165,7 +165,7 @@ export interface ResolvedSegment {
165
165
  loaderData?: any; // For loaders: the resolved data from loader execution
166
166
  /**
167
167
  * True when this loader was awaited before first flush
168
- * (loader(Def, { ssr: false })). Stamped by resolveLoaders (fresh.ts) on
168
+ * (loader(Def, { ssr: false })). Stamped by resolveOwnerLoaders on
169
169
  * document AND shell-capture renders — capture bakes flagged loaders and
170
170
  * awaits the same lane. Feeds segment-system's settled-value delivery and
171
171
  * the dev SSR suspension warning (ssr-suspension-warning.ts).