Add exponential backoff utility
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
ExternallyAbortedExponentialBackoffExternalSignalAbortedError,
|
||||
ExternallyAbortedExponentialBackoffInternalSignalAbortedError,
|
||||
} from './errors.ts';
|
||||
import { ExponentialBackoff } from './exponential-backoff.ts';
|
||||
import type { ExponentialBackoffCallbackParameters, ExponentialBackoffOptions, ExponentialBackoffRunOptions } from './exponential-backoff.ts';
|
||||
|
||||
/**
|
||||
* Options for the ExponentialBackoffExternallyAbortable class
|
||||
*
|
||||
* @extends Partial<ExponentialBackoffOptions>
|
||||
* @property abortSignal - The abort signal to use for this instance
|
||||
*/
|
||||
export type ExponentialBackoffExternallyAbortableOptions = Partial<ExponentialBackoffOptions> & {
|
||||
abortSignal: AbortSignal;
|
||||
};
|
||||
|
||||
/**
|
||||
* An exponential backoff that can be stopped by calling `.abort()` or by passing an
|
||||
* `abortSignal` to the constructor.
|
||||
*
|
||||
* @remarks One instance can run many tasks. Aborting it stops retries for every run.
|
||||
*/
|
||||
export class ExponentialBackoffExternallyAbortable {
|
||||
readonly #abortController = new AbortController();
|
||||
readonly #exponentialBackoff: ExponentialBackoff;
|
||||
|
||||
constructor(options: Partial<ExponentialBackoffExternallyAbortableOptions> = {}) {
|
||||
const { abortSignal, ...backoffOptions } = options;
|
||||
|
||||
this.#exponentialBackoff = new ExponentialBackoff(backoffOptions);
|
||||
|
||||
// Listen for the provided abort signal to be aborted
|
||||
abortSignal?.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
this.#abortController.abort(new ExternallyAbortedExponentialBackoffExternalSignalAbortedError(abortSignal.reason));
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
// Clean up this listener if the internal abort controller is aborted
|
||||
signal: this.#abortController.signal,
|
||||
},
|
||||
);
|
||||
|
||||
// If the abort signal is already aborted, immediately abort the internal abort controller
|
||||
if (abortSignal?.aborted) {
|
||||
this.#abortController.abort(new ExternallyAbortedExponentialBackoffExternalSignalAbortedError(abortSignal.reason));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the function with exponential backoff
|
||||
*
|
||||
* If the function fails but we have not hit the max attempts, the error will be passed to the onError callback
|
||||
* and the function will be retried with an exponential delay
|
||||
*
|
||||
* If every attempt fails, an ExponentialBackoffMaxRetriesHitError will be thrown
|
||||
* with all errors from the task function.
|
||||
*
|
||||
* @param taskFn - The function to run
|
||||
* @param options - Options that control this run
|
||||
*
|
||||
* @throws An {@link ExponentialBackoffMaxRetriesHitError} with all the errors that were thrown by the task function
|
||||
* @throws An {@link ExponentialBackoffStoppedRetriesError} if the abort signal is activated
|
||||
* @throws An {@link ExternallyAbortedExponentialBackoffExternalSignalAbortedError}
|
||||
* if the abort signal that was provided during construction is activated
|
||||
* @throws An {@link ExternallyAbortedExponentialBackoffInternalSignalAbortedError}
|
||||
* if {@link ExponentialBackoffExternallyAbortable.abort} is called on this class
|
||||
*
|
||||
* @returns The result of the function
|
||||
*/
|
||||
public run<T>(
|
||||
taskFn: (callbackParameters: ExponentialBackoffCallbackParameters) => Promise<T>,
|
||||
options: Partial<ExponentialBackoffRunOptions> = {},
|
||||
): Promise<T> {
|
||||
let signal = this.#abortController.signal;
|
||||
|
||||
// If a signal was provided for this run, compose it with the instance-wide signal
|
||||
if (options.signal !== undefined) {
|
||||
signal = AbortSignal.any([ signal, options.signal ]);
|
||||
}
|
||||
|
||||
// Call the exponential backoff run method with the composed signal
|
||||
return this.#exponentialBackoff.run(taskFn, { ...options, signal });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops retries for all current and future runs.
|
||||
*
|
||||
* @param reason - The reason for stopping retries
|
||||
*/
|
||||
public abort(reason: unknown): void {
|
||||
this.#abortController.abort(new ExternallyAbortedExponentialBackoffInternalSignalAbortedError(reason));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user