Skip to content

Commit 13ee4c5

Browse files
authored
Cleanup polls on unsubscribeQueryResult (#1933)
* Cleanup polls on unsubscribeQueryResult * Add polling tests
1 parent a7af01d commit 13ee4c5

File tree

2 files changed

+127
-9
lines changed

2 files changed

+127
-9
lines changed

packages/toolkit/src/query/core/buildMiddleware/polling.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ export const build: SubMiddlewareBuilder = ({
1919
timeout?: TimeoutId
2020
pollingInterval: number
2121
}> = {}
22+
2223
return (next) =>
2324
(action): any => {
2425
const result = next(action)
2526

26-
if (api.internalActions.updateSubscriptionOptions.match(action)) {
27+
if (
28+
api.internalActions.updateSubscriptionOptions.match(action) ||
29+
api.internalActions.unsubscribeQueryResult.match(action)
30+
) {
2731
updatePollingInterval(action.payload, mwApi)
2832
}
2933

@@ -99,27 +103,31 @@ export const build: SubMiddlewareBuilder = ({
99103
}
100104

101105
const lowestPollingInterval = findLowestPollingInterval(subscriptions)
102-
const currentPoll = currentPolls[queryCacheKey]
103106

104107
if (!Number.isFinite(lowestPollingInterval)) {
105-
if (currentPoll?.timeout) {
106-
clearTimeout(currentPoll.timeout)
107-
}
108-
delete currentPolls[queryCacheKey]
108+
cleanupPollForKey(queryCacheKey)
109109
return
110110
}
111111

112+
const currentPoll = currentPolls[queryCacheKey]
112113
const nextPollTimestamp = Date.now() + lowestPollingInterval
113114

114115
if (!currentPoll || nextPollTimestamp < currentPoll.nextPollTimestamp) {
115116
startNextPoll({ queryCacheKey }, api)
116117
}
117118
}
118119

120+
function cleanupPollForKey(key: string) {
121+
const existingPoll = currentPolls[key]
122+
if (existingPoll?.timeout) {
123+
clearTimeout(existingPoll.timeout)
124+
}
125+
delete currentPolls[key]
126+
}
127+
119128
function clearPolls() {
120-
for (const [key, poll] of Object.entries(currentPolls)) {
121-
if (poll?.timeout) clearTimeout(poll.timeout)
122-
delete currentPolls[key]
129+
for (const key of Object.keys(currentPolls)) {
130+
cleanupPollForKey(key)
123131
}
124132
}
125133
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { createApi } from '@reduxjs/toolkit/query'
2+
import { setupApiStore, waitMs } from './helpers'
3+
4+
const mockBaseQuery = jest
5+
.fn()
6+
.mockImplementation((args: any) => ({ data: args }))
7+
8+
const api = createApi({
9+
baseQuery: mockBaseQuery,
10+
tagTypes: ['Posts'],
11+
endpoints: (build) => ({
12+
getPosts: build.query<unknown, number>({
13+
query(pageNumber) {
14+
return { url: 'posts', params: pageNumber }
15+
},
16+
providesTags: ['Posts'],
17+
}),
18+
}),
19+
})
20+
const { getPosts } = api.endpoints
21+
22+
const storeRef = setupApiStore(api)
23+
24+
const getSubscribersForQueryCacheKey = (queryCacheKey: string) =>
25+
storeRef.store.getState()[api.reducerPath].subscriptions[queryCacheKey] || {}
26+
const createSubscriptionGetter = (queryCacheKey: string) => () =>
27+
getSubscribersForQueryCacheKey(queryCacheKey)
28+
29+
describe('polling tests', () => {
30+
it('clears intervals when seeing a resetApiState action', async () => {
31+
await storeRef.store.dispatch(
32+
getPosts.initiate(1, {
33+
subscriptionOptions: { pollingInterval: 10 },
34+
subscribe: true,
35+
})
36+
)
37+
38+
expect(mockBaseQuery).toHaveBeenCalledTimes(1)
39+
40+
storeRef.store.dispatch(api.util.resetApiState())
41+
42+
await waitMs(30)
43+
44+
expect(mockBaseQuery).toHaveBeenCalledTimes(1)
45+
})
46+
47+
it('replaces polling interval when the subscription options are updated', async () => {
48+
const { requestId, queryCacheKey, ...subscription } =
49+
storeRef.store.dispatch(
50+
getPosts.initiate(1, {
51+
subscriptionOptions: { pollingInterval: 10 },
52+
subscribe: true,
53+
})
54+
)
55+
56+
const getSubs = createSubscriptionGetter(queryCacheKey)
57+
58+
expect(Object.keys(getSubs())).toHaveLength(1)
59+
expect(getSubs()[requestId].pollingInterval).toBe(10)
60+
61+
subscription.updateSubscriptionOptions({ pollingInterval: 20 })
62+
63+
expect(Object.keys(getSubs())).toHaveLength(1)
64+
expect(getSubs()[requestId].pollingInterval).toBe(20)
65+
})
66+
67+
it(`doesn't replace the interval when removing a shared query instance with a poll `, async () => {
68+
const subscriptionOne = storeRef.store.dispatch(
69+
getPosts.initiate(1, {
70+
subscriptionOptions: { pollingInterval: 10 },
71+
subscribe: true,
72+
})
73+
)
74+
75+
storeRef.store.dispatch(
76+
getPosts.initiate(1, {
77+
subscriptionOptions: { pollingInterval: 10 },
78+
subscribe: true,
79+
})
80+
)
81+
82+
const getSubs = createSubscriptionGetter(subscriptionOne.queryCacheKey)
83+
84+
expect(Object.keys(getSubs())).toHaveLength(2)
85+
86+
subscriptionOne.unsubscribe()
87+
88+
expect(Object.keys(getSubs())).toHaveLength(1)
89+
})
90+
91+
it('uses lowest specified interval when two components are mounted', async () => {
92+
storeRef.store.dispatch(
93+
getPosts.initiate(1, {
94+
subscriptionOptions: { pollingInterval: 30000 },
95+
subscribe: true,
96+
})
97+
)
98+
99+
storeRef.store.dispatch(
100+
getPosts.initiate(1, {
101+
subscriptionOptions: { pollingInterval: 10 },
102+
subscribe: true,
103+
})
104+
)
105+
106+
await waitMs(20)
107+
108+
expect(mockBaseQuery.mock.calls.length).toBeGreaterThanOrEqual(2)
109+
})
110+
})

0 commit comments

Comments
 (0)