@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2

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 (160) hide show
  1. package/{CLAUDE.md → AGENTS.md} +4 -0
  2. package/README.md +122 -30
  3. package/dist/bin/rango.js +245 -63
  4. package/dist/vite/index.js +859 -418
  5. package/package.json +3 -3
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +49 -8
  9. package/skills/document-cache/SKILL.md +2 -2
  10. package/skills/hooks/SKILL.md +33 -31
  11. package/skills/host-router/SKILL.md +218 -0
  12. package/skills/links/SKILL.md +3 -1
  13. package/skills/loader/SKILL.md +72 -22
  14. package/skills/middleware/SKILL.md +2 -0
  15. package/skills/parallel/SKILL.md +126 -0
  16. package/skills/prerender/SKILL.md +112 -70
  17. package/skills/rango/SKILL.md +0 -1
  18. package/skills/route/SKILL.md +34 -4
  19. package/skills/router-setup/SKILL.md +95 -5
  20. package/skills/typesafety/SKILL.md +35 -23
  21. package/src/__internal.ts +92 -0
  22. package/src/bin/rango.ts +18 -0
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/link-interceptor.ts +4 -0
  26. package/src/browser/navigation-bridge.ts +114 -18
  27. package/src/browser/navigation-client.ts +126 -44
  28. package/src/browser/navigation-store.ts +43 -8
  29. package/src/browser/navigation-transaction.ts +11 -9
  30. package/src/browser/partial-update.ts +80 -15
  31. package/src/browser/prefetch/cache.ts +166 -27
  32. package/src/browser/prefetch/fetch.ts +52 -39
  33. package/src/browser/prefetch/policy.ts +6 -0
  34. package/src/browser/prefetch/queue.ts +92 -20
  35. package/src/browser/prefetch/resource-ready.ts +77 -0
  36. package/src/browser/react/Link.tsx +70 -14
  37. package/src/browser/react/NavigationProvider.tsx +40 -4
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +143 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +6 -1
  44. package/src/browser/server-action-bridge.ts +454 -436
  45. package/src/browser/types.ts +60 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +5 -0
  48. package/src/build/route-trie.ts +19 -3
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +346 -87
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.rsc.tsx +2 -1
  60. package/src/client.tsx +3 -102
  61. package/src/context-var.ts +72 -2
  62. package/src/debug.ts +2 -2
  63. package/src/handle.ts +40 -0
  64. package/src/handles/breadcrumbs.ts +66 -0
  65. package/src/handles/index.ts +1 -0
  66. package/src/host/index.ts +0 -3
  67. package/src/index.rsc.ts +8 -37
  68. package/src/index.ts +40 -66
  69. package/src/prerender/store.ts +57 -15
  70. package/src/prerender.ts +138 -77
  71. package/src/reverse.ts +22 -1
  72. package/src/route-definition/dsl-helpers.ts +73 -25
  73. package/src/route-definition/helpers-types.ts +10 -6
  74. package/src/route-definition/index.ts +3 -3
  75. package/src/route-definition/redirect.ts +11 -3
  76. package/src/route-definition/resolve-handler-use.ts +149 -0
  77. package/src/route-map-builder.ts +7 -1
  78. package/src/route-types.ts +11 -0
  79. package/src/router/content-negotiation.ts +100 -1
  80. package/src/router/find-match.ts +4 -2
  81. package/src/router/handler-context.ts +108 -25
  82. package/src/router/intercept-resolution.ts +11 -4
  83. package/src/router/lazy-includes.ts +4 -1
  84. package/src/router/loader-resolution.ts +123 -11
  85. package/src/router/logging.ts +5 -2
  86. package/src/router/manifest.ts +9 -3
  87. package/src/router/match-api.ts +125 -190
  88. package/src/router/match-middleware/background-revalidation.ts +30 -2
  89. package/src/router/match-middleware/cache-lookup.ts +88 -16
  90. package/src/router/match-middleware/cache-store.ts +53 -10
  91. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  92. package/src/router/match-middleware/segment-resolution.ts +61 -5
  93. package/src/router/match-result.ts +22 -15
  94. package/src/router/metrics.ts +238 -13
  95. package/src/router/middleware-types.ts +53 -12
  96. package/src/router/middleware.ts +172 -85
  97. package/src/router/navigation-snapshot.ts +182 -0
  98. package/src/router/pattern-matching.ts +20 -5
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/revalidation.ts +27 -7
  103. package/src/router/route-snapshot.ts +245 -0
  104. package/src/router/router-context.ts +6 -1
  105. package/src/router/router-interfaces.ts +50 -5
  106. package/src/router/router-options.ts +50 -19
  107. package/src/router/segment-resolution/fresh.ts +200 -19
  108. package/src/router/segment-resolution/helpers.ts +30 -25
  109. package/src/router/segment-resolution/loader-cache.ts +1 -0
  110. package/src/router/segment-resolution/revalidation.ts +429 -301
  111. package/src/router/segment-wrappers.ts +2 -0
  112. package/src/router/trie-matching.ts +20 -2
  113. package/src/router/types.ts +1 -0
  114. package/src/router.ts +88 -15
  115. package/src/rsc/handler.ts +546 -359
  116. package/src/rsc/index.ts +0 -20
  117. package/src/rsc/manifest-init.ts +5 -1
  118. package/src/rsc/progressive-enhancement.ts +25 -8
  119. package/src/rsc/rsc-rendering.ts +35 -43
  120. package/src/rsc/server-action.ts +16 -10
  121. package/src/rsc/ssr-setup.ts +128 -0
  122. package/src/rsc/types.ts +10 -1
  123. package/src/search-params.ts +16 -13
  124. package/src/segment-system.tsx +140 -4
  125. package/src/server/context.ts +148 -16
  126. package/src/server/loader-registry.ts +9 -8
  127. package/src/server/request-context.ts +182 -34
  128. package/src/server.ts +6 -0
  129. package/src/ssr/index.tsx +4 -0
  130. package/src/static-handler.ts +18 -6
  131. package/src/theme/index.ts +4 -13
  132. package/src/types/cache-types.ts +4 -4
  133. package/src/types/handler-context.ts +149 -49
  134. package/src/types/loader-types.ts +36 -9
  135. package/src/types/route-config.ts +17 -8
  136. package/src/types/route-entry.ts +8 -1
  137. package/src/types/segments.ts +2 -5
  138. package/src/urls/path-helper-types.ts +9 -2
  139. package/src/urls/path-helper.ts +48 -13
  140. package/src/urls/pattern-types.ts +12 -0
  141. package/src/urls/response-types.ts +16 -6
  142. package/src/use-loader.tsx +73 -4
  143. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  144. package/src/vite/discovery/discover-routers.ts +23 -5
  145. package/src/vite/discovery/prerender-collection.ts +48 -15
  146. package/src/vite/discovery/state.ts +17 -13
  147. package/src/vite/index.ts +8 -3
  148. package/src/vite/plugin-types.ts +51 -79
  149. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  150. package/src/vite/plugins/expose-action-id.ts +1 -3
  151. package/src/vite/plugins/performance-tracks.ts +88 -0
  152. package/src/vite/plugins/refresh-cmd.ts +127 -0
  153. package/src/vite/plugins/version-plugin.ts +13 -1
  154. package/src/vite/rango.ts +174 -211
  155. package/src/vite/router-discovery.ts +169 -42
  156. package/src/vite/utils/banner.ts +3 -3
  157. package/src/vite/utils/prerender-utils.ts +78 -0
  158. package/src/vite/utils/shared-utils.ts +3 -2
  159. package/skills/testing/SKILL.md +0 -226
  160. package/src/route-definition/route-function.ts +0 -119
