UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

1 lines 114 kB
{"version":3,"file":"load-client.cjs","names":[],"sources":["../../src/load-client.ts"],"sourcesContent":["// Keep this filename free of a secondary extension so declaration generation\n// can rewrite relative imports for both ESM and CJS.\nimport { isNotFound } from './not-found'\nimport { isRedirect } from './redirect'\nimport {\n _getUserHistoryState,\n getLocationChangeInfo,\n runRouteLifecycle,\n} from './router'\nimport { deepEqual } from './utils'\nimport { hydrateSsrMatchId } from './ssr/ssr-match-id'\nimport type { GLOBAL_SEROVAL, GLOBAL_TSR } from './ssr/constants'\nimport type { AnySerializationAdapter } from './ssr/serializer/transformer'\nimport type { TsrSsrGlobal } from './ssr/types'\nimport type { ParsedLocation } from './location'\nimport type { AnyRouteMatch } from './Matches'\nimport type { NotFoundError } from './not-found'\nimport type {\n AnyRoute,\n BeforeLoadContextOptions,\n LoaderFnContext,\n RouteContextOptions,\n RouteLoaderFn,\n} from './route'\nimport type { AnyRedirect } from './redirect'\nimport type { AnyRouter } from './router'\n\ntype RouteComponentType =\n | 'component'\n | 'pendingComponent'\n | 'errorComponent'\n | 'notFoundComponent'\n\nexport function replaceRouteChunk(\n route: AnyRoute,\n lazyFn: AnyRoute['lazyFn'],\n): void {\n route.lazyFn = lazyFn ?? route.lazyFn\n route._lazy = undefined\n}\n\nfunction preloadComponent(\n route: AnyRoute,\n type: RouteComponentType,\n): Promise<void> | undefined {\n return (route.options[type] as any)?.preload?.()\n}\n\nfunction loadComponents(route: AnyRoute): Promise<void> | undefined {\n const component = preloadComponent(route, 'component')\n const pending = preloadComponent(route, 'pendingComponent')\n if (component && pending) {\n return Promise.all([component, pending]).then(() => {})\n }\n return component ?? pending\n}\n\nexport function loadRouteChunk(\n route: AnyRoute,\n // `false` waits only for lazy route options, before a boundary is selected.\n componentType?: 'errorComponent' | 'notFoundComponent' | false,\n): Promise<void> | undefined {\n const afterLazy = () =>\n componentType === false\n ? undefined\n : componentType\n ? preloadComponent(route, componentType)\n : loadComponents(route)\n const current = route._lazy\n if (current) {\n return current === true ? afterLazy() : current.then(afterLazy)\n }\n if (!route.lazyFn) {\n return afterLazy()\n }\n\n const promise = route.lazyFn().then(\n (lazyRoute) => {\n // HMR clears the owner before an obsolete import can settle.\n if (process.env.NODE_ENV === 'production' || route._lazy === promise) {\n const { id: _id, ...options } = lazyRoute.options\n Object.assign(route.options, options)\n route._lazy = true\n }\n },\n (error) => {\n if (process.env.NODE_ENV === 'production' || route._lazy === promise) {\n route._lazy = undefined\n }\n throw error\n },\n )\n route._lazy = promise\n return promise.then(afterLazy)\n}\n\n/** Return the structural lane through the first terminal render boundary. */\nexport function _getRenderedMatches(\n matches: Array<AnyRouteMatch>,\n): Array<AnyRouteMatch> {\n const end =\n matches.findIndex(\n (match) => match.status !== 'success' || match._notFound,\n ) + 1\n return end && end < matches.length ? matches.slice(0, end) : matches\n}\n\n/** Return the lane whose document assets belong to the current presentation. */\nexport function _getAssetMatches(\n matches: Array<AnyRouteMatch>,\n): Array<AnyRouteMatch> {\n let end = matches.length\n for (let index = 0; index < end; index++) {\n const match = matches[index]!\n // `_assetEnd` is only ever set on hydration presentation clones that are\n // `status: 'pending'`, `ssr: 'data-only'`, error-free, and not not-found\n // (see hydrate.ts), and commits clear it — so its presence alone is the guard.\n if (match._assetEnd !== undefined) {\n end = Math.min(end, Math.max(index + 1, match._assetEnd))\n continue\n }\n if (match.status !== 'success' || match._notFound) {\n end = index + 1\n break\n }\n }\n // `end` only ever shrinks to `index + 1 >= 1`, so no zero guard is needed.\n return end < matches.length ? matches.slice(0, end) : matches\n}\n\ndeclare const lanePhase: unique symbol\n\ntype LanePhase = 'matched' | 'contextualized' | 'reduced' | 'projected'\n\n/**\n * Lane matches carry their lane's phase so functions can demand evidence of\n * pipeline position (e.g. `commitMatches` only accepts a projected lane's\n * matches). The brand is phantom — it never exists at runtime.\n */\ntype LaneMatches<TPhase extends LanePhase> = Array<WorkMatch> & {\n readonly [lanePhase]?: TPhase\n}\n\ntype Lane<TPhase extends LanePhase> = [\n location: ParsedLocation,\n matches: LaneMatches<TPhase>,\n background?: Array<BackgroundLoaderTask>,\n backgroundSettlement?: Promise<IndexedOutcome | undefined>,\n] & { readonly [lanePhase]?: TPhase }\n\ntype MatchedLane = Lane<'matched'>\ntype ContextualizedLane = Lane<'contextualized'>\ntype ReducedLane = Lane<'reduced'>\ntype ProjectedLane = Lane<'projected'>\n\nconst SUCCESS = 0\nconst ERROR = 1\nconst NOT_FOUND = 2\n// Control outcomes stay contiguous so the hot path can test them together.\nconst REDIRECTED = 3\nconst CANCELED = 4\n\ntype LoaderOutcome =\n | [typeof SUCCESS, data: unknown]\n | [typeof ERROR, error: unknown]\n | [typeof NOT_FOUND, error: NotFoundError]\n | [typeof REDIRECTED, redirect: AnyRedirect]\n | [typeof CANCELED]\n\ntype IndexedOutcome = [index: number, outcome: LoaderOutcome, boundary?: number]\n\nexport type LoaderFlight = [\n outcome: Promise<LoaderOutcome>,\n controller: AbortController,\n leases: number,\n]\n\ntype WorkMatch = AnyRouteMatch & {\n _flight?: LoaderFlight\n}\n\ndeclare const matchPhase: unique symbol\n\n/**\n * A match whose loader outcome has been applied by `settleInto`, which is the\n * sole granter of this brand (phantom, zero-runtime). Consumers that require\n * it — e.g. `cacheLoaderMatch` — can only be reached after settlement, so the\n * compiler enforces the loader→settle→cache ordering. Sources that arrive\n * already settled (dehydrated server data) must cast at a named boundary.\n */\ntype SettledMatch = WorkMatch & { readonly [matchPhase]: 'settled' }\n\nexport type LaneInputs = [\n routeTree: AnyRoute,\n context: unknown,\n additionalContext: unknown,\n state: object,\n search: object,\n maskedLocation:\n | [\n href: string,\n state: object,\n search: object,\n unmaskOnReload: boolean | undefined,\n ]\n | undefined,\n]\n\nexport type ActivePreload = [\n matches: Array<AnyRouteMatch>,\n controller: AbortController,\n result: Promise<LaneResult>,\n semanticOwner: Array<AnyRouteMatch>,\n inputs: LaneInputs,\n redirects: number,\n]\n\nexport type LoadTransaction = [\n controller: AbortController,\n redirects: number,\n location: ParsedLocation,\n matches: Array<AnyRouteMatch>,\n startedAt: number,\n done: Promise<void>,\n /**\n * Dev-only HMR refresh mode. Presence is the mode flag; a refresh always\n * carries the presentation it started from, while the hydration handoff is\n * genuinely optional — the tuple makes a half-armed refresh unrepresentable.\n */\n refresh?: [\n presentation: Array<AnyRouteMatch>,\n handoff: NonNullable<AnyRouter['_handoff']> | undefined,\n ],\n]\n\nexport type PendingSession = [\n owner: LoadTransaction,\n boundary: number,\n /** Pending reveal time until acknowledged, then minimum-visible-until time. */\n deadline: number,\n timer?: ReturnType<typeof setTimeout>,\n ack?: Promise<boolean>,\n component?: unknown,\n]\n\ntype CoordinatorRouter = AnyRouter & {\n /** Whole speculative lanes that a matching navigation may adopt. */\n _preloads?: Map<string, ActivePreload>\n _refreshNextLoad?: boolean\n _rollbackRefresh?: () => void\n _cancelTransition?: () => void\n}\n\ntype PublicationCheckpoint = {\n previousMatches: Array<AnyRouteMatch>\n previousPresentation: Array<AnyRouteMatch>\n previousCache: Map<string, AnyRouteMatch>\n commitPromise: CoordinatorRouter['_commitPromise']\n published: boolean\n}\n\ntype LoaderTask = [\n index: number,\n outcome: Promise<LoaderOutcome>,\n ready: Promise<IndexedOutcome | undefined>,\n candidate?: WorkMatch,\n]\n\ntype BackgroundLoaderTask = [\n index: number,\n outcome: Promise<LoaderOutcome>,\n ready: Promise<IndexedOutcome | undefined>,\n candidate: WorkMatch,\n]\n\ntype ExecuteLaneOptions = [\n controller: AbortController,\n redirects: number,\n isCurrent: () => boolean,\n base: Array<AnyRouteMatch>,\n preload?: boolean,\n sync?: boolean,\n forceStaleReload?: boolean,\n resolvedPrefix?: number,\n onReady?: () => void,\n]\n\ntype ControlOutcome =\n | [typeof REDIRECTED, redirect: AnyRedirect]\n | [typeof CANCELED]\n\ntype LaneResult = ProjectedLane | ControlOutcome\n\nfunction isControl(\n result: Lane<any> | ControlOutcome,\n): result is ControlOutcome {\n return typeof result[0] === 'number'\n}\n\nexport function waitFor<T>(\n value: T | PromiseLike<T>,\n signal: AbortSignal,\n): Promise<T> {\n if (signal.aborted) {\n return Promise.race([Promise.reject(signal), value])\n }\n return new Promise<T>((resolve, reject) => {\n const abort = () => reject(signal)\n signal.addEventListener('abort', abort, { once: true })\n Promise.resolve(value)\n .then(resolve, reject)\n .finally(() => signal.removeEventListener('abort', abort))\n })\n}\n\nexport function getRoute(router: AnyRouter, match: WorkMatch): AnyRoute {\n return (router.routesById as Record<string, AnyRoute>)[match.routeId]!\n}\n\nfunction normalize(\n value: unknown,\n rejected: boolean,\n routeId?: string,\n): LoaderOutcome {\n if (isRedirect(value)) {\n return [REDIRECTED, value]\n }\n if (isNotFound(value)) {\n value.routeId ||= routeId\n return [NOT_FOUND, value]\n }\n if (rejected && typeof (value as any)?.then === 'function') {\n value = new Error('A Promise was thrown', { cause: value })\n }\n return rejected ? [ERROR, value] : [SUCCESS, value]\n}\n\nfunction normalizeError(route: AnyRoute, cause: unknown): LoaderOutcome {\n let outcome = normalize(cause, true, route.id)\n if (outcome[0] !== ERROR) {\n return outcome\n }\n try {\n route.options.onError?.(outcome[1])\n } catch (onErrorCause) {\n outcome = normalize(onErrorCause, true, route.id)\n }\n return outcome\n}\n\nfunction normalizeLaneError(\n route: AnyRoute,\n cause: unknown,\n options: ExecuteLaneOptions,\n): LoaderOutcome {\n if (options[0].signal.aborted || !options[2]()) {\n options[0].abort()\n return [CANCELED]\n }\n return normalizeError(route, cause)\n}\n\nexport function navigateFrom(router: AnyRouter, location: ParsedLocation) {\n return (opts: any) =>\n router.navigate({\n ...opts,\n _fromLocation: location,\n })\n}\n\nasync function contextualize(\n router: AnyRouter,\n lane: MatchedLane,\n options: ExecuteLaneOptions,\n end: number,\n): Promise<IndexedOutcome | undefined> {\n const [location, matches] = lane\n const signal = options[0].signal\n const preload = !!options[4]\n for (let index = options[7] ?? 0; index < end; index++) {\n const match = matches[index]!\n const route = getRoute(router, match)\n\n match.abortController = options[0]\n // Contextualization is serial, so the previous match already contains the\n // complete parent context for this route.\n const parentContext =\n matches[index - 1]?.context ?? router.options.context ?? {}\n const common = {\n params: match.params,\n location,\n navigate: navigateFrom(router, location),\n buildLocation: router.buildLocation,\n cause: preload ? ('preload' as const) : match.cause,\n abortController: options[0],\n preload,\n matches,\n routeId: route.id,\n }\n let context = parentContext\n try {\n let routeContext = match._ctx\n if (!routeContext && route.options.context) {\n routeContext = match._ctx =\n route.options.context({\n ...common,\n deps: match.loaderDeps,\n context: parentContext,\n } satisfies RouteContextOptions<any, any, any, any, any>) || {}\n }\n context = {\n ...parentContext,\n ...routeContext,\n }\n match.context = context\n } catch (cause) {\n releaseFlight(router, match)\n return [index, normalizeLaneError(route, cause, options)]\n }\n if (signal.aborted || !options[2]()) {\n options[0].abort()\n return [index, [CANCELED]]\n }\n const validationError = match.paramsError ?? match.searchError\n if (validationError !== undefined) {\n releaseFlight(router, match)\n return [index, normalizeLaneError(route, validationError, options)]\n }\n const beforeLoad = route.options.beforeLoad\n if (!beforeLoad) {\n continue\n }\n\n const beforeLoadContext: BeforeLoadContextOptions<\n any,\n any,\n any,\n any,\n any,\n any,\n any,\n any,\n any\n > = {\n ...common,\n search: match.search,\n context,\n ...router.options.additionalContext,\n }\n\n const previousStatus = match.status\n if (previousStatus === 'success') {\n match.status = 'pending'\n }\n options[8]?.()\n try {\n setFetching(router, match, 'beforeLoad', options[0])\n const result = await waitFor(beforeLoad(beforeLoadContext), signal)\n if (!options[2]()) {\n options[0].abort()\n return [index, [CANCELED]]\n }\n const outcome = normalize(result, false, route.id)\n if (outcome[0] !== SUCCESS) {\n releaseFlight(router, match)\n return [index, outcome]\n }\n match.context = {\n ...context,\n ...result,\n }\n } catch (cause) {\n releaseFlight(router, match)\n return [index, normalizeLaneError(route, cause, options)]\n } finally {\n if (previousStatus === 'success' && match.status === 'pending') {\n match.status = 'success'\n }\n setFetching(router, match, false, options[0])\n }\n }\n\n return\n}\n\nfunction releaseOwnedFlight(\n router: AnyRouter,\n id: string,\n flight?: LoaderFlight,\n): AbortController | undefined {\n if (!flight || --flight[2]) {\n return\n }\n if (router._flights?.get(id) === flight) {\n router._flights.delete(id)\n }\n return flight[1]\n}\n\nfunction releaseFlight(router: AnyRouter, match: WorkMatch): void {\n const flight = match._flight\n match._flight = undefined\n releaseOwnedFlight(router, match.id, flight)?.abort()\n}\nexport function laneInputs(\n router: AnyRouter,\n location: ParsedLocation,\n): LaneInputs {\n const masked = location.maskedLocation\n return [\n router.routeTree,\n router.options.context ?? {},\n router.options.additionalContext,\n _getUserHistoryState(location.state),\n location.search,\n masked && [\n masked.href,\n _getUserHistoryState(masked.state),\n masked.search,\n masked.unmaskOnReload,\n ],\n ]\n}\n\nfunction samePreloadLane(\n preload: ActivePreload,\n router: AnyRouter,\n location: ParsedLocation,\n redirects: number,\n): boolean {\n return (\n preload[3] === router._committed &&\n preload[5] === redirects &&\n deepEqual(preload[4], laneInputs(router, location)) &&\n !preload[0].some(\n (match) => getRoute(router, match as WorkMatch).options.preload === false,\n )\n )\n}\n\n/**\n * Not passing in a `next` ownership recipient\n * is equivalent to discarding the match resources\n */\nexport function transferMatchResources(\n router: AnyRouter,\n previous: Array<AnyRouteMatch>,\n next?: Array<AnyRouteMatch>,\n): void {\n const abort: Array<AbortController> = []\n for (const match of previous as Array<WorkMatch>) {\n if (!next?.includes(match)) {\n const flight = match._flight\n match._flight = undefined\n const controller = releaseOwnedFlight(router, match.id, flight)\n if (controller) {\n abort.push(controller)\n }\n }\n }\n for (const controller of abort) {\n controller.abort()\n }\n}\n\nfunction discardPreload(router: AnyRouter, preload: ActivePreload): void {\n preload[1].abort()\n transferMatchResources(router, preload[0])\n}\n\nfunction acquireMatchResources(matches: Array<AnyRouteMatch>): void {\n for (const match of matches as Array<WorkMatch>) {\n const flight = match._flight\n if (flight) {\n flight[2]++\n }\n }\n}\n\nfunction setFetching(\n router: AnyRouter,\n match: WorkMatch,\n value: AnyRouteMatch['isFetching'],\n owner?: AbortController,\n): void {\n match.isFetching = value\n if (owner && router._tx?.[0] !== owner) {\n return\n }\n const store = router.stores.byRoute.get(match.routeId)\n const presented = store?.get()\n if (presented?.id === match.id) {\n store!.set({ ...presented, isFetching: value })\n }\n}\n\nfunction getLoaderContext(\n router: AnyRouter,\n lane: ContextualizedLane,\n match: WorkMatch,\n route: AnyRoute,\n controller: AbortController,\n parentMatchPromise: Promise<WorkMatch> | undefined,\n preload: boolean,\n): LoaderFnContext {\n const location = lane[0]\n return {\n params: match.params,\n location,\n navigate: navigateFrom(router, location),\n cause: preload ? ('preload' as const) : match.cause,\n abortController: controller,\n preload,\n deps: match.loaderDeps,\n parentMatchPromise: parentMatchPromise as any,\n context: match.context,\n route,\n ...router.options.additionalContext,\n }\n}\n\nasync function loadResource(\n router: AnyRouter,\n lane: ContextualizedLane,\n match: WorkMatch,\n route: AnyRoute,\n loader: RouteLoaderFn<any> | undefined,\n parentMatchPromise: Promise<WorkMatch> | undefined,\n preload: boolean,\n owner: AbortController,\n): Promise<LoaderOutcome> {\n const signal = owner.signal\n if (signal.aborted) {\n return [CANCELED]\n }\n if (!loader) {\n return [SUCCESS, undefined]\n }\n\n let flight = match._flight\n let joined = !!flight\n setFetching(router, match, 'loader', owner)\n try {\n for (;;) {\n if (!flight) {\n const controller = new AbortController()\n const outcome = Promise.resolve()\n .then(() =>\n loader(\n getLoaderContext(\n router,\n lane,\n match,\n route,\n controller,\n parentMatchPromise,\n preload,\n ),\n ),\n )\n .then(\n (value) => normalize(value, false, route.id),\n (cause) => normalize(cause, true, route.id),\n )\n .then((result): LoaderOutcome => {\n return result[0] === ERROR && match._flight === flight\n ? normalizeError(route, result[1])\n : result\n })\n flight = [outcome, controller, 1]\n ;(router._flights ??= new Map()).set(match.id, flight)\n }\n match._flight = flight\n match.abortController = flight[1]\n try {\n const outcome = await waitFor(flight[0], signal)\n if (!joined || outcome[0] === SUCCESS || outcome[0] === REDIRECTED) {\n return outcome\n }\n } catch (cause) {\n if (cause === signal) {\n releaseFlight(router, match)\n return [CANCELED]\n }\n throw cause\n }\n releaseFlight(router, match)\n if (signal.aborted) {\n return [CANCELED]\n }\n flight = undefined\n joined = false\n }\n } finally {\n setFetching(router, match, false, owner)\n }\n}\n\nfunction settleInto(\n match: WorkMatch,\n result: LoaderOutcome,\n preload: boolean,\n): asserts match is SettledMatch {\n if (result[0] === SUCCESS) {\n match.loaderData = result[1]\n match.error = undefined\n match.status = 'success'\n match.invalid = false\n match.updatedAt = Date.now()\n match.preload = preload\n } else if (result[0] !== REDIRECTED) {\n // Reduction installs only the selected terminal failure. Every other\n // settled attempt remains a renderable, stale match in that lane.\n match.status = 'success'\n match.error = undefined\n match.invalid = true\n }\n}\n\nexport function cacheLoaderMatch(\n router: CoordinatorRouter,\n match: SettledMatch,\n planned: AnyRouteMatch | undefined,\n): void {\n const current = router._cache.get(match.id) as WorkMatch | undefined\n if (\n current !== planned ||\n router._committed.some(\n (candidate) =>\n candidate.id === match.id &&\n (candidate as WorkMatch)._flight === match._flight,\n )\n ) {\n return\n }\n const cached = {\n ...match,\n _notFound: undefined,\n context: {},\n } as WorkMatch\n if (cached._flight) {\n cached._flight[2]++\n }\n router._cache.set(match.id, cached)\n if (current) {\n releaseFlight(router, current)\n }\n}\n\nfunction getParentSnapshot(\n match: WorkMatch,\n outcome: LoaderOutcome,\n): WorkMatch {\n if (outcome[0] === ERROR || outcome[0] === NOT_FOUND) {\n return {\n ...match,\n status: outcome[0] === ERROR ? 'error' : 'notFound',\n error: outcome[1],\n _flight: undefined,\n }\n }\n return match\n}\n\nfunction createLoaderTask(\n router: AnyRouter,\n lane: ContextualizedLane,\n index: number,\n tasks: Array<LoaderTask>,\n semanticParent: Promise<WorkMatch> | undefined,\n options: ExecuteLaneOptions,\n): Promise<WorkMatch> {\n const match = lane[1][index]!\n const route = getRoute(router, match)\n const preload = !!options[4]\n const plannedCacheMatch = preload ? router._cache.get(match.id) : undefined\n let reload = false\n let reloadFailure: LoaderOutcome | undefined\n try {\n let configured\n if (match.status === 'success') {\n configured = route.options.shouldReload\n if (typeof configured === 'function') {\n configured = configured(\n getLoaderContext(\n router,\n lane,\n match,\n route,\n options[0],\n semanticParent,\n preload,\n ),\n )\n }\n if (!options[2]()) {\n options[0].abort()\n reloadFailure = [CANCELED]\n }\n }\n if (!reloadFailure) {\n if (match.status !== 'success') {\n reload = true\n } else {\n const staleAge =\n options[4] || match.preload\n ? (route.options.preloadStaleTime ??\n router.options.defaultPreloadStaleTime ??\n 30_000)\n : (route.options.staleTime ?? router.options.defaultStaleTime ?? 0)\n reload = !!(\n match.invalid ||\n configured ||\n (configured === undefined &&\n Date.now() - match.updatedAt >= staleAge &&\n (options[6] ||\n match.cause === 'enter' ||\n options[3].some(\n (candidate) =>\n candidate.routeId === match.routeId &&\n candidate.id !== match.id,\n )))\n )\n }\n }\n } catch (cause) {\n match.invalid = true\n releaseFlight(router, match)\n reloadFailure = normalizeLaneError(route, cause, options)\n }\n const routeLoader = route.options.loader\n const loader =\n typeof routeLoader === 'function' ? routeLoader : routeLoader?.handler\n const background = !!(\n routeLoader &&\n reload &&\n match.status === 'success' &&\n !preload &&\n !options[5] &&\n ((typeof routeLoader === 'function'\n ? undefined\n : routeLoader?.staleReloadMode) ??\n router.options.defaultStaleReloadMode) !== 'blocking'\n )\n const loaded = reload && (!preload || route.options.preload !== false)\n const blocking =\n loaded && !background && (match.status !== 'success' || !!routeLoader)\n if (loaded && !routeLoader) {\n match.invalid = false\n match.updatedAt = Date.now()\n }\n let donor = loaded && routeLoader ? router._flights?.get(match.id) : undefined\n if (donor === match._flight) {\n donor = undefined\n } else if (donor) {\n donor[2]++\n }\n if (blocking) {\n const acceptedFlight = match._flight\n match._flight = donor\n releaseOwnedFlight(router, match.id, acceptedFlight)?.abort()\n // A successful route without a loader has no blocking work to present. It\n // still gets a task so its chunk and derived assets participate in the\n // lane, but putting it back into pending would hide an already-rendered\n // ancestor while only a descendant is loading.\n if (match.status === 'success') {\n match.status = 'pending'\n }\n options[8]?.()\n }\n if (!loaded) {\n match.isFetching = false\n }\n const rawOutcome = reloadFailure\n ? Promise.resolve(reloadFailure)\n : !blocking\n ? Promise.resolve<LoaderOutcome>([SUCCESS, match.loaderData])\n : loadResource(\n router,\n lane,\n match,\n route,\n loader,\n semanticParent,\n preload,\n options[0],\n )\n const outcome = rawOutcome.then((result) => {\n if (blocking) {\n settleInto(match, result, preload)\n if (result[0] === SUCCESS) {\n if (preload && routeLoader) {\n cacheLoaderMatch(router, match, plannedCacheMatch)\n }\n // A route is renderable only after both its data and normal component\n // chunk are ready. Its loader data is already available to descendants.\n match.status = 'pending'\n }\n }\n return result\n })\n\n const rawChunkFailure = waitFor(\n Promise.resolve().then(() => loadRouteChunk(route)),\n options[0].signal,\n ).then(\n () => {\n options[8]?.()\n return undefined\n },\n (cause): IndexedOutcome => [\n index,\n normalizeLaneError(route, cause, options),\n ],\n )\n const chunkFailure = rawChunkFailure.then((failure) =>\n outcome.then((result) => {\n if (\n blocking &&\n !failure &&\n result[0] === SUCCESS &&\n match.status === 'pending' &&\n options[2]()\n ) {\n match.status = 'success'\n options[8]?.()\n }\n return failure\n }),\n )\n tasks.push([index, outcome, chunkFailure])\n if (!background) {\n return outcome.then((result) => getParentSnapshot(match, result))\n }\n const candidate: WorkMatch = {\n ...match,\n status: 'pending',\n preload: false,\n _flight: donor,\n }\n match.invalid = false\n match.isFetching = 'loader'\n const backgroundOutcome = loadResource(\n router,\n lane,\n candidate,\n route,\n loader,\n semanticParent,\n false,\n options[0],\n ).then((result) => {\n match.isFetching = false\n settleInto(candidate, result, false)\n return result\n })\n ;(lane[2] ??= []).push([index, backgroundOutcome, chunkFailure, candidate])\n return backgroundOutcome.then((result) =>\n getParentSnapshot(candidate, result),\n )\n}\n\nasync function getNotFoundBoundary(\n router: AnyRouter,\n matches: Array<WorkMatch>,\n indexed: IndexedOutcome | undefined,\n signal: AbortSignal,\n fallback = 0,\n): Promise<number> {\n const cause = indexed?.[1][1] as NotFoundError | undefined\n let index = cause?.routeId\n ? matches.findIndex((match) => match.routeId === cause.routeId)\n : (indexed?.[0] ?? matches.length - 1)\n if (index < 0) {\n index = 0\n }\n for (let i = index; i >= 0; i--) {\n const route = getRoute(router, matches[i]!)\n const loading = loadRouteChunk(route, false)\n if (loading) {\n try {\n await waitFor(loading, signal)\n } catch (cause) {\n if (cause === signal) {\n throw cause\n }\n }\n }\n if (route.options.notFoundComponent) {\n return i\n }\n }\n return cause?.routeId ? index : fallback\n}\n\nfunction discardBackground(router: AnyRouter, lane: Lane<any>): void {\n if (lane[2]) {\n transferMatchResources(\n router,\n lane[2].map((task) => task[3]),\n )\n lane[2] = undefined\n }\n}\n\nasync function settleTasks(\n tasks: Array<LoaderTask>,\n serialFailure?: IndexedOutcome,\n redirectTasks?: Array<BackgroundLoaderTask>,\n gate?: number | Promise<number>,\n): Promise<IndexedOutcome | undefined> {\n let loaderFailure: IndexedOutcome | undefined\n\n try {\n await Promise.all(\n tasks.map((task) =>\n task[1].then(async (outcome) => {\n const taskIndex = task[0]\n if (gate && taskIndex >= (await gate)) {\n return\n }\n if (outcome[0] >= REDIRECTED) {\n throw [taskIndex, outcome] as IndexedOutcome\n }\n if (!loaderFailure && outcome[0] !== SUCCESS) {\n loaderFailure = [taskIndex, outcome]\n // Every started descendant must settle before an ordinary failure\n // wins because a redirect from any of them remains control flow.\n await Promise.all(\n (redirectTasks ?? []).map((nextTask) => {\n if (nextTask[0] <= taskIndex) {\n return\n }\n return nextTask[1].then((nextOutcome) => {\n if (nextOutcome[0] === REDIRECTED) {\n throw [nextTask[0], nextOutcome] as IndexedOutcome\n }\n })\n }),\n )\n }\n }),\n ),\n )\n } catch (cause) {\n return cause as IndexedOutcome\n }\n return serialFailure ?? loaderFailure\n}\n\nasync function reduceLane(\n router: AnyRouter,\n lane: ContextualizedLane,\n tasks: Array<LoaderTask>,\n controller: AbortController,\n redirects: number,\n settlement: Promise<IndexedOutcome | undefined>,\n onReady?: () => void,\n): Promise<ReducedLane | ControlOutcome> {\n const matches = lane[1]\n let failure = await settlement\n let redirectLimitExceeded = false\n const plannedBoundary = matches.findIndex((match) => match._notFound)\n const boundaryOf = (found: IndexedOutcome) =>\n found[1][0] === NOT_FOUND\n ? getNotFoundBoundary(router, matches, found, controller.signal)\n : found[0]\n let readinessEnd = plannedBoundary < 0 ? matches.length : plannedBoundary\n\n if ((failure?.[1][0] ?? 0) >= REDIRECTED) {\n readinessEnd = 0\n } else if (failure) {\n readinessEnd = failure[2] ??= await boundaryOf(failure)\n for (const task of tasks) {\n if (task[0] >= readinessEnd) {\n break\n }\n const outcome = await task[1]\n // Presence means a loader previously succeeded, even with `undefined`.\n if (\n outcome[0] !== SUCCESS &&\n outcome[0] < REDIRECTED &&\n !('loaderData' in matches[task[0]]!)\n ) {\n failure = [task[0], outcome]\n readinessEnd = failure[2] = await boundaryOf(failure)\n break\n }\n }\n }\n\n for (const task of tasks) {\n if (task[0] >= readinessEnd) {\n break\n }\n const chunkFailure = await task[2]\n if (!chunkFailure) {\n continue\n }\n failure = chunkFailure\n break\n }\n\n if ((failure?.[1][0] ?? 0) >= REDIRECTED) {\n const outcome = failure![1]\n if (\n outcome[0] !== REDIRECTED ||\n outcome[1].options.reloadDocument ||\n redirects < 20\n ) {\n discardBackground(router, lane)\n return outcome as ControlOutcome\n }\n redirectLimitExceeded = true\n failure = [0, [ERROR, new Error('Too many redirects')]]\n }\n\n const boundary = failure\n ? (failure[2] ?? (await boundaryOf(failure)))\n : plannedBoundary\n if (boundary >= 0) {\n const outcome = failure?.[1]\n const kind = outcome?.[0]\n const match = matches[boundary]!\n const cause = outcome?.[1]\n const install = () => {\n if (outcome) {\n match._notFound = undefined\n if (kind === ERROR) {\n match.status = 'error'\n } else {\n ;(cause as NotFoundError).routeId = match.routeId\n if (match.routeId === router.routeTree.id) {\n match.status = 'success'\n match._notFound = true\n } else {\n match.status = 'notFound'\n }\n }\n match.error = cause\n match.isFetching = false\n }\n }\n install()\n try {\n await waitFor<unknown>(\n outcome\n ? Promise.resolve().then(() =>\n loadRouteChunk(\n getRoute(router, match),\n kind === ERROR ? 'errorComponent' : 'notFoundComponent',\n ),\n )\n : Promise.all([\n loadRouteChunk(getRoute(router, match)),\n loadRouteChunk(getRoute(router, match), 'notFoundComponent'),\n ]),\n controller.signal,\n )\n } catch (cause) {\n if (cause === controller.signal) {\n discardBackground(router, lane)\n return [CANCELED]\n }\n }\n if (!outcome) {\n match.status = 'success'\n onReady?.()\n } else if (redirectLimitExceeded) {\n controller.abort()\n await Promise.all([\n ...tasks.map((task) => task[1]),\n ...tasks.map((task) => task[2]),\n ...(lane[2] ?? []).map((task) => task[1]),\n ])\n discardBackground(router, lane)\n transferMatchResources(router, matches)\n install()\n }\n }\n\n return lane as ReducedLane\n}\n\nexport async function projectLane(\n router: AnyRouter,\n lane: ReducedLane,\n signal: AbortSignal,\n start = 0,\n end = lane[1].length,\n): Promise<ProjectedLane> {\n const matches = lane[1]\n for (let index = start; index < end; index++) {\n const match = matches[index]!\n const routeOptions = getRoute(router, match).options\n if (routeOptions.head || routeOptions.scripts) {\n try {\n const context = {\n ssr: router.options.ssr,\n matches,\n match,\n params: match.params,\n loaderData: match.loaderData,\n }\n const [head, scripts] = await waitFor(\n Promise.all([\n routeOptions.head?.(context),\n routeOptions.scripts?.(context),\n ]),\n signal,\n )\n match.meta = head?.meta\n match.links = head?.links\n match.headScripts = head?.scripts\n match.styles = head?.styles\n match.scripts = scripts\n } catch (cause) {\n if (cause === signal) {\n break\n }\n console.error(cause)\n }\n }\n if (match.status !== 'success' || match._notFound) {\n break\n }\n }\n return lane as ProjectedLane\n}\n\nasync function executeClientLane(\n router: AnyRouter,\n location: ParsedLocation,\n matches: Array<AnyRouteMatch>,\n options: ExecuteLaneOptions,\n): Promise<LaneResult> {\n const matched = [location, matches as Array<WorkMatch>] as MatchedLane\n let plannedBoundary = matches.findIndex((match) => match._notFound)\n if (router.options.notFoundMode !== 'root' && plannedBoundary >= 0) {\n const boundary = await getNotFoundBoundary(\n router,\n matched[1],\n undefined,\n options[0].signal,\n plannedBoundary,\n )\n if (boundary !== plannedBoundary) {\n matches[plannedBoundary]!._notFound = undefined\n matches[boundary]!._notFound = true\n }\n plannedBoundary = boundary\n }\n let end = plannedBoundary < 0 ? matches.length : plannedBoundary + 1\n // From here on `matched` is contextualized: `contextualize` communicates\n // through mutation plus a failure return, so the phase brand is asserted at\n // the two use sites below rather than granted by a (byte-costing) return.\n const failure = await contextualize(router, matched, options, end)\n if (failure) {\n options[5] = true\n }\n const tasks: Array<LoaderTask> = []\n const start = options[7] ?? 0\n let semanticParent = start\n ? Promise.resolve(matched[1][start - 1]!)\n : undefined\n end = failure?.[0] ?? end\n if (failure?.[1][0] === NOT_FOUND) {\n failure[2] = await getNotFoundBoundary(\n router,\n matched[1],\n failure,\n options[0].signal,\n )\n end = Math.min(end, failure[2] + 1)\n } else if ((failure?.[1][0] ?? 0) >= REDIRECTED) {\n end = 0\n }\n for (let index = start; index < end; index++) {\n if (options[0].signal.aborted) {\n break\n }\n semanticParent = createLoaderTask(\n router,\n matched as ContextualizedLane,\n index,\n tasks,\n semanticParent,\n options,\n )\n }\n let reduced: ReducedLane | ControlOutcome\n try {\n const reduction = reduceLane(\n router,\n matched as ContextualizedLane,\n tasks,\n options[0],\n options[1],\n settleTasks(tasks, failure, matched[2]),\n options[8],\n )\n if (matched[2]?.length) {\n matched[3] = settleTasks(\n matched[2],\n undefined,\n undefined,\n reduction.then(\n (foreground) =>\n isControl(foreground)\n ? 0\n : _getRenderedMatches(foreground[1]).length,\n () => 0,\n ),\n )\n }\n reduced = await reduction\n } catch (cause) {\n discardBackground(router, matched)\n throw cause\n }\n if (isControl(reduced)) {\n return reduced\n }\n return projectLane(\n router,\n reduced,\n options[0].signal,\n options[7] === reduced[1].length ? options[7] : 0,\n )\n}\n\n/**\n * Finds the first route that should show pending UI and its two timing values.\n * A fallback already on screen remains selected after its route loads, so we\n * do not jump to a child fallback. Matches put back into pending by invalidation\n * skip pendingMs, and a route without a usable fallback blocks pending UI for deeper routes.\n */\nfunction pendingConfig(\n router: AnyRouter,\n matches: Array<AnyRouteMatch>,\n):\n | [delay: number, boundary: number, min: number, component: unknown]\n | undefined\n | void {\n const presented = router.stores.matches.get()\n for (let index = 0; index < matches.length; index++) {\n const match = matches[index]!\n const success = match.status === 'success'\n const visible =\n success &&\n presented[index]?.id === match.id &&\n presented[index]?.status === 'pending'\n if (success && !visible) {\n continue\n }\n const route = getRoute(router, match as WorkMatch)\n const delay =\n visible || match.invalid\n ? 0\n : (route.options.pendingMs ?? router.options.defaultPendingMs)\n const component =\n route.options.pendingComponent ??\n (router.options as any).defaultPendingComponent\n return component && typeof delay === 'number' && delay !== Infinity\n ? [\n delay,\n index,\n route.options.pendingMinMs ?? router.options.defaultPendingMinMs ?? 0,\n component,\n ]\n : undefined\n }\n}\n\n/**\n * Waits for `pendingMs`, then presents the complete lane. Rendering applies the\n * selected boundary cutoff while retaining every match's structural state.\n * A replacement load for the same match keeps the timer; choosing a different\n * match resets it. `pendingMinMs` starts after the fallback renders.\n */\nfunction offerPending(router: CoordinatorRouter, tx: LoadTransaction): void {\n if (router._tx !== tx) {\n return\n }\n let session = router._pending\n let tookOver = false\n const sessionMatchId = session?.[0][3][session[1]]?.id\n if (session?.[0] !== tx) {\n if (session && tx[3][session[1]]?.id === sessionMatchId) {\n session[0] = tx\n tookOver = true\n } else {\n clearTimeout(session?.[3])\n router._pending = session = undefined\n }\n }\n const config = pendingConfig(router, tx[3])\n if (!config) {\n return\n }\n const [delay, boundary, min, component] = config\n const matchId = tx[3][boundary]!.id\n if (!session || session[1] !== boundary || sessionMatchId !== matchId) {\n // Hydration and redirects can preserve pending presentation without a session.\n // Do not delay it again; conservatively start pendingMinMs from now.\n clearTimeout(session?.[3])\n const presented = router.stores.matches.get()[boundary]\n const visible = presented?.id === matchId && presented.status === 'pending'\n router._pending = session = [\n tx,\n boundary,\n visible ? Date.now() + min : tx[4] + delay,\n undefined,\n visible ? Promise.resolve(true) : undefined,\n component,\n ]\n }\n if (session[4] && !tookOver && session[5] === component) {\n return\n }\n session[5] = component\n if (!session[4]) {\n clearTimeout(session[3])\n const remaining = session[2] - Date.now()\n if (remaining > 0) {\n session[3] = setTimeout(() => {\n offerPending(router, tx)\n }, remaining)\n return\n }\n session[2] = 0\n }\n const offered = tx[3].map((match) => ({\n ...match,\n _flight: undefined,\n }))\n offered[boundary]!.status = 'pending'\n const ack = router\n .startTransition(() => router.stores.setMatches(offered), offered, true)\n .then((rendered) => {\n if (\n rendered &&\n router._pending === session &&\n session[4] === ack &&\n !session[2]\n ) {\n session[2] = Date.now() + min\n }\n return rendered\n })\n session[4] = ack\n}\n\n/**\n * Cancels pending UI timing when its load ends. The ownership check prevents\n * an older, superseded load from clearing pending UI that a newer load took over.\n */\nfunction finishPending(router: CoordinatorRouter, tx: LoadTransaction): void {\n const session = router._pending\n if (session?.[0] === tx) {\n clearTimeout(session[3])\n router._pending = undefined\n }\n}\n\nfunction publishMatches(\n router: CoordinatorRouter,\n matches: Array<AnyRouteMatch>,\n): void {\n router._committed = matches\n router.stores.setMatches(matches)\n}\n\nfunction discardLane(router: AnyRouter, lane: ProjectedLane): void {\n transferMatchResources(router, lane[1])\n discardBackground(router, lane)\n}\n\nfunction commitMatches(\n router: CoordinatorRouter,\n tx: LoadTransaction,\n matches: LaneMatches<'projected'>,\n resolvedPrefix?: number,\n): void {\n const previous = router._committed\n const previousCached = router._cache\n for (const match of matches) {\n match.preload = false\n if (resolvedPrefix) {\n match._assetEnd = undefined\n }\n }\n const cut = _getRenderedMatches(matches).length\n const cached = new Map<string, AnyRouteMatch>()\n const now = Date.now()\n for (const match of [...previous, ...previousCached.values()]) {\n // Rendered-prefix ids and settled successes anywhere in the lane are\n // authoritative: retaining an older same-id generation would shadow them\n // at the next planning pass. Unsettled beyond-boundary matches are not —\n // they must not evict a newer same-id preload.\n if (\n match.status !== 'success' ||\n matches.some(\n (candidate, index) =>\n candidate.id === match.id &&\n (index < cut || candidate.status === 'success'),\n )\n ) {\n continue\n }\n const work = match as WorkMatch\n const route = getRoute(router, work)\n if (\n !route.options.loader ||\n now - match.updatedAt >=\n (match.preload\n ? (route.options.preloadGcTime ??\n router.options.defaultPreloadGcTime ??\n 300_000)\n : (route.options.gcTime ?? router.options.defaultGcTime ?? 300_000))\n ) {\n continue\n }\n cached.set(\n match.id,\n previousCached.get(match.id) === match\n ? match\n : ({\n ...match,\n _flight: undefined,\n isFetching: false,\n context: {},\n } as WorkMatch),\n )\n }\n // The lane becomes committed before publication can synchronously reenter.\n tx[3] = []\n router._cache = cached\n publishMatches(router, matches)\n transferMatchResources(\n router,\n [...previousCached.values(), ...previous],\n [...matches, ...cached.values()],\n )\n runRouteLifecycle(router, previous, matches, () => router._tx === tx)\n}\n\nfunction commitRefreshMatches(\n router: CoordinatorRouter,\n tx: LoadTransaction,\n matches: LaneMatches<'projected'>,\n checkpoint: PublicationCheckpoint,\n): void {\n const previous = router._committed\n const previousCached = router._cache\n for (const match of matches) {\n match.preload = false\n }\n const cached = new Map<string, AnyRouteMatch>()\n // Delay releasing the previous owners until the HMR render is acknowledged.\n // Old generations must not become reusable cache entries after refresh.\n tx[3] = []\n router._cache = cached\n checkpoint.previousMatches = previous\n checkpoint.previousCache = previousCached\n checkpoint.published = true\n publishMatches(router, matches)\n if (!checkpoint.published || router._tx !== tx) {\n return\n }\n runRouteLifecycle(router, previous, matches, () => router._tx === tx)\n}\n\nfunction settlePublication(\n router: CoordinatorRouter,\n checkpoint: PublicationCheckpoint,\n): void {\n if (!checkpoint.published) {\n return\n }\n checkpoint.published = false\n transferMatchResources(\n router,\n [...checkpoint.previousCache.values(), ...checkpoint.previousMatches],\n [...router._cache.values(), ...router._committed],\n )\n}\n\nfunction rollbackPublication(\n router: CoordinatorRouter,\n tx: LoadTransaction,\n lane: ProjectedLane,\n checkpoint: PublicationCheckpoint,\n): boolean {\n if (\n !checkpoint.published ||\n router._tx !== tx ||\n router._committed !== lane[1]\n ) {\n settlePublication(router, checkpoint)\n return false\n }\n\n const discarded = [...router._cache.values(), ...router._committed]\n const restored = [\n ...checkpoint.previousCache.values(),\n ...checkpoint.previousMatches,\n ]\n router._cache = checkpoint.previousCache\n router._committed = checkpoint.previousMatches\n checkpoint.published = false\n\n for (const match of discarded as Array<WorkMatch>) {\n if (\n !restored.includes(match) &&\n match._flight &&\n router._flights?.get(match.id) === match._flight\n ) {\n router._flights.delete(match.id)\n }\n }\n\n finishPending(router, tx)\n router.batch(() => {\n router.stores.status.set('idle')\n router.stores.setMatches(checkpoint.previousPresentation)\n })\n tx[0].abort()\n transferMatchResources(router, discarded, restored)\n discardBackground(router, lane)\n if (router._tx === tx && router._commitPromise === checkpoint.commitPromise) {\n router._commitPromise?.resolve()\n router._commitPromise = undefined\n }\n return true\n}\n\nasync function transitionRefresh(\n router: CoordinatorRouter,\n tx: LoadTransaction,\n lane: ProjectedLane,\n changeInfo: ReturnType<typeof getLocationChangeInfo>,\n): Promise<boolean | undefined> {\n const checkpoint: PublicationCheckpoint = {\n previousMatches: router._committed,\n previousPresentation: tx[6]?.[0] ?? router.stores.matches.get(),\n previousCache: router._cache,\n commitPromise: router._commitPromise,\n published: false,\n }\n const commit = () => {\n finishPending(router, tx)\n router._rollbackRefresh = rollback\n commitRefreshMatches(router, tx, lane[1], checkpoint)\n if (!checkpoint.published || router._tx !== tx) {\n return\n }\n router.emit({ type: 'onLoad', ...changeInfo })\n if (router._tx === tx) {\n router.emit({ type: 'onBeforeRouteMount', ...changeInfo })\n }\n }\n const rollback = () => {\n if (router._rollbackRefresh === rollback) {\n router._rollbackRefresh = undefined\n }\n const restored = rollbackPublication(router, tx, lane, checkpoint)\n router._cancelTransition?.()\n return restored\n }\n try {\n const rendered = await router.startTransition(commit, lane[1])\n if (router._rollbackRefresh === rollback) {\n router._rollbackRefresh = undefined\n }\n if (checkpoint.published) {\n const handoff = tx[6]?.[1]\n if (handoff && router._handoff === handoff) {\n handoff[1]()\n }\n if (router._tx === tx) {\n tx[6] = undefined\n }\n }\n settlePublication(router, checkpoint)\n return rendered\n } catch (cause) {\n if (rollback()) {\n return\n }\n throw cause\n }\n}\n\nasync function awaitCurrent(\n router: CoordinatorRouter,\n owner?: LoadTransaction,\n): Promise<void> {\n let current = router._tx\n while (current && current !== owner) {\n await current[5]\n if (router._tx === current) {\n return\n }\n current = router._tx\n }\n}\n\nasync function followRedirect(\n router: CoordinatorRouter,\n tx: LoadTransaction,\n redirect: AnyRedirect,\n): Promise<void> {\n await router.navigate({\n ...redirect.options,\n replace: true,\n ignoreBlocker: true,\n _redirects: tx[1] + 1,\n } as any)\n}\n\nfunction restoreCommitted(\n router: CoordinatorRouter,\n tx: LoadTransaction,\n): void {\n finishPending(router, tx)\n tx[0].abort()\n transferMatchResources(router, tx[3])\n tx[3] = []\n if (router._tx !== tx) {\n return\n }\n router.batch(() => {\n router.stores.status.set('idle')\n router.stores.setMatches(router._committed)\n })\n if (router._tx === tx) {\n router._commitPromise?.resolve()\n router._commitPromise = undefined\n }\n}\n\nasync function runBackground(\n router: Coordi