@rangojs/router 0.0.0-experimental.002d056c

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