@rangojs/router 0.26.6 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/action-coordinator.d.ts +0 -4
  4. package/dist/types/browser/action-fence.d.ts +11 -3
  5. package/dist/types/browser/event-controller.d.ts +20 -1
  6. package/dist/types/browser/history-state.d.ts +7 -9
  7. package/dist/types/browser/intercept-utils.d.ts +23 -0
  8. package/dist/types/browser/navigation-store.d.ts +8 -6
  9. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  10. package/dist/types/browser/rsc-router.d.ts +1 -1
  11. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  12. package/dist/types/browser/types.d.ts +2 -1
  13. package/dist/types/browser/wire-protocol.d.ts +25 -0
  14. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  15. package/dist/types/cache/cache-scope.d.ts +34 -22
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  18. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  19. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  20. package/dist/types/cache/segment-codec.d.ts +14 -0
  21. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  22. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  23. package/dist/types/cache/taint.d.ts +5 -3
  24. package/dist/types/cache/types.d.ts +7 -3
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  26. package/dist/types/prefetch-markers.d.ts +4 -6
  27. package/dist/types/prerender/store.d.ts +31 -0
  28. package/dist/types/prerender/warm-request.d.ts +3 -3
  29. package/dist/types/prerender.d.ts +2 -2
  30. package/dist/types/route-name.d.ts +5 -0
  31. package/dist/types/router/error-handling.d.ts +4 -4
  32. package/dist/types/router/instrument.d.ts +4 -3
  33. package/dist/types/router/intercept-resolution.d.ts +25 -9
  34. package/dist/types/router/match-context.d.ts +12 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +35 -87
  38. package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
  39. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  40. package/dist/types/router/prerender-match.d.ts +21 -6
  41. package/dist/types/router/preview-match.d.ts +2 -7
  42. package/dist/types/router/request-classification.d.ts +5 -0
  43. package/dist/types/router/revalidation.d.ts +6 -1
  44. package/dist/types/router/router-context.d.ts +9 -63
  45. package/dist/types/router/router-interfaces.d.ts +8 -14
  46. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  47. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  48. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  49. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution.d.ts +1 -2
  52. package/dist/types/router/segment-wrappers.d.ts +11 -48
  53. package/dist/types/router/telemetry.d.ts +2 -0
  54. package/dist/types/router/types.d.ts +10 -0
  55. package/dist/types/rsc/full-payload.d.ts +2 -2
  56. package/dist/types/rsc/handler-context.d.ts +6 -6
  57. package/dist/types/rsc/json-route-result.d.ts +5 -9
  58. package/dist/types/rsc/request-stages.d.ts +203 -0
  59. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  60. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  61. package/dist/types/rsc/routine-plan.d.ts +1 -1
  62. package/dist/types/rsc/server-action.d.ts +2 -2
  63. package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture.d.ts +132 -15
  65. package/dist/types/rsc/shell-serve.d.ts +37 -12
  66. package/dist/types/server/context.d.ts +9 -8
  67. package/dist/types/server/handle-store.d.ts +16 -0
  68. package/dist/types/server/render-pass.d.ts +72 -0
  69. package/dist/types/server/request-context.d.ts +24 -104
  70. package/dist/types/server/rewrite-trail.d.ts +2 -2
  71. package/dist/types/testing/dispatch.d.ts +35 -18
  72. package/dist/types/testing/serve-shell-request.d.ts +7 -0
  73. package/dist/types/types/cache-types.d.ts +6 -3
  74. package/dist/types/types/error-types.d.ts +9 -1
  75. package/dist/types/types/handler-context.d.ts +18 -7
  76. package/dist/types/types/segments.d.ts +1 -1
  77. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  78. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  79. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  80. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  81. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  82. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  83. package/dist/types/vite/discovery/state.d.ts +12 -0
  84. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  85. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  86. package/dist/types/vite/router-discovery.d.ts +8 -47
  87. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  88. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  89. package/dist/types/wire-params.d.ts +34 -0
  90. package/dist/vite/index.js +3809 -3710
  91. package/package.json +1 -1
  92. package/skills/cache-guide/SKILL.md +8 -5
  93. package/skills/caching/SKILL.md +37 -18
  94. package/skills/client-urls/SKILL.md +2 -1
  95. package/skills/document-cache/SKILL.md +18 -1
  96. package/skills/handler-use/SKILL.md +1 -1
  97. package/skills/intercept/SKILL.md +53 -13
  98. package/skills/layout/SKILL.md +11 -7
  99. package/skills/loader/SKILL.md +30 -13
  100. package/skills/middleware/SKILL.md +19 -1
  101. package/skills/migrate-nextjs/SKILL.md +5 -4
  102. package/skills/parallel/SKILL.md +5 -4
  103. package/skills/ppr/SKILL.md +19 -3
  104. package/skills/prerender/SKILL.md +39 -15
  105. package/skills/rango/SKILL.md +23 -11
  106. package/skills/server-actions/SKILL.md +51 -6
  107. package/skills/testing/predicates.md +1 -1
  108. package/skills/testing/response-routes.md +6 -4
  109. package/skills/testing/serve-shell-request.md +7 -7
  110. package/skills/use-cache/SKILL.md +46 -8
  111. package/skills/ux-architecture/SKILL.md +3 -2
  112. package/src/__internal.ts +7 -1
  113. package/src/browser/action-coordinator.ts +2 -13
  114. package/src/browser/action-fence.ts +11 -3
  115. package/src/browser/connection-warmup.ts +3 -1
  116. package/src/browser/event-controller.ts +42 -3
  117. package/src/browser/history-state.ts +15 -13
  118. package/src/browser/intercept-utils.ts +51 -0
  119. package/src/browser/navigation-bridge.ts +34 -30
  120. package/src/browser/navigation-client.ts +21 -33
  121. package/src/browser/navigation-store.ts +25 -27
  122. package/src/browser/navigation-transaction.ts +7 -11
  123. package/src/browser/partial-update.ts +8 -0
  124. package/src/browser/prefetch/cache.ts +2 -1
  125. package/src/browser/prefetch/fetch.ts +8 -16
  126. package/src/browser/prefetch/queue.ts +4 -4
  127. package/src/browser/react/NavigationProvider.tsx +2 -1
  128. package/src/browser/react/filter-segment-order.ts +2 -2
  129. package/src/browser/rsc-router.tsx +17 -2
  130. package/src/browser/segment-reconciler.ts +66 -53
  131. package/src/browser/server-action-bridge.ts +265 -127
  132. package/src/browser/types.ts +2 -1
  133. package/src/browser/wire-protocol.ts +41 -0
  134. package/src/cache/cache-key-utils.ts +11 -5
  135. package/src/cache/cache-runtime.ts +110 -50
  136. package/src/cache/cache-scope.ts +110 -64
  137. package/src/cache/cf/cf-cache-constants.ts +2 -2
  138. package/src/cache/cf/cf-cache-store.ts +486 -874
  139. package/src/cache/cf/cf-kv-utils.ts +1 -1
  140. package/src/cache/document-cache.ts +42 -15
  141. package/src/cache/handle-snapshot.ts +19 -7
  142. package/src/cache/segment-codec.ts +55 -0
  143. package/src/cache/shell-snapshot.ts +2 -2
  144. package/src/cache/tag-invalidation.ts +21 -2
  145. package/src/cache/taint.ts +5 -3
  146. package/src/cache/types.ts +7 -3
  147. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  148. package/src/handles/pushed-by.ts +1 -1
  149. package/src/prefetch-markers.ts +4 -7
  150. package/src/prerender/store.ts +76 -0
  151. package/src/prerender/warm-request.ts +8 -4
  152. package/src/prerender.ts +2 -2
  153. package/src/route-name.ts +8 -0
  154. package/src/router/error-handling.ts +4 -4
  155. package/src/router/instrument.ts +4 -3
  156. package/src/router/intercept-resolution.ts +77 -75
  157. package/src/router/loader-resolution.ts +5 -4
  158. package/src/router/match-api.ts +4 -5
  159. package/src/router/match-context.ts +33 -12
  160. package/src/router/match-handlers.ts +8 -20
  161. package/src/router/match-middleware/background-revalidation.ts +41 -53
  162. package/src/router/match-middleware/cache-lookup.ts +680 -720
  163. package/src/router/match-middleware/cache-store.ts +14 -12
  164. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  165. package/src/router/match-middleware/segment-resolution.ts +34 -61
  166. package/src/router/match-result.ts +1 -20
  167. package/src/router/middleware.ts +69 -11
  168. package/src/router/navigation-snapshot.ts +4 -3
  169. package/src/router/prerender-match.ts +100 -44
  170. package/src/router/preview-match.ts +6 -41
  171. package/src/router/request-classification.ts +13 -7
  172. package/src/router/revalidation.ts +33 -7
  173. package/src/router/route-snapshot.ts +2 -5
  174. package/src/router/router-context.ts +14 -117
  175. package/src/router/router-interfaces.ts +8 -14
  176. package/src/router/segment-resolution/loader-cache.ts +14 -9
  177. package/src/router/segment-resolution/loader-mask.ts +5 -5
  178. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  179. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  180. package/src/router/segment-resolution/revalidation.ts +897 -776
  181. package/src/router/segment-resolution.ts +4 -13
  182. package/src/router/segment-wrappers.ts +51 -269
  183. package/src/router/telemetry.ts +5 -0
  184. package/src/router/types.ts +10 -0
  185. package/src/router.ts +12 -13
  186. package/src/rsc/full-payload.ts +6 -5
  187. package/src/rsc/handler-context.ts +8 -14
  188. package/src/rsc/handler.ts +126 -507
  189. package/src/rsc/json-route-result.ts +5 -9
  190. package/src/rsc/loader-fetch.ts +6 -5
  191. package/src/rsc/progressive-enhancement.ts +5 -0
  192. package/src/rsc/request-stages.ts +757 -0
  193. package/src/rsc/response-cache-serve.ts +10 -14
  194. package/src/rsc/response-route-handler.ts +13 -5
  195. package/src/rsc/rewrite-passes.ts +2 -1
  196. package/src/rsc/routine-plan.ts +1 -1
  197. package/src/rsc/rsc-rendering.ts +172 -349
  198. package/src/rsc/server-action.ts +87 -113
  199. package/src/rsc/shell-capture-constants.ts +3 -1
  200. package/src/rsc/shell-capture.ts +212 -169
  201. package/src/rsc/shell-serve.ts +121 -18
  202. package/src/rsc/ssr-setup.ts +5 -4
  203. package/src/segment-system.tsx +3 -3
  204. package/src/server/context.ts +22 -25
  205. package/src/server/handle-store.ts +55 -0
  206. package/src/server/render-pass.ts +291 -0
  207. package/src/server/request-context.ts +26 -115
  208. package/src/server/rewrite-trail.ts +6 -4
  209. package/src/ssr/index.tsx +1 -1
  210. package/src/ssr/ssr-root.tsx +1 -0
  211. package/src/testing/dispatch.ts +223 -710
  212. package/src/testing/e2e/page-helpers.ts +2 -2
  213. package/src/testing/serve-shell-request.ts +37 -32
  214. package/src/types/cache-types.ts +6 -3
  215. package/src/types/error-types.ts +9 -1
  216. package/src/types/handler-context.ts +21 -7
  217. package/src/types/segments.ts +1 -1
  218. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  219. package/src/vite/discovery/build-env.ts +148 -0
  220. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  221. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  222. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  223. package/src/vite/discovery/discover-routers.ts +1 -1
  224. package/src/vite/discovery/discovery-errors.ts +1 -1
  225. package/src/vite/discovery/prerender-collection.ts +24 -39
  226. package/src/vite/discovery/state.ts +15 -0
  227. package/src/vite/plugins/version-plugin.ts +1 -1
  228. package/src/vite/plugins/virtual-entries.ts +1 -1
  229. package/src/vite/router-discovery.ts +29 -2428
  230. package/src/vite/temp-rsc-server.ts +223 -0
  231. package/src/vite/utils/prerender-utils.ts +47 -0
  232. package/src/wire-params.ts +35 -0
  233. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  234. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -38,6 +38,7 @@ import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../../segment-fragments.js";
