refactor(rtk-query): track all fulfilled requests using actions

Other changes:

* refactor(rtk-query): rename tally properties
This commit is contained in:
FaberVitale 2021-08-15 17:25:49 +02:00
parent e4c5720d87
commit c563e48f78
4 changed files with 282 additions and 89 deletions

View File

@ -45,6 +45,7 @@ export function computeSelectorSource<S, A extends Action<unknown>>(
return { return {
userState, userState,
monitorState, monitorState,
currentStateIndex,
actionsById, actionsById,
}; };
} }
@ -236,6 +237,8 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
const selectApiStatsOfCurrentQuery = createSelector( const selectApiStatsOfCurrentQuery = createSelector(
selectApiOfCurrentQuery, selectApiOfCurrentQuery,
(selectorsSource) => selectorsSource.actionsById,
(selectorsSource) => selectorsSource.currentStateIndex,
generateApiStatsOfCurrentQuery generateApiStatsOfCurrentQuery
); );

View File

@ -93,6 +93,7 @@ export interface SelectOption<
export interface SelectorsSource<S> { export interface SelectorsSource<S> {
userState: S | null; userState: S | null;
monitorState: RtkQueryMonitorState; monitorState: RtkQueryMonitorState;
currentStateIndex: number;
actionsById: LiftedState<unknown, AnyAction, unknown>['actionsById']; actionsById: LiftedState<unknown, AnyAction, unknown>['actionsById'];
} }
@ -120,11 +121,20 @@ export type QueryTally = {
} & } &
Tally; Tally;
export interface RtkRequestTiming {
requestId: string;
queryKey: string;
endpointName: string;
startedAt: string;
completedAt: string;
duration: string;
}
export interface QueryTimings { export interface QueryTimings {
readonly oldest: { key: string; at: string } | null; readonly oldest: RtkRequestTiming | null;
readonly latest: { key: string; at: string } | null; readonly latest: RtkRequestTiming | null;
readonly slowest: { key: string; duration: string } | null; readonly slowest: RtkRequestTiming | null;
readonly fastest: { key: string; duration: string } | null; readonly fastest: RtkRequestTiming | null;
readonly average: string; readonly average: string;
readonly median: string; readonly median: string;
} }
@ -138,9 +148,9 @@ export interface ApiStats {
readonly timings: ApiTimings; readonly timings: ApiTimings;
readonly tally: Readonly<{ readonly tally: Readonly<{
subscriptions: number; subscriptions: number;
queries: QueryTally; cachedQueries: QueryTally;
tagTypes: number; tagTypes: number;
mutations: QueryTally; cachedMutations: QueryTally;
}>; }>;
} }
@ -158,3 +168,12 @@ export interface RTKStatusFlags {
readonly isSuccess: boolean; readonly isSuccess: boolean;
readonly isError: boolean; readonly isError: boolean;
} }
export type RtkRequest = {
status: QueryStatus;
queryKey: string;
requestId: string;
endpointName: string;
startedTimeStamp?: number;
fulfilledTimeStamp?: number;
};

View File

