Describe the bug
Mutation.execute() never clears this.#retryer after the mutation settles, unlike Query.fetch(), which just got this exact fix in #11163 (merged today). This means a settled Mutation instance keeps its Retryer (and everything closed over by it: the resolved/rejected result, variables, mutationFnContext) reachable for as long as the Mutation instance itself is retained by MutationCache.
Your minimal, reproducible example
packages/query-core/src/mutation.ts, execute():
} finally {
this.#mutationCache.runNext(this)
}
Compare with packages/query-core/src/query.ts's fetch() after #11163:
} finally {
// The settled retryer's promise would otherwise pin this fetch's raw
// result (a second copy after structural sharing) for the query's lifetime
if (this.#retryer === retryer) {
this.#retryer = undefined
}
this.scheduleGc()
}
Mutation's finally block only calls runNext, it never sets this.#retryer = undefined.
Steps to reproduce
- Look at
#11163's changeset/PR description: Query.fetch() left #retryer set after a fetch settled, and the retryer's resolved promise held the raw fetch result, so a query that had refetched held its data twice: once via structural sharing in state.data, and again through the settled retryer's closure. Same shape, same class, different method.
Mutation.execute() builds this.#retryer = createRetryer({...}) the same way Query.fetch() does, awaits this.#retryer.start() inside try, and has a finally block — but that finally only runs this.#mutationCache.runNext(this), it never releases this.#retryer.
MutationCache keeps completed Mutation instances around until gcTime elapses (or indefinitely while an observer/dehydration still references them, per Removable/scheduleGc), so for that whole window the settled retryer (and whatever it closed over) stays reachable through mutation.#retryer, not just through mutation.state.data/variables.
Expected behavior
Same treatment Query.fetch() just got: once the retryer's promise settles, release the reference (guarded by an identity check like if (this.#retryer === retryer), since execute() can in principle be called again on the same instance via continue()).
Additional context
Not filing a PR myself, wanted to flag this mirrors #11163 closely enough that whoever wrote that fix for Query is probably the right person to confirm the same identity-check subtlety applies to Mutation (I don't have full confidence on interaction with continue() re-entering execute() on the same instance).
Describe the bug
Mutation.execute()never clearsthis.#retryerafter the mutation settles, unlikeQuery.fetch(), which just got this exact fix in #11163 (merged today). This means a settledMutationinstance keeps itsRetryer(and everything closed over by it: the resolved/rejected result,variables,mutationFnContext) reachable for as long as theMutationinstance itself is retained byMutationCache.Your minimal, reproducible example
packages/query-core/src/mutation.ts,execute():Compare with
packages/query-core/src/query.ts'sfetch()after #11163:Mutation'sfinallyblock only callsrunNext, it never setsthis.#retryer = undefined.Steps to reproduce
#11163's changeset/PR description:Query.fetch()left#retryerset after a fetch settled, and the retryer's resolved promise held the raw fetch result, so a query that had refetched held its data twice: once via structural sharing instate.data, and again through the settled retryer's closure. Same shape, same class, different method.Mutation.execute()buildsthis.#retryer = createRetryer({...})the same wayQuery.fetch()does,awaitsthis.#retryer.start()insidetry, and has afinallyblock — but thatfinallyonly runsthis.#mutationCache.runNext(this), it never releasesthis.#retryer.MutationCachekeeps completedMutationinstances around untilgcTimeelapses (or indefinitely while an observer/dehydration still references them, perRemovable/scheduleGc), so for that whole window the settled retryer (and whatever it closed over) stays reachable throughmutation.#retryer, not just throughmutation.state.data/variables.Expected behavior
Same treatment
Query.fetch()just got: once the retryer's promise settles, release the reference (guarded by an identity check likeif (this.#retryer === retryer), sinceexecute()can in principle be called again on the same instance viacontinue()).Additional context
Not filing a PR myself, wanted to flag this mirrors #11163 closely enough that whoever wrote that fix for
Queryis probably the right person to confirm the same identity-check subtlety applies toMutation(I don't have full confidence on interaction withcontinue()re-enteringexecute()on the same instance).