diff --git a/.changeset/perf-cache-lookup-optimization.md b/.changeset/perf-cache-lookup-optimization.md new file mode 100644 index 00000000000..117b22d96cf --- /dev/null +++ b/.changeset/perf-cache-lookup-optimization.md @@ -0,0 +1,5 @@ +--- +"@tanstack/query-core": patch +--- + +Optimize `find`/`findAll` lookups in `QueryCache` and `MutationCache` by using index maps for O(1) key-based access when `exact: true` and a key filter are provided, instead of iterating all entries. diff --git a/packages/query-core/src/mutationCache.ts b/packages/query-core/src/mutationCache.ts index d204c904e5f..25684ffc8c7 100644 --- a/packages/query-core/src/mutationCache.ts +++ b/packages/query-core/src/mutationCache.ts @@ -1,6 +1,6 @@ import { notifyManager } from './notifyManager' import { Mutation } from './mutation' -import { matchMutation, noop } from './utils' +import { hashKey, matchMutation, noop } from './utils' import { Subscribable } from './subscribable' import type { MutationObserver } from './mutationObserver' import type { @@ -93,12 +93,14 @@ type MutationCacheListener = (event: MutationCacheNotifyEvent) => void export class MutationCache extends Subscribable { #mutations: Set> #scopes: Map>> + #byMutationKey: Map>> #mutationId: number constructor(public config: MutationCacheConfig = {}) { super() this.#mutations = new Set() this.#scopes = new Map() + this.#byMutationKey = new Map() this.#mutationId = 0 } @@ -131,6 +133,13 @@ export class MutationCache extends Subscribable { this.#scopes.set(scope, [mutation]) } } + const { mutationKey } = mutation.options + if (mutationKey) { + const hash = hashKey(mutationKey) + const keyed = this.#byMutationKey.get(hash) + if (keyed) keyed.push(mutation) + else this.#byMutationKey.set(hash, [mutation]) + } this.notify({ type: 'added', mutation }) } @@ -150,6 +159,18 @@ export class MutationCache extends Subscribable { } } } + const { mutationKey } = mutation.options + if (mutationKey) { + const hash = hashKey(mutationKey) + const keyed = this.#byMutationKey.get(hash) + if (keyed) { + const index = keyed.indexOf(mutation) + if (index !== -1) { + keyed.splice(index, 1) + if (keyed.length === 0) this.#byMutationKey.delete(hash) + } + } + } } // Currently we notify the removal even if the mutation was already removed. @@ -194,6 +215,7 @@ export class MutationCache extends Subscribable { }) this.#mutations.clear() this.#scopes.clear() + this.#byMutationKey.clear() }) } @@ -211,13 +233,50 @@ export class MutationCache extends Subscribable { ): Mutation | undefined { const defaultedFilters = { exact: true, ...filters } - return this.getAll().find((mutation) => - matchMutation(defaultedFilters, mutation), - ) as Mutation | undefined + if (defaultedFilters.exact && defaultedFilters.mutationKey) { + const candidates = this.#byMutationKey.get( + hashKey(defaultedFilters.mutationKey), + ) + if (!candidates) return undefined + const { mutationKey: _m, ...filtersWithoutKey } = defaultedFilters + for (const mutation of candidates) { + if (matchMutation(filtersWithoutKey as MutationFilters, mutation)) { + return mutation as Mutation< + TData, + TError, + TVariables, + TOnMutateResult + > + } + } + return undefined + } + + for (const mutation of this.#mutations) { + if (matchMutation(defaultedFilters, mutation)) { + return mutation as Mutation + } + } + return undefined } findAll(filters: MutationFilters = {}): Array { - return this.getAll().filter((mutation) => matchMutation(filters, mutation)) + if (filters.exact && filters.mutationKey) { + const candidates = this.#byMutationKey.get(hashKey(filters.mutationKey)) + if (!candidates) return [] + const { mutationKey: _m, ...filtersWithoutKey } = filters + return candidates.filter((m) => + matchMutation(filtersWithoutKey as MutationFilters, m), + ) + } + + const result: Array = [] + for (const mutation of this.#mutations) { + if (matchMutation(filters, mutation)) { + result.push(mutation) + } + } + return result } notify(event: MutationCacheNotifyEvent) { @@ -229,7 +288,12 @@ export class MutationCache extends Subscribable { } resumePausedMutations(): Promise { - const pausedMutations = this.getAll().filter((x) => x.state.isPaused) + const pausedMutations: Array = [] + for (const mutation of this.#mutations) { + if (mutation.state.isPaused) { + pausedMutations.push(mutation) + } + } return notifyManager.batch(() => Promise.all( diff --git a/packages/query-core/src/queryCache.ts b/packages/query-core/src/queryCache.ts index dd7123eaac8..f5afc1bbdcb 100644 --- a/packages/query-core/src/queryCache.ts +++ b/packages/query-core/src/queryCache.ts @@ -1,4 +1,4 @@ -import { hashQueryKeyByOptions, matchQuery } from './utils' +import { hashKey, hashQueryKeyByOptions, matchQuery } from './utils' import { Query } from './query' import { notifyManager } from './notifyManager' import { Subscribable } from './subscribable' @@ -185,16 +185,50 @@ export class QueryCache extends Subscribable { ): Query | undefined { const defaultedFilters = { exact: true, ...filters } - return this.getAll().find((query) => - matchQuery(defaultedFilters, query), - ) as Query | undefined + let found: Query | undefined + + if (defaultedFilters.exact) { + const candidate = this.#queries.get(hashKey(defaultedFilters.queryKey)) + if (candidate) { + const { queryKey: _q, ...filtersWithoutKey } = defaultedFilters + found = matchQuery(filtersWithoutKey as QueryFilters, candidate) + ? candidate + : undefined + return found as Query | undefined + } + } + + for (const query of this.#queries.values()) { + if (matchQuery(defaultedFilters, query)) { + found = query + break + } + } + return found as Query | undefined } findAll(filters: QueryFilters = {}): Array { - const queries = this.getAll() - return Object.keys(filters).length > 0 - ? queries.filter((query) => matchQuery(filters, query)) - : queries + if (Object.keys(filters).length === 0) { + return [...this.#queries.values()] + } + + if (filters.exact && filters.queryKey) { + const candidate = this.#queries.get(hashKey(filters.queryKey)) + if (candidate) { + const { queryKey: _q, ...filtersWithoutKey } = filters + return matchQuery(filtersWithoutKey as QueryFilters, candidate) + ? [candidate] + : [] + } + } + + const result: Array = [] + for (const query of this.#queries.values()) { + if (matchQuery(filters, query)) { + result.push(query) + } + } + return result } notify(event: QueryCacheNotifyEvent): void { @@ -207,17 +241,17 @@ export class QueryCache extends Subscribable { onFocus(): void { notifyManager.batch(() => { - this.getAll().forEach((query) => { + for (const query of this.#queries.values()) { query.onFocus() - }) + } }) } onOnline(): void { notifyManager.batch(() => { - this.getAll().forEach((query) => { + for (const query of this.#queries.values()) { query.onOnline() - }) + } }) } }