@geersch/retry
Version:
Backoff strategies to use when retrying a function after a given delay.
42 lines • 2.14 kB
JavaScript
import { defer, firstValueFrom, fromEvent, Observable, retry as retryOperator, switchMap, takeUntil, tap, throwError, timer, } from 'rxjs';
export async function retry(operation, backoffStrategy, options = {}) {
let attempt = 1;
return firstValueFrom(passRetryOperatorToPipe(defer(async () => operation(attempt)).pipe(tap({
error: () => (attempt += 1),
})), backoffStrategy, options));
}
function createTimer(due, signal, error) {
return signal
? timer(due).pipe(takeUntil(fromEvent(signal, 'abort').pipe(switchMap(() => throwError(() => error)))))
: timer(due);
}
export function passRetryOperatorToPipe(observable, backoffStrategy, { abortRetry, maxDelay = 30000, maxRetries = 5, scaleFactor = 1, signal, unrecoverableErrors = [] }) {
if (scaleFactor <= 0) {
throw new TypeError(`Expected 'scaleFactor' to be a positive number greater than zero, got ${scaleFactor}.`);
}
const strategy = typeof backoffStrategy === 'function' ? new backoffStrategy() : backoffStrategy;
const generator = strategy.getGenerator(maxRetries);
const abortSignal = typeof signal === 'function' ? signal() : signal;
return observable.pipe(retryOperator({
count: maxRetries,
delay: (error, retryCount) => {
if (abortSignal?.aborted) {
return throwError(() => error);
}
const isUnrecoverable = unrecoverableErrors.some((errorConstructor) => error instanceof errorConstructor);
if (isUnrecoverable || (abortRetry?.(error, retryCount) ?? false)) {
return throwError(() => error);
}
const { value, done } = generator.next();
if (done) {
return throwError(() => new Error(`The backoff strategy did not yield a delay for retry attempt ${retryCount}.`));
}
let delay = value * scaleFactor;
if (delay > maxDelay) {
delay = maxDelay;
}
return abortSignal ? createTimer(delay, abortSignal, error) : createTimer(delay);
},
}));
}
//# sourceMappingURL=retry.js.map