@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -867
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1133
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -5,15 +5,20 @@ import type {
5
5
  ResolvedSegment,
6
6
  } from "./types.js";
7
7
  import type { ReactNode } from "react";
8
+ import * as React from "react";
8
9
  import { startTransition } from "react";
10
+
11
+ // addTransitionType is only available in React experimental
12
+ const addTransitionType: ((type: string) => void) | undefined =
13
+ "addTransitionType" in React ? (React as any).addTransitionType : undefined;
9
14
  import type { RenderSegmentsOptions } from "../segment-system.js";
10
- import {
11
- mergeSegmentLoaders,
12
- needsLoaderMerge,
13
- insertMissingDiffSegments,
14
- } from "./merge-segment-loaders.js";
15
- import { assertSegmentStructure } from "./segment-structure-assert.js";
16
- import type { BoundTransaction } from "./navigation-bridge.js";
15
+ import { reconcileSegments } from "./segment-reconciler.js";
16
+ import type { ReconcileActor } from "./segment-reconciler.js";
17
+ import { hasActiveIntercept as hasActiveInterceptSlots } from "./intercept-utils.js";
18
+ import type { BoundTransaction } from "./navigation-transaction.js";
19
+ import { ServerRedirect } from "../errors.js";
20
+ import { debugLog } from "./logging.js";
21
+ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
17
22
 
18
23
  /**
19
24
  * Configuration for creating a partial updater
@@ -42,12 +47,30 @@ export interface CommitOverrides {
42
47
  intercept?: boolean;
43
48
  /** Source URL where intercept was triggered from */
44
49
  interceptSourceUrl?: string;
50
+ /** Server-set location state to merge into history.pushState */
51
+ serverState?: Record<string, unknown>;
45
52
  }
46
53
 
47
54
  /**
48
- * Commit context passed to partial updater for URL updates
49
- * Transaction encapsulates all store mutations for atomic commit
55
+ * Discriminated update mode for partial updates.
50
56
  */
57
+ export type UpdateMode =
58
+ | {
59
+ type: "navigate";
60
+ /** Cached segments for the target URL. When provided, these are used to build
61
+ * the segment map instead of the current page's segments. This ensures consistency
62
+ * when we send cached segment IDs to the server - if the server returns empty diff,
63
+ * we use the same segments we told the server we have. */
64
+ targetCacheSegments?: ResolvedSegment[];
65
+ /** Cached handle data for the target URL. When server returns empty diff and we're
66
+ * rendering from cache, this is passed to the UI to restore breadcrumbs etc. */
67
+ targetCacheHandleData?: Record<string, Record<string, unknown[]>>;
68
+ /** Source URL for intercept restore (popstate cache miss) */
69
+ interceptSourceUrl?: string;
70
+ }
71
+ | { type: "leave-intercept" }
72
+ | { type: "stale-revalidation"; interceptSourceUrl?: string }
73
+ | { type: "action"; interceptSourceUrl?: string };
51
74
 
52
75
  /**
53
76
  * Type for the fetchPartialUpdate function
@@ -57,24 +80,9 @@ export type PartialUpdater = (
57
80
  segmentIds: string[] | undefined,
58
81
  isRetry: boolean,
59
82
  signal: AbortSignal | undefined,
60
- type: BoundTransaction,
61
- options?: {
62
- isAction?: boolean;
63
- staleRevalidation?: boolean;
64
- interceptSourceUrl?: string;
65
- /** Cached segments for the target URL. When provided, these are used to build
66
- * the segment map instead of the current page's segments. This ensures consistency
67
- * when we send cached segment IDs to the server - if the server returns empty diff,
68
- * we use the same segments we told the server we have. */
69
- targetCacheSegments?: ResolvedSegment[];
70
- /** Cached handle data for the target URL. When server returns empty diff and we're
71
- * rendering from cache, this is passed to the UI to restore breadcrumbs etc. */
72
- targetCacheHandleData?: Record<string, Record<string, unknown[]>>;
73
- /** When true, we're leaving an intercept state - don't use current segment IDs
74
- * as fallback and force a fresh render from server */
75
- leavingIntercept?: boolean;
76
- },
77
- ) => Promise<Promise<void>>;
83
+ tx: BoundTransaction,
84
+ mode?: UpdateMode,
85
+ ) => Promise<void>;
78
86
 
