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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +3 -12
  3. package/dist/cjs/await-signal.cjs +21 -0
  4. package/dist/cjs/await-signal.cjs.map +1 -0
  5. package/dist/cjs/await-signal.d.cts +1 -0
  6. package/dist/cjs/index.cjs +3 -1
  7. package/dist/cjs/index.d.cts +3 -2
  8. package/dist/cjs/isServer/client.cjs +2 -0
  9. package/dist/cjs/isServer/client.cjs.map +1 -1
  10. package/dist/cjs/isServer/client.d.cts +1 -0
  11. package/dist/cjs/isServer/development.cjs +2 -0
  12. package/dist/cjs/isServer/development.cjs.map +1 -1
  13. package/dist/cjs/isServer/development.d.cts +1 -0
  14. package/dist/cjs/isServer/server.cjs +2 -0
  15. package/dist/cjs/isServer/server.cjs.map +1 -1
  16. package/dist/cjs/isServer/server.d.cts +1 -0
  17. package/dist/cjs/load-client.cjs +1470 -0
  18. package/dist/cjs/load-client.cjs.map +1 -0
  19. package/dist/cjs/load-client.d.cts +147 -0
  20. package/dist/cjs/load-server.cjs +565 -0
  21. package/dist/cjs/load-server.cjs.map +1 -0
  22. package/dist/cjs/load-server.d.cts +16 -0
  23. package/dist/cjs/redirect.cjs.map +1 -1
  24. package/dist/cjs/redirect.d.cts +0 -1
  25. package/dist/cjs/route.cjs.map +1 -1
  26. package/dist/cjs/router.cjs +187 -442
  27. package/dist/cjs/router.cjs.map +1 -1
  28. package/dist/cjs/router.d.cts +61 -51
  29. package/dist/cjs/ssr/client.cjs +2 -2
  30. package/dist/cjs/ssr/createRequestHandler.cjs +60 -9
  31. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  32. package/dist/cjs/ssr/createRequestHandler.d.cts +1 -0
  33. package/dist/cjs/ssr/handlerCallback.cjs +49 -4
  34. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
  35. package/dist/cjs/ssr/handlerCallback.d.cts +3 -0
  36. package/dist/cjs/ssr/server.cjs +4 -0
  37. package/dist/cjs/ssr/server.d.cts +2 -2
  38. package/dist/cjs/ssr/ssr-client.d.cts +1 -10
  39. package/dist/cjs/ssr/ssr-match-id.cjs +2 -2
  40. package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
  41. package/dist/cjs/ssr/ssr-server.cjs +6 -6
  42. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  43. package/dist/cjs/ssr/transformStreamWithRouter.cjs +27 -1
  44. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  45. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +2 -0
  46. package/dist/cjs/ssr/types.d.cts +0 -1
  47. package/dist/cjs/stores.cjs +30 -104
  48. package/dist/cjs/stores.cjs.map +1 -1
  49. package/dist/cjs/stores.d.cts +7 -32
  50. package/dist/cjs/utils.cjs +0 -4
  51. package/dist/cjs/utils.cjs.map +1 -1
  52. package/dist/esm/Matches.d.ts +3 -12
  53. package/dist/esm/Matches.js.map +1 -1
  54. package/dist/esm/await-signal.d.ts +1 -0
  55. package/dist/esm/await-signal.js +21 -0
  56. package/dist/esm/await-signal.js.map +1 -0
  57. package/dist/esm/index.d.ts +3 -2
  58. package/dist/esm/index.js +3 -2
  59. package/dist/esm/isServer/client.d.ts +1 -0
  60. package/dist/esm/isServer/client.js +2 -1
  61. package/dist/esm/isServer/client.js.map +1 -1
  62. package/dist/esm/isServer/development.d.ts +1 -0
  63. package/dist/esm/isServer/development.js +2 -1
  64. package/dist/esm/isServer/development.js.map +1 -1
  65. package/dist/esm/isServer/server.d.ts +1 -0
  66. package/dist/esm/isServer/server.js +2 -1
  67. package/dist/esm/isServer/server.js.map +1 -1
  68. package/dist/esm/load-client.d.ts +147 -0
  69. package/dist/esm/load-client.js +1462 -0
  70. package/dist/esm/load-client.js.map +1 -0
  71. package/dist/esm/load-server.d.ts +16 -0
  72. package/dist/esm/load-server.js +565 -0
  73. package/dist/esm/load-server.js.map +1 -0
  74. package/dist/esm/redirect.d.ts +0 -1
  75. package/dist/esm/redirect.js.map +1 -1
  76. package/dist/esm/route.js.map +1 -1
  77. package/dist/esm/router.d.ts +61 -51
  78. package/dist/esm/router.js +188 -444
  79. package/dist/esm/router.js.map +1 -1
  80. package/dist/esm/ssr/client.js +1 -1
  81. package/dist/esm/ssr/createRequestHandler.d.ts +1 -0
  82. package/dist/esm/ssr/createRequestHandler.js +61 -11
  83. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  84. package/dist/esm/ssr/handlerCallback.d.ts +3 -0
  85. package/dist/esm/ssr/handlerCallback.js +47 -5
  86. package/dist/esm/ssr/handlerCallback.js.map +1 -1
  87. package/dist/esm/ssr/server.d.ts +2 -2
  88. package/dist/esm/ssr/server.js +3 -3
  89. package/dist/esm/ssr/ssr-client.d.ts +1 -10
  90. package/dist/esm/ssr/ssr-match-id.js +2 -2
  91. package/dist/esm/ssr/ssr-match-id.js.map +1 -1
  92. package/dist/esm/ssr/ssr-server.js +6 -6
  93. package/dist/esm/ssr/ssr-server.js.map +1 -1
  94. package/dist/esm/ssr/transformStreamWithRouter.d.ts +2 -0
  95. package/dist/esm/ssr/transformStreamWithRouter.js +27 -1
  96. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  97. package/dist/esm/ssr/types.d.ts +0 -1
  98. package/dist/esm/stores.d.ts +7 -32
  99. package/dist/esm/stores.js +30 -104
  100. package/dist/esm/stores.js.map +1 -1
  101. package/dist/esm/utils.js +1 -4
  102. package/dist/esm/utils.js.map +1 -1
  103. package/package.json +3 -3
  104. package/src/Matches.ts +6 -21
  105. package/src/await-signal.ts +27 -0
  106. package/src/index.ts +1 -3
  107. package/src/isServer/client.ts +1 -0
  108. package/src/isServer/development.ts +1 -0
  109. package/src/isServer/server.ts +1 -0
  110. package/src/load-client.ts +2701 -0
  111. package/src/load-server.ts +849 -0
  112. package/src/redirect.ts +0 -1
  113. package/src/route.ts +2 -11
  114. package/src/router.ts +379 -769
  115. package/src/ssr/createRequestHandler.ts +109 -16
  116. package/src/ssr/handlerCallback.ts +76 -10
  117. package/src/ssr/server.ts +4 -1
  118. package/src/ssr/ssr-client.ts +1 -310
  119. package/src/ssr/ssr-match-id.ts +11 -2
  120. package/src/ssr/ssr-server.ts +9 -8
  121. package/src/ssr/transformStreamWithRouter.ts +37 -1
  122. package/src/ssr/types.ts +0 -1
  123. package/src/stores.ts +46 -208
  124. package/dist/cjs/load-matches.cjs +0 -658
  125. package/dist/cjs/load-matches.cjs.map +0 -1
  126. package/dist/cjs/load-matches.d.cts +0 -18
  127. package/dist/cjs/ssr/ssr-client.cjs +0 -183
  128. package/dist/cjs/ssr/ssr-client.cjs.map +0 -1
  129. package/dist/esm/load-matches.d.ts +0 -18
  130. package/dist/esm/load-matches.js +0 -656
  131. package/dist/esm/load-matches.js.map +0 -1
  132. package/dist/esm/ssr/ssr-client.js +0 -183
  133. package/dist/esm/ssr/ssr-client.js.map +0 -1
  134. package/src/load-matches.ts +0 -1278
