@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,37 @@
1
+ import { isServer, isValidTimeout } from './utils'
2
+
3
+ export abstract class Removable {
4
+ cacheTime!: number
5
+ private gcTimeout?: ReturnType<typeof setTimeout>
6
+
7
+ destroy(): void {
8
+ this.clearGcTimeout()
9
+ }
10
+
11
+ protected scheduleGc(): void {
12
+ this.clearGcTimeout()
13
+
14
+ if (isValidTimeout(this.cacheTime)) {
15
+ this.gcTimeout = setTimeout(() => {
16
+ this.optionalRemove()
17
+ }, this.cacheTime)
18
+ }
19
+ }
20
+
21
+ protected updateCacheTime(newCacheTime: number | undefined): void {
22
+ // Default to 5 minutes (Infinity for server-side) if no cache time is set
23
+ this.cacheTime = Math.max(
24
+ this.cacheTime || 0,
25
+ newCacheTime ?? (isServer ? Infinity : 5 * 60 * 1000),
26
+ )
27
+ }
28
+
29
+ protected clearGcTimeout() {
30
+ if (this.gcTimeout) {
31
+ clearTimeout(this.gcTimeout)
32
+ this.gcTimeout = undefined
33
+ }
34
+ }
35
+
36
+ protected abstract optionalRemove(): void
37
+ }
package/src/retryer.ts ADDED
@@ -0,0 +1,215 @@
1
+ import { focusManager } from './focusManager'
2
+ import { onlineManager } from './onlineManager'
3
+ import { sleep } from './utils'
4
+ import { CancelOptions, NetworkMode } from './types'
5
+
6
+ // TYPES
7
+
8
+ interface RetryerConfig<TData = unknown, TError = unknown> {
9
+ fn: () => TData | Promise<TData>
10
+ abort?: () => void
11
+ onError?: (error: TError) => void
12
+ onSuccess?: (data: TData) => void
13
+ onFail?: (failureCount: number, error: TError) => void
14
+ onPause?: () => void
15
+ onContinue?: () => void
16
+ retry?: RetryValue<TError>
17
+ retryDelay?: RetryDelayValue<TError>
18
+ networkMode: NetworkMode | undefined
19
+ }
20
+
21
+ export interface Retryer<TData = unknown> {
22
+ promise: Promise<TData>
23
+ cancel: (cancelOptions?: CancelOptions) => void
24
+ continue: () => void
25
+ cancelRetry: () => void
26
+ continueRetry: () => void
27
+ }
28
+
29
+ export type RetryValue<TError> = boolean | number | ShouldRetryFunction<TError>
30
+
31
+ type ShouldRetryFunction<TError> = (
32
+ failureCount: number,
33
+ error: TError,
34
+ ) => boolean
35
+
36
+ export type RetryDelayValue<TError> = number | RetryDelayFunction<TError>
37
+
38
+ type RetryDelayFunction<TError = unknown> = (
39
+ failureCount: number,
40
+ error: TError,
41
+ ) => number
42
+
43
+ function defaultRetryDelay(failureCount: number) {
44
+ return Math.min(1000 * 2 ** failureCount, 30000)
45
+ }
46
+
47
+ export function canFetch(networkMode: NetworkMode | undefined): boolean {
48
+ return (networkMode ?? 'online') === 'online'
49
+ ? onlineManager.isOnline()
50
+ : true
51
+ }
52
+
53
+ export class CancelledError {
54
+ revert?: boolean
55
+ silent?: boolean
56
+ constructor(options?: CancelOptions) {
57
+ this.revert = options?.revert
58
+ this.silent = options?.silent
59
+ }
60
+ }
61
+
62
+ export function isCancelledError(value: any): value is CancelledError {
63
+ return value instanceof CancelledError
64
+ }
65
+
66
+ export function createRetryer<TData = unknown, TError = unknown>(
67
+ config: RetryerConfig<TData, TError>,
68
+ ): Retryer<TData> {
69
+ let isRetryCancelled = false
70
+ let failureCount = 0
71
+ let isResolved = false
72
+ let continueFn: ((value?: unknown) => void) | undefined
73
+ let promiseResolve: (data: TData) => void
74
+ let promiseReject: (error: TError) => void
75
+
76
+ const promise = new Promise<TData>((outerResolve, outerReject) => {
77
+ promiseResolve = outerResolve
78
+ promiseReject = outerReject
79
+ })
80
+
81
+ const cancel = (cancelOptions?: CancelOptions): void => {
82
+ if (!isResolved) {
83
+ reject(new CancelledError(cancelOptions))
84
+
85
+ config.abort?.()
86
+ }
87
+ }
88
+ const cancelRetry = () => {
89
+ isRetryCancelled = true
90
+ }
91
+
92
+ const continueRetry = () => {
93
+ isRetryCancelled = false
94
+ }
95
+
96
+ const shouldPause = () =>
97
+ !focusManager.isFocused() ||
98
+ (config.networkMode !== 'always' && !onlineManager.isOnline())
99
+
100
+ const resolve = (value: any) => {
101
+ if (!isResolved) {
102
+ isResolved = true
103
+ config.onSuccess?.(value)
104
+ continueFn?.()
105
+ promiseResolve(value)
106
+ }
107
+ }
108
+
109
+ const reject = (value: any) => {
110
+ if (!isResolved) {
111
+ isResolved = true
112
+ config.onError?.(value)
113
+ continueFn?.()
114
+ promiseReject(value)
115
+ }
116
+ }
117
+
118
+ const pause = () => {
119
+ return new Promise((continueResolve) => {
120
+ continueFn = (value) => {
121
+ if (isResolved || !shouldPause()) {
122
+ return continueResolve(value)
123
+ }
124
+ }
125
+ config.onPause?.()
126
+ }).then(() => {
127
+ continueFn = undefined
128
+ if (!isResolved) {
129
+ config.onContinue?.()
130
+ }
131
+ })
132
+ }
133
+
134
+ // Create loop function
135
+ const run = () => {
136
+ // Do nothing if already resolved
137
+ if (isResolved) {
138
+ return
139
+ }
140
+
141
+ let promiseOrValue: any
142
+
143
+ // Execute query
144
+ try {
145
+ promiseOrValue = config.fn()
146
+ } catch (error) {
147
+ promiseOrValue = Promise.reject(error)
148
+ }
149
+
150
+ Promise.resolve(promiseOrValue)
151
+ .then(resolve)
152
+ .catch((error) => {
153
+ // Stop if the fetch is already resolved
154
+ if (isResolved) {
155
+ return
156
+ }
157
+
158
+ // Do we need to retry the request?
159
+ const retry = config.retry ?? 3
160
+ const retryDelay = config.retryDelay ?? defaultRetryDelay
161
+ const delay =
162
+ typeof retryDelay === 'function'
163
+ ? retryDelay(failureCount, error)
164
+ : retryDelay
165
+ const shouldRetry =
166
+ retry === true ||
167
+ (typeof retry === 'number' && failureCount < retry) ||
168
+ (typeof retry === 'function' && retry(failureCount, error))
169
+
170
+ if (isRetryCancelled || !shouldRetry) {
171
+ // We are done if the query does not need to be retried
172
+ reject(error)
173
+ return
174
+ }
175
+
176
+ failureCount++
177
+
178
+ // Notify on fail
179
+ config.onFail?.(failureCount, error)
180
+
181
+ // Delay
182
+ sleep(delay)
183
+ // Pause if the document is not visible or when the device is offline
184
+ .then(() => {
185
+ if (shouldPause()) {
186
+ return pause()
187
+ }
188
+ })
189
+ .then(() => {
190
+ if (isRetryCancelled) {
191
+ reject(error)
192
+ } else {
193
+ run()
194
+ }
195
+ })
196
+ })
197
+ }
198
+
199
+ // Start loop
200
+ if (canFetch(config.networkMode)) {
201
+ run()
202
+ } else {
203
+ pause().then(run)
204
+ }
205
+
206
+ return {
207
+ promise,
208
+ cancel,
209
+ continue: () => {
210
+ continueFn?.()
211
+ },
212
+ cancelRetry,
213
+ continueRetry,
214
+ }
215
+ }
@@ -0,0 +1,33 @@
1
+ type Listener = () => void
2
+
3
+ export class Subscribable<TListener extends Function = Listener> {
4
+ protected listeners: TListener[]
5
+
6
+ constructor() {
7
+ this.listeners = []
8
+ this.subscribe = this.subscribe.bind(this)
9
+ }
10
+
11
+ subscribe(listener: TListener): () => void {
12
+ this.listeners.push(listener as TListener)
13
+
14
+ this.onSubscribe()
15
+
16
+ return () => {
17
+ this.listeners = this.listeners.filter((x) => x !== listener)
18
+ this.onUnsubscribe()
19
+ }
20
+ }
21
+
22
+ hasListeners(): boolean {
23
+ return this.listeners.length > 0
24
+ }
25
+
26
+ protected onSubscribe(): void {
27
+ // Do nothing
28
+ }
29
+
30
+ protected onUnsubscribe(): void {
31
+ // Do nothing
32
+ }
33
+ }
@@ -0,0 +1,155 @@
1
+ import { sleep } from '../utils'
2
+ import { FocusManager } from '../focusManager'
3
+ import { setIsServer } from '../../../../tests/utils'
4
+
5
+ describe('focusManager', () => {
6
+ let focusManager: FocusManager
7
+ beforeEach(() => {
8
+ jest.resetModules()
9
+ focusManager = new FocusManager()
10
+ })
11
+
12
+ it('should call previous remove handler when replacing an event listener', () => {
13
+ const remove1Spy = jest.fn()
14
+ const remove2Spy = jest.fn()
15
+
16
+ focusManager.setEventListener(() => remove1Spy)
17
+ focusManager.setEventListener(() => remove2Spy)
18
+
19
+ expect(remove1Spy).toHaveBeenCalledTimes(1)
20
+ expect(remove2Spy).not.toHaveBeenCalled()
21
+ })
22
+
23
+ it('should use focused boolean arg', async () => {
24
+ let count = 0
25
+
26
+ const setup = (setFocused: (focused?: boolean) => void) => {
27
+ setTimeout(() => {
28
+ count++
29
+ setFocused(true)
30
+ }, 20)
31
+ return () => void 0
32
+ }
33
+
34
+ focusManager.setEventListener(setup)
35
+
36
+ await sleep(30)
37
+ expect(count).toEqual(1)
38
+ expect(focusManager.isFocused()).toBeTruthy()
39
+ })
40
+
41
+ it('should not notify listeners on focus if already focused', async () => {
42
+ const subscriptionSpy = jest.fn()
43
+ const unsubscribe = focusManager.subscribe(subscriptionSpy)
44
+
45
+ focusManager.setFocused(true)
46
+ expect(subscriptionSpy).toHaveBeenCalledTimes(1)
47
+ subscriptionSpy.mockReset()
48
+
49
+ focusManager.setFocused(false)
50
+ expect(subscriptionSpy).toHaveBeenCalledTimes(0)
51
+
52
+ unsubscribe()
53
+ })
54
+
55
+ it('should return true for isFocused if document is undefined', async () => {
56
+ const { document } = globalThis
57
+
58
+ // @ts-expect-error
59
+ delete globalThis.document
60
+
61
+ focusManager.setFocused()
62
+ expect(focusManager.isFocused()).toBeTruthy()
63
+ globalThis.document = document
64
+ })
65
+
66
+ test('cleanup should still be undefined if window is not defined', async () => {
67
+ const restoreIsServer = setIsServer(true)
68
+
69
+ const unsubscribe = focusManager.subscribe(() => undefined)
70
+ expect(focusManager['cleanup']).toBeUndefined()
71
+
72
+ unsubscribe()
73
+ restoreIsServer()
74
+ })
75
+
76
+ test('cleanup should still be undefined if window.addEventListener is not defined', async () => {
77
+ const { addEventListener } = globalThis.window
78
+
79
+ // @ts-expect-error
80
+ globalThis.window.addEventListener = undefined
81
+
82
+ const unsubscribe = focusManager.subscribe(() => undefined)
83
+ expect(focusManager['cleanup']).toBeUndefined()
84
+
85
+ unsubscribe()
86
+ globalThis.window.addEventListener = addEventListener
87
+ })
88
+
89
+ it('should replace default window listener when a new event listener is set', async () => {
90
+ const addEventListenerSpy = jest.spyOn(
91
+ globalThis.window,
92
+ 'addEventListener',
93
+ )
94
+
95
+ const removeEventListenerSpy = jest.spyOn(
96
+ globalThis.window,
97
+ 'removeEventListener',
98
+ )
99
+
100
+ // Should set the default event listener with window event listeners
101
+ const unsubscribe = focusManager.subscribe(() => undefined)
102
+ expect(addEventListenerSpy).toHaveBeenCalledTimes(2)
103
+
104
+ // Should replace the window default event listener by a new one
105
+ // and it should call window.removeEventListener twice
106
+ focusManager.setEventListener(() => {
107
+ return () => void 0
108
+ })
109
+
110
+ expect(removeEventListenerSpy).toHaveBeenCalledTimes(2)
111
+
112
+ unsubscribe()
113
+ addEventListenerSpy.mockRestore()
114
+ removeEventListenerSpy.mockRestore()
115
+ })
116
+
117
+ test('should call removeEventListener when last listener unsubscribes', () => {
118
+ const addEventListenerSpy = jest.spyOn(
119
+ globalThis.window,
120
+ 'addEventListener',
121
+ )
122
+
123
+ const removeEventListenerSpy = jest.spyOn(
124
+ globalThis.window,
125
+ 'removeEventListener',
126
+ )
127
+
128
+ const unsubscribe1 = focusManager.subscribe(() => undefined)
129
+ const unsubscribe2 = focusManager.subscribe(() => undefined)
130
+ expect(addEventListenerSpy).toHaveBeenCalledTimes(2) // visibilitychange + focus
131
+
132
+ unsubscribe1()
133
+ expect(removeEventListenerSpy).toHaveBeenCalledTimes(0)
134
+ unsubscribe2()
135
+ expect(removeEventListenerSpy).toHaveBeenCalledTimes(2) // visibilitychange + focus
136
+ })
137
+
138
+ test('should keep setup function even if last listener unsubscribes', () => {
139
+ const setupSpy = jest.fn().mockImplementation(() => () => undefined)
140
+
141
+ focusManager.setEventListener(setupSpy)
142
+
143
+ const unsubscribe1 = focusManager.subscribe(() => undefined)
144
+
145
+ expect(setupSpy).toHaveBeenCalledTimes(1)
146
+
147
+ unsubscribe1()
148
+
149
+ const unsubscribe2 = focusManager.subscribe(() => undefined)
150
+
151
+ expect(setupSpy).toHaveBeenCalledTimes(2)
152
+
153
+ unsubscribe2()
154
+ })
155
+ })