@tanstack/query-core 5.83.1 → 5.87.4

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 (163) hide show
  1. package/build/legacy/{hydration-Cvr-9VdO.d.ts → hydration-BYonJkjc.d.ts} +8 -6
  2. package/build/legacy/{hydration-CADtEOkK.d.cts → hydration-_hO-y142.d.cts} +8 -6
  3. package/build/legacy/hydration.d.cts +1 -1
  4. package/build/legacy/hydration.d.ts +1 -1
  5. package/build/legacy/index.cjs +14 -12
  6. package/build/legacy/index.cjs.map +1 -1
  7. package/build/legacy/index.d.cts +5 -4
  8. package/build/legacy/index.d.ts +5 -4
  9. package/build/legacy/index.js +26 -23
  10. package/build/legacy/index.js.map +1 -1
  11. package/build/legacy/infiniteQueryBehavior.d.cts +1 -1
  12. package/build/legacy/infiniteQueryBehavior.d.ts +1 -1
  13. package/build/legacy/infiniteQueryObserver.d.cts +1 -1
  14. package/build/legacy/infiniteQueryObserver.d.ts +1 -1
  15. package/build/legacy/mutation.d.cts +1 -1
  16. package/build/legacy/mutation.d.ts +1 -1
  17. package/build/legacy/mutationCache.d.cts +1 -1
  18. package/build/legacy/mutationCache.d.ts +1 -1
  19. package/build/legacy/mutationObserver.d.cts +1 -1
  20. package/build/legacy/mutationObserver.d.ts +1 -1
  21. package/build/legacy/notifyManager.cjs +2 -1
  22. package/build/legacy/notifyManager.cjs.map +1 -1
  23. package/build/legacy/notifyManager.js +2 -1
  24. package/build/legacy/notifyManager.js.map +1 -1
  25. package/build/legacy/queriesObserver.cjs +19 -13
  26. package/build/legacy/queriesObserver.cjs.map +1 -1
  27. package/build/legacy/queriesObserver.d.cts +1 -1
  28. package/build/legacy/queriesObserver.d.ts +1 -1
  29. package/build/legacy/queriesObserver.js +20 -14
  30. package/build/legacy/queriesObserver.js.map +1 -1
  31. package/build/legacy/query.cjs +78 -61
  32. package/build/legacy/query.cjs.map +1 -1
  33. package/build/legacy/query.d.cts +1 -1
  34. package/build/legacy/query.d.ts +1 -1
  35. package/build/legacy/query.js +79 -62
  36. package/build/legacy/query.js.map +1 -1
  37. package/build/legacy/queryCache.d.cts +1 -1
  38. package/build/legacy/queryCache.d.ts +1 -1
  39. package/build/legacy/queryClient.d.cts +1 -1
  40. package/build/legacy/queryClient.d.ts +1 -1
  41. package/build/legacy/queryObserver.cjs +12 -9
  42. package/build/legacy/queryObserver.cjs.map +1 -1
  43. package/build/legacy/queryObserver.d.cts +1 -1
  44. package/build/legacy/queryObserver.d.ts +1 -1
  45. package/build/legacy/queryObserver.js +12 -9
  46. package/build/legacy/queryObserver.js.map +1 -1
  47. package/build/legacy/removable.cjs +3 -2
  48. package/build/legacy/removable.cjs.map +1 -1
  49. package/build/legacy/removable.js +3 -2
  50. package/build/legacy/removable.js.map +1 -1
  51. package/build/legacy/retryer.cjs +12 -16
  52. package/build/legacy/retryer.cjs.map +1 -1
  53. package/build/legacy/retryer.d.cts +1 -1
  54. package/build/legacy/retryer.d.ts +1 -1
  55. package/build/legacy/retryer.js +12 -16
  56. package/build/legacy/retryer.js.map +1 -1
  57. package/build/legacy/streamedQuery.cjs +7 -8
  58. package/build/legacy/streamedQuery.cjs.map +1 -1
  59. package/build/legacy/streamedQuery.d.cts +18 -10
  60. package/build/legacy/streamedQuery.d.ts +18 -10
  61. package/build/legacy/streamedQuery.js +7 -8
  62. package/build/legacy/streamedQuery.js.map +1 -1
  63. package/build/legacy/timeoutManager.cjs +110 -0
  64. package/build/legacy/timeoutManager.cjs.map +1 -0
  65. package/build/legacy/timeoutManager.d.cts +58 -0
  66. package/build/legacy/timeoutManager.d.ts +58 -0
  67. package/build/legacy/timeoutManager.js +81 -0
  68. package/build/legacy/timeoutManager.js.map +1 -0
  69. package/build/legacy/types.d.cts +1 -1
  70. package/build/legacy/types.d.ts +1 -1
  71. package/build/legacy/utils.cjs +26 -22
  72. package/build/legacy/utils.cjs.map +1 -1
  73. package/build/legacy/utils.d.cts +1 -1
  74. package/build/legacy/utils.d.ts +1 -1
  75. package/build/legacy/utils.js +26 -22
  76. package/build/legacy/utils.js.map +1 -1
  77. package/build/modern/{hydration-Cvr-9VdO.d.ts → hydration-BYonJkjc.d.ts} +8 -6
  78. package/build/modern/{hydration-CADtEOkK.d.cts → hydration-_hO-y142.d.cts} +8 -6
  79. package/build/modern/hydration.d.cts +1 -1
  80. package/build/modern/hydration.d.ts +1 -1
  81. package/build/modern/index.cjs +14 -12
  82. package/build/modern/index.cjs.map +1 -1
  83. package/build/modern/index.d.cts +5 -4
  84. package/build/modern/index.d.ts +5 -4
  85. package/build/modern/index.js +26 -23
  86. package/build/modern/index.js.map +1 -1
  87. package/build/modern/infiniteQueryBehavior.d.cts +1 -1
  88. package/build/modern/infiniteQueryBehavior.d.ts +1 -1
  89. package/build/modern/infiniteQueryObserver.d.cts +1 -1
  90. package/build/modern/infiniteQueryObserver.d.ts +1 -1
  91. package/build/modern/mutation.d.cts +1 -1
  92. package/build/modern/mutation.d.ts +1 -1
  93. package/build/modern/mutationCache.d.cts +1 -1
  94. package/build/modern/mutationCache.d.ts +1 -1
  95. package/build/modern/mutationObserver.d.cts +1 -1
  96. package/build/modern/mutationObserver.d.ts +1 -1
  97. package/build/modern/notifyManager.cjs +2 -1
  98. package/build/modern/notifyManager.cjs.map +1 -1
  99. package/build/modern/notifyManager.js +2 -1
  100. package/build/modern/notifyManager.js.map +1 -1
  101. package/build/modern/queriesObserver.cjs +19 -13
  102. package/build/modern/queriesObserver.cjs.map +1 -1
  103. package/build/modern/queriesObserver.d.cts +1 -1
  104. package/build/modern/queriesObserver.d.ts +1 -1
  105. package/build/modern/queriesObserver.js +20 -14
  106. package/build/modern/queriesObserver.js.map +1 -1
  107. package/build/modern/query.cjs +71 -52
  108. package/build/modern/query.cjs.map +1 -1
  109. package/build/modern/query.d.cts +1 -1
  110. package/build/modern/query.d.ts +1 -1
  111. package/build/modern/query.js +72 -53
  112. package/build/modern/query.js.map +1 -1
  113. package/build/modern/queryCache.d.cts +1 -1
  114. package/build/modern/queryCache.d.ts +1 -1
  115. package/build/modern/queryClient.d.cts +1 -1
  116. package/build/modern/queryClient.d.ts +1 -1
  117. package/build/modern/queryObserver.cjs +12 -9
  118. package/build/modern/queryObserver.cjs.map +1 -1
  119. package/build/modern/queryObserver.d.cts +1 -1
  120. package/build/modern/queryObserver.d.ts +1 -1
  121. package/build/modern/queryObserver.js +12 -9
  122. package/build/modern/queryObserver.js.map +1 -1
  123. package/build/modern/removable.cjs +3 -2
  124. package/build/modern/removable.cjs.map +1 -1
  125. package/build/modern/removable.js +3 -2
  126. package/build/modern/removable.js.map +1 -1
  127. package/build/modern/retryer.cjs +12 -14
  128. package/build/modern/retryer.cjs.map +1 -1
  129. package/build/modern/retryer.d.cts +1 -1
  130. package/build/modern/retryer.d.ts +1 -1
  131. package/build/modern/retryer.js +12 -14
  132. package/build/modern/retryer.js.map +1 -1
  133. package/build/modern/streamedQuery.cjs +7 -8
  134. package/build/modern/streamedQuery.cjs.map +1 -1
  135. package/build/modern/streamedQuery.d.cts +18 -10
  136. package/build/modern/streamedQuery.d.ts +18 -10
  137. package/build/modern/streamedQuery.js +7 -8
  138. package/build/modern/streamedQuery.js.map +1 -1
  139. package/build/modern/timeoutManager.cjs +98 -0
  140. package/build/modern/timeoutManager.cjs.map +1 -0
  141. package/build/modern/timeoutManager.d.cts +58 -0
  142. package/build/modern/timeoutManager.d.ts +58 -0
  143. package/build/modern/timeoutManager.js +70 -0
  144. package/build/modern/timeoutManager.js.map +1 -0
  145. package/build/modern/types.d.cts +1 -1
  146. package/build/modern/types.d.ts +1 -1
  147. package/build/modern/utils.cjs +26 -22
  148. package/build/modern/utils.cjs.map +1 -1
  149. package/build/modern/utils.d.cts +1 -1
  150. package/build/modern/utils.d.ts +1 -1
  151. package/build/modern/utils.js +26 -22
  152. package/build/modern/utils.js.map +1 -1
  153. package/package.json +1 -1
  154. package/src/index.ts +32 -27
  155. package/src/notifyManager.ts +3 -1
  156. package/src/queriesObserver.ts +24 -15
  157. package/src/query.ts +96 -69
  158. package/src/queryObserver.ts +19 -11
  159. package/src/removable.ts +5 -3
  160. package/src/retryer.ts +20 -17
  161. package/src/streamedQuery.ts +47 -23
  162. package/src/timeoutManager.ts +135 -0
  163. package/src/utils.ts +39 -31
