Skip to content

Commit 9392c4c

Browse files
authored
Improve perf for internal map/filter ops (#5126)
* Add filterMap util * Use filterMap instead of multiple filters * Copy getCurrent util * Iterate over current arrays where possible * Track pending requests counter * Fix typo in build config name * Remove flatten tests * Use reduce+flat for smaller filterMap size
1 parent 78781fd commit 9392c4c

File tree

11 files changed

+102
-84
lines changed

11 files changed

+102
-84
lines changed

packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import type {
44
BaseQueryMeta,
55
BaseQueryResult,
66
} from '../../baseQueryTypes'
7-
import type { BaseEndpointDefinition } from '../../endpointDefinitions'
8-
import { DefinitionType, isAnyQueryDefinition } from '../../endpointDefinitions'
7+
import type {
8+
BaseEndpointDefinition,
9+
DefinitionType,
10+
} from '../../endpointDefinitions'
11+
import { isAnyQueryDefinition } from '../../endpointDefinitions'
912
import type { QueryCacheKey, RootState } from '../apiState'
1013
import type {
1114
MutationResultSelectorResult,

packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,25 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({
3838
)
3939

4040
const isQueryEnd = isAnyOf(
41-
isFulfilled(mutationThunk, queryThunk),
42-
isRejected(mutationThunk, queryThunk),
41+
isFulfilled(queryThunk, mutationThunk),
42+
isRejected(queryThunk, mutationThunk),
4343
)
44-
4544
let pendingTagInvalidations: FullTagDescription<string>[] = []
45+
// Track via counter so we can avoid iterating over state every time
46+
let pendingRequestCount = 0
4647

4748
const handler: ApiMiddlewareInternalHandler = (action, mwApi) => {
49+
if (
50+
queryThunk.pending.match(action) ||
51+
mutationThunk.pending.match(action)
52+
) {
53+
pendingRequestCount++
54+
}
55+
56+
if (isQueryEnd(action)) {
57+
pendingRequestCount = Math.max(0, pendingRequestCount - 1)
58+
}
59+
4860
if (isThunkActionWithTags(action)) {
4961
invalidateTags(
5062
calculateProvidedByThunk(
@@ -72,16 +84,8 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({
7284
}
7385
}
7486

75-
function hasPendingRequests(
76-
state: CombinedState<EndpointDefinitions, string, string>,
77-
) {
78-
const { queries, mutations } = state
79-
for (const cacheRecord of [queries, mutations]) {
80-
for (const key in cacheRecord) {
81-
if (cacheRecord[key]?.status === QueryStatus.pending) return true
82-
}
83-
}
84-
return false
87+
function hasPendingRequests() {
88+
return pendingRequestCount > 0
8589
}
8690

8791
function invalidateTags(
@@ -95,7 +99,7 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({
9599

96100
if (
97101
state.config.invalidationBehavior === 'delayed' &&
98-
hasPendingRequests(state)
102+
hasPendingRequests()
99103
) {
100104
return
101105
}

packages/toolkit/src/query/core/buildSelectors.ts

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {
1313
TagTypesFrom,
1414
} from '../endpointDefinitions'
1515
import { expandTagDescription } from '../endpointDefinitions'
16-
import { flatten, isNotNullish } from '../utils'
16+
import { filterMap, isNotNullish } from '../utils'
1717
import type {
1818
InfiniteData,
1919
InfiniteQueryConfigOptions,
@@ -342,7 +342,8 @@ export function buildSelectors<
342342
}> {
343343
const apiState = state[reducerPath]
344344
const toInvalidate = new Set<QueryCacheKey>()
345-
for (const tag of tags.filter(isNotNullish).map(expandTagDescription)) {
345+
const finalTags = filterMap(tags, isNotNullish, expandTagDescription)
346+
for (const tag of finalTags) {
346347
const provided = apiState.provided.tags[tag.type]
347348
if (!provided) {
348349
continue
@@ -353,27 +354,23 @@ export function buildSelectors<
353354
? // id given: invalidate all queries that provide this type & id
354355
provided[tag.id]
355356
: // no id: invalidate all queries that provide this type
356-
flatten(Object.values(provided))) ?? []
357+
Object.values(provided).flat()) ?? []
357358

358359
for (const invalidate of invalidateSubscriptions) {
359360
toInvalidate.add(invalidate)
360361
}
361362
}
362363

363-
return flatten(
364-
Array.from(toInvalidate.values()).map((queryCacheKey) => {
365-
const querySubState = apiState.queries[queryCacheKey]
366-
return querySubState
367-
? [
368-
{
369-
queryCacheKey,
370-
endpointName: querySubState.endpointName!,
371-
originalArgs: querySubState.originalArgs,
372-
},
373-
]
374-
: []
375-
}),
376-
)
364+
return Array.from(toInvalidate.values()).flatMap((queryCacheKey) => {
365+
const querySubState = apiState.queries[queryCacheKey]
366+
return querySubState
367+
? {
368+
queryCacheKey,
369+
endpointName: querySubState.endpointName!,
370+
originalArgs: querySubState.originalArgs,
371+
}
372+
: []
373+
})
377374
}
378375

379376
function selectCachedArgsForQuery<
@@ -382,18 +379,18 @@ export function buildSelectors<
382379
state: RootState,
383380
queryName: QueryName,
384381
): Array<QueryArgFromAnyQuery<Definitions[QueryName]>> {
385-
return Object.values(selectQueries(state) as QueryState<any>)
386-
.filter(
387-
(
388-
entry,
389-
): entry is Exclude<
390-
QuerySubState<Definitions[QueryName]>,
391-
{ status: QueryStatus.uninitialized }
392-
> =>
393-
entry?.endpointName === queryName &&
394-
entry.status !== QueryStatus.uninitialized,
395-
)
396-
.map((entry) => entry.originalArgs)
382+
return filterMap(
383+
Object.values(selectQueries(state) as QueryState<any>),
384+
(
385+
entry,
386+
): entry is Exclude<
387+
QuerySubState<Definitions[QueryName]>,
388+
{ status: QueryStatus.uninitialized }
389+
> =>
390+
entry?.endpointName === queryName &&
391+
entry.status !== QueryStatus.uninitialized,
392+
(entry) => entry.originalArgs,
393+
)
397394
}
398395

399396
function getHasNextPage(

packages/toolkit/src/query/core/buildSlice.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import type { ApiContext } from '../apiTypes'
5757
import { isUpsertQuery } from './buildInitiate'
5858
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
5959
import type { UnwrapPromise } from '../tsHelpers'
60+
import { getCurrent } from '../utils/getCurrent'
6061

6162
/**
6263
* A typesafe single entry to be upserted into the cache
@@ -587,7 +588,7 @@ export function buildSlice({
587588
draft: InvalidationState<any>,
588589
queryCacheKey: QueryCacheKey,
589590
) {
590-
const existingTags = draft.keys[queryCacheKey] ?? []
591+
const existingTags = getCurrent(draft.keys[queryCacheKey] ?? [])
591592

592593
// Delete this cache key from any existing tags that may have provided it
593594
for (const tag of existingTags) {
@@ -596,7 +597,7 @@ export function buildSlice({
596597
const tagSubscriptions = draft.tags[tagType]?.[tagId]
597598

598599
if (tagSubscriptions) {
599-
draft.tags[tagType][tagId] = tagSubscriptions.filter(
600+
draft.tags[tagType][tagId] = getCurrent(tagSubscriptions).filter(
600601
(qc) => qc !== queryCacheKey,
601602
)
602603
}

packages/toolkit/src/query/endpointDefinitions.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import type {
3939
} from './tsHelpers'
4040
import { isNotNullish } from './utils'
4141
import type { NamedSchemaError } from './standardSchema'
42+
import { filterMap } from './utils/filterMap'
4243

4344
const rawResultType = /* @__PURE__ */ Symbol()
4445
const resultType = /* @__PURE__ */ Symbol()
@@ -1406,20 +1407,21 @@ export function calculateProvidedBy<ResultType, QueryArg, ErrorType, MetaType>(
14061407
meta: MetaType | undefined,
14071408
assertTagTypes: AssertTagTypes,
14081409
): readonly FullTagDescription<string>[] {
1409-
if (isFunction(description)) {
1410-
return description(
1411-
result as ResultType,
1412-
error as undefined,
1413-
queryArg,
1414-
meta as MetaType,
1410+
const finalDescription = isFunction(description)
1411+
? description(
1412+
result as ResultType,
1413+
error as undefined,
1414+
queryArg,
1415+
meta as MetaType,
1416+
)
1417+
: description
1418+
1419+
if (finalDescription) {
1420+
return filterMap(finalDescription, isNotNullish, (tag) =>
1421+
assertTagTypes(expandTagDescription(tag)),
14151422
)
1416-
.filter(isNotNullish)
1417-
.map(expandTagDescription)
1418-
.map(assertTagTypes)
1419-
}
1420-
if (Array.isArray(description)) {
1421-
return description.map(expandTagDescription).map(assertTagTypes)
14221423
}
1424+
14231425
return []
14241426
}
14251427

packages/toolkit/src/query/tests/utils.test.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { vi } from 'vitest'
2-
import {
3-
isOnline,
4-
isDocumentVisible,
5-
flatten,
6-
joinUrls,
7-
} from '@internal/query/utils'
2+
import { isOnline, isDocumentVisible, joinUrls } from '@internal/query/utils'
83

94
afterAll(() => {
105
vi.restoreAllMocks()
@@ -96,14 +91,3 @@ describe('joinUrls', () => {
9691
expect(joinUrls(base, url)).toBe(expected)
9792
})
9893
})
99-
100-
describe('flatten', () => {
101-
test('flattens an array to a depth of 1', () => {
102-
expect(flatten([1, 2, [3, 4]])).toEqual([1, 2, 3, 4])
103-
})
104-
test('does not flatten to a depth of 2', () => {
105-
const flattenResult = flatten([1, 2, [3, 4, [5, 6]]])
106-
expect(flattenResult).not.toEqual([1, 2, 3, 4, 5, 6])
107-
expect(flattenResult).toEqual([1, 2, 3, 4, [5, 6]])
108-
})
109-
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Preserve type guard predicate behavior when passing to mapper
2+
export function filterMap<T, U, S extends T = T>(
3+
array: readonly T[],
4+
predicate: (item: T, index: number) => item is S,
5+
mapper: (item: S, index: number) => U | U[],
6+
): U[]
7+
8+
export function filterMap<T, U>(
9+
array: readonly T[],
10+
predicate: (item: T, index: number) => boolean,
11+
mapper: (item: T, index: number) => U | U[],
12+
): U[]
13+
14+
export function filterMap<T, U>(
15+
array: readonly T[],
16+
predicate: (item: T, index: number) => boolean,
17+
mapper: (item: T, index: number) => U | U[],
18+
): U[] {
19+
return array
20+
.reduce<(U | U[])[]>((acc, item, i) => {
21+
if (predicate(item as any, i)) {
22+
acc.push(mapper(item as any, i))
23+
}
24+
return acc
25+
}, [])
26+
.flat() as U[]
27+
}

packages/toolkit/src/query/utils/flatten.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { Draft } from 'immer'
2+
import { current, isDraft } from 'immer'
3+
4+
export function getCurrent<T>(value: T | Draft<T>): T {
5+
return (isDraft(value) ? current(value) : value) as T
6+
}

packages/toolkit/src/query/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export * from './capitalize'
22
export * from './copyWithStructuralSharing'
33
export * from './countObjectKeys'
4-
export * from './flatten'
4+
export * from './filterMap'
55
export * from './isAbsoluteUrl'
66
export * from './isDocumentVisible'
77
export * from './isNotNullish'

0 commit comments

Comments
 (0)