38
38
  import { PREFETCH_HEADER } from "../../prefetch-markers.js";
39
39
  import type { RscPayload } from "../types.js";
40
40
  import { getRegisteredStore } from "../navigation-store-handle.js";
41
+ import { writePartialParams } from "../wire-protocol.js";
41
42
 
42
43
  /**
43
44
  * Decoder injected at app startup (see setPrefetchDecoder). This is
@@ -153,24 +154,15 @@ function buildPrefetchUrl(
153
154
  if (targetUrl.origin !== window.location.origin) {
154
155
  return null;
155
156
  }
156
- targetUrl.searchParams.set("_rsc_partial", "true");
157
- if (segmentIds.length > 0) {
158
- targetUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
159
- }
160
- if (version) {
161
- targetUrl.searchParams.set("_rsc_v", version);
162
- }
163
- if (routerId) {
164
- targetUrl.searchParams.set("_rsc_rid", routerId);
165
- }
166
157
  // The page's rewrite target, so the server diffs against the route that
167
158
  // rendered it, as for the navigation.
168
- const rewriteTarget =
169
- getRegisteredStore()?.getHistoryEntryMemory()?.rewriteTarget;
170
- if (rewriteTarget) {
171
- targetUrl.searchParams.set("_rsc_rt", rewriteTarget);
172
- }
173
- return targetUrl;
159
+ return writePartialParams(targetUrl, {
160
+ segmentIds,
161
+ emptySegments: "omit",
162
+ version,
163
+ routerId,
164
+ rewriteTarget: getRegisteredStore()?.getHistoryEntryMemory()?.rewriteTarget,
165
+ });
174
166
  }
175
167
 
176
168
  /**
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { wait, waitForIdle, waitForViewportImages } from "./resource-ready.js";
18
+ import { WIRE_PREFIX } from "../../wire-params.js";
18
19
 
19
20
  // Max prefetches executing at once. Mirrors DEFAULT_PREFETCH_CONCURRENCY
20
21
  // (router/prefetch-limits.ts); kept as a local literal so the client bundle
@@ -128,16 +129,15 @@ export function enqueuePrefetch(
128
129
 
129
130
  /**
130
131
  * Normalize a URL-like string for keep-alive matching: parse against a
131
- * placeholder origin and strip internal `_rsc_*` query params. Returns
132
+ * placeholder origin and strip every `WIRE_PREFIX` query param. Returns
132
133
  * `pathname + search` so comparisons ignore hash and the internal params
133
- * that prefetch appends to targets (`_rsc_partial`, `_rsc_segments`,
134
- * `_rsc_v`, `_rsc_rid`, `_rsc_stale`).
134
+ * that prefetch appends to targets.
135
135
  */