@@ -1,7 +1,7 @@
1
1
  import { notifyManager } from './notifyManager'
2
2
  import { QueryObserver } from './queryObserver'
3
3
  import { Subscribable } from './subscribable'
4
- import { replaceEqualDeep } from './utils'
4
+ import { replaceEqualDeep, shallowEqualObjects } from './utils'
5
5
  import type {
6
6
  DefaultedQueryObserverOptions,
7
7
  QueryObserverOptions,
@@ -118,30 +118,39 @@ export class QueriesObserver<
118
118
  observer.getCurrentResult(),
119
119
  )
120
120
 
121
+ const hasLengthChange = prevObservers.length !== newObservers.length
121
122
  const hasIndexChange = newObservers.some(
122
123
  (observer, index) => observer !== prevObservers[index],
123
124
  )
125
+ const hasStructuralChange = hasLengthChange || hasIndexChange
124
126
 
125
- if (prevObservers.length === newObservers.length && !hasIndexChange) {
126
- return
127
- }
127
+ const hasResultChange = hasStructuralChange
128
+ ? true
129
+ : newResult.some((result, index) => {
130
+ const prev = this.#result[index]
131
+ return !prev || !shallowEqualObjects(result, prev)
132
+ })
128
133
 
129
- this.#observers = newObservers
130
- this.#result = newResult
134
+ if (!hasStructuralChange && !hasResultChange) return
131
135
 
