@rangojs/router 0.21.0 → 0.22.1

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/bin/rango.js +13 -1
  2. package/dist/types/browser/event-controller.d.ts +30 -13
  3. package/dist/types/browser/navigation-store.d.ts +9 -0
  4. package/dist/types/browser/partial-update.d.ts +2 -1
  5. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  6. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  7. package/dist/types/browser/react/context.d.ts +14 -0
  8. package/dist/types/browser/rsc-router.d.ts +13 -2
  9. package/dist/types/browser/types.d.ts +10 -0
  10. package/dist/types/build/generate-manifest.d.ts +2 -0
  11. package/dist/types/cache/background-task.d.ts +22 -0
  12. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  13. package/dist/types/cache/cache-policy.d.ts +17 -0
  14. package/dist/types/cache/cache-scope.d.ts +1 -1
  15. package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
  16. package/dist/types/cache/document-cache.d.ts +8 -5
  17. package/dist/types/cache/handle-snapshot.d.ts +23 -0
  18. package/dist/types/cache/index.d.ts +1 -1
  19. package/dist/types/cache/invalidation-order.d.ts +14 -0
  20. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  21. package/dist/types/cache/shell-snapshot.d.ts +10 -1
  22. package/dist/types/cache/store-scope.d.ts +22 -0
  23. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  24. package/dist/types/cache/types.d.ts +63 -14
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
  26. package/dist/types/errors.d.ts +6 -0
  27. package/dist/types/handles/deferred-resolution.d.ts +7 -0
  28. package/dist/types/internal-suspense-audit.d.ts +1 -0
  29. package/dist/types/loader-store.d.ts +4 -8
  30. package/dist/types/outlet-provider.d.ts +2 -1
  31. package/dist/types/prerender/cloudflare.d.ts +49 -0
  32. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  33. package/dist/types/prerender/index.d.ts +12 -0
  34. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  35. package/dist/types/prerender/on-demand.d.ts +272 -0
  36. package/dist/types/prerender/producer-guard.d.ts +33 -0
  37. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  38. package/dist/types/prerender/warm-request.d.ts +65 -0
  39. package/dist/types/prerender/warm.d.ts +65 -0
  40. package/dist/types/prerender/writable-store.d.ts +170 -0
  41. package/dist/types/prerender.d.ts +27 -0
  42. package/dist/types/reverse.d.ts +1 -1
  43. package/dist/types/route-content-wrapper.d.ts +3 -1
  44. package/dist/types/router/error-handling.d.ts +2 -0
  45. package/dist/types/router/handler-context.d.ts +1 -1
  46. package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
  47. package/dist/types/router/pattern-matching.d.ts +2 -0
  48. package/dist/types/router/prefetch-default.d.ts +4 -4
  49. package/dist/types/router/prerender-match.d.ts +23 -1
  50. package/dist/types/router/route-trie-builder.d.ts +8 -1
  51. package/dist/types/router/router-context.d.ts +1 -0
  52. package/dist/types/router/router-interfaces.d.ts +30 -0
  53. package/dist/types/router/router-options.d.ts +24 -0
  54. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
  56. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  57. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  58. package/dist/types/router/trie-matching.d.ts +2 -0
  59. package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
  60. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  61. package/dist/types/rsc/shell-capture.d.ts +14 -1
  62. package/dist/types/rsc/shell-serve.d.ts +2 -2
  63. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  64. package/dist/types/rsc/types.d.ts +8 -0
  65. package/dist/types/segment-boundary-content.d.ts +17 -0
  66. package/dist/types/segment-loader-promise.d.ts +2 -6
  67. package/dist/types/server/build-version-table.d.ts +9 -0
  68. package/dist/types/server/context.d.ts +12 -4
  69. package/dist/types/server/handle-store.d.ts +2 -0
  70. package/dist/types/server/request-context.d.ts +63 -2
  71. package/dist/types/suspense-audit-react.d.ts +26 -0
  72. package/dist/types/suspense-audit.d.ts +157 -0
  73. package/dist/types/testing/action-form.d.ts +34 -0
  74. package/dist/types/testing/flight.entry.d.ts +2 -0
  75. package/dist/types/testing/index.d.ts +4 -0
  76. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  77. package/dist/types/testing/serve-shell-request.d.ts +34 -4
  78. package/dist/types/testing/shell-status.d.ts +14 -6
  79. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  80. package/dist/types/thenable-status.d.ts +6 -0
  81. package/dist/types/types/handler-context.d.ts +7 -0
  82. package/dist/types/types/route-entry.d.ts +5 -0
  83. package/dist/types/urls/path-helper.d.ts +18 -0
  84. package/dist/types/vite/discovery/state.d.ts +11 -0
  85. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  86. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  87. package/dist/vite/index.js +180 -497
  88. package/package.json +13 -3
  89. package/skills/cache-guide/SKILL.md +18 -17
  90. package/skills/caching/SKILL.md +40 -4
  91. package/skills/catalog.json +1 -1
  92. package/skills/cloudflare/SKILL.md +131 -0
  93. package/skills/comparison/references/framework-comparison.md +1 -1
  94. package/skills/deployment-caching/SKILL.md +33 -12
  95. package/skills/document-cache/SKILL.md +16 -14
  96. package/skills/hooks/SKILL.md +22 -0
  97. package/skills/hooks/handle-and-actions.md +3 -4
  98. package/skills/hooks/navigation.md +10 -0
  99. package/skills/hooks/state.md +5 -4
  100. package/skills/host-router/SKILL.md +18 -0
  101. package/skills/loader/SKILL.md +6 -4
  102. package/skills/middleware/SKILL.md +1 -1
  103. package/skills/migrate-nextjs/SKILL.md +82 -9
  104. package/skills/ppr/SKILL.md +31 -8
  105. package/skills/prerender/SKILL.md +645 -8
  106. package/skills/rango/SKILL.md +1 -0
  107. package/skills/response-routes/SKILL.md +5 -5
  108. package/skills/route/SKILL.md +29 -0
  109. package/skills/testing/SKILL.md +2 -1
  110. package/skills/testing/cache-prerender.md +18 -17
  111. package/skills/testing/serve-shell-request.md +104 -8
  112. package/skills/use-cache/SKILL.md +9 -5
  113. package/skills/vercel/SKILL.md +2 -0
  114. package/skills/view-transitions/SKILL.md +9 -4
  115. package/src/browser/event-controller.ts +69 -25
  116. package/src/browser/link-interceptor.ts +2 -2
  117. package/src/browser/navigation-bridge.ts +75 -35
  118. package/src/browser/navigation-store.ts +26 -0
  119. package/src/browser/navigation-transaction.ts +9 -2
  120. package/src/browser/network-error-handler.ts +2 -0
  121. package/src/browser/partial-update.ts +48 -12
  122. package/src/browser/prefetch/default-strategy.ts +22 -42
  123. package/src/browser/react/Link.tsx +37 -42
  124. package/src/browser/react/NavigationProvider.tsx +63 -3
  125. package/src/browser/react/context.ts +17 -0
  126. package/src/browser/react/location-state.ts +5 -19
  127. package/src/browser/react/use-action.ts +4 -1
  128. package/src/browser/react/use-handle.ts +4 -20
  129. package/src/browser/react/use-link-status.ts +4 -3
  130. package/src/browser/react/use-navigation.ts +4 -1
  131. package/src/browser/react/use-params.ts +4 -1
  132. package/src/browser/react/use-pathname.ts +4 -1
  133. package/src/browser/react/use-search-params.ts +5 -3
  134. package/src/browser/react/use-segments.ts +36 -54
  135. package/src/browser/rsc-router.tsx +79 -39
  136. package/src/browser/segment-reconciler.ts +3 -3
  137. package/src/browser/server-action-bridge.ts +26 -6
  138. package/src/browser/types.ts +11 -0
  139. package/src/build/generate-manifest.ts +17 -0
  140. package/src/build/merge-full-manifests.ts +5 -0
  141. package/src/build/route-trie.ts +1 -0
  142. package/src/cache/background-task.ts +61 -0
  143. package/src/cache/cache-key-utils.ts +91 -12
  144. package/src/cache/cache-policy.ts +11 -2
  145. package/src/cache/cache-runtime.ts +23 -23
  146. package/src/cache/cache-scope.ts +38 -36
  147. package/src/cache/cf/cf-cache-store.ts +16 -11
  148. package/src/cache/document-cache.ts +35 -14
  149. package/src/cache/handle-snapshot.ts +50 -0
  150. package/src/cache/index.ts +1 -0
  151. package/src/cache/invalidation-order.ts +53 -0
  152. package/src/cache/memory-segment-store.ts +52 -1
  153. package/src/cache/shell-snapshot.ts +17 -0
  154. package/src/cache/store-scope.ts +36 -0
  155. package/src/cache/tag-invalidation.ts +7 -5
  156. package/src/cache/types.ts +69 -14
  157. package/src/cache/vercel/vercel-cache-store.ts +20 -6
  158. package/src/client.tsx +40 -3
  159. package/src/errors.ts +13 -6
  160. package/src/handles/deferred-resolution.ts +7 -2
  161. package/src/internal-suspense-audit.ts +12 -0
  162. package/src/loader-store.ts +4 -9
  163. package/src/outlet-provider.tsx +25 -1
  164. package/src/prerender/cloudflare.ts +130 -0
  165. package/src/prerender/create-prerender-trigger.ts +780 -0
  166. package/src/prerender/index.ts +39 -0
  167. package/src/prerender/memory-prerender-store.ts +115 -0
  168. package/src/prerender/on-demand.ts +339 -0
  169. package/src/prerender/producer-guard.ts +66 -0
  170. package/src/prerender/shell-manifest-key.ts +12 -17
  171. package/src/prerender/warm-request.ts +154 -0
  172. package/src/prerender/warm.ts +193 -0
  173. package/src/prerender/writable-store.ts +278 -0
  174. package/src/prerender.ts +29 -0
  175. package/src/reverse.ts +1 -1
  176. package/src/route-content-wrapper.tsx +107 -13
  177. package/src/router/error-handling.ts +12 -12
  178. package/src/router/find-match.ts +1 -0
  179. package/src/router/handler-context.ts +2 -0
  180. package/src/router/intercept-resolution.ts +15 -8
  181. package/src/router/loader-resolution.ts +2 -10
  182. package/src/router/match-middleware/background-revalidation.ts +2 -0
  183. package/src/router/match-middleware/cache-lookup.ts +230 -38
  184. package/src/router/pattern-matching.ts +14 -0
  185. package/src/router/prefetch-default.ts +4 -4
  186. package/src/router/prerender-match.ts +460 -352
  187. package/src/router/route-trie-builder.ts +9 -0
  188. package/src/router/router-context.ts +1 -0
  189. package/src/router/router-interfaces.ts +33 -0
  190. package/src/router/router-options.ts +30 -0
  191. package/src/router/segment-resolution/fresh.ts +20 -19
  192. package/src/router/segment-resolution/loader-cache.ts +15 -17
  193. package/src/router/segment-resolution/revalidation.ts +34 -24
  194. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  195. package/src/router/trie-matching.ts +3 -0
  196. package/src/router.ts +107 -1
  197. package/src/rsc/handler.ts +51 -4
  198. package/src/rsc/progressive-enhancement.ts +5 -1
  199. package/src/rsc/response-cache-serve.ts +10 -4
  200. package/src/rsc/rsc-rendering.ts +53 -11
  201. package/src/rsc/shell-build-manifest.ts +10 -8
  202. package/src/rsc/shell-capture-constants.ts +39 -7
  203. package/src/rsc/shell-capture.ts +108 -19
  204. package/src/rsc/shell-serve.ts +12 -6
  205. package/src/rsc/ssr-module-loader.ts +26 -0
  206. package/src/rsc/types.ts +8 -0
  207. package/src/segment-boundary-content.ts +25 -0
  208. package/src/segment-loader-promise.ts +28 -38
  209. package/src/segment-system.tsx +117 -57
  210. package/src/server/build-version-table.ts +15 -0
  211. package/src/server/context.ts +40 -17
  212. package/src/server/cookie-store.ts +5 -0
  213. package/src/server/handle-store.ts +13 -0
  214. package/src/server/loader-registry.ts +6 -1
  215. package/src/server/request-context.ts +87 -1
  216. package/src/ssr/ssr-root.tsx +20 -13
  217. package/src/suspense-audit-react.tsx +98 -0
  218. package/src/suspense-audit.ts +1030 -0
  219. package/src/testing/action-form.ts +66 -0
  220. package/src/testing/flight-runtime.d.ts +27 -0
  221. package/src/testing/flight.entry.ts +3 -0
  222. package/src/testing/index.ts +19 -0
  223. package/src/testing/internal/server-modules.ts +32 -0
  224. package/src/testing/render-route.tsx +25 -12
  225. package/src/testing/serve-shell-request.ts +190 -34
  226. package/src/testing/shell-status.ts +17 -6
  227. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  228. package/src/thenable-status.ts +10 -0
  229. package/src/types/handler-context.ts +7 -0
  230. package/src/types/route-entry.ts +6 -0
  231. package/src/urls/path-helper.ts +69 -9
  232. package/src/use-loader.tsx +57 -30
  233. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  234. package/src/vite/discovery/discover-routers.ts +15 -0
  235. package/src/vite/discovery/shell-prerender-phase.ts +5 -5
  236. package/src/vite/discovery/state.ts +17 -1
  237. package/src/vite/inject-client-debug.ts +42 -20
  238. package/src/vite/plugins/virtual-entries.ts +8 -1
  239. package/src/vite/router-discovery.ts +14 -0
  240. package/src/vite/utils/bundle-analysis.ts +39 -7
  241. package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
  242. package/dist/types/segment-content-promise.d.ts +0 -13
  243. package/src/browser/react/subscribe-to-nothing.ts +0 -7
  244. package/src/segment-content-promise.ts +0 -67