@@ -0,0 +1,2701 @@
1
+ // Keep this filename free of a secondary extension so declaration generation
2
+ // can rewrite relative imports for both ESM and CJS.
3
+ import { isNotFound } from './not-found'
4
+ import { isRedirect } from './redirect'
5
+ import {
6
+ _getUserHistoryState,
7
+ getLocationChangeInfo,
8
+ runRouteLifecycle,
9
+ } from './router'
10
+ import { deepEqual } from './utils'
11
+ import { hydrateSsrMatchId } from './ssr/ssr-match-id'
12
+ import type { GLOBAL_SEROVAL, GLOBAL_TSR } from './ssr/constants'
13
+ import type { AnySerializationAdapter } from './ssr/serializer/transformer'
14
+ import type { TsrSsrGlobal } from './ssr/types'
15
+ import type { ParsedLocation } from './location'
16
+ import type { AnyRouteMatch } from './Matches'
17
+ import type { NotFoundError } from './not-found'
18
+ import type {
19
+ AnyRoute,
20
+ BeforeLoadContextOptions,
21
+ LoaderFnContext,
22
+ RouteContextOptions,
23
+ RouteLoaderFn,
24
+ } from './route'
25
+ import type { AnyRedirect } from './redirect'
26
+ import type { AnyRouter } from './router'
27
+
28
+ type RouteComponentType =
29
+ | 'component'
30
+ | 'pendingComponent'
31
+ | 'errorComponent'
32
+ | 'notFoundComponent'
33
+
34
+ export function replaceRouteChunk(
35
+ route: AnyRoute,
36
+ lazyFn: AnyRoute['lazyFn'],
37
+ ): void {
38
+ route.lazyFn = lazyFn ?? route.lazyFn
39
+ route._lazy = undefined
40
+ }
41
+
42
+ function preloadComponent(
43
+ route: AnyRoute,
44
+ type: RouteComponentType,
45
+ ): Promise<void> | undefined {
46
+ return (route.options[type] as any)?.preload?.()
47
+ }
48
+
49
+ function loadComponents(route: AnyRoute): Promise<void> | undefined {
50
+ const component = preloadComponent(route, 'component')
51
+ const pending = preloadComponent(route, 'pendingComponent')
52
+ if (component && pending) {
53
+ return Promise.all([component, pending]).then(() => {})
54
+ }
55
+ return component ?? pending
56
+ }
57
+
58
+ export function loadRouteChunk(
59
+ route: AnyRoute,
60
+ // `false` waits only for lazy route options, before a boundary is selected.
61
+ componentType?: 'errorComponent' | 'notFoundComponent' | false,
62
+ ): Promise<void> | undefined {
63
+ const afterLazy = () =>
64
+ componentType === false
65
+ ? undefined
66
+ : componentType
67
+ ? preloadComponent(route, componentType)
68
+ : loadComponents(route)
69
+ const current = route._lazy
70
+ if (current) {
71
+ return current === true ? afterLazy() : current.then(afterLazy)
72
+ }
73
+ if (!route.lazyFn) {
74
+ return afterLazy()
75
+ }
76
+
77
+ const promise = route.lazyFn().then(
78
+ (lazyRoute) => {
79
+ // HMR clears the owner before an obsolete import can settle.
80
+ if (process.env.NODE_ENV === 'production' || route._lazy === promise) {
81
+ const { id: _id, ...options } = lazyRoute.options
82
+ Object.assign(route.options, options)
83
+ route._lazy = true
84
+ }
85
+ },
86
+ (error) => {
87
+ if (process.env.NODE_ENV === 'production' || route._lazy === promise) {
88
+ route._lazy = undefined
89
+ }
90
+ throw error
91
+ },
92
+ )
93
+ route._lazy = promise
94
+ return promise.then(afterLazy)
95
+ }
96
+
97
+ /** Return the structural lane through the first terminal render boundary. */
98
+ export function _getRenderedMatches(
99
+ matches: Array<AnyRouteMatch>,
100
+ ): Array<AnyRouteMatch> {
101
+ const end =
102
+ matches.findIndex(
103
+ (match) => match.status !== 'success' || match._notFound,
104
+ ) + 1
105
+ return end && end < matches.length ? matches.slice(0, end) : matches
106
+ }
107
+
108
+ /** Return the lane whose document assets belong to the current presentation. */
109
+ export function _getAssetMatches(
110
+ matches: Array<AnyRouteMatch>,
111
+ ): Array<AnyRouteMatch> {
112
+ let end = matches.length
113
+ for (let index = 0; index < end; index++) {
114
+ const match = matches[index]!
115
+ // `_assetEnd` is only ever set on hydration presentation clones that are
116
+ // `status: 'pending'`, `ssr: 'data-only'`, error-free, and not not-found
117
+ // (see hydrate.ts), and commits clear it — so its presence alone is the guard.
118
+ if (match._assetEnd !== undefined) {
119
+ end = Math.min(end, Math.max(index + 1, match._assetEnd))
120
+ continue
121
+ }
122
+ if (match.status !== 'success' || match._notFound) {
123
+ end = index + 1
124
+ break
125
+ }
126
+ }
127
+ // `end` only ever shrinks to `index + 1 >= 1`, so no zero guard is needed.
128
+ return end < matches.length ? matches.slice(0, end) : matches
129
+ }
130
+
131
+ declare const lanePhase: unique symbol
132
+
133
+ type LanePhase = 'matched' | 'contextualized' | 'reduced' | 'projected'
134
+
135
+ /**
136
+ * Lane matches carry their lane's phase so functions can demand evidence of
137
+ * pipeline position (e.g. `commitMatches` only accepts a projected lane's
138
+ * matches). The brand is phantom — it never exists at runtime.
139
+ */
140
+ type LaneMatches<TPhase extends LanePhase> = Array<WorkMatch> & {
141
+ readonly [lanePhase]?: TPhase
142
+ }
143
+
144
+ type Lane<TPhase extends LanePhase> = [
145
+ location: ParsedLocation,
146
+ matches: LaneMatches<TPhase>,
147
+ background?: Array<BackgroundLoaderTask>,
148
+ backgroundSettlement?: Promise<IndexedOutcome | undefined>,
149
+ ] & { readonly [lanePhase]?: TPhase }
150
+
151
+ type MatchedLane = Lane<'matched'>
152
+ type ContextualizedLane = Lane<'contextualized'>
153
+ type ReducedLane = Lane<'reduced'>
154
+ type ProjectedLane = Lane<'projected'>
155
+
156
+ const SUCCESS = 0
157
+ const ERROR = 1
158
+ const NOT_FOUND = 2
159
+ // Control outcomes stay contiguous so the hot path can test them together.
160
+ const REDIRECTED = 3
161
+ const CANCELED = 4
162
+
163
+ 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]
169
+
170
+ type IndexedOutcome = [index: number, outcome: LoaderOutcome, boundary?: number]
171
+
172
+ export type LoaderFlight = [
173
+ outcome: Promise<LoaderOutcome>,
174
+ controller: AbortController,
175
+ leases: number,
176
+ ]
177
+
178
+ type WorkMatch = AnyRouteMatch & {
179
+ _flight?: LoaderFlight
180
+ }
181
+
182
+ declare const matchPhase: unique symbol
183
+
184
+ /**
185
+ * A match whose loader outcome has been applied by `settleInto`, which is the
186
+ * sole granter of this brand (phantom, zero-runtime). Consumers that require
187
+ * it — e.g. `cacheLoaderMatch` — can only be reached after settlement, so the
188
+ * compiler enforces the loader→settle→cache ordering. Sources that arrive
189
+ * already settled (dehydrated server data) must cast at a named boundary.
190
+ */
191
+ type SettledMatch = WorkMatch & { readonly [matchPhase]: 'settled' }
192
+
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
+ export type LoadTransaction = [
219
+ controller: AbortController,
220
+ redirects: number,
221
+ location: ParsedLocation,
222
+ matches: Array<AnyRouteMatch>,
223
+ startedAt: number,
224
+ done: Promise<void>,
225
+ /**
226
+ * 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.
229
+ */
230
+ refresh?: [
231
+ presentation: Array<AnyRouteMatch>,
232
+ handoff: NonNullable<AnyRouter['_handoff']> | undefined,
233
+ ],
234
+ ]
235
+
236
+ export type PendingSession = [
237
+ owner: LoadTransaction,
238
+ boundary: number,
239
+ /** Pending reveal time until acknowledged, then minimum-visible-until time. */
240
+ deadline: number,
241
+ timer?: ReturnType<typeof setTimeout>,
242
+ ack?: Promise<boolean>,
243
+ component?: unknown,
244
+ ]
245
+
246
+ type CoordinatorRouter = AnyRouter & {
247
+ /** Whole speculative lanes that a matching navigation may adopt. */
248
+ _preloads?: Map<string, ActivePreload>
249
+ _refreshNextLoad?: boolean
250
+ _rollbackRefresh?: () => void
251
+ _cancelTransition?: () => void
252
+ }
253
+
254
+ type PublicationCheckpoint = {
255
+ previousMatches: Array<AnyRouteMatch>
256
+ previousPresentation: Array<AnyRouteMatch>
257
+ previousCache: Map<string, AnyRouteMatch>
258
+ commitPromise: CoordinatorRouter['_commitPromise']
259
+ published: boolean
260
+ }
261
+
262
+ type LoaderTask = [
263
+ index: number,
264
+ outcome: Promise<LoaderOutcome>,
265
+ ready: Promise<IndexedOutcome | undefined>,
266
+ candidate?: WorkMatch,
267
+ ]
268
+
269
+ type BackgroundLoaderTask = [
270
+ index: number,
271
+ outcome: Promise<LoaderOutcome>,
272
+ ready: Promise<IndexedOutcome | undefined>,
273
+ candidate: WorkMatch,
274
+ ]
275
+
276
+ type ExecuteLaneOptions = [
277
+ controller: AbortController,
278
+ redirects: number,
279
+ isCurrent: () => boolean,
280
+ base: Array<AnyRouteMatch>,
281
+ preload?: boolean,
282
+ sync?: boolean,
283
+ forceStaleReload?: boolean,
284
+ resolvedPrefix?: number,
285
+ onReady?: () => void,
286
+ ]
287
+
288
+ type ControlOutcome =
289
+ | [typeof REDIRECTED, redirect: AnyRedirect]
290
+ | [typeof CANCELED]
291
+
292
+ type LaneResult = ProjectedLane | ControlOutcome
293
+
294
+ function isControl(
295
+ result: Lane<any> | ControlOutcome,
296
+ ): result is ControlOutcome {
297
+ return typeof result[0] === 'number'
298
+ }
299
+
300
+ export function waitFor<T>(
301
+ value: T | PromiseLike<T>,
302
+ signal: AbortSignal,
303
+ ): Promise<T> {
304
+ if (signal.aborted) {
305
+ return Promise.race([Promise.reject(signal), value])
306
+ }
307
+ return new Promise<T>((resolve, reject) => {
308
+ const abort = () => reject(signal)
309
+ signal.addEventListener('abort', abort, { once: true })
310
+ Promise.resolve(value)
311
+ .then(resolve, reject)
312
+ .finally(() => signal.removeEventListener('abort', abort))
313
+ })
314
+ }
315
+
316
+ export function getRoute(router: AnyRouter, match: WorkMatch): AnyRoute {
317
+ return (router.routesById as Record<string, AnyRoute>)[match.routeId]!
318
+ }
319
+
320
+ function normalize(
321
+ value: unknown,
322
+ rejected: boolean,
323
+ routeId?: string,
324
+ ): LoaderOutcome {
325
+ if (isRedirect(value)) {
326
+ return [REDIRECTED, value]
327
+ }
328
+ if (isNotFound(value)) {
329
+ value.routeId ||= routeId
330
+ return [NOT_FOUND, value]
331
+ }
332
+ if (rejected && typeof (value as any)?.then === 'function') {
333
+ value = new Error('A Promise was thrown', { cause: value })
334
+ }
335
+ return rejected ? [ERROR, value] : [SUCCESS, value]
336
+ }
337
+
338
+ function normalizeError(route: AnyRoute, cause: unknown): LoaderOutcome {
339
+ let outcome = normalize(cause, true, route.id)
340
+ if (outcome[0] !== ERROR) {
341
+ return outcome
342
+ }
343
+ try {
344
+ route.options.onError?.(outcome[1])
345
+ } catch (onErrorCause) {
346
+ outcome = normalize(onErrorCause, true, route.id)
347
+ }
348
+ return outcome
349
+ }
350
+
351
+ function normalizeLaneError(
352
+ route: AnyRoute,
353
+ cause: unknown,
354
+ options: ExecuteLaneOptions,
355
+ ): LoaderOutcome {
356
+ if (options[0].signal.aborted || !options[2]()) {
357
+ options[0].abort()
358
+ return [CANCELED]
359
+ }
360
+ return normalizeError(route, cause)
361
+ }
362
+
363
+ export function navigateFrom(router: AnyRouter, location: ParsedLocation) {
364
+ return (opts: any) =>
365
+ router.navigate({
366
+ ...opts,
367
+ _fromLocation: location,
368
+ })
369
+ }
370
+
371
+ async function contextualize(
372
+ router: AnyRouter,
373
+ lane: MatchedLane,
374
+ options: ExecuteLaneOptions,
375
+ end: number,
376
+ ): Promise<IndexedOutcome | undefined> {
377
+ 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++) {
381
+ const match = matches[index]!
382
+ const route = getRoute(router, match)
383
+
384
+ match.abortController = options[0]
385
+ // Contextualization is serial, so the previous match already contains the
386
+ // complete parent context for this route.
387
+ const parentContext =
388
+ matches[index - 1]?.context ?? router.options.context ?? {}
389
+ const common = {
390
+ params: match.params,
391
+ location,
392
+ navigate: navigateFrom(router, location),
393
+ buildLocation: router.buildLocation,
394
+ cause: preload ? ('preload' as const) : match.cause,
395
+ abortController: options[0],
396
+ preload,
397
+ matches,
398
+ routeId: route.id,
399
+ }
400
+ let context = parentContext
401
+ try {
402
+ let routeContext = match._ctx
403
+ if (!routeContext && route.options.context) {
404
+ routeContext = match._ctx =
405
+ route.options.context({
406
+ ...common,
407
+ deps: match.loaderDeps,
408
+ context: parentContext,
409
+ } satisfies RouteContextOptions<any, any, any, any, any>) || {}
410
+ }
411
+ context = {
412
+ ...parentContext,
413
+ ...routeContext,
414
+ }
415
+ match.context = context
416
+ } catch (cause) {
417
+ releaseFlight(router, match)
418
+ return [index, normalizeLaneError(route, cause, options)]
419
+ }
420
+ if (signal.aborted || !options[2]()) {
421
+ options[0].abort()
422
+ return [index, [CANCELED]]
423
+ }
424
+ const validationError = match.paramsError ?? match.searchError
425
+ if (validationError !== undefined) {
426
+ releaseFlight(router, match)
427
+ return [index, normalizeLaneError(route, validationError, options)]
428
+ }
429
+ const beforeLoad = route.options.beforeLoad
430
+ if (!beforeLoad) {
431
+ continue
432
+ }
433
+
434
+ const beforeLoadContext: BeforeLoadContextOptions<
435
+ any,
436
+ any,
437
+ any,
438
+ any,
439
+ any,
440
+ any,
441
+ any,
442
+ any,
443
+ any
444
+ > = {
445
+ ...common,
446
+ search: match.search,
447
+ context,
448
+ ...router.options.additionalContext,
449
+ }
450
+
451
+ const previousStatus = match.status
452
+ if (previousStatus === 'success') {
453
+ match.status = 'pending'
454
+ }
455
+ options[8]?.()
456
+ try {
457
+ setFetching(router, match, 'beforeLoad', options[0])
458
+ const result = await waitFor(beforeLoad(beforeLoadContext), signal)
459
+ if (!options[2]()) {
460
+ options[0].abort()
461
+ return [index, [CANCELED]]
462
+ }
463
+ const outcome = normalize(result, false, route.id)
464
+ if (outcome[0] !== SUCCESS) {
465
+ releaseFlight(router, match)
466
+ return [index, outcome]
467
+ }
468
+ match.context = {
469
+ ...context,
470
+ ...result,
471
+ }
472
+ } catch (cause) {
473
+ releaseFlight(router, match)
474
+ return [index, normalizeLaneError(route, cause, options)]
475
+ } finally {
476
+ if (previousStatus === 'success' && match.status === 'pending') {
477
+ match.status = 'success'
478
+ }
479
+ setFetching(router, match, false, options[0])
480
+ }
481
+ }
482
+
483
+ return
484
+ }
485
+
486
+ function releaseOwnedFlight(
487
+ router: AnyRouter,
488
+ id: string,
489
+ flight?: LoaderFlight,
490
+ ): AbortController | undefined {
491
+ if (!flight || --flight[2]) {
492
+ return
493
+ }
494
+ if (router._flights?.get(id) === flight) {
495
+ router._flights.delete(id)
496
+ }
497
+ return flight[1]
498
+ }
499
+
500
+ function releaseFlight(router: AnyRouter, match: WorkMatch): void {
501
+ const flight = match._flight
502
+ 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
+ )
539
+ }
540
+
541
+ /**
542
+ * Not passing in a `next` ownership recipient
543
+ * is equivalent to discarding the match resources
544
+ */
545
+ export function transferMatchResources(
546
+ router: AnyRouter,
547
+ previous: Array<AnyRouteMatch>,
548
+ next?: Array<AnyRouteMatch>,
549
+ ): void {
550
+ const abort: Array<AbortController> = []
551
+ for (const match of previous as Array<WorkMatch>) {
552
+ if (!next?.includes(match)) {
553
+ const flight = match._flight
554
+ match._flight = undefined
555
+ const controller = releaseOwnedFlight(router, match.id, flight)
556
+ if (controller) {
557
+ abort.push(controller)
558
+ }
559
+ }
560
+ }
561
+ for (const controller of abort) {
562
+ controller.abort()
563
+ }
564
+ }
565
+
566
+ function discardPreload(router: AnyRouter, preload: ActivePreload): void {
567
+ preload[1].abort()
568
+ transferMatchResources(router, preload[0])
569
+ }
570
+
571
+ function acquireMatchResources(matches: Array<AnyRouteMatch>): void {
572
+ for (const match of matches as Array<WorkMatch>) {
573
+ const flight = match._flight
574
+ if (flight) {
575
+ flight[2]++
576
+ }
577
+ }
578
+ }
579
+
580
+ function setFetching(
581
+ router: AnyRouter,
582
+ match: WorkMatch,
583
+ value: AnyRouteMatch['isFetching'],
584
+ owner?: AbortController,
585
+ ): void {
586
+ match.isFetching = value
587
+ if (owner && router._tx?.[0] !== owner) {
588
+ return
589
+ }
590
+ const store = router.stores.byRoute.get(match.routeId)
591
+ const presented = store?.get()
592
+ if (presented?.id === match.id) {
593
+ store!.set({ ...presented, isFetching: value })
594
+ }
595
+ }
596
+
597
+ function getLoaderContext(
598
+ router: AnyRouter,
599
+ lane: ContextualizedLane,
600
+ match: WorkMatch,
601
+ route: AnyRoute,
602
+ controller: AbortController,
603
+ parentMatchPromise: Promise<WorkMatch> | undefined,
604
+ preload: boolean,
605
+ ): LoaderFnContext {
606
+ const location = lane[0]
607
+ return {
608
+ params: match.params,
609
+ location,
610
+ navigate: navigateFrom(router, location),
611
+ cause: preload ? ('preload' as const) : match.cause,
612
+ abortController: controller,
613
+ preload,
614
+ deps: match.loaderDeps,
615
+ parentMatchPromise: parentMatchPromise as any,
616
+ context: match.context,
617
+ route,
618
+ ...router.options.additionalContext,
619
+ }
620
+ }
621
+
622
+ async function loadResource(
623
+ router: AnyRouter,
624
+ lane: ContextualizedLane,
625
+ match: WorkMatch,
626
+ route: AnyRoute,
627
+ loader: RouteLoaderFn<any> | undefined,
628
+ parentMatchPromise: Promise<WorkMatch> | undefined,
629
+ preload: boolean,
630
+ owner: AbortController,
631
+ ): Promise<LoaderOutcome> {
632
+ const signal = owner.signal
633
+ if (signal.aborted) {
634
+ return [CANCELED]
635
+ }
636
+ if (!loader) {
637
+ return [SUCCESS, undefined]
638
+ }
639
+
640
+ let flight = match._flight
641
+ let joined = !!flight
642
+ setFetching(router, match, 'loader', owner)
643
+ try {
644
+ for (;;) {
645
+ if (!flight) {
646
+ const controller = new AbortController()
647
+ const outcome = Promise.resolve()
648
+ .then(() =>
649
+ loader(
650
+ getLoaderContext(
651
+ router,
652
+ lane,
653
+ match,
654
+ route,
655
+ controller,
656
+ parentMatchPromise,
657
+ preload,
658
+ ),
659
+ ),
660
+ )
661
+ .then(
662
+ (value) => normalize(value, false, route.id),
663
+ (cause) => normalize(cause, true, route.id),
664
+ )
665
+ .then((result): LoaderOutcome => {
666
+ return result[0] === ERROR && match._flight === flight
667
+ ? normalizeError(route, result[1])
668
+ : 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
693
+ }
694
+ } finally {
695
+ setFetching(router, match, false, owner)
696
+ }
697
+ }
698
+
699
+ function settleInto(
700
+ match: WorkMatch,
701
+ result: LoaderOutcome,
702
+ preload: boolean,
703
+ ): asserts match is SettledMatch {
704
+ if (result[0] === SUCCESS) {
705
+ match.loaderData = result[1]
706
+ match.error = undefined
707
+ match.status = 'success'
708
+ match.invalid = false
709
+ match.updatedAt = Date.now()
710
+ match.preload = preload
711
+ } else if (result[0] !== REDIRECTED) {
712
+ // Reduction installs only the selected terminal failure. Every other
713
+ // settled attempt remains a renderable, stale match in that lane.
714
+ match.status = 'success'
715
+ match.error = undefined
716
+ match.invalid = true
717
+ }
718
+ }
719
+
720
+ export function cacheLoaderMatch(
721
+ router: CoordinatorRouter,
722
+ match: SettledMatch,
723
+ planned: AnyRouteMatch | undefined,
724
+ ): void {
725
+ const current = router._cache.get(match.id) as WorkMatch | undefined
726
+ if (
727
+ current !== planned ||
728
+ router._committed.some(
729
+ (candidate) =>
730
+ candidate.id === match.id &&
731
+ (candidate as WorkMatch)._flight === match._flight,
732
+ )
733
+ ) {
734
+ return
735
+ }
736
+ const cached = {
737
+ ...match,
738
+ _notFound: undefined,
739
+ context: {},
740
+ } as WorkMatch
741
+ if (cached._flight) {
742
+ cached._flight[2]++
743
+ }
744
+ router._cache.set(match.id, cached)
745
+ if (current) {
746
+ releaseFlight(router, current)
747
+ }
748
+ }
749
+
750
+ function getParentSnapshot(
751
+ match: WorkMatch,
752
+ outcome: LoaderOutcome,
753
+ ): WorkMatch {
754
+ if (outcome[0] === ERROR || outcome[0] === NOT_FOUND) {
755
+ return {
756
+ ...match,
757
+ status: outcome[0] === ERROR ? 'error' : 'notFound',
758
+ error: outcome[1],
759
+ _flight: undefined,
760
+ }
761
+ }
762
+ return match
763
+ }
764
+
765
+ function createLoaderTask(
766
+ router: AnyRouter,
767
+ lane: ContextualizedLane,
768
+ index: number,
769
+ tasks: Array<LoaderTask>,
770
+ semanticParent: Promise<WorkMatch> | undefined,
771
+ options: ExecuteLaneOptions,
772
+ ): Promise<WorkMatch> {
773
+ const match = lane[1][index]!
774
+ const route = getRoute(router, match)
775
+ const preload = !!options[4]
776
+ const plannedCacheMatch = preload ? router._cache.get(match.id) : undefined
777
+ let reload = false
778
+ let reloadFailure: LoaderOutcome | undefined
779
+ try {
780
+ let configured
781
+ if (match.status === 'success') {
782
+ configured = route.options.shouldReload
783
+ if (typeof configured === 'function') {
784
+ configured = configured(
785
+ getLoaderContext(
786
+ router,
787
+ lane,
788
+ match,
789
+ route,
790
+ options[0],
791
+ semanticParent,
792
+ preload,
793
+ ),
794
+ )
795
+ }
796
+ if (!options[2]()) {
797
+ options[0].abort()
798
+ reloadFailure = [CANCELED]
799
+ }
800
+ }
801
+ if (!reloadFailure) {
802
+ if (match.status !== 'success') {
803
+ reload = true
804
+ } else {
805
+ const staleAge =
806
+ options[4] || match.preload
807
+ ? (route.options.preloadStaleTime ??
808
+ router.options.defaultPreloadStaleTime ??
809
+ 30_000)
810
+ : (route.options.staleTime ?? router.options.defaultStaleTime ?? 0)
811
+ reload = !!(
812
+ match.invalid ||
813
+ configured ||
814
+ (configured === undefined &&
815
+ Date.now() - match.updatedAt >= staleAge &&
816
+ (options[6] ||
817
+ match.cause === 'enter' ||
818
+ options[3].some(
819
+ (candidate) =>
820
+ candidate.routeId === match.routeId &&
821
+ candidate.id !== match.id,
822
+ )))
823
+ )
824
+ }
825
+ }
826
+ } catch (cause) {
827
+ match.invalid = true
828
+ releaseFlight(router, match)
829
+ reloadFailure = normalizeLaneError(route, cause, options)
830
+ }
831
+ const routeLoader = route.options.loader
832
+ const loader =
833
+ typeof routeLoader === 'function' ? routeLoader : routeLoader?.handler
834
+ const background = !!(
835
+ routeLoader &&
836
+ reload &&
837
+ match.status === 'success' &&
838
+ !preload &&
839
+ !options[5] &&
840
+ ((typeof routeLoader === 'function'
841
+ ? undefined
842
+ : routeLoader?.staleReloadMode) ??
843
+ router.options.defaultStaleReloadMode) !== 'blocking'
844
+ )
845
+ const loaded = reload && (!preload || route.options.preload !== false)
846
+ const blocking =
847
+ loaded && !background && (match.status !== 'success' || !!routeLoader)
848
+ if (loaded && !routeLoader) {
849
+ match.invalid = false
850
+ match.updatedAt = Date.now()
851
+ }
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]++
857
+ }
858
+ if (blocking) {
859
+ const acceptedFlight = match._flight
860
+ match._flight = donor
861
+ releaseOwnedFlight(router, match.id, acceptedFlight)?.abort()
862
+ // A successful route without a loader has no blocking work to present. It
863
+ // still gets a task so its chunk and derived assets participate in the
864
+ // lane, but putting it back into pending would hide an already-rendered
865
+ // ancestor while only a descendant is loading.
866
+ if (match.status === 'success') {
867
+ match.status = 'pending'
868
+ }
869
+ options[8]?.()
870
+ }
871
+ if (!loaded) {
872
+ match.isFetching = false
873
+ }
874
+ const rawOutcome = reloadFailure
875
+ ? Promise.resolve(reloadFailure)
876
+ : !blocking
877
+ ? Promise.resolve<LoaderOutcome>([SUCCESS, match.loaderData])
878
+ : loadResource(
879
+ router,
880
+ lane,
881
+ match,
882
+ route,
883
+ loader,
884
+ semanticParent,
885
+ preload,
886
+ options[0],
887
+ )
888
+ const outcome = rawOutcome.then((result) => {
889
+ if (blocking) {
890
+ settleInto(match, result, preload)
891
+ if (result[0] === SUCCESS) {
892
+ if (preload && routeLoader) {
893
+ cacheLoaderMatch(router, match, plannedCacheMatch)
894
+ }
895
+ // A route is renderable only after both its data and normal component
896
+ // chunk are ready. Its loader data is already available to descendants.
897
+ match.status = 'pending'
898
+ }
899
+ }
900
+ return result
901
+ })
902
+
903
+ const rawChunkFailure = waitFor(
904
+ Promise.resolve().then(() => loadRouteChunk(route)),
905
+ options[0].signal,
906
+ ).then(
907
+ () => {
908
+ options[8]?.()
909
+ return undefined
910
+ },
911
+ (cause): IndexedOutcome => [
912
+ index,
913
+ normalizeLaneError(route, cause, options),
914
+ ],
915
+ )
916
+ const chunkFailure = rawChunkFailure.then((failure) =>
917
+ outcome.then((result) => {
918
+ if (
919
+ blocking &&
920
+ !failure &&
921
+ result[0] === SUCCESS &&
922
+ match.status === 'pending' &&
923
+ options[2]()
924
+ ) {
925
+ match.status = 'success'
926
+ options[8]?.()
927
+ }
928
+ return failure
929
+ }),
930
+ )
931
+ tasks.push([index, outcome, chunkFailure])
932
+ if (!background) {
933
+ return outcome.then((result) => getParentSnapshot(match, result))
934
+ }
935
+ const candidate: WorkMatch = {
936
+ ...match,
937
+ status: 'pending',
938
+ preload: false,
939
+ _flight: donor,
940
+ }
941
+ match.invalid = false
942
+ match.isFetching = 'loader'
943
+ const backgroundOutcome = loadResource(
944
+ router,
945
+ lane,
946
+ candidate,
947
+ route,
948
+ loader,
949
+ semanticParent,
950
+ false,
951
+ options[0],
952
+ ).then((result) => {
953
+ match.isFetching = false
954
+ settleInto(candidate, result, false)
955
+ return result
956
+ })
957
+ ;(lane[2] ??= []).push([index, backgroundOutcome, chunkFailure, candidate])
958
+ return backgroundOutcome.then((result) =>
959
+ getParentSnapshot(candidate, result),
960
+ )
961
+ }
962
+
963
+ async function getNotFoundBoundary(
964
+ router: AnyRouter,
965
+ matches: Array<WorkMatch>,
966
+ indexed: IndexedOutcome | undefined,
967
+ signal: AbortSignal,
968
+ fallback = 0,
969
+ ): Promise<number> {
970
+ const cause = indexed?.[1][1] as NotFoundError | undefined
971
+ let index = cause?.routeId
972
+ ? matches.findIndex((match) => match.routeId === cause.routeId)
973
+ : (indexed?.[0] ?? matches.length - 1)
974
+ if (index < 0) {
975
+ index = 0
976
+ }
977
+ for (let i = index; i >= 0; i--) {
978
+ const route = getRoute(router, matches[i]!)
979
+ const loading = loadRouteChunk(route, false)
980
+ if (loading) {
981
+ try {
982
+ await waitFor(loading, signal)
983
+ } catch (cause) {
984
+ if (cause === signal) {
985
+ throw cause
986
+ }
987
+ }
988
+ }
989
+ if (route.options.notFoundComponent) {
990
+ return i
991
+ }
992
+ }
993
+ return cause?.routeId ? index : fallback
994
+ }
995
+
996
+ function discardBackground(router: AnyRouter, lane: Lane<any>): void {
997
+ if (lane[2]) {
998
+ transferMatchResources(
999
+ router,
1000
+ lane[2].map((task) => task[3]),
1001
+ )
1002
+ lane[2] = undefined
1003
+ }
1004
+ }
1005
+
1006
+ async function settleTasks(
1007
+ tasks: Array<LoaderTask>,
1008
+ serialFailure?: IndexedOutcome,
1009
+ redirectTasks?: Array<BackgroundLoaderTask>,
1010
+ gate?: number | Promise<number>,
1011
+ ): Promise<IndexedOutcome | undefined> {
1012
+ let loaderFailure: IndexedOutcome | undefined
1013
+
1014
+ try {
1015
+ await Promise.all(
1016
+ tasks.map((task) =>
1017
+ task[1].then(async (outcome) => {
1018
+ const taskIndex = task[0]
1019
+ if (gate && taskIndex >= (await gate)) {
1020
+ return
1021
+ }
1022
+ if (outcome[0] >= REDIRECTED) {
1023
+ throw [taskIndex, outcome] as IndexedOutcome
1024
+ }
1025
+ if (!loaderFailure && outcome[0] !== SUCCESS) {
1026
+ loaderFailure = [taskIndex, outcome]
1027
+ // Every started descendant must settle before an ordinary failure
1028
+ // wins because a redirect from any of them remains control flow.
1029
+ await Promise.all(
1030
+ (redirectTasks ?? []).map((nextTask) => {
1031
+ if (nextTask[0] <= taskIndex) {
1032
+ return
1033
+ }
1034
+ return nextTask[1].then((nextOutcome) => {
1035
+ if (nextOutcome[0] === REDIRECTED) {
1036
+ throw [nextTask[0], nextOutcome] as IndexedOutcome
1037
+ }
1038
+ })
1039
+ }),
1040
+ )
1041
+ }
1042
+ }),
1043
+ ),
1044
+ )
1045
+ } catch (cause) {
1046
+ return cause as IndexedOutcome
1047
+ }
1048
+ return serialFailure ?? loaderFailure
1049
+ }
1050
+
1051
+ async function reduceLane(
1052
+ router: AnyRouter,
1053
+ lane: ContextualizedLane,
1054
+ tasks: Array<LoaderTask>,
1055
+ controller: AbortController,
1056
+ redirects: number,
1057
+ settlement: Promise<IndexedOutcome | undefined>,
1058
+ onReady?: () => void,
1059
+ ): Promise<ReducedLane | ControlOutcome> {
1060
+ const matches = lane[1]
1061
+ let failure = await settlement
1062
+ let redirectLimitExceeded = false
1063
+ const plannedBoundary = matches.findIndex((match) => match._notFound)
1064
+ const boundaryOf = (found: IndexedOutcome) =>
1065
+ found[1][0] === NOT_FOUND
1066
+ ? getNotFoundBoundary(router, matches, found, controller.signal)
1067
+ : found[0]
1068
+ let readinessEnd = plannedBoundary < 0 ? matches.length : plannedBoundary
1069
+
1070
+ if ((failure?.[1][0] ?? 0) >= REDIRECTED) {
1071
+ readinessEnd = 0
1072
+ } else if (failure) {
1073
+ readinessEnd = failure[2] ??= await boundaryOf(failure)
1074
+ for (const task of tasks) {
1075
+ if (task[0] >= readinessEnd) {
1076
+ break
1077
+ }
1078
+ const outcome = await task[1]
1079
+ // Presence means a loader previously succeeded, even with `undefined`.
1080
+ if (
1081
+ outcome[0] !== SUCCESS &&
1082
+ outcome[0] < REDIRECTED &&
1083
+ !('loaderData' in matches[task[0]]!)
1084
+ ) {
1085
+ failure = [task[0], outcome]
1086
+ readinessEnd = failure[2] = await boundaryOf(failure)
1087
+ break
1088
+ }
1089
+ }
1090
+ }
1091
+
1092
+ for (const task of tasks) {
1093
+ if (task[0] >= readinessEnd) {
1094
+ break
1095
+ }
1096
+ const chunkFailure = await task[2]
1097
+ if (!chunkFailure) {
1098
+ continue
1099
+ }
1100
+ failure = chunkFailure
1101
+ break
1102
+ }
1103
+
1104
+ if ((failure?.[1][0] ?? 0) >= REDIRECTED) {
1105
+ const outcome = failure![1]
1106
+ if (
1107
+ outcome[0] !== REDIRECTED ||
1108
+ outcome[1].options.reloadDocument ||
1109
+ redirects < 20
1110
+ ) {
1111
+ discardBackground(router, lane)
1112
+ return outcome as ControlOutcome
1113
+ }
1114
+ redirectLimitExceeded = true
1115
+ failure = [0, [ERROR, new Error('Too many redirects')]]
1116
+ }
1117
+
1118
+ const boundary = failure
1119
+ ? (failure[2] ?? (await boundaryOf(failure)))
1120
+ : plannedBoundary
1121
+ if (boundary >= 0) {
1122
+ const outcome = failure?.[1]
1123
+ const kind = outcome?.[0]
1124
+ const match = matches[boundary]!
1125
+ const cause = outcome?.[1]
1126
+ const install = () => {
1127
+ if (outcome) {
1128
+ match._notFound = undefined
1129
+ if (kind === ERROR) {
1130
+ match.status = 'error'
1131
+ } else {
1132
+ ;(cause as NotFoundError).routeId = match.routeId
1133
+ if (match.routeId === router.routeTree.id) {
1134
+ match.status = 'success'
1135
+ match._notFound = true
1136
+ } else {
1137
+ match.status = 'notFound'
1138
+ }
1139
+ }
1140
+ match.error = cause
1141
+ match.isFetching = false
1142
+ }
1143
+ }
1144
+ install()
1145
+ try {
1146
+ await waitFor<unknown>(
1147
+ outcome
1148
+ ? Promise.resolve().then(() =>
1149
+ loadRouteChunk(
1150
+ getRoute(router, match),
1151
+ kind === ERROR ? 'errorComponent' : 'notFoundComponent',
1152
+ ),
1153
+ )
1154
+ : Promise.all([
1155
+ loadRouteChunk(getRoute(router, match)),
1156
+ loadRouteChunk(getRoute(router, match), 'notFoundComponent'),
1157
+ ]),
1158
+ controller.signal,
1159
+ )
1160
+ } catch (cause) {
1161
+ if (cause === controller.signal) {
1162
+ discardBackground(router, lane)
1163
+ return [CANCELED]
1164
+ }
1165
+ }
1166
+ if (!outcome) {
1167
+ match.status = 'success'
1168
+ onReady?.()
1169
+ } else if (redirectLimitExceeded) {
1170
+ controller.abort()
1171
+ await Promise.all([
1172
+ ...tasks.map((task) => task[1]),
1173
+ ...tasks.map((task) => task[2]),
1174
+ ...(lane[2] ?? []).map((task) => task[1]),
1175
+ ])
1176
+ discardBackground(router, lane)
1177
+ transferMatchResources(router, matches)
1178
+ install()
1179
+ }
1180
+ }
1181
+
1182
+ return lane as ReducedLane
1183
+ }
1184
+
1185
+ export async function projectLane(
1186
+ router: AnyRouter,
1187
+ lane: ReducedLane,
1188
+ signal: AbortSignal,
1189
+ start = 0,
1190
+ end = lane[1].length,
1191
+ ): Promise<ProjectedLane> {
1192
+ const matches = lane[1]
1193
+ for (let index = start; index < end; index++) {
1194
+ const match = matches[index]!
1195
+ const routeOptions = getRoute(router, match).options
1196
+ if (routeOptions.head || routeOptions.scripts) {
1197
+ try {
1198
+ const context = {
1199
+ ssr: router.options.ssr,
1200
+ matches,
1201
+ match,
1202
+ params: match.params,
1203
+ loaderData: match.loaderData,
1204
+ }
1205
+ const [head, scripts] = await waitFor(
1206
+ Promise.all([
1207
+ routeOptions.head?.(context),
1208
+ routeOptions.scripts?.(context),
1209
+ ]),
1210
+ signal,
1211
+ )
1212
+ match.meta = head?.meta
1213
+ match.links = head?.links
1214
+ match.headScripts = head?.scripts
1215
+ match.styles = head?.styles
1216
+ match.scripts = scripts
1217
+ } catch (cause) {
1218
+ if (cause === signal) {
1219
+ break
1220
+ }
1221
+ console.error(cause)
1222
+ }
1223
+ }
1224
+ if (match.status !== 'success' || match._notFound) {
1225
+ break
1226
+ }
1227
+ }
1228
+ return lane as ProjectedLane
1229
+ }
1230
+
1231
+ async function executeClientLane(
1232
+ router: AnyRouter,
1233
+ location: ParsedLocation,
1234
+ matches: Array<AnyRouteMatch>,
1235
+ options: ExecuteLaneOptions,
1236
+ ): Promise<LaneResult> {
1237
+ const matched = [location, matches as Array<WorkMatch>] as MatchedLane
1238
+ let plannedBoundary = matches.findIndex((match) => match._notFound)
1239
+ if (router.options.notFoundMode !== 'root' && plannedBoundary >= 0) {
1240
+ const boundary = await getNotFoundBoundary(
1241
+ router,
1242
+ matched[1],
1243
+ undefined,
1244
+ options[0].signal,
1245
+ plannedBoundary,
1246
+ )
1247
+ if (boundary !== plannedBoundary) {
1248
+ matches[plannedBoundary]!._notFound = undefined
1249
+ matches[boundary]!._notFound = true
1250
+ }
1251
+ plannedBoundary = boundary
1252
+ }
1253
+ 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
+ const tasks: Array<LoaderTask> = []
1262
+ const start = options[7] ?? 0
1263
+ let semanticParent = start
1264
+ ? Promise.resolve(matched[1][start - 1]!)
1265
+ : 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
1277
+ }
1278
+ for (let index = start; index < end; index++) {
1279
+ if (options[0].signal.aborted) {
1280
+ break
1281
+ }
1282
+ semanticParent = createLoaderTask(
1283
+ router,
1284
+ matched as ContextualizedLane,
1285
+ index,
1286
+ tasks,
1287
+ semanticParent,
1288
+ options,
1289
+ )
1290
+ }
1291
+ let reduced: ReducedLane | ControlOutcome
1292
+ try {
1293
+ const reduction = reduceLane(
1294
+ router,
1295
+ matched as ContextualizedLane,
1296
+ tasks,
1297
+ options[0],
1298
+ options[1],
1299
+ settleTasks(tasks, failure, matched[2]),
1300
+ options[8],
1301
+ )
1302
+ if (matched[2]?.length) {
1303
+ matched[3] = settleTasks(
1304
+ matched[2],
1305
+ undefined,
1306
+ undefined,
1307
+ reduction.then(
1308
+ (foreground) =>
1309
+ isControl(foreground)
1310
+ ? 0
1311
+ : _getRenderedMatches(foreground[1]).length,
1312
+ () => 0,
1313
+ ),
1314
+ )
1315
+ }
1316
+ reduced = await reduction
1317
+ } catch (cause) {
1318
+ discardBackground(router, matched)
1319
+ throw cause
1320
+ }
1321
+ if (isControl(reduced)) {
1322
+ return reduced
1323
+ }
1324
+ return projectLane(
1325
+ router,
1326
+ reduced,
1327
+ options[0].signal,
1328
+ options[7] === reduced[1].length ? options[7] : 0,
1329
+ )
1330
+ }
1331
+
1332
+ /**
1333
+ * Finds the first route that should show pending UI and its two timing values.
1334
+ * A fallback already on screen remains selected after its route loads, so we
1335
+ * do not jump to a child fallback. Matches put back into pending by invalidation
1336
+ * skip pendingMs, and a route without a usable fallback blocks pending UI for deeper routes.
1337
+ */
1338
+ function pendingConfig(
1339
+ router: AnyRouter,
1340
+ matches: Array<AnyRouteMatch>,
1341
+ ):
1342
+ | [delay: number, boundary: number, min: number, component: unknown]
1343
+ | undefined
1344
+ | void {
1345
+ const presented = router.stores.matches.get()
1346
+ for (let index = 0; index < matches.length; index++) {
1347
+ const match = matches[index]!
1348
+ const success = match.status === 'success'
1349
+ const visible =
1350
+ success &&
1351
+ presented[index]?.id === match.id &&
1352
+ presented[index]?.status === 'pending'
1353
+ if (success && !visible) {
1354
+ continue
1355
+ }
1356
+ const route = getRoute(router, match as WorkMatch)
1357
+ const delay =
1358
+ visible || match.invalid
1359
+ ? 0
1360
+ : (route.options.pendingMs ?? router.options.defaultPendingMs)
1361
+ const component =
1362
+ route.options.pendingComponent ??
1363
+ (router.options as any).defaultPendingComponent
1364
+ return component && typeof delay === 'number' && delay !== Infinity
1365
+ ? [
1366
+ delay,
1367
+ index,
1368
+ route.options.pendingMinMs ?? router.options.defaultPendingMinMs ?? 0,
1369
+ component,
1370
+ ]
1371
+ : undefined
1372
+ }
1373
+ }
1374
+
1375
+ /**
1376
+ * Waits for `pendingMs`, then presents the complete lane. Rendering applies the
1377
+ * selected boundary cutoff while retaining every match's structural state.
1378
+ * A replacement load for the same match keeps the timer; choosing a different
1379
+ * match resets it. `pendingMinMs` starts after the fallback renders.
1380
+ */
1381
+ function offerPending(router: CoordinatorRouter, tx: LoadTransaction): void {
1382
+ if (router._tx !== tx) {
1383
+ return
1384
+ }
1385
+ let session = router._pending
1386
+ 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
1391
+ tookOver = true
1392
+ } else {
1393
+ clearTimeout(session?.[3])
1394
+ router._pending = session = undefined
1395
+ }
1396
+ }
1397
+ const config = pendingConfig(router, tx[3])
1398
+ if (!config) {
1399
+ return
1400
+ }
1401
+ const [delay, boundary, min, component] = config
1402
+ const matchId = tx[3][boundary]!.id
1403
+ if (!session || session[1] !== boundary || sessionMatchId !== matchId) {
1404
+ // Hydration and redirects can preserve pending presentation without a session.
1405
+ // Do not delay it again; conservatively start pendingMinMs from now.
1406
+ clearTimeout(session?.[3])
1407
+ const presented = router.stores.matches.get()[boundary]
1408
+ const visible = presented?.id === matchId && presented.status === 'pending'
1409
+ router._pending = session = [
1410
+ tx,
1411
+ boundary,
1412
+ visible ? Date.now() + min : tx[4] + delay,
1413
+ undefined,
1414
+ visible ? Promise.resolve(true) : undefined,
1415
+ component,
1416
+ ]
1417
+ }
1418
+ if (session[4] && !tookOver && session[5] === component) {
1419
+ return
1420
+ }
1421
+ session[5] = component
1422
+ if (!session[4]) {
1423
+ clearTimeout(session[3])
1424
+ const remaining = session[2] - Date.now()
1425
+ if (remaining > 0) {
1426
+ session[3] = setTimeout(() => {
1427
+ offerPending(router, tx)
1428
+ }, remaining)
1429
+ return
1430
+ }
1431
+ session[2] = 0
1432
+ }
1433
+ const offered = tx[3].map((match) => ({
1434
+ ...match,
1435
+ _flight: undefined,
1436
+ }))
1437
+ offered[boundary]!.status = 'pending'
1438
+ const ack = router
1439
+ .startTransition(() => router.stores.setMatches(offered), offered, true)
1440
+ .then((rendered) => {
1441
+ if (
1442
+ rendered &&
1443
+ router._pending === session &&
1444
+ session[4] === ack &&
1445
+ !session[2]
1446
+ ) {
1447
+ session[2] = Date.now() + min
1448
+ }
1449
+ return rendered
1450
+ })
1451
+ session[4] = ack
1452
+ }
1453
+
1454
+ /**
1455
+ * Cancels pending UI timing when its load ends. The ownership check prevents
1456
+ * an older, superseded load from clearing pending UI that a newer load took over.
1457
+ */
1458
+ function finishPending(router: CoordinatorRouter, tx: LoadTransaction): void {
1459
+ const session = router._pending
1460
+ if (session?.[0] === tx) {
1461
+ clearTimeout(session[3])
1462
+ router._pending = undefined
1463
+ }
1464
+ }
1465
+
1466
+ function publishMatches(
1467
+ router: CoordinatorRouter,
1468
+ matches: Array<AnyRouteMatch>,
1469
+ ): void {
1470
+ router._committed = matches
1471
+ router.stores.setMatches(matches)
1472
+ }
1473
+
1474
+ function discardLane(router: AnyRouter, lane: ProjectedLane): void {
1475
+ transferMatchResources(router, lane[1])
1476
+ discardBackground(router, lane)
1477
+ }
1478
+
1479
+ function commitMatches(
1480
+ router: CoordinatorRouter,
1481
+ tx: LoadTransaction,
1482
+ matches: LaneMatches<'projected'>,
1483
+ resolvedPrefix?: number,
1484
+ ): void {
1485
+ const previous = router._committed
1486
+ const previousCached = router._cache
1487
+ for (const match of matches) {
1488
+ match.preload = false
1489
+ if (resolvedPrefix) {
1490
+ match._assetEnd = undefined
1491
+ }
1492
+ }
1493
+ const cut = _getRenderedMatches(matches).length
1494
+ const cached = new Map<string, AnyRouteMatch>()
1495
+ const now = Date.now()
1496
+ for (const match of [...previous, ...previousCached.values()]) {
1497
+ // Rendered-prefix ids and settled successes anywhere in the lane are
1498
+ // authoritative: retaining an older same-id generation would shadow them
1499
+ // at the next planning pass. Unsettled beyond-boundary matches are not —
1500
+ // they must not evict a newer same-id preload.
1501
+ if (
1502
+ match.status !== 'success' ||
1503
+ matches.some(
1504
+ (candidate, index) =>
1505
+ candidate.id === match.id &&
1506
+ (index < cut || candidate.status === 'success'),
1507
+ )
1508
+ ) {
1509
+ continue
1510
+ }
1511
+ const work = match as WorkMatch
1512
+ const route = getRoute(router, work)
1513
+ if (
1514
+ !route.options.loader ||
1515
+ now - match.updatedAt >=
1516
+ (match.preload
1517
+ ? (route.options.preloadGcTime ??
1518
+ router.options.defaultPreloadGcTime ??
1519
+ 300_000)
1520
+ : (route.options.gcTime ?? router.options.defaultGcTime ?? 300_000))
1521
+ ) {
1522
+ continue
1523
+ }
1524
+ cached.set(
1525
+ match.id,
1526
+ previousCached.get(match.id) === match
1527
+ ? match
1528
+ : ({
1529
+ ...match,
1530
+ _flight: undefined,
1531
+ isFetching: false,
1532
+ context: {},
1533
+ } as WorkMatch),
1534
+ )
1535
+ }
1536
+ // The lane becomes committed before publication can synchronously reenter.
1537
+ tx[3] = []
1538
+ router._cache = cached
1539
+ publishMatches(router, matches)
1540
+ transferMatchResources(
1541
+ router,
1542
+ [...previousCached.values(), ...previous],
1543
+ [...matches, ...cached.values()],
1544
+ )
1545
+ runRouteLifecycle(router, previous, matches, () => router._tx === tx)
1546
+ }
1547
+
1548
+ function commitRefreshMatches(
1549
+ router: CoordinatorRouter,
1550
+ tx: LoadTransaction,
1551
+ matches: LaneMatches<'projected'>,
1552
+ checkpoint: PublicationCheckpoint,
1553
+ ): void {
1554
+ const previous = router._committed
1555
+ const previousCached = router._cache
1556
+ for (const match of matches) {
1557
+ match.preload = false
1558
+ }
1559
+ const cached = new Map<string, AnyRouteMatch>()
1560
+ // Delay releasing the previous owners until the HMR render is acknowledged.
1561
+ // Old generations must not become reusable cache entries after refresh.
1562
+ tx[3] = []
1563
+ router._cache = cached
1564
+ checkpoint.previousMatches = previous
1565
+ checkpoint.previousCache = previousCached
1566
+ checkpoint.published = true
1567
+ publishMatches(router, matches)
1568
+ if (!checkpoint.published || router._tx !== tx) {
1569
+ return
1570
+ }
1571
+ runRouteLifecycle(router, previous, matches, () => router._tx === tx)
1572
+ }
1573
+
1574
+ function settlePublication(
1575
+ router: CoordinatorRouter,
1576
+ checkpoint: PublicationCheckpoint,
1577
+ ): void {
1578
+ if (!checkpoint.published) {
1579
+ return
1580
+ }
1581
+ checkpoint.published = false
1582
+ transferMatchResources(
1583
+ router,
1584
+ [...checkpoint.previousCache.values(), ...checkpoint.previousMatches],
1585
+ [...router._cache.values(), ...router._committed],
1586
+ )
1587
+ }
1588
+
1589
+ function rollbackPublication(
1590
+ router: CoordinatorRouter,
1591
+ tx: LoadTransaction,
1592
+ lane: ProjectedLane,
1593
+ checkpoint: PublicationCheckpoint,
1594
+ ): boolean {
1595
+ if (
1596
+ !checkpoint.published ||
1597
+ router._tx !== tx ||
1598
+ router._committed !== lane[1]
1599
+ ) {
1600
+ settlePublication(router, checkpoint)
1601
+ return false
1602
+ }
1603
+
1604
+ const discarded = [...router._cache.values(), ...router._committed]
1605
+ const restored = [
1606
+ ...checkpoint.previousCache.values(),
1607
+ ...checkpoint.previousMatches,
1608
+ ]
1609
+ router._cache = checkpoint.previousCache
1610
+ router._committed = checkpoint.previousMatches
1611
+ checkpoint.published = false
1612
+
1613
+ for (const match of discarded as Array<WorkMatch>) {
1614
+ if (
1615
+ !restored.includes(match) &&
1616
+ match._flight &&
1617
+ router._flights?.get(match.id) === match._flight
1618
+ ) {
1619
+ router._flights.delete(match.id)
1620
+ }
1621
+ }
1622
+
1623
+ finishPending(router, tx)
1624
+ router.batch(() => {
1625
+ router.stores.status.set('idle')
1626
+ router.stores.setMatches(checkpoint.previousPresentation)
1627
+ })
1628
+ tx[0].abort()
1629
+ transferMatchResources(router, discarded, restored)
1630
+ discardBackground(router, lane)
1631
+ if (router._tx === tx && router._commitPromise === checkpoint.commitPromise) {
1632
+ router._commitPromise?.resolve()
1633
+ router._commitPromise = undefined
1634
+ }
1635
+ return true
1636
+ }
1637
+
1638
+ async function transitionRefresh(
1639
+ router: CoordinatorRouter,
1640
+ tx: LoadTransaction,
1641
+ lane: ProjectedLane,
1642
+ changeInfo: ReturnType<typeof getLocationChangeInfo>,
1643
+ ): Promise<boolean | undefined> {
1644
+ const checkpoint: PublicationCheckpoint = {
1645
+ previousMatches: router._committed,
1646
+ previousPresentation: tx[6]?.[0] ?? router.stores.matches.get(),
1647
+ previousCache: router._cache,
1648
+ commitPromise: router._commitPromise,
1649
+ published: false,
1650
+ }
1651
+ const commit = () => {
1652
+ finishPending(router, tx)
1653
+ router._rollbackRefresh = rollback
1654
+ commitRefreshMatches(router, tx, lane[1], checkpoint)
1655
+ if (!checkpoint.published || router._tx !== tx) {
1656
+ return
1657
+ }
1658
+ router.emit({ type: 'onLoad', ...changeInfo })
1659
+ if (router._tx === tx) {
1660
+ router.emit({ type: 'onBeforeRouteMount', ...changeInfo })
1661
+ }
1662
+ }
1663
+ const rollback = () => {
1664
+ if (router._rollbackRefresh === rollback) {
1665
+ router._rollbackRefresh = undefined
1666
+ }
1667
+ const restored = rollbackPublication(router, tx, lane, checkpoint)
1668
+ router._cancelTransition?.()
1669
+ return restored
1670
+ }
1671
+ try {
1672
+ const rendered = await router.startTransition(commit, lane[1])
1673
+ if (router._rollbackRefresh === rollback) {
1674
+ router._rollbackRefresh = undefined
1675
+ }
1676
+ if (checkpoint.published) {
1677
+ const handoff = tx[6]?.[1]
1678
+ if (handoff && router._handoff === handoff) {
1679
+ handoff[1]()
1680
+ }
1681
+ if (router._tx === tx) {
1682
+ tx[6] = undefined
1683
+ }
1684
+ }
1685
+ settlePublication(router, checkpoint)
1686
+ return rendered
1687
+ } catch (cause) {
1688
+ if (rollback()) {
1689
+ return
1690
+ }
1691
+ throw cause
1692
+ }
1693
+ }
1694
+
1695
+ async function awaitCurrent(
1696
+ router: CoordinatorRouter,
1697
+ owner?: LoadTransaction,
1698
+ ): Promise<void> {
1699
+ let current = router._tx
1700
+ while (current && current !== owner) {
1701
+ await current[5]
1702
+ if (router._tx === current) {
1703
+ return
1704
+ }
1705
+ current = router._tx
1706
+ }
1707
+ }
1708
+
1709
+ async function followRedirect(
1710
+ router: CoordinatorRouter,
1711
+ tx: LoadTransaction,
1712
+ redirect: AnyRedirect,
1713
+ ): Promise<void> {
1714
+ await router.navigate({
1715
+ ...redirect.options,
1716
+ replace: true,
1717
+ ignoreBlocker: true,
1718
+ _redirects: tx[1] + 1,
1719
+ } as any)
1720
+ }
1721
+
1722
+ function restoreCommitted(
1723
+ router: CoordinatorRouter,
1724
+ tx: LoadTransaction,
1725
+ ): void {
1726
+ finishPending(router, tx)
1727
+ tx[0].abort()
1728
+ transferMatchResources(router, tx[3])
1729
+ tx[3] = []
1730
+ if (router._tx !== tx) {
1731
+ return
1732
+ }
1733
+ router.batch(() => {
1734
+ router.stores.status.set('idle')
1735
+ router.stores.setMatches(router._committed)
1736
+ })
1737
+ if (router._tx === tx) {
1738
+ router._commitPromise?.resolve()
1739
+ router._commitPromise = undefined
1740
+ }
1741
+ }
1742
+
1743
+ async function runBackground(
1744
+ router: CoordinatorRouter,
1745
+ tx: LoadTransaction,
1746
+ base: Array<AnyRouteMatch>,
1747
+ tasks: Array<BackgroundLoaderTask>,
1748
+ settlement: Promise<IndexedOutcome | undefined>,
1749
+ ): Promise<void> {
1750
+ const next = base.map((match) => ({ ...match }))
1751
+ acquireMatchResources(next)
1752
+ for (const task of tasks) {
1753
+ releaseFlight(router, next[task[0]]!)
1754
+ next[task[0]] = task[3]
1755
+ }
1756
+ // Phase jump: the clones inherit beforeLoad context from the committed
1757
+ // foreground lane, which already ran `contextualize` for these matches.
1758
+ const lane = [tx[2], next] as ContextualizedLane
1759
+ let reduced: ReducedLane | ControlOutcome
1760
+ try {
1761
+ reduced = await reduceLane(router, lane, tasks, tx[0], tx[1], settlement)
1762
+ } catch (cause) {
1763
+ transferMatchResources(router, next)
1764
+ throw cause
1765
+ }
1766
+ if (isControl(reduced)) {
1767
+ transferMatchResources(router, next)
1768
+ if (
1769
+ reduced[0] === REDIRECTED &&
1770
+ router._tx === tx &&
1771
+ router._committed === base
1772
+ ) {
1773
+ await followRedirect(router, tx, reduced[1])
1774
+ }
1775
+ return
1776
+ }
1777
+ const projected = await projectLane(router, reduced, tx[0].signal)
1778
+ if (router._tx !== tx || router._committed !== base) {
1779
+ transferMatchResources(router, projected[1])
1780
+ return
1781
+ }
1782
+ for (const match of projected[1] as Array<WorkMatch>) {
1783
+ const cached = router._cache.get(match.id) as WorkMatch | undefined
1784
+ if (cached?._flight && cached._flight === match._flight) {
1785
+ router._cache.delete(match.id)
1786
+ releaseFlight(router, cached)
1787
+ }
1788
+ }
1789
+ publishMatches(router, projected[1])
1790
+ transferMatchResources(router, base, projected[1])
1791
+ }
1792
+
1793
+ async function runClientTransaction(
1794
+ router: CoordinatorRouter,
1795
+ tx: LoadTransaction,
1796
+ forceStaleReload: boolean,
1797
+ onReady?: () => void,
1798
+ sync?: boolean,
1799
+ resolvedPrefix?: number,
1800
+ adopted?: ActivePreload,
1801
+ retained?: ActivePreload,
1802
+ ): Promise<void> {
1803
+ const options: ExecuteLaneOptions = [
1804
+ tx[0],
1805
+ tx[1],
1806
+ () => router._tx === tx && !!tx[3].length,
1807
+ router._committed,
1808
+ undefined,
1809
+ sync,
1810
+ forceStaleReload,
1811
+ resolvedPrefix,
1812
+ onReady,
1813
+ ]
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
+ }
1849
+
1850
+ if (isControl(result)) {
1851
+ if (result[0] === REDIRECTED && router._tx === tx) {
1852
+ finishPending(router, tx)
1853
+ transferMatchResources(router, tx[3])
1854
+ tx[3] = []
1855
+ if (router._tx === tx) {
1856
+ if (process.env.NODE_ENV !== 'production' && tx[6]) {
1857
+ router._refreshNextLoad = true
1858
+ }
1859
+ await followRedirect(router, tx, result[1])
1860
+ }
1861
+ } else {
1862
+ restoreCommitted(router, tx)
1863
+ }
1864
+ return
1865
+ }
1866
+ const pending = router._pending
1867
+ if (pending?.[0] === tx) {
1868
+ /**
1869
+ * Loading finished, so cancel any pending reveal. If the fallback rendered,
1870
+ * wait out the rest of `pendingMinMs` before replacing it. If it never
1871
+ * rendered, there is no minimum wait; if another load took it over, that
1872
+ * load owns the deadline.
1873
+ */
1874
+ clearTimeout(pending[3])
1875
+ if (pending[4]) {
1876
+ const signal = tx[0].signal
1877
+ let rendered = false
1878
+ try {
1879
+ rendered = await waitFor(pending[4], signal)
1880
+ } catch (cause) {
1881
+ if (cause !== signal) {
1882
+ throw cause
1883
+ }
1884
+ }
1885
+ if (rendered && router._pending === pending && pending[0] === tx) {
1886
+ const remaining = pending[2] - Date.now()
1887
+ if (remaining > 0) {
1888
+ try {
1889
+ await waitFor(
1890
+ new Promise<void>((resolve) => {
1891
+ pending[3] = setTimeout(resolve, remaining)
1892
+ }),
1893
+ signal,
1894
+ )
1895
+ } catch {}
1896
+ clearTimeout(pending[3])
1897
+ }
1898
+ }
1899
+ }
1900
+ }
1901
+ if (router._tx !== tx) {
1902
+ finishPending(router, tx)
1903
+ discardLane(router, result)
1904
+ return
1905
+ }
1906
+ const toLocation = tx[2]
1907
+ const changeInfo = getLocationChangeInfo(
1908
+ toLocation,
1909
+ router.stores.resolvedLocation.get(),
1910
+ )
1911
+ const background = result[2]
1912
+ await router.startViewTransition(async () => {
1913
+ if (router._tx !== tx) {
1914
+ discardLane(router, result)
1915
+ return
1916
+ }
1917
+ const commit = () => {
1918
+ finishPending(router, tx)
1919
+ commitMatches(router, tx, result[1], resolvedPrefix)
1920
+ if (router._tx !== tx) {
1921
+ return
1922
+ }
1923
+ router.emit({ type: 'onLoad', ...changeInfo })
1924
+ if (router._tx === tx) {
1925
+ router.emit({ type: 'onBeforeRouteMount', ...changeInfo })
1926
+ }
1927
+ }
1928
+ const rendered =
1929
+ process.env.NODE_ENV !== 'production' && tx[6]
1930
+ ? await transitionRefresh(router, tx, result, changeInfo)
1931
+ : await router.startTransition(commit, result[1])
1932
+ if (
1933
+ process.env.NODE_ENV !== 'production' &&
1934
+ tx[6] &&
1935
+ rendered === undefined
1936
+ ) {
1937
+ return
1938
+ }
1939
+ if (router._tx !== tx) {
1940
+ discardBackground(router, result)
1941
+ return
1942
+ }
1943
+ if (background?.length) {
1944
+ // Publish refreshes only after the foreground render acknowledgement.
1945
+ // Otherwise a fast refresh can replace the acknowledged generation
1946
+ // before the framework commits it and strand the navigation.
1947
+ runBackground(router, tx, result[1], background, result[3]!).catch(
1948
+ console.error,
1949
+ )
1950
+ }
1951
+ router.batch(() => {
1952
+ router.stores.resolvedLocation.set(toLocation)
1953
+ router.stores.status.set('idle')
1954
+ if (router._tx === tx) {
1955
+ router.emit({ type: 'onResolved', ...changeInfo })
1956
+ }
1957
+ if (rendered && router._tx === tx) {
1958
+ router.emit({ type: 'onRendered', ...changeInfo })
1959
+ }
1960
+ })
1961
+ if (router._tx !== tx) {
1962
+ return
1963
+ }
1964
+ router._commitPromise?.resolve()
1965
+ router._commitPromise = undefined
1966
+ })
1967
+ }
1968
+
1969
+ export async function loadClientRoute(
1970
+ router: CoordinatorRouter,
1971
+ opts?: { sync?: boolean; _dedupe?: boolean },
1972
+ ): Promise<void> {
1973
+ let rematerialize = false
1974
+ if (process.env.NODE_ENV !== 'production') {
1975
+ router._rollbackRefresh?.()
1976
+ rematerialize = !!router._refreshNextLoad || !!router._tx?.[6]
1977
+ }
1978
+ const refreshPresentation = rematerialize
1979
+ ? router.stores.matches.get()
1980
+ : undefined
1981
+ const previousOwner = router._tx
1982
+ const resolvedLocation = router.stores.resolvedLocation.get()
1983
+ const previousLocation = resolvedLocation ?? router.stores.location.get()
1984
+ const location = router.latestLocation
1985
+ const pendingLocation = router._pendingLocation as
1986
+ | (ParsedLocation & { _redirects?: number })
1987
+ | undefined
1988
+ const redirects =
1989
+ pendingLocation?.href === location.href
1990
+ ? (pendingLocation._redirects ?? 0)
1991
+ : 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
+ const handoff = router._handoff
2007
+ const hydrationController = rematerialize ? undefined : handoff?.[0]()
2008
+ const preflight = new AbortController()
2009
+ const previousPreflight = router._preflight
2010
+ router._preflight = preflight
2011
+ if (!rematerialize && !hydrationController) {
2012
+ handoff?.[1]()
2013
+ }
2014
+ previousPreflight?.abort()
2015
+ if (preflight.signal.aborted || router._tx !== previousOwner) {
2016
+ await awaitCurrent(router, previousOwner)
2017
+ return
2018
+ }
2019
+
2020
+ const changeInfo = getLocationChangeInfo(location, resolvedLocation)
2021
+ router.emit({ type: 'onBeforeNavigate', ...changeInfo })
2022
+ if (!preflight.signal.aborted && router._tx === previousOwner) {
2023
+ router.emit({ type: 'onBeforeLoad', ...changeInfo })
2024
+ }
2025
+ if (preflight.signal.aborted || router._tx !== previousOwner) {
2026
+ preflight.abort()
2027
+ await awaitCurrent(router, previousOwner)
2028
+ return
2029
+ }
2030
+ 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
+ let matches: Array<AnyRouteMatch>
2060
+ 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
2089
+ }
2090
+ await router.navigate({
2091
+ ...cause.options,
2092
+ replace: true,
2093
+ ignoreBlocker: true,
2094
+ })
2095
+ await awaitCurrent(router, previousOwner)
2096
+ return
2097
+ }
2098
+ resolvedPrefix = hydrationController ? handoff![1](matches) : undefined
2099
+ if (resolvedPrefix) {
2100
+ controller = hydrationController!
2101
+ } else {
2102
+ hydrationController?.abort()
2103
+ }
2104
+ }
2105
+ if (router._preflight !== preflight || router._tx !== previousOwner) {
2106
+ preflight.abort()
2107
+ transferMatchResources(router, matches)
2108
+ await awaitCurrent(router, previousOwner)
2109
+ return
2110
+ }
2111
+ router._preflight = undefined
2112
+
2113
+ const tx: LoadTransaction = [
2114
+ controller,
2115
+ redirects,
2116
+ location,
2117
+ matches,
2118
+ Date.now(),
2119
+ Promise.resolve()
2120
+ .then(() =>
2121
+ runClientTransaction(
2122
+ router,
2123
+ tx,
2124
+ sameHref,
2125
+ () => offerPending(router, tx),
2126
+ opts?.sync,
2127
+ resolvedPrefix,
2128
+ adopted,
2129
+ retained,
2130
+ ),
2131
+ )
2132
+ .catch(() => {
2133
+ if (router._tx === tx) {
2134
+ restoreCommitted(router, tx)
2135
+ }
2136
+ }),
2137
+ ]
2138
+ if (process.env.NODE_ENV !== 'production' && rematerialize) {
2139
+ // `refreshPresentation` is always captured when `rematerialize` is set.
2140
+ tx[6] = [refreshPresentation!, handoff]
2141
+ router._refreshNextLoad = undefined
2142
+ }
2143
+ router._tx = tx
2144
+ if (!rematerialize && router._handoff === handoff) {
2145
+ router._handoff = undefined
2146
+ }
2147
+ if (previousOwner) {
2148
+ for (const match of router.stores.matches.get() as Array<WorkMatch>) {
2149
+ if (router._tx !== tx) {
2150
+ break
2151
+ }
2152
+ if (match.isFetching) {
2153
+ setFetching(router, match, false)
2154
+ }
2155
+ }
2156
+ previousOwner[0].abort()
2157
+ transferMatchResources(router, previousOwner[3])
2158
+ }
2159
+ if (router._tx !== tx) {
2160
+ transferMatchResources(router, tx[3])
2161
+ tx[3] = []
2162
+ await awaitCurrent(router, tx)
2163
+ return
2164
+ }
2165
+
2166
+ router.batch(() => {
2167
+ router.stores.status.set('pending')
2168
+ router.stores.location.set(location)
2169
+ })
2170
+ offerPending(router, tx)
2171
+ try {
2172
+ await tx[5]
2173
+ } finally {
2174
+ await awaitCurrent(router, tx)
2175
+ }
2176
+ }
2177
+
2178
+ export async function refreshClientRoute(
2179
+ router: CoordinatorRouter,
2180
+ ): Promise<void> {
2181
+ router._rollbackRefresh?.()
2182
+ const pending = router._tx
2183
+ if (pending && !pending[6] && router.stores.status.get() === 'pending') {
2184
+ await pending[5]
2185
+ if (router._tx !== pending) {
2186
+ await awaitCurrent(router, pending)
2187
+ }
2188
+ }
2189
+ // Existing owners remain alive for rollback but cannot donate stale work.
2190
+ router._flights?.clear()
2191
+ router.clearCache()
2192
+ router._refreshNextLoad = true
2193
+ await loadClientRoute(router, { sync: true })
2194
+ }
2195
+
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
+ export async function preloadClientRoute(
2221
+ router: CoordinatorRouter,
2222
+ opts: any,
2223
+ redirects = 0,
2224
+ ): Promise<Array<AnyRouteMatch> | undefined> {
2225
+ if (redirects > 20) {
2226
+ return
2227
+ }
2228
+ const owner = router._tx
2229
+ if (
2230
+ process.env.NODE_ENV !== 'production' &&
2231
+ (router._refreshNextLoad || owner?.[6])
2232
+ ) {
2233
+ return
2234
+ }
2235
+ const location = opts._builtLocation ?? router.buildLocation(opts)
2236
+ const base = router._committed
2237
+ const controller = new AbortController()
2238
+ let matches: Array<AnyRouteMatch> | undefined
2239
+ let preload: ActivePreload | undefined
2240
+ let replaced: ActivePreload | undefined
2241
+ 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
+ matches = router.matchRoutes(location, {
2256
+ _controller: controller,
2257
+ })
2258
+ 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]
2289
+ }
2290
+ router._preloads.delete(location.href)
2291
+ if (isControl(result)) {
2292
+ controller.abort()
2293
+ 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
+ controller.abort()
2306
+ if (matches) {
2307
+ transferMatchResources(router, matches)
2308
+ }
2309
+ }
2310
+ if (router._tx !== owner) {
2311
+ return
2312
+ }
2313
+ if (!isNotFound(cause)) {
2314
+ console.error(cause)
2315
+ }
2316
+ return
2317
+ }
2318
+ }
2319
+
2320
+ // --- SSR hydration (client entry via @tanstack/router-core/ssr/client) ---
2321
+
2322
+ declare global {
2323
+ interface Window {
2324
+ [GLOBAL_TSR]?: TsrSsrGlobal
2325
+ [GLOBAL_SEROVAL]?: any
2326
+ }
2327
+ }
2328
+
2329
+ export async function hydrate(router: AnyRouter): Promise<void> {
2330
+ if (process.env.NODE_ENV !== 'production' && !window.$_TSR) {
2331
+ throw new Error(
2332
+ 'Invariant failed: Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!',
2333
+ )
2334
+ }
2335
+ const tsr = window.$_TSR!
2336
+
2337
+ const adapters = router.options.serializationAdapters as
2338
+ | Array<AnySerializationAdapter>
2339
+ | undefined
2340
+ if (adapters?.length) {
2341
+ tsr.t = new Map(
2342
+ adapters.map((adapter) => [adapter.key, adapter.fromSerializable]),
2343
+ )
2344
+ tsr.buffer.forEach((script) => script())
2345
+ }
2346
+ tsr.initialized = true
2347
+
2348
+ const dehydratedRouter = tsr.router
2349
+ if (process.env.NODE_ENV !== 'production' && !dehydratedRouter) {
2350
+ throw new Error(
2351
+ 'Invariant failed: Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',
2352
+ )
2353
+ }
2354
+ 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 }
2361
+
2362
+ const dehydratedMatches = dehydratedRouter!.matches
2363
+
2364
+ const controller = new AbortController()
2365
+ const previousPreflight = router._preflight
2366
+ router._preflight = controller
2367
+ 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()
2380
+
2381
+ let location!: AnyRouter['latestLocation']
2382
+ let candidates!: Array<AnyRouteMatch>
2383
+ let handoffInputs!: ReturnType<typeof laneInputs>
2384
+ try {
2385
+ await waitFor(
2386
+ router.options.hydrate?.(dehydratedRouter!.dehydratedData),
2387
+ controller.signal,
2388
+ )
2389
+ if (!isCurrent()) {
2390
+ return
2391
+ }
2392
+ router.updateLatestLocation()
2393
+ location = router.latestLocation
2394
+ router.stores.location.set(location)
2395
+ handoffInputs = laneInputs(router, location)
2396
+ candidates = router.matchRoutes(location, {
2397
+ _controller: controller,
2398
+ })
2399
+ } catch (cause) {
2400
+ retire(cause)
2401
+ if (cause !== controller.signal) {
2402
+ throw cause
2403
+ }
2404
+ }
2405
+ if (!isCurrent()) {
2406
+ return
2407
+ }
2408
+ const committed: Array<AnyRouteMatch> = []
2409
+ let pendingBoundary: number | undefined
2410
+ let verifiedAssetEnd = 0
2411
+ const retryFrom = (index: number) => {
2412
+ // The failing route's identity is still verified, but no descendant is.
2413
+ verifiedAssetEnd = Math.min(verifiedAssetEnd, index + 1)
2414
+ const removed = committed.splice(index)
2415
+ for (const match of removed) {
2416
+ if (
2417
+ getRoute(router, match).options.loader &&
2418
+ (match.status === 'success' ||
2419
+ (!match.invalid && 'loaderData' in match))
2420
+ ) {
2421
+ cacheLoaderMatch(
2422
+ router,
2423
+ // Phase jump: dehydrated server data is already past the loader
2424
+ // phase — the guard above verified a settled success (or transported
2425
+ // loaderData), so this clone is settled without a client settleInto.
2426
+ {
2427
+ ...match,
2428
+ status: 'success',
2429
+ error: undefined,
2430
+ preload: true,
2431
+ } as SettledMatch,
2432
+ router._cache.get(match.id),
2433
+ )
2434
+ }
2435
+ }
2436
+ transferMatchResources(router, removed)
2437
+ }
2438
+
2439
+ // A longer server lane is valid only when the local match already caps the
2440
+ // branch at a global not-found boundary. Otherwise no transported work is
2441
+ // safe to attach to the shorter client lane.
2442
+ const shared =
2443
+ dehydratedMatches.length > candidates.length
2444
+ ? candidates.findIndex((match) => match._notFound) + 1
2445
+ : dehydratedMatches.length
2446
+ let isTerminal = false
2447
+ for (let index = 0; index < shared; index++) {
2448
+ const candidate = candidates[index]!
2449
+ const dehydrated = dehydratedMatches[index]!
2450
+ if (
2451
+ typeof dehydrated.i !== 'string' ||
2452
+ hydrateSsrMatchId(dehydrated.i) !== candidate.id
2453
+ ) {
2454
+ pendingBoundary ??= index
2455
+ break
2456
+ }
2457
+ verifiedAssetEnd = index + 1
2458
+ const route = getRoute(router, candidate)
2459
+ if (
2460
+ 'l' in dehydrated ||
2461
+ (dehydrated.s === 'success' &&
2462
+ dehydrated.e === undefined &&
2463
+ route.options.loader)
2464
+ ) {
2465
+ candidate.loaderData = dehydrated.l
2466
+ }
2467
+ candidate.status = dehydrated.s
2468
+ candidate.ssr = dehydrated.ssr
2469
+ route.options.ssr = candidate.ssr
2470
+ candidate.updatedAt = dehydrated.u
2471
+ candidate.error = dehydrated.e
2472
+ candidate._notFound ||= dehydrated.g
2473
+ const terminal =
2474
+ candidate.status === 'error' ||
2475
+ candidate.status === 'notFound' ||
2476
+ candidate._notFound
2477
+ if (terminal) {
2478
+ isTerminal = true
2479
+ committed.push(candidate)
2480
+ if (candidate.ssr === false || candidate.ssr === 'data-only') {
2481
+ pendingBoundary ??= index
2482
+ }
2483
+ break
2484
+ }
2485
+ if (candidate.status === 'pending') {
2486
+ pendingBoundary ??= index
2487
+ break
2488
+ }
2489
+
2490
+ committed.push(candidate)
2491
+ if (candidate.ssr === 'data-only') {
2492
+ pendingBoundary ??= index
2493
+ }
2494
+ }
2495
+ let verifiedContextEnd = verifiedAssetEnd
2496
+
2497
+ if (
2498
+ !isTerminal &&
2499
+ committed.length === shared &&
2500
+ shared < candidates.length
2501
+ ) {
2502
+ pendingBoundary = shared
2503
+ }
2504
+
2505
+ // Hooks observe structural membership. Execution remains limited to
2506
+ // `committed`, the accepted server prefix.
2507
+ const chunks = committed.map(async (match) => {
2508
+ try {
2509
+ const route = getRoute(router, match)
2510
+ if (match._notFound) {
2511
+ await Promise.all([
2512
+ loadRouteChunk(route),
2513
+ loadRouteChunk(route, 'notFoundComponent'),
2514
+ ])
2515
+ } else {
2516
+ await loadRouteChunk(
2517
+ route,
2518
+ match.status === 'error'
2519
+ ? 'errorComponent'
2520
+ : match.status === 'notFound'
2521
+ ? 'notFoundComponent'
2522
+ : undefined,
2523
+ )
2524
+ }
2525
+ return true
2526
+ } catch {
2527
+ return false
2528
+ }
2529
+ })
2530
+ let chunkFailure = 0
2531
+ try {
2532
+ while (
2533
+ chunkFailure < chunks.length &&
2534
+ (await waitFor(chunks[chunkFailure]!, controller.signal))
2535
+ ) {
2536
+ chunkFailure++
2537
+ }
2538
+ } catch {
2539
+ isCurrent()
2540
+ return
2541
+ }
2542
+ if (!isCurrent()) {
2543
+ return
2544
+ }
2545
+ if (chunkFailure < committed.length) {
2546
+ verifiedContextEnd = Math.min(verifiedContextEnd, chunkFailure)
2547
+ retryFrom(chunkFailure)
2548
+ }
2549
+
2550
+ // The first pending match is already visible, so prepare its route context
2551
+ // without granting its beforeLoad or loader any hydration authority.
2552
+ const contextEnd = Math.max(
2553
+ pendingBoundary === committed.length
2554
+ ? committed.length + 1
2555
+ : committed.length,
2556
+ verifiedContextEnd,
2557
+ )
2558
+ for (let index = 0; index < contextEnd; index++) {
2559
+ const match = candidates[index]!
2560
+ const route = getRoute(router, match)
2561
+ const parentContext =
2562
+ candidates[index - 1]?.context ?? router.options.context ?? {}
2563
+ let routeContext
2564
+ if (route.options.context) {
2565
+ try {
2566
+ routeContext = match._ctx =
2567
+ route.options.context({
2568
+ deps: match.loaderDeps,
2569
+ params: match.params,
2570
+ context: parentContext,
2571
+ location,
2572
+ navigate: navigateFrom(router, location),
2573
+ buildLocation: router.buildLocation,
2574
+ cause: match.cause,
2575
+ abortController: controller,
2576
+ preload: false,
2577
+ matches: candidates,
2578
+ routeId: route.id,
2579
+ }) || {}
2580
+ } catch {
2581
+ if (!isCurrent()) {
2582
+ return
2583
+ }
2584
+ if (
2585
+ match.status !== 'error' &&
2586
+ match.status !== 'notFound' &&
2587
+ !match._notFound
2588
+ ) {
2589
+ retryFrom(index)
2590
+ break
2591
+ }
2592
+ }
2593
+ if (!isCurrent()) {
2594
+ return
2595
+ }
2596
+ }
2597
+ match.context = {
2598
+ ...parentContext,
2599
+ ...routeContext,
2600
+ ...(committed[index] && dehydratedMatches[index]!.b),
2601
+ }
2602
+ }
2603
+
2604
+ await projectLane(
2605
+ router,
2606
+ [location, candidates] as any,
2607
+ controller.signal,
2608
+ 0,
2609
+ verifiedAssetEnd,
2610
+ )
2611
+ if (!isCurrent()) {
2612
+ return
2613
+ }
2614
+ const needsClientLoad =
2615
+ pendingBoundary !== undefined || committed.length < shared
2616
+ const committedMatches =
2617
+ isTerminal && committed.length === shared ? candidates : committed
2618
+ let presented = needsClientLoad ? candidates : committedMatches
2619
+ let dataOnlyAssetEnd: number | undefined
2620
+ if (needsClientLoad && pendingBoundary !== undefined) {
2621
+ const boundary = presented[pendingBoundary]!
2622
+ dataOnlyAssetEnd =
2623
+ boundary.status === 'success' &&
2624
+ boundary.ssr === 'data-only' &&
2625
+ boundary.error === undefined &&
2626
+ !boundary._notFound &&
2627
+ verifiedAssetEnd > pendingBoundary + 1
2628
+ ? verifiedAssetEnd
2629
+ : undefined
2630
+ presented = presented.slice()
2631
+ presented[pendingBoundary] = {
2632
+ ...boundary,
2633
+ status: 'pending',
2634
+ ssr: boundary.ssr === 'data-only' ? 'data-only' : false,
2635
+ _assetEnd: dataOnlyAssetEnd,
2636
+ }
2637
+ }
2638
+
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
2647
+ ? controller
2648
+ : undefined
2649
+ const handoff: NonNullable<AnyRouter['_handoff']> = [
2650
+ claim,
2651
+ (matches) => {
2652
+ if (router._handoff !== handoff) {
2653
+ return
2654
+ }
2655
+ const prefix = committedMatches.length
2656
+ if (
2657
+ !matches ||
2658
+ !claim() ||
2659
+ committedMatches.some((match, index) => match.id !== matches[index]?.id)
2660
+ ) {
2661
+ router._handoff = undefined
2662
+ controller.abort()
2663
+ return
2664
+ }
2665
+ let handoffAssetEnd = dataOnlyAssetEnd
2666
+ if (handoffAssetEnd !== undefined) {
2667
+ for (let index = prefix; index < handoffAssetEnd; index++) {
2668
+ if (candidates[index]?.id !== matches[index]?.id) {
2669
+ handoffAssetEnd =
2670
+ index > (pendingBoundary ?? -1) + 1 ? index : undefined
2671
+ break
2672
+ }
2673
+ }
2674
+ }
2675
+ const clones = committedMatches.map((match) => ({ ...match }))
2676
+ if (handoffAssetEnd !== undefined) {
2677
+ clones[pendingBoundary!]!._assetEnd = handoffAssetEnd
2678
+ }
2679
+ transferMatchResources(router, matches.splice(0, prefix, ...clones))
2680
+ for (let index = prefix; index < matches.length; index++) {
2681
+ const match = matches[index]!
2682
+ const hydrated = candidates[index]
2683
+ if (hydrated?.id === match.id && hydrated._ctx) {
2684
+ match._ctx = hydrated._ctx
2685
+ }
2686
+ match.abortController = controller
2687
+ }
2688
+ return prefix
2689
+ },
2690
+ ]
2691
+ router._committed = committedMatches
2692
+ router._handoff = handoff
2693
+ router._preflight = undefined
2694
+ router.batch(() => {
2695
+ router.stores.setMatches(presented)
2696
+ router.stores.status.set('idle')
2697
+ if (!needsClientLoad) {
2698
+ router.stores.resolvedLocation.set(router.stores.location.get())
2699
+ }
2700
+ })
2701
+ }