Skip to content

Commit 19b95c4

Browse files
authored
perf(query-core): Improve mutationCache implementation performance (#8496)
--------- Co-authored-by: Josh Story <[email protected]>
1 parent 3124193 commit 19b95c4

File tree

1 file changed

+58
-31
lines changed

1 file changed

+58
-31
lines changed

packages/query-core/src/mutationCache.ts

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,15 @@ type MutationCacheListener = (event: MutationCacheNotifyEvent) => void
8282
// CLASS
8383

8484
export class MutationCache extends Subscribable<MutationCacheListener> {
85-
#mutations: Map<string, Array<Mutation<any, any, any, any>>>
85+
#mutations: Set<Mutation<any, any, any, any>>
86+
#scopes: Map<string, Array<Mutation<any, any, any, any>>>
8687
#mutationId: number
8788

8889
constructor(public config: MutationCacheConfig = {}) {
8990
super()
90-
this.#mutations = new Map()
91-
this.#mutationId = Date.now()
91+
this.#mutations = new Set()
92+
this.#scopes = new Map()
93+
this.#mutationId = 0
9294
}
9395

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

111113
add(mutation: Mutation<any, any, any, any>): void {
114+
this.#mutations.add(mutation)
112115
const scope = scopeFor(mutation)
113-
const mutations = this.#mutations.get(scope) ?? []
114-
mutations.push(mutation)
115-
this.#mutations.set(scope, mutations)
116+
if (typeof scope === 'string') {
117+
const scopedMutations = this.#scopes.get(scope)
118+
if (scopedMutations) {
119+
scopedMutations.push(mutation)
120+
} else {
121+
this.#scopes.set(scope, [mutation])
122+
}
123+
}
116124
this.notify({ type: 'added', mutation })
117125
}
118126

119127
remove(mutation: Mutation<any, any, any, any>): void {
120-
const scope = scopeFor(mutation)
121-
if (this.#mutations.has(scope)) {
122-
const mutations = this.#mutations
123-
.get(scope)
124-
?.filter((x) => x !== mutation)
125-
if (mutations) {
126-
if (mutations.length === 0) {
127-
this.#mutations.delete(scope)
128-
} else {
129-
this.#mutations.set(scope, mutations)
128+
if (this.#mutations.delete(mutation)) {
129+
const scope = scopeFor(mutation)
130+
if (typeof scope === 'string') {
131+
const scopedMutations = this.#scopes.get(scope)
132+
if (scopedMutations) {
133+
if (scopedMutations.length > 1) {
134+
const index = scopedMutations.indexOf(mutation)
135+
if (index !== -1) {
136+
scopedMutations.splice(index, 1)
137+
}
138+
} else if (scopedMutations[0] === mutation) {
139+
this.#scopes.delete(scope)
140+
}
130141
}
131142
}
132143
}
133144

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

137150
canRun(mutation: Mutation<any, any, any, any>): boolean {
138-
const firstPendingMutation = this.#mutations
139-
.get(scopeFor(mutation))
140-
?.find((m) => m.state.status === 'pending')
141-
142-
// we can run if there is no current pending mutation (start use-case)
143-
// or if WE are the first pending mutation (continue use-case)
144-
return !firstPendingMutation || firstPendingMutation === mutation
151+
const scope = scopeFor(mutation)
152+
if (typeof scope === 'string') {
153+
const mutationsWithSameScope = this.#scopes.get(scope)
154+
const firstPendingMutation = mutationsWithSameScope?.find(
155+
(m) => m.state.status === 'pending',
156+
)
157+
// we can run if there is no current pending mutation (start use-case)
158+
// or if WE are the first pending mutation (continue use-case)
159+
return !firstPendingMutation || firstPendingMutation === mutation
160+
} else {
161+
// For unscoped mutations there are never any pending mutations in front of the
162+
// current mutation
163+
return true
164+
}
145165
}
146166

147167
runNext(mutation: Mutation<any, any, any, any>): Promise<unknown> {
148-
const foundMutation = this.#mutations
149-
.get(scopeFor(mutation))
150-
?.find((m) => m !== mutation && m.state.isPaused)
168+
const scope = scopeFor(mutation)
169+
if (typeof scope === 'string') {
170+
const foundMutation = this.#scopes
171+
.get(scope)
172+
?.find((m) => m !== mutation && m.state.isPaused)
151173

152-
return foundMutation?.continue() ?? Promise.resolve()
174+
return foundMutation?.continue() ?? Promise.resolve()
175+
} else {
176+
return Promise.resolve()
177+
}
153178
}
154179

155180
clear(): void {
156181
notifyManager.batch(() => {
157-
this.getAll().forEach((mutation) => {
158-
this.remove(mutation)
182+
this.#mutations.forEach((mutation) => {
183+
this.notify({ type: 'removed', mutation })
159184
})
185+
this.#mutations.clear()
186+
this.#scopes.clear()
160187
})
161188
}
162189

163190
getAll(): Array<Mutation> {
164-
return [...this.#mutations.values()].flat()
191+
return Array.from(this.#mutations)
165192
}
166193

167194
find<
@@ -203,5 +230,5 @@ export class MutationCache extends Subscribable<MutationCacheListener> {
203230
}
204231

205232
function scopeFor(mutation: Mutation<any, any, any, any>) {
206-
return mutation.options.scope?.id ?? String(mutation.mutationId)
233+
return mutation.options.scope?.id
207234
}

0 commit comments

Comments
 (0)