@@ -281,7 +281,7 @@ import type { RouteSearchParams, RouteParams } from "@rangojs/router";
281
281
 
282
282
  // RouteSearchParams<"name"> resolves the search schema to a typed object
283
283
  type SP = RouteSearchParams<"search">;
284
- // { q: string; page?: number; sort?: string }
284
+ // { q: string | undefined; page?: number; sort?: string }
285
285
 
286
286
  // RouteParams<"name"> resolves URL params from the route pattern
287
287
  type P = RouteParams<"blogPost">;
@@ -334,7 +334,7 @@ export const ProductLoader = createLoader(async (ctx) => {
334
334
  });
335
335
 
336
336
  // In server component - type is inferred
337
- import { useLoader } from "@rangojs/router";
337
+ import { useLoader } from "@rangojs/router/client";
338
338
 
339
339
  async function ProductPage() {
340
340
  const product = await useLoader(ProductLoader);
@@ -369,8 +369,18 @@ interface PaginationData {
369
369
  perPage: number;
370
370
  }
371
371
  export const Pagination = createVar<PaginationData>();
372
+
373
+ // Non-cacheable var — reading inside cache() or "use cache" throws at runtime
374
+ const Session = createVar<SessionData>({ cache: false });
372
375
  ```
373
376
 
377
+ `createVar` accepts an optional options object. The `cache` option (default
378
+ `true`) controls whether the var's values can be read inside cache scopes.
379
+ Write-level escalation is also supported: `ctx.set(Var, value, { cache: false })`
380
+ marks a specific write as non-cacheable even if the var itself is cacheable.
381
+ "Least cacheable wins" — if either says `cache: false`, the value throws on
382
+ read inside `cache()` or `"use cache"`.
383
+
374
384
  ### Producer (handler or middleware)
375
385
 
376
386
  ```typescript
@@ -414,26 +424,30 @@ Both approaches coexist: `ctx.get("user")` (global via Vars) and
414
424
  Handles have typed data:
415
425
 
416
426
  ```typescript
417
- // handles/breadcrumbs.ts
418
- import { createHandle } from "@rangojs/router";
419
-
420
- // All export patterns work: export const, const + export { X }, export { X as Y }
421
- export const Breadcrumbs = createHandle<{ label: string; href: string }>();
422
-
423
- // In route definition - use handle() DSL
424
- import { urls } from "@rangojs/router";
425
-
426
- export const urlpatterns = urls(({ path, handle }) => [
427
- path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
428
- handle(Breadcrumbs, { label: "Products", href: "/shop/products" }),
429
- ]),
430
- ]);
431
-
432
- // In client - typed array
427
+ // Built-in Breadcrumbs handle — import from "@rangojs/router"
428
+ import { Breadcrumbs } from "@rangojs/router";
429
+ // Type: Handle<BreadcrumbItem, BreadcrumbItem[]>
430
+ // BreadcrumbItem: { label: string; href: string; content?: ReactNode | Promise<ReactNode> }
431
+
432
+ // In route handler — push is fully typed
433
+ path("/shop/product/:slug", (ctx) => {
434
+ const breadcrumb = ctx.use(Breadcrumbs);
435
+ breadcrumb({ label: "Products", href: "/shop/products" });
436
+ return <ProductPage />;
437
+ }, { name: "product" });
438
+
439
+ // In client — typed array
440
+ import { useHandle, Breadcrumbs } from "@rangojs/router/client";
433
441
  function BreadcrumbNav() {
434
442
  const crumbs = useHandle(Breadcrumbs);
435
- // crumbs: Array<{ label: string; href: string }>
443
+ // crumbs: BreadcrumbItem[]
436
444
  }
445
+
446
+ // Custom handles also work the same way
447
+ import { createHandle } from "@rangojs/router";
448
+ export const PageTitle = createHandle<string, string>(
449
+ (segments) => segments.flat().at(-1) ?? "Default Title"
450
+ );
437
451
  ```
438
452
 
439
453
  ## Ref Prop Type Safety (Loaders & Handles)
@@ -447,14 +461,12 @@ export const ProductLoader = createLoader(async (ctx) => {
447
461
  return { product: await fetchProduct(ctx.params.slug) };
448
462
  });
449
463
 
450
- // handles.ts
451
- export const Breadcrumbs = createHandle<{ label: string; href: string }>();
464
+ // Built-in Breadcrumbs — or any custom handle created with createHandle()
452
465
 
453
466
  // Client component — typeof infers all generics
454
467
  ("use client");
455
- import { useLoader, useHandle } from "@rangojs/router/client";
468
+ import { useLoader, useHandle, type Breadcrumbs } from "@rangojs/router/client";
456
469
  import type { ProductLoader } from "../loaders";
457
- import type { Breadcrumbs } from "../handles";
458
470
 
459
471
  function MyComponent({
460
472
  loader,
package/src/__internal.ts CHANGED
@@ -164,6 +164,98 @@ export type {
164
164
  */
165
165
  export type { InternalHandlerContext } from "./types.js";
166
166
 
167
+ // ============================================================================
168
+ // Rendering (Internal)
169
+ // ============================================================================
170
+
171
+ /**
172
+ * @internal
173
+ * Builds React element trees from route segments.
174
+ */
175
+ export { renderSegments } from "./segment-system.js";
176
+
177
+ // ============================================================================
178
+ // Error Utilities (Internal)
179
+ // ============================================================================
180
+
181
+ /**
182
+ * @internal
183
+ * Error sanitization and network error utilities.
184
+ */
185
+ export { sanitizeError, NetworkError, isNetworkError } from "./errors.js";
186
+
187
+ // ============================================================================
188
+ // Type Utilities (Internal)
189
+ // ============================================================================
190
+
191
+ /**
192
+ * @internal
193
+ * Scoped view of GeneratedRouteMap for Handler<"localName", ScopedRouteMap<"prefix">>.
194
+ */
195
+ export type { ScopedRouteMap } from "./types.js";
196
+
197
+ /**
198
+ * @internal
199
+ * Type-level utilities for reverse URL generation.
200
+ */
201
+ export type { MergeRoutes, SanitizePrefix } from "./reverse.js";
202
+
203
+ /**
204
+ * @internal
205
+ * Individual telemetry event types.
206
+ */
207
+ export type {
208
+ RequestStartEvent,
209
+ RequestEndEvent,
210
+ RequestErrorEvent,
211
+ RequestTimeoutEvent,
212
+ LoaderStartEvent,
213
+ LoaderEndEvent,
214
+ LoaderErrorEvent,
215
+ HandlerErrorEvent,
216
+ CacheDecisionEvent,
217
+ RevalidationDecisionEvent,
218
+ } from "./router/telemetry.js";
219
+
220
+ // ============================================================================
221
+ // Pre-render / Static Handler Guards (Internal)
222
+ // ============================================================================
223
+
224
+ /**
225
+ * @internal
226
+ * Type guard for prerender handler definitions.
227
+ */
228
+ export { isPrerenderHandler, isPassthroughHandler } from "./prerender.js";
229
+
230
+ /**
231
+ * @internal
232
+ * Type guard for static handler definitions.
233
+ */
234
+ export { isStaticHandler } from "./static-handler.js";
235
+
236
+ // ============================================================================
237
+ // URL Pattern Internals
238
+ // ============================================================================
239
+
240
+ /**
241
+ * @internal
242
+ * Sentinel used to tag response-type route entries.
243
+ */
244
+ export { RESPONSE_TYPE } from "./urls.js";
245
+
246
+ // ============================================================================
247
+ // Route Match Debug (Internal)
248
+ // ============================================================================
249
+
250
+ /**
251
+ * @internal
252
+ * Debug utilities for route matching performance analysis.
253
+ */
254
+ export {
255
+ enableMatchDebug,
256
+ getMatchDebugStats,
257
+ } from "./router/pattern-matching.js";
258
+
167
259
  // ============================================================================
168
260
  // Debug Utilities (Internal)
169
261
  // ============================================================================
package/src/bin/rango.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  writeCombinedRouteTypes,
7
7
  detectUnresolvableIncludes,
8
8
  detectUnresolvableIncludesForUrlsFile,
9
+ findNestedRouterConflict,
10
+ formatNestedRouterConflictError,
9
11
  type UnresolvableInclude,
10
12
  } from "../build/generate-route-types.ts";
11
13
 
@@ -205,6 +207,14 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
205
207
  console.warn("");
206
208
  }
207
209
 
210
+ const nestedRouterConflict = findNestedRouterConflict(routerFiles);
211
+ if (nestedRouterConflict) {
212
+ console.error(
213
+ `\n${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}\n`,
214
+ );
215
+ process.exit(1);
216
+ }
217
+
208
218
  // Phase 3: Write all outputs (only reached if diagnostics pass or --static)
209
219
  for (const urlsFile of urlsFiles) {
210
220
  writePerModuleRouteTypesForFile(urlsFile);
@@ -259,6 +269,14 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
259
269
  process.exit(1);
260
270
  }
261
271
 
272
+ const nestedRouterConflict = findNestedRouterConflict(routerEntries);
273
+ if (nestedRouterConflict) {
274
+ console.error(
275
+ `\n${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}\n`,
276
+ );
277
+ process.exit(1);
278
+ }
279
+
262
280
  let discoverAndWriteRouteTypes: typeof import("../build/runtime-discovery.ts").discoverAndWriteRouteTypes;
263
281
  try {
264
282
  const mod = await import("../build/runtime-discovery.ts");
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Mutable app version — updated after HMR revalidation.
3
+ * Read by prefetch, navigation, and context code.
4
+ */
5
+
6
+ let currentVersion: string | undefined;
7
+
8
+ export function getAppVersion(): string | undefined {
9
+ return currentVersion;
10
+ }
11
+
12
+ export function setAppVersion(version: string | undefined): void {
13
+ currentVersion = version;
14
+ }
@@ -79,6 +79,8 @@ export interface DerivedNavigationState {
79
79
  state: "idle" | "loading";
80
80
  /** Whether any operation is streaming */
81
81
  isStreaming: boolean;
82
+ /** Whether a navigation is active (fetching or streaming, before commit) */
83
+ isNavigating: boolean;
82
84
  /** Current committed location */
83
85
  location: NavigationLocation;
84
86
  /** URL being navigated to (null if idle) */
@@ -389,6 +391,9 @@ export function createEventController(
389
391
  return {
390
392
  state,
391
393
  isStreaming,
394
+ // True when a navigation is active (fetching or streaming, before
395
+ // commit). Broader than pendingUrl which clears during streaming.
396
+ isNavigating: currentNavigation !== null,
392
397
  location,
393
398
  // pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
394
399
  // Background revalidations (skipLoadingState) don't expose a pending URL.
@@ -117,6 +117,7 @@ export function setupLinkInterception(
117
117
  // Read navigation options from data attributes (set by Link component)
118
118
  const scrollAttr = link.getAttribute("data-scroll");
119
119
  const replaceAttr = link.getAttribute("data-replace");
120
+ const revalidateAttr = link.getAttribute("data-revalidate");
120
121
 
121
122
  const navigateOptions: NavigateOptions = {};
122
123
  if (scrollAttr === "false") {
@@ -125,6 +126,9 @@ export function setupLinkInterception(
125
126
  if (replaceAttr === "true") {
126
127
  navigateOptions.replace = true;
127
128
  }
129
+ if (revalidateAttr === "false") {
130
+ navigateOptions.revalidate = false;
131
+ }
128
132
 
129
133
  onNavigate(href, navigateOptions);
130
134
  };
@@ -4,12 +4,19 @@ import type {
4
4
  NavigateOptionsInternal,
5
5
  ResolvedSegment,
6
6
  } from "./types.js";
7
+ import { setAppVersion } from "./app-version.js";
7
8
  import * as React from "react";
8
9
  import { startTransition } from "react";
9
10
  import {
10
11
  createNavigationTransaction,
11
12
  resolveNavigationState,
12
13
  } from "./navigation-transaction.js";
14
+ import { buildHistoryState } from "./history-state.js";
15
+ import {
16
+ handleNavigationStart,
17
+ handleNavigationEnd,
18
+ ensureHistoryKey,
19
+ } from "./scroll-restoration.js";
13
20
 
14
21
  // addTransitionType is only available in React experimental
15
22
  const addTransitionType: ((type: string) => void) | undefined =
@@ -18,7 +25,6 @@ const addTransitionType: ((type: string) => void) | undefined =
18
25
  import { setupLinkInterception } from "./link-interceptor.js";
19
26
  import { createPartialUpdater } from "./partial-update.js";
20
27
  import { generateHistoryKey } from "./navigation-store.js";
21
- import { handleNavigationEnd } from "./scroll-restoration.js";
22
28
  import type { EventController } from "./event-controller.js";
23
29
  import { isInterceptOnlyCache } from "./intercept-utils.js";
24
30
  import {
@@ -35,11 +41,6 @@ if (typeof Symbol.dispose === "undefined") {
35
41
  (Symbol as any).dispose = Symbol("Symbol.dispose");
36
42
  }
37
43
 
38
- /** Get IDs of non-loader segments (layouts, routes, parallels). */
39
- function getNonLoaderSegmentIds(segments: ResolvedSegment[]): string[] {
40
- return segments.filter((s) => s.type !== "loader").map((s) => s.id);
41
- }
42
-
43
44
  export { createNavigationTransaction };
44
45
 
45
46
  /**
@@ -67,8 +68,8 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
67
68
  export function createNavigationBridge(
68
69
  config: NavigationBridgeConfigWithController,
69
70
  ): NavigationBridge {
70
- const { store, client, eventController, onUpdate, renderSegments, version } =
71
- config;
71
+ const { store, client, eventController, onUpdate, renderSegments } = config;
72
+ let version = config.version;
72
73
 
73
74
  // Create shared partial updater
74
75
  const fetchPartialUpdate = createPartialUpdater({
@@ -76,7 +77,7 @@ export function createNavigationBridge(
76
77
  client,
77
78
  onUpdate,
78
79
  renderSegments,
79
- version,
80
+ getVersion: () => version,
80
81
  });
81
82
 
82
83
  return {
@@ -114,6 +115,85 @@ export function createNavigationBridge(
114
115
  return;
115
116
  }
116
117
 
118
+ // Shallow navigation: skip RSC fetch when revalidate is false
119
+ // and the pathname hasn't changed (search param / hash only change).
120
+ if (
121
+ options?.revalidate === false &&
122
+ targetUrl.pathname === new URL(window.location.href).pathname
123
+ ) {
124
+ // Preserve intercept context from the current history entry so that
125
+ // popstate uses the correct cache key (:intercept suffix) and restores
126
+ // the right full-page vs modal semantics.
127
+ const currentHistoryState = window.history.state;
128
+ const isIntercept = currentHistoryState?.intercept === true;
129
+ const interceptSourceUrl = isIntercept
130
+ ? currentHistoryState?.sourceUrl
131
+ : undefined;
132
+
133
+ const historyKey = generateHistoryKey(url, { intercept: isIntercept });
134
+
135
+ // Copy current segments to the new history key so back/forward restores instantly
136
+ const currentKey = store.getHistoryKey();
137
+ const currentCache = store.getCachedSegments(currentKey);
138
+ if (currentCache?.segments) {
139
+ const currentHandleData = eventController.getHandleState().data;
140
+ store.cacheSegmentsForHistory(
141
+ historyKey,
142
+ currentCache.segments,
143
+ currentHandleData,
144
+ );
145
+ }
146
+
147
+ // Save current scroll position before changing URL
148
+ handleNavigationStart();
149
+
150
+ // Snapshot old state before pushState/replaceState overwrites it
151
+ const oldState = window.history.state;
152
+
153
+ // Update browser URL (carry intercept context into history state)
154
+ const historyState = buildHistoryState(
155
+ resolvedState,
156
+ {
157
+ intercept: isIntercept || undefined,
158
+ sourceUrl: interceptSourceUrl,
159
+ },
160
+ {},
161
+ );
162
+ if (options.replace) {
163
+ window.history.replaceState(historyState, "", url);
164
+ } else {
165
+ window.history.pushState(historyState, "", url);
166
+ }
167
+
168
+ // Ensure new history entry has a scroll restoration key
169
+ ensureHistoryKey();
170
+
171
+ // Notify useLocationState() hooks when state changes
172
+ const hasOldState =
173
+ oldState &&
174
+ typeof oldState === "object" &&
175
+ ("state" in oldState ||
176
+ Object.keys(oldState).some((k) => k.startsWith("__rsc_ls_")));
177
+ const hasNewState =
178
+ historyState &&
179
+ ("state" in historyState ||
180
+ Object.keys(historyState).some((k) => k.startsWith("__rsc_ls_")));
181
+ if (hasOldState || hasNewState) {
182
+ window.dispatchEvent(new Event("__rsc_locationstate"));
183
+ }
184
+
185
+ // Update store history key so future navigations reference the right cache
186
+ store.setHistoryKey(historyKey);
187
+ store.setCurrentUrl(url);
188
+
189
+ // Notify hooks — location updates, state stays idle
190
+ eventController.setLocation(targetUrl);
191
+
192
+ // Handle post-navigation scroll
193
+ handleNavigationEnd({ scroll: options.scroll });
194
+ return;
195
+ }
196
+
117
197
  // Only abort pending requests when navigating to a different route
118
198
  // Same-route navigation (e.g., /todos -> /todos) should not cancel in-flight actions
119
199
  const currentPath = new URL(window.location.href).pathname;
@@ -189,7 +269,7 @@ export function createNavigationBridge(
189
269
  !isLeavingIntercept &&
190
270
  !options?._skipCache;
191
271
 
192
- using tx = createNavigationTransaction(store, eventController, url, {
272
+ const tx = createNavigationTransaction(store, eventController, url, {
193
273
  ...options,
194
274
  state: resolvedState,
195
275
  skipLoadingState: hasUsableCache,
@@ -200,7 +280,7 @@ export function createNavigationBridge(
200
280
  await fetchPartialUpdate(
201
281
  url,
202
282
  hasUsableCache
203
- ? getNonLoaderSegmentIds(cachedSegments!)
283
+ ? cachedSegments!.map((s) => s.id)
204
284
  : options?._skipCache
205
285
  ? [] // Action redirect: send no segments so server renders everything fresh
206
286
  : undefined,
@@ -224,7 +304,7 @@ export function createNavigationBridge(
224
304
  );
225
305
  } catch (error) {
226
306
  // Server-side redirect with location state: the current transaction's
227
- // `using` cleanup resets loading state. Re-navigate to the redirect
307
+ // cleanup resets loading state. Re-navigate to the redirect
228
308
  // target carrying the server-set state into history.pushState.
229
309
  if (error instanceof ServerRedirect) {
230
310
  const redirectUrl = validateRedirectOrigin(
@@ -260,6 +340,8 @@ export function createNavigationBridge(
260
340
  }
261
341
 
262
342
  throw error;
343
+ } finally {
344
+ tx[Symbol.dispose]();
263
345
  }
264
346
  },
265
347
 
@@ -269,7 +351,7 @@ export function createNavigationBridge(
269
351
  async refresh(): Promise<void> {
270
352
  eventController.abortNavigation();
271
353
 
272
- using tx = createNavigationTransaction(
354
+ const tx = createNavigationTransaction(
273
355
  store,
274
356
  eventController,
275
357
  window.location.href,
@@ -299,6 +381,8 @@ export function createNavigationBridge(
299
381
  return;
300
382
  }
301
383
  throw error;
384
+ } finally {
385
+ tx[Symbol.dispose]();
302
386
  }
303
387
  },
304
388
 
@@ -364,6 +448,12 @@ export function createNavigationBridge(
364
448
  store.setCurrentUrl(url);
365
449
  store.setPath(new URL(url).pathname);
366
450
 
451
+ // Restore router identity from cache so subsequent navigations
452
+ // don't falsely detect an app switch.
453
+ if (cached?.routerId) {
454
+ store.setRouterId?.(cached.routerId);
455
+ }
456
+
367
457
  // Render from cache - force await to skip loading fallbacks
368
458
  try {
369
459
  const root = await renderSegments(cachedSegments, {
@@ -389,6 +479,7 @@ export function createNavigationBridge(
389
479
  cachedHandleData,
390
480
  params: cachedParams,
391
481
  },
482
+ scroll: { restore: true, isStreaming },
392
483
  };
393
484
  const hasTransition = cachedSegments.some((s) => s.transition);
394
485
  if (hasTransition) {
@@ -402,14 +493,11 @@ export function createNavigationBridge(
402
493
  onUpdate(popstateUpdate);
403
494
  }
404
495
 
405
- // Restore scroll position for back/forward navigation
406
- handleNavigationEnd({ restore: true, isStreaming });
407
-
408
496
  // SWR: If stale, trigger background revalidation
409
497
  if (isStale) {
410
498
  debugLog("[Browser] Cache is stale, background revalidating...");
411
499
  // Background revalidation - don't await, just fire and forget
412
- const segmentIds = getNonLoaderSegmentIds(cachedSegments);
500
+ const segmentIds = cachedSegments.map((s) => s.id);
413
501
 
414
502
  const tx = createNavigationTransaction(
415
503
  store,
@@ -457,7 +545,7 @@ export function createNavigationBridge(
457
545
  }
458
546
 
459
547
  // Fetch if not cached
460
- using tx = createNavigationTransaction(store, eventController, url, {
548
+ const tx = createNavigationTransaction(store, eventController, url, {
461
549
  replace: true,
462
550
  });
463
551
 
@@ -498,6 +586,8 @@ export function createNavigationBridge(
498
586
  }
499
587
 
500
588
  throw error;
589
+ } finally {
590
+ tx[Symbol.dispose]();
501
591
  }
502
592
  },
503
593
 
@@ -549,6 +639,12 @@ export function createNavigationBridge(
549
639
  window.removeEventListener("pageshow", handlePageShow);
550
640
  };
551
641
  },
642
+
643
+ updateVersion(newVersion: string): void {
644
+ version = newVersion;
645
+ setAppVersion(newVersion);
646
+ store.clearHistoryCache();
647
+ },
552
648
  };
553
649
  }
554
650