136
136
  function normalizeForMatch(urlish: string): string {
137
137
  try {
138
138
  const u = new URL(urlish, "http://placeholder");
139
139
  for (const k of [...u.searchParams.keys()]) {
140
- if (k.startsWith("_rsc_")) u.searchParams.delete(k);
140
+ if (k.startsWith(WIRE_PREFIX)) u.searchParams.delete(k);
141
141
  }
142
142
  return u.pathname + u.search;
143
143
  } catch {
@@ -78,7 +78,8 @@ async function processHandles(
78
78
 
79
79
  // This nav's instance token, captured before any await — processHandles runs
80
80
  // right after its own commit, so this is that commit's token. generateHistoryKey
81
- // is URL-only, so an A->B->A revisit reuses the key; the token lets a late
81
+ // is the URL (plus the source page for an intercept), so an A->B->A revisit
82
+ // reuses the key; the token lets a late
82
83
  // resolution tell its own visit apart from a newer same-URL visit, so a stale
83
84
  // nav can never clobber a fresher one's live state or cache (P1).
84
85
  const myInstance = store.getNavInstance();
@@ -33,8 +33,8 @@ export function loaderParentId(id: string): string {
33
33
  * 2. Place each parallel slot id (contains ".@") immediately after its
34
34
  * parent layout/route id. Raw segment-resolution emission order does NOT
35
35
  * guarantee this: route-mounted parallels are resolved/pushed BEFORE the
36
- * route handler's segment is appended (see fresh.ts:resolveSegment for
37
- * routes, and revalidation.ts ~915-919), so matched can read
36
+ * route handler's segment is appended (see resolveSegment in
37
+ * segment-resolution/revalidation.ts), so matched can read
38
38
  * `[..., R0.@panel, R0]`. collectHandleData consumes segmentOrder verbatim
39
39
  * with later-wins semantics, so without normalization the route handler's
40
40
  * Meta would override the slot's more-specific Meta — backwards.
@@ -42,6 +42,7 @@ import { setAppVersion } from "./app-version.js";
42
42
  import {
43
43
  isInterceptSegment,
44
44
  splitInterceptSegments,
45
+ wholeInterceptMatched,
45
46
  } from "./intercept-utils.js";
46
47
  import { createAppShellRef } from "./app-shell.js";
47
48
  import { bootLog, IS_BROWSER_DEBUG } from "./logging.js";
@@ -75,7 +76,7 @@ export interface InitBrowserAppOptions {
75
76
  storeOptions?: {
76
77
  /**
77
78
  * Maximum number of history entries to cache
78
- * @default 10
79
+ * @default 20
79
80
  */
80
81
  cacheSize?: number;
81
82
  };
@@ -531,6 +532,9 @@ export async function initBrowserApp(
531
532
  const streamingToken = handle.startStreaming();
532
533
 
533
534
  const interceptSourceUrl = store.getInterceptSourceUrl();
535
+ // Read before a version change below clears the history cache.
536
+ const onScreen =
537
+ store.getCachedSegments(store.getHistoryKey())?.segments ?? [];
534
538
 
535
539
  try {
536
540
  const { payload, streamComplete } = await client.fetchPartial({
@@ -582,7 +586,17 @@ export async function initBrowserApp(
582
586
  // a paramless document GET would run the SSR path and surface the
583
587
  // not-found page during that same transient.
584
588
  if (payload.metadata?.isPartial) {
585
- const segments = payload.metadata.segments || [];
589
+ // Held nothing: an intercept answer keeps the page under the slot
590
+ // from the entry on screen; the layouts and the slot are fresh.
591
+ let segments = payload.metadata.segments || [];
592
+ const kept = wholeInterceptMatched(onScreen, payload.metadata);
593
+ if (kept) {
594
+ payload.metadata.matched = kept;
595
+ const byId = new Map(
596
+ [...onScreen, ...segments].map((s) => [s.id, s]),
597
+ );
598
+ segments = kept.flatMap((id) => byId.get(id) ?? []);
599
+ }
586
600
  const matched = payload.metadata.matched || [];
587
601
 
588
602
  // Derive intercept state from the returned payload, not the
@@ -600,6 +614,7 @@ export async function initBrowserApp(
600
614
 
601
615
  const historyKey = generateHistoryKey(window.location.href, {
602
616
  intercept: responseIsIntercept,
617
+ interceptSource: interceptSourceUrl ?? undefined,
603
618
  });
604
619
  store.setHistoryKey(historyKey);
605
620
  const currentHandleData = liveHandleDataForCache(
@@ -46,6 +46,60 @@ export interface ReconcileResult {
46
46
  interceptSegments: ResolvedSegment[];
47
47
  }
48
48
 
49
+ /**
50
+ * A segment the server sent over its cached copy, keeping the cached
51
+ * structural properties so the React tree keeps its shape. Changing them
52
+ * between renders alters the element nesting (with/without
53
+ * RouteContentWrapper, MountContextProvider, etc.), which remounts components
54
+ * and destroys useActionState.
55
+ */
56
+ function mergeCached(
57
+ fromServer: ResolvedSegment,
58
+ fromCache: ResolvedSegment,
59
+ actor: ReconcileActor,
60
+ ): ResolvedSegment {
61
+ let merged = fromServer;
62
+
63
+ // When server returns component: null for a layout segment, it means
64
+ // "this segment doesn't need re-rendering" - preserve the cached component
65
+ // to maintain the outlet chain and prevent React tree changes
66
+ if (
67
+ fromServer.component === null &&
68
+ fromServer.type === "layout" &&
69
+ fromCache.component != null
70
+ ) {
71
+ merged = { ...merged, component: fromCache.component };
72
+ }
73
+
74
+ // Loading preservation is actor-aware:
75
+ // - action/stale-revalidation: always preserve cached value to prevent
76
+ // tree remount (even when cached is undefined, to avoid adding a
77
+ // Suspense boundary that wasn't there before)
78
+ // - navigation: only when cached is defined (building a new tree)
79
+ if (
80
+ fromServer.loading !== fromCache.loading &&
81
+ (actor !== "navigation" || fromCache.loading !== undefined)
82
+ ) {
83
+ merged = { ...merged, loading: fromCache.loading };
84
+ }
85
+
86
+ // mountPath: SSR segments may lack mountPath while revalidated segments
87
+ // include it. The conditional MountContextProvider wrapper changes tree depth.
88
+ if (fromServer.mountPath !== fromCache.mountPath) {
89
+ merged = { ...merged, mountPath: fromCache.mountPath };
90
+ }
91
+
92
+ // Dev-mode assertion: warn if the merged result still differs from cache
93
+ // in tree-structural properties. This catches bugs where the merge code
94
+ // above fails to preserve a value it should have.
95
+ assertSegmentStructure(
96
+ fromCache,
97
+ merged,
98
+ actor === "action" ? "action-bridge" : "partial-update",
99
+ );
100
+ return merged;
101
+ }
102
+
49
103
  /**
50
104
  * Single source of truth for merging server segments with cached segments.
51
105
  *
@@ -70,7 +124,6 @@ export interface ReconcileResult {
70
124
  */
71
125
  export function reconcileSegments(input: ReconcileInput): ReconcileResult {
72
126
  const { actor, matched, diff, insertMissingDiff } = input;
73
- const context = actor === "action" ? "action-bridge" : "partial-update";
74
127
 
75
128
  // Build lookup maps from arrays
76
129
  const serverSegments = new Map<string, ResolvedSegment>();
@@ -111,59 +164,13 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
111
164
 
112
165
  if (fromServer) {
113
166
  const inDiff = diffSet.has(segId);
114
- // Preserve cached structural properties to maintain consistent React tree.
115
- // Changing these between renders alters the element nesting
116
- // (with/without RouteContentWrapper, MountContextProvider, etc.),
117
- // causing React to remount components and destroy useActionState.
118
167
  if (fromCache) {
119
- let merged = fromServer;
120
-
121
- // When server returns component: null for a layout segment, it means
122
- // "this segment doesn't need re-rendering" - preserve the cached component
123
- // to maintain the outlet chain and prevent React tree changes
124
- if (
125
- fromServer.component === null &&
126
- fromServer.type === "layout" &&
127
- fromCache.component != null
128
- ) {
129
- merged = { ...merged, component: fromCache.component };
130
- }
131
-
132
- // Loading preservation is actor-aware:
133
- // - action/stale-revalidation: always preserve cached value to prevent
134
- // tree remount (even when cached is undefined, to avoid adding a
135
- // Suspense boundary that wasn't there before)
136
- // - navigation: only when cached is defined (building a new tree)
137
- if (actor !== "navigation") {
138
- if (fromServer.loading !== fromCache.loading) {
139
- merged = { ...merged, loading: fromCache.loading };
140
- }
141
- } else {
142
- if (
143
- fromCache.loading !== undefined &&
144
- fromServer.loading !== fromCache.loading
145
- ) {
146
- merged = { ...merged, loading: fromCache.loading };
147
- }
148
- }
149
-
150
- // mountPath: SSR segments may lack mountPath while revalidated segments
151
- // include it. The conditional MountContextProvider wrapper changes tree depth.
152
- if (fromServer.mountPath !== fromCache.mountPath) {
153
- merged = { ...merged, mountPath: fromCache.mountPath };
154
- }
155
-
156
- // Dev-mode assertion: warn if the merged result still differs from cache
157
- // in tree-structural properties. This catches bugs where the merge code
158
- // above fails to preserve a value it should have.
159
- assertSegmentStructure(fromCache, merged, context);
160
-
161
168
  if (IS_BROWSER_DEBUG) {
162
169
  debugLog(
163
170
  `[reconcile] ${segId}: SERVER+CACHE merge (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, component=${fromServer.component === null ? "null→cached" : "server"})`,
164
171
  );
165
172
  }
166
- return merged;
173
+ return mergeCached(fromServer, fromCache, actor);
167
174
  }
168
175
  if (IS_BROWSER_DEBUG) {
169
176
  debugLog(
@@ -221,11 +228,13 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
221
228
  }
222
229
 
223
230
  /**
224
- * Reconcile error segments with cached segments.
231
+ * Reconcile an error-boundary response with cached segments.
225
232
  *
226
- * For error responses, the server returns the error boundary segment.
227
- * This function overlays error segments onto the full cached tree,
228
- * preserving sibling layouts that aren't in the error parent chain.
233
+ * The server returns the error boundary segment and, above it, what the
234
+ * action's revalidation re-ran (rsc/server-action.ts). This function overlays
235
+ * them onto the full cached tree, preserving sibling layouts that aren't in
236
+ * the error parent chain. The error segment replaces its cached copy; every
237
+ * other segment merges as an action's does (mergeCached).
229
238
  */
230
239
  export function reconcileErrorSegments(
231
240
  cachedSegments: ResolvedSegment[],
@@ -236,7 +245,11 @@ export function reconcileErrorSegments(
236
245
 
237
246
  const segments = cachedSegments.map((cached) => {
238
247
  const fromServer = errorMap.get(cached.id);
239
- return fromServer || cached;
248
+ return !fromServer
249
+ ? cached
250
+ : fromServer.type === "error"
251
+ ? fromServer
252
+ : mergeCached(fromServer, cached, "action");
240
253
  });
241
254
 
242
255
  const { main, intercept } = splitInterceptSegments(segments);