132
- if (!this.hasListeners()) {
133
- return
136
+ if (hasStructuralChange) {
137
+ this.#observers = newObservers
134
138
  }
135
139
 
136
- difference(prevObservers, newObservers).forEach((observer) => {
137
- observer.destroy()
138
- })
140
+ this.#result = newResult
139
141
 
140
- difference(newObservers, prevObservers).forEach((observer) => {
141
- observer.subscribe((result) => {
142
- this.#onUpdate(observer, result)
142
+ if (!this.hasListeners()) return
143
+
144
+ if (hasStructuralChange) {
145
+ difference(prevObservers, newObservers).forEach((observer) => {
146
+ observer.destroy()
143
147
  })
144
- })
148
+ difference(newObservers, prevObservers).forEach((observer) => {
149
+ observer.subscribe((result) => {
150
+ this.#onUpdate(observer, result)
151
+ })
152
+ })
153
+ }
145
154
 
146
155
  this.#notify()
147
156
  })
package/src/query.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  timeUntilStale,
9
9
  } from './utils'
10
10
  import { notifyManager } from './notifyManager'
11
- import { canFetch, createRetryer, isCancelledError } from './retryer'
11
+ import { CancelledError, canFetch, createRetryer } from './retryer'
12
12
  import { Removable } from './removable'
13
13
  import type { QueryCache } from './queryCache'
14
14
  import type { QueryClient } from './queryClient'
@@ -205,6 +205,18 @@ export class Query<
205
205
  this.options = { ...this.#defaultOptions, ...options }
206
206
 
207
207
  this.updateGcTime(this.options.gcTime)
208
+
209
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
210
+ if (this.state && this.state.data === undefined) {
211
+ const defaultState = getDefaultState(this.options)
212
+ if (defaultState.data !== undefined) {
213
+ this.setData(defaultState.data, {
214
+ updatedAt: defaultState.dataUpdatedAt,
215
+ manual: true,
216
+ })
217
+ this.#initialState = defaultState
218
+ }
219
+ }
208
220
  }
