@rangojs/router 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ HistoryEntryMemory,
2
3
  NavigationLocation,
3
4
  SegmentState,
4
5
  NavigationStore,
@@ -13,10 +14,18 @@ import {
13
14
  getRangoState,
14
15
  getRangoStateCookieName,
15
16
  } from "./rango-state.js";
17
+ import { getHistoryStateKey } from "./scroll-restoration.js";
18
+ import { setCurrentHistoryStateListener } from "./react/location-state-shared.js";
16
19
 
17
20
  // Maximum number of history entries to cache (URLs visited)
18
21
  const HISTORY_CACHE_SIZE = 20;
19
22
 
23
+ // Bound on remembered history entries (transition({ when }) sources), matching
24
+ // scroll-restoration's MAX_SCROLL_ENTRIES: deep enough for any realistic
25
+ // back/forward run, bounded for long sessions. An evicted entry reads as
26
+ // unknown (undefined route name and state), never guessed.
27
+ const HISTORY_ENTRY_MEMORY_SIZE = 200;
28
+
20
29
  // Cache entry:
21
30
  // [url-key, segments, stale, handleData?, routerId?, navInstance?, handlesPending?]
22
31
  // stale=true means the data may be outdated and should be revalidated on access.
@@ -233,6 +242,11 @@ export function createNavigationStore(
233
242
  // Current history key (set on navigation, stored in history.state)
234
243
  let currentHistoryKey = config?.initialHistoryKey || generateHistoryKey();
235
244
 
245
+ // Per-history-entry memory (rememberDisplayedEntry), keyed by the entry's
246
+ // history.state.key — unique per entry, unlike the URL-derived history key.
247
+ const entryMemory = new Map<string, HistoryEntryMemory>();
248
+ let displayedEntryKey: string | undefined;
249
+
236
250
  // Store initial segments if provided (not stale)
237
251
  if (config?.initialHistoryKey && config?.initialSegments) {
238
252
  historyCache.push([
@@ -375,7 +389,7 @@ export function createNavigationStore(
375
389
  }
376
390
  }
377
391
 
378
- return {
392
+ const store: NavigationStore = {
379
393
  // ========================================================================
380
394
  // Internal Segment State (for bridges)
381
395
  // ========================================================================
@@ -426,6 +440,27 @@ export function createNavigationStore(
426
440
  currentHistoryKey = key;
427
441
  },
428
442
 
443
+ rememberDisplayedEntry(routeName?: string): void {
444
+ if (typeof window === "undefined") return;
445
+ const key = getHistoryStateKey();
446
+ const previous = entryMemory.get(key);
447
+ entryMemory.delete(key);
448
+ entryMemory.set(key, {
449
+ routeName: routeName ?? previous?.routeName,
450
+ state: window.history.state,
451
+ });
452
+ if (entryMemory.size > HISTORY_ENTRY_MEMORY_SIZE) {
453
+ const oldest = entryMemory.keys().next().value;
454
+ if (oldest !== undefined) entryMemory.delete(oldest);
455
+ }
456
+ displayedEntryKey = key;
457
+ },
458
+
459
+ getHistoryEntryMemory(entryKey?: string): HistoryEntryMemory | undefined {
460
+ const key = entryKey ?? displayedEntryKey;
461
+ return key === undefined ? undefined : entryMemory.get(key);
462
+ },
463
+
429
464
  /**
430
465
  * Current nav-instance token: the instance of the most recently committed
431
466
  * navigation (the value last written by cacheSegmentsForHistory). A late
@@ -682,4 +717,11 @@ export function createNavigationStore(
682
717
  });
683
718
  },
684
719
  };
720
+
721
+ // Writes to the displayed entry after its commit (Def.write()/delete(), a
722
+ // flash read, merged server state) refresh its memory.
723
+ if (typeof window !== "undefined") {
724
+ setCurrentHistoryStateListener(() => store.rememberDisplayedEntry());
725
+ }
726
+ return store;
685
727
  }
@@ -1,5 +1,5 @@
1
1
  import type {
2
- NavigateOptions,
2
+ NavigateOptionsInternal,
3
3
  NavigationStore,
4
4
  ResolvedSegment,
5
5
  StreamingToken,
@@ -13,21 +13,13 @@ import type { EventController, NavigationHandle } from "./event-controller.js";
13
13
  import { debugLog } from "./logging.js";
14
14
  import {
15
15
  buildHistoryState,
16
+ hasLocationState,
16
17
  mergeLocationState,
17
18
  pushHistoryWithIdx,
18
19
  } from "./history-state.js";
19
20
 
20
21
  export { resolveNavigationState } from "./history-state.js";
21
22
 
22
- /** Check if a history state object contains location state keys. */
23
- function hasLocationState(state: unknown): boolean {
24
- if (!state || typeof state !== "object") return false;
25
- return (
26
- "state" in state ||
27
- Object.keys(state).some((k) => k.startsWith("__rsc_ls_"))
28
- );
29
- }
30
-
31
23
  if (typeof Symbol.dispose === "undefined") {
32
24
  (Symbol as any).dispose = Symbol("Symbol.dispose");
33
25
  }
@@ -59,6 +51,14 @@ interface CommitOptions {
59
51
  * merged in. The page left was saved by handleTraversalStart.
60
52
  */
61
53
  traversal?: boolean;
54
+ /** The committed route's name (payload metadata), remembered per history entry. */
55
+ routeName?: string;
56
+ /**
57
+ * transition({ when }) decision already made at this navigation's first
58
+ * presentation (the optimistic clientUrls() swap). Not a commit input: the
59
+ * partial updater reuses it instead of deciding again.
60
+ */
61
+ transitionGatedOff?: boolean;
62
62
  }
63
63
 
64
64
  /**
@@ -79,6 +79,8 @@ interface BoundCommitOverrides {
79
79
  cacheOnly?: boolean;
80
80
  /** Server-set location state to merge into history.pushState */
81
81
  serverState?: Record<string, unknown>;
82
+ /** The committed route's name (payload metadata), remembered per history entry. */
83
+ routeName?: string;
82
84
  }
83
85
 
84
86
  /**
@@ -86,6 +88,14 @@ interface BoundCommitOverrides {
86
88
  */
87
89
  export interface BoundTransaction {
88
90
  readonly currentUrl: string;
91
+ /** Bound `replace` option (history replace instead of push). */
92
+ readonly replace?: boolean;
93
+ /** Bound `traversal` option: a back/forward navigation. */
94
+ readonly traversal?: boolean;
95
+ /** Bound user state (resolved), pushed with the entry at commit. */
96
+ readonly state?: unknown;
97
+ /** Bound `transitionGatedOff`: the decision made at first presentation. */
98
+ readonly transitionGatedOff?: boolean;
89
99
  /** Start streaming and get a token to end it when the stream completes */
90
100
  startStreaming(): StreamingToken;
91
101
  /** Commit the navigation. Returns the effective scroll option for the caller to handle. */
@@ -117,7 +127,7 @@ export function createNavigationTransaction(
117
127
  store: NavigationStore,
118
128
  eventController: EventController,
119
129
  url: string,
120
- options?: NavigateOptions & { skipLoadingState?: boolean },
130
+ options?: NavigateOptionsInternal & { skipLoadingState?: boolean },
121
131
  ): NavigationTransaction {
122
132
  let committed = false;
123
133
  const currentUrl = window.location.href;
@@ -168,6 +178,9 @@ export function createNavigationTransaction(
168
178
  store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
169
179
 
170
180
  if (storeOnly) {
181
+ // Same entry (an action refetch): refresh its state, keep its name
182
+ // unless the payload names it.
183
+ store.rememberDisplayedEntry(opts.routeName);
171
184
  debugLog("[Browser] Store updated (action)");
172
185
  handle.complete(parsedUrl);
173
186
  return { scroll: false };
@@ -177,6 +190,7 @@ export function createNavigationTransaction(
177
190
  if (serverState && Object.keys(serverState).length > 0) {
178
191
  mergeLocationState(serverState);
179
192
  }
193
+ store.rememberDisplayedEntry(opts.routeName);
180
194
  handle.complete(parsedUrl);
181
195
  debugLog("[Browser] Traversal committed, historyKey:", historyKey);
182
196
  return { scroll };
@@ -192,6 +206,7 @@ export function createNavigationTransaction(
192
206
 
193
207
  pushHistoryWithIdx(historyState, url, replace ?? false);
194
208
  ensureHistoryKey();
209
+ store.rememberDisplayedEntry(opts.routeName);
195
210
 
196
211
  if (hasLocationState(oldState) || hasLocationState(historyState)) {
197
212
  window.dispatchEvent(new Event("__rsc_locationstate"));
@@ -219,6 +234,10 @@ export function createNavigationTransaction(
219
234
  get currentUrl() {
220
235
  return currentUrl;
221
236
  },
237
+ replace: opts.replace,
238
+ traversal: opts.traversal,
239
+ state: opts.state,
240
+ transitionGatedOff: opts.transitionGatedOff,
222
241
  startStreaming() {
223
242
  return handle.startStreaming();
224
243
  },
@@ -249,6 +268,7 @@ export function createNavigationTransaction(
249
268
  interceptSourceUrl,
250
269
  cacheOnly,
251
270
  serverState,
271
+ routeName: overrides?.routeName ?? opts.routeName,
252
272
  });
253
273
  },
254
274
  };
@@ -36,6 +36,13 @@ import {
36
36
  collectClientRevalidationDecisions,
37
37
  setActiveInterceptTargets,
38
38
  } from "../client-urls/navigation.js";
39
+ import {
40
+ decideCommitGatedOff,
41
+ mergeSegmentParams,
42
+ type TransitionWhenActionInput,
43
+ } from "./transition-when.js";
44
+ import { buildHistoryState } from "./history-state.js";
45
+ import type { TransitionWhenKind } from "../types/segments.js";
39
46
 
40
47
  function toScrollPayload(
41
48
  scroll: boolean | undefined,
@@ -43,9 +50,16 @@ function toScrollPayload(
43
50
  return { enabled: scroll !== false ? scroll : false };
44
51
  }
45
52
 
53
+ /**
54
+ * Hold/animate decision for a commit: a non-intercept segment carries a
55
+ * transition, no intercept is presented, and this navigation's
56
+ * transition({ when }) decision did not gate it off.
57
+ */
46
58
  export function shouldStartViewTransition(
47
- segments: ResolvedSegment[],
59
+ segments: readonly ResolvedSegment[],
60
+ transitionGatedOff: boolean = false,
48
61
  ): boolean {
62
+ if (transitionGatedOff) return false;
49
63
  let hasIntercept = false;
50
64
  let hasTransition = false;
51
65
  for (const s of segments) {
@@ -107,6 +121,8 @@ export interface CommitOverrides {
107
121
  interceptSourceUrl?: string;
108
122
  /** Server-set location state to merge into history.pushState */
109
123
  serverState?: Record<string, unknown>;
124
+ /** The committed route's name (payload metadata), remembered per history entry. */
125
+ routeName?: string;
110
126
  }
111
127
 
112
128
  /**
@@ -132,10 +148,21 @@ export type UpdateMode =
132
148
  * boundaries do not animate the identical repaint.
133
149
  */
134
150
  optimisticPresented?: boolean;
151
+ /**
152
+ * A router.refresh() (or cross-tab refresh) of the current URL:
153
+ * transition({ when }) sees kind "revalidate", not "replace".
154
+ */
155
+ refresh?: boolean;
135
156
  }
136
157
  | { type: "leave-intercept"; interceptSourceUrl?: string }
137
158
  | { type: "stale-revalidation"; interceptSourceUrl?: string }
138
- | { type: "action"; interceptSourceUrl?: string; actionId?: string };
159
+ | {
160
+ type: "action";
161
+ interceptSourceUrl?: string;
162
+ actionId?: string;
163
+ /** The action's FormData and result, for transition({ when }) `action`. */
164
+ action?: TransitionWhenActionInput;
165
+ };
139
166
 
140
167
  /**
141
168
  * Type for the fetchPartialUpdate function
@@ -213,6 +240,54 @@ export function createPartialUpdater(
213
240
  }
214
241
  }
215
242
 
243
+ // transition({ when }). Every decision below runs before tx.commit()
244
+ // moves the store, so `from` is still the location being left:
245
+ // segmentState.currentUrl is the committed URL even on a popstate cache
246
+ // miss, where tx.currentUrl and window.location already point at the
247
+ // target.
248
+ const whenKind: TransitionWhenKind =
249
+ mode.type === "action"
250
+ ? "action"
251
+ : mode.type === "stale-revalidation" ||
252
+ (mode.type === "navigate" && mode.refresh)
253
+ ? "revalidate"
254
+ : tx.traversal
255
+ ? "pop"
256
+ : tx.replace
257
+ ? "replace"
258
+ : "push";
259
+ // Decide once per navigation (browser/transition-when.ts): reuse the
260
+ // decision the optimistic clientUrls() swap made; otherwise decide over
261
+ // every segment this commit presents, kept or re-sent (#989). The
262
+ // destination state is what the commit will leave on the entry.
263
+ const decideGatedOff = (
264
+ segmentsToCommit: readonly ResolvedSegment[],
265
+ metadata: {
266
+ params?: Record<string, string>;
267
+ routeName?: string;
268
+ locationState?: Record<string, unknown>;
269
+ },
270
+ ): boolean =>
271
+ tx.transitionGatedOff ??
272
+ // A stale revalidation the user navigated away from is discarded after
273
+ // the render (history-key check below): its predicate must not run.
274
+ ((mode.type === "stale-revalidation" &&
275
+ store.getHistoryKey() !== historyKeyAtStart) ||
276
+ decideCommitGatedOff(store, segmentsToCommit, whenKind, {
277
+ to: () => ({
278
+ url,
279
+ params: metadata.params ?? mergeSegmentParams(segmentsToCommit),
280
+ routeName: metadata.routeName,
281
+ state: tx.traversal
282
+ ? { ...window.history.state, ...metadata.locationState }
283
+ : buildHistoryState(tx.state, undefined, metadata.locationState),
284
+ }),
285
+ action:
286
+ mode.type === "action"
287
+ ? { ...mode.action, id: mode.actionId }
288
+ : undefined,
289
+ }));
290
+
216
291
  const targetCache =
217
292
  mode.type === "navigate" && mode.targetCacheSegments?.length
218
293
  ? mode.targetCacheSegments
@@ -349,13 +424,19 @@ export function createPartialUpdater(
349
424
  "[Browser] No diff but navigating with cached segments - rendering target route",
350
425
  );
351
426
 
427
+ const cachedGatedOff = decideGatedOff(
428
+ existingSegments,
429
+ payload.metadata,
430
+ );
352
431
  const newTree = await renderSegments(existingSegments, {
353
432
  forceAwait: true,
433
+ transitionGatedOff: cachedGatedOff,
354
434
  });
355
435
 
356
436
  const { scroll: commitScroll } = tx.commit(
357
437
  matchedIds,
358
438
  existingSegments,
439
+ { routeName: payload.metadata.routeName },
359
440
  );
360
441
 
361
442
  if (mode.targetCacheHandleData) {
@@ -376,7 +457,7 @@ export function createPartialUpdater(
376
457
  scroll: toScrollPayload(commitScroll),
377
458
  };
378
459
 
379
- if (shouldStartViewTransition(existingSegments)) {
460
+ if (shouldStartViewTransition(existingSegments, cachedGatedOff)) {
380
461
  commitInTransition(onUpdate, existingSegments, cachedUpdate, [
381
462
  "navigation",
382
463
  ]);
@@ -400,6 +481,7 @@ export function createPartialUpdater(
400
481
  const { scroll: leaveScroll } = tx.commit(
401
482
  matchedIds,
402
483
  existingSegments,
484
+ { routeName: payload.metadata.routeName },
403
485
  );
404
486
 
405
487
  onUpdate({
@@ -415,7 +497,9 @@ export function createPartialUpdater(
415
497
  debugLog(
416
498
  "[Browser] No changes - all revalidations returned false, keeping existing UI",
417
499
  );
418
- tx.commit(matchedIds, existingSegments);
500
+ tx.commit(matchedIds, existingSegments, {
501
+ routeName: payload.metadata.routeName,
502
+ });
419
503
  debugLog("[Browser] Navigation complete (no re-render)");
420
504
  return;
421
505
  }
@@ -477,7 +561,13 @@ export function createPartialUpdater(
477
561
  return;
478
562
  }
479
563
 
564
+ // Decide before the tree is built: on a gated-off commit every segment
565
+ // keeps its <ViewTransition> with "none" classes, so the decision must
566
+ // reach renderSegments. The response carries no decision, so a reused
567
+ // prefetch is decided against the real source.
568
+ const gatedOff = decideGatedOff(reconciled.segments, payload.metadata);
480
569
  const renderOptions = {
570
+ transitionGatedOff: gatedOff,
481
571
  isAction: mode.type === "action",
482
572
  // forceAwait unwraps the ROUTER loader promises during render so they
483
573
  // land without a loading()/fallback frame. A fully-prefetched nav has
@@ -557,16 +647,15 @@ export function createPartialUpdater(
557
647
 
558
648
  const allSegmentIds = matchedIds;
559
649
  const serverLocationState = payload.metadata?.locationState;
560
- const overrides: CommitOverrides | undefined = isInterceptResponse
561
- ? {
562
- scroll: false,
563
- intercept: true,
564
- interceptSourceUrl: effectiveInterceptSource,
565
- ...(serverLocationState && { serverState: serverLocationState }),
566
- }
567
- : serverLocationState
568
- ? { serverState: serverLocationState }
569
- : undefined;
650
+ const overrides: CommitOverrides = {
651
+ routeName: payload.metadata?.routeName,
652
+ ...(isInterceptResponse && {
653
+ scroll: false,
654
+ intercept: true,
655
+ interceptSourceUrl: effectiveInterceptSource,
656
+ }),
657
+ ...(serverLocationState && { serverState: serverLocationState }),
658
+ };
570
659
  const { scroll: navScroll } = tx.commit(
571
660
  allSegmentIds,
572
661
  reconciled.segments,
@@ -585,7 +674,10 @@ export function createPartialUpdater(
585
674
 
586
675
  debugLog("[partial-update] updating document");
587
676
 
588
- const hasTransition = shouldStartViewTransition(reconciled.segments);
677
+ const hasTransition = shouldStartViewTransition(
678
+ reconciled.segments,
679
+ gatedOff,
680
+ );
589
681
  const optimisticPresented =
590
682
  mode.type === "navigate" && mode.optimisticPresented === true;
591
683
  // [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. Reports which reconciled
@@ -600,6 +692,7 @@ export function createPartialUpdater(
600
692
  withTransition: reconciled.segments
601
693
  .filter((s) => s.transition)
602
694
  .map((s) => s.id),
695
+ transitionGatedOff: gatedOff,
603
696
  all: reconciled.segments.map((s) => s.id),
604
697
  });
605
698
  }
@@ -617,7 +710,10 @@ export function createPartialUpdater(
617
710
  metadata: payload.metadata!,
618
711
  scroll: scrollPayload,
619
712
  };
620
- if (mode.type === "action" || mode.type === "stale-revalidation") {
713
+ if (
714
+ !gatedOff &&
715
+ (mode.type === "action" || mode.type === "stale-revalidation")
716
+ ) {
621
717
  commitInTransition(
622
718
  onUpdate,
623
719
  reconciled.mainSegments,
@@ -633,7 +729,10 @@ export function createPartialUpdater(
633
729
  ? ["navigation", OPTIMISTIC_COMMIT_TRANSITION_TYPE]
634
730
  : ["navigation"],
635
731
  );
636
- } else if (fullyPrefetched || isSameStructureNav || optimisticPresented) {
732
+ } else if (
733
+ !gatedOff &&
734
+ (fullyPrefetched || isSameStructureNav || optimisticPresented)
735
+ ) {
637
736
  // Content-hold commit, two triggers. Fully-prefetched nav: the payload
638
737
  // is fully resolved (forceAwait above), so the transition commits
639
738
  // synchronously — no fallback flash. Same-structure nav: the re-run
@@ -663,10 +762,14 @@ export function createPartialUpdater(
663
762
  optimisticPresented ? [OPTIMISTIC_COMMIT_TRANSITION_TYPE] : [],
664
763
  );
665
764
  } else {
666
- // Cold/partially-prefetched nav that mounts NEW segments: normal
667
- // commit so fallbacks stream like a first load and the click has
668
- // visible feedback. Explicit transition() routes keep the
669
- // content-hold via the hasTransition branch above (the opt-in).
765
+ // Cold/partially-prefetched nav that mounts NEW segments, or any
766
+ // commit transition({ when }) gated off (navigation, action refetch,
767
+ // stale revalidation): normal commit so fallbacks stream
768
+ // like a first load and the click has visible feedback. A gated-off
769
+ // segment keeps its key, so its loading() fallback comes from its
770
+ // boundary re-suspending on this urgent commit, not from a remount
771
+ // (#995). Explicit transition() routes keep the content-hold via the
772
+ // hasTransition branch above (the opt-in).
670
773
  onUpdate(update);
671
774
  }
672
775
 
@@ -684,7 +787,10 @@ export function createPartialUpdater(
684
787
 
685
788
  const segmentIds = segments.map((s: ResolvedSegment) => s.id);
686
789
 
687
- const newTree = await renderSegments(segments);
790
+ const fullGatedOff = decideGatedOff(segments, payload.metadata ?? {});
791
+ const newTree = await renderSegments(segments, {
792
+ transitionGatedOff: fullGatedOff,
793
+ });
688
794
 
689
795
  if (signal?.aborted) {
690
796
  debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
@@ -692,13 +798,15 @@ export function createPartialUpdater(
692
798
  }
693
799
 
694
800
  const fullUpdateServerState = payload.metadata?.locationState;
695
- const { scroll: fullScroll } = fullUpdateServerState
696
- ? tx.commit(segmentIds, segments, {
697
- serverState: fullUpdateServerState,
698
- })
699
- : tx.commit(segmentIds, segments);
801
+ const { scroll: fullScroll } = tx.commit(segmentIds, segments, {
802
+ routeName: payload.metadata?.routeName,
803
+ ...(fullUpdateServerState && { serverState: fullUpdateServerState }),
804
+ });
700
805
 
701
- const fullHasTransition = shouldStartViewTransition(segments);
806
+ const fullHasTransition = shouldStartViewTransition(
807
+ segments,
808
+ fullGatedOff,
809
+ );
702
810
  const fullScrollPayload = toScrollPayload(fullScroll);
703
811
  const fullUpdate: NavigationUpdate = {
704
812
  root: newTree,
@@ -720,13 +828,16 @@ export function createPartialUpdater(
720
828
  );
721
829
  return;
722
830
  }
723
- commitInTransition(
724
- onUpdate,
725
- segments,
726
- fullUpdate,
727
- fullHasTransition ? ["action"] : [],
728
- );
729
- } else if (mode.type === "action") {
831
+ if (fullGatedOff) onUpdate(fullUpdate);
832
+ else {
833
+ commitInTransition(
834
+ onUpdate,
835
+ segments,
836
+ fullUpdate,
837
+ fullHasTransition ? ["action"] : [],
838
+ );
839
+ }
840
+ } else if (mode.type === "action" && !fullGatedOff) {
730
841
  commitInTransition(
731
842
  onUpdate,
732
843
  segments,
@@ -15,11 +15,11 @@
15
15
  * - Source-scoped: built by `buildSourceKey(rangoState, sourceHref, target)`
16
16
  * — shape `rangoState\0sourceHref\0/target?...`. Embeds the Rango state
17
17
  * (so rotation invalidates source-scoped entries too), the source href, and
18
- * the exact source segment tree. Populated when the
19
- * server tags a response with `X-RSC-Prefetch-Scope: source` (intercept
20
- * modals etc.), OR when a Link opts in with `prefetchKey=":source"` — in
21
- * both cases so source-sensitive responses cannot bleed into navigations
22
- * from other pages.
18
+ * the exact source segment tree. Populated when the server tags a
19
+ * response with `X-RSC-Prefetch-Scope: source` (a route an intercept
20
+ * targets, matched or not), OR when a Link opts in with
21
+ * `prefetchKey=":source"` — in both cases so source-sensitive responses
22
+ * cannot bleed into navigations from other pages.
23
23
  *
24
24
  * Also tracks in-flight prefetch promises. Each promise resolves to the
25
25
  * decoded prefetch entry (or null), letting navigation adopt a
@@ -172,7 +172,7 @@ let generation = 0;
172
172
  * rotation invalidates source-scoped entries alongside wildcard ones,
173
173
  * plus the source page href so the key is unique per originating page.
174
174
  * Populated either when the server tags a response with
175
- * `X-RSC-Prefetch-Scope: source` (intercept modals, etc.) or when a
175
+ * `X-RSC-Prefetch-Scope: source` (a route an intercept targets) or when a
176
176
  * Link opts in via `prefetchKey=":source"`.
177
177
  *
178
178
  * Source-scoped keys retain `_rsc_segments` because their exact source tree is
@@ -189,8 +189,8 @@ function buildPrefetchUrl(
189
189
  * - Otherwise: dual inflight registration under both `wildcardKey` and
190
190
  * `sourceKey` so same-source navigations adopt directly via their own
191
191
  * source key. Storage key is chosen at response time from the
192
- * `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (intercept
193
- * modals etc.), anything else → `wildcardKey`. The entry records its scope
192
+ * `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (a route an
193
+ * intercept targets), anything else → `wildcardKey`. The entry records its scope
194
194
  * so cross-source navigations that adopted via `wildcardKey` can bail out
195
195
  * in `navigation-client.ts` when the adopted entry turns out source-scoped.
196
196
  */