UNPKG

@redux-devtools/rtk-query-monitor

Version:
412 lines (405 loc) 14.7 kB
import { isAllOf, isPlainObject } from '@reduxjs/toolkit'; import { QueryStatus } from '@reduxjs/toolkit/query'; import { isRtkQuery262Provided } from '../types'; import { missingTagId } from '../monitor-config'; import { compareJSONPrimitive } from './comparators'; import { emptyArray } from './object'; import { formatMs } from './formatters'; import * as statistics from './statistics'; const rtkqueryApiStateKeys = ['queries', 'mutations', 'config', 'provided', 'subscriptions']; /** * Type guard used to select apis from the user store state. * @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[rtkqueryApiStateKeys[i]])) { return false; } } return true; } /** * Indexes api states by their `reducerPath`. * * Returns `null` if there are no api slice or `reduxStoreState` * is not an object. * * @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[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]++; } } } 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?.arg?.queryCacheKey; const queryKey = typeof queryCacheKey === 'string' ? queryCacheKey : action.meta.requestId; const endpointName = action.meta?.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.startedTimeStamp === 'number') { requestRecord.startedTimeStamp = action.meta.startedTimeStamp; } if (action.meta.requestStatus === QueryStatus.fulfilled && typeof action.meta.fulfilledTimeStamp === 'number') { requestRecord.fulfilledTimeStamp = action.meta.fulfilledTimeStamp; } } } const requestIds = Object.keys(requestById); // Patch queries that have pending actions that are committed 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; } } } // Add queries that have pending and fulfilled actions committed 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?.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(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; } // Handle `api.provided` schema change with RTK Query tag handling. // Originally, `api.provided` was a `Record<string, Record<string, string[]>>`, // directly containing the tag names. // With https://github.com/reduxjs/redux-toolkit/pull/4910 , that changes to // change the top level to be `{tags, keys}`, with `tags` containing the tag names. // Handle the newer structure by extracting the right field if it exists. const actualProvided = isRtkQuery262Provided(provided) ? provided.tags : provided; const tagTypes = Object.keys(actualProvided); if (tagTypes.length < 1) { return emptyArray; } const output = []; for (const [type, tagIds] of Object.entries(actualProvided)) { if (tagIds) { for (const [id, queryKeys] of Object.entries(tagIds)) { if (queryKeys.includes(resInfo.queryKey)) { const tag = { type }; if (id !== missingTagId) { tag.id = id; } output.push(tag); } } } } return output; } /** * Computes query status flags. * @param status * @see https://redux-toolkit.js.org/rtk-query/usage/queries#frequently-used-query-hook-return-values * @see https://github.com/reduxjs/redux-toolkit/blob/b718e01d323d3ab4b913e5d88c9b90aa790bb975/src/query/core/apiState.ts#L63 */ export function getQueryStatusFlags(_ref) { let { status, data } = _ref; return { isUninitialized: status === QueryStatus.uninitialized, isFetching: status === QueryStatus.pending, isSuccess: status === QueryStatus.fulfilled && !!data, isError: status === QueryStatus.rejected }; } /** * endpoint matcher * @param endpointName * @see https://github.com/reduxjs/redux-toolkit/blob/b718e01d323d3ab4b913e5d88c9b90aa790bb975/src/query/core/buildThunks.ts#L415 */ export function matchesEndpoint(endpointName) { return action => endpointName != null && action?.meta?.arg?.endpointName === endpointName; } function matchesQueryKey(queryKey) { return action => action?.meta?.arg?.queryCacheKey === queryKey; } function macthesRequestId(requestId) { return action => action?.meta?.requestId === requestId; } function matchesReducerPath(reducerPath) { return action => typeof action?.type === 'string' && action.type.startsWith(reducerPath); } function matchesExecuteQuery(reducerPath) { return action => { 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 => 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; }