import { Action , AnyAction , isAllOf , isPlainObject } from '@reduxjs/toolkit' ; import { QueryStatus } from '@reduxjs/toolkit/query' ; import { QueryInfo , RtkQueryMonitorState , RtkQueryApiState , RTKQuerySubscribers , RtkQueryTag , RTKStatusFlags , RtkQueryState , MutationInfo , ApiStats , QueryTally , RtkQueryProvided , ApiTimings , QueryTimings , SelectorsSource , RtkMutationState , RtkResourceInfo , RtkRequest , RtkRequestTiming , } from '../types' ; import { missingTagId } from '../monitor-config' ; import { Comparator , compareJSONPrimitive } from './comparators' ; import { emptyArray } from './object' ; import { formatMs } from './formatters' ; import * as statistics from './statistics' ; const rtkqueryApiStateKeys = [ 'queries' , 'mutations' , 'config' , 'provided' , 'subscriptions' , ] ; param val returns { boolean } export function isApiSlice ( val ) { if ( ! isPlainObject ( val ) ) { return false ; } for ( let i = 0 , len = rtkqueryApiStateKeys . length ; i < len ; i ++ ) { if ( ! isPlainObject ( ( val as Record < string , unknown > ) [ rtkqueryApiStateKeys [ i ] ] ) ) { return false ; } } return true ; } param reduxStoreState returns export function getApiStatesOf ( reduxStoreState ) { if ( ! isPlainObject ( reduxStoreState ) ) { return null ; } const output = { } ; const keys = Object . keys ( reduxStoreState ) ; for ( let i = 0 , len = keys . length ; i < len ; i ++ ) { const key = keys [ i ] ; const value = ( reduxStoreState as Record < string , unknown > ) [ key ] ; if ( isApiSlice ( value ) ) { output [ key ] = value ; } } if ( Object . keys ( output ) . length === 0 ) { return null ; } return output ; } export function extractAllApiQueries ( apiStatesByReducerPath ) { if ( ! apiStatesByReducerPath ) { return emptyArray ; } const reducerPaths = Object . keys ( apiStatesByReducerPath ) ; const output = [ ] ; for ( let i = 0 , len = reducerPaths . length ; i < len ; i ++ ) { const reducerPath = reducerPaths [ i ] ; const api = apiStatesByReducerPath [ reducerPath ] ; const queryKeys = Object . keys ( api . queries ) ; for ( let j = 0 , qKeysLen = queryKeys . length ; j < qKeysLen ; j ++ ) { const queryKey = queryKeys [ j ] ; const state = api . queries [ queryKey ] ; if ( state ) { output . push ( { type : 'query' , reducerPath , queryKey , state , } ) ; } } } return output ; } export function extractAllApiMutations ( apiStatesByReducerPath ) { if ( ! apiStatesByReducerPath ) { return emptyArray ; } const reducerPaths = Object . keys ( apiStatesByReducerPath ) ; const output = [ ] ; for ( let i = 0 , len = reducerPaths . length ; i < len ; i ++ ) { const reducerPath = reducerPaths [ i ] ; const api = apiStatesByReducerPath [ reducerPath ] ; const mutationKeys = Object . keys ( api . mutations ) ; for ( let j = 0 , mKeysLen = mutationKeys . length ; j < mKeysLen ; j ++ ) { const queryKey = mutationKeys [ j ] ; const state = api . mutations [ queryKey ] ; if ( state ) { output . push ( { type : 'mutation' , reducerPath , queryKey , state , } ) ; } } } return output ; } function computeQueryTallyOf ( queryState ) { const queries = Object . values ( queryState ) ; const output = { count : 0 , } ; for ( let i = 0 , len = queries . length ; i < len ; i ++ ) { const query = queries [ i ] ; if ( query ) { output . count ++ ; if ( ! output [ query . status ] ) { output [ query . status ] = 1 ; } else { ( output [ query . status ] as number ) ++ ; } } } return output ; } function tallySubscriptions ( subsState ) { const subsOfQueries = Object . values ( subsState ) ; let output = 0 ; for ( let i = 0 , len = subsOfQueries . length ; i < len ; i ++ ) { const subsOfQuery = subsOfQueries [ i ] ; if ( subsOfQuery ) { output += Object . keys ( subsOfQuery ) . length ; } } return output ; } function computeRtkQueryRequests ( type , api , sortedActions , currentStateIndex ) { const requestById = { } ; const matcher = type === 'queries' ? matchesExecuteQuery ( api . config . reducerPath ) : matchesExecuteMutation ( api . config . reducerPath ) ; for ( let i = 0 , len = sortedActions . length ; i < len && i <= currentStateIndex ; i ++ ) { const action = sortedActions [ i ] ; if ( matcher ( action ) ) { let requestRecord = requestById [ action . meta . requestId ] ; if ( ! requestRecord ) { const queryCacheKey = ( action . meta as Record < string , any > ) ?. arg ?. queryCacheKey ; const queryKey = typeof queryCacheKey === 'string' ? queryCacheKey : action . meta . requestId ; const endpointName = ( action . meta as any ) ?. arg ?. endpointName ?? '-' ; requestById [ action . meta . requestId ] = requestRecord = { queryKey , requestId : action . meta . requestId , endpointName , status : action . meta . requestStatus , } ; } requestRecord . status = action . meta . requestStatus ; if ( action . meta . requestStatus === QueryStatus . pending && typeof ( action . meta as any ) . startedTimeStamp === 'number' ) { requestRecord . startedTimeStamp = ( action . meta as any ) . startedTimeStamp ; } if ( action . meta . requestStatus === QueryStatus . fulfilled && typeof ( action . meta as any ) . fulfilledTimeStamp === 'number' ) { requestRecord . fulfilledTimeStamp = ( action . meta as any ) . fulfilledTimeStamp ; } } } const requestIds = Object . keys ( requestById ) ; for ( let i = 0 , len = requestIds . length ; i < len ; i ++ ) { const requestId = requestIds [ i ] ; const request = requestById [ requestId ] ; if ( typeof request . startedTimeStamp === 'undefined' && typeof request . fulfilledTimeStamp === 'number' ) { const startedTimeStampFromCache = api [ type ] [ request . queryKey ] ?. startedTimeStamp ; if ( typeof startedTimeStampFromCache === 'number' ) { request . startedTimeStamp = startedTimeStampFromCache ; } } } const queryCacheEntries = Object . entries ( api [ type ] ?? { } ) ; for ( let i = 0 , len = queryCacheEntries . length ; i < len ; i ++ ) { const [ queryCacheKey , queryCache ] = queryCacheEntries [ i ] ; const requestId = type === 'queries' ? ( queryCache as typeof api [ 'queries' ] [ string ] ) ?. requestId ?? '' : queryCacheKey ; if ( queryCache && ! Object . prototype . hasOwnProperty . call ( requestById , requestId ) ) { const startedTimeStamp = queryCache ?. startedTimeStamp ; const fulfilledTimeStamp = queryCache ?. fulfilledTimeStamp ; if ( typeof startedTimeStamp === 'number' && typeof fulfilledTimeStamp === 'number' ) { requestById [ requestId ] = { queryKey : queryCacheKey , requestId , endpointName : queryCache . endpointName ?? '' , startedTimeStamp , fulfilledTimeStamp , status : queryCache . status , } ; } } } return requestById ; } function formatRtkRequest ( rtkRequest ) { if ( ! rtkRequest ) { return null ; } const fulfilledTimeStamp = rtkRequest . fulfilledTimeStamp ; const startedTimeStamp = rtkRequest . startedTimeStamp ; const output = { queryKey : rtkRequest . queryKey , requestId : rtkRequest . requestId , endpointName : rtkRequest . endpointName , startedAt : '-' , completedAt : '-' , duration : '-' , } ; if ( typeof fulfilledTimeStamp === 'number' && typeof startedTimeStamp === 'number' ) { output . startedAt = new Date ( startedTimeStamp ) . toISOString ( ) ; output . completedAt = new Date ( fulfilledTimeStamp ) . toISOString ( ) ; output . duration = formatMs ( fulfilledTimeStamp - startedTimeStamp ) ; } return output ; } function computeQueryApiTimings ( requestById ) { const requests = Object . values ( requestById ) ; let latestRequest = null ; let oldestRequest = null ; let slowestRequest = null ; let fastestRequest = null ; let slowestDuration = 0 ; let fastestDuration = Number . MAX_SAFE_INTEGER ; const pendingDurations = [ ] ; for ( let i = 0 , len = requests . length ; i < len ; i ++ ) { const request = requests [ i ] ; const { fulfilledTimeStamp , startedTimeStamp } = request ; if ( typeof fulfilledTimeStamp === 'number' ) { const latestFulfilledTimeStamp = latestRequest ?. fulfilledTimeStamp || 0 ; const oldestFulfilledTimeStamp = oldestRequest ?. fulfilledTimeStamp || Number . MAX_SAFE_INTEGER ; if ( fulfilledTimeStamp > latestFulfilledTimeStamp ) { latestRequest = request ; } if ( fulfilledTimeStamp < oldestFulfilledTimeStamp ) { oldestRequest = request ; } if ( typeof startedTimeStamp === 'number' && startedTimeStamp <= fulfilledTimeStamp ) { const pendingDuration = fulfilledTimeStamp - startedTimeStamp ; pendingDurations . push ( pendingDuration ) ; if ( pendingDuration > slowestDuration ) { slowestDuration = pendingDuration ; slowestRequest = request ; } if ( pendingDuration < fastestDuration ) { fastestDuration = pendingDuration ; fastestRequest = request ; } } } } const average = pendingDurations . length > 0 ? formatMs ( statistics . mean ( pendingDurations ) ) : '-' ; const median = pendingDurations . length > 0 ? formatMs ( statistics . median ( pendingDurations ) ) : '-' ; return { latest : formatRtkRequest ( latestRequest ) , oldest : formatRtkRequest ( oldestRequest ) , slowest : formatRtkRequest ( slowestRequest ) , fastest : formatRtkRequest ( fastestRequest ) , average , median , } ; } function computeApiTimings ( api , actionsById , currentStateIndex ) { const sortedActions = Object . entries ( actionsById ) . sort ( ( thisAction , thatAction ) => compareJSONPrimitive ( Number ( thisAction [ 0 ] ) , Number ( thatAction [ 0 ] ) ) ) . map ( ( entry ) => entry [ 1 ] . action ) ; const queryRequestsById = computeRtkQueryRequests ( 'queries' , api , sortedActions , currentStateIndex ) ; const mutationRequestsById = computeRtkQueryRequests ( 'mutations' , api , sortedActions , currentStateIndex ) ; return { queries : computeQueryApiTimings ( queryRequestsById ) , mutations : computeQueryApiTimings ( mutationRequestsById ) , } ; } export function generateApiStatsOfCurrentQuery ( api , actionsById , currentStateIndex ) { if ( ! api ) { return null ; } return { timings : computeApiTimings ( api , actionsById , currentStateIndex ) , tally : { cachedQueries : computeQueryTallyOf ( api . queries ) , cachedMutations : computeQueryTallyOf ( api . mutations ) , tagTypes : Object . keys ( api . provided ) . length , subscriptions : tallySubscriptions ( api . subscriptions ) , } , } ; } export function flipComparator < T > ( comparator ) { return function flipped ( a , b ) { return comparator ( b , a ) ; } ; } export function isQuerySelected ( selectedQueryKey , queryInfo ) { return ( ! ! selectedQueryKey && selectedQueryKey . queryKey === queryInfo . queryKey && selectedQueryKey . reducerPath === queryInfo . reducerPath ) ; } export function getApiStateOf ( queryInfo , apiStates ) { if ( ! apiStates || ! queryInfo ) { return null ; } return apiStates [ queryInfo . reducerPath ] ?? null ; } export function getQuerySubscriptionsOf ( queryInfo , apiStates ) { if ( ! apiStates || ! queryInfo ) { return null ; } return ( apiStates [ queryInfo . reducerPath ] ?. subscriptions ?. [ queryInfo . queryKey ] ?? null ) ; } export function getProvidedOf ( queryInfo , apiStates ) { if ( ! apiStates || ! queryInfo ) { return null ; } return apiStates [ queryInfo . reducerPath ] ?. provided ?? null ; } export function getQueryTagsOf ( resInfo , provided ) { if ( ! resInfo || resInfo . type === 'mutation' || ! provided ) { return emptyArray ; } const tagTypes = Object . keys ( provided ) ; if ( tagTypes . length < 1 ) { return emptyArray ; } const output = [ ] ; for ( const [ type , tagIds ] of Object . entries ( provided ) ) { if ( tagIds ) { for ( const [ id , queryKeys ] of Object . entries ( tagIds ) ) { if ( ( queryKeys as unknown [ ] ) . includes ( resInfo . queryKey ) ) { const tag = { type } ; if ( id !== missingTagId ) { tag . id = id ; } output . push ( tag ) ; } } } } return output ; } param status see https see https export function getQueryStatusFlags ( { status , data , } ) { return { isUninitialized : status === QueryStatus . uninitialized , isFetching : status === QueryStatus . pending , isSuccess : status === QueryStatus . fulfilled && ! ! data , isError : status === QueryStatus . rejected , } ; } param endpointName see https export function matchesEndpoint ( endpointName ) { return ( action ) : action is Action => endpointName != null && action ?. meta ?. arg ?. endpointName === endpointName ; } function matchesQueryKey ( queryKey ) { return ( action ) : action is Action => action ?. meta ?. arg ?. queryCacheKey === queryKey ; } function macthesRequestId ( requestId ) { return ( action ) : action is Action => action ?. meta ?. requestId === requestId ; } function matchesReducerPath ( reducerPath ) { return ( action ) : action is Action < string > => typeof action ?. type === 'string' && action . type . startsWith ( reducerPath ) ; } function matchesExecuteQuery ( reducerPath ) { return ( action ) : action is Action < string > & { meta : { requestId : string ; requestStatus : QueryStatus ; } ; } => { return ( typeof action ?. type === 'string' && action . type . startsWith ( `${ reducerPath }/executeQuery` ) && typeof action . meta ?. requestId === 'string' && typeof action . meta ?. requestStatus === 'string' ) ; } ; } function matchesExecuteMutation ( reducerPath ) { return ( action ) : action is Action < string > & { meta : { requestId : string ; requestStatus : QueryStatus ; } ; } => typeof action ?. type === 'string' && action . type . startsWith ( `${ reducerPath }/executeMutation` ) && typeof action . meta ?. requestId === 'string' && typeof action . meta ?. requestStatus === 'string' ; } export function getActionsOfCurrentQuery ( currentQuery , actionById ) { if ( ! currentQuery ) { return emptyArray ; } let matcher ; if ( currentQuery . type === 'mutation' ) { matcher = isAllOf ( matchesReducerPath ( currentQuery . reducerPath ) , macthesRequestId ( currentQuery . queryKey ) ) ; } else { matcher = isAllOf ( matchesReducerPath ( currentQuery . reducerPath ) , matchesQueryKey ( currentQuery . queryKey ) ) ; } const output = [ ] ; for ( const [ , liftedAction ] of Object . entries ( actionById ) ) { if ( matcher ( liftedAction ?. action ) ) { output . push ( liftedAction . action ) ; } } return output . length === 0 ? emptyArray : output ; }