@tanstack/query-core
Version:
The framework agnostic core that powers TanStack Query
220 lines (188 loc) • 5.43 kB
text/typescript
import { focusManager } from './focusManager'
import { onlineManager } from './onlineManager'
import { isServer, sleep } from './utils'
import type { CancelOptions, DefaultError, NetworkMode } from './types'
// TYPES
interface RetryerConfig<TData = unknown, TError = DefaultError> {
fn: () => TData | Promise<TData>
abort?: () => void
onError?: (error: TError) => void
onSuccess?: (data: TData) => void
onFail?: (failureCount: number, error: TError) => void
onPause?: () => void
onContinue?: () => void
retry?: RetryValue<TError>
retryDelay?: RetryDelayValue<TError>
networkMode: NetworkMode | undefined
}
export interface Retryer<TData = unknown> {
promise: Promise<TData>
cancel: (cancelOptions?: CancelOptions) => void
continue: () => Promise<unknown>
cancelRetry: () => void
continueRetry: () => void
}
export type RetryValue<TError> = boolean | number | ShouldRetryFunction<TError>
type ShouldRetryFunction<TError = DefaultError> = (
failureCount: number,
error: TError,
) => boolean
export type RetryDelayValue<TError> = number | RetryDelayFunction<TError>
type RetryDelayFunction<TError = DefaultError> = (
failureCount: number,
error: TError,
) => number
function defaultRetryDelay(failureCount: number) {
return Math.min(1000 * 2 ** failureCount, 30000)
}
export function canFetch(networkMode: NetworkMode | undefined): boolean {
return (networkMode ?? 'online') === 'online'
? onlineManager.isOnline()
: true
}
export class CancelledError {
revert?: boolean
silent?: boolean
constructor(options?: CancelOptions) {
this.revert = options?.revert
this.silent = options?.silent
}
}
export function isCancelledError(value: any): value is CancelledError {
return value instanceof CancelledError
}
export function createRetryer<TData = unknown, TError = DefaultError>(
config: RetryerConfig<TData, TError>,
): Retryer<TData> {
let isRetryCancelled = false
let failureCount = 0
let isResolved = false
let continueFn: ((value?: unknown) => boolean) | undefined
let promiseResolve: (data: TData) => void
let promiseReject: (error: TError) => void
const promise = new Promise<TData>((outerResolve, outerReject) => {
promiseResolve = outerResolve
promiseReject = outerReject
})
const cancel = (cancelOptions?: CancelOptions): void => {
if (!isResolved) {
reject(new CancelledError(cancelOptions))
config.abort?.()
}
}
const cancelRetry = () => {
isRetryCancelled = true
}
const continueRetry = () => {
isRetryCancelled = false
}
const shouldPause = () =>
!focusManager.isFocused() ||
(config.networkMode !== 'always' && !onlineManager.isOnline())
const resolve = (value: any) => {
if (!isResolved) {
isResolved = true
config.onSuccess?.(value)
continueFn?.()
promiseResolve(value)
}
}
const reject = (value: any) => {
if (!isResolved) {
isResolved = true
config.onError?.(value)
continueFn?.()
promiseReject(value)
}
}
const pause = () => {
return new Promise((continueResolve) => {
continueFn = (value) => {
const canContinue = isResolved || !shouldPause()
if (canContinue) {
continueResolve(value)
}
return canContinue
}
config.onPause?.()
}).then(() => {
continueFn = undefined
if (!isResolved) {
config.onContinue?.()
}
})
}
// Create loop function
const run = () => {
// Do nothing if already resolved
if (isResolved) {
return
}
let promiseOrValue: any
// Execute query
try {
promiseOrValue = config.fn()
} catch (error) {
promiseOrValue = Promise.reject(error)
}
Promise.resolve(promiseOrValue)
.then(resolve)
.catch((error) => {
// Stop if the fetch is already resolved
if (isResolved) {
return
}
// Do we need to retry the request?
const retry = config.retry ?? (isServer ? 0 : 3)
const retryDelay = config.retryDelay ?? defaultRetryDelay
const delay =
typeof retryDelay === 'function'
? retryDelay(failureCount, error)
: retryDelay
const shouldRetry =
retry === true ||
(typeof retry === 'number' && failureCount < retry) ||
(typeof retry === 'function' && retry(failureCount, error))
if (isRetryCancelled || !shouldRetry) {
// We are done if the query does not need to be retried
reject(error)
return
}
failureCount++
// Notify on fail
config.onFail?.(failureCount, error)
// Delay
sleep(delay)
// Pause if the document is not visible or when the device is offline
.then(() => {
if (shouldPause()) {
return pause()
}
return
})
.then(() => {
if (isRetryCancelled) {
reject(error)
} else {
run()
}
})
})
}
// Start loop
if (canFetch(config.networkMode)) {
run()
} else {
pause().then(run)
}
return {
promise,
cancel,
continue: () => {
const didContinue = continueFn?.()
return didContinue ? promise : Promise.resolve()
},
cancelRetry,
continueRetry,
}
}