From 5d63686ae383d26b05417b980682c1cfefeed1b1 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Date: Thu, 13 Aug 2026 11:12:18 +0530 Subject: [PATCH] fix(angular-query): hold a pending task while a triggered mutation runs 'mutate' is fire and forget, so nothing else keeps the application busy while the mutation runs. The pending task was registered from the observer subscription callback, which is batched through the notify manager and therefore runs in a later task than the mutation it reports. Between the 'mutate' call and that first notification the application looks stable, so 'ApplicationRef.whenStable()' resolves while the mutation is still in flight and the result signals still read as idle. Take the pending task in 'mutate' itself and release it once the mutation settles. 'mutateAsync' is unaffected because it hands the caller a promise to await. --- .changeset/tidy-moons-repeat.md | 5 ++++ .../src/__tests__/pending-tasks.test.ts | 25 +++++++++++++++++++ .../src/inject-mutation.ts | 10 +++++++- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-moons-repeat.md diff --git a/.changeset/tidy-moons-repeat.md b/.changeset/tidy-moons-repeat.md new file mode 100644 index 00000000000..4c2511d0d28 --- /dev/null +++ b/.changeset/tidy-moons-repeat.md @@ -0,0 +1,5 @@ +--- +'@tanstack/angular-query-experimental': patch +--- + +Register the Angular pending task when `mutate` is called, so `whenStable()` no longer resolves while the mutation is still running diff --git a/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts b/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts index c5a1f58d81f..ec71e9840f0 100644 --- a/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts +++ b/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts @@ -155,6 +155,31 @@ describe('PendingTasks Integration', () => { }) }) + // The observer reports a running mutation through a batched notification that + // only lands in a later task. This test uses real timers because it asserts + // on what `whenStable()` does before that notification arrives. + describe('Task registration timing', () => { + it('should let whenStable wait for a mutation that was just triggered', async () => { + vi.useRealTimers() + + const app = TestBed.inject(ApplicationRef) + + const mutation = TestBed.runInInjectionContext(() => + injectMutation(() => ({ + mutationFn: (value: string) => sleep(5).then(() => value), + })), + ) + + TestBed.tick() + mutation.mutate('mutated') + + await app.whenStable() + + expect(mutation.status()).toBe('success') + expect(mutation.data()).toBe('mutated') + }) + }) + describe('Race Conditions', () => { it('should handle query that completes during initial subscription', async () => { const key = queryKey() diff --git a/packages/angular-query-experimental/src/inject-mutation.ts b/packages/angular-query-experimental/src/inject-mutation.ts index 7eb605047f3..94ede348d56 100644 --- a/packages/angular-query-experimental/src/inject-mutation.ts +++ b/packages/angular-query-experimental/src/inject-mutation.ts @@ -87,7 +87,15 @@ export function injectMutation< >(() => { const observer = observerSignal() return (variables, mutateOptions) => { - observer.mutate(variables, mutateOptions).catch(noop) + // `mutate` is fire and forget, so nothing else keeps the application + // busy while the mutation runs. The observer reports the pending state + // in a batched notification that only arrives in a later task, so hold a + // pending task from the moment the mutation starts instead. + const releasePendingTask = pendingTasks.add() + observer + .mutate(variables, mutateOptions) + .catch(noop) + .finally(releasePendingTask) } })