@tanstack/router-core 1.171.15 → 1.171.16

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