@tanstack/router-core 1.171.18 → 1.171.19

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.
@@ -357,6 +357,7 @@ async function contextualize(
357
357
  options: ExecuteLaneOptions,
358
358
  end: number,
359
359
  planSuccessfulLane: () => void,
360
+ retainedEnd: number,
360
361
  ): Promise<IndexedOutcome | undefined> {
361
362
  const [location, matches] = lane
362
363
  const signal = options[0 /* controller */].signal
@@ -433,7 +434,8 @@ async function contextualize(
433
434
  }
434
435
 
435
436
  const previousStatus = match.status
436
- if (previousStatus === 'success') {
437
+ // Retain only a success that is mounted through the same valid prefix.
438
+ if (previousStatus === 'success' && index >= retainedEnd) {
437
439
  match.status = 'pending'
438
440
  }
439
441
  options[8 /* onReady */]?.()
@@ -511,40 +513,21 @@ function transferMatchResources(
511
513
  router: AnyRouter,
512
514
  previous: Array<AnyRouteMatch>,
513
515
  next?: Array<AnyRouteMatch>,
516
+ deferSameIdFlight?: true,
514
517
  ): void {
515
518
  const abort: Array<AbortController> = []
516
519
  for (const match of previous as Array<WorkMatch>) {
517
520
  if (!next?.includes(match)) {
518
- const flight = match._flight
519
- match._flight = undefined
520
- const controller = releaseOwnedFlight(router, match, flight)
521
- if (controller) {
522
- abort.push(controller)
523
- }
524
- }
525
- }
526
- for (const controller of abort) {
527
- controller.abort()
528
- }
529
- }
530
-
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
521
  const flight = match._flight
540
522
  match._flight = undefined
541
523
  if (
524
+ deferSameIdFlight &&
542
525
  flight?.[2 /* leases */] === 1 &&
543
526
  router._flights?.get(match.id) === flight &&
544
527
  !(
545
528
  process.env.NODE_ENV !== 'production' && router._tx?.[6 /* refresh */]
546
529
  ) &&
547
- next.some((candidate) => candidate.id === match.id)
530
+ next?.some((candidate) => candidate.id === match.id)
548
531
  ) {
549
532
  // The successor has not made its same-ID reload decision yet.
550
533
  flight[2 /* leases */] = 0
@@ -561,19 +544,6 @@ function transferPredecessorResources(
561
544
  }
562
545
  }
563
546
 
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
- }
575
- }
576
-
577
547
  function acquireMatchResources(matches: Array<AnyRouteMatch>): void {
578
548
  for (const match of matches as Array<WorkMatch>) {
579
549
  const flight = match._flight
@@ -775,6 +745,7 @@ function createLoaderTask(
775
745
  tasks: Array<LoaderTask>,
776
746
  semanticParent: Promise<WorkMatch> | undefined,
777
747
  options: ExecuteLaneOptions,
748
+ retainedEnd: number,
778
749
  ): Promise<WorkMatch> {
779
750
  const match = lane[1 /* matches */][index]!
780
751
  const route = getRoute(router, match)
@@ -879,11 +850,9 @@ function createLoaderTask(
879
850
  const acceptedFlight = match._flight
880
851
  match._flight = donor
881
852
  releaseOwnedFlight(router, match, acceptedFlight)?.abort()
882
- // A successful route without a loader has no blocking work to present. It
883
- // still gets a task so its chunk and derived assets participate in the
884
- // lane, but putting it back into pending would hide an already-rendered
885
- // ancestor while only a descendant is loading.
886
- if (match.status === 'success') {
853
+ // A successful route without a loader has no blocking work to present. A
854
+ // mounted success likewise remains renderable while its loader revalidates.
855
+ if (match.status === 'success' && index >= retainedEnd) {
887
856
  match.status = 'pending'
888
857
  }
889
858
  options[8 /* onReady */]?.()
@@ -1173,18 +1142,19 @@ async function reduceLane(
1173
1142
  }
1174
1143
  }
1175
1144
  install()
1145
+ const route = getRoute(router, match)
1176
1146
  try {
1177
1147
  await waitFor<unknown>(
1178
1148
  outcome
1179
1149
  ? Promise.resolve().then(() =>
1180
1150
  loadRouteChunk(
1181
- getRoute(router, match),
1151
+ route,
1182
1152
  kind === ERROR ? 'errorComponent' : 'notFoundComponent',
1183
1153
  ),
1184
1154
  )
1185
1155
  : Promise.all([
1186
- loadRouteChunk(getRoute(router, match)),
1187
- loadRouteChunk(getRoute(router, match), 'notFoundComponent'),
1156
+ loadRouteChunk(route),
1157
+ loadRouteChunk(route, 'notFoundComponent'),
1188
1158
  ]),
1189
1159
  controller.signal,
1190
1160
  )
@@ -1268,6 +1238,7 @@ async function executeClientLane(
1268
1238
  options: ExecuteLaneOptions,
1269
1239
  ): Promise<LaneResult> {
1270
1240
  const matched = [location, matches as Array<WorkMatch>] as MatchedLane
1241
+ const presented = router.stores.matches.get()
1271
1242
  let plannedBoundary = matches.findIndex((match) => match._notFound)
1272
1243
  if (router.options.notFoundMode !== 'root' && plannedBoundary >= 0) {
1273
1244
  const boundary = await getNotFoundBoundary(
@@ -1284,6 +1255,24 @@ async function executeClientLane(
1284
1255
  plannedBoundary = boundary
1285
1256
  }
1286
1257
  let end = plannedBoundary < 0 ? matches.length : plannedBoundary + 1
1258
+ let retainedEnd = 0
1259
+ while (retainedEnd < end && retainedEnd !== plannedBoundary) {
1260
+ const match = matches[retainedEnd]!
1261
+ const committed = options[3 /* base */][retainedEnd]
1262
+ const visible = presented[retainedEnd]
1263
+ if (
1264
+ committed?.id !== match.id ||
1265
+ committed.status !== 'success' ||
1266
+ committed._notFound ||
1267
+ match.preload ||
1268
+ visible?.id !== match.id ||
1269
+ visible.status !== 'success' ||
1270
+ visible._notFound
1271
+ ) {
1272
+ break
1273
+ }
1274
+ retainedEnd++
1275
+ }
1287
1276
  const tasks: Array<LoaderTask> = []
1288
1277
  const start = options[7 /* resolvedPrefix */] ?? 0
1289
1278
  let semanticParent = start
@@ -1301,6 +1290,7 @@ async function executeClientLane(
1301
1290
  tasks,
1302
1291
  semanticParent,
1303
1292
  options,
1293
+ retainedEnd,
1304
1294
  )
1305
1295
  }
1306
1296
  }
@@ -1313,6 +1303,7 @@ async function executeClientLane(
1313
1303
  options,
1314
1304
  end,
1315
1305
  planSuccessfulLane,
1306
+ retainedEnd,
1316
1307
  )
1317
1308
  if (failure) {
1318
1309
  options[5 /* sync */] = true
@@ -1331,7 +1322,16 @@ async function executeClientLane(
1331
1322
  planSuccessfulLane()
1332
1323
  }
1333
1324
  if (options[2 /* isCurrent */]() && !options[4 /* preload */]) {
1334
- releaseUnownedFlights(router)
1325
+ const abort: Array<AbortController> = []
1326
+ for (const [id, flight] of router._flights ?? []) {
1327
+ if (!flight[2 /* leases */]) {
1328
+ router._flights!.delete(id)
1329
+ abort.push(flight[1 /* controller */])
1330
+ }
1331
+ }
1332
+ for (const controller of abort) {
1333
+ controller.abort()
1334
+ }
1335
1335
  }
1336
1336
  let reduced: ReducedLane | ControlOutcome
1337
1337
  try {
@@ -1376,49 +1376,6 @@ async function executeClientLane(
1376
1376
  )
1377
1377
  }
1378
1378
 
1379
- /**
1380
- * Finds the first route that should show pending UI and its two timing values.
1381
- * A fallback already on screen remains selected after its route loads, so we
1382
- * do not jump to a child fallback. Matches put back into pending by invalidation
1383
- * skip pendingMs, and a route without a usable fallback blocks pending UI for deeper routes.
1384
- */
1385
- function pendingConfig(
1386
- router: AnyRouter,
1387
- matches: Array<AnyRouteMatch>,
1388
- ):
1389
- | [delay: number, boundary: number, min: number, component: unknown]
1390
- | undefined
1391
- | void {
1392
- const presented = router.stores.matches.get()
1393
- for (let index = 0; index < matches.length; index++) {
1394
- const match = matches[index]!
1395
- const success = match.status === 'success'
1396
- const visible =
1397
- success &&
1398
- presented[index]?.id === match.id &&
1399
- presented[index]?.status === 'pending'
1400
- if (success && !visible) {
1401
- continue
1402
- }
1403
- const route = getRoute(router, match as WorkMatch)
1404
- const delay =
1405
- visible || match.invalid
1406
- ? 0
1407
- : (route.options.pendingMs ?? router.options.defaultPendingMs)
1408
- const component =
1409
- route.options.pendingComponent ??
1410
- (router.options as any).defaultPendingComponent
1411
- return component && typeof delay === 'number' && delay !== Infinity
1412
- ? [
1413
- delay,
1414
- index,
1415
- route.options.pendingMinMs ?? router.options.defaultPendingMinMs ?? 0,
1416
- component,
1417
- ]
1418
- : undefined
1419
- }
1420
- }
1421
-
1422
1379
  /**
1423
1380
  * Waits for `pendingMs`, then presents the complete lane. Rendering applies the
1424
1381
  * selected boundary cutoff while retaining every match's structural state.
@@ -1445,12 +1402,41 @@ function offerPending(router: CoordinatorRouter, tx: LoadTransaction): void {
1445
1402
  router._pending = session = undefined
1446
1403
  }
1447
1404
  }
1448
- const config = pendingConfig(router, tx[3 /* matches */])
1449
- if (!config) {
1405
+ const matches = tx[3 /* matches */]
1406
+ const presented = router.stores.matches.get()
1407
+ let boundary = -1
1408
+ let delay: number | undefined
1409
+ let min!: number
1410
+ let component: unknown
1411
+ let presentedPending = false
1412
+ for (let index = 0; index < matches.length; index++) {
1413
+ const match = matches[index]!
1414
+ const success = match.status === 'success'
1415
+ presentedPending =
1416
+ presented[index]?.id === match.id &&
1417
+ presented[index]?.status === 'pending'
1418
+ if (success && !presentedPending) {
1419
+ continue
1420
+ }
1421
+ const route = getRoute(router, match as WorkMatch)
1422
+ delay =
1423
+ (success && presentedPending) || match.invalid
1424
+ ? 0
1425
+ : (route.options.pendingMs ?? router.options.defaultPendingMs)
1426
+ component =
1427
+ route.options.pendingComponent ??
1428
+ (router.options as any).defaultPendingComponent
1429
+ if (!component || typeof delay !== 'number' || delay === Infinity) {
1430
+ return
1431
+ }
1432
+ boundary = index
1433
+ min = route.options.pendingMinMs ?? router.options.defaultPendingMinMs ?? 0
1434
+ break
1435
+ }
1436
+ if (boundary < 0) {
1450
1437
  return
1451
1438
  }
1452
- const [delay, boundary, min, component] = config
1453
- const matchId = tx[3 /* matches */][boundary]!.id
1439
+ const matchId = matches[boundary]!.id
1454
1440
  if (
1455
1441
  !session ||
1456
1442
  session[1 /* boundary */] !== boundary ||
@@ -1459,14 +1445,12 @@ function offerPending(router: CoordinatorRouter, tx: LoadTransaction): void {
1459
1445
  // Hydration and redirects can preserve pending presentation without a session.
1460
1446
  // Do not delay it again; conservatively start pendingMinMs from now.
1461
1447
  clearTimeout(session?.[3 /* timer */])
1462
- const presented = router.stores.matches.get()[boundary]
1463
- const visible = presented?.id === matchId && presented.status === 'pending'
1464
1448
  router._pending = session = [
1465
1449
  tx,
1466
1450
  boundary,
1467
- visible ? Date.now() + min : tx[4 /* startedAt */] + delay,
1451
+ presentedPending ? Date.now() + min : tx[4 /* startedAt */] + delay!,
1468
1452
  undefined,
1469
- visible ? Promise.resolve(true) : undefined,
1453
+ presentedPending ? Promise.resolve(true) : undefined,
1470
1454
  component,
1471
1455
  ]
1472
1456
  }
@@ -1482,14 +1466,15 @@ function offerPending(router: CoordinatorRouter, tx: LoadTransaction): void {
1482
1466
  clearTimeout(session[3 /* timer */])
1483
1467
  const remaining = session[2 /* deadline */] - Date.now()
1484
1468
  if (remaining > 0) {
1485
- session[3 /* timer */] = setTimeout(() => {
1486
- offerPending(router, tx)
1487
- }, remaining)
1469
+ session[3 /* timer */] = setTimeout(
1470
+ () => offerPending(router, tx),
1471
+ remaining,
1472
+ )
1488
1473
  return
1489
1474
  }
1490
1475
  session[2 /* deadline */] = 0
1491
1476
  }
1492
- const offered = tx[3 /* matches */].map((match) => ({
1477
+ const offered = matches.map((match) => ({
1493
1478
  ...match,
1494
1479
  _flight: undefined,
1495
1480
  }))
@@ -2149,10 +2134,11 @@ export async function loadClientRoute(
2149
2134
  }
2150
2135
  }
2151
2136
  previousOwner[0 /* controller */].abort()
2152
- transferPredecessorResources(
2137
+ transferMatchResources(
2153
2138
  router,
2154
2139
  previousOwner[3 /* matches */],
2155
2140
  tx[3 /* matches */],
2141
+ true,
2156
2142
  )
2157
2143
  }
2158
2144
  if (router._tx !== tx) {
@@ -2165,7 +2151,11 @@ export async function loadClientRoute(
2165
2151
  router.stores.status.set('pending')
2166
2152
  router.stores.location.set(location)
2167
2153
  })
2168
- offerPending(router, tx)
2154
+ // Cold loads have no committed UI to retain, but provisional not-found
2155
+ // matches must wait for lazy routes to place the final boundary.
2156
+ if (!resolvedLocation && !matches.some((match) => match._notFound)) {
2157
+ offerPending(router, tx)
2158
+ }
2169
2159
  try {
2170
2160
  await tx[5 /* done */]
2171
2161
  } finally {