@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.
Files changed (85) hide show
  1. package/build/cjs/focusManager.js +101 -0
  2. package/build/cjs/focusManager.js.map +1 -0
  3. package/build/cjs/hydration.js +112 -0
  4. package/build/cjs/hydration.js.map +1 -0
  5. package/build/cjs/index.js +51 -0
  6. package/build/cjs/index.js.map +1 -0
  7. package/build/cjs/infiniteQueryBehavior.js +160 -0
  8. package/build/cjs/infiniteQueryBehavior.js.map +1 -0
  9. package/build/cjs/infiniteQueryObserver.js +92 -0
  10. package/build/cjs/infiniteQueryObserver.js.map +1 -0
  11. package/build/cjs/logger.js +18 -0
  12. package/build/cjs/logger.js.map +1 -0
  13. package/build/cjs/mutation.js +258 -0
  14. package/build/cjs/mutation.js.map +1 -0
  15. package/build/cjs/mutationCache.js +99 -0
  16. package/build/cjs/mutationCache.js.map +1 -0
  17. package/build/cjs/mutationObserver.js +130 -0
  18. package/build/cjs/mutationObserver.js.map +1 -0
  19. package/build/cjs/notifyManager.js +114 -0
  20. package/build/cjs/notifyManager.js.map +1 -0
  21. package/build/cjs/onlineManager.js +100 -0
  22. package/build/cjs/onlineManager.js.map +1 -0
  23. package/build/cjs/queriesObserver.js +170 -0
  24. package/build/cjs/queriesObserver.js.map +1 -0
  25. package/build/cjs/query.js +474 -0
  26. package/build/cjs/query.js.map +1 -0
  27. package/build/cjs/queryCache.js +140 -0
  28. package/build/cjs/queryCache.js.map +1 -0
  29. package/build/cjs/queryClient.js +357 -0
  30. package/build/cjs/queryClient.js.map +1 -0
  31. package/build/cjs/queryObserver.js +521 -0
  32. package/build/cjs/queryObserver.js.map +1 -0
  33. package/build/cjs/removable.js +47 -0
  34. package/build/cjs/removable.js.map +1 -0
  35. package/build/cjs/retryer.js +177 -0
  36. package/build/cjs/retryer.js.map +1 -0
  37. package/build/cjs/subscribable.js +43 -0
  38. package/build/cjs/subscribable.js.map +1 -0
  39. package/build/cjs/utils.js +356 -0
  40. package/build/cjs/utils.js.map +1 -0
  41. package/build/esm/index.js +3077 -0
  42. package/build/esm/index.js.map +1 -0
  43. package/build/stats-html.html +2689 -0
  44. package/build/umd/index.development.js +3106 -0
  45. package/build/umd/index.development.js.map +1 -0
  46. package/build/umd/index.production.js +12 -0
  47. package/build/umd/index.production.js.map +1 -0
  48. package/package.json +25 -0
  49. package/src/focusManager.ts +89 -0
  50. package/src/hydration.ts +164 -0
  51. package/src/index.ts +35 -0
  52. package/src/infiniteQueryBehavior.ts +214 -0
  53. package/src/infiniteQueryObserver.ts +159 -0
  54. package/src/logger.native.ts +11 -0
  55. package/src/logger.ts +9 -0
  56. package/src/mutation.ts +349 -0
  57. package/src/mutationCache.ts +157 -0
  58. package/src/mutationObserver.ts +195 -0
  59. package/src/notifyManager.ts +96 -0
  60. package/src/onlineManager.ts +89 -0
  61. package/src/queriesObserver.ts +211 -0
  62. package/src/query.ts +612 -0
  63. package/src/queryCache.ts +206 -0
  64. package/src/queryClient.ts +716 -0
  65. package/src/queryObserver.ts +748 -0
  66. package/src/removable.ts +37 -0
  67. package/src/retryer.ts +215 -0
  68. package/src/subscribable.ts +33 -0
  69. package/src/tests/focusManager.test.tsx +155 -0
  70. package/src/tests/hydration.test.tsx +429 -0
  71. package/src/tests/infiniteQueryBehavior.test.tsx +124 -0
  72. package/src/tests/infiniteQueryObserver.test.tsx +64 -0
  73. package/src/tests/mutationCache.test.tsx +260 -0
  74. package/src/tests/mutationObserver.test.tsx +75 -0
  75. package/src/tests/mutations.test.tsx +363 -0
  76. package/src/tests/notifyManager.test.tsx +51 -0
  77. package/src/tests/onlineManager.test.tsx +148 -0
  78. package/src/tests/queriesObserver.test.tsx +330 -0
  79. package/src/tests/query.test.tsx +888 -0
  80. package/src/tests/queryCache.test.tsx +236 -0
  81. package/src/tests/queryClient.test.tsx +1435 -0
  82. package/src/tests/queryObserver.test.tsx +802 -0
  83. package/src/tests/utils.test.tsx +360 -0
  84. package/src/types.ts +705 -0
  85. 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
+ })