import { Action } from "history" ; import type { Location } from "history" ; import type { RouteData } from "./routeData" ; import type { RouteMatch } from "./routeMatching" ; import type { ClientRoute } from "./routes" ; import { matchClientRoutes } from "./routeMatching" ; import invariant from "./invariant" ; export interface CatchData < T = any > { status : number ; statusText : string ; data : T ; } export interface TransitionManagerState { /**
     * The current location the user sees in the browser, during a transition this
     * is the "old page"
     */ location : Location ; /**
     * The current set of route matches the user sees in the browser. During a
     * transition this are the "old matches"
     */ matches : ClientMatch [ ] ; /**
     * Only used When both navigation and fetch loads are pending, the fetch loads
     * may need to use the next matches to load data.
     */ nextMatches ? : ClientMatch [ ] ; /**
     * Data from the loaders that user sees in the browser. During a transition
     * this is the "old" data, unless there are multiple pending forms, in which
     * case this may be updated as fresh data loads complete
     */ loaderData : RouteData ; /**
     * Holds the action data for the latest NormalPostSubmission
     */ actionData ? : RouteData ; /**
     * Tracks the latest, non-keyed pending submission
     */ transition : Transition ; /**
     * Persists thrown response loader/action data. TODO: should probably be an array
     * and keep track of them all and pass the array to ErrorBoundary.
     */ catch ? : CatchData ; /**
     * Persists uncaught loader/action errors. TODO: should probably be an array
     * and keep track of them all and pass the array to ErrorBoundary.
     */ error ? : Error ; /**
     * The id of the nested ErrorBoundary in which to render the error.
     *
     * - undefined: no error
     * - null: error, but no routes have a boundary, use a default
     * - string: actual id
     */ errorBoundaryId : null | string ; /**
     * The id of the nested ErrorBoundary in which to render the error.
     *
     * - undefined: no error
     * - null: error, but no routes have a boundary, use a default
     * - string: actual id
     */ catchBoundaryId : null | string ; fetchers : Map < string , Fetcher > ; } export interface TransitionManagerInit { routes : ClientRoute [ ] ; location : Location ; loaderData : RouteData ; actionData ? : RouteData ; catch ? : CatchData ; error ? : Error ; catchBoundaryId ? : null | string ; errorBoundaryId ? : null | string ; onChange : ( state : TransitionManagerState ) => void ; onRedirect : ( to : string , state ? : any ) => void ; } export interface Submission { action : string ; method : string ; formData : FormData ; encType : string ; key : string ; } export interface ActionSubmission extends Submission { method : "POST" | "PUT" | "PATCH" | "DELETE" ; } export interface LoaderSubmission extends Submission { method : "GET" ; } export type TransitionStates = { Idle : { state : "idle" ; type : "idle" ; submission : undefined ; location : undefined ; } ; SubmittingAction : { state : "submitting" ; type : "actionSubmission" ; submission : ActionSubmission ; location : Location ; } ; SubmittingLoader : { state : "submitting" ; type : "loaderSubmission" ; submission : LoaderSubmission ; location : Location ; } ; LoadingLoaderSubmissionRedirect : { state : "loading" ; type : "loaderSubmissionRedirect" ; submission : LoaderSubmission ; location : Location ; } ; LoadingAction : { state : "loading" ; type : "actionReload" ; submission : ActionSubmission ; location : Location ; } ; LoadingActionRedirect : { state : "loading" ; type : "actionRedirect" ; submission : ActionSubmission ; location : Location ; } ; LoadingFetchActionRedirect : { state : "loading" ; type : "fetchActionRedirect" ; submission : undefined ; location : Location ; } ; LoadingRedirect : { state : "loading" ; type : "normalRedirect" ; submission : undefined ; location : Location ; } ; Loading : { state : "loading" ; type : "normalLoad" ; location : Location ; submission : undefined ; } ; } ; export type Transition = TransitionStates [ keyof TransitionStates ] ; export type Redirects = { Loader : { isRedirect : true ; type : "loader" ; } ; Action : { isRedirect : true ; type : "action" ; } ; LoaderSubmission : { isRedirect : true ; type : "loaderSubmission" ; } ; FetchAction : { isRedirect : true ; type : "fetchAction" ; } ; } ; type FetcherStates < TData = any > = { Idle : { state : "idle" ; type : "init" ; submission : undefined ; data : undefined ; } ; SubmittingAction : { state : "submitting" ; type : "actionSubmission" ; submission : ActionSubmission ; data : undefined ; } ; SubmittingLoader : { state : "submitting" ; type : "loaderSubmission" ; submission : LoaderSubmission ; data : TData | undefined ; } ; ReloadingAction : { state : "loading" ; type : "actionReload" ; submission : ActionSubmission ; data : TData ; } ; Loading : { state : "loading" ; type : "normalLoad" ; submission : undefined ; data : TData | undefined ; } ; Done : { state : "idle" ; type : "done" ; submission : undefined ; data : TData ; } ; } ; export type Fetcher < TData = any > = FetcherStates < TData > [ keyof FetcherStates < TData > ] ; type ClientMatch = RouteMatch < ClientRoute > ; type DataResult = { match : ClientMatch ; value : TransitionRedirect | Error | any ; } ; type DataRedirectResult = { match : ClientMatch ; value : TransitionRedirect ; } ; type DataErrorResult = { match : ClientMatch ; value : Error ; } ; type DataCatchResult = { match : ClientMatch ; value : CatchValue ; } ; export class CatchValue { constructor ( public status , public statusText , public data ) { } } export type NavigationEvent = { type : "navigation" ; action : Action ; location : Location ; submission ? : Submission ; } ; export type FetcherEvent = { type : "fetcher" ; key : string ; submission ? : Submission ; href : string ; } ; export type DataEvent = NavigationEvent | FetcherEvent ; function isActionSubmission ( submission ) { return [ "POST" , "PUT" , "PATCH" , "DELETE" ] . includes ( submission . method ) ; } function isLoaderSubmission ( submission ) { return submission . method === "GET" ; } interface _Location extends Location { state : { isRedirect : boolean ; type : string ; } | null ; } interface RedirectLocation extends _Location { state : { isRedirect : true ; type : string ; } ; } function isRedirectLocation ( location ) { return ( Boolean ( location . state ) && ( location as RedirectLocation ) . state . isRedirect ) ; } interface LoaderRedirectLocation extends RedirectLocation { state : { isRedirect : true ; type : "loader" ; } ; } function isLoaderRedirectLocation ( location ) { return isRedirectLocation ( location ) && location . state . type === "loader" ; } interface ActionRedirectLocation extends RedirectLocation { state : { isRedirect : true ; type : "action" ; } ; } function isActionRedirectLocation ( location ) { return isRedirectLocation ( location ) && location . state . type === "action" ; } interface FetchActionRedirectLocation extends RedirectLocation { state : { isRedirect : true ; type : "fetchAction" ; } ; } function isFetchActionRedirect ( location ) { return isRedirectLocation ( location ) && location . state . type === "fetchAction" ; } interface LoaderSubmissionRedirectLocation extends RedirectLocation { state : { isRedirect : true ; type : "loaderSubmission" ; } ; } function isLoaderSubmissionRedirectLocation ( location ) { return ( isRedirectLocation ( location ) && location . state . type === "loaderSubmission" ) ; } export class TransitionRedirect { location : string ; constructor ( location ) { this . location = typeof location === "string" ? location : location . pathname + location . search ; } } export const IDLE_TRANSITION = { state : "idle" , submission : undefined , location : undefined , type : "idle" } ; export const IDLE_FETCHER = { state : "idle" , type : "init" , data : undefined , submission : undefined } ; export function createTransitionManager ( init ) { let { routes } = init ; let pendingNavigationController ; let fetchControllers = new Map < string , AbortController > ( ) ; let incrementingLoadId = 0 ; let navigationLoadId = - 1 ; let fetchReloadIds = new Map < string , number > ( ) ; let matches = matchClientRoutes ( routes , init . location ) ; if ( ! matches ) { matches = [ { params : { } , pathname : "" , route : routes [ 0 ] } ] ; } let state = { location : init . location , loaderData : init . loaderData || { } , actionData : init . actionData , catch : init . catch , error : init . error , catchBoundaryId : init . catchBoundaryId || null , errorBoundaryId : init . errorBoundaryId || null , matches , nextMatches : undefined , transition : IDLE_TRANSITION , fetchers : new Map ( ) } ; function update ( updates ) { state = Object . assign ( { } , state , updates ) ; init . onChange ( state ) ; } function getState ( ) { return state ; } function getFetcher < TData = any > ( key ) { return state . fetchers . get ( key ) || IDLE_FETCHER ; } function deleteFetcher ( key ) { if ( fetchControllers . has ( key ) ) abortFetcher ( key ) ; fetchReloadIds . delete ( key ) ; state . fetchers . delete ( key ) ; } async function send ( event ) { switch ( event . type ) { case "navigation" : { let { action , location , submission } = event ; let matches = matchClientRoutes ( routes , location ) ; if ( ! matches ) { matches = [ { params : { } , pathname : "" , route : routes [ 0 ] } ] ; await handleNotFoundNavigation ( location , matches ) ; } else if ( ! submission && isHashChangeOnly ( location ) ) { await handleHashChange ( location , matches ) ; } else if ( action === Action . Pop ) { await handleLoad ( location , matches ) ; } else if ( submission && isActionSubmission ( submission ) ) { await handleActionSubmissionNavigation ( location , submission , matches ) ; } else if ( submission && isLoaderSubmission ( submission ) ) { await handleLoaderSubmissionNavigation ( location , submission , matches ) ; } else if ( isActionRedirectLocation ( location ) ) { await handleActionRedirect ( location , matches ) ; } else if ( isLoaderSubmissionRedirectLocation ( location ) ) { await handleLoaderSubmissionRedirect ( location , matches ) ; } else if ( isLoaderRedirectLocation ( location ) ) { await handleLoaderRedirect ( location , matches ) ; } else if ( isFetchActionRedirect ( location ) ) { await handleFetchActionRedirect ( location , matches ) ; } else { await handleLoad ( location , matches ) ; } navigationLoadId = - 1 ; break ; } case "fetcher" : { let { key , submission , href } = event ; let matches = matchClientRoutes ( routes , href ) ; invariant ( matches , "No matches found" ) ; let match = matches . slice ( - 1 ) [ 0 ] ; if ( fetchControllers . has ( key ) ) abortFetcher ( key ) ; if ( submission && isActionSubmission ( submission ) ) { await handleActionFetchSubmission ( key , submission , match ) ; } else if ( submission && isLoaderSubmission ( submission ) ) { await handleLoaderFetchSubmission ( href , key , submission , match ) ; } else { await handleLoaderFetch ( href , key , match ) ; } break ; } default : { throw new Error ( `Unknown data event type: ${ event . type }` ) ; } } } function dispose ( ) { abortNormalNavigation ( ) ; for ( let [ , controller ] of fetchControllers ) { controller . abort ( ) ; } } async function handleActionFetchSubmission ( key , submission , match ) { let currentFetcher = state . fetchers . get ( key ) ; let fetcher = { state : "submitting" , type : "actionSubmission" , submission , data : currentFetcher ?. data || undefined } ; state . fetchers . set ( key , fetcher ) ; update ( { fetchers : new Map ( state . fetchers ) } ) ; let controller = new AbortController ( ) ; fetchControllers . set ( key , controller ) ; let result = await callAction ( submission , match , controller . signal ) ; if ( controller . signal . aborted ) { return ; } if ( isRedirectResult ( result ) ) { let locationState = { isRedirect : true , type : "fetchAction" } ; init . onRedirect ( result . value . location , locationState ) ; return ; } if ( maybeBailOnError ( match , key , result ) ) { return ; } if ( await maybeBailOnCatch ( match , key , result ) ) { return ; } let loadFetcher = { state : "loading" , type : "actionReload" , data : result . value , submission } ; state . fetchers . set ( key , loadFetcher ) ; update ( { fetchers : new Map ( state . fetchers ) } ) ; let maybeActionErrorResult = isErrorResult ( result ) ? result : undefined ; let maybeActionCatchResult = isCatchResult ( result ) ? result : undefined ; let loadId = ++ incrementingLoadId ; fetchReloadIds . set ( key , loadId ) ; let matchesToLoad = state . nextMatches || state . matches ; let hrefToLoad = createHref ( state . transition . location || state . location ) ; let results = await callLoaders ( state , createUrl ( hrefToLoad ) , matchesToLoad , controller . signal , maybeActionErrorResult , maybeActionCatchResult , submission , match . route . id , loadFetcher ) ; if ( controller . signal . aborted ) { return ; } fetchReloadIds . delete ( key ) ; fetchControllers . delete ( key ) ; let redirect = findRedirect ( results ) ; if ( redirect ) { let locationState = { isRedirect : true , type : "loader" } ; init . onRedirect ( redirect . location , locationState ) ; return ; } let [ error , errorBoundaryId ] = findErrorAndBoundaryId ( results , state . matches , maybeActionErrorResult ) ; let [ catchVal , catchBoundaryId ] = await findCatchAndBoundaryId ( results , state . matches , maybeActionCatchResult ) ; let doneFetcher = { state : "idle" , type : "done" , data : result . value , submission : undefined } ; state . fetchers . set ( key , doneFetcher ) ; let abortedKeys = abortStaleFetchLoads ( loadId ) ; if ( abortedKeys ) { markFetchersDone ( abortedKeys ) ; } let yeetedNavigation = yeetStaleNavigationLoad ( loadId ) ; if ( yeetedNavigation ) { let { transition } = state ; invariant ( transition . state === "loading" , "Expected loading transition" ) ; update ( { location : transition . location , matches : state . nextMatches , error , errorBoundaryId , catch : catchVal , catchBoundaryId , loaderData : makeLoaderData ( state , results , matchesToLoad ) , actionData : transition . type === "actionReload" ? state . actionData : undefined , transition : IDLE_TRANSITION , fetchers : new Map ( state . fetchers ) } ) ; } else { update ( { fetchers : new Map ( state . fetchers ) , error , errorBoundaryId , loaderData : makeLoaderData ( state , results , matchesToLoad ) } ) ; } } function yeetStaleNavigationLoad ( landedId ) { let isLoadingNavigation = state . transition . state === "loading" ; if ( isLoadingNavigation && navigationLoadId < landedId ) { abortNormalNavigation ( ) ; return true ; } return false ; } function markFetchersDone ( keys ) { for ( let key of keys ) { let fetcher = getFetcher ( key ) ; let doneFetcher = { state : "idle" , type : "done" , data : fetcher . data , submission : undefined } ; state . fetchers . set ( key , doneFetcher ) ; } } function abortStaleFetchLoads ( landedId ) { let yeetedKeys = [ ] ; for ( let [ key , id ] of fetchReloadIds ) { if ( id < landedId ) { let fetcher = state . fetchers . get ( key ) ; invariant ( fetcher , `Expected fetcher: ${ key }` ) ; if ( fetcher . state === "loading" ) { abortFetcher ( key ) ; fetchReloadIds . delete ( key ) ; yeetedKeys . push ( key ) ; } } } return yeetedKeys . length ? yeetedKeys : false ; } async function handleLoaderFetchSubmission ( href , key , submission , match ) { let currentFetcher = state . fetchers . get ( key ) ; let fetcher = { state : "submitting" , type : "loaderSubmission" , submission , data : currentFetcher ?. data || undefined } ; state . fetchers . set ( key , fetcher ) ; update ( { fetchers : new Map ( state . fetchers ) } ) ; let controller = new AbortController ( ) ; fetchControllers . set ( key , controller ) ; let result = await callLoader ( match , createUrl ( href ) , controller . signal ) ; fetchControllers . delete ( key ) ; if ( controller . signal . aborted ) { return ; } if ( isRedirectResult ( result ) ) { let locationState = { isRedirect : true , type : "loader" } ; init . onRedirect ( result . value . location , locationState ) ; return ; } if ( maybeBailOnError ( match , key , result ) ) { return ; } if ( await maybeBailOnCatch ( match , key , result ) ) { return ; } let doneFetcher = { state : "idle" , type : "done" , data : result . value , submission : undefined } ; state . fetchers . set ( key , doneFetcher ) ; update ( { fetchers : new Map ( state . fetchers ) } ) ; } async function handleLoaderFetch ( href , key , match ) { let currentFetcher = state . fetchers . get ( key ) ; let fetcher = { state : "loading" , type : "normalLoad" , submission : undefined , data : currentFetcher ?. data || undefined } ; state . fetchers . set ( key , fetcher ) ; update ( { fetchers : new Map ( state . fetchers ) } ) ; let controller = new AbortController ( ) ; fetchControllers . set ( key , controller ) ; let result = await callLoader ( match , createUrl ( href ) , controller . signal ) ; if ( controller . signal . aborted ) return ; fetchControllers . delete ( key ) ; if ( isRedirectResult ( result ) ) { let locationState = { isRedirect : true , type : "loader" } ; init . onRedirect ( result . value . location , locationState ) ; return ; } if ( maybeBailOnError ( match , key , result ) ) { return ; } if ( await maybeBailOnCatch ( match , key , result ) ) { return ; } let doneFetcher = { state : "idle" , type : "done" , data : result . value , submission : undefined } ; state . fetchers . set ( key , doneFetcher ) ; update ( { fetchers : new Map ( state . fetchers ) } ) ; } async function maybeBailOnCatch ( match , key , result ) { if ( isCatchResult ( result ) ) { let catchBoundaryId = findNearestCatchBoundary ( match , state . matches ) ; state . fetchers . delete ( key ) ; update ( { transition : IDLE_TRANSITION , fetchers : new Map ( state . fetchers ) , catch : { data : result . value . data , status : result . value . status , statusText : result . value . statusText } , catchBoundaryId } ) ; return true ; } return false ; } function maybeBailOnError ( match , key , result ) { if ( isErrorResult ( result ) ) { let errorBoundaryId = findNearestBoundary ( match , state . matches ) ; state . fetchers . delete ( key ) ; update ( { fetchers : new Map ( state . fetchers ) , error : result . value , errorBoundaryId } ) ; return true ; } return false ; } async function handleNotFoundNavigation ( location , matches ) { abortNormalNavigation ( ) ; let transition = { state : "loading" , type : "normalLoad" , submission : undefined , location } ; update ( { transition , nextMatches : matches } ) ; await Promise . resolve ( ) ; let catchBoundaryId = findNearestCatchBoundary ( matches [ 0 ] , matches ) ; update ( { location , matches , catch : { data : null , status : 404 , statusText : "Not Found" } , catchBoundaryId , transition : IDLE_TRANSITION } ) ; } async function handleActionSubmissionNavigation ( location , submission , matches ) { abortNormalNavigation ( ) ; let transition = { state : "submitting" , type : "actionSubmission" , submission , location } ; update ( { transition , nextMatches : matches } ) ; let controller = new AbortController ( ) ; pendingNavigationController = controller ; if ( ! isIndexRequestAction ( submission . action ) && matches [ matches . length - 1 ] . route . id . endsWith ( "/index" ) ) { matches = matches . slice ( 0 , - 1 ) ; } let leafMatch = matches . slice ( - 1 ) [ 0 ] ; let result = await callAction ( submission , leafMatch , controller . signal ) ; if ( controller . signal . aborted ) { return ; } if ( isRedirectResult ( result ) ) { let locationState = { isRedirect : true , type : "action" } ; init . onRedirect ( result . value . location , locationState ) ; return ; } if ( isCatchResult ( result ) ) { let [ catchVal , catchBoundaryId ] = await findCatchAndBoundaryId ( [ result ] , matches , result ) ; update ( { transition : IDLE_TRANSITION , catch : catchVal , catchBoundaryId } ) ; return ; } let loadTransition = { state : "loading" , type : "actionReload" , submission , location } ; update ( { transition : loadTransition , actionData : { [ leafMatch . route . id ] : result . value } } ) ; await loadPageData ( location , matches , submission , leafMatch . route . id , result ) ; } async function handleLoaderSubmissionNavigation ( location , submission , matches ) { abortNormalNavigation ( ) ; let transition = { state : "submitting" , type : "loaderSubmission" , submission , location } ; update ( { transition , nextMatches : matches } ) ; await loadPageData ( location , matches , submission ) ; } async function handleHashChange ( location , matches ) { abortNormalNavigation ( ) ; let transition = { state : "loading" , type : "normalLoad" , submission : undefined , location } ; update ( { transition , nextMatches : matches } ) ; await Promise . resolve ( ) ; update ( { location , matches , transition : IDLE_TRANSITION } ) ; } async function handleLoad ( location , matches ) { abortNormalNavigation ( ) ; let transition = { state : "loading" , type : "normalLoad" , submission : undefined , location } ; update ( { transition , nextMatches : matches } ) ; await loadPageData ( location , matches ) ; } async function handleLoaderRedirect ( location , matches ) { abortNormalNavigation ( ) ; let transition = { state : "loading" , type : "normalRedirect" , submission : undefined , location } ; update ( { transition , nextMatches : matches } ) ; await loadPageData ( location , matches ) ; } async function handleLoaderSubmissionRedirect ( location , matches ) { abortNormalNavigation ( ) ; invariant ( state . transition . type === "loaderSubmission" , `Unexpected transition: ${ JSON . stringify ( state . transition ) }` ) ; let { submission } = state . transition ; let transition = { state : "loading" , type : "loaderSubmissionRedirect" , submission , location : location } ; update ( { transition , nextMatches : matches } ) ; await loadPageData ( location , matches , submission ) ; } async function handleFetchActionRedirect ( location , matches ) { abortNormalNavigation ( ) ; let transition = { state : "loading" , type : "fetchActionRedirect" , submission : undefined , location } ; update ( { transition , nextMatches : matches } ) ; await loadPageData ( location , matches ) ; } async function handleActionRedirect ( location , matches ) { abortNormalNavigation ( ) ; invariant ( state . transition . type === "actionSubmission" || state . transition . type === "actionReload" , `Unexpected transition: ${ JSON . stringify ( state . transition ) }` ) ; let { submission } = state . transition ; let transition = { state : "loading" , type : "actionRedirect" , submission , location } ; update ( { transition , nextMatches : matches } ) ; await loadPageData ( location , matches , submission ) ; } function isHashChangeOnly ( location ) { return ( createHref ( state . location ) === createHref ( location ) && state . location . hash !== location . hash ) ; } async function loadPageData ( location , matches , submission ? , submissionRouteId ? , actionResult ? ) { let maybeActionErrorResult = actionResult && isErrorResult ( actionResult ) ? actionResult : undefined ; let maybeActionCatchResult = actionResult && isCatchResult ( actionResult ) ? actionResult : undefined ; let controller = new AbortController ( ) ; pendingNavigationController = controller ; navigationLoadId = ++ incrementingLoadId ; let results = await callLoaders ( state , createUrl ( createHref ( location ) ) , matches , controller . signal , maybeActionErrorResult , maybeActionCatchResult , submission , submissionRouteId ) ; if ( controller . signal . aborted ) { return ; } let redirect = findRedirect ( results ) ; if ( redirect ) { if ( state . transition . type === "actionReload" ) { let locationState = { isRedirect : true , type : "action" } ; init . onRedirect ( redirect . location , locationState ) ; } else if ( state . transition . type === "loaderSubmission" ) { let locationState = { isRedirect : true , type : "loaderSubmission" } ; init . onRedirect ( redirect . location , locationState ) ; } else { let locationState = { isRedirect : true , type : "loader" } ; init . onRedirect ( redirect . location , locationState ) ; } return ; } let [ error , errorBoundaryId ] = findErrorAndBoundaryId ( results , matches , maybeActionErrorResult ) ; let [ catchVal , catchBoundaryId ] = await findCatchAndBoundaryId ( results , matches , maybeActionErrorResult ) ; let abortedIds = abortStaleFetchLoads ( navigationLoadId ) ; if ( abortedIds ) { markFetchersDone ( abortedIds ) ; } update ( { location , matches , error , errorBoundaryId , catch : catchVal , catchBoundaryId , loaderData : makeLoaderData ( state , results , matches ) , actionData : state . transition . type === "actionReload" ? state . actionData : undefined , transition : IDLE_TRANSITION , fetchers : abortedIds ? new Map ( state . fetchers ) : state . fetchers } ) ; } function abortNormalNavigation ( ) { pendingNavigationController ?. abort ( ) ; } function abortFetcher ( key ) { let controller = fetchControllers . get ( key ) ; invariant ( controller , `Expected fetch controller: ${ key }` ) ; controller . abort ( ) ; fetchControllers . delete ( key ) ; } return { send , getState , getFetcher , deleteFetcher , dispose , get _internalFetchControllers ( ) { return fetchControllers ; } } ; } function isIndexRequestAction ( action ) { let indexRequest = false ; let searchParams = new URLSearchParams ( action . split ( "?" , 2 ) [ 1 ] || "" ) ; for ( let param of searchParams . getAll ( "index" ) ) { if ( ! param ) { indexRequest = true ; } } return indexRequest ; } async function callLoaders ( state , url , matches , signal , actionErrorResult ? , actionCatchResult ? , submission ? , submissionRouteId ? , fetcher ? ) { let matchesToLoad = filterMatchesToLoad ( state , url , matches , actionErrorResult , actionCatchResult , submission , submissionRouteId , fetcher ) ; return Promise . all ( matchesToLoad . map ( match => callLoader ( match , url , signal ) ) ) ; } async function callLoader ( match , url , signal ) { invariant ( match . route . loader , `Expected loader for ${ match . route . id }` ) ; try { let { params } = match ; let value = await match . route . loader ( { params , url , signal } ) ; return { match , value } ; } catch ( error ) { return { match , value : error } ; } } async function callAction ( submission , match , signal ) { if ( ! match . route . action ) { throw new Error ( `Route "${ match . route . id }" does not have an action, but you are trying ` + `to submit to it. To fix this, please add an \`action\` function to the route` ) ; } try { let value = await match . route . action ( { url : createUrl ( submission . action ) , params : match . params , submission , signal } ) ; return { match , value } ; } catch ( error ) { return { match , value : error } ; } } function filterMatchesToLoad ( state , url , matches , actionErrorResult ? , actionCatchResult ? , submission ? , submissionRouteId ? , fetcher ? ) { if ( submissionRouteId && ( actionCatchResult || actionErrorResult ) ) { let foundProblematicRoute = false ; matches = matches . filter ( match => { if ( foundProblematicRoute ) { return false ; } if ( match . route . id === submissionRouteId ) { foundProblematicRoute = true ; return false ; } return true ; } ) ; } let isNew = ( match , index ) => { if ( ! state . matches [ index ] ) return true ; return match . route . id !== state . matches [ index ] . route . id ; } ; let matchPathChanged = ( match , index ) => { return ( state . matches [ index ] . pathname !== match . pathname || ( state . matches [ index ] . route . path ?. endsWith ( "*" ) && state . matches [ index ] . params [ "*" ] !== match . params [ "*" ] ) ) ; } ; let filterByRouteProps = ( match , index ) => { if ( ! match . route . loader ) { return false ; } if ( isNew ( match , index ) || matchPathChanged ( match , index ) ) { return true ; } if ( match . route . shouldReload ) { let prevUrl = createUrl ( createHref ( state . location ) ) ; return match . route . shouldReload ( { prevUrl , url , submission , params : match . params } ) ; } return true ; } ; let isInRootCatchBoundary = state . matches . length === 1 ; if ( isInRootCatchBoundary ) { return matches . filter ( match => ! ! match . route . loader ) ; } if ( fetcher ?. type === "actionReload" ) { return matches . filter ( filterByRouteProps ) ; } else if ( state . transition . type === "actionReload" || state . transition . type === "actionRedirect" || createHref ( url ) === createHref ( state . location ) || url . searchParams . toString ( ) !== state . location . search ) { return matches . filter ( filterByRouteProps ) ; } return matches . filter ( ( match , index , arr ) => { if ( ( actionErrorResult || actionCatchResult ) && arr . length - 1 === index ) { return false ; } return ( match . route . loader && ( isNew ( match , index ) || matchPathChanged ( match , index ) ) ) ; } ) ; } function isRedirectResult ( result ) { return result . value instanceof TransitionRedirect ; } function createHref ( location ) { return location . pathname + location . search ; } function findRedirect ( results ) { for ( let result of results ) { if ( isRedirectResult ( result ) ) { return result . value ; } } return null ; } async function findCatchAndBoundaryId ( results , matches , actionCatchResult ? ) { let loaderCatchResult ; for ( let result of results ) { if ( isCatchResult ( result ) ) { loaderCatchResult = result ; break ; } } let extractCatchData = async ( res ) => ( { status : res . status , statusText : res . statusText , data : res . data } ) ; if ( actionCatchResult && loaderCatchResult ) { let boundaryId = findNearestCatchBoundary ( loaderCatchResult . match , matches ) ; return [ await extractCatchData ( actionCatchResult . value ) , boundaryId ] ; } if ( loaderCatchResult ) { let boundaryId = findNearestCatchBoundary ( loaderCatchResult . match , matches ) ; return [ await extractCatchData ( loaderCatchResult . value ) , boundaryId ] ; } return [ undefined , undefined ] ; } function findErrorAndBoundaryId ( results , matches , actionErrorResult ? ) { let loaderErrorResult ; for ( let result of results ) { if ( isErrorResult ( result ) ) { loaderErrorResult = result ; break ; } } if ( actionErrorResult && loaderErrorResult ) { let boundaryId = findNearestBoundary ( loaderErrorResult . match , matches ) ; return [ actionErrorResult . value , boundaryId ] ; } if ( actionErrorResult ) { let boundaryId = findNearestBoundary ( actionErrorResult . match , matches ) ; return [ actionErrorResult . value , boundaryId ] ; } if ( loaderErrorResult ) { let boundaryId = findNearestBoundary ( loaderErrorResult . match , matches ) ; return [ loaderErrorResult . value , boundaryId ] ; } return [ undefined , undefined ] ; } function findNearestCatchBoundary ( matchWithError , matches ) { let nearestBoundaryId = null ; for ( let match of matches ) { if ( match . route . CatchBoundary ) { nearestBoundaryId = match . route . id ; } if ( match === matchWithError ) { break ; } } return nearestBoundaryId ; } function findNearestBoundary ( matchWithError , matches ) { let nearestBoundaryId = null ; for ( let match of matches ) { if ( match . route . ErrorBoundary ) { nearestBoundaryId = match . route . id ; } if ( match === matchWithError ) { break ; } } return nearestBoundaryId ; } function makeLoaderData ( state , results , matches ) { let newData = { } ; for ( let { match , value } of results ) { newData [ match . route . id ] = value ; } let loaderData = { } ; for ( let { route } of matches ) { let value = newData [ route . id ] !== undefined ? newData [ route . id ] : state . loaderData [ route . id ] ; if ( value !== undefined ) { loaderData [ route . id ] = value ; } } return loaderData ; } function isCatchResult ( result ) { return result . value instanceof CatchValue ; } function isErrorResult ( result ) { return result . value instanceof Error ; } function createUrl ( href ) { return new URL ( href , window . location . origin ) ; }