@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,802 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sleep,
|
|
3
|
+
queryKey,
|
|
4
|
+
expectType,
|
|
5
|
+
mockLogger,
|
|
6
|
+
createQueryClient,
|
|
7
|
+
} from '../../../../tests/utils'
|
|
8
|
+
import {
|
|
9
|
+
QueryClient,
|
|
10
|
+
QueryObserver,
|
|
11
|
+
QueryObserverResult,
|
|
12
|
+
focusManager,
|
|
13
|
+
} from '..'
|
|
14
|
+
|
|
15
|
+
describe('queryObserver', () => {
|
|
16
|
+
let queryClient: QueryClient
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
queryClient = createQueryClient()
|
|
20
|
+
queryClient.mount()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
queryClient.clear()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('should trigger a fetch when subscribed', async () => {
|
|
28
|
+
const key = queryKey()
|
|
29
|
+
const queryFn = jest.fn<string, unknown[]>().mockReturnValue('data')
|
|
30
|
+
const observer = new QueryObserver(queryClient, { queryKey: key, queryFn })
|
|
31
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
32
|
+
await sleep(1)
|
|
33
|
+
unsubscribe()
|
|
34
|
+
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('should notify when switching query', async () => {
|
|
38
|
+
const key1 = queryKey()
|
|
39
|
+
const key2 = queryKey()
|
|
40
|
+
const results: QueryObserverResult[] = []
|
|
41
|
+
const observer = new QueryObserver(queryClient, {
|
|
42
|
+
queryKey: key1,
|
|
43
|
+
queryFn: () => 1,
|
|
44
|
+
})
|
|
45
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
46
|
+
results.push(result)
|
|
47
|
+
})
|
|
48
|
+
await sleep(1)
|
|
49
|
+
observer.setOptions({ queryKey: key2, queryFn: () => 2 })
|
|
50
|
+
await sleep(1)
|
|
51
|
+
unsubscribe()
|
|
52
|
+
expect(results.length).toBe(4)
|
|
53
|
+
expect(results[0]).toMatchObject({ data: undefined, status: 'loading' })
|
|
54
|
+
expect(results[1]).toMatchObject({ data: 1, status: 'success' })
|
|
55
|
+
expect(results[2]).toMatchObject({ data: undefined, status: 'loading' })
|
|
56
|
+
expect(results[3]).toMatchObject({ data: 2, status: 'success' })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('should be able to fetch with a selector', async () => {
|
|
60
|
+
const key = queryKey()
|
|
61
|
+
const observer = new QueryObserver(queryClient, {
|
|
62
|
+
queryKey: key,
|
|
63
|
+
queryFn: () => ({ count: 1 }),
|
|
64
|
+
select: (data) => ({ myCount: data.count }),
|
|
65
|
+
})
|
|
66
|
+
let observerResult
|
|
67
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
68
|
+
expectType<QueryObserverResult<{ myCount: number }>>(result)
|
|
69
|
+
observerResult = result
|
|
70
|
+
})
|
|
71
|
+
await sleep(1)
|
|
72
|
+
unsubscribe()
|
|
73
|
+
expect(observerResult).toMatchObject({ data: { myCount: 1 } })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('should be able to fetch with a selector using the fetch method', async () => {
|
|
77
|
+
const key = queryKey()
|
|
78
|
+
const observer = new QueryObserver(queryClient, {
|
|
79
|
+
queryKey: key,
|
|
80
|
+
queryFn: () => ({ count: 1 }),
|
|
81
|
+
select: (data) => ({ myCount: data.count }),
|
|
82
|
+
})
|
|
83
|
+
const observerResult = await observer.refetch()
|
|
84
|
+
expectType<{ myCount: number } | undefined>(observerResult.data)
|
|
85
|
+
expect(observerResult.data).toMatchObject({ myCount: 1 })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('should be able to fetch with a selector and object syntax', async () => {
|
|
89
|
+
const key = queryKey()
|
|
90
|
+
const observer = new QueryObserver(queryClient, {
|
|
91
|
+
queryKey: key,
|
|
92
|
+
queryFn: () => ({ count: 1 }),
|
|
93
|
+
select: (data) => ({ myCount: data.count }),
|
|
94
|
+
})
|
|
95
|
+
let observerResult
|
|
96
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
97
|
+
observerResult = result
|
|
98
|
+
})
|
|
99
|
+
await sleep(1)
|
|
100
|
+
unsubscribe()
|
|
101
|
+
expect(observerResult).toMatchObject({ data: { myCount: 1 } })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('should run the selector again if the data changed', async () => {
|
|
105
|
+
const key = queryKey()
|
|
106
|
+
let count = 0
|
|
107
|
+
const observer = new QueryObserver(queryClient, {
|
|
108
|
+
queryKey: key,
|
|
109
|
+
queryFn: () => ({ count }),
|
|
110
|
+
select: (data) => {
|
|
111
|
+
count++
|
|
112
|
+
return { myCount: data.count }
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
const observerResult1 = await observer.refetch()
|
|
116
|
+
const observerResult2 = await observer.refetch()
|
|
117
|
+
expect(count).toBe(2)
|
|
118
|
+
expect(observerResult1.data).toMatchObject({ myCount: 0 })
|
|
119
|
+
expect(observerResult2.data).toMatchObject({ myCount: 1 })
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('should run the selector again if the selector changed', async () => {
|
|
123
|
+
const key = queryKey()
|
|
124
|
+
let count = 0
|
|
125
|
+
const results: QueryObserverResult[] = []
|
|
126
|
+
const queryFn = () => ({ count: 1 })
|
|
127
|
+
const select1 = (data: ReturnType<typeof queryFn>) => {
|
|
128
|
+
count++
|
|
129
|
+
return { myCount: data.count }
|
|
130
|
+
}
|
|
131
|
+
const select2 = (_data: ReturnType<typeof queryFn>) => {
|
|
132
|
+
count++
|
|
133
|
+
return { myCount: 99 }
|
|
134
|
+
}
|
|
135
|
+
const observer = new QueryObserver(queryClient, {
|
|
136
|
+
queryKey: key,
|
|
137
|
+
queryFn,
|
|
138
|
+
select: select1,
|
|
139
|
+
})
|
|
140
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
141
|
+
results.push(result)
|
|
142
|
+
})
|
|
143
|
+
await sleep(1)
|
|
144
|
+
observer.setOptions({
|
|
145
|
+
queryKey: key,
|
|
146
|
+
queryFn,
|
|
147
|
+
select: select2,
|
|
148
|
+
})
|
|
149
|
+
await sleep(1)
|
|
150
|
+
await observer.refetch()
|
|
151
|
+
unsubscribe()
|
|
152
|
+
expect(count).toBe(2)
|
|
153
|
+
expect(results.length).toBe(5)
|
|
154
|
+
expect(results[0]).toMatchObject({
|
|
155
|
+
status: 'loading',
|
|
156
|
+
isFetching: true,
|
|
157
|
+
data: undefined,
|
|
158
|
+
})
|
|
159
|
+
expect(results[1]).toMatchObject({
|
|
160
|
+
status: 'success',
|
|
161
|
+
isFetching: false,
|
|
162
|
+
data: { myCount: 1 },
|
|
163
|
+
})
|
|
164
|
+
expect(results[2]).toMatchObject({
|
|
165
|
+
status: 'success',
|
|
166
|
+
isFetching: false,
|
|
167
|
+
data: { myCount: 99 },
|
|
168
|
+
})
|
|
169
|
+
expect(results[3]).toMatchObject({
|
|
170
|
+
status: 'success',
|
|
171
|
+
isFetching: true,
|
|
172
|
+
data: { myCount: 99 },
|
|
173
|
+
})
|
|
174
|
+
expect(results[4]).toMatchObject({
|
|
175
|
+
status: 'success',
|
|
176
|
+
isFetching: false,
|
|
177
|
+
data: { myCount: 99 },
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('should not run the selector again if the data and selector did not change', async () => {
|
|
182
|
+
const key = queryKey()
|
|
183
|
+
let count = 0
|
|
184
|
+
const results: QueryObserverResult[] = []
|
|
185
|
+
const queryFn = () => ({ count: 1 })
|
|
186
|
+
const select = (data: ReturnType<typeof queryFn>) => {
|
|
187
|
+
count++
|
|
188
|
+
return { myCount: data.count }
|
|
189
|
+
}
|
|
190
|
+
const observer = new QueryObserver(queryClient, {
|
|
191
|
+
queryKey: key,
|
|
192
|
+
queryFn,
|
|
193
|
+
select,
|
|
194
|
+
})
|
|
195
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
196
|
+
results.push(result)
|
|
197
|
+
})
|
|
198
|
+
await sleep(1)
|
|
199
|
+
observer.setOptions({
|
|
200
|
+
queryKey: key,
|
|
201
|
+
queryFn,
|
|
202
|
+
select,
|
|
203
|
+
})
|
|
204
|
+
await sleep(1)
|
|
205
|
+
await observer.refetch()
|
|
206
|
+
unsubscribe()
|
|
207
|
+
expect(count).toBe(1)
|
|
208
|
+
expect(results.length).toBe(4)
|
|
209
|
+
expect(results[0]).toMatchObject({
|
|
210
|
+
status: 'loading',
|
|
211
|
+
isFetching: true,
|
|
212
|
+
data: undefined,
|
|
213
|
+
})
|
|
214
|
+
expect(results[1]).toMatchObject({
|
|
215
|
+
status: 'success',
|
|
216
|
+
isFetching: false,
|
|
217
|
+
data: { myCount: 1 },
|
|
218
|
+
})
|
|
219
|
+
expect(results[2]).toMatchObject({
|
|
220
|
+
status: 'success',
|
|
221
|
+
isFetching: true,
|
|
222
|
+
data: { myCount: 1 },
|
|
223
|
+
})
|
|
224
|
+
expect(results[3]).toMatchObject({
|
|
225
|
+
status: 'success',
|
|
226
|
+
isFetching: false,
|
|
227
|
+
data: { myCount: 1 },
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('should not run the selector again if the data did not change', async () => {
|
|
232
|
+
const key = queryKey()
|
|
233
|
+
let count = 0
|
|
234
|
+
const observer = new QueryObserver(queryClient, {
|
|
235
|
+
queryKey: key,
|
|
236
|
+
queryFn: () => ({ count: 1 }),
|
|
237
|
+
select: (data) => {
|
|
238
|
+
count++
|
|
239
|
+
return { myCount: data.count }
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
const observerResult1 = await observer.refetch()
|
|
243
|
+
const observerResult2 = await observer.refetch()
|
|
244
|
+
expect(count).toBe(1)
|
|
245
|
+
expect(observerResult1.data).toMatchObject({ myCount: 1 })
|
|
246
|
+
expect(observerResult2.data).toMatchObject({ myCount: 1 })
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('should always run the selector again if selector throws an error and selector is not referentially stable', async () => {
|
|
250
|
+
const key = queryKey()
|
|
251
|
+
const results: QueryObserverResult[] = []
|
|
252
|
+
const queryFn = async () => {
|
|
253
|
+
await sleep(10)
|
|
254
|
+
return { count: 1 }
|
|
255
|
+
}
|
|
256
|
+
const observer = new QueryObserver(queryClient, {
|
|
257
|
+
queryKey: key,
|
|
258
|
+
queryFn,
|
|
259
|
+
select: () => {
|
|
260
|
+
throw new Error('selector error')
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
264
|
+
results.push(result)
|
|
265
|
+
})
|
|
266
|
+
await sleep(50)
|
|
267
|
+
await observer.refetch()
|
|
268
|
+
unsubscribe()
|
|
269
|
+
expect(results[0]).toMatchObject({
|
|
270
|
+
status: 'loading',
|
|
271
|
+
isFetching: true,
|
|
272
|
+
data: undefined,
|
|
273
|
+
})
|
|
274
|
+
expect(results[1]).toMatchObject({
|
|
275
|
+
status: 'error',
|
|
276
|
+
isFetching: false,
|
|
277
|
+
data: undefined,
|
|
278
|
+
})
|
|
279
|
+
expect(results[2]).toMatchObject({
|
|
280
|
+
status: 'error',
|
|
281
|
+
isFetching: true,
|
|
282
|
+
data: undefined,
|
|
283
|
+
})
|
|
284
|
+
expect(results[3]).toMatchObject({
|
|
285
|
+
status: 'error',
|
|
286
|
+
isFetching: false,
|
|
287
|
+
data: undefined,
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('should return stale data if selector throws an error', async () => {
|
|
292
|
+
const key = queryKey()
|
|
293
|
+
const results: QueryObserverResult[] = []
|
|
294
|
+
let shouldError = false
|
|
295
|
+
const error = new Error('select error')
|
|
296
|
+
const observer = new QueryObserver(queryClient, {
|
|
297
|
+
queryKey: key,
|
|
298
|
+
retry: 0,
|
|
299
|
+
queryFn: async () => {
|
|
300
|
+
await sleep(10)
|
|
301
|
+
return shouldError ? 2 : 1
|
|
302
|
+
},
|
|
303
|
+
select: (num) => {
|
|
304
|
+
if (shouldError) {
|
|
305
|
+
throw error
|
|
306
|
+
}
|
|
307
|
+
shouldError = true
|
|
308
|
+
return String(num)
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
313
|
+
results.push(result)
|
|
314
|
+
})
|
|
315
|
+
await sleep(50)
|
|
316
|
+
await observer.refetch()
|
|
317
|
+
unsubscribe()
|
|
318
|
+
|
|
319
|
+
expect(results[0]).toMatchObject({
|
|
320
|
+
status: 'loading',
|
|
321
|
+
isFetching: true,
|
|
322
|
+
data: undefined,
|
|
323
|
+
error: null,
|
|
324
|
+
})
|
|
325
|
+
expect(results[1]).toMatchObject({
|
|
326
|
+
status: 'success',
|
|
327
|
+
isFetching: false,
|
|
328
|
+
data: '1',
|
|
329
|
+
error: null,
|
|
330
|
+
})
|
|
331
|
+
expect(results[2]).toMatchObject({
|
|
332
|
+
status: 'success',
|
|
333
|
+
isFetching: true,
|
|
334
|
+
data: '1',
|
|
335
|
+
error: null,
|
|
336
|
+
})
|
|
337
|
+
expect(results[3]).toMatchObject({
|
|
338
|
+
status: 'error',
|
|
339
|
+
isFetching: false,
|
|
340
|
+
data: '1',
|
|
341
|
+
error,
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('should structurally share the selector', async () => {
|
|
346
|
+
const key = queryKey()
|
|
347
|
+
let count = 0
|
|
348
|
+
const observer = new QueryObserver(queryClient, {
|
|
349
|
+
queryKey: key,
|
|
350
|
+
queryFn: () => ({ count: ++count }),
|
|
351
|
+
select: () => ({ myCount: 1 }),
|
|
352
|
+
})
|
|
353
|
+
const observerResult1 = await observer.refetch()
|
|
354
|
+
const observerResult2 = await observer.refetch()
|
|
355
|
+
expect(count).toBe(2)
|
|
356
|
+
expect(observerResult1.data).toBe(observerResult2.data)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('should not trigger a fetch when subscribed and disabled', async () => {
|
|
360
|
+
const key = queryKey()
|
|
361
|
+
const queryFn = jest.fn<string, unknown[]>().mockReturnValue('data')
|
|
362
|
+
const observer = new QueryObserver(queryClient, {
|
|
363
|
+
queryKey: key,
|
|
364
|
+
queryFn,
|
|
365
|
+
enabled: false,
|
|
366
|
+
})
|
|
367
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
368
|
+
await sleep(1)
|
|
369
|
+
unsubscribe()
|
|
370
|
+
expect(queryFn).toHaveBeenCalledTimes(0)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('should not trigger a fetch when not subscribed', async () => {
|
|
374
|
+
const key = queryKey()
|
|
375
|
+
const queryFn = jest.fn<string, unknown[]>().mockReturnValue('data')
|
|
376
|
+
new QueryObserver(queryClient, { queryKey: key, queryFn })
|
|
377
|
+
await sleep(1)
|
|
378
|
+
expect(queryFn).toHaveBeenCalledTimes(0)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
test('should be able to watch a query without defining a query function', async () => {
|
|
382
|
+
const key = queryKey()
|
|
383
|
+
const queryFn = jest.fn<string, unknown[]>().mockReturnValue('data')
|
|
384
|
+
const callback = jest.fn()
|
|
385
|
+
const observer = new QueryObserver(queryClient, {
|
|
386
|
+
queryKey: key,
|
|
387
|
+
enabled: false,
|
|
388
|
+
})
|
|
389
|
+
const unsubscribe = observer.subscribe(callback)
|
|
390
|
+
await queryClient.fetchQuery(key, queryFn)
|
|
391
|
+
unsubscribe()
|
|
392
|
+
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
393
|
+
expect(callback).toHaveBeenCalledTimes(2)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
test('should accept unresolved query config in update function', async () => {
|
|
397
|
+
const key = queryKey()
|
|
398
|
+
const queryFn = jest.fn<string, unknown[]>().mockReturnValue('data')
|
|
399
|
+
const observer = new QueryObserver(queryClient, {
|
|
400
|
+
queryKey: key,
|
|
401
|
+
enabled: false,
|
|
402
|
+
})
|
|
403
|
+
const results: QueryObserverResult<unknown>[] = []
|
|
404
|
+
const unsubscribe = observer.subscribe((x) => {
|
|
405
|
+
results.push(x)
|
|
406
|
+
})
|
|
407
|
+
observer.setOptions({ enabled: false, staleTime: 10 })
|
|
408
|
+
await queryClient.fetchQuery(key, queryFn)
|
|
409
|
+
await sleep(100)
|
|
410
|
+
unsubscribe()
|
|
411
|
+
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
412
|
+
expect(results.length).toBe(3)
|
|
413
|
+
expect(results[0]).toMatchObject({ isStale: true })
|
|
414
|
+
expect(results[1]).toMatchObject({ isStale: false })
|
|
415
|
+
expect(results[2]).toMatchObject({ isStale: true })
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('should be able to handle multiple subscribers', async () => {
|
|
419
|
+
const key = queryKey()
|
|
420
|
+
const queryFn = jest.fn<string, unknown[]>().mockReturnValue('data')
|
|
421
|
+
const observer = new QueryObserver<string>(queryClient, {
|
|
422
|
+
queryKey: key,
|
|
423
|
+
enabled: false,
|
|
424
|
+
})
|
|
425
|
+
const results1: QueryObserverResult<string>[] = []
|
|
426
|
+
const results2: QueryObserverResult<string>[] = []
|
|
427
|
+
const unsubscribe1 = observer.subscribe((x) => {
|
|
428
|
+
results1.push(x)
|
|
429
|
+
})
|
|
430
|
+
const unsubscribe2 = observer.subscribe((x) => {
|
|
431
|
+
results2.push(x)
|
|
432
|
+
})
|
|
433
|
+
await queryClient.fetchQuery(key, queryFn)
|
|
434
|
+
await sleep(50)
|
|
435
|
+
unsubscribe1()
|
|
436
|
+
unsubscribe2()
|
|
437
|
+
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
438
|
+
expect(results1.length).toBe(2)
|
|
439
|
+
expect(results2.length).toBe(2)
|
|
440
|
+
expect(results1[0]).toMatchObject({ data: undefined })
|
|
441
|
+
expect(results1[1]).toMatchObject({ data: 'data' })
|
|
442
|
+
expect(results2[0]).toMatchObject({ data: undefined })
|
|
443
|
+
expect(results2[1]).toMatchObject({ data: 'data' })
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
test('should stop retry when unsubscribing', async () => {
|
|
447
|
+
const key = queryKey()
|
|
448
|
+
let count = 0
|
|
449
|
+
const observer = new QueryObserver(queryClient, {
|
|
450
|
+
queryKey: key,
|
|
451
|
+
queryFn: () => {
|
|
452
|
+
count++
|
|
453
|
+
return Promise.reject<unknown>('reject')
|
|
454
|
+
},
|
|
455
|
+
retry: 10,
|
|
456
|
+
retryDelay: 50,
|
|
457
|
+
})
|
|
458
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
459
|
+
await sleep(70)
|
|
460
|
+
unsubscribe()
|
|
461
|
+
await sleep(200)
|
|
462
|
+
expect(count).toBe(2)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('should clear interval when unsubscribing to a refetchInterval query', async () => {
|
|
466
|
+
const key = queryKey()
|
|
467
|
+
|
|
468
|
+
const fetchData = () => Promise.resolve('data')
|
|
469
|
+
const observer = new QueryObserver(queryClient, {
|
|
470
|
+
queryKey: key,
|
|
471
|
+
queryFn: fetchData,
|
|
472
|
+
cacheTime: 0,
|
|
473
|
+
refetchInterval: 1,
|
|
474
|
+
})
|
|
475
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
476
|
+
// @ts-expect-error
|
|
477
|
+
expect(observer.refetchIntervalId).not.toBeUndefined()
|
|
478
|
+
unsubscribe()
|
|
479
|
+
// @ts-expect-error
|
|
480
|
+
expect(observer.refetchIntervalId).toBeUndefined()
|
|
481
|
+
await sleep(10)
|
|
482
|
+
expect(queryClient.getQueryCache().find(key)).toBeUndefined()
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
test('uses placeholderData as non-cache data when loading a query with no data', async () => {
|
|
486
|
+
const key = queryKey()
|
|
487
|
+
const observer = new QueryObserver(queryClient, {
|
|
488
|
+
queryKey: key,
|
|
489
|
+
queryFn: () => 'data',
|
|
490
|
+
placeholderData: 'placeholder',
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
expect(observer.getCurrentResult()).toMatchObject({
|
|
494
|
+
status: 'success',
|
|
495
|
+
data: 'placeholder',
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
const results: QueryObserverResult<unknown>[] = []
|
|
499
|
+
|
|
500
|
+
const unsubscribe = observer.subscribe((x) => {
|
|
501
|
+
results.push(x)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
await sleep(10)
|
|
505
|
+
unsubscribe()
|
|
506
|
+
|
|
507
|
+
expect(results.length).toBe(2)
|
|
508
|
+
expect(results[0]).toMatchObject({ status: 'success', data: 'placeholder' })
|
|
509
|
+
expect(results[1]).toMatchObject({ status: 'success', data: 'data' })
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
test('the retrier should not throw an error when reject if the retrier is already resolved', async () => {
|
|
513
|
+
const key = queryKey()
|
|
514
|
+
let count = 0
|
|
515
|
+
|
|
516
|
+
const observer = new QueryObserver(queryClient, {
|
|
517
|
+
queryKey: key,
|
|
518
|
+
queryFn: () => {
|
|
519
|
+
count++
|
|
520
|
+
return Promise.reject<unknown>(`reject ${count}`)
|
|
521
|
+
},
|
|
522
|
+
retry: 1,
|
|
523
|
+
retryDelay: 20,
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
527
|
+
|
|
528
|
+
// Simulate a race condition when an unsubscribe and a retry occur.
|
|
529
|
+
await sleep(20)
|
|
530
|
+
unsubscribe()
|
|
531
|
+
|
|
532
|
+
// A second reject is triggered for the retry
|
|
533
|
+
// but the retryer has already set isResolved to true
|
|
534
|
+
// so it does nothing and no error is thrown
|
|
535
|
+
|
|
536
|
+
// Should not log an error
|
|
537
|
+
queryClient.clear()
|
|
538
|
+
await sleep(40)
|
|
539
|
+
expect(mockLogger.error).not.toHaveBeenNthCalledWith(1, 'reject 1')
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('should throw an error if enabled option type is not valid', async () => {
|
|
543
|
+
const key = queryKey()
|
|
544
|
+
|
|
545
|
+
expect(
|
|
546
|
+
() =>
|
|
547
|
+
new QueryObserver(queryClient, {
|
|
548
|
+
queryKey: key,
|
|
549
|
+
queryFn: () => 'data',
|
|
550
|
+
//@ts-expect-error
|
|
551
|
+
enabled: null,
|
|
552
|
+
}),
|
|
553
|
+
).toThrowError('Expected enabled to be a boolean')
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
test('getCurrentQuery should return the current query', async () => {
|
|
557
|
+
const key = queryKey()
|
|
558
|
+
|
|
559
|
+
const observer = new QueryObserver(queryClient, {
|
|
560
|
+
queryKey: key,
|
|
561
|
+
queryFn: () => 'data',
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
expect(observer.getCurrentQuery().queryKey).toEqual(key)
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
test('should throw an error if throwOnError option is true', async () => {
|
|
568
|
+
const key = queryKey()
|
|
569
|
+
|
|
570
|
+
const observer = new QueryObserver(queryClient, {
|
|
571
|
+
queryKey: key,
|
|
572
|
+
queryFn: () => Promise.reject<unknown>('error'),
|
|
573
|
+
retry: false,
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
let error: string | null = null
|
|
577
|
+
try {
|
|
578
|
+
await observer.refetch({ throwOnError: true })
|
|
579
|
+
} catch (err) {
|
|
580
|
+
error = err as string
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
expect(error).toEqual('error')
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
test('should not refetch in background if refetchIntervalInBackground is false', async () => {
|
|
587
|
+
const key = queryKey()
|
|
588
|
+
const queryFn = jest.fn<string, unknown[]>().mockReturnValue('data')
|
|
589
|
+
|
|
590
|
+
focusManager.setFocused(false)
|
|
591
|
+
const observer = new QueryObserver(queryClient, {
|
|
592
|
+
queryKey: key,
|
|
593
|
+
queryFn,
|
|
594
|
+
refetchIntervalInBackground: false,
|
|
595
|
+
refetchInterval: 10,
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
599
|
+
await sleep(30)
|
|
600
|
+
|
|
601
|
+
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
602
|
+
|
|
603
|
+
// Clean-up
|
|
604
|
+
unsubscribe()
|
|
605
|
+
focusManager.setFocused(true)
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
test('should not use replaceEqualDeep for select value when structuralSharing option is true', async () => {
|
|
609
|
+
const key = queryKey()
|
|
610
|
+
|
|
611
|
+
const data = { value: 'data' }
|
|
612
|
+
const selectedData = { value: 'data' }
|
|
613
|
+
|
|
614
|
+
const observer = new QueryObserver(queryClient, {
|
|
615
|
+
queryKey: key,
|
|
616
|
+
queryFn: () => data,
|
|
617
|
+
select: () => data,
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
621
|
+
|
|
622
|
+
await sleep(10)
|
|
623
|
+
expect(observer.getCurrentResult().data).toBe(data)
|
|
624
|
+
|
|
625
|
+
observer.setOptions({
|
|
626
|
+
queryKey: key,
|
|
627
|
+
queryFn: () => data,
|
|
628
|
+
structuralSharing: false,
|
|
629
|
+
select: () => selectedData,
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
await observer.refetch()
|
|
633
|
+
expect(observer.getCurrentResult().data).toBe(selectedData)
|
|
634
|
+
|
|
635
|
+
unsubscribe()
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
test('should prefer isDataEqual to structuralSharing', async () => {
|
|
639
|
+
const key = queryKey()
|
|
640
|
+
|
|
641
|
+
const data = { value: 'data' }
|
|
642
|
+
const newData = { value: 'data' }
|
|
643
|
+
|
|
644
|
+
const observer = new QueryObserver(queryClient, {
|
|
645
|
+
queryKey: key,
|
|
646
|
+
queryFn: () => data,
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
const unsubscribe = observer.subscribe(() => undefined)
|
|
650
|
+
|
|
651
|
+
await sleep(10)
|
|
652
|
+
expect(observer.getCurrentResult().data).toBe(data)
|
|
653
|
+
|
|
654
|
+
observer.setOptions({
|
|
655
|
+
queryKey: key,
|
|
656
|
+
queryFn: () => newData,
|
|
657
|
+
isDataEqual: () => true,
|
|
658
|
+
structuralSharing: false,
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
await observer.refetch()
|
|
662
|
+
expect(observer.getCurrentResult().data).toBe(data)
|
|
663
|
+
|
|
664
|
+
unsubscribe()
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
test('select function error using placeholderdata should log an error', () => {
|
|
668
|
+
const key = queryKey()
|
|
669
|
+
|
|
670
|
+
new QueryObserver(queryClient, {
|
|
671
|
+
queryKey: key,
|
|
672
|
+
queryFn: () => 'data',
|
|
673
|
+
placeholderData: 'placeholderdata',
|
|
674
|
+
select: () => {
|
|
675
|
+
throw new Error('error')
|
|
676
|
+
},
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
expect(mockLogger.error).toHaveBeenNthCalledWith(1, new Error('error'))
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
test('should not use replaceEqualDeep for select value when structuralSharing option is true and placeholderdata is defined', () => {
|
|
683
|
+
const key = queryKey()
|
|
684
|
+
|
|
685
|
+
const data = { value: 'data' }
|
|
686
|
+
const selectedData1 = { value: 'data' }
|
|
687
|
+
const selectedData2 = { value: 'data' }
|
|
688
|
+
const placeholderData1 = { value: 'data' }
|
|
689
|
+
const placeholderData2 = { value: 'data' }
|
|
690
|
+
|
|
691
|
+
const observer = new QueryObserver(queryClient, {
|
|
692
|
+
queryKey: key,
|
|
693
|
+
queryFn: () => data,
|
|
694
|
+
select: () => data,
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
observer.setOptions({
|
|
698
|
+
queryKey: key,
|
|
699
|
+
queryFn: () => data,
|
|
700
|
+
select: () => {
|
|
701
|
+
return selectedData1
|
|
702
|
+
},
|
|
703
|
+
placeholderData: placeholderData1,
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
observer.setOptions({
|
|
707
|
+
queryKey: key,
|
|
708
|
+
queryFn: () => data,
|
|
709
|
+
select: () => {
|
|
710
|
+
return selectedData2
|
|
711
|
+
},
|
|
712
|
+
placeholderData: placeholderData2,
|
|
713
|
+
structuralSharing: false,
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
expect(observer.getCurrentResult().data).toBe(selectedData2)
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
test('should not use an undefined value returned by select as placeholderdata', () => {
|
|
720
|
+
const key = queryKey()
|
|
721
|
+
|
|
722
|
+
const data = { value: 'data' }
|
|
723
|
+
const selectedData = { value: 'data' }
|
|
724
|
+
const placeholderData1 = { value: 'data' }
|
|
725
|
+
const placeholderData2 = { value: 'data' }
|
|
726
|
+
|
|
727
|
+
const observer = new QueryObserver(queryClient, {
|
|
728
|
+
queryKey: key,
|
|
729
|
+
queryFn: () => data,
|
|
730
|
+
select: () => data,
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
observer.setOptions({
|
|
734
|
+
queryKey: key,
|
|
735
|
+
queryFn: () => data,
|
|
736
|
+
select: () => {
|
|
737
|
+
return selectedData
|
|
738
|
+
},
|
|
739
|
+
placeholderData: placeholderData1,
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
expect(observer.getCurrentResult().isPlaceholderData).toBe(true)
|
|
743
|
+
|
|
744
|
+
observer.setOptions({
|
|
745
|
+
queryKey: key,
|
|
746
|
+
queryFn: () => data,
|
|
747
|
+
//@ts-expect-error
|
|
748
|
+
select: () => undefined,
|
|
749
|
+
placeholderData: placeholderData2,
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
expect(observer.getCurrentResult().isPlaceholderData).toBe(false)
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
test('updateResult should not notify cache listeners if cache option is false', async () => {
|
|
756
|
+
const key = queryKey()
|
|
757
|
+
|
|
758
|
+
const data1 = { value: 'data 1' }
|
|
759
|
+
const data2 = { value: 'data 2' }
|
|
760
|
+
|
|
761
|
+
await queryClient.prefetchQuery(key, () => data1)
|
|
762
|
+
const observer = new QueryObserver(queryClient, {
|
|
763
|
+
queryKey: key,
|
|
764
|
+
})
|
|
765
|
+
await queryClient.prefetchQuery(key, () => data2)
|
|
766
|
+
|
|
767
|
+
const spy = jest.fn()
|
|
768
|
+
const unsubscribe = queryClient.getQueryCache().subscribe(spy)
|
|
769
|
+
observer.updateResult({ cache: false })
|
|
770
|
+
|
|
771
|
+
expect(spy).toHaveBeenCalledTimes(0)
|
|
772
|
+
|
|
773
|
+
unsubscribe()
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
test('should not notify observer when the stale timeout expires and the current result is stale', async () => {
|
|
777
|
+
const key = queryKey()
|
|
778
|
+
const queryFn = () => 'data'
|
|
779
|
+
|
|
780
|
+
await queryClient.prefetchQuery(key, queryFn)
|
|
781
|
+
const observer = new QueryObserver(queryClient, {
|
|
782
|
+
queryKey: key,
|
|
783
|
+
queryFn,
|
|
784
|
+
staleTime: 20,
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
const spy = jest.fn()
|
|
788
|
+
const unsubscribe = observer.subscribe(spy)
|
|
789
|
+
await queryClient.refetchQueries(key)
|
|
790
|
+
await sleep(10)
|
|
791
|
+
|
|
792
|
+
// Force isStale to true
|
|
793
|
+
// because no use case has been found to reproduce this condition
|
|
794
|
+
// @ts-ignore
|
|
795
|
+
observer['currentResult'].isStale = true
|
|
796
|
+
spy.mockReset()
|
|
797
|
+
await sleep(30)
|
|
798
|
+
expect(spy).not.toHaveBeenCalled()
|
|
799
|
+
|
|
800
|
+
unsubscribe()
|
|
801
|
+
})
|
|
802
|
+
})
|