79
87
  /**
80
88
  * Create a partial updater for fetching and applying RSC partial updates
@@ -84,18 +92,6 @@ export type PartialUpdater = (
84
92
  *
85
93
  * @param config - Partial update configuration
86
94
  * @returns fetchPartialUpdate function
87
- *
88
- * @example
89
- * ```typescript
90
- * const fetchPartialUpdate = createPartialUpdater({
91
- * store,
92
- * client,
93
- * onUpdate: (update) => store.emit(update),
94
- * renderSegments,
95
- * });
96
- *
97
- * await fetchPartialUpdate('/new-page');
98
- * ```
99
95
  */
100
96
  export function createPartialUpdater(
101
97
  config: PartialUpdateConfig,
@@ -103,20 +99,16 @@ export function createPartialUpdater(
103
99
  const { store, client, onUpdate, renderSegments, version } = config;
104
100
 
105
101
  /**
106
- * Build a lookup map from current page's cached segments
102
+ * Get current page's cached segments as an array
107
103
  */
108
- function getCurrentSegmentMap(): Map<string, ResolvedSegment> {
104
+ function getCurrentCachedSegments(): ResolvedSegment[] {
109
105
  const currentKey = store.getHistoryKey();
110
106
  const cached = store.getCachedSegments(currentKey);
111
- const cachedSegments = cached?.segments || [];
112
- const map = new Map<string, ResolvedSegment>();
113
- cachedSegments.forEach((s) => map.set(s.id, s));
114
- return map;
107
+ return cached?.segments || [];
115
108
  }
116
109
 
117
110
  /**
118
111
  * Fetch partial update and trigger UI update
119
- * Returns a promise that resolves when the RSC stream is fully consumed
120
112
  *
121
113
  * @param tx - Transaction for committing segment state (required)
122
114
  * @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
@@ -127,41 +119,34 @@ export function createPartialUpdater(
127
119
  isRetry: boolean,
128
120
  signal: AbortSignal | undefined,
129
121
  tx: BoundTransaction,
130
- options?: {
131
- isAction?: boolean;
132
- staleRevalidation?: boolean;
133
- interceptSourceUrl?: string;
134
- targetCacheSegments?: ResolvedSegment[];
135
- targetCacheHandleData?: Record<string, Record<string, unknown[]>>;
136
- leavingIntercept?: boolean;
137
- },
138
- ): Promise<Promise<void>> {
139
- const {
140
- isAction = false,
141
- staleRevalidation = false,
142
- interceptSourceUrl,
143
- targetCacheSegments,
144
- targetCacheHandleData,
145
- leavingIntercept = false,
146
- } = options || {};
122
+ mode: UpdateMode = { type: "navigate" },
123
+ ): Promise<void> {
147
124
  const segmentState = store.getSegmentState();
148
125
  const url = targetUrl || window.location.href;
149
126
 
150
127
  // Capture history key at start for stale revalidation consistency check
151
128
  const historyKeyAtStart = store.getHistoryKey();
152
129
 
153
- // When leaving intercept, don't send current segment IDs - we need fresh non-intercept segments
154
- // Filter out intercept-related segments (parallel slots like @modal) from current segments
130
+ // Derive interceptSourceUrl from modes that carry it
131
+ const interceptSourceUrl =
132
+ mode.type === "stale-revalidation" ||
133
+ mode.type === "action" ||
134
+ mode.type === "navigate"
135
+ ? mode.interceptSourceUrl
136
+ : undefined;
137
+
138
+ // When leaving intercept, filter out intercept-specific segments
155
139
  let segments: string[];
156
- if (leavingIntercept) {
157
- // When leaving intercept, only send segments that aren't intercept-specific
158
- // The server will return the non-intercept version of the route
140
+ if (mode.type === "leave-intercept") {
159
141
  const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
160
- // Filter out modal/intercept segments - keep only the base route segments
161
-
162
- // TODO: why this?
163
- segments = currentSegments.filter((id) => !id.includes(".@"));
164
- console.log(
142
+ const currentCached = getCurrentCachedSegments();
143
+ const interceptIds = new Set(
144
+ currentCached
145
+ .filter((s) => s.namespace?.startsWith("intercept:"))
146
+ .map((s) => s.id),
147
+ );
148
+ segments = currentSegments.filter((id) => !interceptIds.has(id));
149
+ debugLog(
165
150
  `[Browser] Leaving intercept - filtered segments: ${segments.join(", ")}`,
166
151
  );
167
152
  } else {
@@ -169,79 +154,92 @@ export function createPartialUpdater(
169
154
  }
170
155
 
171
156
  // For intercept revalidation, use the intercept source URL as previousUrl
172
- // This tells the server the route should be treated as an intercept
173
157
  const previousUrl =
174
158
  interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
175
159
 
176
- console.log(`\n[Browser] >>> NAVIGATION`);
177
- console.log(`[Browser] From: ${previousUrl}`);
178
- console.log(`[Browser] To: ${url}`);
179
- console.log(`[Browser] Segments to send: ${segments.join(", ")}`);
160
+ debugLog(`\n[Browser] >>> NAVIGATION`);
161
+ debugLog(`[Browser] From: ${previousUrl}`);
162
+ debugLog(`[Browser] To: ${url}`);
163
+ debugLog(`[Browser] Segments to send: ${segments.join(", ")}`);
180
164
  if (interceptSourceUrl) {
181
- console.log(`[Browser] Intercept context from: ${interceptSourceUrl}`);
165
+ debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
182
166
  }
183
167
 
184
- // Build segment map for merging with server diff.
185
- // When targetCacheSegments is provided (navigating to a cached route), use those
186
- // to ensure consistency - we use the same segments we told the server we have.
168
+ // Get cached segments for merging with server diff.
169
+ // When navigating with targetCacheSegments, use those for consistency.
187
170
  // Otherwise fall back to current page's segments (for same-route revalidation).
188
- let currentSegmentMap: Map<string, ResolvedSegment>;
189
- if (targetCacheSegments && targetCacheSegments.length > 0) {
190
- currentSegmentMap = new Map();
191
- targetCacheSegments.forEach((s) => currentSegmentMap.set(s.id, s));
192
- } else {
193
- currentSegmentMap = getCurrentSegmentMap();
194
- }
195
- // Mark navigation as streaming (response received, now parsing RSC)
196
- // The token is ended when the stream completes
197
- const streamingToken = tx.startStreaming();
171
+ const targetCache =
172
+ mode.type === "navigate" ? mode.targetCacheSegments : undefined;
173
+ const cachedSegs =
174
+ targetCache && targetCache.length > 0
175
+ ? targetCache
176
+ : getCurrentCachedSegments();
177
+
198
178
  // Fetch partial payload (no abort signal - RSC doesn't support it well)
199
- const { payload, streamComplete: rawStreamComplete } =
200
- await client.fetchPartial({
201
- targetUrl: url,
202
- segmentIds: segments,
203
- previousUrl,
204
- staleRevalidation,
205
- version,
206
- });
207
- console.log("payload.metadata", payload.metadata);
179
+ let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
180
+ fetchResult = await client.fetchPartial({
181
+ targetUrl: url,
182
+ segmentIds: segments,
183
+ previousUrl,
184
+ // Mark stale when explicitly requested OR when no segments are sent
185
+ // (action redirect sends empty segments for a fresh render).
186
+ staleRevalidation:
187
+ mode.type === "stale-revalidation" || segments.length === 0,
188
+ version,
189
+ });
190
+ // Mark navigation as streaming (response received, now parsing RSC).
191
+ // Called after fetchPartial so pendingUrl stays set during the network wait,
192
+ // allowing useLinkStatus to show per-link pending indicators.
193
+ const streamingToken = tx.startStreaming();
194
+ const { payload, streamComplete: rawStreamComplete } = fetchResult;
195
+ debugLog("payload.metadata", payload.metadata);
208
196
 
209
197
  const streamComplete = rawStreamComplete.then(() => {
210
198
  streamingToken.end();
211
199
  });
212
200
 
201
+ // Handle server-side redirect with state
202
+ if (payload.metadata?.redirect) {
203
+ if (signal?.aborted) {
204
+ debugLog("[Browser] Ignoring stale redirect (aborted)");
205
+ return;
206
+ }
207
+ const redirectUrl = validateRedirectOrigin(
208
+ payload.metadata.redirect.url,
209
+ window.location.origin,
210
+ );
211
+ if (!redirectUrl) {
212
+ debugLog("[Browser] Ignoring blocked redirect payload");
213
+ return;
214
+ }
215
+ const serverState = payload.metadata.locationState;
216
+ throw new ServerRedirect(redirectUrl, serverState);
217
+ }
218
+
213
219
  if (payload.metadata?.isPartial) {
214
220
  const { segments: newSegments, matched, diff } = payload.metadata;
215
221
 
216
222
  // Check if this navigation is stale (a newer one started)
217
223
  if (signal?.aborted) {
218
- console.log(`[Browser] Ignoring stale navigation (aborted)`);
219
- return streamComplete;
224
+ debugLog("[Browser] Ignoring stale navigation (aborted)");
225
+ return;
220
226
  }
221
227
 
222
- console.log(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
223
- console.log(`[Browser] Diff: ${diff?.join(", ")}`);
224
-
225
- // Create lookup for new segments from server
226
- const newSegmentMap = new Map<string, ResolvedSegment>();
227
- (newSegments || []).forEach((s: ResolvedSegment) =>
228
- newSegmentMap.set(s.id, s),
229
- );
228
+ debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
229
+ debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
230
230
 
231
231
  // If diff is empty, nothing changed on server side.
232
- // However, if we're navigating with targetCacheSegments (to a different route),
233
- // we still need to render those segments since the UI is showing the old route.
234
232
  if (!diff || diff.length === 0) {
235
233
  const matchedIds = matched || [];
234
+ const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
236
235
  const existingSegments = matchedIds
237
- .map((id: string) => currentSegmentMap.get(id))
236
+ .map((id: string) => cacheMap.get(id))
238
237
  .filter(Boolean) as ResolvedSegment[];
239
238
 
240
239
  // When navigating with cached segments to a different route, render them.
241
- // targetCacheSegments being provided means we're navigating to a cached route.
242
- if (targetCacheSegments && targetCacheSegments.length > 0) {
243
- console.log(
244
- `[Browser] No diff but navigating with cached segments - rendering target route`,
240
+ if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
241
+ debugLog(
242
+ "[Browser] No diff but navigating with cached segments - rendering target route",
245
243
  );
246
244
 
247
245
  const newTree = await renderSegments(existingSegments, {
@@ -252,29 +250,40 @@ export function createPartialUpdater(
252
250
 
253
251
  // Include cachedHandleData in metadata so NavigationProvider can restore
254
252
  // breadcrumbs and other handle data from cache.
255
- // IMPORTANT: Remove `handles` from metadata to prevent NavigationProvider from
253
+ // Remove `handles` from metadata to prevent NavigationProvider from
256
254
  // processing an empty handles stream, which would clear the cached breadcrumbs.
257
- // When rendering from cache with empty diff, we want to use cachedHandleData instead.
258
255
  const { handles: _unusedHandles, ...metadataWithoutHandles } =
259
256
  payload.metadata!;
260
- onUpdate({
257
+ const cachedUpdate = {
261
258
  root: newTree,
262
259
  metadata: {
263
260
  ...metadataWithoutHandles,
264
- cachedHandleData: targetCacheHandleData,
261
+ cachedHandleData: mode.targetCacheHandleData,
265
262
  },
266
- });
263
+ };
267
264
 
268
- console.log(`[Browser] Navigation complete (rendered from cache)\n`);
269
- return streamComplete;
265
+ const cachedHasTransition = existingSegments.some(
266
+ (s) => s.transition,
267
+ );
268
+ if (cachedHasTransition) {
269
+ startTransition(() => {
270
+ if (addTransitionType) {
271
+ addTransitionType("navigation");
272
+ }
273
+ onUpdate(cachedUpdate);
274
+ });
275
+ } else {
276
+ onUpdate(cachedUpdate);
277
+ }
278
+
279
+ debugLog("[Browser] Navigation complete (rendered from cache)");
280
+ return;
270
281
  }
271
282
 
272
283
  // When leaving intercept, force re-render even with empty diff
273
- // The matched segments are the non-intercept segments, which we need to render
274
- // to remove the modal from the UI
275
- if (leavingIntercept) {
276
- console.log(
277
- `[Browser] Leaving intercept - forcing re-render to remove modal`,
284
+ if (mode.type === "leave-intercept") {
285
+ debugLog(
286
+ "[Browser] Leaving intercept - forcing re-render to remove modal",
278
287
  );
279
288
 
280
289
  const newTree = await renderSegments(existingSegments, {
@@ -288,90 +297,39 @@ export function createPartialUpdater(
288
297
  metadata: payload.metadata,
289
298
  });
290
299
 
291
- console.log(`[Browser] Navigation complete (left intercept)\n`);
292
- return streamComplete;
300
+ debugLog("[Browser] Navigation complete (left intercept)");
301
+ return;
293
302
  }
294
303
 
295
304
  // Same route revalidation with no changes - skip UI update
296
- console.log(
297
- `[Browser] No changes - all revalidations returned false, keeping existing UI`,
305
+ debugLog(
306
+ "[Browser] No changes - all revalidations returned false, keeping existing UI",
298
307
  );
299
308
  tx.commit(matchedIds, existingSegments);
300
- console.log(`[Browser] Navigation complete (no re-render)\n`);
301
- return streamComplete;
309
+ debugLog("[Browser] Navigation complete (no re-render)");
310
+ return;
302
311
  }
303
312
 
304
- // Build full segment list by merging:
305
- // - New/changed segments from server response (diff)
306
- // - Unchanged segments from current page's cache
313
+ // Reconcile server segments with cached segments (single source of truth)
307
314
  const matchedIds = matched || [];
308
- console.log(`[Browser] matchedIds: ${matchedIds.join(", ")}`);
309
- console.log(
310
- `[Browser] currentSegmentMap keys: ${[...currentSegmentMap.keys()].join(", ")}`,
311
- );
312
- console.log(
313
- `[Browser] newSegmentMap keys: ${[...newSegmentMap.keys()].join(", ")}`,
314
- newSegmentMap,
315
- );
316
-
317
- // First pass: build segments from matched IDs
318
- const matchedIdSet = new Set(matchedIds);
319
- const allSegments = matchedIds
320
- .map((id: string) => {
321
- // First check server response (new/updated segments)
322
- const fromServer = newSegmentMap.get(id);
323
- if (fromServer) {
324
- // For partial revalidation (stale or action), merge server's new loader data
325
- // with cached loader data when server returns fewer loaders than cached
326
- const fromCache = currentSegmentMap.get(id);
327
- // Dev-mode assertion: warn if tree structure would change
328
- if (fromCache) {
329
- assertSegmentStructure(fromCache, fromServer, "partial-update");
330
- }
331
- if (
332
- (staleRevalidation || isAction) &&
333
- needsLoaderMerge(fromServer, fromCache)
334
- ) {
335
- return mergeSegmentLoaders(fromServer, fromCache);
336
- }
337
- // When server returns component: null for a layout segment, it means
338
- // "this segment doesn't need re-rendering" - preserve the cached component
339
- // to maintain the outlet chain and prevent React tree changes
340
- if (
341
- fromServer.component === null &&
342
- fromServer.type === "layout" &&
343
- fromCache?.component != null
344
- ) {
345
- console.log(
346
- `[Browser] Preserving cached component for layout ${id} (server returned null)`,
347
- );
348
- return { ...fromServer, component: fromCache.component };
349
- }
350
- return fromServer;
351
- }
352
- // Fall back to current page's cached segments
353
- const fromCache = currentSegmentMap.get(id);
354
- if (!fromCache) {
355
- console.warn(`[Browser] Missing segment: ${id}`);
356
- return fromCache;
357
- }
358
- // Clear loading for cached segments to prevent suspense - server decided
359
- // this segment doesn't need re-rendering, so show content as-is
360
- if (fromCache.loading !== undefined) {
361
- return { ...fromCache, loading: undefined };
362
- }
363
- return fromCache;
364
- })
365
- .filter(Boolean) as ResolvedSegment[];
366
-
367
- // Insert diff segments not in matchedIds (e.g., loader segments from consolidation fetch)
368
- insertMissingDiffSegments(allSegments, diff, matchedIdSet, newSegmentMap);
315
+ const actor: ReconcileActor =
316
+ mode.type === "stale-revalidation" || mode.type === "action"
317
+ ? "stale-revalidation"
318
+ : "navigation";
319
+
320
+ const reconciled = reconcileSegments({
321
+ actor,
322
+ matched: matchedIds,
323
+ diff: diff || [],
324
+ serverSegments: newSegments || [],
325
+ cachedSegments: cachedSegs,
326
+ insertMissingDiff: true,
327
+ });
369
328
 
370
329
  // HMR RESILIENCE: Check if we're missing any matched segments
371
- // Note: allSegments may include additional diff segments, so we check matchedIds specifically
372
- const allSegmentIdSet = new Set(allSegments.map((s) => s.id));
330
+ const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
373
331
  const missingIds = matchedIds.filter(
374
- (id: string) => !allSegmentIdSet.has(id),
332
+ (id: string) => !reconciledIdSet.has(id),
375
333
  );
376
334
 
377
335
  if (missingIds.length > 0) {
@@ -384,52 +342,39 @@ export function createPartialUpdater(
384
342
  );
385
343
  }
386
344
  if (signal?.aborted) {
387
- console.log(
388
- `[Browser] Ignoring stale navigation (aborted during HMR retry)`,
345
+ debugLog(
346
+ "[Browser] Ignoring stale navigation (aborted during HMR retry)",
389
347
  );
390
- return streamComplete;
348
+ return;
391
349
  }
392
- if (isAction) {
393
- return streamComplete;
350
+ if (mode.type === "action") {
351
+ return;
394
352
  }
395
353
  console.warn(
396
354
  `[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
397
355
  );
398
356
 
399
357
  // Refetch with empty segments = server sends everything
400
- return fetchPartialUpdate(url, [], true, signal, tx, { isAction });
358
+ return fetchPartialUpdate(url, [], true, signal, tx, mode);
401
359
  }
402
360
 
403
- // INTERCEPT HANDLING: Separate intercept segments for explicit injection
404
- // Intercept segments have namespace starting with "intercept:" or ID containing .@
405
- // This makes the flow clearer and easier to debug
406
- const isInterceptSegment = (s: ResolvedSegment) =>
407
- s.namespace?.startsWith("intercept:") ||
408
- (s.type === "parallel" && s.id.includes(".@"));
409
-
410
- const interceptSegments = allSegments.filter(isInterceptSegment);
411
- const mainSegments = allSegments.filter((s) => !isInterceptSegment(s));
412
-
413
361
  if (signal?.aborted) {
414
- console.log(
415
- `[Browser] Ignoring stale navigation (aborted before render)`,
416
- );
417
- return streamComplete;
362
+ debugLog("[Browser] Ignoring stale navigation (aborted before render)");
363
+ return;
418
364
  }
419
365
 
420
366
  // Rebuild tree on client (await for loader data resolution)
421
- // Race against abort signal to allow cancellation during loader awaiting
422
- // Pass intercept segments separately for explicit handling
423
- // For stale revalidation, use forceAwait to ensure no loading fallbacks
424
367
  const renderOptions = {
425
- isAction,
426
- forceAwait: staleRevalidation,
368
+ isAction: mode.type === "action",
369
+ forceAwait: mode.type === "stale-revalidation",
427
370
  interceptSegments:
428
- interceptSegments.length > 0 ? interceptSegments : undefined,
371
+ reconciled.interceptSegments.length > 0
372
+ ? reconciled.interceptSegments
373
+ : undefined,
429
374
  };
430
375
  const newTree = await (signal
431
376
  ? Promise.race([
432
- renderSegments(mainSegments, renderOptions),
377
+ renderSegments(reconciled.mainSegments, renderOptions),
433
378
  new Promise<never>((_, reject) => {
434
379
  if (signal.aborted) {
435
380
  reject(new DOMException("Navigation aborted", "AbortError"));
@@ -439,85 +384,79 @@ export function createPartialUpdater(
439
384
  });
440
385
  }),
441
386
  ])
442
- : renderSegments(mainSegments, renderOptions));
387
+ : renderSegments(reconciled.mainSegments, renderOptions));
443
388
 
444
389
  // Final abort check before committing - another navigation may have started
445
390
  if (signal?.aborted) {
446
- console.log(
447
- `[Browser] Ignoring stale navigation (aborted before commit)`,
448
- );
449
- return streamComplete;
391
+ debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
392
+ return;
450
393
  }
451
394
 
452
395
  // Check if this is an intercept response (any slot is active)
453
- // If so, disable scroll to keep the current scroll position
454
- const hasActiveIntercept = payload.metadata?.slots
455
- ? Object.values(payload.metadata.slots).some((slot) => slot.active)
456
- : false;
457
-
458
- // BUG FIX: When navigating with cached target segments but receiving an intercept response,
459
- // the background segments should come from the SOURCE page (where we navigated from),
460
- // not the TARGET cache. This happens when:
461
- // 1. User visits /product/xxx (detail page) - cached under key "/product/xxx"
462
- // 2. User navigates back to /
463
- // 3. User clicks product link → cache hit for "/product/xxx" (detail page)
464
- // 4. But server returns intercept response (modal with index background)
465
- // 5. Without this fix: background uses detail page segments (wrong!)
466
- // 6. With this fix: rebuild currentSegmentMap from source page
467
- if (hasActiveIntercept && targetCacheSegments) {
468
- console.log(
469
- `[Browser] Intercept response with target cache - rebuilding segment map from source page`,
470
- );
471
- currentSegmentMap = getCurrentSegmentMap();
472
- }
396
+ const isInterceptResponse = hasActiveInterceptSlots(
397
+ payload.metadata?.slots,
398
+ );
473
399
 
474
- // Track intercept context for action revalidation (only on navigation, not actions or stale revalidation)
475
- if (!isAction && !staleRevalidation) {
476
- if (hasActiveIntercept) {
477
- // Save the source URL for action revalidation to maintain intercept context
478
- store.setInterceptSourceUrl(segmentState.currentUrl);
400
+ // Track intercept context (only on navigation, not actions or stale revalidation)
401
+ // Use the authoritative source from mode/history state when restoring an
402
+ // intercept via popstate cache miss; fall back to the current URL for fresh
403
+ // intercept navigations.
404
+ const effectiveInterceptSource =
405
+ interceptSourceUrl || segmentState.currentUrl;
406
+ if (mode.type !== "action" && mode.type !== "stale-revalidation") {
407
+ if (isInterceptResponse) {
408
+ store.setInterceptSourceUrl(effectiveInterceptSource);
479
409
  } else {
480
- // Clear intercept context when navigating to a non-intercept route
481
410
  store.setInterceptSourceUrl(null);
482
411
  }
483
412
  }
484
413
 
485
414
  // Commit navigation - transaction handles all store mutations atomically
486
- // For intercept responses: disable scroll, mark as intercept, include source URL
487
- // Use allSegmentIds (derived from allSegments) instead of matchedIds because
488
- // we may have added diff segments (like loader segments) not in the matched array
489
- const allSegmentIds = allSegments.map((s) => s.id);
490
- tx.commit(
491
- allSegmentIds,
492
- allSegments,
493
- hasActiveIntercept
494
- ? {
495
- scroll: false,
496
- intercept: true,
497
- interceptSourceUrl: segmentState.currentUrl,
498
- }
499
- : undefined,
500
- );
415
+ const allSegmentIds = reconciled.segments.map((s) => s.id);
416
+ const serverLocationState = payload.metadata?.locationState;
417
+ const overrides: CommitOverrides | undefined = isInterceptResponse
418
+ ? {
419
+ scroll: false,
420
+ intercept: true,
421
+ interceptSourceUrl: effectiveInterceptSource,
422
+ ...(serverLocationState && { serverState: serverLocationState }),
423
+ }
424
+ : serverLocationState
425
+ ? { serverState: serverLocationState }
426
+ : undefined;
427
+ tx.commit(allSegmentIds, reconciled.segments, overrides);
501
428
 
502
429
  // For stale revalidation: verify history key hasn't changed before updating UI
503
- // If user navigated away, skip UI update to avoid corrupting current view
504
- if (staleRevalidation) {
430
+ if (mode.type === "stale-revalidation") {
505
431
  const historyKeyNow = store.getHistoryKey();
506
432
  if (historyKeyNow !== historyKeyAtStart) {
507
- console.log(
433
+ debugLog(
508
434
  `[Browser] Stale revalidation: history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
509
435
  );
510
- return streamComplete;
436
+ return;
511
437
  }
512
438
  }
513
439
 
514
- console.log("[partial-update] updating document");
440
+ debugLog("[partial-update] updating document");
515
441
 
516
442
  // Emit update to trigger React render
517
- // For stale revalidation: wait for stream to complete (loaders resolved), then update
518
- // For actions: wrap in startTransition to avoid UI flickering
519
- if (isAction || staleRevalidation) {
443
+ const hasTransition = reconciled.mainSegments.some((s) => s.transition);
444
+
445
+ if (mode.type === "action" || mode.type === "stale-revalidation") {
520
446
  startTransition(() => {
447
+ if (hasTransition && addTransitionType) {
448
+ addTransitionType("action");
449
+ }
450
+ onUpdate({
451
+ root: newTree,
452
+ metadata: payload.metadata!,
453
+ });
454
+ });
455
+ } else if (hasTransition) {
456
+ startTransition(() => {
457
+ if (addTransitionType) {
458
+ addTransitionType("navigation");
459
+ }
521
460
  onUpdate({
522
461
  root: newTree,
523
462
  metadata: payload.metadata!,
@@ -530,54 +469,65 @@ export function createPartialUpdater(
530
469
  });
531
470
  }
532
471
 
533
- console.log(`[Browser] Navigation complete\n`);
534
- return streamComplete;
472
+ debugLog("[Browser] Navigation complete");
473
+ return;
535
474
  } else {
536
475
  // Full update (fallback)
537
- // Use client-side renderSegments instead of payload.root to ensure
538
- // consistent component references with action revalidation.
539
- // Server-rendered RSC tree has different component references than
540
- // client-created tree, which causes React to remount LoaderBoundary
541
- // when actions trigger revalidation.
542
476
  console.warn(`[Browser] Full update (fallback)`);
543
477
 
544
478
  const segments = payload.metadata?.segments || [];
545
479
 
546
- // Check if this navigation is stale (a newer one started)
547
480
  if (signal?.aborted) {
548
- console.log(`[Browser] Ignoring stale navigation (aborted)`);
549
- return streamComplete;
481
+ debugLog("[Browser] Ignoring stale navigation (aborted)");
482
+ return;
550
483
  }
551
484
 
552
485
  const segmentIds = segments.map((s: ResolvedSegment) => s.id);
553
486
 
554
- // Render on client for consistent component references
555
487
  const newTree = await renderSegments(segments);
556
488
 
557
- // Final abort check before committing - another navigation may have started
558
489
  if (signal?.aborted) {
559
- console.log(
560
- `[Browser] Ignoring stale navigation (aborted before commit)`,
561
- );
562
- return streamComplete;
490
+ debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
491
+ return;
563
492
  }
564
493
 
565
- // Commit navigation - transaction handles all store mutations atomically
566
- tx.commit(segmentIds, segments);
494
+ const fullUpdateServerState = payload.metadata?.locationState;
495
+ if (fullUpdateServerState) {
496
+ tx.commit(segmentIds, segments, { serverState: fullUpdateServerState });
497
+ } else {
498
+ tx.commit(segmentIds, segments);
499
+ }
567
500
 
568
- // Emit update to trigger React render
569
- // For stale revalidation: wait for stream to complete, then update
570
- // For actions: wrap in startTransition to avoid UI flickering
571
- if (staleRevalidation) {
501
+ const fullHasTransition = segments.some(
502
+ (s: ResolvedSegment) => s.transition,
503
+ );
504
+
505
+ if (mode.type === "stale-revalidation") {
572
506
  await rawStreamComplete;
573
507
  startTransition(() => {
508
+ if (fullHasTransition && addTransitionType) {
509
+ addTransitionType("action");
510
+ }
574
511
  onUpdate({
575
512
  root: newTree,
576
513
  metadata: payload.metadata!,
577
514
  });
578
515
  });
579
- } else if (isAction) {
516
+ } else if (mode.type === "action") {
580
517
  startTransition(async () => {
518
+ if (fullHasTransition && addTransitionType) {
519
+ addTransitionType("action");
520
+ }
521
+ onUpdate({
522
+ root: newTree,
523
+ metadata: payload.metadata!,
524
+ });
525
+ });
526
+ } else if (fullHasTransition) {
527
+ startTransition(() => {
528
+ if (addTransitionType) {
529
+ addTransitionType("navigation");
530
+ }
581
531
  onUpdate({
582
532
  root: newTree,
583
533
  metadata: payload.metadata!,
@@ -590,7 +540,7 @@ export function createPartialUpdater(
590
540
  });
591
541
  }
592
542
 
593
- return streamComplete;
543
+ return;
594
544
  }
595
545
  }
596
546