97 lines
4.0 KiB
TypeScript
97 lines
4.0 KiB
TypeScript
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));
|
|
}
|
|
}
|