@tanstack/query-core
Version:
The framework agnostic core that powers TanStack Query
1 lines • 11.6 kB
Source Map (JSON)
{"version":3,"file":"retryer.cjs","names":["onlineManager","noop","focusManager","isServerEnvironment"],"sources":["../../src/retryer.ts"],"sourcesContent":["import { focusManager } from './focusManager'\nimport { onlineManager } from './onlineManager'\nimport { isServer as isServerEnvironment } from './environmentManager'\nimport { noop, sleep } from './utils'\nimport type { CancelOptions, DefaultError, NetworkMode } from './types'\n\n// TYPES\n\ninterface RetryerConfig<TData = unknown, TError = DefaultError> {\n fn: () => TData | Promise<TData>\n initialPromise?: Promise<TData>\n onCancel?: (error: TError) => void\n onFail?: (failureCount: number, error: TError) => void\n onPause?: () => void\n onContinue?: () => void\n retry?: RetryValue<TError>\n retryDelay?: RetryDelayValue<TError>\n networkMode: NetworkMode | undefined\n canRun: () => boolean\n}\n\n/**\n * Runs a function with retries, and can pause, continue, or cancel it. Queries and mutations use\n * one for each fetch or execution.\n */\nexport interface Retryer<TData = unknown> {\n /**\n * The promise that resolves with the data, or rejects once retries are exhausted or it is\n * cancelled.\n */\n promise: Promise<TData>\n /**\n * Cancels the run, rejecting `promise` with a `CancelledError`.\n */\n cancel: (cancelOptions?: CancelOptions) => void\n /**\n * Resumes a paused run, and returns `promise`.\n */\n continue: () => Promise<unknown>\n /**\n * Stops further retries. The current attempt still finishes.\n */\n cancelRetry: () => void\n /**\n * Allows retries again after `cancelRetry`.\n */\n continueRetry: () => void\n /**\n * Returns whether the run can start now, based on the network mode and `canRun`.\n */\n canStart: () => boolean\n /**\n * Starts the run, or waits until it can start, and returns `promise`.\n */\n start: () => Promise<TData>\n /**\n * Returns whether `promise` is still pending, resolved, or rejected.\n */\n status: () => 'pending' | 'resolved' | 'rejected'\n}\n\ntype RetryerStatus = 'pending' | 'resolved' | 'rejected'\n\n/** @inline */\nexport type RetryValue<TError> = boolean | number | ShouldRetryFunction<TError>\n\n/** @inline */\ntype ShouldRetryFunction<TError = DefaultError> = (\n failureCount: number,\n error: TError,\n) => boolean\n\n/** @inline */\nexport type RetryDelayValue<TError> = number | RetryDelayFunction<TError>\n\n/** @inline */\ntype RetryDelayFunction<TError = DefaultError> = (\n failureCount: number,\n error: TError,\n) => number\n\n/**\n * The default `retryDelay`: doubles with every failure, starting at 1 second, up to 30 seconds.\n * @param failureCount - The number of failures so far.\n * @returns The delay before the next retry, in milliseconds.\n */\nfunction defaultRetryDelay(failureCount: number) {\n return Math.min(1000 * 2 ** failureCount, 30000)\n}\n\n/**\n * Checks whether a fetch can run for the given network mode.\n * @param networkMode - The `networkMode` option. Defaults to `'online'`.\n * @returns `true` if the network mode is not `'online'`, or if it is and the client is online.\n */\nexport function canFetch(networkMode: NetworkMode | undefined): boolean {\n return (networkMode ?? 'online') === 'online'\n ? onlineManager.isOnline()\n : true\n}\n\n/**\n * The error thrown by a `Retryer` (and surfaced to `query.promise`/`mutation`) when a fetch is cancelled, e.g. via\n * `query.cancel()`. `revert`, if `true`, tells the caller to restore the state the query was in before the fetch\n * started instead of surfacing the error. `silent`, if `true`, tells the caller to suppress this error and instead\n * resolve with the promise of the fetch that triggered the cancellation.\n * @example\n * ```ts\n * query.cancel()\n *\n * try {\n * await query.promise\n * } catch (error) {\n * if (error instanceof CancelledError) {\n * // the fetch was cancelled, e.g. via `query.cancel()`\n * }\n * }\n * ```\n */\nexport class CancelledError extends Error {\n revert?: boolean\n silent?: boolean\n constructor(options?: CancelOptions) {\n super('CancelledError')\n this.revert = options?.revert\n this.silent = options?.silent\n }\n}\n\n/**\n * Checks whether a value is a `CancelledError`.\n * @param value - The value to check.\n * @returns `true` if `value` is a `CancelledError`.\n * @deprecated Use instanceof `CancelledError` instead.\n */\nexport function isCancelledError(value: any): value is CancelledError {\n return value instanceof CancelledError\n}\n\n/**\n * Creates a retryer, which runs a function and retries it on failure according to the `retry` and\n * `retryDelay` options. It pauses while the network mode doesn't allow fetching or `canRun` returns\n * `false`, and can be cancelled. Queries and mutations use it to run their `queryFn` and\n * `mutationFn`.\n * @param config - The function to run, the retry options, and the callbacks for failures, pausing,\n * continuing, and cancellation.\n * @returns The retryer, whose `promise` settles with the result of the function.\n */\nexport function createRetryer<TData = unknown, TError = DefaultError>(\n config: RetryerConfig<TData, TError>,\n): Retryer<TData> {\n let isRetryCancelled = false\n let failureCount = 0\n let continueFn: ((value?: unknown) => void) | undefined\n let status: RetryerStatus = 'pending'\n let promiseResolve: (data: TData) => void\n let promiseReject: (error: TError) => void\n\n const promise = new Promise<TData>((resolve, reject) => {\n promiseResolve = resolve\n promiseReject = reject\n })\n promise.catch(noop)\n\n const isResolved = () => status !== 'pending'\n\n const cancel = (cancelOptions?: CancelOptions): void => {\n if (!isResolved()) {\n const error = new CancelledError(cancelOptions) as TError\n reject(error)\n\n config.onCancel?.(error)\n }\n }\n const cancelRetry = () => {\n isRetryCancelled = true\n }\n\n const continueRetry = () => {\n isRetryCancelled = false\n }\n\n const canContinue = () =>\n focusManager.isFocused() &&\n (config.networkMode === 'always' || onlineManager.isOnline()) &&\n config.canRun()\n\n const canStart = () => canFetch(config.networkMode) && config.canRun()\n\n const resolve = (value: any) => {\n if (!isResolved()) {\n continueFn?.()\n status = 'resolved'\n promiseResolve(value)\n }\n }\n\n const reject = (value: any) => {\n if (!isResolved()) {\n continueFn?.()\n status = 'rejected'\n promiseReject(value)\n }\n }\n\n const pause = (canResume: () => boolean) => {\n return new Promise((continueResolve) => {\n continueFn = (value) => {\n if (isResolved() || canResume()) {\n continueResolve(value)\n }\n }\n config.onPause?.()\n }).then(() => {\n continueFn = undefined\n if (!isResolved()) {\n config.onContinue?.()\n }\n })\n }\n\n // Create loop function\n const run = () => {\n // Do nothing if already resolved\n if (isResolved()) {\n return\n }\n\n let promiseOrValue: any\n\n // we can re-use config.initialPromise on the first call of run()\n const initialPromise =\n failureCount === 0 ? config.initialPromise : undefined\n\n // Execute query\n try {\n promiseOrValue = initialPromise ?? config.fn()\n } catch (error) {\n promiseOrValue = Promise.reject(error)\n }\n\n Promise.resolve(promiseOrValue)\n .then(resolve)\n .catch((error) => {\n // Stop if the fetch is already resolved\n if (isResolved()) {\n return\n }\n\n // Do we need to retry the request?\n const retry = config.retry ?? (isServerEnvironment() ? 0 : 3)\n const retryDelay = config.retryDelay ?? defaultRetryDelay\n const delay =\n typeof retryDelay === 'function'\n ? retryDelay(failureCount, error)\n : retryDelay\n const shouldRetry =\n retry === true ||\n (typeof retry === 'number' && failureCount < retry) ||\n (typeof retry === 'function' && retry(failureCount, error))\n\n if (isRetryCancelled || !shouldRetry) {\n // We are done if the query does not need to be retried\n reject(error)\n return\n }\n\n failureCount++\n\n // Notify on fail\n config.onFail?.(failureCount, error)\n\n // Delay\n sleep(delay)\n // Pause if the document is not visible or when the device is offline\n .then(() => {\n return canContinue() ? undefined : pause(canContinue)\n })\n .then(() => {\n if (isRetryCancelled) {\n reject(error)\n } else {\n run()\n }\n })\n })\n }\n\n return {\n promise,\n status: () => status,\n cancel,\n continue: () => {\n continueFn?.()\n return promise\n },\n cancelRetry,\n continueRetry,\n canStart,\n start: () => {\n // Start loop\n if (canStart()) {\n run()\n } else {\n // Waiting to start (e.g. queued behind a scope) does not depend on focus,\n // only on the same conditions that allow starting in the first place\n pause(canStart).then(run)\n }\n return promise\n },\n }\n}\n"],"mappings":"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"}