@@ -35,7 +35,10 @@ import {
35
35
  shouldStartViewTransition,
36
36
  } from "./partial-update.js";
37
37
  import { decideCommitGatedOff, mergeSegmentParams } from "./transition-when.js";
38
- import { generateHistoryKey } from "./navigation-store.js";
38
+ import {
39
+ generateHistoryKey,
40
+ liveHandleDataForCache,
41
+ } from "./navigation-store.js";
39
42
  import type { EventController } from "./event-controller.js";
40
43
  import { isInterceptOnlyCache } from "./intercept-utils.js";
41
44
  import {
@@ -54,6 +57,7 @@ import {
54
57
  } from "./prefetch/loader.js";
55
58
  import type { PrefetchStrategy } from "../router/prefetch-default.js";
56
59
  import { beginClientUrlNavigation } from "../client-urls/navigation.js";
60
+ import { auditTreeCause } from "../suspense-audit.js";
57
61
 
58
62
  // Polyfill Symbol.dispose for Safari and older browsers
59
63
  if (typeof Symbol.dispose === "undefined") {
@@ -62,6 +66,9 @@ if (typeof Symbol.dispose === "undefined") {
62
66
 
63
67
  export { createNavigationTransaction };
64
68
 
69
+ // Mirrors the redirect-chain limit browsers apply to document navigations.
70
+ const MAX_REDIRECT_HOPS = 20;
71
+
65
72
  /**
66
73
  * Extended configuration for navigation bridge with event controller
67
74
  */
@@ -149,6 +156,45 @@ export function createNavigationBridge(
149
156
  getVersion: () => version,
150
157
  });
151
158
 
159
+ /**
160
+ * Re-navigates to a server redirect target, counting hops so a target that
161
+ * redirects back ends in the error boundary instead of looping.
162
+ */
163
+ async function followServerRedirect(
164
+ self: {
165
+ navigate: (
166
+ url: string,
167
+ options?: NavigateOptionsInternal,
168
+ ) => Promise<void>;
169
+ },
170
+ error: ServerRedirect,
171
+ url: string,
172
+ hops: number,
173
+ options: { replace?: boolean; transition?: boolean },
174
+ ): Promise<void> {
175
+ const redirectUrl = validateRedirectOrigin(
176
+ error.url,
177
+ window.location.origin,
178
+ );
179
+ if (!redirectUrl) {
180
+ return;
181
+ }
182
+ if (hops >= MAX_REDIRECT_HOPS) {
183
+ const loopError = new Error(
184
+ `[rango] Server redirect loop: stopped after following ${hops} redirects, at ${url}`,
185
+ );
186
+ console.error(loopError.message);
187
+ emitNavigationError(onUpdate, loopError, url);
188
+ return;
189
+ }
190
+ return self.navigate(redirectUrl, {
191
+ state: error.state,
192
+ ...options,
193
+ _skipCache: true,
194
+ _redirectHops: hops + 1,
195
+ });
196
+ }
197
+
152
198
  return {
153
199
  /**
154
200
  * Navigate to a URL
@@ -205,7 +251,11 @@ export function createNavigationBridge(
205
251
  const currentKey = store.getHistoryKey();
206
252
  const currentCache = store.getCachedSegments(currentKey);
207
253
  if (currentCache?.segments) {
208
- const currentHandleData = eventController.getHandleState().data;
254
+ const currentHandleData = liveHandleDataForCache(
255
+ eventController,
256
+ store,
257
+ currentKey,
258
+ );
209
259
  store.cacheSegmentsForHistory(
210
260
  historyKey,
211
261
  currentCache.segments,
@@ -283,7 +333,11 @@ export function createNavigationBridge(
283
333
  const sourceHistoryKey = store.getHistoryKey();
284
334
  const sourceCached = store.getCachedSegments(sourceHistoryKey);
285
335
  if (sourceCached?.segments && sourceCached.segments.length > 0) {
286
- const currentHandleData = eventController.getHandleState().data;
336
+ const currentHandleData = liveHandleDataForCache(
337
+ eventController,
338
+ store,
339
+ sourceHistoryKey,
340
+ );
287
341
  store.updateCacheHandleData(sourceHistoryKey, currentHandleData);
288
342
  }
289
343
 
@@ -413,29 +467,12 @@ export function createNavigationBridge(
413
467
  },
414
468
  );
415
469
  } catch (error) {
416
- // Server-side redirect with location state: the current transaction's
417
- // cleanup resets loading state. Re-navigate to the redirect
418
- // target carrying the server-set state into history.pushState.
419
- if (error instanceof ServerRedirect) {
420
- const redirectUrl = validateRedirectOrigin(
421
- error.url,
422
- window.location.origin,
423
- );
424
- if (!redirectUrl) {
425
- return;
426
- }
427
- return this.navigate(redirectUrl, {
428
- state: error.state,
429
- replace: options?.replace,
430
- transition: options?.transition,
431
- _skipCache: true,
432
- } as NavigateOptionsInternal);
433
- }
434
-
435
470
  // Aborted, or superseded by a newer navigation. A superseded nav may
436
471
  // reject with a non-AbortError (e.g. a Flight decode that fails after its
437
472
  // signal was aborted), so check the signal too -- otherwise we would
438
- // render a boundary that clobbers the newer navigation's content.
473
+ // render a boundary that clobbers the newer navigation's content. Before
474
+ // the redirect branch: a superseded nav must not follow its redirect
475
+ // over the navigation that replaced it.
439
476
  if (
440
477
  (error instanceof DOMException && error.name === "AbortError") ||
441
478
  tx.handle.signal.aborted
@@ -444,6 +481,19 @@ export function createNavigationBridge(
444
481
  return;
445
482
  }
446
483
 
484
+ // Server-side redirect with location state: the current transaction's
485
+ // cleanup resets loading state. Re-navigate to the redirect
486
+ // target carrying the server-set state into history.pushState.
487
+ if (error instanceof ServerRedirect) {
488
+ return followServerRedirect(
489
+ this,
490
+ error,
491
+ url,
492
+ options?._redirectHops ?? 0,
493
+ { replace: options?.replace, transition: options?.transition },
494
+ );
495
+ }
496
+
447
497
  const networkError = toNetworkError(error, {
448
498
  url,
449
499
  operation: "navigation",
@@ -670,6 +720,7 @@ export function createNavigationBridge(
670
720
  !isIntercept &&
671
721
  !isLeavingIntercept &&
672
722
  shouldStartViewTransition(cachedSegments, gatedOff);
723
+ if (process.env.NODE_ENV !== "production") auditTreeCause("popstate");
673
724
  if (hasTransition) {
674
725
  startTransition(() => {
675
726
  if (addTransitionType) {
@@ -786,18 +837,7 @@ export function createNavigationBridge(
786
837
  // and Back would hit the redirect again. Before setLocation below, or
787
838
  // the hooks would report the redirecting URL first.
788
839
  if (error instanceof ServerRedirect) {
789
- const redirectUrl = validateRedirectOrigin(
790
- error.url,
791
- window.location.origin,
792
- );
793
- if (!redirectUrl) {
794
- return;
795
- }
796
- return this.navigate(redirectUrl, {
797
- state: error.state,
798
- replace: true,
799
- _skipCache: true,
800
- } as NavigateOptionsInternal);
840
+ return followServerRedirect(this, error, url, 0, { replace: true });
801
841
  }
802
842
 
803
843
  // tx.commit() never ran, so the location is still the page being
@@ -8,7 +8,9 @@ import type {
8
8
  ResolvedSegment,
9
9
  HandleData,
10
10
  } from "./types.js";
11
+ import type { EventController } from "./event-controller.js";
11
12
  import { clearPrefetchCache } from "./prefetch/cache.js";
13
+ import { LoaderStore } from "../loader-store.js";
12
14
  import {
13
15
  adoptRangoState,
14
16
  getRangoState,
@@ -73,6 +75,28 @@ export function cloneHandleData(handleData: HandleData): HandleData {
73
75
  return cloned;
74
76
  }
75
77
 
78
+ /**
79
+ * Handle data to write into the history cache for the page on screen. During
80
+ * the hydration window getHandleState() is the document snapshot while late
81
+ * pushes sit queued in the controller (they are already in the cache entry,
82
+ * written by rsc-router), so writing the snapshot back would drop them; the
83
+ * entry's own data is the live view then.
84
+ */
85
+ export function liveHandleDataForCache(
86
+ eventController: Pick<
87
+ EventController,
88
+ "getHandleState" | "getHydrationSnapshot"
89
+ >,
90
+ store: Pick<NavigationStore, "getCachedSegments">,
91
+ historyKey: string,
92
+ ): HandleData {
93
+ if (eventController.getHydrationSnapshot()) {
94
+ const cached = store.getCachedSegments(historyKey)?.handleData;
95
+ if (cached) return cached;
96
+ }
97
+ return eventController.getHandleState().data;
98
+ }
99
+
76
100
  // BroadcastChannel for cross-tab cache invalidation
77
101
  const CACHE_INVALIDATION_CHANNEL = "rsc-router-cache-invalidation";
78
102
 
@@ -390,6 +414,8 @@ export function createNavigationStore(
390
414
  }
391
415
 
392
416
  const store: NavigationStore = {
417
+ loaders: new LoaderStore(),
418
+
393
419
  // ========================================================================
394
420
  // Internal Segment State (for bridges)
395
421
  // ========================================================================
@@ -4,7 +4,10 @@ import type {
4
4
  ResolvedSegment,
5
5
  StreamingToken,
6
6
  } from "./types.js";
7
- import { generateHistoryKey } from "./navigation-store.js";
7
+ import {
8
+ generateHistoryKey,
9
+ liveHandleDataForCache,
10
+ } from "./navigation-store.js";
8
11
  import {
9
12
  handleNavigationStart,
10
13
  ensureHistoryKey,
@@ -180,7 +183,11 @@ export function createNavigationTransaction(
180
183
 
181
184
  store.setHistoryKey(historyKey);
182
185
 
183
- const currentHandleData = eventController.getHandleState().data;
186
+ // storeOnly re-caches the page on screen (an action refetch), which can
187
+ // commit with no tree and so never release the hydration window.
188
+ const currentHandleData = storeOnly
189
+ ? liveHandleDataForCache(eventController, store, historyKey)
190
+ : eventController.getHandleState().data;
184
191
  store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
185
192
 
186
193
  if (storeOnly) {
@@ -2,6 +2,7 @@ import { NetworkError, isNetworkError } from "../errors.js";
2
2
  import { RenderErrorThrower } from "../render-error-thrower.js";
3
3
  import type { UpdateSubscriber } from "./types.js";
4
4
  import { createElement, startTransition } from "react";
5
+ import { auditTreeCause } from "../suspense-audit.js";
5
6
 
6
7
  /**
7
8
  * Convert an unknown error to a NetworkError, or return null if not network-related.
@@ -33,6 +34,7 @@ function emitErrorToBoundary(
33
34
  error: unknown,
34
35
  pathname: string,
35
36
  ): void {
37
+ if (process.env.NODE_ENV !== "production") auditTreeCause("error");
36
38
  startTransition(() => {
37
39
  onUpdate({
38
40
  root: createElement(RenderErrorThrower, { error }),
@@ -31,7 +31,7 @@ import {
31
31
  } from "./validate-redirect-origin.js";
32
32
  import type { NavigationUpdate } from "./types.js";
33
33
  import { OPTIMISTIC_COMMIT_TRANSITION_TYPE } from "./optimistic-commit.js";
34
- import { loaderStore } from "../loader-store.js";
34
+ import type { LoaderStore } from "../loader-store.js";
35
35
  import {
36
36
  collectClientRevalidationDecisions,
37
37
  setActiveInterceptTargets,
@@ -44,6 +44,7 @@ import {
44
44
  import { buildHistoryState } from "./history-state.js";
45
45
  import { addLocationState } from "./react/location-state-shared.js";
46
46
  import type { TransitionWhenKind } from "../types/segments.js";
47
+ import { auditTreeCause, type TreeUpdateCause } from "../suspense-audit.js";
47
48
 
48
49
  function toScrollPayload(
49
50
  scroll: boolean | undefined,
@@ -70,6 +71,13 @@ export function shouldStartViewTransition(
70
71
  return !hasIntercept && hasTransition;
71
72
  }
72
73
 
74
+ // Dev only (suspense-audit.ts I6): what this updater's commit is.
75
+ function treeCause(mode: UpdateMode, traversal?: boolean): TreeUpdateCause {
76
+ if (mode.type === "action") return "action";
77
+ if (mode.type === "stale-revalidation") return "stale-revalidation";
78
+ return traversal ? "popstate" : "navigation";
79
+ }
80
+
73
81
  /**
74
82
  * Transition commit. Every held commit announces the loader streams the
75
83
  * committed tree is still receiving (loader-store.ts announcePendingStreams)
@@ -79,13 +87,14 @@ export function shouldStartViewTransition(
79
87
  * its fallback. Also used by renderRoute's navigate() (testing/render-route.tsx).
80
88
  */
81
89
  export function commitInTransition(
90
+ loaders: LoaderStore,
82
91
  onUpdate: UpdateSubscriber,
83
92
  segments: readonly ResolvedSegment[],
84
93
  update: NavigationUpdate,
85
94
  transitionTypes: readonly string[],
86
95
  ): void {
87
96
  startTransition(() => {
88
- loaderStore.announcePendingStreams(segments);
97
+ loaders.announcePendingStreams(segments);
89
98
  if (addTransitionType) {
90
99
  for (const type of transitionTypes) addTransitionType(type);
91
100
  }
@@ -464,10 +473,17 @@ export function createPartialUpdater(
464
473
  scroll: toScrollPayload(commitScroll),
465
474
  };
466
475
 
476
+ if (process.env.NODE_ENV !== "production") {
477
+ auditTreeCause(treeCause(mode, tx.traversal));
478
+ }
467
479
  if (shouldStartViewTransition(existingSegments, cachedGatedOff)) {
468
- commitInTransition(onUpdate, existingSegments, cachedUpdate, [
469
- "navigation",
470
- ]);
480
+ commitInTransition(
481
+ store.loaders,
482
+ onUpdate,
483
+ existingSegments,
484
+ cachedUpdate,
485
+ ["navigation"],
486
+ );
471
487
  } else {
472
488
  onUpdate(cachedUpdate);
473
489
  }
@@ -491,6 +507,9 @@ export function createPartialUpdater(
491
507
  { routeName: payload.metadata.routeName },
492
508
  );
493
509
 
510
+ if (process.env.NODE_ENV !== "production") {
511
+ auditTreeCause(treeCause(mode, tx.traversal));
512
+ }
494
513
  onUpdate({
495
514
  root: newTree,
496
515
  metadata: payload.metadata,
@@ -718,11 +737,15 @@ export function createPartialUpdater(
718
737
  metadata: payload.metadata!,
719
738
  scroll: scrollPayload,
720
739
  };
740
+ if (process.env.NODE_ENV !== "production") {
741
+ auditTreeCause(treeCause(mode, tx.traversal));
742
+ }
721
743
  if (
722
744
  !gatedOff &&
723
745
  (mode.type === "action" || mode.type === "stale-revalidation")
724
746
  ) {
725
747
  commitInTransition(
748
+ store.loaders,
726
749
  onUpdate,
727
750
  reconciled.mainSegments,
728
751
  update,
@@ -730,6 +753,7 @@ export function createPartialUpdater(
730
753
  );
731
754
  } else if (hasTransition) {
732
755
  commitInTransition(
756
+ store.loaders,
733
757
  onUpdate,
734
758
  reconciled.mainSegments,
735
759
  update,
@@ -764,6 +788,7 @@ export function createPartialUpdater(
764
788
  // segment reconciles in place (clientGroup key), so a read that still
765
789
  // suspends must hold the presented content, not flash a fallback.
766
790
  commitInTransition(
791
+ store.loaders,
767
792
  onUpdate,
768
793
  reconciled.mainSegments,
769
794
  update,
@@ -772,12 +797,13 @@ export function createPartialUpdater(
772
797
  } else {
773
798
  // Cold/partially-prefetched nav that mounts NEW segments, or any
774
799
  // commit transition({ when }) gated off (navigation, action refetch,
775
- // stale revalidation): normal commit so fallbacks stream
776
- // like a first load and the click has visible feedback. A gated-off
777
- // segment keeps its key, so its loading() fallback comes from its
778
- // boundary re-suspending on this urgent commit, not from a remount
779
- // (#995). Explicit transition() routes keep the content-hold via the
780
- // hasTransition branch above (the opt-in).
800
+ // stale revalidation): normal, unanimated commit. A segment with work
801
+ // still pending shows its loading() fallback like a first load; one
802
+ // whose data is already in hand shows none. A gated-off segment keeps
803
+ // its key, so a fallback comes from its boundary suspending on pending
804
+ // work in this urgent commit, not from a remount (#995). Explicit
805
+ // transition() routes keep the content-hold via the hasTransition
806
+ // branch above (the opt-in).
781
807
  onUpdate(update);
782
808
  }
783
809
 
@@ -822,6 +848,9 @@ export function createPartialUpdater(
822
848
  scroll: fullScrollPayload,
823
849
  };
824
850
 
851
+ if (process.env.NODE_ENV !== "production") {
852
+ auditTreeCause(treeCause(mode, tx.traversal));
853
+ }
825
854
  if (mode.type === "stale-revalidation") {
826
855
  await rawStreamComplete;
827
856
  // Mirror the partial branch's history-key staleness guard (above): the
@@ -836,9 +865,13 @@ export function createPartialUpdater(
836
865
  );
837
866
  return;
838
867
  }
868
+ if (process.env.NODE_ENV !== "production") {
869
+ auditTreeCause("stale-revalidation");
870
+ }
839
871
  if (fullGatedOff) onUpdate(fullUpdate);
840
872
  else {
841
873
  commitInTransition(
874
+ store.loaders,
842
875
  onUpdate,
843
876
  segments,
844
877
  fullUpdate,
@@ -847,13 +880,16 @@ export function createPartialUpdater(
847
880
  }
848
881
  } else if (mode.type === "action" && !fullGatedOff) {
849
882
  commitInTransition(
883
+ store.loaders,
850
884
  onUpdate,
851
885
  segments,
852
886
  fullUpdate,
853
887
  fullHasTransition ? ["action"] : [],
854
888
  );
855
889
  } else if (fullHasTransition) {
856
- commitInTransition(onUpdate, segments, fullUpdate, ["navigation"]);
890
+ commitInTransition(store.loaders, onUpdate, segments, fullUpdate, [
891
+ "navigation",
892
+ ]);
857
893
  } else {
858
894
  onUpdate(fullUpdate);
859
895
  }
@@ -2,68 +2,48 @@
2
2
  * Router-wide default prefetch strategy (client seat).
3
3
  *
4
4
  * The server resolves `createRouter({ defaultPrefetch })` once at router init
5
- * (router/prefetch-default.ts) and ships it in initial payload metadata; the
6
- * browser entry applies it here before hydration — same lifecycle as
7
- * `initPrefetchCache` / `setPrefetchConcurrency`. Every `<Link>` without an
8
- * explicit `prefetch` prop and every eligible intercepted plain anchor that has
9
- * not opted out with `data-prefetch="false"` or `data-prefetch="none"` uses it.
10
- * Containers use the same `false`/`none` vocabulary via
11
- * `data-prefetch-scope`.
12
- *
13
- * The module initial value must equal the server resolver's environment default:
14
- * `"none"` in development and `"viewport"` in production. During SSR this
15
- * module is never initialized from metadata, so keeping both seats aligned also
16
- * gives metadata-less payloads the documented behavior.
5
+ * (router/prefetch-default.ts) and ships it in initial payload metadata. The
6
+ * router store carries it to its readers: `<Link>` reads
7
+ * `NavigationStoreContext.defaultPrefetch`, and delegated plain anchors get it
8
+ * from the navigation bridge. Every `<Link>` without an explicit `prefetch`
9
+ * prop and every eligible intercepted plain anchor that has not opted out with
10
+ * `data-prefetch="false"` or `data-prefetch="none"` uses it. Containers use the
11
+ * same `false`/`none` vocabulary via `data-prefetch-scope`.
17
12
  */
18
13
 
19
14
  import type { PrefetchStrategy } from "../../router/prefetch-default.js";
20
15
 
21
- let hoverNoneQuery: MediaQueryList | null = null;
22
-
23
- // Mirrors DEFAULT_PREFETCH_STRATEGY without pulling router-layer code into the
24
- // client bundle. NODE_ENV is folded by the app build.
25
- let defaultStrategy: PrefetchStrategy =
16
+ /**
17
+ * The default for a payload without `defaultPrefetch`: the server resolver's
18
+ * environment default (`"none"` in development, `"viewport"` in production),
19
+ * mirrored without pulling router-layer code into the client bundle. NODE_ENV
20
+ * is folded by the app build.
21
+ */
22
+ export const ENVIRONMENT_DEFAULT_PREFETCH: PrefetchStrategy =
26
23
  process.env.NODE_ENV === "production" ? "viewport" : "none";
27
24
 
28
- function getHoverNoneQuery(): MediaQueryList | null {
29
- if (typeof window === "undefined") return null;
30
- if (!hoverNoneQuery) {
31
- hoverNoneQuery = window.matchMedia("(hover: none)");
32
- }
33
- return hoverNoneQuery;
34
- }
35
-
36
- /** Reset module state between tests that replace browser media globals. */
37
- export function resetAdaptiveStrategyForTesting(): void {
38
- hoverNoneQuery = null;
25
+ function hoverNoneQuery(): MediaQueryList | null {
26
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function")
27
+ return null;
28
+ return window.matchMedia("(hover: none)");
39
29
  }
40
30
 
41
31
  /**
42
- * Apply the server-resolved default strategy. Called once at browser app init
43
- * from payload metadata; also used by tests to reset state.
32
+ * Resolve adaptive to the strategy for the current input capability. Called
33
+ * when a prefetch is armed or triggered, never during render.
44
34
  */
45
- export function setDefaultPrefetchStrategy(strategy: PrefetchStrategy): void {
46
- defaultStrategy = strategy;
47
- }
48
-
49
- /** Current default strategy for Links and delegated plain anchors. */
50
- export function getDefaultPrefetchStrategy(): PrefetchStrategy {
51
- return defaultStrategy;
52
- }
53
-
54
- /** Resolve adaptive to the strategy for the current input capability. */
55
35
  export function resolveAdaptiveStrategy(
56
36
  strategy: PrefetchStrategy,
57
37
  ): PrefetchStrategy {
58
38
  if (strategy !== "adaptive") return strategy;
59
- return getHoverNoneQuery()?.matches ? "viewport" : "hover";
39
+ return hoverNoneQuery()?.matches ? "viewport" : "hover";
60
40
  }
61
41
 
62
42
  /** Subscribe to changes in the input capability used by adaptive prefetch. */
63
43
  export function subscribeToAdaptiveStrategyChange(
64
44
  listener: () => void,
65
45
  ): () => void {
66
- const query = getHoverNoneQuery();
46
+ const query = hoverNoneQuery();
67
47
  if (!query) return () => {};
68
48
  if (typeof query.addEventListener === "function") {
69
49
  query.addEventListener("change", listener);