@tanstack/router-core 1.171.16-pre.0 → 1.171.16

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.
@@ -2,12 +2,7 @@
2
2
  // can rewrite relative imports for both ESM and CJS.
3
3
  import { isNotFound } from './not-found'
4
4
  import { isRedirect } from './redirect'
5
- import {
6
- _getUserHistoryState,
7
- getLocationChangeInfo,
8
- runRouteLifecycle,
9
- } from './router'
10
- import { deepEqual } from './utils'
5
+ import { getLocationChangeInfo, runRouteLifecycle } from './router'
11
6
  import { hydrateSsrMatchId } from './ssr/ssr-match-id'
12
7
  import type { GLOBAL_SEROVAL, GLOBAL_TSR } from './ssr/constants'
13
8
  import type { AnySerializationAdapter } from './ssr/serializer/transformer'
@@ -46,26 +41,35 @@ function preloadComponent(
46
41
  return (route.options[type] as any)?.preload?.()
47
42
  }
48
43
 
49
- function loadComponents(route: AnyRoute): Promise<void> | undefined {
44
+ function loadComponents(
45
+ route: AnyRoute,
46
+ onPendingReady?: () => void,
47
+ ): Promise<void> | undefined {
50
48
  const component = preloadComponent(route, 'component')
51
49
  const pending = preloadComponent(route, 'pendingComponent')
52
- if (component && pending) {
53
- return Promise.all([component, pending]).then(() => {})
50
+ const pendingReady =
51
+ onPendingReady && pending ? pending.then(onPendingReady) : pending
52
+ if (onPendingReady && !pending) {
53
+ onPendingReady()
54
54
  }
55
- return component ?? pending
55
+ if (component && pendingReady) {
56
+ return Promise.all([component, pendingReady]).then(() => {})
57
+ }
58
+ return component ?? pendingReady
56
59
  }
57
60
 
58
61
  export function loadRouteChunk(
59
62
  route: AnyRoute,
60
63
  // `false` waits only for lazy route options, before a boundary is selected.
61
64
  componentType?: 'errorComponent' | 'notFoundComponent' | false,
65
+ onPendingReady?: () => void,
62
66
  ): Promise<void> | undefined {
63
67
  const afterLazy = () =>
64
68
  componentType === false
65
69
  ? undefined
66
70
  : componentType
67
71
  ? preloadComponent(route, componentType)
68
- : loadComponents(route)
72
+ : loadComponents(route, onPendingReady)
69
73
  const current = route._lazy
70
74
  if (current) {
71
75
  return current === true ? afterLazy() : current.then(afterLazy)
@@ -161,11 +165,11 @@ const REDIRECTED = 3
161
165
  const CANCELED = 4
162
166
 
163
167
  type LoaderOutcome =
164
- | [typeof SUCCESS, data: unknown]
165
- | [typeof ERROR, error: unknown]
166
- | [typeof NOT_FOUND, error: NotFoundError]
167
- | [typeof REDIRECTED, redirect: AnyRedirect]
168
- | [typeof CANCELED]
168
+ | [kind: typeof SUCCESS, data: unknown]
169
+ | [kind: typeof ERROR, error: unknown]
170
+ | [kind: typeof NOT_FOUND, error: NotFoundError]
171
+ | [kind: typeof REDIRECTED, redirect: AnyRedirect]
172
+ | [kind: typeof CANCELED]
169
173
 
170
174
  type IndexedOutcome = [index: number, outcome: LoaderOutcome, boundary?: number]
171
175
 
@@ -190,31 +194,6 @@ declare const matchPhase: unique symbol
190
194
  */
191
195
  type SettledMatch = WorkMatch & { readonly [matchPhase]: 'settled' }
192
196
 
193
- export type LaneInputs = [
194
- routeTree: AnyRoute,
195
- context: unknown,
196
- additionalContext: unknown,
197
- state: object,
198
- search: object,
199
- maskedLocation:
200
- | [
201
- href: string,
202
- state: object,
203
- search: object,
204
- unmaskOnReload: boolean | undefined,
205
- ]
206
- | undefined,
207
- ]
208
-
209
- export type ActivePreload = [
210
- matches: Array<AnyRouteMatch>,
211
- controller: AbortController,
212
- result: Promise<LaneResult>,
213
- semanticOwner: Array<AnyRouteMatch>,
214
- inputs: LaneInputs,
215
- redirects: number,
216
- ]
217
-
218
197
  export type LoadTransaction = [
219
198
  controller: AbortController,
220
199
  redirects: number,
@@ -224,12 +203,14 @@ export type LoadTransaction = [
224
203
  done: Promise<void>,
225
204
  /**
226
205
  * Dev-only HMR refresh mode. Presence is the mode flag; a refresh always
227
- * carries the presentation it started from, while the hydration handoff is
228
- * genuinely optional the tuple makes a half-armed refresh unrepresentable.
206
+ * carries the presentation it started from and its optional hydration
207
+ * handoff. While a publication awaits acknowledgement, its rollback lives
208
+ * with the transaction that owns the publication.
229
209
  */
230
210
  refresh?: [
231
211
  presentation: Array<AnyRouteMatch>,
232
212
  handoff: NonNullable<AnyRouter['_handoff']> | undefined,
213
+ rollback?: () => boolean,
233
214
  ],
234
215
  ]
235
216
 
@@ -244,10 +225,9 @@ export type PendingSession = [
244
225
  ]
245
226
 
246
227
  type CoordinatorRouter = AnyRouter & {
247
- /** Whole speculative lanes that a matching navigation may adopt. */
248
- _preloads?: Map<string, ActivePreload>
228
+ /** Active speculative lanes retained for cancellation, invalidation, and cache clearing. */
229
+ _preloads?: Map<AbortController, Array<AnyRouteMatch>>
249
230
  _refreshNextLoad?: boolean
250
- _rollbackRefresh?: () => void
251
231
  _cancelTransition?: () => void
252
232
  }
253
233
 
@@ -262,14 +242,14 @@ type PublicationCheckpoint = {
262
242
  type LoaderTask = [
263
243
  index: number,
264
244
  outcome: Promise<LoaderOutcome>,
265
- ready: Promise<IndexedOutcome | undefined>,
245
+ chunkFailure: Promise<IndexedOutcome | undefined>,
266
246
  candidate?: WorkMatch,
267
247
  ]
268
248
 
269
249
  type BackgroundLoaderTask = [
270
250
  index: number,
271
251
  outcome: Promise<LoaderOutcome>,
272
- ready: Promise<IndexedOutcome | undefined>,
252
+ chunkFailure: Promise<IndexedOutcome | undefined>,
273
253
  candidate: WorkMatch,
274
254
  ]
275
255
 
@@ -286,15 +266,15 @@ type ExecuteLaneOptions = [
286
266
  ]
287
267
 
288
268
  type ControlOutcome =
289
- | [typeof REDIRECTED, redirect: AnyRedirect]
290
- | [typeof CANCELED]
269
+ | [kind: typeof REDIRECTED, redirect: AnyRedirect]
270
+ | [kind: typeof CANCELED]
291
271
 
292
272
  type LaneResult = ProjectedLane | ControlOutcome
293
273
 
294
274
  function isControl(
295
275
  result: Lane<any> | ControlOutcome,
296
276
  ): result is ControlOutcome {
297
- return typeof result[0] === 'number'
277
+ return typeof result[0 /* location or kind */] === 'number'
298
278
  }
299
279
 
300
280
  export function waitFor<T>(
@@ -337,11 +317,11 @@ function normalize(
337
317
 
338
318
  function normalizeError(route: AnyRoute, cause: unknown): LoaderOutcome {
339
319
  let outcome = normalize(cause, true, route.id)
340
- if (outcome[0] !== ERROR) {
320
+ if (outcome[0 /* kind */] !== ERROR) {
341
321
  return outcome
342
322
  }
343
323
  try {
344
- route.options.onError?.(outcome[1])
324
+ route.options.onError?.(outcome[1 /* error */])
345
325
  } catch (onErrorCause) {
346
326
  outcome = normalize(onErrorCause, true, route.id)
347
327
  }
@@ -353,8 +333,11 @@ function normalizeLaneError(
353
333
  cause: unknown,
354
334
  options: ExecuteLaneOptions,
355
335
  ): LoaderOutcome {
356
- if (options[0].signal.aborted || !options[2]()) {
357
- options[0].abort()
336
+ if (
337
+ options[0 /* controller */].signal.aborted ||
338
+ !options[2 /* isCurrent */]()
339
+ ) {
340
+ options[0 /* controller */].abort()
358
341
  return [CANCELED]
359
342
  }
360
343
  return normalizeError(route, cause)
@@ -373,15 +356,16 @@ async function contextualize(
373
356
  lane: MatchedLane,
374
357
  options: ExecuteLaneOptions,
375
358
  end: number,
359
+ planSuccessfulLane: () => void,
376
360
  ): Promise<IndexedOutcome | undefined> {
377
361
  const [location, matches] = lane
378
- const signal = options[0].signal
379
- const preload = !!options[4]
380
- for (let index = options[7] ?? 0; index < end; index++) {
362
+ const signal = options[0 /* controller */].signal
363
+ const preload = !!options[4 /* preload */]
364
+ for (let index = options[7 /* resolvedPrefix */] ?? 0; index < end; index++) {
381
365
  const match = matches[index]!
382
366
  const route = getRoute(router, match)
383
367
 
384
- match.abortController = options[0]
368
+ match.abortController = options[0 /* controller */]
385
369
  // Contextualization is serial, so the previous match already contains the
386
370
  // complete parent context for this route.
387
371
  const parentContext =
@@ -392,7 +376,7 @@ async function contextualize(
392
376
  navigate: navigateFrom(router, location),
393
377
  buildLocation: router.buildLocation,
394
378
  cause: preload ? ('preload' as const) : match.cause,
395
- abortController: options[0],
379
+ abortController: options[0 /* controller */],
396
380
  preload,
397
381
  matches,
398
382
  routeId: route.id,
@@ -417,8 +401,8 @@ async function contextualize(
417
401
  releaseFlight(router, match)
418
402
  return [index, normalizeLaneError(route, cause, options)]
419
403
  }
420
- if (signal.aborted || !options[2]()) {
421
- options[0].abort()
404
+ if (signal.aborted || !options[2 /* isCurrent */]()) {
405
+ options[0 /* controller */].abort()
422
406
  return [index, [CANCELED]]
423
407
  }
424
408
  const validationError = match.paramsError ?? match.searchError
@@ -452,16 +436,16 @@ async function contextualize(
452
436
  if (previousStatus === 'success') {
453
437
  match.status = 'pending'
454
438
  }
455
- options[8]?.()
439
+ options[8 /* onReady */]?.()
456
440
  try {
457
- setFetching(router, match, 'beforeLoad', options[0])
441
+ setFetching(router, match, 'beforeLoad', options[0 /* controller */])
458
442
  const result = await waitFor(beforeLoad(beforeLoadContext), signal)
459
- if (!options[2]()) {
460
- options[0].abort()
443
+ if (!options[2 /* isCurrent */]()) {
444
+ options[0 /* controller */].abort()
461
445
  return [index, [CANCELED]]
462
446
  }
463
447
  const outcome = normalize(result, false, route.id)
464
- if (outcome[0] !== SUCCESS) {
448
+ if (outcome[0 /* kind */] !== SUCCESS) {
465
449
  releaseFlight(router, match)
466
450
  return [index, outcome]
467
451
  }
@@ -476,73 +460,54 @@ async function contextualize(
476
460
  if (previousStatus === 'success' && match.status === 'pending') {
477
461
  match.status = 'success'
478
462
  }
479
- setFetching(router, match, false, options[0])
463
+ setFetching(router, match, false, options[0 /* controller */])
480
464
  }
481
465
  }
482
466
 
467
+ // Let a synchronous lane claim predecessor flights before this frame yields.
468
+ planSuccessfulLane()
483
469
  return
484
470
  }
485
471
 
486
472
  function releaseOwnedFlight(
487
473
  router: AnyRouter,
488
- id: string,
474
+ match: WorkMatch,
489
475
  flight?: LoaderFlight,
490
476
  ): AbortController | undefined {
491
- if (!flight || --flight[2]) {
477
+ if (!flight || --flight[2 /* leases */]) {
492
478
  return
493
479
  }
494
- if (router._flights?.get(id) === flight) {
495
- router._flights.delete(id)
480
+ if (router._flights?.get(match.id) === flight) {
481
+ const current = router._tx
482
+ if (
483
+ current &&
484
+ !current[0 /* controller */].signal.aborted &&
485
+ !(process.env.NODE_ENV !== 'production' && current[6 /* refresh */]) &&
486
+ !current[3 /* matches */].includes(match) &&
487
+ current[3 /* matches */].some((candidate) => candidate.id === match.id) &&
488
+ current[3 /* matches */].some(
489
+ (candidate) => candidate.isFetching === 'beforeLoad',
490
+ )
491
+ ) {
492
+ // Keep work discoverable only while the current lane is still running
493
+ // beforeLoad. Loader planning performs the matching zero-owner sweep.
494
+ return
495
+ }
496
+ router._flights.delete(match.id)
496
497
  }
497
- return flight[1]
498
+ return flight[1 /* controller */]
498
499
  }
499
500
 
500
501
  function releaseFlight(router: AnyRouter, match: WorkMatch): void {
501
502
  const flight = match._flight
502
503
  match._flight = undefined
503
- releaseOwnedFlight(router, match.id, flight)?.abort()
504
- }
505
- export function laneInputs(
506
- router: AnyRouter,
507
- location: ParsedLocation,
508
- ): LaneInputs {
509
- const masked = location.maskedLocation
510
- return [
511
- router.routeTree,
512
- router.options.context ?? {},
513
- router.options.additionalContext,
514
- _getUserHistoryState(location.state),
515
- location.search,
516
- masked && [
517
- masked.href,
518
- _getUserHistoryState(masked.state),
519
- masked.search,
520
- masked.unmaskOnReload,
521
- ],
522
- ]
523
- }
524
-
525
- function samePreloadLane(
526
- preload: ActivePreload,
527
- router: AnyRouter,
528
- location: ParsedLocation,
529
- redirects: number,
530
- ): boolean {
531
- return (
532
- preload[3] === router._committed &&
533
- preload[5] === redirects &&
534
- deepEqual(preload[4], laneInputs(router, location)) &&
535
- !preload[0].some(
536
- (match) => getRoute(router, match as WorkMatch).options.preload === false,
537
- )
538
- )
504
+ releaseOwnedFlight(router, match, flight)?.abort()
539
505
  }
540
-
541
506
  /**
542
507
  * Not passing in a `next` ownership recipient
543
508
  * is equivalent to discarding the match resources
544
509
  */
545
- export function transferMatchResources(
510
+ function transferMatchResources(
546
511
  router: AnyRouter,
547
512
  previous: Array<AnyRouteMatch>,
548
513
  next?: Array<AnyRouteMatch>,
@@ -552,7 +517,7 @@ export function transferMatchResources(
552
517
  if (!next?.includes(match)) {
553
518
  const flight = match._flight
554
519
  match._flight = undefined
555
- const controller = releaseOwnedFlight(router, match.id, flight)
520
+ const controller = releaseOwnedFlight(router, match, flight)
556
521
  if (controller) {
557
522
  abort.push(controller)
558
523
  }
@@ -563,16 +528,57 @@ export function transferMatchResources(
563
528
  }
564
529
  }
565
530
 
566
- function discardPreload(router: AnyRouter, preload: ActivePreload): void {
567
- preload[1].abort()
568
- transferMatchResources(router, preload[0])
531
+ function transferPredecessorResources(
532
+ router: AnyRouter,
533
+ previous: Array<AnyRouteMatch>,
534
+ next: Array<AnyRouteMatch>,
535
+ ): void {
536
+ const abort: Array<AbortController> = []
537
+ for (const match of previous as Array<WorkMatch>) {
538
+ if (!next.includes(match)) {
539
+ const flight = match._flight
540
+ match._flight = undefined
541
+ if (
542
+ flight?.[2 /* leases */] === 1 &&
543
+ router._flights?.get(match.id) === flight &&
544
+ !(
545
+ process.env.NODE_ENV !== 'production' && router._tx?.[6 /* refresh */]
546
+ ) &&
547
+ next.some((candidate) => candidate.id === match.id)
548
+ ) {
549
+ // The successor has not made its same-ID reload decision yet.
550
+ flight[2 /* leases */] = 0
551
+ } else {
552
+ const controller = releaseOwnedFlight(router, match, flight)
553
+ if (controller) {
554
+ abort.push(controller)
555
+ }
556
+ }
557
+ }
558
+ }
559
+ for (const controller of abort) {
560
+ controller.abort()
561
+ }
562
+ }
563
+
564
+ function releaseUnownedFlights(router: AnyRouter): void {
565
+ const abort: Array<AbortController> = []
566
+ for (const [id, flight] of router._flights ?? []) {
567
+ if (!flight[2 /* leases */]) {
568
+ router._flights!.delete(id)
569
+ abort.push(flight[1 /* controller */])
570
+ }
571
+ }
572
+ for (const controller of abort) {
573
+ controller.abort()
574
+ }
569
575
  }
570
576
 
571
577
  function acquireMatchResources(matches: Array<AnyRouteMatch>): void {
572
578
  for (const match of matches as Array<WorkMatch>) {
573
579
  const flight = match._flight
574
580
  if (flight) {
575
- flight[2]++
581
+ flight[2 /* leases */]++
576
582
  }
577
583
  }
578
584
  }
@@ -584,7 +590,7 @@ function setFetching(
584
590
  owner?: AbortController,
585
591
  ): void {
586
592
  match.isFetching = value
587
- if (owner && router._tx?.[0] !== owner) {
593
+ if (owner && router._tx?.[0 /* controller */] !== owner) {
588
594
  return
589
595
  }
590
596
  const store = router.stores.byRoute.get(match.routeId)
@@ -603,7 +609,7 @@ function getLoaderContext(
603
609
  parentMatchPromise: Promise<WorkMatch> | undefined,
604
610
  preload: boolean,
605
611
  ): LoaderFnContext {
606
- const location = lane[0]
612
+ const location = lane[0 /* location */]
607
613
  return {
608
614
  params: match.params,
609
615
  location,
@@ -638,13 +644,12 @@ async function loadResource(
638
644
  }
639
645
 
640
646
  let flight = match._flight
641
- let joined = !!flight
642
647
  setFetching(router, match, 'loader', owner)
643
648
  try {
644
- for (;;) {
645
- if (!flight) {
646
- const controller = new AbortController()
647
- const outcome = Promise.resolve()
649
+ if (!flight) {
650
+ const controller = new AbortController()
651
+ flight = [
652
+ Promise.resolve()
648
653
  .then(() =>
649
654
  loader(
650
655
  getLoaderContext(
@@ -663,34 +668,35 @@ async function loadResource(
663
668
  (cause) => normalize(cause, true, route.id),
664
669
  )
665
670
  .then((result): LoaderOutcome => {
666
- return result[0] === ERROR && match._flight === flight
667
- ? normalizeError(route, result[1])
671
+ // The registry controls discovery; leases keep current consumers
672
+ // sharing the same terminal outcome.
673
+ if (
674
+ result[0 /* kind */] !== SUCCESS &&
675
+ router._flights?.get(match.id) === flight
676
+ ) {
677
+ router._flights!.delete(match.id)
678
+ if (!flight![2 /* leases */]) {
679
+ controller.abort()
680
+ }
681
+ }
682
+ return result[0 /* kind */] === ERROR && flight![2 /* leases */]
683
+ ? normalizeError(route, result[1 /* error */])
668
684
  : result
669
- })
670
- flight = [outcome, controller, 1]
671
- ;(router._flights ??= new Map()).set(match.id, flight)
672
- }
673
- match._flight = flight
674
- match.abortController = flight[1]
675
- try {
676
- const outcome = await waitFor(flight[0], signal)
677
- if (!joined || outcome[0] === SUCCESS || outcome[0] === REDIRECTED) {
678
- return outcome
679
- }
680
- } catch (cause) {
681
- if (cause === signal) {
682
- releaseFlight(router, match)
683
- return [CANCELED]
684
- }
685
- throw cause
686
- }
687
- releaseFlight(router, match)
688
- if (signal.aborted) {
689
- return [CANCELED]
690
- }
691
- flight = undefined
692
- joined = false
685
+ }),
686
+ controller,
687
+ 1,
688
+ ]
689
+ ;(router._flights ??= new Map()).set(match.id, flight)
693
690
  }
691
+ match._flight = flight
692
+ match.abortController = flight[1 /* controller */]
693
+ return await waitFor(flight[0 /* outcome */], signal)
694
+ } catch (cause) {
695
+ if (cause !== signal) {
696
+ throw cause
697
+ }
698
+ releaseFlight(router, match)
699
+ return [CANCELED]
694
700
  } finally {
695
701
  setFetching(router, match, false, owner)
696
702
  }
@@ -701,14 +707,14 @@ function settleInto(
701
707
  result: LoaderOutcome,
702
708
  preload: boolean,
703
709
  ): asserts match is SettledMatch {
704
- if (result[0] === SUCCESS) {
705
- match.loaderData = result[1]
710
+ if (result[0 /* kind */] === SUCCESS) {
711
+ match.loaderData = result[1 /* data */]
706
712
  match.error = undefined
707
713
  match.status = 'success'
708
714
  match.invalid = false
709
715
  match.updatedAt = Date.now()
710
716
  match.preload = preload
711
- } else if (result[0] !== REDIRECTED) {
717
+ } else if (result[0 /* kind */] !== REDIRECTED) {
712
718
  // Reduction installs only the selected terminal failure. Every other
713
719
  // settled attempt remains a renderable, stale match in that lane.
714
720
  match.status = 'success'
@@ -739,7 +745,7 @@ export function cacheLoaderMatch(
739
745
  context: {},
740
746
  } as WorkMatch
741
747
  if (cached._flight) {
742
- cached._flight[2]++
748
+ cached._flight[2 /* leases */]++
743
749
  }
744
750
  router._cache.set(match.id, cached)
745
751
  if (current) {
@@ -751,11 +757,11 @@ function getParentSnapshot(
751
757
  match: WorkMatch,
752
758
  outcome: LoaderOutcome,
753
759
  ): WorkMatch {
754
- if (outcome[0] === ERROR || outcome[0] === NOT_FOUND) {
760
+ if (outcome[0 /* kind */] === ERROR || outcome[0 /* kind */] === NOT_FOUND) {
755
761
  return {
756
762
  ...match,
757
- status: outcome[0] === ERROR ? 'error' : 'notFound',
758
- error: outcome[1],
763
+ status: outcome[0 /* kind */] === ERROR ? 'error' : 'notFound',
764
+ error: outcome[1 /* error */],
759
765
  _flight: undefined,
760
766
  }
761
767
  }
@@ -770,14 +776,14 @@ function createLoaderTask(
770
776
  semanticParent: Promise<WorkMatch> | undefined,
771
777
  options: ExecuteLaneOptions,
772
778
  ): Promise<WorkMatch> {
773
- const match = lane[1][index]!
779
+ const match = lane[1 /* matches */][index]!
774
780
  const route = getRoute(router, match)
775
- const preload = !!options[4]
781
+ const preload = !!options[4 /* preload */]
776
782
  const plannedCacheMatch = preload ? router._cache.get(match.id) : undefined
783
+ let configured
777
784
  let reload = false
778
785
  let reloadFailure: LoaderOutcome | undefined
779
786
  try {
780
- let configured
781
787
  if (match.status === 'success') {
782
788
  configured = route.options.shouldReload
783
789
  if (typeof configured === 'function') {
@@ -787,14 +793,14 @@ function createLoaderTask(
787
793
  lane,
788
794
  match,
789
795
  route,
790
- options[0],
796
+ options[0 /* controller */],
791
797
  semanticParent,
792
798
  preload,
793
799
  ),
794
800
  )
795
801
  }
796
- if (!options[2]()) {
797
- options[0].abort()
802
+ if (!options[2 /* isCurrent */]()) {
803
+ options[0 /* controller */].abort()
798
804
  reloadFailure = [CANCELED]
799
805
  }
800
806
  }
@@ -803,7 +809,7 @@ function createLoaderTask(
803
809
  reload = true
804
810
  } else {
805
811
  const staleAge =
806
- options[4] || match.preload
812
+ options[4 /* preload */] || match.preload
807
813
  ? (route.options.preloadStaleTime ??
808
814
  router.options.defaultPreloadStaleTime ??
809
815
  30_000)
@@ -813,9 +819,9 @@ function createLoaderTask(
813
819
  configured ||
814
820
  (configured === undefined &&
815
821
  Date.now() - match.updatedAt >= staleAge &&
816
- (options[6] ||
822
+ (options[6 /* forceStaleReload */] ||
817
823
  match.cause === 'enter' ||
818
- options[3].some(
824
+ options[3 /* base */].some(
819
825
  (candidate) =>
820
826
  candidate.routeId === match.routeId &&
821
827
  candidate.id !== match.id,
@@ -831,12 +837,27 @@ function createLoaderTask(
831
837
  const routeLoader = route.options.loader
832
838
  const loader =
833
839
  typeof routeLoader === 'function' ? routeLoader : routeLoader?.handler
840
+ let donor =
841
+ (!preload || route.options.preload !== false) &&
842
+ routeLoader &&
843
+ !(process.env.NODE_ENV !== 'production' && router._tx?.[6 /* refresh */])
844
+ ? router._flights?.get(match.id)
845
+ : undefined
846
+ if (donor === match._flight || reloadFailure) {
847
+ donor = undefined
848
+ } else if (donor && !reload && !preload && configured === undefined) {
849
+ // Normal cache policy accepts an already-running generation even when this
850
+ // lane itself would not have started another loader.
851
+ reload = true
852
+ } else if (!reload) {
853
+ donor = undefined
854
+ }
834
855
  const background = !!(
835
856
  routeLoader &&
836
857
  reload &&
837
858
  match.status === 'success' &&
838
859
  !preload &&
839
- !options[5] &&
860
+ !options[5 /* sync */] &&
840
861
  ((typeof routeLoader === 'function'
841
862
  ? undefined
842
863
  : routeLoader?.staleReloadMode) ??
@@ -845,20 +866,19 @@ function createLoaderTask(
845
866
  const loaded = reload && (!preload || route.options.preload !== false)
846
867
  const blocking =
847
868
  loaded && !background && (match.status !== 'success' || !!routeLoader)
869
+ const onLazyReady =
870
+ route.lazyFn && route._lazy !== true ? options[8 /* onReady */] : undefined
848
871
  if (loaded && !routeLoader) {
849
872
  match.invalid = false
850
873
  match.updatedAt = Date.now()
851
874
  }
852
- let donor = loaded && routeLoader ? router._flights?.get(match.id) : undefined
853
- if (donor === match._flight) {
854
- donor = undefined
855
- } else if (donor) {
856
- donor[2]++
875
+ if (donor) {
876
+ donor[2 /* leases */]++
857
877
  }
858
878
  if (blocking) {
859
879
  const acceptedFlight = match._flight
860
880
  match._flight = donor
861
- releaseOwnedFlight(router, match.id, acceptedFlight)?.abort()
881
+ releaseOwnedFlight(router, match, acceptedFlight)?.abort()
862
882
  // A successful route without a loader has no blocking work to present. It
863
883
  // still gets a task so its chunk and derived assets participate in the
864
884
  // lane, but putting it back into pending would hide an already-rendered
@@ -866,7 +886,7 @@ function createLoaderTask(
866
886
  if (match.status === 'success') {
867
887
  match.status = 'pending'
868
888
  }
869
- options[8]?.()
889
+ options[8 /* onReady */]?.()
870
890
  }
871
891
  if (!loaded) {
872
892
  match.isFetching = false
@@ -883,13 +903,17 @@ function createLoaderTask(
883
903
  loader,
884
904
  semanticParent,
885
905
  preload,
886
- options[0],
906
+ options[0 /* controller */],
887
907
  )
888
908
  const outcome = rawOutcome.then((result) => {
889
909
  if (blocking) {
890
910
  settleInto(match, result, preload)
891
- if (result[0] === SUCCESS) {
892
- if (preload && routeLoader) {
911
+ if (result[0 /* kind */] === SUCCESS) {
912
+ if (
913
+ preload &&
914
+ routeLoader &&
915
+ !options[0 /* controller */].signal.aborted
916
+ ) {
893
917
  cacheLoaderMatch(router, match, plannedCacheMatch)
894
918
  }
895
919
  // A route is renderable only after both its data and normal component
@@ -901,13 +925,10 @@ function createLoaderTask(
901
925
  })
902
926
 
903
927
  const rawChunkFailure = waitFor(
904
- Promise.resolve().then(() => loadRouteChunk(route)),
905
- options[0].signal,
928
+ Promise.resolve().then(() => loadRouteChunk(route, undefined, onLazyReady)),
929
+ options[0 /* controller */].signal,
906
930
  ).then(
907
- () => {
908
- options[8]?.()
909
- return undefined
910
- },
931
+ () => undefined,
911
932
  (cause): IndexedOutcome => [
912
933
  index,
913
934
  normalizeLaneError(route, cause, options),
@@ -918,12 +939,12 @@ function createLoaderTask(
918
939
  if (
919
940
  blocking &&
920
941
  !failure &&
921
- result[0] === SUCCESS &&
942
+ result[0 /* kind */] === SUCCESS &&
922
943
  match.status === 'pending' &&
923
- options[2]()
944
+ options[2 /* isCurrent */]()
924
945
  ) {
925
946
  match.status = 'success'
926
- options[8]?.()
947
+ options[8 /* onReady */]?.()
927
948
  }
928
949
  return failure
929
950
  }),
@@ -948,13 +969,18 @@ function createLoaderTask(
948
969
  loader,
949
970
  semanticParent,
950
971
  false,
951
- options[0],
972
+ options[0 /* controller */],
952
973
  ).then((result) => {
953
974
  match.isFetching = false
954
975
  settleInto(candidate, result, false)
955
976
  return result
956
977
  })
957
- ;(lane[2] ??= []).push([index, backgroundOutcome, chunkFailure, candidate])
978
+ ;(lane[2 /* background */] ??= []).push([
979
+ index,
980
+ backgroundOutcome,
981
+ chunkFailure,
982
+ candidate,
983
+ ])
958
984
  return backgroundOutcome.then((result) =>
959
985
  getParentSnapshot(candidate, result),
960
986
  )
@@ -967,10 +993,12 @@ async function getNotFoundBoundary(
967
993
  signal: AbortSignal,
968
994
  fallback = 0,
969
995
  ): Promise<number> {
970
- const cause = indexed?.[1][1] as NotFoundError | undefined
996
+ const cause = indexed?.[1 /* outcome */][1 /* error or redirect */] as
997
+ | NotFoundError
998
+ | undefined
971
999
  let index = cause?.routeId
972
1000
  ? matches.findIndex((match) => match.routeId === cause.routeId)
973
- : (indexed?.[0] ?? matches.length - 1)
1001
+ : (indexed?.[0 /* index */] ?? matches.length - 1)
974
1002
  if (index < 0) {
975
1003
  index = 0
976
1004
  }
@@ -994,12 +1022,12 @@ async function getNotFoundBoundary(
994
1022
  }
995
1023
 
996
1024
  function discardBackground(router: AnyRouter, lane: Lane<any>): void {
997
- if (lane[2]) {
1025
+ if (lane[2 /* background */]) {
998
1026
  transferMatchResources(
999
1027
  router,
1000
- lane[2].map((task) => task[3]),
1028
+ lane[2 /* background */].map((task) => task[3 /* candidate */]),
1001
1029
  )
1002
- lane[2] = undefined
1030
+ lane[2 /* background */] = undefined
1003
1031
  }
1004
1032
  }
1005
1033
 
@@ -1014,26 +1042,29 @@ async function settleTasks(
1014
1042
  try {
1015
1043
  await Promise.all(
1016
1044
  tasks.map((task) =>
1017
- task[1].then(async (outcome) => {
1018
- const taskIndex = task[0]
1045
+ task[1 /* outcome */].then(async (outcome) => {
1046
+ const taskIndex = task[0 /* index */]
1019
1047
  if (gate && taskIndex >= (await gate)) {
1020
1048
  return
1021
1049
  }
1022
- if (outcome[0] >= REDIRECTED) {
1050
+ if (outcome[0 /* kind */] >= REDIRECTED) {
1023
1051
  throw [taskIndex, outcome] as IndexedOutcome
1024
1052
  }
1025
- if (!loaderFailure && outcome[0] !== SUCCESS) {
1053
+ if (!loaderFailure && outcome[0 /* kind */] !== SUCCESS) {
1026
1054
  loaderFailure = [taskIndex, outcome]
1027
1055
  // Every started descendant must settle before an ordinary failure
1028
1056
  // wins because a redirect from any of them remains control flow.
1029
1057
  await Promise.all(
1030
1058
  (redirectTasks ?? []).map((nextTask) => {
1031
- if (nextTask[0] <= taskIndex) {
1059
+ if (nextTask[0 /* index */] <= taskIndex) {
1032
1060
  return
1033
1061
  }
1034
- return nextTask[1].then((nextOutcome) => {
1035
- if (nextOutcome[0] === REDIRECTED) {
1036
- throw [nextTask[0], nextOutcome] as IndexedOutcome
1062
+ return nextTask[1 /* outcome */].then((nextOutcome) => {
1063
+ if (nextOutcome[0 /* kind */] === REDIRECTED) {
1064
+ throw [
1065
+ nextTask[0 /* index */],
1066
+ nextOutcome,
1067
+ ] as IndexedOutcome
1037
1068
  }
1038
1069
  })
1039
1070
  }),
@@ -1057,43 +1088,43 @@ async function reduceLane(
1057
1088
  settlement: Promise<IndexedOutcome | undefined>,
1058
1089
  onReady?: () => void,
1059
1090
  ): Promise<ReducedLane | ControlOutcome> {
1060
- const matches = lane[1]
1091
+ const matches = lane[1 /* matches */]
1061
1092
  let failure = await settlement
1062
1093
  let redirectLimitExceeded = false
1063
1094
  const plannedBoundary = matches.findIndex((match) => match._notFound)
1064
1095
  const boundaryOf = (found: IndexedOutcome) =>
1065
- found[1][0] === NOT_FOUND
1096
+ found[1 /* outcome */][0 /* kind */] === NOT_FOUND
1066
1097
  ? getNotFoundBoundary(router, matches, found, controller.signal)
1067
- : found[0]
1098
+ : found[0 /* index */]
1068
1099
  let readinessEnd = plannedBoundary < 0 ? matches.length : plannedBoundary
1069
1100
 
1070
- if ((failure?.[1][0] ?? 0) >= REDIRECTED) {
1101
+ if ((failure?.[1 /* outcome */][0 /* kind */] ?? 0) >= REDIRECTED) {
1071
1102
  readinessEnd = 0
1072
1103
  } else if (failure) {
1073
- readinessEnd = failure[2] ??= await boundaryOf(failure)
1104
+ readinessEnd = failure[2 /* boundary */] ??= await boundaryOf(failure)
1074
1105
  for (const task of tasks) {
1075
- if (task[0] >= readinessEnd) {
1106
+ if (task[0 /* index */] >= readinessEnd) {
1076
1107
  break
1077
1108
  }
1078
- const outcome = await task[1]
1109
+ const outcome = await task[1 /* outcome */]
1079
1110
  // Presence means a loader previously succeeded, even with `undefined`.
1080
1111
  if (
1081
- outcome[0] !== SUCCESS &&
1082
- outcome[0] < REDIRECTED &&
1083
- !('loaderData' in matches[task[0]]!)
1112
+ outcome[0 /* kind */] !== SUCCESS &&
1113
+ outcome[0 /* kind */] < REDIRECTED &&
1114
+ !('loaderData' in matches[task[0 /* index */]]!)
1084
1115
  ) {
1085
- failure = [task[0], outcome]
1086
- readinessEnd = failure[2] = await boundaryOf(failure)
1116
+ failure = [task[0 /* index */], outcome]
1117
+ readinessEnd = failure[2 /* boundary */] = await boundaryOf(failure)
1087
1118
  break
1088
1119
  }
1089
1120
  }
1090
1121
  }
1091
1122
 
1092
1123
  for (const task of tasks) {
1093
- if (task[0] >= readinessEnd) {
1124
+ if (task[0 /* index */] >= readinessEnd) {
1094
1125
  break
1095
1126
  }
1096
- const chunkFailure = await task[2]
1127
+ const chunkFailure = await task[2 /* chunkFailure */]
1097
1128
  if (!chunkFailure) {
1098
1129
  continue
1099
1130
  }
@@ -1101,11 +1132,11 @@ async function reduceLane(
1101
1132
  break
1102
1133
  }
1103
1134
 
1104
- if ((failure?.[1][0] ?? 0) >= REDIRECTED) {
1105
- const outcome = failure![1]
1135
+ if ((failure?.[1 /* outcome */][0 /* kind */] ?? 0) >= REDIRECTED) {
1136
+ const outcome = failure![1 /* outcome */]
1106
1137
  if (
1107
- outcome[0] !== REDIRECTED ||
1108
- outcome[1].options.reloadDocument ||
1138
+ outcome[0 /* kind */] !== REDIRECTED ||
1139
+ outcome[1 /* redirect */].options.reloadDocument ||
1109
1140
  redirects < 20
1110
1141
  ) {
1111
1142
  discardBackground(router, lane)
@@ -1116,13 +1147,13 @@ async function reduceLane(
1116
1147
  }
1117
1148
 
1118
1149
  const boundary = failure
1119
- ? (failure[2] ?? (await boundaryOf(failure)))
1150
+ ? (failure[2 /* boundary */] ?? (await boundaryOf(failure)))
1120
1151
  : plannedBoundary
1121
1152
  if (boundary >= 0) {
1122
- const outcome = failure?.[1]
1123
- const kind = outcome?.[0]
1153
+ const outcome = failure?.[1 /* outcome */]
1154
+ const kind = outcome?.[0 /* kind */]
1124
1155
  const match = matches[boundary]!
1125
- const cause = outcome?.[1]
1156
+ const cause = outcome?.[1 /* error or redirect */]
1126
1157
  const install = () => {
1127
1158
  if (outcome) {
1128
1159
  match._notFound = undefined
@@ -1169,9 +1200,11 @@ async function reduceLane(
1169
1200
  } else if (redirectLimitExceeded) {
1170
1201
  controller.abort()
1171
1202
  await Promise.all([
1172
- ...tasks.map((task) => task[1]),
1173
- ...tasks.map((task) => task[2]),
1174
- ...(lane[2] ?? []).map((task) => task[1]),
1203
+ ...tasks.map((task) => task[1 /* outcome */]),
1204
+ ...tasks.map((task) => task[2 /* chunkFailure */]),
1205
+ ...(lane[2 /* background */] ?? []).map(
1206
+ (task) => task[1 /* outcome */],
1207
+ ),
1175
1208
  ])
1176
1209
  discardBackground(router, lane)
1177
1210
  transferMatchResources(router, matches)
@@ -1187,9 +1220,9 @@ export async function projectLane(
1187
1220
  lane: ReducedLane,
1188
1221
  signal: AbortSignal,
1189
1222
  start = 0,
1190
- end = lane[1].length,
1223
+ end = lane[1 /* matches */].length,
1191
1224
  ): Promise<ProjectedLane> {
1192
- const matches = lane[1]
1225
+ const matches = lane[1 /* matches */]
1193
1226
  for (let index = start; index < end; index++) {
1194
1227
  const match = matches[index]!
1195
1228
  const routeOptions = getRoute(router, match).options
@@ -1239,9 +1272,9 @@ async function executeClientLane(
1239
1272
  if (router.options.notFoundMode !== 'root' && plannedBoundary >= 0) {
1240
1273
  const boundary = await getNotFoundBoundary(
1241
1274
  router,
1242
- matched[1],
1275
+ matched[1 /* matches */],
1243
1276
  undefined,
1244
- options[0].signal,
1277
+ options[0 /* controller */].signal,
1245
1278
  plannedBoundary,
1246
1279
  )
1247
1280
  if (boundary !== plannedBoundary) {
@@ -1251,42 +1284,54 @@ async function executeClientLane(
1251
1284
  plannedBoundary = boundary
1252
1285
  }
1253
1286
  let end = plannedBoundary < 0 ? matches.length : plannedBoundary + 1
1254
- // From here on `matched` is contextualized: `contextualize` communicates
1255
- // through mutation plus a failure return, so the phase brand is asserted at
1256
- // the two use sites below rather than granted by a (byte-costing) return.
1257
- const failure = await contextualize(router, matched, options, end)
1258
- if (failure) {
1259
- options[5] = true
1260
- }
1261
1287
  const tasks: Array<LoaderTask> = []
1262
- const start = options[7] ?? 0
1288
+ const start = options[7 /* resolvedPrefix */] ?? 0
1263
1289
  let semanticParent = start
1264
- ? Promise.resolve(matched[1][start - 1]!)
1290
+ ? Promise.resolve(matched[1 /* matches */][start - 1]!)
1265
1291
  : undefined
1266
- end = failure?.[0] ?? end
1267
- if (failure?.[1][0] === NOT_FOUND) {
1268
- failure[2] = await getNotFoundBoundary(
1269
- router,
1270
- matched[1],
1271
- failure,
1272
- options[0].signal,
1273
- )
1274
- end = Math.min(end, failure[2] + 1)
1275
- } else if ((failure?.[1][0] ?? 0) >= REDIRECTED) {
1276
- end = 0
1292
+ const planSuccessfulLane = () => {
1293
+ for (let index = start; index < end; index++) {
1294
+ if (options[0 /* controller */].signal.aborted) {
1295
+ break
1296
+ }
1297
+ semanticParent = createLoaderTask(
1298
+ router,
1299
+ matched as ContextualizedLane,
1300
+ index,
1301
+ tasks,
1302
+ semanticParent,
1303
+ options,
1304
+ )
1305
+ }
1277
1306
  }
1278
- for (let index = start; index < end; index++) {
1279
- if (options[0].signal.aborted) {
1280
- break
1307
+ // From here on `matched` is contextualized: `contextualize` communicates
1308
+ // through mutation plus a failure return, so the phase brand is asserted at
1309
+ // the two use sites below rather than granted by a (byte-costing) return.
1310
+ const failure = await contextualize(
1311
+ router,
1312
+ matched,
1313
+ options,
1314
+ end,
1315
+ planSuccessfulLane,
1316
+ )
1317
+ if (failure) {
1318
+ options[5 /* sync */] = true
1319
+ end = failure[0 /* index */]
1320
+ if (failure[1 /* outcome */][0 /* kind */] === NOT_FOUND) {
1321
+ failure[2 /* boundary */] = await getNotFoundBoundary(
1322
+ router,
1323
+ matched[1 /* matches */],
1324
+ failure,
1325
+ options[0 /* controller */].signal,
1326
+ )
1327
+ end = Math.min(end, failure[2 /* boundary */] + 1)
1328
+ } else if (failure[1 /* outcome */][0 /* kind */] >= REDIRECTED) {
1329
+ end = 0
1281
1330
  }
1282
- semanticParent = createLoaderTask(
1283
- router,
1284
- matched as ContextualizedLane,
1285
- index,
1286
- tasks,
1287
- semanticParent,
1288
- options,
1289
- )
1331
+ planSuccessfulLane()
1332
+ }
1333
+ if (options[2 /* isCurrent */]() && !options[4 /* preload */]) {
1334
+ releaseUnownedFlights(router)
1290
1335
  }
1291
1336
  let reduced: ReducedLane | ControlOutcome
1292
1337
  try {
@@ -1294,21 +1339,21 @@ async function executeClientLane(
1294
1339
  router,
1295
1340
  matched as ContextualizedLane,
1296
1341
  tasks,
1297
- options[0],
1298
- options[1],
1299
- settleTasks(tasks, failure, matched[2]),
1300
- options[8],
1342
+ options[0 /* controller */],
1343
+ options[1 /* redirects */],
1344
+ settleTasks(tasks, failure, matched[2 /* background */]),
1345
+ options[8 /* onReady */],
1301
1346
  )
1302
- if (matched[2]?.length) {
1303
- matched[3] = settleTasks(
1304
- matched[2],
1347
+ if (matched[2 /* background */]?.length) {
1348
+ matched[3 /* backgroundSettlement */] = settleTasks(
1349
+ matched[2 /* background */],
1305
1350
  undefined,
1306
1351
  undefined,
1307
1352
  reduction.then(
1308
1353
  (foreground) =>
1309
1354
  isControl(foreground)
1310
1355
  ? 0
1311
- : _getRenderedMatches(foreground[1]).length,
1356
+ : _getRenderedMatches(foreground[1 /* matches */]).length,
1312
1357
  () => 0,
1313
1358
  ),
1314
1359
  )
@@ -1324,8 +1369,10 @@ async function executeClientLane(
1324
1369
  return projectLane(
1325
1370
  router,
1326
1371
  reduced,
1327
- options[0].signal,
1328
- options[7] === reduced[1].length ? options[7] : 0,
1372
+ options[0 /* controller */].signal,
1373
+ options[7 /* resolvedPrefix */] === reduced[1 /* matches */].length
1374
+ ? options[7 /* resolvedPrefix */]
1375
+ : 0,
1329
1376
  )
1330
1377
  }
1331
1378
 
@@ -1384,71 +1431,83 @@ function offerPending(router: CoordinatorRouter, tx: LoadTransaction): void {
1384
1431
  }
1385
1432
  let session = router._pending
1386
1433
  let tookOver = false
1387
- const sessionMatchId = session?.[0][3][session[1]]?.id
1388
- if (session?.[0] !== tx) {
1389
- if (session && tx[3][session[1]]?.id === sessionMatchId) {
1390
- session[0] = tx
1434
+ const sessionMatchId =
1435
+ session?.[0 /* owner */][3 /* matches */][session[1 /* boundary */]]?.id
1436
+ if (session?.[0 /* owner */] !== tx) {
1437
+ if (
1438
+ session &&
1439
+ tx[3 /* matches */][session[1 /* boundary */]]?.id === sessionMatchId
1440
+ ) {
1441
+ session[0 /* owner */] = tx
1391
1442
  tookOver = true
1392
1443
  } else {
1393
- clearTimeout(session?.[3])
1444
+ clearTimeout(session?.[3 /* timer */])
1394
1445
  router._pending = session = undefined
1395
1446
  }
1396
1447
  }
1397
- const config = pendingConfig(router, tx[3])
1448
+ const config = pendingConfig(router, tx[3 /* matches */])
1398
1449
  if (!config) {
1399
1450
  return
1400
1451
  }
1401
1452
  const [delay, boundary, min, component] = config
1402
- const matchId = tx[3][boundary]!.id
1403
- if (!session || session[1] !== boundary || sessionMatchId !== matchId) {
1453
+ const matchId = tx[3 /* matches */][boundary]!.id
1454
+ if (
1455
+ !session ||
1456
+ session[1 /* boundary */] !== boundary ||
1457
+ sessionMatchId !== matchId
1458
+ ) {
1404
1459
  // Hydration and redirects can preserve pending presentation without a session.
1405
1460
  // Do not delay it again; conservatively start pendingMinMs from now.
1406
- clearTimeout(session?.[3])
1461
+ clearTimeout(session?.[3 /* timer */])
1407
1462
  const presented = router.stores.matches.get()[boundary]
1408
1463
  const visible = presented?.id === matchId && presented.status === 'pending'
1409
1464
  router._pending = session = [
1410
1465
  tx,
1411
1466
  boundary,
1412
- visible ? Date.now() + min : tx[4] + delay,
1467
+ visible ? Date.now() + min : tx[4 /* startedAt */] + delay,
1413
1468
  undefined,
1414
1469
  visible ? Promise.resolve(true) : undefined,
1415
1470
  component,
1416
1471
  ]
1417
1472
  }
1418
- if (session[4] && !tookOver && session[5] === component) {
1473
+ if (
1474
+ session[4 /* ack */] &&
1475
+ !tookOver &&
1476
+ session[5 /* component */] === component
1477
+ ) {
1419
1478
  return
1420
1479
  }
1421
- session[5] = component
1422
- if (!session[4]) {
1423
- clearTimeout(session[3])
1424
- const remaining = session[2] - Date.now()
1480
+ session[5 /* component */] = component
1481
+ if (!session[4 /* ack */]) {
1482
+ clearTimeout(session[3 /* timer */])
1483
+ const remaining = session[2 /* deadline */] - Date.now()
1425
1484
  if (remaining > 0) {
1426
- session[3] = setTimeout(() => {
1485
+ session[3 /* timer */] = setTimeout(() => {
1427
1486
  offerPending(router, tx)
1428
1487
  }, remaining)
1429
1488
  return
1430
1489
  }
1431
- session[2] = 0
1490
+ session[2 /* deadline */] = 0
1432
1491
  }
1433
- const offered = tx[3].map((match) => ({
1492
+ const offered = tx[3 /* matches */].map((match) => ({
1434
1493
  ...match,
1435
1494
  _flight: undefined,
1436
1495
  }))
1437
1496
  offered[boundary]!.status = 'pending'
1438
1497
  const ack = router
1439
- .startTransition(() => router.stores.setMatches(offered), offered, true)
1498
+ .startTransition(() => router.stores.setMatches(offered), offered)
1440
1499
  .then((rendered) => {
1441
1500
  if (
1442
1501
  rendered &&
1443
1502
  router._pending === session &&
1444
- session[4] === ack &&
1445
- !session[2]
1503
+ session[4 /* ack */] === ack &&
1504
+ !session[2 /* deadline */]
1446
1505
  ) {
1447
- session[2] = Date.now() + min
1506
+ session[2 /* deadline */] = Date.now() + min
1448
1507
  }
1449
1508
  return rendered
1450
1509
  })
1451
- session[4] = ack
1510
+ session[4 /* ack */] = ack
1452
1511
  }
1453
1512
 
1454
1513
  /**
@@ -1457,8 +1516,8 @@ function offerPending(router: CoordinatorRouter, tx: LoadTransaction): void {
1457
1516
  */
1458
1517
  function finishPending(router: CoordinatorRouter, tx: LoadTransaction): void {
1459
1518
  const session = router._pending
1460
- if (session?.[0] === tx) {
1461
- clearTimeout(session[3])
1519
+ if (session?.[0 /* owner */] === tx) {
1520
+ clearTimeout(session[3 /* timer */])
1462
1521
  router._pending = undefined
1463
1522
  }
1464
1523
  }
@@ -1472,7 +1531,7 @@ function publishMatches(
1472
1531
  }
1473
1532
 
1474
1533
  function discardLane(router: AnyRouter, lane: ProjectedLane): void {
1475
- transferMatchResources(router, lane[1])
1534
+ transferMatchResources(router, lane[1 /* matches */])
1476
1535
  discardBackground(router, lane)
1477
1536
  }
1478
1537
 
@@ -1534,7 +1593,7 @@ function commitMatches(
1534
1593
  )
1535
1594
  }
1536
1595
  // The lane becomes committed before publication can synchronously reenter.
1537
- tx[3] = []
1596
+ tx[3 /* matches */] = []
1538
1597
  router._cache = cached
1539
1598
  publishMatches(router, matches)
1540
1599
  transferMatchResources(
@@ -1559,7 +1618,7 @@ function commitRefreshMatches(
1559
1618
  const cached = new Map<string, AnyRouteMatch>()
1560
1619
  // Delay releasing the previous owners until the HMR render is acknowledged.
1561
1620
  // Old generations must not become reusable cache entries after refresh.
1562
- tx[3] = []
1621
+ tx[3 /* matches */] = []
1563
1622
  router._cache = cached
1564
1623
  checkpoint.previousMatches = previous
1565
1624
  checkpoint.previousCache = previousCached
@@ -1595,7 +1654,7 @@ function rollbackPublication(
1595
1654
  if (
1596
1655
  !checkpoint.published ||
1597
1656
  router._tx !== tx ||
1598
- router._committed !== lane[1]
1657
+ router._committed !== lane[1 /* matches */]
1599
1658
  ) {
1600
1659
  settlePublication(router, checkpoint)
1601
1660
  return false
@@ -1625,7 +1684,7 @@ function rollbackPublication(
1625
1684
  router.stores.status.set('idle')
1626
1685
  router.stores.setMatches(checkpoint.previousPresentation)
1627
1686
  })
1628
- tx[0].abort()
1687
+ tx[0 /* controller */].abort()
1629
1688
  transferMatchResources(router, discarded, restored)
1630
1689
  discardBackground(router, lane)
1631
1690
  if (router._tx === tx && router._commitPromise === checkpoint.commitPromise) {
@@ -1641,17 +1700,18 @@ async function transitionRefresh(
1641
1700
  lane: ProjectedLane,
1642
1701
  changeInfo: ReturnType<typeof getLocationChangeInfo>,
1643
1702
  ): Promise<boolean | undefined> {
1703
+ const refresh = tx[6 /* refresh */]!
1644
1704
  const checkpoint: PublicationCheckpoint = {
1645
1705
  previousMatches: router._committed,
1646
- previousPresentation: tx[6]?.[0] ?? router.stores.matches.get(),
1706
+ previousPresentation: refresh[0 /* presentation */],
1647
1707
  previousCache: router._cache,
1648
1708
  commitPromise: router._commitPromise,
1649
1709
  published: false,
1650
1710
  }
1651
1711
  const commit = () => {
1652
1712
  finishPending(router, tx)
1653
- router._rollbackRefresh = rollback
1654
- commitRefreshMatches(router, tx, lane[1], checkpoint)
1713
+ refresh[2 /* rollback */] = rollback
1714
+ commitRefreshMatches(router, tx, lane[1 /* matches */], checkpoint)
1655
1715
  if (!checkpoint.published || router._tx !== tx) {
1656
1716
  return
1657
1717
  }
@@ -1661,25 +1721,25 @@ async function transitionRefresh(
1661
1721
  }
1662
1722
  }
1663
1723
  const rollback = () => {
1664
- if (router._rollbackRefresh === rollback) {
1665
- router._rollbackRefresh = undefined
1724
+ if (refresh[2 /* rollback */] === rollback) {
1725
+ refresh[2 /* rollback */] = undefined
1666
1726
  }
1667
1727
  const restored = rollbackPublication(router, tx, lane, checkpoint)
1668
1728
  router._cancelTransition?.()
1669
1729
  return restored
1670
1730
  }
1671
1731
  try {
1672
- const rendered = await router.startTransition(commit, lane[1])
1673
- if (router._rollbackRefresh === rollback) {
1674
- router._rollbackRefresh = undefined
1732
+ const rendered = await router.startTransition(commit, lane[1 /* matches */])
1733
+ if (refresh[2 /* rollback */] === rollback) {
1734
+ refresh[2 /* rollback */] = undefined
1675
1735
  }
1676
1736
  if (checkpoint.published) {
1677
- const handoff = tx[6]?.[1]
1737
+ const handoff = refresh[1 /* handoff */]
1678
1738
  if (handoff && router._handoff === handoff) {
1679
- handoff[1]()
1739
+ handoff[1 /* finish */]()
1680
1740
  }
1681
1741
  if (router._tx === tx) {
1682
- tx[6] = undefined
1742
+ tx[6 /* refresh */] = undefined
1683
1743
  }
1684
1744
  }
1685
1745
  settlePublication(router, checkpoint)
@@ -1698,7 +1758,7 @@ async function awaitCurrent(
1698
1758
  ): Promise<void> {
1699
1759
  let current = router._tx
1700
1760
  while (current && current !== owner) {
1701
- await current[5]
1761
+ await current[5 /* done */]
1702
1762
  if (router._tx === current) {
1703
1763
  return
1704
1764
  }
@@ -1715,7 +1775,7 @@ async function followRedirect(
1715
1775
  ...redirect.options,
1716
1776
  replace: true,
1717
1777
  ignoreBlocker: true,
1718
- _redirects: tx[1] + 1,
1778
+ _redirects: tx[1 /* redirects */] + 1,
1719
1779
  } as any)
1720
1780
  }
1721
1781
 
@@ -1724,9 +1784,9 @@ function restoreCommitted(
1724
1784
  tx: LoadTransaction,
1725
1785
  ): void {
1726
1786
  finishPending(router, tx)
1727
- tx[0].abort()
1728
- transferMatchResources(router, tx[3])
1729
- tx[3] = []
1787
+ tx[0 /* controller */].abort()
1788
+ transferMatchResources(router, tx[3 /* matches */])
1789
+ tx[3 /* matches */] = []
1730
1790
  if (router._tx !== tx) {
1731
1791
  return
1732
1792
  }
@@ -1750,15 +1810,22 @@ async function runBackground(
1750
1810
  const next = base.map((match) => ({ ...match }))
1751
1811
  acquireMatchResources(next)
1752
1812
  for (const task of tasks) {
1753
- releaseFlight(router, next[task[0]]!)
1754
- next[task[0]] = task[3]
1813
+ releaseFlight(router, next[task[0 /* index */]]!)
1814
+ next[task[0 /* index */]] = task[3 /* candidate */]
1755
1815
  }
1756
1816
  // Phase jump: the clones inherit beforeLoad context from the committed
1757
1817
  // foreground lane, which already ran `contextualize` for these matches.
1758
- const lane = [tx[2], next] as ContextualizedLane
1818
+ const lane = [tx[2 /* location */], next] as ContextualizedLane
1759
1819
  let reduced: ReducedLane | ControlOutcome
1760
1820
  try {
1761
- reduced = await reduceLane(router, lane, tasks, tx[0], tx[1], settlement)
1821
+ reduced = await reduceLane(
1822
+ router,
1823
+ lane,
1824
+ tasks,
1825
+ tx[0 /* controller */],
1826
+ tx[1 /* redirects */],
1827
+ settlement,
1828
+ )
1762
1829
  } catch (cause) {
1763
1830
  transferMatchResources(router, next)
1764
1831
  throw cause
@@ -1766,28 +1833,32 @@ async function runBackground(
1766
1833
  if (isControl(reduced)) {
1767
1834
  transferMatchResources(router, next)
1768
1835
  if (
1769
- reduced[0] === REDIRECTED &&
1836
+ reduced[0 /* kind */] === REDIRECTED &&
1770
1837
  router._tx === tx &&
1771
1838
  router._committed === base
1772
1839
  ) {
1773
- await followRedirect(router, tx, reduced[1])
1840
+ await followRedirect(router, tx, reduced[1 /* redirect */])
1774
1841
  }
1775
1842
  return
1776
1843
  }
1777
- const projected = await projectLane(router, reduced, tx[0].signal)
1844
+ const projected = await projectLane(
1845
+ router,
1846
+ reduced,
1847
+ tx[0 /* controller */].signal,
1848
+ )
1778
1849
  if (router._tx !== tx || router._committed !== base) {
1779
- transferMatchResources(router, projected[1])
1850
+ transferMatchResources(router, projected[1 /* matches */])
1780
1851
  return
1781
1852
  }
1782
- for (const match of projected[1] as Array<WorkMatch>) {
1853
+ for (const match of projected[1 /* matches */] as Array<WorkMatch>) {
1783
1854
  const cached = router._cache.get(match.id) as WorkMatch | undefined
1784
1855
  if (cached?._flight && cached._flight === match._flight) {
1785
1856
  router._cache.delete(match.id)
1786
1857
  releaseFlight(router, cached)
1787
1858
  }
1788
1859
  }
1789
- publishMatches(router, projected[1])
1790
- transferMatchResources(router, base, projected[1])
1860
+ publishMatches(router, projected[1 /* matches */])
1861
+ transferMatchResources(router, base, projected[1 /* matches */])
1791
1862
  }
1792
1863
 
1793
1864
  async function runClientTransaction(
@@ -1797,13 +1868,11 @@ async function runClientTransaction(
1797
1868
  onReady?: () => void,
1798
1869
  sync?: boolean,
1799
1870
  resolvedPrefix?: number,
1800
- adopted?: ActivePreload,
1801
- retained?: ActivePreload,
1802
1871
  ): Promise<void> {
1803
1872
  const options: ExecuteLaneOptions = [
1804
- tx[0],
1805
- tx[1],
1806
- () => router._tx === tx && !!tx[3].length,
1873
+ tx[0 /* controller */],
1874
+ tx[1 /* redirects */],
1875
+ () => router._tx === tx && !!tx[3 /* matches */].length,
1807
1876
  router._committed,
1808
1877
  undefined,
1809
1878
  sync,
@@ -1811,52 +1880,23 @@ async function runClientTransaction(
1811
1880
  resolvedPrefix,
1812
1881
  onReady,
1813
1882
  ]
1814
- let result: LaneResult
1815
- try {
1816
- result = adopted
1817
- ? await adopted[2]
1818
- : await executeClientLane(router, tx[2], tx[3], options)
1819
- } finally {
1820
- if (retained) {
1821
- discardPreload(router, retained)
1822
- }
1823
- }
1824
- if (
1825
- adopted &&
1826
- router._tx === tx &&
1827
- ((isControl(result) && result[0] === CANCELED) ||
1828
- (!isControl(result) &&
1829
- result[1].some(
1830
- (match) => match.status !== 'success' || match._notFound,
1831
- )))
1832
- ) {
1833
- // Successful loaders already seeded the cache; retry only the guard lane.
1834
- const donors = tx[3] as Array<WorkMatch>
1835
- tx[3] = []
1836
- transferMatchResources(router, donors)
1837
- tx[0].abort()
1838
- if (router._tx !== tx) {
1839
- return
1840
- }
1841
- const controller = new AbortController()
1842
- tx[0] = options[0] = controller
1843
- tx[3] = router.matchRoutes(tx[2], {
1844
- _controller: controller,
1845
- })
1846
- acquireMatchResources(tx[3])
1847
- result = await executeClientLane(router, tx[2], tx[3], options)
1848
- }
1883
+ const result = await executeClientLane(
1884
+ router,
1885
+ tx[2 /* location */],
1886
+ tx[3 /* matches */],
1887
+ options,
1888
+ )
1849
1889
 
1850
1890
  if (isControl(result)) {
1851
- if (result[0] === REDIRECTED && router._tx === tx) {
1891
+ if (result[0 /* kind */] === REDIRECTED && router._tx === tx) {
1852
1892
  finishPending(router, tx)
1853
- transferMatchResources(router, tx[3])
1854
- tx[3] = []
1893
+ transferMatchResources(router, tx[3 /* matches */])
1894
+ tx[3 /* matches */] = []
1855
1895
  if (router._tx === tx) {
1856
- if (process.env.NODE_ENV !== 'production' && tx[6]) {
1896
+ if (process.env.NODE_ENV !== 'production' && tx[6 /* refresh */]) {
1857
1897
  router._refreshNextLoad = true
1858
1898
  }
1859
- await followRedirect(router, tx, result[1])
1899
+ await followRedirect(router, tx, result[1 /* redirect */])
1860
1900
  }
1861
1901
  } else {
1862
1902
  restoreCommitted(router, tx)
@@ -1864,36 +1904,40 @@ async function runClientTransaction(
1864
1904
  return
1865
1905
  }
1866
1906
  const pending = router._pending
1867
- if (pending?.[0] === tx) {
1907
+ if (pending?.[0 /* owner */] === tx) {
1868
1908
  /**
1869
1909
  * Loading finished, so cancel any pending reveal. If the fallback rendered,
1870
1910
  * wait out the rest of `pendingMinMs` before replacing it. If it never
1871
1911
  * rendered, there is no minimum wait; if another load took it over, that
1872
1912
  * load owns the deadline.
1873
1913
  */
1874
- clearTimeout(pending[3])
1875
- if (pending[4]) {
1876
- const signal = tx[0].signal
1914
+ clearTimeout(pending[3 /* timer */])
1915
+ if (pending[4 /* ack */]) {
1916
+ const signal = tx[0 /* controller */].signal
1877
1917
  let rendered = false
1878
1918
  try {
1879
- rendered = await waitFor(pending[4], signal)
1919
+ rendered = await waitFor(pending[4 /* ack */], signal)
1880
1920
  } catch (cause) {
1881
1921
  if (cause !== signal) {
1882
1922
  throw cause
1883
1923
  }
1884
1924
  }
1885
- if (rendered && router._pending === pending && pending[0] === tx) {
1886
- const remaining = pending[2] - Date.now()
1925
+ if (
1926
+ rendered &&
1927
+ router._pending === pending &&
1928
+ pending[0 /* owner */] === tx
1929
+ ) {
1930
+ const remaining = pending[2 /* deadline */] - Date.now()
1887
1931
  if (remaining > 0) {
1888
1932
  try {
1889
1933
  await waitFor(
1890
1934
  new Promise<void>((resolve) => {
1891
- pending[3] = setTimeout(resolve, remaining)
1935
+ pending[3 /* timer */] = setTimeout(resolve, remaining)
1892
1936
  }),
1893
1937
  signal,
1894
1938
  )
1895
1939
  } catch {}
1896
- clearTimeout(pending[3])
1940
+ clearTimeout(pending[3 /* timer */])
1897
1941
  }
1898
1942
  }
1899
1943
  }
@@ -1903,12 +1947,12 @@ async function runClientTransaction(
1903
1947
  discardLane(router, result)
1904
1948
  return
1905
1949
  }
1906
- const toLocation = tx[2]
1950
+ const toLocation = tx[2 /* location */]
1907
1951
  const changeInfo = getLocationChangeInfo(
1908
1952
  toLocation,
1909
1953
  router.stores.resolvedLocation.get(),
1910
1954
  )
1911
- const background = result[2]
1955
+ const background = result[2 /* background */]
1912
1956
  await router.startViewTransition(async () => {
1913
1957
  if (router._tx !== tx) {
1914
1958
  discardLane(router, result)
@@ -1916,7 +1960,7 @@ async function runClientTransaction(
1916
1960
  }
1917
1961
  const commit = () => {
1918
1962
  finishPending(router, tx)
1919
- commitMatches(router, tx, result[1], resolvedPrefix)
1963
+ commitMatches(router, tx, result[1 /* matches */], resolvedPrefix)
1920
1964
  if (router._tx !== tx) {
1921
1965
  return
1922
1966
  }
@@ -1926,12 +1970,12 @@ async function runClientTransaction(
1926
1970
  }
1927
1971
  }
1928
1972
  const rendered =
1929
- process.env.NODE_ENV !== 'production' && tx[6]
1973
+ process.env.NODE_ENV !== 'production' && tx[6 /* refresh */]
1930
1974
  ? await transitionRefresh(router, tx, result, changeInfo)
1931
- : await router.startTransition(commit, result[1])
1975
+ : await router.startTransition(commit, result[1 /* matches */])
1932
1976
  if (
1933
1977
  process.env.NODE_ENV !== 'production' &&
1934
- tx[6] &&
1978
+ tx[6 /* refresh */] &&
1935
1979
  rendered === undefined
1936
1980
  ) {
1937
1981
  return
@@ -1944,9 +1988,13 @@ async function runClientTransaction(
1944
1988
  // Publish refreshes only after the foreground render acknowledgement.
1945
1989
  // Otherwise a fast refresh can replace the acknowledged generation
1946
1990
  // before the framework commits it and strand the navigation.
1947
- runBackground(router, tx, result[1], background, result[3]!).catch(
1948
- console.error,
1949
- )
1991
+ runBackground(
1992
+ router,
1993
+ tx,
1994
+ result[1 /* matches */],
1995
+ background,
1996
+ result[3 /* backgroundSettlement */]!,
1997
+ ).catch(console.error)
1950
1998
  }
1951
1999
  router.batch(() => {
1952
2000
  router.stores.resolvedLocation.set(toLocation)
@@ -1968,12 +2016,12 @@ async function runClientTransaction(
1968
2016
 
1969
2017
  export async function loadClientRoute(
1970
2018
  router: CoordinatorRouter,
1971
- opts?: { sync?: boolean; _dedupe?: boolean },
2019
+ opts?: { sync?: boolean },
1972
2020
  ): Promise<void> {
1973
2021
  let rematerialize = false
1974
2022
  if (process.env.NODE_ENV !== 'production') {
1975
- router._rollbackRefresh?.()
1976
- rematerialize = !!router._refreshNextLoad || !!router._tx?.[6]
2023
+ router._tx?.[6 /* refresh */]?.[2 /* rollback */]?.()
2024
+ rematerialize = !!router._refreshNextLoad || !!router._tx?.[6 /* refresh */]
1977
2025
  }
1978
2026
  const refreshPresentation = rematerialize
1979
2027
  ? router.stores.matches.get()
@@ -1989,121 +2037,73 @@ export async function loadClientRoute(
1989
2037
  pendingLocation?.href === location.href
1990
2038
  ? (pendingLocation._redirects ?? 0)
1991
2039
  : 0
1992
- // A same-location navigation joins the transaction already loading it
1993
- // instead of restarting its work. Reload requests never carry the flag,
1994
- // and same-location redirects must restart the lane they came from.
1995
- if (
1996
- opts?._dedupe &&
1997
- !redirects &&
1998
- previousOwner &&
1999
- !rematerialize &&
2000
- previousOwner[2].href === location.href &&
2001
- router.stores.status.get() === 'pending'
2002
- ) {
2003
- await awaitCurrent(router)
2004
- return
2005
- }
2006
2040
  const handoff = router._handoff
2007
- const hydrationController = rematerialize ? undefined : handoff?.[0]()
2041
+ const hydrationController = rematerialize
2042
+ ? undefined
2043
+ : handoff?.[0 /* claim */]()
2008
2044
  const preflight = new AbortController()
2009
2045
  const previousPreflight = router._preflight
2010
2046
  router._preflight = preflight
2011
2047
  if (!rematerialize && !hydrationController) {
2012
- handoff?.[1]()
2048
+ handoff?.[1 /* finish */]()
2013
2049
  }
2014
2050
  previousPreflight?.abort()
2015
- if (preflight.signal.aborted || router._tx !== previousOwner) {
2051
+ // The preflight controller is not exposed to route hooks. Every replacement
2052
+ // aborts its predecessor, so a live signal is the sole authority here.
2053
+ if (preflight.signal.aborted) {
2016
2054
  await awaitCurrent(router, previousOwner)
2017
2055
  return
2018
2056
  }
2019
2057
 
2020
2058
  const changeInfo = getLocationChangeInfo(location, resolvedLocation)
2021
2059
  router.emit({ type: 'onBeforeNavigate', ...changeInfo })
2022
- if (!preflight.signal.aborted && router._tx === previousOwner) {
2060
+ if (!preflight.signal.aborted) {
2023
2061
  router.emit({ type: 'onBeforeLoad', ...changeInfo })
2024
2062
  }
2025
- if (preflight.signal.aborted || router._tx !== previousOwner) {
2026
- preflight.abort()
2063
+ if (preflight.signal.aborted) {
2027
2064
  await awaitCurrent(router, previousOwner)
2028
2065
  return
2029
2066
  }
2030
2067
  const sameHref = previousLocation.href === location.href
2031
- let adopted = router._preloads?.get(location.href)
2032
- let retained: ActivePreload | undefined
2033
- if (rematerialize && adopted) {
2034
- router._preloads!.delete(location.href)
2035
- discardPreload(router, adopted)
2036
- adopted = undefined
2037
- if (preflight.signal.aborted || router._tx !== previousOwner) {
2038
- preflight.abort()
2039
- await awaitCurrent(router, previousOwner)
2040
- return
2041
- }
2042
- }
2043
- if (
2044
- adopted &&
2045
- (hydrationController ||
2046
- !samePreloadLane(
2047
- adopted,
2048
- router,
2049
- pendingLocation?.href === location.href ? pendingLocation : location,
2050
- redirects,
2051
- ))
2052
- ) {
2053
- router._preloads!.delete(location.href)
2054
- // Keep incompatible loader flights alive through the real lane's reload
2055
- // decisions so matching generations can still donate their work.
2056
- retained = adopted
2057
- adopted = undefined
2058
- }
2059
2068
  let matches: Array<AnyRouteMatch>
2060
2069
  let controller = preflight
2061
- let resolvedPrefix: number | undefined
2062
- if (adopted) {
2063
- controller = adopted[1]
2064
- matches = adopted[0]
2065
- router._preloads!.delete(location.href)
2066
- } else {
2067
- try {
2068
- matches =
2069
- process.env.NODE_ENV !== 'production' && rematerialize
2070
- ? router.matchRoutes(location, {
2071
- _controller: preflight,
2072
- _rematerialize: true,
2073
- })
2074
- : router.matchRoutes(location, { _controller: preflight })
2075
- acquireMatchResources(matches)
2076
- } catch (cause) {
2077
- preflight.abort()
2078
- if (retained) {
2079
- discardPreload(router, retained)
2080
- }
2081
- if (!isRedirect(cause)) {
2082
- if (process.env.NODE_ENV !== 'production' && rematerialize) {
2083
- router._refreshNextLoad = undefined
2084
- }
2085
- await awaitCurrent(router)
2086
- router._commitPromise?.resolve()
2087
- router._commitPromise = undefined
2088
- return
2070
+ try {
2071
+ matches =
2072
+ process.env.NODE_ENV !== 'production' && rematerialize
2073
+ ? router.matchRoutes(location, {
2074
+ _controller: preflight,
2075
+ _rematerialize: true,
2076
+ })
2077
+ : router.matchRoutes(location, { _controller: preflight })
2078
+ acquireMatchResources(matches)
2079
+ } catch (cause) {
2080
+ preflight.abort()
2081
+ if (!isRedirect(cause)) {
2082
+ if (process.env.NODE_ENV !== 'production' && rematerialize) {
2083
+ router._refreshNextLoad = undefined
2089
2084
  }
2090
- await router.navigate({
2091
- ...cause.options,
2092
- replace: true,
2093
- ignoreBlocker: true,
2094
- })
2095
- await awaitCurrent(router, previousOwner)
2085
+ await awaitCurrent(router)
2086
+ router._commitPromise?.resolve()
2087
+ router._commitPromise = undefined
2096
2088
  return
2097
2089
  }
2098
- resolvedPrefix = hydrationController ? handoff![1](matches) : undefined
2099
- if (resolvedPrefix) {
2100
- controller = hydrationController!
2101
- } else {
2102
- hydrationController?.abort()
2103
- }
2090
+ await router.navigate({
2091
+ ...cause.options,
2092
+ replace: true,
2093
+ ignoreBlocker: true,
2094
+ })
2095
+ await awaitCurrent(router, previousOwner)
2096
+ return
2104
2097
  }
2105
- if (router._preflight !== preflight || router._tx !== previousOwner) {
2106
- preflight.abort()
2098
+ const resolvedPrefix = hydrationController
2099
+ ? handoff![1 /* finish */](matches)
2100
+ : undefined
2101
+ if (resolvedPrefix) {
2102
+ controller = hydrationController!
2103
+ } else {
2104
+ hydrationController?.abort()
2105
+ }
2106
+ if (preflight.signal.aborted) {
2107
2107
  transferMatchResources(router, matches)
2108
2108
  await awaitCurrent(router, previousOwner)
2109
2109
  return
@@ -2125,8 +2125,6 @@ export async function loadClientRoute(
2125
2125
  () => offerPending(router, tx),
2126
2126
  opts?.sync,
2127
2127
  resolvedPrefix,
2128
- adopted,
2129
- retained,
2130
2128
  ),
2131
2129
  )
2132
2130
  .catch(() => {
@@ -2137,13 +2135,10 @@ export async function loadClientRoute(
2137
2135
  ]
2138
2136
  if (process.env.NODE_ENV !== 'production' && rematerialize) {
2139
2137
  // `refreshPresentation` is always captured when `rematerialize` is set.
2140
- tx[6] = [refreshPresentation!, handoff]
2138
+ tx[6 /* refresh */] = [refreshPresentation!, handoff]
2141
2139
  router._refreshNextLoad = undefined
2142
2140
  }
2143
2141
  router._tx = tx
2144
- if (!rematerialize && router._handoff === handoff) {
2145
- router._handoff = undefined
2146
- }
2147
2142
  if (previousOwner) {
2148
2143
  for (const match of router.stores.matches.get() as Array<WorkMatch>) {
2149
2144
  if (router._tx !== tx) {
@@ -2153,23 +2148,26 @@ export async function loadClientRoute(
2153
2148
  setFetching(router, match, false)
2154
2149
  }
2155
2150
  }
2156
- previousOwner[0].abort()
2157
- transferMatchResources(router, previousOwner[3])
2151
+ previousOwner[0 /* controller */].abort()
2152
+ transferPredecessorResources(
2153
+ router,
2154
+ previousOwner[3 /* matches */],
2155
+ tx[3 /* matches */],
2156
+ )
2158
2157
  }
2159
2158
  if (router._tx !== tx) {
2160
- transferMatchResources(router, tx[3])
2161
- tx[3] = []
2159
+ transferMatchResources(router, tx[3 /* matches */])
2160
+ tx[3 /* matches */] = []
2162
2161
  await awaitCurrent(router, tx)
2163
2162
  return
2164
2163
  }
2165
-
2166
2164
  router.batch(() => {
2167
2165
  router.stores.status.set('pending')
2168
2166
  router.stores.location.set(location)
2169
2167
  })
2170
2168
  offerPending(router, tx)
2171
2169
  try {
2172
- await tx[5]
2170
+ await tx[5 /* done */]
2173
2171
  } finally {
2174
2172
  await awaitCurrent(router, tx)
2175
2173
  }
@@ -2178,10 +2176,14 @@ export async function loadClientRoute(
2178
2176
  export async function refreshClientRoute(
2179
2177
  router: CoordinatorRouter,
2180
2178
  ): Promise<void> {
2181
- router._rollbackRefresh?.()
2179
+ router._tx?.[6 /* refresh */]?.[2 /* rollback */]?.()
2182
2180
  const pending = router._tx
2183
- if (pending && !pending[6] && router.stores.status.get() === 'pending') {
2184
- await pending[5]
2181
+ if (
2182
+ pending &&
2183
+ !pending[6 /* refresh */] &&
2184
+ router.stores.status.get() === 'pending'
2185
+ ) {
2186
+ await pending[5 /* done */]
2185
2187
  if (router._tx !== pending) {
2186
2188
  await awaitCurrent(router, pending)
2187
2189
  }
@@ -2193,30 +2195,6 @@ export async function refreshClientRoute(
2193
2195
  await loadClientRoute(router, { sync: true })
2194
2196
  }
2195
2197
 
2196
- function followPreloadRedirect(
2197
- router: CoordinatorRouter,
2198
- result: ControlOutcome,
2199
- location: ParsedLocation,
2200
- owner: LoadTransaction | undefined,
2201
- redirects: number,
2202
- ): Promise<Array<AnyRouteMatch> | undefined> | undefined {
2203
- if (
2204
- result[0] === REDIRECTED &&
2205
- !result[1].options.reloadDocument &&
2206
- router._tx === owner
2207
- ) {
2208
- return preloadClientRoute(
2209
- router,
2210
- {
2211
- ...result[1].options,
2212
- _fromLocation: location,
2213
- },
2214
- redirects + 1,
2215
- )
2216
- }
2217
- return
2218
- }
2219
-
2220
2198
  export async function preloadClientRoute(
2221
2199
  router: CoordinatorRouter,
2222
2200
  opts: any,
@@ -2225,96 +2203,70 @@ export async function preloadClientRoute(
2225
2203
  if (redirects > 20) {
2226
2204
  return
2227
2205
  }
2228
- const owner = router._tx
2229
2206
  if (
2230
2207
  process.env.NODE_ENV !== 'production' &&
2231
- (router._refreshNextLoad || owner?.[6])
2208
+ (router._refreshNextLoad || router._tx?.[6 /* refresh */])
2232
2209
  ) {
2233
2210
  return
2234
2211
  }
2235
2212
  const location = opts._builtLocation ?? router.buildLocation(opts)
2236
2213
  const base = router._committed
2237
2214
  const controller = new AbortController()
2238
- let matches: Array<AnyRouteMatch> | undefined
2239
- let preload: ActivePreload | undefined
2240
- let replaced: ActivePreload | undefined
2215
+ let matches: Array<AnyRouteMatch>
2241
2216
  try {
2242
- const pending = router._preloads?.get(location.href)
2243
- if (pending) {
2244
- if (samePreloadLane(pending, router, location, redirects)) {
2245
- const result = await pending[2]
2246
- return isControl(result)
2247
- ? followPreloadRedirect(router, result, location, owner, redirects)
2248
- : result[1]
2249
- }
2250
- router._preloads!.delete(location.href)
2251
- // Keep the superseded lane alive until this lane has made its reload
2252
- // decisions. Its active flights are the synchronous donor authority.
2253
- replaced = pending
2254
- }
2255
2217
  matches = router.matchRoutes(location, {
2256
2218
  _controller: controller,
2257
2219
  })
2258
2220
  acquireMatchResources(matches)
2259
- const promise = Promise.resolve()
2260
- .then(() =>
2261
- executeClientLane(router, location, matches!, [
2262
- controller,
2263
- redirects,
2264
- // Preload lanes run to completion even when unrelated navigations
2265
- // commit: finished work seeds the cache, and adoption safety is
2266
- // enforced independently by samePreloadLane's base identity check.
2267
- () => true,
2268
- base,
2269
- true,
2270
- ]),
2271
- )
2272
- .finally(() => {
2273
- if (replaced) {
2274
- discardPreload(router, replaced)
2275
- }
2276
- })
2277
- preload = [
2278
- matches,
2279
- controller,
2280
- promise,
2281
- base,
2282
- laneInputs(router, location),
2283
- redirects,
2284
- ]
2285
- ;(router._preloads ??= new Map()).set(location.href, preload)
2286
- const result = await promise
2287
- if (router._preloads?.get(location.href) !== preload) {
2288
- return isControl(result) ? undefined : result[1]
2221
+ } catch (cause) {
2222
+ controller.abort()
2223
+ if (!isNotFound(cause)) {
2224
+ console.error(cause)
2289
2225
  }
2290
- router._preloads.delete(location.href)
2291
- if (isControl(result)) {
2292
- controller.abort()
2226
+ return
2227
+ }
2228
+ ;(router._preloads ??= new Map()).set(controller, matches)
2229
+ let active: boolean
2230
+ try {
2231
+ let result: LaneResult
2232
+ try {
2233
+ result = await executeClientLane(router, location, matches, [
2234
+ controller,
2235
+ redirects,
2236
+ // Preload lanes run to completion even when unrelated navigations commit:
2237
+ // finished work seeds the cache.
2238
+ () => true,
2239
+ base,
2240
+ true,
2241
+ ])
2242
+ } finally {
2243
+ active = router._preloads.delete(controller)
2293
2244
  transferMatchResources(router, matches)
2294
- return followPreloadRedirect(router, result, location, owner, redirects)
2295
- }
2296
-
2297
- transferMatchResources(router, result[1])
2298
- controller.abort()
2299
- return result[1]
2300
- } catch (cause) {
2301
- if (!preload || router._preloads?.get(location.href) === preload) {
2302
- if (preload) {
2303
- router._preloads!.delete(location.href)
2304
- }
2305
2245
  controller.abort()
2306
- if (matches) {
2307
- transferMatchResources(router, matches)
2308
- }
2309
2246
  }
2310
- if (router._tx !== owner) {
2311
- return
2247
+ if (!isControl(result)) {
2248
+ return result[1 /* matches */]
2249
+ }
2250
+ if (
2251
+ active &&
2252
+ result[0 /* kind */] === REDIRECTED &&
2253
+ !result[1 /* redirect */].options.reloadDocument
2254
+ ) {
2255
+ return preloadClientRoute(
2256
+ router,
2257
+ {
2258
+ ...result[1 /* redirect */].options,
2259
+ _fromLocation: location,
2260
+ },
2261
+ redirects + 1,
2262
+ )
2312
2263
  }
2264
+ } catch (cause) {
2313
2265
  if (!isNotFound(cause)) {
2314
2266
  console.error(cause)
2315
2267
  }
2316
- return
2317
2268
  }
2269
+ return
2318
2270
  }
2319
2271
 
2320
2272
  // --- SSR hydration (client entry via @tanstack/router-core/ssr/client) ---
@@ -2352,12 +2304,13 @@ export async function hydrate(router: AnyRouter): Promise<void> {
2352
2304
  )
2353
2305
  }
2354
2306
  router.ssr = { manifest: dehydratedRouter!.manifest }
2355
- const nonce = (
2356
- document.querySelector('meta[property="csp-nonce"]') as
2357
- | HTMLMetaElement
2358
- | undefined
2359
- )?.content
2360
- router.options.ssr = { nonce }
2307
+ router.options.ssr = {
2308
+ nonce: (
2309
+ document.querySelector('meta[property="csp-nonce"]') as
2310
+ | HTMLMetaElement
2311
+ | undefined
2312
+ )?.content,
2313
+ }
2361
2314
 
2362
2315
  const dehydratedMatches = dehydratedRouter!.matches
2363
2316
 
@@ -2365,22 +2318,14 @@ export async function hydrate(router: AnyRouter): Promise<void> {
2365
2318
  const previousPreflight = router._preflight
2366
2319
  router._preflight = controller
2367
2320
  previousPreflight?.abort()
2368
- const retire = (cause?: unknown) => {
2369
- if (router._preflight === controller) {
2370
- router._preflight = undefined
2371
- }
2372
- controller.abort(cause)
2373
- return false
2374
- }
2375
- const isCurrent = () =>
2376
- (!router._tx &&
2377
- router._preflight === controller &&
2378
- !controller.signal.aborted) ||
2379
- retire()
2321
+ // Route context can abort this controller itself. Only a new slot owner
2322
+ // supersedes hydration.
2323
+ const isCurrent = () => router._preflight === controller
2380
2324
 
2381
2325
  let location!: AnyRouter['latestLocation']
2382
2326
  let candidates!: Array<AnyRouteMatch>
2383
- let handoffInputs!: ReturnType<typeof laneInputs>
2327
+ let handoffHistoryHref!: string
2328
+ let handoffHistoryState: unknown
2384
2329
  try {
2385
2330
  await waitFor(
2386
2331
  router.options.hydrate?.(dehydratedRouter!.dehydratedData),
@@ -2389,15 +2334,22 @@ export async function hydrate(router: AnyRouter): Promise<void> {
2389
2334
  if (!isCurrent()) {
2390
2335
  return
2391
2336
  }
2337
+ // Hydration trusts transported context and beforeLoad. The raw history
2338
+ // entry owns the handoff; route structure is verified after rematching.
2339
+ const historyLocation = router.history.location
2340
+ handoffHistoryHref = historyLocation.href
2341
+ handoffHistoryState = historyLocation.state
2392
2342
  router.updateLatestLocation()
2393
2343
  location = router.latestLocation
2394
2344
  router.stores.location.set(location)
2395
- handoffInputs = laneInputs(router, location)
2396
2345
  candidates = router.matchRoutes(location, {
2397
2346
  _controller: controller,
2398
2347
  })
2399
2348
  } catch (cause) {
2400
- retire(cause)
2349
+ if (isCurrent()) {
2350
+ router._preflight = undefined
2351
+ }
2352
+ controller.abort(cause)
2401
2353
  if (cause !== controller.signal) {
2402
2354
  throw cause
2403
2355
  }
@@ -2492,8 +2444,6 @@ export async function hydrate(router: AnyRouter): Promise<void> {
2492
2444
  pendingBoundary ??= index
2493
2445
  }
2494
2446
  }
2495
- let verifiedContextEnd = verifiedAssetEnd
2496
-
2497
2447
  if (
2498
2448
  !isTerminal &&
2499
2449
  committed.length === shared &&
@@ -2536,14 +2486,12 @@ export async function hydrate(router: AnyRouter): Promise<void> {
2536
2486
  chunkFailure++
2537
2487
  }
2538
2488
  } catch {
2539
- isCurrent()
2540
2489
  return
2541
2490
  }
2542
2491
  if (!isCurrent()) {
2543
2492
  return
2544
2493
  }
2545
2494
  if (chunkFailure < committed.length) {
2546
- verifiedContextEnd = Math.min(verifiedContextEnd, chunkFailure)
2547
2495
  retryFrom(chunkFailure)
2548
2496
  }
2549
2497
 
@@ -2553,7 +2501,9 @@ export async function hydrate(router: AnyRouter): Promise<void> {
2553
2501
  pendingBoundary === committed.length
2554
2502
  ? committed.length + 1
2555
2503
  : committed.length,
2556
- verifiedContextEnd,
2504
+ // `chunks.length` keeps the pre-retry committed length, so a smaller
2505
+ // `chunkFailure` is the exclusive bound of the verified context prefix.
2506
+ chunkFailure < chunks.length ? chunkFailure : verifiedAssetEnd,
2557
2507
  )
2558
2508
  for (let index = 0; index < contextEnd; index++) {
2559
2509
  const match = candidates[index]!
@@ -2619,12 +2569,9 @@ export async function hydrate(router: AnyRouter): Promise<void> {
2619
2569
  let dataOnlyAssetEnd: number | undefined
2620
2570
  if (needsClientLoad && pendingBoundary !== undefined) {
2621
2571
  const boundary = presented[pendingBoundary]!
2572
+ // A verified descendant proves this data-only boundary was nonterminal.
2622
2573
  dataOnlyAssetEnd =
2623
- boundary.status === 'success' &&
2624
- boundary.ssr === 'data-only' &&
2625
- boundary.error === undefined &&
2626
- !boundary._notFound &&
2627
- verifiedAssetEnd > pendingBoundary + 1
2574
+ boundary.ssr === 'data-only' && verifiedAssetEnd > pendingBoundary + 1
2628
2575
  ? verifiedAssetEnd
2629
2576
  : undefined
2630
2577
  presented = presented.slice()
@@ -2636,29 +2583,33 @@ export async function hydrate(router: AnyRouter): Promise<void> {
2636
2583
  }
2637
2584
  }
2638
2585
 
2639
- const claim = () =>
2640
- needsClientLoad &&
2641
- !router._tx &&
2642
- router.latestLocation.state === location.state &&
2643
- deepEqual(handoffInputs, laneInputs(router, router.latestLocation)) &&
2644
- router._committed === committedMatches &&
2645
- committedMatches.length &&
2646
- !controller.signal.aborted
2586
+ const claim = () => {
2587
+ const historyLocation = router.history.location
2588
+ return needsClientLoad &&
2589
+ !router._tx &&
2590
+ historyLocation.href === handoffHistoryHref &&
2591
+ historyLocation.state === handoffHistoryState &&
2592
+ router._committed === committedMatches &&
2593
+ committedMatches.length &&
2594
+ !controller.signal.aborted
2647
2595
  ? controller
2648
2596
  : undefined
2597
+ }
2649
2598
  const handoff: NonNullable<AnyRouter['_handoff']> = [
2650
2599
  claim,
2651
2600
  (matches) => {
2652
2601
  if (router._handoff !== handoff) {
2653
2602
  return
2654
2603
  }
2604
+ // `finish` is single-use. Consume the slot before validating or moving
2605
+ // resources so reentrant work cannot claim the same handoff.
2606
+ router._handoff = undefined
2655
2607
  const prefix = committedMatches.length
2656
2608
  if (
2657
2609
  !matches ||
2658
2610
  !claim() ||
2659
2611
  committedMatches.some((match, index) => match.id !== matches[index]?.id)
2660
2612
  ) {
2661
- router._handoff = undefined
2662
2613
  controller.abort()
2663
2614
  return
2664
2615
  }
@@ -2666,8 +2617,7 @@ export async function hydrate(router: AnyRouter): Promise<void> {
2666
2617
  if (handoffAssetEnd !== undefined) {
2667
2618
  for (let index = prefix; index < handoffAssetEnd; index++) {
2668
2619
  if (candidates[index]?.id !== matches[index]?.id) {
2669
- handoffAssetEnd =
2670
- index > (pendingBoundary ?? -1) + 1 ? index : undefined
2620
+ handoffAssetEnd = index > pendingBoundary! + 1 ? index : undefined
2671
2621
  break
2672
2622
  }
2673
2623
  }