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) } })