209
221
 
210
222
  protected optionalRemove() {
@@ -372,11 +384,17 @@ export class Query<
372
384
  }
373
385
  }
374
386
 
375
- fetch(
387
+ async fetch(
376
388
  options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
377
389
  fetchOptions?: FetchOptions<TQueryFnData>,
378
390
  ): Promise<TData> {
379
- if (this.state.fetchStatus !== 'idle') {
391
+ if (
392
+ this.state.fetchStatus !== 'idle' &&
393
+ // If the promise in the retyer is already rejected, we have to definitely
394
+ // re-start the fetch; there is a chance that the query is still in a
395
+ // pending state when that happens
396
+ this.#retryer?.status() !== 'rejected'
397
+ ) {
380
398
  if (this.state.data !== undefined && fetchOptions?.cancelRefetch) {
381
399
  // Silently cancel current fetch if the user wants to cancel refetch
382
400
  this.cancel({ silent: true })
@@ -495,69 +513,21 @@ export class Query<
495
513
  this.#dispatch({ type: 'fetch', meta: context.fetchOptions?.meta })
496
514
  }
497
515
 
498
- const onError = (error: TError | { silent?: boolean }) => {
499
- // Optimistically update state if needed
500
- if (!(isCancelledError(error) && error.silent)) {
501
- this.#dispatch({
502
- type: 'error',
503
- error: error as TError,
504
- })
505
- }
506
-
507
- if (!isCancelledError(error)) {
508
- // Notify cache callback
509
- this.#cache.config.onError?.(
510
- error as any,
511
- this as Query<any, any, any, any>,
512
- )
513
- this.#cache.config.onSettled?.(
514
- this.state.data,
515
- error as any,
516
- this as Query<any, any, any, any>,
517
- )
518
- }
519
-
520
- // Schedule query gc after fetching
521
- this.scheduleGc()
522
- }
523
-
524
516
  // Try to fetch the data
525
517
  this.#retryer = createRetryer({
526
518
  initialPromise: fetchOptions?.initialPromise as
527
519
  | Promise<TData>
528
520
  | undefined,
529
521
  fn: context.fetchFn as () => Promise<TData>,
530
- abort: abortController.abort.bind(abortController),
531
- onSuccess: (data) => {
532
- if (data === undefined) {
533
- if (process.env.NODE_ENV !== 'production') {
534
- console.error(
535
- `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${this.queryHash}`,
536
- )
537
- }
538
- onError(new Error(`${this.queryHash} data is undefined`) as any)
539
- return
522
+ onCancel: (error) => {
523
+ if (error instanceof CancelledError && error.revert) {
524
+ this.setState({
525
+ ...this.#revertState,
526
+ fetchStatus: 'idle' as const,
527
+ })
540
528
  }
541
-
542
- try {
543
- this.setData(data)
544
- } catch (error) {
545
- onError(error as TError)
546
- return
547
- }
548
-
549
- // Notify cache callback
550
- this.#cache.config.onSuccess?.(data, this as Query<any, any, any, any>)
551
- this.#cache.config.onSettled?.(
552
- data,
553
- this.state.error as any,
554
- this as Query<any, any, any, any>,
555
- )
556
-
557
- // Schedule query gc after fetching
558
- this.scheduleGc()
529
+ abortController.abort()
559
530
  },
560
- onError,
561
531
  onFail: (failureCount, error) => {
562
532
  this.#dispatch({ type: 'failed', failureCount, error })
563
533
  },
@@ -573,7 +543,66 @@ export class Query<
573
543
  canRun: () => true,
574
544
  })
575
545
 
