@tanstack/query-core 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/focusManager.js +101 -0
- package/build/cjs/focusManager.js.map +1 -0
- package/build/cjs/hydration.js +112 -0
- package/build/cjs/hydration.js.map +1 -0
- package/build/cjs/index.js +51 -0
- package/build/cjs/index.js.map +1 -0
- package/build/cjs/infiniteQueryBehavior.js +160 -0
- package/build/cjs/infiniteQueryBehavior.js.map +1 -0
- package/build/cjs/infiniteQueryObserver.js +92 -0
- package/build/cjs/infiniteQueryObserver.js.map +1 -0
- package/build/cjs/logger.js +18 -0
- package/build/cjs/logger.js.map +1 -0
- package/build/cjs/mutation.js +258 -0
- package/build/cjs/mutation.js.map +1 -0
- package/build/cjs/mutationCache.js +99 -0
- package/build/cjs/mutationCache.js.map +1 -0
- package/build/cjs/mutationObserver.js +130 -0
- package/build/cjs/mutationObserver.js.map +1 -0
- package/build/cjs/notifyManager.js +114 -0
- package/build/cjs/notifyManager.js.map +1 -0
- package/build/cjs/onlineManager.js +100 -0
- package/build/cjs/onlineManager.js.map +1 -0
- package/build/cjs/queriesObserver.js +170 -0
- package/build/cjs/queriesObserver.js.map +1 -0
- package/build/cjs/query.js +474 -0
- package/build/cjs/query.js.map +1 -0
- package/build/cjs/queryCache.js +140 -0
- package/build/cjs/queryCache.js.map +1 -0
- package/build/cjs/queryClient.js +357 -0
- package/build/cjs/queryClient.js.map +1 -0
- package/build/cjs/queryObserver.js +521 -0
- package/build/cjs/queryObserver.js.map +1 -0
- package/build/cjs/removable.js +47 -0
- package/build/cjs/removable.js.map +1 -0
- package/build/cjs/retryer.js +177 -0
- package/build/cjs/retryer.js.map +1 -0
- package/build/cjs/subscribable.js +43 -0
- package/build/cjs/subscribable.js.map +1 -0
- package/build/cjs/utils.js +356 -0
- package/build/cjs/utils.js.map +1 -0
- package/build/esm/index.js +3077 -0
- package/build/esm/index.js.map +1 -0
- package/build/stats-html.html +2689 -0
- package/build/umd/index.development.js +3106 -0
- package/build/umd/index.development.js.map +1 -0
- package/build/umd/index.production.js +12 -0
- package/build/umd/index.production.js.map +1 -0
- package/package.json +25 -0
- package/src/focusManager.ts +89 -0
- package/src/hydration.ts +164 -0
- package/src/index.ts +35 -0
- package/src/infiniteQueryBehavior.ts +214 -0
- package/src/infiniteQueryObserver.ts +159 -0
- package/src/logger.native.ts +11 -0
- package/src/logger.ts +9 -0
- package/src/mutation.ts +349 -0
- package/src/mutationCache.ts +157 -0
- package/src/mutationObserver.ts +195 -0
- package/src/notifyManager.ts +96 -0
- package/src/onlineManager.ts +89 -0
- package/src/queriesObserver.ts +211 -0
- package/src/query.ts +612 -0
- package/src/queryCache.ts +206 -0
- package/src/queryClient.ts +716 -0
- package/src/queryObserver.ts +748 -0
- package/src/removable.ts +37 -0
- package/src/retryer.ts +215 -0
- package/src/subscribable.ts +33 -0
- package/src/tests/focusManager.test.tsx +155 -0
- package/src/tests/hydration.test.tsx +429 -0
- package/src/tests/infiniteQueryBehavior.test.tsx +124 -0
- package/src/tests/infiniteQueryObserver.test.tsx +64 -0
- package/src/tests/mutationCache.test.tsx +260 -0
- package/src/tests/mutationObserver.test.tsx +75 -0
- package/src/tests/mutations.test.tsx +363 -0
- package/src/tests/notifyManager.test.tsx +51 -0
- package/src/tests/onlineManager.test.tsx +148 -0
- package/src/tests/queriesObserver.test.tsx +330 -0
- package/src/tests/query.test.tsx +888 -0
- package/src/tests/queryCache.test.tsx +236 -0
- package/src/tests/queryClient.test.tsx +1435 -0
- package/src/tests/queryObserver.test.tsx +802 -0
- package/src/tests/utils.test.tsx +360 -0
- package/src/types.ts +705 -0
- package/src/utils.ts +435 -0
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sleep,
|
|
3
|
+
queryKey,
|
|
4
|
+
mockVisibilityState,
|
|
5
|
+
mockLogger,
|
|
6
|
+
createQueryClient,
|
|
7
|
+
} from '../../../../tests/utils'
|
|
8
|
+
import {
|
|
9
|
+
QueryCache,
|
|
10
|
+
QueryClient,
|
|
11
|
+
QueryObserver,
|
|
12
|
+
isCancelledError,
|
|
13
|
+
isError,
|
|
14
|
+
onlineManager,
|
|
15
|
+
QueryFunctionContext,
|
|
16
|
+
QueryObserverResult,
|
|
17
|
+
} from '..'
|
|
18
|
+
import { waitFor } from '@testing-library/react'
|
|
19
|
+
|
|
20
|
+
describe('query', () => {
|
|
21
|
+
let queryClient: QueryClient
|
|
22
|
+
let queryCache: QueryCache
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
queryClient = createQueryClient()
|
|
26
|
+
queryCache = queryClient.getQueryCache()
|
|
27
|
+
queryClient.mount()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
queryClient.clear()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('should use the longest cache time it has seen', async () => {
|
|
35
|
+
const key = queryKey()
|
|
36
|
+
await queryClient.prefetchQuery(key, () => 'data', {
|
|
37
|
+
cacheTime: 100,
|
|
38
|
+
})
|
|
39
|
+
await queryClient.prefetchQuery(key, () => 'data', {
|
|
40
|
+
cacheTime: 200,
|
|
41
|
+
})
|
|
42
|
+
await queryClient.prefetchQuery(key, () => 'data', {
|
|
43
|
+
cacheTime: 10,
|
|
44
|
+
})
|
|
45
|
+
const query = queryCache.find(key)!
|
|
46
|
+
expect(query.cacheTime).toBe(200)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should continue retry after focus regain and resolve all promises', async () => {
|
|
50
|
+
const key = queryKey()
|
|
51
|
+
|
|
52
|
+
// make page unfocused
|
|
53
|
+
const visibilityMock = mockVisibilityState('hidden')
|
|
54
|
+
|
|
55
|
+
let count = 0
|
|
56
|
+
let result
|
|
57
|
+
|
|
58
|
+
const promise = queryClient.fetchQuery(
|
|
59
|
+
key,
|
|
60
|
+
async () => {
|
|
61
|
+
count++
|
|
62
|
+
|
|
63
|
+
if (count === 3) {
|
|
64
|
+
return `data${count}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(`error${count}`)
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
retry: 3,
|
|
71
|
+
retryDelay: 1,
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
promise.then((data) => {
|
|
76
|
+
result = data
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Check if we do not have a result
|
|
80
|
+
expect(result).toBeUndefined()
|
|
81
|
+
|
|
82
|
+
// Check if the query is really paused
|
|
83
|
+
await sleep(50)
|
|
84
|
+
expect(result).toBeUndefined()
|
|
85
|
+
|
|
86
|
+
// Reset visibilityState to original value
|
|
87
|
+
visibilityMock.mockRestore()
|
|
88
|
+
window.dispatchEvent(new FocusEvent('focus'))
|
|
89
|
+
|
|
90
|
+
// There should not be a result yet
|
|
91
|
+
expect(result).toBeUndefined()
|
|
92
|
+
|
|
93
|
+
// By now we should have a value
|
|
94
|
+
await sleep(50)
|
|
95
|
+
expect(result).toBe('data3')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should continue retry after reconnect and resolve all promises', async () => {
|
|
99
|
+
const key = queryKey()
|
|
100
|
+
|
|
101
|
+
onlineManager.setOnline(false)
|
|
102
|
+
|
|
103
|
+
let count = 0
|
|
104
|
+
let result
|
|
105
|
+
|
|
106
|
+
const promise = queryClient.fetchQuery(
|
|
107
|
+
key,
|
|
108
|
+
async () => {
|
|
109
|
+
count++
|
|
110
|
+
|
|
111
|
+
if (count === 3) {
|
|
112
|
+
return `data${count}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error(`error${count}`)
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
retry: 3,
|
|
119
|
+
retryDelay: 1,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
promise.then((data) => {
|
|
124
|
+
result = data
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Check if we do not have a result
|
|
128
|
+
expect(result).toBeUndefined()
|
|
129
|
+
|
|
130
|
+
// Check if the query is really paused
|
|
131
|
+
await sleep(50)
|
|
132
|
+
expect(result).toBeUndefined()
|
|
133
|
+
|
|
134
|
+
// Reset navigator to original value
|
|
135
|
+
onlineManager.setOnline(true)
|
|
136
|
+
|
|
137
|
+
// There should not be a result yet
|
|
138
|
+
expect(result).toBeUndefined()
|
|
139
|
+
|
|
140
|
+
// By now we should have a value
|
|
141
|
+
await sleep(50)
|
|
142
|
+
expect(result).toBe('data3')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should throw a CancelledError when a paused query is cancelled', async () => {
|
|
146
|
+
const key = queryKey()
|
|
147
|
+
|
|
148
|
+
// make page unfocused
|
|
149
|
+
const visibilityMock = mockVisibilityState('hidden')
|
|
150
|
+
|
|
151
|
+
let count = 0
|
|
152
|
+
let result
|
|
153
|
+
|
|
154
|
+
const promise = queryClient.fetchQuery(
|
|
155
|
+
key,
|
|
156
|
+
async (): Promise<unknown> => {
|
|
157
|
+
count++
|
|
158
|
+
throw new Error(`error${count}`)
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
retry: 3,
|
|
162
|
+
retryDelay: 1,
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
promise.catch((data) => {
|
|
167
|
+
result = data
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const query = queryCache.find(key)!
|
|
171
|
+
|
|
172
|
+
// Check if the query is really paused
|
|
173
|
+
await sleep(50)
|
|
174
|
+
expect(result).toBeUndefined()
|
|
175
|
+
|
|
176
|
+
// Cancel query
|
|
177
|
+
query.cancel()
|
|
178
|
+
|
|
179
|
+
// Check if the error is set to the cancelled error
|
|
180
|
+
await sleep(0)
|
|
181
|
+
expect(isCancelledError(result)).toBe(true)
|
|
182
|
+
|
|
183
|
+
// Reset visibilityState to original value
|
|
184
|
+
visibilityMock.mockRestore()
|
|
185
|
+
window.dispatchEvent(new FocusEvent('focus'))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('should provide context to queryFn', async () => {
|
|
189
|
+
const key = queryKey()
|
|
190
|
+
|
|
191
|
+
const queryFn = jest
|
|
192
|
+
.fn<
|
|
193
|
+
Promise<'data'>,
|
|
194
|
+
[QueryFunctionContext<ReturnType<typeof queryKey>>]
|
|
195
|
+
>()
|
|
196
|
+
.mockResolvedValue('data')
|
|
197
|
+
|
|
198
|
+
queryClient.prefetchQuery(key, queryFn)
|
|
199
|
+
|
|
200
|
+
await sleep(10)
|
|
201
|
+
|
|
202
|
+
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
203
|
+
const args = queryFn.mock.calls[0]![0]
|
|
204
|
+
expect(args).toBeDefined()
|
|
205
|
+
expect(args.pageParam).toBeUndefined()
|
|
206
|
+
expect(args.queryKey).toEqual(key)
|
|
207
|
+
if (typeof AbortSignal === 'function') {
|
|
208
|
+
expect(args.signal).toBeInstanceOf(AbortSignal)
|
|
209
|
+
} else {
|
|
210
|
+
expect(args.signal).toBeUndefined()
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('should continue if cancellation is not supported and signal is not consumed', async () => {
|
|
215
|
+
const key = queryKey()
|
|
216
|
+
|
|
217
|
+
queryClient.prefetchQuery(key, async () => {
|
|
218
|
+
await sleep(100)
|
|
219
|
+
return 'data'
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
await sleep(10)
|
|
223
|
+
|
|
224
|
+
// Subscribe and unsubscribe to simulate cancellation because the last observer unsubscribed
|
|
225
|
+
const observer = new QueryObserver(queryClient, {
|
|
226
|
+
queryKey: key,
|
|
227
|
+
enabled: false,
|
|
228
|
+
})
|
|
229
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
230
|
+
unsubscribe()
|
|
231
|
+
|
|
232
|
+
await sleep(100)
|
|
233
|
+
|
|
234
|
+
const query = queryCache.find(key)!
|
|
235
|
+
|
|
236
|
+
expect(query.state).toMatchObject({
|
|
237
|
+
data: 'data',
|
|
238
|
+
status: 'success',
|
|
239
|
+
dataUpdateCount: 1,
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('should not continue when last observer unsubscribed if the signal was consumed', async () => {
|
|
244
|
+
const key = queryKey()
|
|
245
|
+
|
|
246
|
+
queryClient.prefetchQuery(key, async ({ signal }) => {
|
|
247
|
+
await sleep(100)
|
|
248
|
+
return signal?.aborted ? 'aborted' : 'data'
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
await sleep(10)
|
|
252
|
+
|
|
253
|
+
// Subscribe and unsubscribe to simulate cancellation because the last observer unsubscribed
|
|
254
|
+
const observer = new QueryObserver(queryClient, {
|
|
255
|
+
queryKey: key,
|
|
256
|
+
enabled: false,
|
|
257
|
+
})
|
|
258
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
259
|
+
unsubscribe()
|
|
260
|
+
|
|
261
|
+
await sleep(100)
|
|
262
|
+
|
|
263
|
+
const query = queryCache.find(key)!
|
|
264
|
+
|
|
265
|
+
if (typeof AbortSignal === 'function') {
|
|
266
|
+
expect(query.state).toMatchObject({
|
|
267
|
+
data: undefined,
|
|
268
|
+
status: 'loading',
|
|
269
|
+
fetchStatus: 'idle',
|
|
270
|
+
})
|
|
271
|
+
} else {
|
|
272
|
+
expect(query.state).toMatchObject({
|
|
273
|
+
data: 'data',
|
|
274
|
+
status: 'success',
|
|
275
|
+
fetchStatus: 'idle',
|
|
276
|
+
dataUpdateCount: 1,
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('should provide an AbortSignal to the queryFn that provides info about the cancellation state', async () => {
|
|
282
|
+
const key = queryKey()
|
|
283
|
+
|
|
284
|
+
const queryFn = jest.fn<
|
|
285
|
+
Promise<unknown>,
|
|
286
|
+
[QueryFunctionContext<ReturnType<typeof queryKey>>]
|
|
287
|
+
>()
|
|
288
|
+
const onAbort = jest.fn()
|
|
289
|
+
const abortListener = jest.fn()
|
|
290
|
+
let error
|
|
291
|
+
|
|
292
|
+
queryFn.mockImplementation(async ({ signal }) => {
|
|
293
|
+
if (signal) {
|
|
294
|
+
signal.onabort = onAbort
|
|
295
|
+
signal.addEventListener('abort', abortListener)
|
|
296
|
+
}
|
|
297
|
+
await sleep(10)
|
|
298
|
+
if (signal) {
|
|
299
|
+
signal.onabort = null
|
|
300
|
+
signal.removeEventListener('abort', abortListener)
|
|
301
|
+
}
|
|
302
|
+
throw new Error()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const promise = queryClient.fetchQuery(key, queryFn, {
|
|
306
|
+
retry: 3,
|
|
307
|
+
retryDelay: 10,
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
promise.catch((e) => {
|
|
311
|
+
error = e
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const query = queryCache.find(key)!
|
|
315
|
+
|
|
316
|
+
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
317
|
+
|
|
318
|
+
let signal = queryFn.mock.calls[0]![0].signal
|
|
319
|
+
|
|
320
|
+
if (typeof AbortSignal === 'function') {
|
|
321
|
+
signal = queryFn.mock.calls[0]![0].signal
|
|
322
|
+
expect(signal?.aborted).toBe(false)
|
|
323
|
+
}
|
|
324
|
+
expect(onAbort).not.toHaveBeenCalled()
|
|
325
|
+
expect(abortListener).not.toHaveBeenCalled()
|
|
326
|
+
|
|
327
|
+
query.cancel()
|
|
328
|
+
|
|
329
|
+
await sleep(100)
|
|
330
|
+
|
|
331
|
+
if (typeof AbortSignal === 'function') {
|
|
332
|
+
expect(signal?.aborted).toBe(true)
|
|
333
|
+
expect(onAbort).toHaveBeenCalledTimes(1)
|
|
334
|
+
expect(abortListener).toHaveBeenCalledTimes(1)
|
|
335
|
+
}
|
|
336
|
+
expect(isCancelledError(error)).toBe(true)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('should not continue if explicitly cancelled', async () => {
|
|
340
|
+
const key = queryKey()
|
|
341
|
+
|
|
342
|
+
const queryFn = jest.fn<unknown, unknown[]>()
|
|
343
|
+
|
|
344
|
+
queryFn.mockImplementation(async () => {
|
|
345
|
+
await sleep(10)
|
|
346
|
+
throw new Error()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
let error
|
|
350
|
+
|
|
351
|
+
const promise = queryClient.fetchQuery(key, queryFn, {
|
|
352
|
+
retry: 3,
|
|
353
|
+
retryDelay: 10,
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
promise.catch((e) => {
|
|
357
|
+
error = e
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const query = queryCache.find(key)!
|
|
361
|
+
query.cancel()
|
|
362
|
+
|
|
363
|
+
await sleep(100)
|
|
364
|
+
|
|
365
|
+
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
366
|
+
expect(isCancelledError(error)).toBe(true)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test('should not error if reset while loading', async () => {
|
|
370
|
+
const key = queryKey()
|
|
371
|
+
|
|
372
|
+
const queryFn = jest.fn<unknown, unknown[]>()
|
|
373
|
+
|
|
374
|
+
queryFn.mockImplementation(async () => {
|
|
375
|
+
await sleep(10)
|
|
376
|
+
throw new Error()
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
queryClient.fetchQuery(key, queryFn, {
|
|
380
|
+
retry: 3,
|
|
381
|
+
retryDelay: 10,
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
// Ensure the query is loading
|
|
385
|
+
const query = queryCache.find(key)!
|
|
386
|
+
expect(query.state.status).toBe('loading')
|
|
387
|
+
|
|
388
|
+
// Reset the query while it is loading
|
|
389
|
+
query.reset()
|
|
390
|
+
|
|
391
|
+
await sleep(100)
|
|
392
|
+
|
|
393
|
+
// The query should
|
|
394
|
+
expect(queryFn).toHaveBeenCalledTimes(1) // have been called,
|
|
395
|
+
expect(query.state.error).toBe(null) // not have an error, and
|
|
396
|
+
expect(query.state.fetchStatus).toBe('idle') // not be loading any longer
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test('should be able to refetch a cancelled query', async () => {
|
|
400
|
+
const key = queryKey()
|
|
401
|
+
|
|
402
|
+
const queryFn = jest.fn<unknown, unknown[]>()
|
|
403
|
+
|
|
404
|
+
queryFn.mockImplementation(async () => {
|
|
405
|
+
await sleep(50)
|
|
406
|
+
return 'data'
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
queryClient.prefetchQuery(key, queryFn)
|
|
410
|
+
const query = queryCache.find(key)!
|
|
411
|
+
await sleep(10)
|
|
412
|
+
query.cancel()
|
|
413
|
+
await sleep(100)
|
|
414
|
+
|
|
415
|
+
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
416
|
+
expect(isCancelledError(query.state.error)).toBe(true)
|
|
417
|
+
const result = await query.fetch()
|
|
418
|
+
expect(result).toBe('data')
|
|
419
|
+
expect(query.state.error).toBe(null)
|
|
420
|
+
expect(queryFn).toHaveBeenCalledTimes(2)
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
test('cancelling a resolved query should not have any effect', async () => {
|
|
424
|
+
const key = queryKey()
|
|
425
|
+
await queryClient.prefetchQuery(key, async () => 'data')
|
|
426
|
+
const query = queryCache.find(key)!
|
|
427
|
+
query.cancel()
|
|
428
|
+
await sleep(10)
|
|
429
|
+
expect(query.state.data).toBe('data')
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test('cancelling a rejected query should not have any effect', async () => {
|
|
433
|
+
const key = queryKey()
|
|
434
|
+
|
|
435
|
+
await queryClient.prefetchQuery(key, async (): Promise<unknown> => {
|
|
436
|
+
throw new Error('error')
|
|
437
|
+
})
|
|
438
|
+
const query = queryCache.find(key)!
|
|
439
|
+
query.cancel()
|
|
440
|
+
await sleep(10)
|
|
441
|
+
|
|
442
|
+
expect(isError(query.state.error)).toBe(true)
|
|
443
|
+
expect(isCancelledError(query.state.error)).toBe(false)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
test('the previous query status should be kept when refetching', async () => {
|
|
447
|
+
const key = queryKey()
|
|
448
|
+
|
|
449
|
+
await queryClient.prefetchQuery(key, () => 'data')
|
|
450
|
+
const query = queryCache.find(key)!
|
|
451
|
+
expect(query.state.status).toBe('success')
|
|
452
|
+
|
|
453
|
+
await queryClient.prefetchQuery(
|
|
454
|
+
key,
|
|
455
|
+
() => Promise.reject<string>('reject'),
|
|
456
|
+
{
|
|
457
|
+
retry: false,
|
|
458
|
+
},
|
|
459
|
+
)
|
|
460
|
+
expect(query.state.status).toBe('error')
|
|
461
|
+
|
|
462
|
+
queryClient.prefetchQuery(
|
|
463
|
+
key,
|
|
464
|
+
async () => {
|
|
465
|
+
await sleep(10)
|
|
466
|
+
return Promise.reject<unknown>('reject')
|
|
467
|
+
},
|
|
468
|
+
{ retry: false },
|
|
469
|
+
)
|
|
470
|
+
expect(query.state.status).toBe('error')
|
|
471
|
+
|
|
472
|
+
await sleep(100)
|
|
473
|
+
expect(query.state.status).toBe('error')
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
test('queries with cacheTime 0 should be removed immediately after unsubscribing', async () => {
|
|
477
|
+
const key = queryKey()
|
|
478
|
+
let count = 0
|
|
479
|
+
const observer = new QueryObserver(queryClient, {
|
|
480
|
+
queryKey: key,
|
|
481
|
+
queryFn: () => {
|
|
482
|
+
count++
|
|
483
|
+
return 'data'
|
|
484
|
+
},
|
|
485
|
+
cacheTime: 0,
|
|
486
|
+
staleTime: Infinity,
|
|
487
|
+
})
|
|
488
|
+
const unsubscribe1 = observer.subscribe(() => undefined)
|
|
489
|
+
unsubscribe1()
|
|
490
|
+
await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
|
|
491
|
+
const unsubscribe2 = observer.subscribe(() => undefined)
|
|
492
|
+
unsubscribe2()
|
|
493
|
+
|
|
494
|
+
await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
|
|
495
|
+
expect(count).toBe(1)
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
test('should be garbage collected when unsubscribed to', async () => {
|
|
499
|
+
const key = queryKey()
|
|
500
|
+
const observer = new QueryObserver(queryClient, {
|
|
501
|
+
queryKey: key,
|
|
502
|
+
queryFn: async () => 'data',
|
|
503
|
+
cacheTime: 0,
|
|
504
|
+
})
|
|
505
|
+
expect(queryCache.find(key)).toBeDefined()
|
|
506
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
507
|
+
expect(queryCache.find(key)).toBeDefined()
|
|
508
|
+
unsubscribe()
|
|
509
|
+
await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
test('should be garbage collected later when unsubscribed and query is fetching', async () => {
|
|
513
|
+
const key = queryKey()
|
|
514
|
+
const observer = new QueryObserver(queryClient, {
|
|
515
|
+
queryKey: key,
|
|
516
|
+
queryFn: async () => {
|
|
517
|
+
await sleep(20)
|
|
518
|
+
return 'data'
|
|
519
|
+
},
|
|
520
|
+
cacheTime: 10,
|
|
521
|
+
})
|
|
522
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
523
|
+
await sleep(20)
|
|
524
|
+
expect(queryCache.find(key)).toBeDefined()
|
|
525
|
+
observer.refetch()
|
|
526
|
+
unsubscribe()
|
|
527
|
+
await sleep(10)
|
|
528
|
+
// unsubscribe should not remove even though cacheTime has elapsed b/c query is still fetching
|
|
529
|
+
expect(queryCache.find(key)).toBeDefined()
|
|
530
|
+
await sleep(10)
|
|
531
|
+
// should be removed after an additional staleTime wait
|
|
532
|
+
await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
test('should not be garbage collected unless there are no subscribers', async () => {
|
|
536
|
+
const key = queryKey()
|
|
537
|
+
const observer = new QueryObserver(queryClient, {
|
|
538
|
+
queryKey: key,
|
|
539
|
+
queryFn: async () => 'data',
|
|
540
|
+
cacheTime: 0,
|
|
541
|
+
})
|
|
542
|
+
expect(queryCache.find(key)).toBeDefined()
|
|
543
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
544
|
+
await sleep(100)
|
|
545
|
+
expect(queryCache.find(key)).toBeDefined()
|
|
546
|
+
unsubscribe()
|
|
547
|
+
await sleep(100)
|
|
548
|
+
expect(queryCache.find(key)).toBeUndefined()
|
|
549
|
+
queryClient.setQueryData(key, 'data')
|
|
550
|
+
await sleep(100)
|
|
551
|
+
expect(queryCache.find(key)).toBeDefined()
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
test('should return proper count of observers', async () => {
|
|
555
|
+
const key = queryKey()
|
|
556
|
+
const options = { queryKey: key, queryFn: async () => 'data' }
|
|
557
|
+
const observer = new QueryObserver(queryClient, options)
|
|
558
|
+
const observer2 = new QueryObserver(queryClient, options)
|
|
559
|
+
const observer3 = new QueryObserver(queryClient, options)
|
|
560
|
+
const query = queryCache.find(key)
|
|
561
|
+
|
|
562
|
+
expect(query?.getObserversCount()).toEqual(0)
|
|
563
|
+
|
|
564
|
+
const unsubscribe1 = observer.subscribe(() => undefined)
|
|
565
|
+
const unsubscribe2 = observer2.subscribe(() => undefined)
|
|
566
|
+
const unsubscribe3 = observer3.subscribe(() => undefined)
|
|
567
|
+
expect(query?.getObserversCount()).toEqual(3)
|
|
568
|
+
|
|
569
|
+
unsubscribe3()
|
|
570
|
+
expect(query?.getObserversCount()).toEqual(2)
|
|
571
|
+
|
|
572
|
+
unsubscribe2()
|
|
573
|
+
expect(query?.getObserversCount()).toEqual(1)
|
|
574
|
+
|
|
575
|
+
unsubscribe1()
|
|
576
|
+
expect(query?.getObserversCount()).toEqual(0)
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
test('stores meta object in query', async () => {
|
|
580
|
+
const meta = {
|
|
581
|
+
it: 'works',
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const key = queryKey()
|
|
585
|
+
|
|
586
|
+
await queryClient.prefetchQuery(key, () => 'data', {
|
|
587
|
+
meta,
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
const query = queryCache.find(key)!
|
|
591
|
+
|
|
592
|
+
expect(query.meta).toBe(meta)
|
|
593
|
+
expect(query.options.meta).toBe(meta)
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
test('updates meta object on change', async () => {
|
|
597
|
+
const meta = {
|
|
598
|
+
it: 'works',
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const key = queryKey()
|
|
602
|
+
const queryFn = () => 'data'
|
|
603
|
+
|
|
604
|
+
await queryClient.prefetchQuery(key, queryFn, {
|
|
605
|
+
meta,
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
await queryClient.prefetchQuery(key, queryFn, {
|
|
609
|
+
meta: undefined,
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
const query = queryCache.find(key)!
|
|
613
|
+
|
|
614
|
+
expect(query.meta).toBeUndefined()
|
|
615
|
+
expect(query.options.meta).toBeUndefined()
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
test('provides meta object inside query function', async () => {
|
|
619
|
+
const meta = {
|
|
620
|
+
it: 'works',
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const queryFn = jest.fn(() => 'data')
|
|
624
|
+
|
|
625
|
+
const key = queryKey()
|
|
626
|
+
|
|
627
|
+
await queryClient.prefetchQuery(key, queryFn, {
|
|
628
|
+
meta,
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
expect(queryFn).toBeCalledWith(
|
|
632
|
+
expect.objectContaining({
|
|
633
|
+
meta,
|
|
634
|
+
}),
|
|
635
|
+
)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
test('should refetch the observer when online method is called', async () => {
|
|
639
|
+
const key = queryKey()
|
|
640
|
+
|
|
641
|
+
const observer = new QueryObserver(queryClient, {
|
|
642
|
+
queryKey: key,
|
|
643
|
+
queryFn: () => 'data',
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
const refetchSpy = jest.spyOn(observer, 'refetch')
|
|
647
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
648
|
+
queryCache.onOnline()
|
|
649
|
+
|
|
650
|
+
// Should refetch the observer
|
|
651
|
+
expect(refetchSpy).toHaveBeenCalledTimes(1)
|
|
652
|
+
|
|
653
|
+
unsubscribe()
|
|
654
|
+
refetchSpy.mockRestore()
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
test('should not add an existing observer', async () => {
|
|
658
|
+
const key = queryKey()
|
|
659
|
+
|
|
660
|
+
await queryClient.prefetchQuery(key, () => 'data')
|
|
661
|
+
const query = queryCache.find(key)!
|
|
662
|
+
expect(query.getObserversCount()).toEqual(0)
|
|
663
|
+
|
|
664
|
+
const observer = new QueryObserver(queryClient, {
|
|
665
|
+
queryKey: key,
|
|
666
|
+
})
|
|
667
|
+
expect(query.getObserversCount()).toEqual(0)
|
|
668
|
+
|
|
669
|
+
query.addObserver(observer)
|
|
670
|
+
expect(query.getObserversCount()).toEqual(1)
|
|
671
|
+
|
|
672
|
+
query.addObserver(observer)
|
|
673
|
+
expect(query.getObserversCount()).toEqual(1)
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
test('should not try to remove an observer that does not exist', async () => {
|
|
677
|
+
const key = queryKey()
|
|
678
|
+
|
|
679
|
+
await queryClient.prefetchQuery(key, () => 'data')
|
|
680
|
+
const query = queryCache.find(key)!
|
|
681
|
+
const observer = new QueryObserver(queryClient, {
|
|
682
|
+
queryKey: key,
|
|
683
|
+
})
|
|
684
|
+
expect(query.getObserversCount()).toEqual(0)
|
|
685
|
+
|
|
686
|
+
const notifySpy = jest.spyOn(queryCache, 'notify')
|
|
687
|
+
expect(() => query.removeObserver(observer)).not.toThrow()
|
|
688
|
+
expect(notifySpy).not.toHaveBeenCalled()
|
|
689
|
+
|
|
690
|
+
notifySpy.mockRestore()
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
test('should not dispatch "invalidate" on invalidate() if already invalidated', async () => {
|
|
694
|
+
const key = queryKey()
|
|
695
|
+
|
|
696
|
+
await queryClient.prefetchQuery(key, () => 'data')
|
|
697
|
+
const query = queryCache.find(key)!
|
|
698
|
+
|
|
699
|
+
query.invalidate()
|
|
700
|
+
expect(query.state.isInvalidated).toBeTruthy()
|
|
701
|
+
|
|
702
|
+
const dispatchOriginal = query['dispatch']
|
|
703
|
+
const dispatchSpy = jest.fn()
|
|
704
|
+
query['dispatch'] = dispatchSpy
|
|
705
|
+
|
|
706
|
+
query.invalidate()
|
|
707
|
+
|
|
708
|
+
expect(query.state.isInvalidated).toBeTruthy()
|
|
709
|
+
expect(dispatchSpy).not.toHaveBeenCalled()
|
|
710
|
+
|
|
711
|
+
query['dispatch'] = dispatchOriginal
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
test('fetch should not dispatch "fetch" if state meta and fetchOptions meta are the same object', async () => {
|
|
715
|
+
const key = queryKey()
|
|
716
|
+
|
|
717
|
+
const queryFn = async () => {
|
|
718
|
+
await sleep(10)
|
|
719
|
+
return 'data'
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
await queryClient.prefetchQuery(key, queryFn)
|
|
723
|
+
const query = queryCache.find(key)!
|
|
724
|
+
|
|
725
|
+
const meta = { meta1: '1' }
|
|
726
|
+
|
|
727
|
+
// This first fetch will set the state.meta value
|
|
728
|
+
query.fetch(
|
|
729
|
+
{
|
|
730
|
+
queryKey: key,
|
|
731
|
+
queryFn,
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
meta,
|
|
735
|
+
},
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
// Spy on private dispatch method
|
|
739
|
+
const dispatchOriginal = query['dispatch']
|
|
740
|
+
const dispatchSpy = jest.fn()
|
|
741
|
+
query['dispatch'] = dispatchSpy
|
|
742
|
+
|
|
743
|
+
// Second fetch in parallel with the same meta
|
|
744
|
+
query.fetch(
|
|
745
|
+
{
|
|
746
|
+
queryKey: key,
|
|
747
|
+
queryFn,
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
meta,
|
|
751
|
+
// cancelRefetch must be set to true to enter in the case to test
|
|
752
|
+
// where isFetching is true
|
|
753
|
+
cancelRefetch: true,
|
|
754
|
+
},
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
// Should not call dispatch with type set to fetch
|
|
758
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith({
|
|
759
|
+
meta,
|
|
760
|
+
type: 'fetch',
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
// Clean-up
|
|
764
|
+
await sleep(20)
|
|
765
|
+
query['dispatch'] = dispatchOriginal
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
test('fetch should not set the signal in the queryFnContext if AbortController is undefined', async () => {
|
|
769
|
+
const key = queryKey()
|
|
770
|
+
|
|
771
|
+
// Mock the AbortController to be undefined
|
|
772
|
+
const AbortControllerOriginal = globalThis['AbortController']
|
|
773
|
+
//@ts-expect-error
|
|
774
|
+
globalThis['AbortController'] = undefined
|
|
775
|
+
|
|
776
|
+
let signalTest: any
|
|
777
|
+
await queryClient.prefetchQuery(key, ({ signal }) => {
|
|
778
|
+
signalTest = signal
|
|
779
|
+
return 'data'
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
expect(signalTest).toBeUndefined()
|
|
783
|
+
|
|
784
|
+
// Clean-up
|
|
785
|
+
//@ts-ignore
|
|
786
|
+
globalThis['AbortController'] = AbortControllerOriginal
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
test('fetch should throw an error if the queryFn is not defined', async () => {
|
|
790
|
+
const key = queryKey()
|
|
791
|
+
|
|
792
|
+
const observer = new QueryObserver(queryClient, {
|
|
793
|
+
queryKey: key,
|
|
794
|
+
queryFn: undefined,
|
|
795
|
+
retry: false,
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
799
|
+
await sleep(10)
|
|
800
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Missing queryFn')
|
|
801
|
+
|
|
802
|
+
unsubscribe()
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
test('fetch should dispatch an error if the queryFn returns undefined', async () => {
|
|
806
|
+
const key = queryKey()
|
|
807
|
+
|
|
808
|
+
const observer = new QueryObserver(queryClient, {
|
|
809
|
+
queryKey: key,
|
|
810
|
+
queryFn: () => undefined,
|
|
811
|
+
retry: false,
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
let observerResult: QueryObserverResult<unknown, unknown> | undefined
|
|
815
|
+
|
|
816
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
817
|
+
observerResult = result
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
await sleep(10)
|
|
821
|
+
|
|
822
|
+
const error = new Error('Query data cannot be undefined')
|
|
823
|
+
|
|
824
|
+
expect(observerResult).toMatchObject({
|
|
825
|
+
isError: true,
|
|
826
|
+
error,
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
expect(mockLogger.error).toHaveBeenCalledWith(error)
|
|
830
|
+
unsubscribe()
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
test('fetch should dispatch fetch if is fetching and current promise is undefined', async () => {
|
|
834
|
+
const key = queryKey()
|
|
835
|
+
|
|
836
|
+
const queryFn = async () => {
|
|
837
|
+
await sleep(10)
|
|
838
|
+
return 'data'
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
await queryClient.prefetchQuery(key, queryFn)
|
|
842
|
+
const query = queryCache.find(key)!
|
|
843
|
+
|
|
844
|
+
query.fetch({
|
|
845
|
+
queryKey: key,
|
|
846
|
+
queryFn,
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
// Force promise to undefined
|
|
850
|
+
// because no use case have been identified
|
|
851
|
+
query['promise'] = undefined
|
|
852
|
+
|
|
853
|
+
// Spy on private dispatch method
|
|
854
|
+
const dispatchOriginal = query['dispatch']
|
|
855
|
+
const dispatchSpy = jest.fn()
|
|
856
|
+
query['dispatch'] = dispatchSpy
|
|
857
|
+
|
|
858
|
+
query.fetch({
|
|
859
|
+
queryKey: key,
|
|
860
|
+
queryFn,
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
// Should call dispatch with type set to fetch
|
|
864
|
+
expect(dispatchSpy).toHaveBeenCalledWith({
|
|
865
|
+
meta: undefined,
|
|
866
|
+
type: 'fetch',
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
// Clean-up
|
|
870
|
+
await sleep(20)
|
|
871
|
+
query['dispatch'] = dispatchOriginal
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
test('constructor should call initialDataUpdatedAt if defined as a function', async () => {
|
|
875
|
+
const key = queryKey()
|
|
876
|
+
|
|
877
|
+
const initialDataUpdatedAtSpy = jest.fn()
|
|
878
|
+
|
|
879
|
+
await queryClient.prefetchQuery({
|
|
880
|
+
queryKey: key,
|
|
881
|
+
queryFn: () => 'data',
|
|
882
|
+
initialData: 'initial',
|
|
883
|
+
initialDataUpdatedAt: initialDataUpdatedAtSpy,
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
expect(initialDataUpdatedAtSpy).toHaveBeenCalled()
|
|
887
|
+
})
|
|
888
|
+
})
|