@tanstack/router-core 1.171.14 → 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.
- package/dist/cjs/Matches.cjs.map +1 -1
- package/dist/cjs/Matches.d.cts +3 -12
- package/dist/cjs/await-signal.cjs +21 -0
- package/dist/cjs/await-signal.cjs.map +1 -0
- package/dist/cjs/await-signal.d.cts +1 -0
- package/dist/cjs/index.cjs +3 -1
- package/dist/cjs/index.d.cts +3 -2
- package/dist/cjs/isServer/client.cjs +2 -0
- package/dist/cjs/isServer/client.cjs.map +1 -1
- package/dist/cjs/isServer/client.d.cts +1 -0
- package/dist/cjs/isServer/development.cjs +2 -0
- package/dist/cjs/isServer/development.cjs.map +1 -1
- package/dist/cjs/isServer/development.d.cts +1 -0
- package/dist/cjs/isServer/server.cjs +2 -0
- package/dist/cjs/isServer/server.cjs.map +1 -1
- package/dist/cjs/isServer/server.d.cts +1 -0
- package/dist/cjs/load-client.cjs +1470 -0
- package/dist/cjs/load-client.cjs.map +1 -0
- package/dist/cjs/load-client.d.cts +147 -0
- package/dist/cjs/load-server.cjs +565 -0
- package/dist/cjs/load-server.cjs.map +1 -0
- package/dist/cjs/load-server.d.cts +16 -0
- package/dist/cjs/redirect.cjs.map +1 -1
- package/dist/cjs/redirect.d.cts +0 -1
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/router.cjs +187 -445
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +62 -52
- package/dist/cjs/scroll-restoration.cjs +27 -34
- package/dist/cjs/scroll-restoration.cjs.map +1 -1
- package/dist/cjs/ssr/client.cjs +2 -2
- package/dist/cjs/ssr/createRequestHandler.cjs +60 -9
- package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
- package/dist/cjs/ssr/createRequestHandler.d.cts +1 -0
- package/dist/cjs/ssr/handlerCallback.cjs +49 -4
- package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
- package/dist/cjs/ssr/handlerCallback.d.cts +3 -0
- package/dist/cjs/ssr/server.cjs +4 -0
- package/dist/cjs/ssr/server.d.cts +2 -2
- package/dist/cjs/ssr/ssr-client.d.cts +1 -10
- package/dist/cjs/ssr/ssr-match-id.cjs +2 -2
- package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +6 -6
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +27 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.d.cts +2 -0
- package/dist/cjs/ssr/types.d.cts +0 -1
- package/dist/cjs/stores.cjs +30 -104
- package/dist/cjs/stores.cjs.map +1 -1
- package/dist/cjs/stores.d.cts +7 -32
- package/dist/cjs/utils.cjs +0 -4
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/Matches.d.ts +3 -12
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/await-signal.d.ts +1 -0
- package/dist/esm/await-signal.js +21 -0
- package/dist/esm/await-signal.js.map +1 -0
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +3 -2
- package/dist/esm/isServer/client.d.ts +1 -0
- package/dist/esm/isServer/client.js +2 -1
- package/dist/esm/isServer/client.js.map +1 -1
- package/dist/esm/isServer/development.d.ts +1 -0
- package/dist/esm/isServer/development.js +2 -1
- package/dist/esm/isServer/development.js.map +1 -1
- package/dist/esm/isServer/server.d.ts +1 -0
- package/dist/esm/isServer/server.js +2 -1
- package/dist/esm/isServer/server.js.map +1 -1
- package/dist/esm/load-client.d.ts +147 -0
- package/dist/esm/load-client.js +1462 -0
- package/dist/esm/load-client.js.map +1 -0
- package/dist/esm/load-server.d.ts +16 -0
- package/dist/esm/load-server.js +565 -0
- package/dist/esm/load-server.js.map +1 -0
- package/dist/esm/redirect.d.ts +0 -1
- package/dist/esm/redirect.js.map +1 -1
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +62 -52
- package/dist/esm/router.js +188 -446
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/scroll-restoration.js +27 -34
- package/dist/esm/scroll-restoration.js.map +1 -1
- package/dist/esm/ssr/client.js +1 -1
- package/dist/esm/ssr/createRequestHandler.d.ts +1 -0
- package/dist/esm/ssr/createRequestHandler.js +61 -11
- package/dist/esm/ssr/createRequestHandler.js.map +1 -1
- package/dist/esm/ssr/handlerCallback.d.ts +3 -0
- package/dist/esm/ssr/handlerCallback.js +47 -5
- package/dist/esm/ssr/handlerCallback.js.map +1 -1
- package/dist/esm/ssr/server.d.ts +2 -2
- package/dist/esm/ssr/server.js +3 -3
- package/dist/esm/ssr/ssr-client.d.ts +1 -10
- package/dist/esm/ssr/ssr-match-id.js +2 -2
- package/dist/esm/ssr/ssr-match-id.js.map +1 -1
- package/dist/esm/ssr/ssr-server.js +6 -6
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.d.ts +2 -0
- package/dist/esm/ssr/transformStreamWithRouter.js +27 -1
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/dist/esm/ssr/types.d.ts +0 -1
- package/dist/esm/stores.d.ts +7 -32
- package/dist/esm/stores.js +30 -104
- package/dist/esm/stores.js.map +1 -1
- package/dist/esm/utils.js +1 -4
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/Matches.ts +6 -21
- package/src/await-signal.ts +27 -0
- package/src/index.ts +1 -3
- package/src/isServer/client.ts +1 -0
- package/src/isServer/development.ts +1 -0
- package/src/isServer/server.ts +1 -0
- package/src/load-client.ts +2701 -0
- package/src/load-server.ts +849 -0
- package/src/redirect.ts +0 -1
- package/src/route.ts +2 -11
- package/src/router.ts +380 -774
- package/src/scroll-restoration.ts +40 -56
- package/src/ssr/createRequestHandler.ts +109 -16
- package/src/ssr/handlerCallback.ts +76 -10
- package/src/ssr/server.ts +4 -1
- package/src/ssr/ssr-client.ts +1 -310
- package/src/ssr/ssr-match-id.ts +11 -2
- package/src/ssr/ssr-server.ts +9 -8
- package/src/ssr/transformStreamWithRouter.ts +37 -1
- package/src/ssr/types.ts +0 -1
- package/src/stores.ts +46 -208
- package/dist/cjs/load-matches.cjs +0 -658
- package/dist/cjs/load-matches.cjs.map +0 -1
- package/dist/cjs/load-matches.d.cts +0 -18
- package/dist/cjs/ssr/ssr-client.cjs +0 -183
- package/dist/cjs/ssr/ssr-client.cjs.map +0 -1
- package/dist/esm/load-matches.d.ts +0 -18
- package/dist/esm/load-matches.js +0 -656
- package/dist/esm/load-matches.js.map +0 -1
- package/dist/esm/ssr/ssr-client.js +0 -183
- package/dist/esm/ssr/ssr-client.js.map +0 -1
- 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 : 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
|
+
}
|