576
- return this.#retryer.start()
546
+ try {
547
+ const data = await this.#retryer.start()
548
+ // this is more of a runtime guard
549
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
550
+ if (data === undefined) {
551
+ if (process.env.NODE_ENV !== 'production') {
552
+ console.error(
553
+ `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${this.queryHash}`,
554
+ )
555
+ }
556
+ throw new Error(`${this.queryHash} data is undefined`)
557
+ }
558
+
559
+ this.setData(data)
560
+
561
+ // Notify cache callback
562
+ this.#cache.config.onSuccess?.(data, this as Query<any, any, any, any>)
563
+ this.#cache.config.onSettled?.(
564
+ data,
565
+ this.state.error as any,
566
+ this as Query<any, any, any, any>,
567
+ )
568
+ return data
569
+ } catch (error) {
570
+ if (error instanceof CancelledError) {
571
+ if (error.silent) {
572
+ // silent cancellation implies a new fetch is going to be started,
573
+ // so we piggyback onto that promise
574
+ return this.#retryer.promise
575
+ } else if (error.revert) {
576
+ // transform error into reverted state data
577
+ // if the initial fetch was cancelled, we have no data, so we have
578
+ // to get reject with a CancelledError
579
+ if (this.state.data === undefined) {
580
+ throw error
581
+ }
582
+ return this.state.data
583
+ }
584
+ }
585
+ this.#dispatch({
586
+ type: 'error',
587
+ error: error as TError,
588
+ })
589
+
590
+ // Notify cache callback
591
+ this.#cache.config.onError?.(
592
+ error as any,
593
+ this as Query<any, any, any, any>,
594
+ )
595
+ this.#cache.config.onSettled?.(
596
+ this.state.data,
597
+ error as any,
598
+ this as Query<any, any, any, any>,
599
+ )
600
+
601
+ throw error // rethrow the error for further handling
602
+ } finally {
603
+ // Schedule query gc after fetching
604
+ this.scheduleGc()
605
+ }
577
606
  }
578
607
 
579
608
  #dispatch(action: Action<TData, TError>): void {
@@ -604,29 +633,27 @@ export class Query<
604
633
  fetchMeta: action.meta ?? null,
605
634
  }
606
635
  case 'success':
607
- // If fetching ends successfully, we don't need revertState as a fallback anymore.
608
- this.#revertState = undefined
609
- return {
636
+ const newState = {
610
637
  ...state,
611
638
  data: action.data,
612
639
  dataUpdateCount: state.dataUpdateCount + 1,
613
640
  dataUpdatedAt: action.dataUpdatedAt ?? Date.now(),
614
641
  error: null,
615
642
  isInvalidated: false,
616
- status: 'success',
643
+ status: 'success' as const,
617
644
  ...(!action.manual && {
618
- fetchStatus: 'idle',
645
+ fetchStatus: 'idle' as const,
619
646
  fetchFailureCount: 0,
620
647
  fetchFailureReason: null,
621
648
  }),
622
649
  }
650
+ // If fetching ends successfully, we don't need revertState as a fallback anymore.
651
+ // For manual updates, capture the state to revert to it in case of a cancellation.
652
+ this.#revertState = action.manual ? newState : undefined
653
+
654
+ return newState
623
655
  case 'error':
624
656
  const error = action.error
625
-
626
- if (isCancelledError(error) && error.revert && this.#revertState) {
627
- return { ...this.#revertState, fetchStatus: 'idle' }
628
- }
629
-
630
657
  return {
631
658
  ...state,
632
659
  error,
@@ -13,6 +13,8 @@ import {
13
13
  shallowEqualObjects,
14
14
  timeUntilStale,
15
15
  } from './utils'
16
+ import { timeoutManager } from './timeoutManager'
17
+ import type { ManagedTimerId } from './timeoutManager'
16
18
  import type { FetchOptions, Query, QueryState } from './query'
17
19
  import type { QueryClient } from './queryClient'
18
20
  import type { PendingThenable, Thenable } from './thenable'
@@ -62,8 +64,8 @@ export class QueryObserver<
62
64
  // This property keeps track of the last query with defined data.
63
65
  // It will be used to pass the previous data and query to the placeholder function between renders.
64
66
  #lastQueryWithDefinedData?: Query<TQueryFnData, TError, TQueryData, TQueryKey>
65
- #staleTimeoutId?: ReturnType<typeof setTimeout>
66
- #refetchIntervalId?: ReturnType<typeof setInterval>
67
+ #staleTimeoutId?: ManagedTimerId
68
+ #refetchIntervalId?: ManagedTimerId
67
69
  #currentRefetchInterval?: number | false
68
70
  #trackedProps = new Set<keyof QueryObserverResult>()
69
71
 
@@ -82,11 +84,6 @@ export class QueryObserver<
82
84
  this.#client = client
83
85
  this.#selectError = null
84
86
  this.#currentThenable = pendingThenable()
85
- if (!this.options.experimental_prefetchInRender) {
86
- this.#currentThenable.reject(
87
- new Error('experimental_prefetchInRender feature flag is not enabled'),
88
- )
89
- }
90
87
 
91
88
  this.bindMethods()
92
89
  this.setOptions(options)
@@ -272,6 +269,17 @@ export class QueryObserver<
272
269
  get: (target, key) => {
273
270
  this.trackProp(key as keyof QueryObserverResult)
274
271
  onPropTracked?.(key as keyof QueryObserverResult)
272
+ if (
273
+ key === 'promise' &&
274
+ !this.options.experimental_prefetchInRender &&
275
+ this.#currentThenable.status === 'pending'
276
+ ) {
277
+ this.#currentThenable.reject(
278
+ new Error(
279
+ 'experimental_prefetchInRender feature flag is not enabled',
280
+ ),
281
+ )
282
+ }
275
283
  return Reflect.get(target, key)
276
284
  },
277
285
  })
