UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

1,411 lines 52.4 kB
import { deepEqual } from "./utils.js"; import { isNotFound } from "./not-found.js"; import { isRedirect } from "./redirect.js"; import { hydrateSsrMatchId } from "./ssr/ssr-match-id.js"; import { _getUserHistoryState, getLocationChangeInfo, runRouteLifecycle } from "./router.js"; //#region src/load-client.ts function replaceRouteChunk(route, lazyFn) { route.lazyFn = lazyFn ?? route.lazyFn; route._lazy = void 0; } function preloadComponent(route, type) { return route.options[type]?.preload?.(); } function loadComponents(route) { const component = preloadComponent(route, "component"); const pending = preloadComponent(route, "pendingComponent"); if (component && pending) return Promise.all([component, pending]).then(() => {}); return component ?? pending; } function loadRouteChunk(route, componentType) { const afterLazy = () => componentType === false ? void 0 : componentType ? preloadComponent(route, componentType) : loadComponents(route); const current = route._lazy; if (current) return current === true ? afterLazy() : current.then(afterLazy); if (!route.lazyFn) return afterLazy(); const promise = route.lazyFn().then((lazyRoute) => { if (process.env.NODE_ENV === "production" || route._lazy === promise) { const { id: _id, ...options } = lazyRoute.options; Object.assign(route.options, options); route._lazy = true; } }, (error) => { if (process.env.NODE_ENV === "production" || route._lazy === promise) route._lazy = void 0; throw error; }); route._lazy = promise; return promise.then(afterLazy); } /** Return the structural lane through the first terminal render boundary. */ function _getRenderedMatches(matches) { const end = matches.findIndex((match) => match.status !== "success" || match._notFound) + 1; return end && end < matches.length ? matches.slice(0, end) : matches; } /** Return the lane whose document assets belong to the current presentation. */ function _getAssetMatches(matches) { let end = matches.length; for (let index = 0; index < end; index++) { const match = matches[index]; if (match._assetEnd !== void 0) { end = Math.min(end, Math.max(index + 1, match._assetEnd)); continue; } if (match.status !== "success" || match._notFound) { end = index + 1; break; } } return end < matches.length ? matches.slice(0, end) : matches; } const SUCCESS = 0; const ERROR = 1; const NOT_FOUND = 2; const REDIRECTED = 3; const CANCELED = 4; function isControl(result) { return typeof result[0] === "number"; } function waitFor(value, signal) { if (signal.aborted) return Promise.race([Promise.reject(signal), value]); return new Promise((resolve, reject) => { const abort = () => reject(signal); signal.addEventListener("abort", abort, { once: true }); Promise.resolve(value).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)); }); } function getRoute(router, match) { return router.routesById[match.routeId]; } function normalize(value, rejected, routeId) { if (isRedirect(value)) return [REDIRECTED, value]; if (isNotFound(value)) { value.routeId ||= routeId; return [NOT_FOUND, value]; } if (rejected && typeof value?.then === "function") value = new Error("A Promise was thrown", { cause: value }); return rejected ? [ERROR, value] : [SUCCESS, value]; } function normalizeError(route, cause) { let outcome = normalize(cause, true, route.id); if (outcome[0] !== ERROR) return outcome; try { route.options.onError?.(outcome[1]); } catch (onErrorCause) { outcome = normalize(onErrorCause, true, route.id); } return outcome; } function normalizeLaneError(route, cause, options) { if (options[0].signal.aborted || !options[2]()) { options[0].abort(); return [CANCELED]; } return normalizeError(route, cause); } function navigateFrom(router, location) { return (opts) => router.navigate({ ...opts, _fromLocation: location }); } async function contextualize(router, lane, options, end) { const [location, matches] = lane; const signal = options[0].signal; const preload = !!options[4]; for (let index = options[7] ?? 0; index < end; index++) { const match = matches[index]; const route = getRoute(router, match); match.abortController = options[0]; const parentContext = matches[index - 1]?.context ?? router.options.context ?? {}; const common = { params: match.params, location, navigate: navigateFrom(router, location), buildLocation: router.buildLocation, cause: preload ? "preload" : match.cause, abortController: options[0], preload, matches, routeId: route.id }; let context = parentContext; try { let routeContext = match._ctx; if (!routeContext && route.options.context) routeContext = match._ctx = route.options.context({ ...common, deps: match.loaderDeps, context: parentContext }) || {}; context = { ...parentContext, ...routeContext }; match.context = context; } catch (cause) { releaseFlight(router, match); return [index, normalizeLaneError(route, cause, options)]; } if (signal.aborted || !options[2]()) { options[0].abort(); return [index, [CANCELED]]; } const validationError = match.paramsError ?? match.searchError; if (validationError !== void 0) { releaseFlight(router, match); return [index, normalizeLaneError(route, validationError, options)]; } const beforeLoad = route.options.beforeLoad; if (!beforeLoad) continue; const beforeLoadContext = { ...common, search: match.search, context, ...router.options.additionalContext }; const previousStatus = match.status; if (previousStatus === "success") match.status = "pending"; options[8]?.(); try { setFetching(router, match, "beforeLoad", options[0]); const result = await waitFor(beforeLoad(beforeLoadContext), signal); if (!options[2]()) { options[0].abort(); return [index, [CANCELED]]; } const outcome = normalize(result, false, route.id); if (outcome[0] !== SUCCESS) { releaseFlight(router, match); return [index, outcome]; } match.context = { ...context, ...result }; } catch (cause) { releaseFlight(router, match); return [index, normalizeLaneError(route, cause, options)]; } finally { if (previousStatus === "success" && match.status === "pending") match.status = "success"; setFetching(router, match, false, options[0]); } } } function releaseOwnedFlight(router, id, flight) { if (!flight || --flight[2]) return; if (router._flights?.get(id) === flight) router._flights.delete(id); return flight[1]; } function releaseFlight(router, match) { const flight = match._flight; match._flight = void 0; releaseOwnedFlight(router, match.id, flight)?.abort(); } function laneInputs(router, location) { const masked = location.maskedLocation; return [ router.routeTree, router.options.context ?? {}, router.options.additionalContext, _getUserHistoryState(location.state), location.search, masked && [ masked.href, _getUserHistoryState(masked.state), masked.search, masked.unmaskOnReload ] ]; } function samePreloadLane(preload, router, location, redirects) { return preload[3] === router._committed && preload[5] === redirects && deepEqual(preload[4], laneInputs(router, location)) && !preload[0].some((match) => getRoute(router, match).options.preload === false); } /** * Not passing in a `next` ownership recipient * is equivalent to discarding the match resources */ function transferMatchResources(router, previous, next) { const abort = []; for (const match of previous) if (!next?.includes(match)) { const flight = match._flight; match._flight = void 0; const controller = releaseOwnedFlight(router, match.id, flight); if (controller) abort.push(controller); } for (const controller of abort) controller.abort(); } function discardPreload(router, preload) { preload[1].abort(); transferMatchResources(router, preload[0]); } function acquireMatchResources(matches) { for (const match of matches) { const flight = match._flight; if (flight) flight[2]++; } } function setFetching(router, match, value, owner) { match.isFetching = value; if (owner && router._tx?.[0] !== owner) return; const store = router.stores.byRoute.get(match.routeId); const presented = store?.get(); if (presented?.id === match.id) store.set({ ...presented, isFetching: value }); } function getLoaderContext(router, lane, match, route, controller, parentMatchPromise, preload) { const location = lane[0]; return { params: match.params, location, navigate: navigateFrom(router, location), cause: preload ? "preload" : match.cause, abortController: controller, preload, deps: match.loaderDeps, parentMatchPromise, context: match.context, route, ...router.options.additionalContext }; } async function loadResource(router, lane, match, route, loader, parentMatchPromise, preload, owner) { const signal = owner.signal; if (signal.aborted) return [CANCELED]; if (!loader) return [SUCCESS, void 0]; let flight = match._flight; let joined = !!flight; setFetching(router, match, "loader", owner); try { for (;;) { if (!flight) { const controller = new AbortController(); flight = [ Promise.resolve().then(() => loader(getLoaderContext(router, lane, match, route, controller, parentMatchPromise, preload))).then((value) => normalize(value, false, route.id), (cause) => normalize(cause, true, route.id)).then((result) => { return result[0] === ERROR && match._flight === flight ? normalizeError(route, result[1]) : result; }), controller, 1 ]; (router._flights ??= /* @__PURE__ */ new Map()).set(match.id, flight); } match._flight = flight; match.abortController = flight[1]; try { const outcome = await waitFor(flight[0], signal); if (!joined || outcome[0] === SUCCESS || outcome[0] === REDIRECTED) return outcome; } catch (cause) { if (cause === signal) { releaseFlight(router, match); return [CANCELED]; } throw cause; } releaseFlight(router, match); if (signal.aborted) return [CANCELED]; flight = void 0; joined = false; } } finally { setFetching(router, match, false, owner); } } function settleInto(match, result, preload) { if (result[0] === SUCCESS) { match.loaderData = result[1]; match.error = void 0; match.status = "success"; match.invalid = false; match.updatedAt = Date.now(); match.preload = preload; } else if (result[0] !== REDIRECTED) { match.status = "success"; match.error = void 0; match.invalid = true; } } function cacheLoaderMatch(router, match, planned) { const current = router._cache.get(match.id); if (current !== planned || router._committed.some((candidate) => candidate.id === match.id && candidate._flight === match._flight)) return; const cached = { ...match, _notFound: void 0, context: {} }; if (cached._flight) cached._flight[2]++; router._cache.set(match.id, cached); if (current) releaseFlight(router, current); } function getParentSnapshot(match, outcome) { if (outcome[0] === ERROR || outcome[0] === NOT_FOUND) return { ...match, status: outcome[0] === ERROR ? "error" : "notFound", error: outcome[1], _flight: void 0 }; return match; } function createLoaderTask(router, lane, index, tasks, semanticParent, options) { const match = lane[1][index]; const route = getRoute(router, match); const preload = !!options[4]; const plannedCacheMatch = preload ? router._cache.get(match.id) : void 0; let reload = false; let reloadFailure; try { let configured; if (match.status === "success") { configured = route.options.shouldReload; if (typeof configured === "function") configured = configured(getLoaderContext(router, lane, match, route, options[0], semanticParent, preload)); if (!options[2]()) { options[0].abort(); reloadFailure = [CANCELED]; } } if (!reloadFailure) if (match.status !== "success") reload = true; else { const staleAge = options[4] || match.preload ? route.options.preloadStaleTime ?? router.options.defaultPreloadStaleTime ?? 3e4 : route.options.staleTime ?? router.options.defaultStaleTime ?? 0; reload = !!(match.invalid || configured || configured === void 0 && Date.now() - match.updatedAt >= staleAge && (options[6] || match.cause === "enter" || options[3].some((candidate) => candidate.routeId === match.routeId && candidate.id !== match.id))); } } catch (cause) { match.invalid = true; releaseFlight(router, match); reloadFailure = normalizeLaneError(route, cause, options); } const routeLoader = route.options.loader; const loader = typeof routeLoader === "function" ? routeLoader : routeLoader?.handler; const background = !!(routeLoader && reload && match.status === "success" && !preload && !options[5] && ((typeof routeLoader === "function" ? void 0 : routeLoader?.staleReloadMode) ?? router.options.defaultStaleReloadMode) !== "blocking"); const loaded = reload && (!preload || route.options.preload !== false); const blocking = loaded && !background && (match.status !== "success" || !!routeLoader); if (loaded && !routeLoader) { match.invalid = false; match.updatedAt = Date.now(); } let donor = loaded && routeLoader ? router._flights?.get(match.id) : void 0; if (donor === match._flight) donor = void 0; else if (donor) donor[2]++; if (blocking) { const acceptedFlight = match._flight; match._flight = donor; releaseOwnedFlight(router, match.id, acceptedFlight)?.abort(); if (match.status === "success") match.status = "pending"; options[8]?.(); } if (!loaded) match.isFetching = false; const outcome = (reloadFailure ? Promise.resolve(reloadFailure) : !blocking ? Promise.resolve([SUCCESS, match.loaderData]) : loadResource(router, lane, match, route, loader, semanticParent, preload, options[0])).then((result) => { if (blocking) { settleInto(match, result, preload); if (result[0] === SUCCESS) { if (preload && routeLoader) cacheLoaderMatch(router, match, plannedCacheMatch); match.status = "pending"; } } return result; }); const chunkFailure = waitFor(Promise.resolve().then(() => loadRouteChunk(route)), options[0].signal).then(() => { options[8]?.(); }, (cause) => [index, normalizeLaneError(route, cause, options)]).then((failure) => outcome.then((result) => { if (blocking && !failure && result[0] === SUCCESS && match.status === "pending" && options[2]()) { match.status = "success"; options[8]?.(); } return failure; })); tasks.push([ index, outcome, chunkFailure ]); if (!background) return outcome.then((result) => getParentSnapshot(match, result)); const candidate = { ...match, status: "pending", preload: false, _flight: donor }; match.invalid = false; match.isFetching = "loader"; const backgroundOutcome = loadResource(router, lane, candidate, route, loader, semanticParent, false, options[0]).then((result) => { match.isFetching = false; settleInto(candidate, result, false); return result; }); (lane[2] ??= []).push([ index, backgroundOutcome, chunkFailure, candidate ]); return backgroundOutcome.then((result) => getParentSnapshot(candidate, result)); } async function getNotFoundBoundary(router, matches, indexed, signal, fallback = 0) { const cause = indexed?.[1][1]; let index = cause?.routeId ? matches.findIndex((match) => match.routeId === cause.routeId) : indexed?.[0] ?? matches.length - 1; if (index < 0) index = 0; for (let i = index; i >= 0; i--) { const route = getRoute(router, matches[i]); const loading = loadRouteChunk(route, false); if (loading) try { await waitFor(loading, signal); } catch (cause) { if (cause === signal) throw cause; } if (route.options.notFoundComponent) return i; } return cause?.routeId ? index : fallback; } function discardBackground(router, lane) { if (lane[2]) { transferMatchResources(router, lane[2].map((task) => task[3])); lane[2] = void 0; } } async function settleTasks(tasks, serialFailure, redirectTasks, gate) { let loaderFailure; try { await Promise.all(tasks.map((task) => task[1].then(async (outcome) => { const taskIndex = task[0]; if (gate && taskIndex >= await gate) return; if (outcome[0] >= REDIRECTED) throw [taskIndex, outcome]; if (!loaderFailure && outcome[0] !== SUCCESS) { loaderFailure = [taskIndex, outcome]; await Promise.all((redirectTasks ?? []).map((nextTask) => { if (nextTask[0] <= taskIndex) return; return nextTask[1].then((nextOutcome) => { if (nextOutcome[0] === REDIRECTED) throw [nextTask[0], nextOutcome]; }); })); } }))); } catch (cause) { return cause; } return serialFailure ?? loaderFailure; } async function reduceLane(router, lane, tasks, controller, redirects, settlement, onReady) { const matches = lane[1]; let failure = await settlement; let redirectLimitExceeded = false; const plannedBoundary = matches.findIndex((match) => match._notFound); const boundaryOf = (found) => found[1][0] === NOT_FOUND ? getNotFoundBoundary(router, matches, found, controller.signal) : found[0]; let readinessEnd = plannedBoundary < 0 ? matches.length : plannedBoundary; if ((failure?.[1][0] ?? 0) >= REDIRECTED) readinessEnd = 0; else if (failure) { readinessEnd = failure[2] ??= await boundaryOf(failure); for (const task of tasks) { if (task[0] >= readinessEnd) break; const outcome = await task[1]; if (outcome[0] !== SUCCESS && outcome[0] < REDIRECTED && !("loaderData" in matches[task[0]])) { failure = [task[0], outcome]; readinessEnd = failure[2] = await boundaryOf(failure); break; } } } for (const task of tasks) { if (task[0] >= readinessEnd) break; const chunkFailure = await task[2]; if (!chunkFailure) continue; failure = chunkFailure; break; } if ((failure?.[1][0] ?? 0) >= REDIRECTED) { const outcome = failure[1]; if (outcome[0] !== REDIRECTED || outcome[1].options.reloadDocument || redirects < 20) { discardBackground(router, lane); return outcome; } redirectLimitExceeded = true; failure = [0, [ERROR, /* @__PURE__ */ new Error("Too many redirects")]]; } const boundary = failure ? failure[2] ?? await boundaryOf(failure) : plannedBoundary; if (boundary >= 0) { const outcome = failure?.[1]; const kind = outcome?.[0]; const match = matches[boundary]; const cause = outcome?.[1]; const install = () => { if (outcome) { match._notFound = void 0; if (kind === ERROR) match.status = "error"; else { cause.routeId = match.routeId; if (match.routeId === router.routeTree.id) { match.status = "success"; match._notFound = true; } else match.status = "notFound"; } match.error = cause; match.isFetching = false; } }; install(); try { await waitFor(outcome ? Promise.resolve().then(() => loadRouteChunk(getRoute(router, match), kind === ERROR ? "errorComponent" : "notFoundComponent")) : Promise.all([loadRouteChunk(getRoute(router, match)), loadRouteChunk(getRoute(router, match), "notFoundComponent")]), controller.signal); } catch (cause) { if (cause === controller.signal) { discardBackground(router, lane); return [CANCELED]; } } if (!outcome) { match.status = "success"; onReady?.(); } else if (redirectLimitExceeded) { controller.abort(); await Promise.all([ ...tasks.map((task) => task[1]), ...tasks.map((task) => task[2]), ...(lane[2] ?? []).map((task) => task[1]) ]); discardBackground(router, lane); transferMatchResources(router, matches); install(); } } return lane; } async function projectLane(router, lane, signal, start = 0, end = lane[1].length) { const matches = lane[1]; for (let index = start; index < end; index++) { const match = matches[index]; const routeOptions = getRoute(router, match).options; if (routeOptions.head || routeOptions.scripts) try { const context = { ssr: router.options.ssr, matches, match, params: match.params, loaderData: match.loaderData }; const [head, scripts] = await waitFor(Promise.all([routeOptions.head?.(context), routeOptions.scripts?.(context)]), signal); match.meta = head?.meta; match.links = head?.links; match.headScripts = head?.scripts; match.styles = head?.styles; match.scripts = scripts; } catch (cause) { if (cause === signal) break; console.error(cause); } if (match.status !== "success" || match._notFound) break; } return lane; } async function executeClientLane(router, location, matches, options) { const matched = [location, matches]; let plannedBoundary = matches.findIndex((match) => match._notFound); if (router.options.notFoundMode !== "root" && plannedBoundary >= 0) { const boundary = await getNotFoundBoundary(router, matched[1], void 0, options[0].signal, plannedBoundary); if (boundary !== plannedBoundary) { matches[plannedBoundary]._notFound = void 0; matches[boundary]._notFound = true; } plannedBoundary = boundary; } let end = plannedBoundary < 0 ? matches.length : plannedBoundary + 1; const failure = await contextualize(router, matched, options, end); if (failure) options[5] = true; const tasks = []; const start = options[7] ?? 0; let semanticParent = start ? Promise.resolve(matched[1][start - 1]) : void 0; end = failure?.[0] ?? end; if (failure?.[1][0] === NOT_FOUND) { failure[2] = await getNotFoundBoundary(router, matched[1], failure, options[0].signal); end = Math.min(end, failure[2] + 1); } else if ((failure?.[1][0] ?? 0) >= REDIRECTED) end = 0; for (let index = start; index < end; index++) { if (options[0].signal.aborted) break; semanticParent = createLoaderTask(router, matched, index, tasks, semanticParent, options); } let reduced; try { const reduction = reduceLane(router, matched, tasks, options[0], options[1], settleTasks(tasks, failure, matched[2]), options[8]); if (matched[2]?.length) matched[3] = settleTasks(matched[2], void 0, void 0, reduction.then((foreground) => isControl(foreground) ? 0 : _getRenderedMatches(foreground[1]).length, () => 0)); reduced = await reduction; } catch (cause) { discardBackground(router, matched); throw cause; } if (isControl(reduced)) return reduced; return projectLane(router, reduced, options[0].signal, options[7] === reduced[1].length ? options[7] : 0); } /** * Finds the first route that should show pending UI and its two timing values. * A fallback already on screen remains selected after its route loads, so we * do not jump to a child fallback. Matches put back into pending by invalidation * skip pendingMs, and a route without a usable fallback blocks pending UI for deeper routes. */ function pendingConfig(router, matches) { const presented = router.stores.matches.get(); for (let index = 0; index < matches.length; index++) { const match = matches[index]; const success = match.status === "success"; const visible = success && presented[index]?.id === match.id && presented[index]?.status === "pending"; if (success && !visible) continue; const route = getRoute(router, match); const delay = visible || match.invalid ? 0 : route.options.pendingMs ?? router.options.defaultPendingMs; const component = route.options.pendingComponent ?? router.options.defaultPendingComponent; return component && typeof delay === "number" && delay !== Infinity ? [ delay, index, route.options.pendingMinMs ?? router.options.defaultPendingMinMs ?? 0, component ] : void 0; } } /** * Waits for `pendingMs`, then presents the complete lane. Rendering applies the * selected boundary cutoff while retaining every match's structural state. * A replacement load for the same match keeps the timer; choosing a different * match resets it. `pendingMinMs` starts after the fallback renders. */ function offerPending(router, tx) { if (router._tx !== tx) return; let session = router._pending; let tookOver = false; const sessionMatchId = session?.[0][3][session[1]]?.id; if (session?.[0] !== tx) if (session && tx[3][session[1]]?.id === sessionMatchId) { session[0] = tx; tookOver = true; } else { clearTimeout(session?.[3]); router._pending = session = void 0; } const config = pendingConfig(router, tx[3]); if (!config) return; const [delay, boundary, min, component] = config; const matchId = tx[3][boundary].id; if (!session || session[1] !== boundary || sessionMatchId !== matchId) { clearTimeout(session?.[3]); const presented = router.stores.matches.get()[boundary]; const visible = presented?.id === matchId && presented.status === "pending"; router._pending = session = [ tx, boundary, visible ? Date.now() + min : tx[4] + delay, void 0, visible ? Promise.resolve(true) : void 0, component ]; } if (session[4] && !tookOver && session[5] === component) return; session[5] = component; if (!session[4]) { clearTimeout(session[3]); const remaining = session[2] - Date.now(); if (remaining > 0) { session[3] = setTimeout(() => { offerPending(router, tx); }, remaining); return; } session[2] = 0; } const offered = tx[3].map((match) => ({ ...match, _flight: void 0 })); offered[boundary].status = "pending"; const ack = router.startTransition(() => router.stores.setMatches(offered), offered, true).then((rendered) => { if (rendered && router._pending === session && session[4] === ack && !session[2]) session[2] = Date.now() + min; return rendered; }); session[4] = ack; } /** * Cancels pending UI timing when its load ends. The ownership check prevents * an older, superseded load from clearing pending UI that a newer load took over. */ function finishPending(router, tx) { const session = router._pending; if (session?.[0] === tx) { clearTimeout(session[3]); router._pending = void 0; } } function publishMatches(router, matches) { router._committed = matches; router.stores.setMatches(matches); } function discardLane(router, lane) { transferMatchResources(router, lane[1]); discardBackground(router, lane); } function commitMatches(router, tx, matches, resolvedPrefix) { const previous = router._committed; const previousCached = router._cache; for (const match of matches) { match.preload = false; if (resolvedPrefix) match._assetEnd = void 0; } const cut = _getRenderedMatches(matches).length; const cached = /* @__PURE__ */ new Map(); const now = Date.now(); for (const match of [...previous, ...previousCached.values()]) { if (match.status !== "success" || matches.some((candidate, index) => candidate.id === match.id && (index < cut || candidate.status === "success"))) continue; const route = getRoute(router, match); if (!route.options.loader || now - match.updatedAt >= (match.preload ? route.options.preloadGcTime ?? router.options.defaultPreloadGcTime ?? 3e5 : route.options.gcTime ?? router.options.defaultGcTime ?? 3e5)) continue; cached.set(match.id, previousCached.get(match.id) === match ? match : { ...match, _flight: void 0, isFetching: false, context: {} }); } tx[3] = []; router._cache = cached; publishMatches(router, matches); transferMatchResources(router, [...previousCached.values(), ...previous], [...matches, ...cached.values()]); runRouteLifecycle(router, previous, matches, () => router._tx === tx); } function commitRefreshMatches(router, tx, matches, checkpoint) { const previous = router._committed; const previousCached = router._cache; for (const match of matches) match.preload = false; const cached = /* @__PURE__ */ new Map(); tx[3] = []; router._cache = cached; checkpoint.previousMatches = previous; checkpoint.previousCache = previousCached; checkpoint.published = true; publishMatches(router, matches); if (!checkpoint.published || router._tx !== tx) return; runRouteLifecycle(router, previous, matches, () => router._tx === tx); } function settlePublication(router, checkpoint) { if (!checkpoint.published) return; checkpoint.published = false; transferMatchResources(router, [...checkpoint.previousCache.values(), ...checkpoint.previousMatches], [...router._cache.values(), ...router._committed]); } function rollbackPublication(router, tx, lane, checkpoint) { if (!checkpoint.published || router._tx !== tx || router._committed !== lane[1]) { settlePublication(router, checkpoint); return false; } const discarded = [...router._cache.values(), ...router._committed]; const restored = [...checkpoint.previousCache.values(), ...checkpoint.previousMatches]; router._cache = checkpoint.previousCache; router._committed = checkpoint.previousMatches; checkpoint.published = false; for (const match of discarded) if (!restored.includes(match) && match._flight && router._flights?.get(match.id) === match._flight) router._flights.delete(match.id); finishPending(router, tx); router.batch(() => { router.stores.status.set("idle"); router.stores.setMatches(checkpoint.previousPresentation); }); tx[0].abort(); transferMatchResources(router, discarded, restored); discardBackground(router, lane); if (router._tx === tx && router._commitPromise === checkpoint.commitPromise) { router._commitPromise?.resolve(); router._commitPromise = void 0; } return true; } async function transitionRefresh(router, tx, lane, changeInfo) { const checkpoint = { previousMatches: router._committed, previousPresentation: tx[6]?.[0] ?? router.stores.matches.get(), previousCache: router._cache, commitPromise: router._commitPromise, published: false }; const commit = () => { finishPending(router, tx); router._rollbackRefresh = rollback; commitRefreshMatches(router, tx, lane[1], checkpoint); if (!checkpoint.published || router._tx !== tx) return; router.emit({ type: "onLoad", ...changeInfo }); if (router._tx === tx) router.emit({ type: "onBeforeRouteMount", ...changeInfo }); }; const rollback = () => { if (router._rollbackRefresh === rollback) router._rollbackRefresh = void 0; const restored = rollbackPublication(router, tx, lane, checkpoint); router._cancelTransition?.(); return restored; }; try { const rendered = await router.startTransition(commit, lane[1]); if (router._rollbackRefresh === rollback) router._rollbackRefresh = void 0; if (checkpoint.published) { const handoff = tx[6]?.[1]; if (handoff && router._handoff === handoff) handoff[1](); if (router._tx === tx) tx[6] = void 0; } settlePublication(router, checkpoint); return rendered; } catch (cause) { if (rollback()) return; throw cause; } } async function awaitCurrent(router, owner) { let current = router._tx; while (current && current !== owner) { await current[5]; if (router._tx === current) return; current = router._tx; } } async function followRedirect(router, tx, redirect) { await router.navigate({ ...redirect.options, replace: true, ignoreBlocker: true, _redirects: tx[1] + 1 }); } function restoreCommitted(router, tx) { finishPending(router, tx); tx[0].abort(); transferMatchResources(router, tx[3]); tx[3] = []; if (router._tx !== tx) return; router.batch(() => { router.stores.status.set("idle"); router.stores.setMatches(router._committed); }); if (router._tx === tx) { router._commitPromise?.resolve(); router._commitPromise = void 0; } } async function runBackground(router, tx, base, tasks, settlement) { const next = base.map((match) => ({ ...match })); acquireMatchResources(next); for (const task of tasks) { releaseFlight(router, next[task[0]]); next[task[0]] = task[3]; } const lane = [tx[2], next]; let reduced; try { reduced = await reduceLane(router, lane, tasks, tx[0], tx[1], settlement); } catch (cause) { transferMatchResources(router, next); throw cause; } if (isControl(reduced)) { transferMatchResources(router, next); if (reduced[0] === REDIRECTED && router._tx === tx && router._committed === base) await followRedirect(router, tx, reduced[1]); return; } const projected = await projectLane(router, reduced, tx[0].signal); if (router._tx !== tx || router._committed !== base) { transferMatchResources(router, projected[1]); return; } for (const match of projected[1]) { const cached = router._cache.get(match.id); if (cached?._flight && cached._flight === match._flight) { router._cache.delete(match.id); releaseFlight(router, cached); } } publishMatches(router, projected[1]); transferMatchResources(router, base, projected[1]); } async function runClientTransaction(router, tx, forceStaleReload, onReady, sync, resolvedPrefix, adopted, retained) { const options = [ tx[0], tx[1], () => router._tx === tx && !!tx[3].length, router._committed, void 0, sync, forceStaleReload, resolvedPrefix, onReady ]; let result; try { result = adopted ? await adopted[2] : await executeClientLane(router, tx[2], tx[3], options); } finally { if (retained) discardPreload(router, retained); } if (adopted && router._tx === tx && (isControl(result) && result[0] === CANCELED || !isControl(result) && result[1].some((match) => match.status !== "success" || match._notFound))) { const donors = tx[3]; tx[3] = []; transferMatchResources(router, donors); tx[0].abort(); if (router._tx !== tx) return; const controller = new AbortController(); tx[0] = options[0] = controller; tx[3] = router.matchRoutes(tx[2], { _controller: controller }); acquireMatchResources(tx[3]); result = await executeClientLane(router, tx[2], tx[3], options); } if (isControl(result)) { if (result[0] === REDIRECTED && router._tx === tx) { finishPending(router, tx); transferMatchResources(router, tx[3]); tx[3] = []; if (router._tx === tx) { if (process.env.NODE_ENV !== "production" && tx[6]) router._refreshNextLoad = true; await followRedirect(router, tx, result[1]); } } else restoreCommitted(router, tx); return; } const pending = router._pending; if (pending?.[0] === tx) { /** * Loading finished, so cancel any pending reveal. If the fallback rendered, * wait out the rest of `pendingMinMs` before replacing it. If it never * rendered, there is no minimum wait; if another load took it over, that * load owns the deadline. */ clearTimeout(pending[3]); if (pending[4]) { const signal = tx[0].signal; let rendered = false; try { rendered = await waitFor(pending[4], signal); } catch (cause) { if (cause !== signal) throw cause; } if (rendered && router._pending === pending && pending[0] === tx) { const remaining = pending[2] - Date.now(); if (remaining > 0) { try { await waitFor(new Promise((resolve) => { pending[3] = setTimeout(resolve, remaining); }), signal); } catch {} clearTimeout(pending[3]); } } } } if (router._tx !== tx) { finishPending(router, tx); discardLane(router, result); return; } const toLocation = tx[2]; const changeInfo = getLocationChangeInfo(toLocation, router.stores.resolvedLocation.get()); const background = result[2]; await router.startViewTransition(async () => { if (router._tx !== tx) { discardLane(router, result); return; } const commit = () => { finishPending(router, tx); commitMatches(router, tx, result[1], resolvedPrefix); if (router._tx !== tx) return; router.emit({ type: "onLoad", ...changeInfo }); if (router._tx === tx) router.emit({ type: "onBeforeRouteMount", ...changeInfo }); }; const rendered = process.env.NODE_ENV !== "production" && tx[6] ? await transitionRefresh(router, tx, result, changeInfo) : await router.startTransition(commit, result[1]); if (process.env.NODE_ENV !== "production" && tx[6] && rendered === void 0) return; if (router._tx !== tx) { discardBackground(router, result); return; } if (background?.length) runBackground(router, tx, result[1], background, result[3]).catch(console.error); router.batch(() => { router.stores.resolvedLocation.set(toLocation); router.stores.status.set("idle"); if (router._tx === tx) router.emit({ type: "onResolved", ...changeInfo }); if (rendered && router._tx === tx) router.emit({ type: "onRendered", ...changeInfo }); }); if (router._tx !== tx) return; router._commitPromise?.resolve(); router._commitPromise = void 0; }); } async function loadClientRoute(router, opts) { let rematerialize = false; if (process.env.NODE_ENV !== "production") { router._rollbackRefresh?.(); rematerialize = !!router._refreshNextLoad || !!router._tx?.[6]; } const refreshPresentation = rematerialize ? router.stores.matches.get() : void 0; const previousOwner = router._tx; const resolvedLocation = router.stores.resolvedLocation.get(); const previousLocation = resolvedLocation ?? router.stores.location.get(); const location = router.latestLocation; const pendingLocation = router._pendingLocation; const redirects = pendingLocation?.href === location.href ? pendingLocation._redirects ?? 0 : 0; if (opts?._dedupe && !redirects && previousOwner && !rematerialize && previousOwner[2].href === location.href && router.stores.status.get() === "pending") { await awaitCurrent(router); return; } const handoff = router._handoff; const hydrationController = rematerialize ? void 0 : handoff?.[0](); const preflight = new AbortController(); const previousPreflight = router._preflight; router._preflight = preflight; if (!rematerialize && !hydrationController) handoff?.[1](); previousPreflight?.abort(); if (preflight.signal.aborted || router._tx !== previousOwner) { await awaitCurrent(router, previousOwner); return; } const changeInfo = getLocationChangeInfo(location, resolvedLocation); router.emit({ type: "onBeforeNavigate", ...changeInfo }); if (!preflight.signal.aborted && router._tx === previousOwner) router.emit({ type: "onBeforeLoad", ...changeInfo }); if (preflight.signal.aborted || router._tx !== previousOwner) { preflight.abort(); await awaitCurrent(router, previousOwner); return; } const sameHref = previousLocation.href === location.href; let adopted = router._preloads?.get(location.href); let retained; if (rematerialize && adopted) { router._preloads.delete(location.href); discardPreload(router, adopted); adopted = void 0; if (preflight.signal.aborted || router._tx !== previousOwner) { preflight.abort(); await awaitCurrent(router, previousOwner); return; } } if (adopted && (hydrationController || !samePreloadLane(adopted, router, pendingLocation?.href === location.href ? pendingLocation : location, redirects))) { router._preloads.delete(location.href); retained = adopted; adopted = void 0; } let matches; let controller = preflight; let resolvedPrefix; if (adopted) { controller = adopted[1]; matches = adopted[0]; router._preloads.delete(location.href); } else { try { matches = process.env.NODE_ENV !== "production" && rematerialize ? router.matchRoutes(location, { _controller: preflight, _rematerialize: true }) : router.matchRoutes(location, { _controller: preflight }); acquireMatchResources(matches); } catch (cause) { preflight.abort(); if (retained) discardPreload(router, retained); if (!isRedirect(cause)) { if (process.env.NODE_ENV !== "production" && rematerialize) router._refreshNextLoad = void 0; await awaitCurrent(router); router._commitPromise?.resolve(); router._commitPromise = void 0; return; } await router.navigate({ ...cause.options, replace: true, ignoreBlocker: true }); await awaitCurrent(router, previousOwner); return; } resolvedPrefix = hydrationController ? handoff[1](matches) : void 0; if (resolvedPrefix) controller = hydrationController; else hydrationController?.abort(); } if (router._preflight !== preflight || router._tx !== previousOwner) { preflight.abort(); transferMatchResources(router, matches); await awaitCurrent(router, previousOwner); return; } router._preflight = void 0; const tx = [ controller, redirects, location, matches, Date.now(), Promise.resolve().then(() => runClientTransaction(router, tx, sameHref, () => offerPending(router, tx), opts?.sync, resolvedPrefix, adopted, retained)).catch(() => { if (router._tx === tx) restoreCommitted(router, tx); }) ]; if (process.env.NODE_ENV !== "production" && rematerialize) { tx[6] = [refreshPresentation, handoff]; router._refreshNextLoad = void 0; } router._tx = tx; if (!rematerialize && router._handoff === handoff) router._handoff = void 0; if (previousOwner) { for (const match of router.stores.matches.get()) { if (router._tx !== tx) break; if (match.isFetching) setFetching(router, match, false); } previousOwner[0].abort(); transferMatchResources(router, previousOwner[3]); } if (router._tx !== tx) { transferMatchResources(router, tx[3]); tx[3] = []; await awaitCurrent(router, tx); return; } router.batch(() => { router.stores.status.set("pending"); router.stores.location.set(location); }); offerPending(router, tx); try { await tx[5]; } finally { await awaitCurrent(router, tx); } } async function refreshClientRoute(router) { router._rollbackRefresh?.(); const pending = router._tx; if (pending && !pending[6] && router.stores.status.get() === "pending") { await pending[5]; if (router._tx !== pending) await awaitCurrent(router, pending); } router._flights?.clear(); router.clearCache(); router._refreshNextLoad = true; await loadClientRoute(router, { sync: true }); } function followPreloadRedirect(router, result, location, owner, redirects) { if (result[0] === REDIRECTED && !result[1].options.reloadDocument && router._tx === owner) return preloadClientRoute(router, { ...result[1].options, _fromLocation: location }, redirects + 1); } async function preloadClientRoute(router, opts, redirects = 0) { if (redirects > 20) return; const owner = router._tx; if (process.env.NODE_ENV !== "production" && (router._refreshNextLoad || owner?.[6])) return; const location = opts._builtLocation ?? router.buildLocation(opts); const base = router._committed; const controller = new AbortController(); let matches; let preload; let replaced; try { const pending = router._preloads?.get(location.href); if (pending) { if (samePreloadLane(pending, router, location, redirects)) { const result = await pending[2]; return isControl(result) ? followPreloadRedirect(router, result, location, owner, redirects) : result[1]; } router._preloads.delete(location.href); replaced = pending; } matches = router.matchRoutes(location, { _controller: controller }); acquireMatchResources(matches); const promise = Promise.resolve().then(() => executeClientLane(router, location, matches, [ controller, redirects, () => true, base, true ])).finally(() => { if (replaced) discardPreload(router, replaced); }); preload = [ matches, controller, promise, base, laneInputs(router, location), redirects ]; (router._preloads ??= /* @__PURE__ */ new Map()).set(location.href, preload); const result = await promise; if (router._preloads?.get(location.href) !== preload) return isControl(result) ? void 0 : result[1]; router._preloads.delete(location.href); if (isControl(result)) { controller.abort(); transferMatchResources(router, matches); return followPreloadRedirect(router, result, location, owner, redirects); } transferMatchResources(router, result[1]); controller.abort(); return result[1]; } catch (cause) { if (!preload || router._preloads?.get(location.href) === preload) { if (preload) router._preloads.delete(location.href); controller.abort(); if (matches) transferMatchResources(router, matches); } if (router._tx !== owner) return; if (!isNotFound(cause)) console.error(cause); return; } } async function hydrate(router) { if (process.env.NODE_ENV !== "production" && !window.$_TSR) throw new Error("Invariant failed: Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!"); const tsr = window.$_TSR; const adapters = router.options.serializationAdapters; if (adapters?.length) { tsr.t = new Map(adapters.map((adapter) => [adapter.key, adapter.fromSerializable])); tsr.buffer.forEach((script) => script()); } tsr.initialized = true; const dehydratedRouter = tsr.router; if (process.env.NODE_ENV !== "production" && !dehydratedRouter) throw new Error("Invariant failed: Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!"); router.ssr = { manifest: dehydratedRouter.manifest }; const nonce = document.querySelector("meta[property=\"csp-nonce\"]")?.content; router.options.ssr = { nonce }; const dehydratedMatches = dehydratedRouter.matches; const controller = new AbortController(); const previousPreflight = router._preflight; router._preflight = controller; previousPreflight?.abort(); const retire = (cause) => { if (router._preflight === controller) router._preflight = void 0; controller.abort(cause); return false; }; const isCurrent = () => !router._tx && router._preflight === controller && !controller.signal.aborted || retire(); let location; let candidates; let handoffInputs; try { await waitFor(router.options.hydrate?.(dehydratedRouter.dehydratedData), controller.signal); if (!isCurrent()) return; router.updateLatestLocation(); location = router.latestLocation; router.stores.location.set(location); handoffInputs = laneInputs(router, location); candidates = router.matchRoutes(location, { _controller: controller }); } catch (cause) { retire(cause); if (cause !== controller.signal) throw cause; } if (!isCurrent()) return; const committed = []; let pendingBoundary; let verifiedAssetEnd = 0; const retryFrom = (index) => { verifiedAssetEnd = Math.min(verifiedAssetEnd, index + 1); const removed = committed.splice(index); for (const match of removed) if (getRoute(router, match).options.loader && (match.status === "success" || !match.invalid && "loaderData" in match)) cacheLoaderMatch(router, { ...match, status: "success", error: void 0, preload: true }, router._cache.get(match.id)); transferMatchResources(router, removed); }; const shared = dehydratedMatches.length > candidates.length ? candidates.findIndex((match) => match._notFound) + 1 : dehydratedMatches.length; let isTerminal = false; for (let index = 0; index < shared; index++) { const candidate = candidates[index]; const dehydrated = dehydratedMatches[index]; if (typeof dehydrated.i !== "string" || hydrateSsrMatchId(dehydrated.i) !== candidate.id) { pendingBoundary ??= index; break; } verifiedAssetEnd = index + 1; const route = getRoute(router, candidate); if ("l" in dehydrated || dehydrated.s === "success" && dehydrated.e === void 0 && route.options.loader) candidate.loaderData = dehydrated.l; candidate.status = dehydrated.s; candidate.ssr = dehydrated.ssr; route.options.ssr = candidate.ssr; candidate.updatedAt = dehydrated.u; candidate.error = dehydrated.e; candidate._notFound ||= dehydrated.g; if (candidate.status === "error" || candidate.status === "notFound" || candidate._notFound) { isTerminal = true; committed.push(candidate); if (candidate.ssr === false || candidate.ssr === "data-only") pendingBoundary ??= index; break; } if (candidate.status === "pending") { pendingBoundary ??= index; break; } committed.push(candidate); if (candidate.ssr === "data-only") pendingBoundary ??= index; } let verifiedContextEnd = verifiedAssetEnd; if (!isTerminal && committed.length === shared && shared < candidates.length) pendingBoundary = shared; const chunks = committed.map(async (match) => { try { const route = getRoute(router, match); if (match._notFound) await Promise.all([loadRouteChunk(route), loadRouteChunk(route, "notFoundComponent")]); else await loadRouteChunk(route, match.status === "error" ? "errorComponent" : match.status === "notFound" ? "notFoundComponent" : void 0); return true; } catch { return false; } }); let chunkFailure = 0; try { while (chunkFailure < chunks.length && await waitFor(chunks[chunkFailure], controller.signal)) chunkFailure++; } catch { isCurrent(); return; } if (!isCurrent()) return; if (chunkFailure < committed.length) { verifiedContextEnd = Math.min(verifiedContextEnd, chunkFailure); retryFrom(chunkFailure); } const contextEnd = Math.max(pendingBoundary === committed.length ? committed.length + 1 : committed.length, verifiedContextEnd); for (let index = 0; index < contextEnd; index++) { const match = candidates[index]; const route = getRoute(router, match); const parentContext = candidates[index - 1]?.context ?? router.options.context ?? {}; let routeContext; if (route.options.context) { try { routeContext = match._ctx = route.options.context({ deps: match.loaderDeps, params: match.params, context: parentContext, location, navigate: navigateFrom(router, location), buildLocation: router.buildLocation, cause: match.cause, abortController: controller, preload: false, matches: candidates, routeId: route.id }) || {}; } catch { if (!isCurrent()) return; if (match.status !== "error" && match.status !== "notFound" && !match._notFound) { retryFrom(index); break; } } if (!isCurrent()) return; } match.context = { ...parentContext, ...routeContext, ...committed[index] && dehydratedMatches[index].b }; } await projectLane(router, [location, candidates], controller.signal, 0, verifiedAssetEnd); if (!isCurrent()) return; const needsClientLoad = pendingBoundary !== void 0 || committed.length < shared; const committedMatches = isTerminal && committed.length === shared ? candidates : committed; let presented = needsClientLoad ? candidates : committedMatches; let