Skip to content

Commit

Permalink
perf(query-core): Improve mutationCache implementation performance (#…
Browse files Browse the repository at this point in the history
…8496)

---------

  Co-authored-by: Josh Story <[email protected]>
  • Loading branch information
TkDodo authored Jan 3, 2025
1 parent 3124193 commit 19b95c4
Showing 1 changed file with 58 additions and 31 deletions.
89 changes: 58 additions & 31 deletions packages/query-core/src/mutationCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ type MutationCacheListener = (event: MutationCacheNotifyEvent) => void
// CLASS

export class MutationCache extends Subscribable<MutationCacheListener> {
#mutations: Map<string, Array<Mutation<any, any, any, any>>>
#mutations: Set<Mutation<any, any, any, any>>
#scopes: Map<string, Array<Mutation<any, any, any, any>>>
#mutationId: number

constructor(public config: MutationCacheConfig = {}) {
super()
this.#mutations = new Map()
this.#mutationId = Date.now()
this.#mutations = new Set()
this.#scopes = new Map()
this.#mutationId = 0
}

build<TData, TError, TVariables, TContext>(
Expand All @@ -109,59 +111,84 @@ export class MutationCache extends Subscribable<MutationCacheListener> {
}

add(mutation: Mutation<any, any, any, any>): void {
this.#mutations.add(mutation)
const scope = scopeFor(mutation)
const mutations = this.#mutations.get(scope) ?? []
mutations.push(mutation)
this.#mutations.set(scope, mutations)
if (typeof scope === 'string') {
const scopedMutations = this.#scopes.get(scope)
if (scopedMutations) {
scopedMutations.push(mutation)
} else {
this.#scopes.set(scope, [mutation])
}
}
this.notify({ type: 'added', mutation })
}

remove(mutation: Mutation<any, any, any, any>): void {
const scope = scopeFor(mutation)
if (this.#mutations.has(scope)) {
const mutations = this.#mutations
.get(scope)
?.filter((x) => x !== mutation)
if (mutations) {
if (mutations.length === 0) {
this.#mutations.delete(scope)
} else {
this.#mutations.set(scope, mutations)
if (this.#mutations.delete(mutation)) {
const scope = scopeFor(mutation)
if (typeof scope === 'string') {
const scopedMutations = this.#scopes.get(scope)
if (scopedMutations) {
if (scopedMutations.length > 1) {
const index = scopedMutations.indexOf(mutation)
if (index !== -1) {
scopedMutations.splice(index, 1)
}
} else if (scopedMutations[0] === mutation) {
this.#scopes.delete(scope)
}
}
}
}

// Currently we notify the removal even if the mutation was already removed.
// Consider making this an error or not notifying of the removal depending on the desired semantics.
this.notify({ type: 'removed', mutation })
}

canRun(mutation: Mutation<any, any, any, any>): boolean {
const firstPendingMutation = this.#mutations
.get(scopeFor(mutation))
?.find((m) => m.state.status === 'pending')

// we can run if there is no current pending mutation (start use-case)
// or if WE are the first pending mutation (continue use-case)
return !firstPendingMutation || firstPendingMutation === mutation
const scope = scopeFor(mutation)
if (typeof scope === 'string') {
const mutationsWithSameScope = this.#scopes.get(scope)
const firstPendingMutation = mutationsWithSameScope?.find(
(m) => m.state.status === 'pending',
)
// we can run if there is no current pending mutation (start use-case)
// or if WE are the first pending mutation (continue use-case)
return !firstPendingMutation || firstPendingMutation === mutation
} else {
// For unscoped mutations there are never any pending mutations in front of the
// current mutation
return true
}
}

runNext(mutation: Mutation<any, any, any, any>): Promise<unknown> {
const foundMutation = this.#mutations
.get(scopeFor(mutation))
?.find((m) => m !== mutation && m.state.isPaused)
const scope = scopeFor(mutation)
if (typeof scope === 'string') {
const foundMutation = this.#scopes
.get(scope)
?.find((m) => m !== mutation && m.state.isPaused)

return foundMutation?.continue() ?? Promise.resolve()
return foundMutation?.continue() ?? Promise.resolve()
} else {
return Promise.resolve()
}
}

clear(): void {
notifyManager.batch(() => {
this.getAll().forEach((mutation) => {
this.remove(mutation)
this.#mutations.forEach((mutation) => {
this.notify({ type: 'removed', mutation })
})
this.#mutations.clear()
this.#scopes.clear()
})
}

getAll(): Array<Mutation> {
return [...this.#mutations.values()].flat()
return Array.from(this.#mutations)
}

find<
Expand Down Expand Up @@ -203,5 +230,5 @@ export class MutationCache extends Subscribable<MutationCacheListener> {
}

function scopeFor(mutation: Mutation<any, any, any, any>) {
return mutation.options.scope?.id ?? String(mutation.mutationId)
return mutation.options.scope?.id
}

0 comments on commit 19b95c4

Please sign in to comment.