@@ -359,7 +367,7 @@ export class QueryObserver<
359
367
  // To mitigate this issue we always add 1 ms to the timeout.
360
368
  const timeout = time + 1
361
369
 
362
- this.#staleTimeoutId = setTimeout(() => {
370
+ this.#staleTimeoutId = timeoutManager.setTimeout(() => {
363
371
  if (!this.#currentResult.isStale) {
364
372
  this.updateResult()
365
373
  }
@@ -388,7 +396,7 @@ export class QueryObserver<
388
396
  return
389
397
  }
390
398
 
391
- this.#refetchIntervalId = setInterval(() => {
399
+ this.#refetchIntervalId = timeoutManager.setInterval(() => {
392
400
  if (
393
401
  this.options.refetchIntervalInBackground ||
394
402
  focusManager.isFocused()
@@ -405,14 +413,14 @@ export class QueryObserver<
405
413
 
406
414
  #clearStaleTimeout(): void {
407
415
  if (this.#staleTimeoutId) {
408
- clearTimeout(this.#staleTimeoutId)
416
+ timeoutManager.clearTimeout(this.#staleTimeoutId)
409
417
  this.#staleTimeoutId = undefined
410
418
  }
411
419
  }
412
420
 
413
421
  #clearRefetchInterval(): void {
414
422
  if (this.#refetchIntervalId) {
415
- clearInterval(this.#refetchIntervalId)
423
+ timeoutManager.clearInterval(this.#refetchIntervalId)
416
424
  this.#refetchIntervalId = undefined
417
425
  }
418
426
  }
package/src/removable.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import { timeoutManager } from './timeoutManager'
1
2
  import { isServer, isValidTimeout } from './utils'
3
+ import type { ManagedTimerId } from './timeoutManager'
2
4
 
3
5
  export abstract class Removable {
4
6
  gcTime!: number
5
- #gcTimeout?: ReturnType<typeof setTimeout>
7
+ #gcTimeout?: ManagedTimerId
6
8
 
7
9
  destroy(): void {
8
10
  this.clearGcTimeout()
@@ -12,7 +14,7 @@ export abstract class Removable {
12
14
  this.clearGcTimeout()
13
15
 
14
16
  if (isValidTimeout(this.gcTime)) {
15
- this.#gcTimeout = setTimeout(() => {
17
+ this.#gcTimeout = timeoutManager.setTimeout(() => {
16
18
  this.optionalRemove()
17
19
  }, this.gcTime)
18
20
  }
@@ -28,7 +30,7 @@ export abstract class Removable {
28
30
 
29
31
  protected clearGcTimeout() {
30
32
  if (this.#gcTimeout) {
31
- clearTimeout(this.#gcTimeout)
33
+ timeoutManager.clearTimeout(this.#gcTimeout)
32
34
  this.#gcTimeout = undefined
33
35
  }
34
36
  }
package/src/retryer.ts CHANGED
@@ -2,6 +2,7 @@ import { focusManager } from './focusManager'
2
2
  import { onlineManager } from './onlineManager'
3
3
  import { pendingThenable } from './thenable'
4
4
  import { isServer, sleep } from './utils'
5
+ import type { Thenable } from './thenable'
5
6
  import type { CancelOptions, DefaultError, NetworkMode } from './types'
6
7
 
7
8
  // TYPES
@@ -9,9 +10,7 @@ import type { CancelOptions, DefaultError, NetworkMode } from './types'
9
10
  interface RetryerConfig<TData = unknown, TError = DefaultError> {
10
11
  fn: () => TData | Promise<TData>
11
12
  initialPromise?: Promise<TData>
12
- abort?: () => void
13
- onError?: (error: TError) => void
14
- onSuccess?: (data: TData) => void
13
+ onCancel?: (error: TError) => void
15
14
  onFail?: (failureCount: number, error: TError) => void
16
15
  onPause?: () => void
17
16
  onContinue?: () => void
@@ -29,6 +28,7 @@ export interface Retryer<TData = unknown> {
29
28
  continueRetry: () => void
30
29
  canStart: () => boolean
31
30
  start: () => Promise<TData>
31
+ status: () => 'pending' | 'resolved' | 'rejected'
32
32
  }
33
33
 
34
34
  export type RetryValue<TError> = boolean | number | ShouldRetryFunction<TError>
@@ -65,6 +65,9 @@ export class CancelledError extends Error {
65
65
  }
66
66
  }
67
67
 
68
+ /**
69
+ * @deprecated Use instanceof `CancelledError` instead.
70
+ */
68
71
  export function isCancelledError(value: any): value is CancelledError {
69
72
  return value instanceof CancelledError
70
73
  }
@@ -74,16 +77,19 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
74
77
  ): Retryer<TData> {
75
78
  let isRetryCancelled = false
76
79
  let failureCount = 0
77
- let isResolved = false
78
80
  let continueFn: ((value?: unknown) => void) | undefined
79
81
 
80
82
  const thenable = pendingThenable<TData>()
81
83
 
84
+ const isResolved = () =>
85
+ (thenable.status as Thenable<TData>['status']) !== 'pending'
86
+
82
87
  const cancel = (cancelOptions?: CancelOptions): void => {
83
- if (!isResolved) {
84
- reject(new CancelledError(cancelOptions))
88
+ if (!isResolved()) {
89
+ const error = new CancelledError(cancelOptions) as TError
90
+ reject(error)
85
91
 
86
- config.abort?.()
92
+ config.onCancel?.(error)
87
93
  }
88
94
  }
89
95
  const cancelRetry = () => {
@@ -102,18 +108,14 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
102
108
  const canStart = () => canFetch(config.networkMode) && config.canRun()
103
109
 
104
110
  const resolve = (value: any) => {
105
- if (!isResolved) {
106
- isResolved = true
107
- config.onSuccess?.(value)
111
+ if (!isResolved()) {
108
112
  continueFn?.()
109
113
  thenable.resolve(value)
110
114
  }
111
115
  }
112
116
 
113
117
  const reject = (value: any) => {
114
- if (!isResolved) {
115
- isResolved = true
116
- config.onError?.(value)
118
+ if (!isResolved()) {
117
119
  continueFn?.()
118
120
  thenable.reject(value)
119
121
  }
@@ -122,14 +124,14 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
122
124
  const pause = () => {
123
125
  return new Promise((continueResolve) => {
124
126
  continueFn = (value) => {
125
- if (isResolved || canContinue()) {
127
+ if (isResolved() || canContinue()) {
126
128
  continueResolve(value)
127
129
  }
128
130
  }
129
131
  config.onPause?.()
130
132
  }).then(() => {
131
133
  continueFn = undefined
132
- if (!isResolved) {
134
+ if (!isResolved()) {
133
135
  config.onContinue?.()
134
136
  }
135
137
  })
@@ -138,7 +140,7 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
138
140
  // Create loop function
139
141
  const run = () => {
140
142
  // Do nothing if already resolved
141
- if (isResolved) {
143
+ if (isResolved()) {
142
144
  return
143
145
  }
144
146
 
@@ -159,7 +161,7 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
159
161
  .then(resolve)
160
162
  .catch((error) => {
161
163
  // Stop if the fetch is already resolved
162
- if (isResolved) {
164
+ if (isResolved()) {
163
165
  return
164
166
  }
165
167
 
@@ -204,6 +206,7 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
204
206
 
205
207
  return {
206
208
  promise: thenable,
209
+ status: () => thenable.status,
207
210
  cancel,
208
211
  continue: () => {
209
212
  continueFn?.()
@@ -1,6 +1,34 @@
1
1
  import { addToEnd } from './utils'
2
2
  import type { QueryFunction, QueryFunctionContext, QueryKey } from './types'
3
3
 
4
+ type BaseStreamedQueryParams<TQueryFnData, TQueryKey extends QueryKey> = {
5
+ streamFn: (
6
+ context: QueryFunctionContext<TQueryKey>,
7
+ ) => AsyncIterable<TQueryFnData> | Promise<AsyncIterable<TQueryFnData>>
8
+ refetchMode?: 'append' | 'reset' | 'replace'
9
+ }
10
+
11
+ type SimpleStreamedQueryParams<
12
+ TQueryFnData,
13
+ TQueryKey extends QueryKey,
14
+ > = BaseStreamedQueryParams<TQueryFnData, TQueryKey> & {
15
+ reducer?: never
16
+ initialValue?: never
17
+ }
18
+
19
+ type ReducibleStreamedQueryParams<
20
+ TQueryFnData,
21
+ TData,
22
+ TQueryKey extends QueryKey,
23
+ > = BaseStreamedQueryParams<TQueryFnData, TQueryKey> & {
24
+ reducer: (acc: TData, chunk: TQueryFnData) => TData
25
+ initialValue: TData
26
+ }
27
+
28
+ type StreamedQueryParams<TQueryFnData, TData, TQueryKey extends QueryKey> =
29
+ | SimpleStreamedQueryParams<TQueryFnData, TQueryKey>
30
+ | ReducibleStreamedQueryParams<TQueryFnData, TData, TQueryKey>
31
+
4
32
  /**
5
33
  * This is a helper function to create a query function that streams data from an AsyncIterable.
6
34
  * Data will be an Array of all the chunks received.
@@ -11,31 +39,29 @@ import type { QueryFunction, QueryFunctionContext, QueryKey } from './types'
11
39
  * Defaults to `'reset'`, erases all data and puts the query back into `pending` state.
12
40
  * Set to `'append'` to append new data to the existing data.
13
41
  * Set to `'replace'` to write all data to the cache once the stream ends.
14
- * @param maxChunks - The maximum number of chunks to keep in the cache.
15
- * Defaults to `undefined`, meaning all chunks will be kept.
16
- * If `undefined` or `0`, the number of chunks is unlimited.
17
- * If the number of chunks exceeds this number, the oldest chunk will be removed.
42
+ * @param reducer - A function to reduce the streamed chunks into the final data.
43
+ * Defaults to a function that appends chunks to the end of the array.
44
+ * @param initialValue - Initial value to be used while the first chunk is being fetched.
18
45
  */
19
46
  export function streamedQuery<
20
47
  TQueryFnData = unknown,
48
+ TData = Array<TQueryFnData>,
21
49
  TQueryKey extends QueryKey = QueryKey,
22
50
  >({
23
- queryFn,
51
+ streamFn,
24
52
  refetchMode = 'reset',
25
- maxChunks,
26
- }: {
27
- queryFn: (
28
- context: QueryFunctionContext<TQueryKey>,
29
- ) => AsyncIterable<TQueryFnData> | Promise<AsyncIterable<TQueryFnData>>
30
- refetchMode?: 'append' | 'reset' | 'replace'
31
- maxChunks?: number
32
- }): QueryFunction<Array<TQueryFnData>, TQueryKey> {
53
+ reducer = (items, chunk) =>
54
+ addToEnd(items as Array<TQueryFnData>, chunk) as TData,
55
+ initialValue = [] as TData,
56
+ }: StreamedQueryParams<TQueryFnData, TData, TQueryKey>): QueryFunction<
57
+ TData,
58
+ TQueryKey
59
+ > {
33
60
  return async (context) => {
34
61
  const query = context.client
35
62
  .getQueryCache()
36
63
  .find({ queryKey: context.queryKey, exact: true })
37
64
  const isRefetch = !!query && query.state.data !== undefined
38
-
39
65
  if (isRefetch && refetchMode === 'reset') {
40
66
  query.setState({
41
67
  status: 'pending',
@@ -45,8 +71,9 @@ export function streamedQuery<
45
71
  })
46
72
  }
47
73
 
48
- let result: Array<TQueryFnData> = []
49
- const stream = await queryFn(context)
74
+ let result = initialValue
75
+
76
+ const stream = await streamFn(context)
50
77
 
51
78
  for await (const chunk of stream) {
52
79
  if (context.signal.aborted) {
@@ -55,19 +82,16 @@ export function streamedQuery<
55
82
 
56
83
  // don't append to the cache directly when replace-refetching
57
84
  if (!isRefetch || refetchMode !== 'replace') {
58
- context.client.setQueryData<Array<TQueryFnData>>(
59
- context.queryKey,
60
- (prev = []) => {
61
- return addToEnd(prev, chunk, maxChunks)
62
- },
85
+ context.client.setQueryData<TData>(context.queryKey, (prev) =>
86
+ reducer(prev === undefined ? initialValue : prev, chunk),
63
87
  )
64
88
  }
65
- result = addToEnd(result, chunk, maxChunks)
89
+ result = reducer(result, chunk)
66
90
  }
67
91
 
68
92
  // finalize result: replace-refetching needs to write to the cache
69
93
  if (isRefetch && refetchMode === 'replace' && !context.signal.aborted) {
70
- context.client.setQueryData<Array<TQueryFnData>>(context.queryKey, result)
94
+ context.client.setQueryData<TData>(context.queryKey, result)
71
95
  }
72
96
 
73
97
  return context.client.getQueryData(context.queryKey)!