Skip to content

Commit

Permalink
fix(core): make sure pausedMutations always execute in serial (#4902)
Browse files Browse the repository at this point in the history
* fix: make sure pausedMutations always execute in serial

when resumePausedMutations is called multiple times, we are starting the second paused mutation even though the first one has not yet finished. This can happen when a focus event and an online event happen in short succession

* fix: always return the same Promise when resumePausedMutations is called
  • Loading branch information
TkDodo committed Jan 30, 2023
1 parent 65cd596 commit 8991d7c
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 9 deletions.
25 changes: 17 additions & 8 deletions packages/query-core/src/mutationCache.ts
Expand Up @@ -79,6 +79,7 @@ export class MutationCache extends Subscribable<MutationCacheListener> {

private mutations: Mutation<any, any, any, any>[]
private mutationId: number
private resuming: Promise<void> | undefined

constructor(config?: MutationCacheConfig) {
super()
Expand Down Expand Up @@ -153,13 +154,21 @@ export class MutationCache extends Subscribable<MutationCacheListener> {
}

resumePausedMutations(): Promise<void> {
const pausedMutations = this.mutations.filter((x) => x.state.isPaused)
return notifyManager.batch(() =>
pausedMutations.reduce(
(promise, mutation) =>
promise.then(() => mutation.continue().catch(noop)),
Promise.resolve(),
),
)
if (!this.resuming) {
const pausedMutations = this.mutations.filter((x) => x.state.isPaused)
this.resuming = notifyManager
.batch(() =>
pausedMutations.reduce(
(promise, mutation) =>
promise.then(() => mutation.continue().catch(noop)),
Promise.resolve(),
),
)
.then(() => {
this.resuming = undefined
})
}

return this.resuming
}
}
80 changes: 79 additions & 1 deletion packages/query-core/src/tests/queryClient.test.tsx
Expand Up @@ -8,8 +8,9 @@ import type {
QueryFunction,
QueryObserverOptions,
} from '..'
import { InfiniteQueryObserver, QueryObserver } from '..'
import { InfiniteQueryObserver, MutationObserver, QueryObserver } from '..'
import { focusManager, onlineManager } from '..'
import { noop } from '../utils'

describe('queryClient', () => {
let queryClient: QueryClient
Expand Down Expand Up @@ -1468,6 +1469,83 @@ describe('queryClient', () => {
onlineManager.setOnline(undefined)
})

test('should resume paused mutations when coming online', async () => {
const consoleMock = jest.spyOn(console, 'error')
consoleMock.mockImplementation(() => undefined)
onlineManager.setOnline(false)

const observer1 = new MutationObserver(queryClient, {
mutationFn: async () => 1,
})

const observer2 = new MutationObserver(queryClient, {
mutationFn: async () => 2,
})
void observer1.mutate().catch(noop)
void observer2.mutate().catch(noop)

await waitFor(() => {
expect(observer1.getCurrentResult().isPaused).toBeTruthy()
expect(observer2.getCurrentResult().isPaused).toBeTruthy()
})

onlineManager.setOnline(true)

await waitFor(() => {
expect(observer1.getCurrentResult().status).toBe('success')
expect(observer1.getCurrentResult().status).toBe('success')
})

onlineManager.setOnline(undefined)
})

test('should resume paused mutations one after the other when invoked manually at the same time', async () => {
const consoleMock = jest.spyOn(console, 'error')
consoleMock.mockImplementation(() => undefined)
onlineManager.setOnline(false)

const orders: Array<string> = []

const observer1 = new MutationObserver(queryClient, {
mutationFn: async () => {
orders.push('1start')
await sleep(50)
orders.push('1end')
return 1
},
})

const observer2 = new MutationObserver(queryClient, {
mutationFn: async () => {
orders.push('2start')
await sleep(20)
orders.push('2end')
return 2
},
})
void observer1.mutate().catch(noop)
void observer2.mutate().catch(noop)

await waitFor(() => {
expect(observer1.getCurrentResult().isPaused).toBeTruthy()
expect(observer2.getCurrentResult().isPaused).toBeTruthy()
})

onlineManager.setOnline(undefined)
void queryClient.resumePausedMutations()
await sleep(5)
await queryClient.resumePausedMutations()

await waitFor(() => {
expect(observer1.getCurrentResult().status).toBe('success')
expect(observer2.getCurrentResult().status).toBe('success')
})

console.log('orders', orders)

expect(orders).toEqual(['1start', '1end', '2start', '2end'])
})

test('should notify queryCache and mutationCache after multiple mounts and single unmount', async () => {
const testClient = createQueryClient()
testClient.mount()
Expand Down

0 comments on commit 8991d7c

Please sign in to comment.