Last active
April 13, 2025 09:21
-
-
Save vitaly-t/6e3d285854d882b1618c7e435df164c4 to your computer and use it in GitHub Desktop.
retry-async
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Retry-status object type, for use with RetryCB. | |
*/ | |
export type RetryStatus<D = unknown> = { | |
/** | |
* Retry index, starting from 0. | |
*/ | |
readonly index: number, | |
/** | |
* Retry overall duration, in milliseconds. | |
*/ | |
readonly duration: number, | |
/** | |
* Last error, if available; | |
* it is undefined only when "retryAsync" calls "func" with index = 0. | |
*/ | |
readonly error?: Error, | |
/** | |
* Extra data for status handlers, if specified. | |
*/ | |
readonly data?: D | |
}; | |
/** | |
* Retry-status callback type. | |
*/ | |
export type RetryCB<T, D = unknown> = (s: RetryStatus<D>) => T; | |
/** | |
* Type for options passed into retryAsync function. | |
*/ | |
export type RetryOptions<D = unknown> = { | |
/** | |
* Maximum number of retries (infinite by default), | |
* or a callback to indicate the need for another retry. | |
*/ | |
readonly retry?: number | RetryCB<boolean, D>, | |
/** | |
* Retry delays, in milliseconds (no delay by default), | |
* or a callback that returns the delays. | |
*/ | |
readonly delay?: number | RetryCB<number, D>, | |
/** | |
* Error notifications. | |
*/ | |
readonly error?: RetryCB<void, D>, | |
/** | |
* Extra data for status handlers. | |
*/ | |
readonly data?: D | |
}; | |
/** | |
* Retries async operation returned from "func" callback, according to "options". | |
*/ | |
export function retryAsync<T, D>(func: RetryCB<Promise<T>, D>, options?: RetryOptions<D>): Promise<T> { | |
const start = Date.now(); | |
let index = 0, e: any; | |
let {retry = Number.POSITIVE_INFINITY, delay = -1, error, data} = options ?? {}; | |
const s = () => ({index, duration: Date.now() - start, error: e, data}); | |
const c = (): Promise<T> => func(s()).catch(err => { | |
e = err; | |
typeof error === 'function' && error(s()); | |
if ((typeof retry === 'function' ? (retry(s()) ? 1 : 0) : retry--) <= 0) { | |
return Promise.reject(e); | |
} | |
const d = typeof delay === 'function' ? delay(s()) : delay; | |
index++; | |
return d >= 0 ? (new Promise(a => setTimeout(a, d))).then(c) : c(); | |
}); | |
return c(); | |
} |
The most recent update added templated option {data: D}
, which can be used as an extra parameter inside status handlers.
For example, you can use it for implementing your own external retry-strategy, like shown below:
import {retryAsync, RetryCB, RetryStatus} from './retry-async';
type RetryStrategy = 'http' | 'generic';
function retryForStrategy(s: RetryStatus<RetryStrategy>): boolean {
// 3 attempts for generic operations, and 5 for http:
return s.data === 'http' ? s.index < 5 : s.index < 3;
}
function delayForStrategy(s: RetryStatus<RetryStrategy>): number {
// 500ms for generic operations, incremental 500ms for http:
return s.data === 'http' ? (s.index + 1) * 500 : 500;
}
function retryAsyncStrategy<T>(func: RetryCB<Promise<T>, RetryStrategy>, strategy?: RetryStrategy): Promise<T> {
return retryAsync(func, {retry: retryForStrategy, delay: delayForStrategy, data: strategy});
}
So above, if you pass optional "http"
parameter into retryAsyncStrategy
, it will execute a different retry strategy. It's just a more elegant way to abstract retry strategies away from the code that relies on them, through simple parametrization.
UPDATE:
For the example above, it would make sense to replace the verbose retryAsyncStrategy
with two simpler functions, as below:
function retryHttp<T>(func: RetryCB<Promise<T>, RetryStrategy>): Promise<T> {
return retryAsync(func, {retry: retryForStrategy, delay: delayForStrategy, data: 'http'});
}
function retryGeneric<T>(func: RetryCB<Promise<T>>): Promise<T> {
return retryAsync(func, {retry: retryForStrategy, delay: delayForStrategy});
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Why would you want to change it that way when you already have
error
handler that provides the details?No, it doesn't,
retryAsync
requires that the wrapper always returns a promise.That is not applicable here! The original function is always async, this is what it is all about, retrying an async function, so it always returns a Promise. No need throwing an exception, though you can, if you want.
In all, I sense a lot of confusion around how
retryAsync
works, even though the source code is all up in front. Somehow you managed to make all the wrong assumptions here :) Perhaps you haven't worked with promises and async code in JavaScript enough.@infinity-matrix if you have questions, open a discission in the repo, it will be easier there, as gists are too limited.