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 * @property abortSignal - The abort signal to use for this instance */ export type ExponentialBackoffExternallyAbortableOptions = Partial & { 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 = {}) { 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( taskFn: (callbackParameters: ExponentialBackoffCallbackParameters) => Promise, options: Partial = {}, ): Promise { 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)); } }