@ -48,10 +48,9 @@ function sortQueryByStatus(
return thisTerm - thatTerm; return thisTerm - thatTerm;
} }
function compareJSONPrimitive<T extends string | number | boolean | null>( export function compareJSONPrimitive<
a: T, T extends string | number | boolean | null
b: T >(a: T, b: T): number {
): number {
if (a === b) { if (a === b) {
return 0; return 0;
} }

View File

@ -1,4 +1,4 @@
import { AnyAction, isAllOf, isAnyOf, isPlainObject } from '@reduxjs/toolkit'; import { Action, AnyAction, isAllOf, isPlainObject } from '@reduxjs/toolkit';
import { QueryStatus } from '@reduxjs/toolkit/query'; import { QueryStatus } from '@reduxjs/toolkit/query';
import { import {
QueryInfo, QueryInfo,
@ -17,9 +17,11 @@ import {
SelectorsSource, SelectorsSource,
RtkMutationState, RtkMutationState,
RtkResourceInfo, RtkResourceInfo,
RtkRequest,
RtkRequestTiming,
} from '../types'; } from '../types';
import { missingTagId } from '../monitor-config'; import { missingTagId } from '../monitor-config';
import { Comparator } from './comparators'; import { Comparator, compareJSONPrimitive } from './comparators';
import { emptyArray } from './object'; import { emptyArray } from './object';
import { formatMs } from './formatters'; import { formatMs } from './formatters';
import * as statistics from './statistics'; import * as statistics from './statistics';
@ -199,44 +201,186 @@ function tallySubscriptions(
return output; return output;
} }
function computeRtkQueryRequests(
type: 'queries' | 'mutations',
api: RtkQueryApiState,
sortedActions: AnyAction[],
currentStateIndex: SelectorsSource<unknown>['currentStateIndex']
): Readonly<Record<string, RtkRequest>> {
const requestById: Record<string, RtkRequest> = {};
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: RtkRequest | undefined =
requestById[action.meta.requestId];
if (!requestRecord) {
const queryCacheKey: string | undefined = (
action.meta as Record<string, any>
)?.arg?.queryCacheKey;
const queryKey =
typeof queryCacheKey === 'string'
? queryCacheKey
: action.meta.requestId;
const endpointName: string =
(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);
// 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: string =
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: RtkRequest | null
): RtkRequestTiming | null {
if (!rtkRequest) {
return null;
}
const fulfilledTimeStamp = rtkRequest.fulfilledTimeStamp;
const startedTimeStamp = rtkRequest.startedTimeStamp;
const output: RtkRequestTiming = {
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( function computeQueryApiTimings(
queriesOrMutations: requestById: Readonly<Record<string, RtkRequest>>
| RtkQueryApiState['queries']
| RtkQueryApiState['mutations']
): QueryTimings { ): QueryTimings {
type SpeedReport = { key: string | null; at: string | number }; const requests = Object.values(requestById);
type DurationReport = { key: string | null; duration: string | number };
let latest: null | SpeedReport = { key: null, at: -1 }; let latestRequest: RtkRequest | null = null;
let oldest: null | SpeedReport = { let oldestRequest: null | RtkRequest = null;
key: null, let slowestRequest: RtkRequest | null = null;
at: Number.MAX_SAFE_INTEGER, let fastestRequest: RtkRequest | null = null;
}; let slowestDuration = 0;
let slowest: null | DurationReport = { key: null, duration: -1 }; let fastestDuration = Number.MAX_SAFE_INTEGER;
let fastest: null | DurationReport = {
key: null,
duration: Number.MAX_SAFE_INTEGER,
};
const pendingDurations: number[] = []; const pendingDurations: number[] = [];
const queryKeys = Object.keys(queriesOrMutations); for (let i = 0, len = requests.length; i < len; i++) {
const request = requests[i];
for (let i = 0, len = queryKeys.length; i < len; i++) { const { fulfilledTimeStamp, startedTimeStamp } = request;
const queryKey = queryKeys[i];
const query = queriesOrMutations[queryKey];
const fulfilledTimeStamp = query?.fulfilledTimeStamp;
const startedTimeStamp = query?.startedTimeStamp;
if (typeof fulfilledTimeStamp === 'number') { if (typeof fulfilledTimeStamp === 'number') {
if (fulfilledTimeStamp > latest.at) { const latestFulfilledTimeStamp = latestRequest?.fulfilledTimeStamp || 0;
latest.key = queryKey; const oldestFulfilledTimeStamp =
latest.at = fulfilledTimeStamp; oldestRequest?.fulfilledTimeStamp || Number.MAX_SAFE_INTEGER;
if (fulfilledTimeStamp > latestFulfilledTimeStamp) {
latestRequest = request;
} }
if (fulfilledTimeStamp < oldest.at) { if (fulfilledTimeStamp < oldestFulfilledTimeStamp) {
oldest.key = queryKey; oldestRequest = request;
oldest.at = fulfilledTimeStamp;
} }
if ( if (
@ -244,46 +388,21 @@ function computeQueryApiTimings(
startedTimeStamp <= fulfilledTimeStamp startedTimeStamp <= fulfilledTimeStamp
) { ) {
const pendingDuration = fulfilledTimeStamp - startedTimeStamp; const pendingDuration = fulfilledTimeStamp - startedTimeStamp;
pendingDurations.push(pendingDuration); pendingDurations.push(pendingDuration);
if (pendingDuration > slowest.duration) { if (pendingDuration > slowestDuration) {
slowest.key = queryKey; slowestDuration = pendingDuration;
slowest.duration = pendingDuration; slowestRequest = request;
} }
if (pendingDuration < fastest.duration) { if (pendingDuration < fastestDuration) {
fastest.key = queryKey; fastestDuration = pendingDuration;
fastest.duration = pendingDuration; fastestRequest = request;
} }
} }
} }
} }
if (latest.key !== null) {
latest.at = new Date(latest.at).toISOString();
} else {
latest = null;
}
if (oldest.key !== null) {
oldest.at = new Date(oldest.at).toISOString();
} else {
oldest = null;
}
if (slowest.key !== null) {
slowest.duration = formatMs(slowest.duration as number);
} else {
slowest = null;
}
if (fastest.key !== null) {
fastest.duration = formatMs(fastest.duration as number);
} else {
fastest = null;
}
const average = const average =
pendingDurations.length > 0 pendingDurations.length > 0
? formatMs(statistics.mean(pendingDurations)) ? formatMs(statistics.mean(pendingDurations))
@ -295,34 +414,60 @@ function computeQueryApiTimings(
: '-'; : '-';
return { return {
latest, latest: formatRtkRequest(latestRequest),
oldest, oldest: formatRtkRequest(oldestRequest),
slowest, slowest: formatRtkRequest(slowestRequest),
fastest, fastest: formatRtkRequest(fastestRequest),
average, average,
median, median,
} as QueryTimings; };
} }
function computeApiTimings(api: RtkQueryApiState): ApiTimings { function computeApiTimings(
api: RtkQueryApiState,
actionsById: SelectorsSource<unknown>['actionsById'],
currentStateIndex: SelectorsSource<unknown>['currentStateIndex']
): ApiTimings {
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 { return {
queries: computeQueryApiTimings(api.queries), queries: computeQueryApiTimings(queryRequestsById),
mutations: computeQueryApiTimings(api.mutations), mutations: computeQueryApiTimings(mutationRequestsById),
}; };
} }
export function generateApiStatsOfCurrentQuery( export function generateApiStatsOfCurrentQuery(
api: RtkQueryApiState | null api: RtkQueryApiState | null,
actionsById: SelectorsSource<unknown>['actionsById'],
currentStateIndex: SelectorsSource<unknown>['currentStateIndex']
): ApiStats | null { ): ApiStats | null {
if (!api) { if (!api) {
return null; return null;
} }
return { return {
timings: computeApiTimings(api), timings: computeApiTimings(api, actionsById, currentStateIndex),
tally: { tally: {
queries: computeQueryTallyOf(api.queries), cachedQueries: computeQueryTallyOf(api.queries),
mutations: computeQueryTallyOf(api.mutations), cachedMutations: computeQueryTallyOf(api.mutations),
tagTypes: Object.keys(api.provided).length, tagTypes: Object.keys(api.provided).length,
subscriptions: tallySubscriptions(api.subscriptions), subscriptions: tallySubscriptions(api.subscriptions),
}, },
@ -441,29 +586,56 @@ export function getQueryStatusFlags({
* @see https://github.com/reduxjs/redux-toolkit/blob/b718e01d323d3ab4b913e5d88c9b90aa790bb975/src/query/core/buildThunks.ts#L415 * @see https://github.com/reduxjs/redux-toolkit/blob/b718e01d323d3ab4b913e5d88c9b90aa790bb975/src/query/core/buildThunks.ts#L415
*/ */
export function matchesEndpoint(endpointName: unknown) { export function matchesEndpoint(endpointName: unknown) {
return (action: any): action is AnyAction => return (action: any): action is Action =>
endpointName != null && action?.meta?.arg?.endpointName === endpointName; endpointName != null && action?.meta?.arg?.endpointName === endpointName;
} }
function matchesQueryKey(queryKey: string) { function matchesQueryKey(queryKey: string) {
return (action: any): action is AnyAction => return (action: any): action is Action =>
action?.meta?.arg?.queryCacheKey === queryKey; action?.meta?.arg?.queryCacheKey === queryKey;
} }
function macthesRequestId(requestId: string) { function macthesRequestId(requestId: string) {
return (action: any): action is AnyAction => return (action: any): action is Action =>
action?.meta?.requestId === requestId; action?.meta?.requestId === requestId;
} }
function matchesReducerPath(reducerPath: string) { function matchesReducerPath(reducerPath: string) {
return (action: any): action is AnyAction => return (action: any): action is Action<string> =>
typeof action?.type === 'string' && action.type.startsWith(reducerPath); typeof action?.type === 'string' && action.type.startsWith(reducerPath);
} }
function matchesExecuteQuery(reducerPath: string) {
return (
action: any
): 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: string) {
return (
action: any
): 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( export function getActionsOfCurrentQuery(
currentQuery: RtkResourceInfo | null, currentQuery: RtkResourceInfo | null,
actionById: SelectorsSource<unknown>['actionsById'] actionById: SelectorsSource<unknown>['actionsById']
): AnyAction[] { ): Action[] {
if (!currentQuery) { if (!currentQuery) {
return emptyArray; return emptyArray;
} }