diff --git a/source/errors.ts b/source/errors.ts index 0793481..b4de714 100644 --- a/source/errors.ts +++ b/source/errors.ts @@ -40,3 +40,43 @@ export class ExponentialBackoffStoppedRetriesError extends Error { this.name = 'ExponentialBackoffStoppedRetriesError'; } } + +/** + * Error thrown when an exponential backoff option is too small + */ +export class ExponentialBackoffNumberTooSmallError extends Error { + constructor(option: string, value: number, min: number) { + super(`Exponential backoff option "${option}" is too small. Must be at least ${min}. Received value: ${value}`); + this.name = 'ExponentialBackoffNumberTooSmallError'; + } +} + +/** + * Error thrown when an exponential backoff option is out of bounds + */ +export class ExponentialBackoffNumberOutOfBoundsError extends Error { + constructor(option: string, value: number, min: number, max: number) { + super(`Exponential backoff option "${option}" is out of bounds. Must be between ${min} and ${max}. Received value: ${value}`); + this.name = 'ExponentialBackoffNumberOutOfBoundsError'; + } +} + +/** + * Error thrown when an exponential backoff option is an invalid infinite integer + */ +export class ExponentialBackoffNumberNotFiniteError extends Error { + constructor(option: string, value: number) { + super(`Exponential backoff option "${option}" is invalid. Must be a finite number. Received value: ${value}`); + this.name = 'ExponentialBackoffNumberNotFiniteError'; + } +} + +/** + * Error thrown when an exponential backoff option is not an integer + */ +export class ExponentialBackoffNonIntegerError extends Error { + constructor(option: string, value: number) { + super(`Exponential backoff option "${option}" is invalid. Must be an integer. Received value: ${value}`); + this.name = 'ExponentialBackoffNonIntegerError'; + } +} diff --git a/source/exponential-backoff.ts b/source/exponential-backoff.ts index 8b7b8a8..f359f8c 100644 --- a/source/exponential-backoff.ts +++ b/source/exponential-backoff.ts @@ -1,233 +1,12 @@ -import { ExponentialBackoffStoppedRetriesError, ExponentialBackoffMaxRetriesHitError } from './errors.ts'; - -/** - * Exponential backoff is a technique used to retry a function after a delay. - * - * The delay increases exponentially with each attempt, up to a maximum delay. - * - * The jitter is a random amount of time subtracted from the delay to prevent thundering herd problems. - * - * The growth rate is the factor by which the delay increases with each attempt. - */ -export class ExponentialBackoff { - readonly #options: ExponentialBackoffOptions; - - /** - * Creates a new exponential-backoff instance. - * - * Unspecified options use the defaults listed below. - * - * @param options - Exponential-backoff configuration overrides. - * @param options.maxDelay - Maximum delay between retries. Default: `10_000` ms. - * @param options.maxAttempts - Maximum number of attempts; `0` retries indefinitely. Default: `10`. - * @param options.baseDelay - Delay used as the basis for the first retry. Default: `1_000` ms. - * @param options.growthRate - Multiplier applied to the delay after each attempt. Default: `2`. - * @param options.jitter - Maximum proportional reduction subtracted from each delay (0–1). Default: `0.1`. - */ - constructor(options: Partial = {}) { - this.#options = { - maxDelay: 10_000, - maxAttempts: 10, - baseDelay: 1_000, - growthRate: 2, - jitter: 0.1, - ...options, - }; - - ExponentialBackoff.validateOptions(this.#options); - } - - /** - * Create a new ExponentialBackoff instance - * - * @param config - The configuration for the exponential backoff - * @returns The ExponentialBackoff instance - */ - static from(config?: Partial): ExponentialBackoff { - const backoff = new ExponentialBackoff(config); - - return backoff; - } - - /** - * Run the function with exponential backoff - * - * @param taskFn - The function to run - * @param onError - The callback to call when an error occurs - * @param options - The configuration for the exponential backoff - * - * @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 - * - * @returns The result of the function - */ - static run( - taskFn: (callbackParameters: ExponentialBackoffCallbackParameters) => Promise, - onError = (_error: Error): void => {}, - options?: Partial, - ): Promise { - const backoff = ExponentialBackoff.from(options); - - return backoff.run(taskFn, onError); - } - - /** - * Calculate the delay before we should attempt to retry - * - * @param options - The configuration for the exponential backoff - * @param attempt - The current attempt number - * @returns The time in milliseconds before another attempt should be made - */ - public static calculateDelay(options: ExponentialBackoffOptions, attempt: number): number { - // Get the power of the growth rate - const power = options.growthRate ** attempt; - - // Get the delay before jitter or limit - const rawDelay = options.baseDelay * power; - - // Cap the delay to the maximum. Do this before the jitter so jitter does not become larger than delay - const cappedDelay = Math.min(rawDelay, options.maxDelay); - - // Get a random number for the amount to "jitter" the delay by - const jitterAmount = Math.random(); - - // Calculate the jitter - const jitter = jitterAmount * options.jitter * cappedDelay; - - // Subtract the jitter from the delay - return cappedDelay - jitter; - } - - /** - * Validate the options for the exponential backoff - * - * @param options - The options to validate - * - * @throws An error if the options are invalid - */ - public static validateOptions(options: ExponentialBackoffOptions): void { - // Validate the max delay is a finite number not less than 0 - if (!Number.isFinite(options.maxDelay)) { - throw new Error('maxDelay must be a finite number'); - } - - if (options.maxDelay < 0) { - throw new Error('maxDelay must be not less than 0'); - } - - // Validate the max attempts is a finite number not less than 0 - if (!Number.isFinite(options.maxAttempts)) { - throw new Error('maxAttempts must be a finite number'); - } - - if (options.maxAttempts < 0) { - throw new Error('maxAttempts must be not less than 0'); - } - - // Validate the base delay is a finite number not less than 0 - if (!Number.isFinite(options.baseDelay)) { - throw new Error('baseDelay must be a finite number'); - } - - if (options.baseDelay < 0) { - throw new Error('baseDelay must be not less than 0'); - } - - // Validate the growth rate is a finite number not less than 0 - if (!Number.isFinite(options.growthRate)) { - throw new Error('growthRate must be a finite number'); - } - - if (options.growthRate < 0) { - throw new Error('growthRate must be not less than 0'); - } - - // Validate the jitter is a finite number not less than 0 or greater than 1 - if (!Number.isFinite(options.jitter)) { - throw new Error('jitter must be a finite number'); - } - - if (options.jitter < 0 || options.jitter > 1) { - throw new Error('jitter must be not less than 0 or greater than 1'); - } - } - - /** - * 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 the function fails and we have hit the max attempts, an ExponentialBackoffMaxRetriesHitError will be thrown with all the errors that were thrown by the task function - * - * @param taskFn - The function to run - * @param onError - The callback to call when an error occurs - * - * @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 - * - * @returns The result of the function - */ - async run( - taskFn: (callbackParameters: ExponentialBackoffCallbackParameters) => Promise, - onError = (_error: Error): void => {}, - ): Promise { - // Initialize an abort signal to allow the task function to be aborted - const abortController = new AbortController(); - const stopRetries = abortController.abort.bind(abortController); - - // Initialize an empty array to store the errors - const errors: Error[] = []; - - // Initialize the attempt counter - let attempt = 0; - - // If the max attempts is 0, we should continue indefinitely. - const unlimitedAttempts = this.#options.maxAttempts === 0; - - // Loop until we succeed, hit the max attempts, or the abort signal is activated - while (true) { - try { - // Await the promise before returning so its execution context remains in the try-catch - // If we didn't await, this `run` function would successfully return and any errors would not be caught here. - return await taskFn({ stopRetries }); - } catch (error) { - // Store the error in case we fail every attempt - const errorInstance = error instanceof Error ? error : new Error(`${error}`); - onError(errorInstance); - - // If we have unlimited attempts, don't append this to the errors array to prevent a memory leak. - if (!unlimitedAttempts) { - errors.push(errorInstance); - } - } - - // Calculate the count for next attempt. Do this now so we can exit before waiting and before running the next attempt. - const nextAttemptCount = attempt + 1; - const nextAttemptExceedsMaxAttempts = nextAttemptCount >= this.#options.maxAttempts; - - // If the next attempt exceeds the max attempts, break out of the loop - if (!unlimitedAttempts && nextAttemptExceedsMaxAttempts) { - break; - } - - // Check if the abort signal has been aborted - if (abortController.signal.aborted) { - // Throw an error if the abort signal has been aborted - throw new ExponentialBackoffStoppedRetriesError(abortController.signal.reason); - } - - // Wait before going to the next attempt - const delay = ExponentialBackoff.calculateDelay(this.#options, attempt); - await new Promise((resolve) => setTimeout(resolve, delay)); - - attempt++; - } - - // We completed the loop without ever succeeding. Throw an ExponentialBackoffMaxRetriesHitError with all the errors we got - throw new ExponentialBackoffMaxRetriesHitError(errors); - } -} +import { + ExponentialBackoffStoppedRetriesError, + ExponentialBackoffMaxRetriesHitError, + ExponentialBackoffNonIntegerError, + ExponentialBackoffNumberTooSmallError, + ExponentialBackoffNumberOutOfBoundsError, + ExponentialBackoffNumberNotFiniteError, +} from './errors.ts'; +import { isWithinBounds } from './misc.ts'; export type ExponentialBackoffOptions = { @@ -273,3 +52,250 @@ export type ExponentialBackoffStopRetriesFunction = (reason: unknown) => void; export type ExponentialBackoffCallbackParameters = { stopRetries: ExponentialBackoffStopRetriesFunction; }; + +/** + * Exponential backoff is a technique used to retry a function after a delay. + * + * The delay increases exponentially with each attempt, up to a maximum delay. + * + * The jitter is a random amount of time subtracted from the delay to prevent thundering herd problems. + * + * The growth rate is the factor by which the delay increases with each attempt. + */ +export class ExponentialBackoff { + readonly #options: ExponentialBackoffOptions; + + /** + * Creates a new exponential-backoff instance. + * + * Unspecified options use the defaults listed below. + * + * @param options - Exponential-backoff configuration overrides. + * @param options.maxDelay - Maximum delay between retries. Default: `10_000` ms. + * @param options.maxAttempts - Maximum number of attempts; `0` retries indefinitely. Default: `10`. + * @param options.baseDelay - Delay used as the basis for the first retry. Default: `1_000` ms. + * @param options.growthRate - Multiplier applied to the delay after each attempt. Default: `2`. + * @param options.jitter - Maximum proportional reduction subtracted from each delay (0–1). Default: `0.1`. + * + * @throws An {@link ExponentialBackoffNumberNotFiniteError} if a provided option is not a finite number + * @throws An {@link ExponentialBackoffNumberOutOfBoundsError} if a provided option is out of bounds + * @throws An {@link ExponentialBackoffNumberTooSmallError} if a provided option is too small + * @throws An {@link ExponentialBackoffNonIntegerError} if a provided option is not an integer + */ + constructor(options: Partial = {}) { + this.#options = { + maxDelay: 10_000, + maxAttempts: 10, + baseDelay: 1_000, + growthRate: 2, + jitter: 0.1, + ...options, + }; + + ExponentialBackoff.validateOptions(this.#options); + } + + /** + * Create a new ExponentialBackoff instance + * + * @param config - The configuration for the exponential backoff + * + * @throws An {@link ExponentialBackoffNumberNotFiniteError} if a provided option is not a finite number + * @throws An {@link ExponentialBackoffNumberOutOfBoundsError} if a provided option is out of bounds + * @throws An {@link ExponentialBackoffNumberTooSmallError} if a provided option is too small + * @throws An {@link ExponentialBackoffNonIntegerError} if a provided option is not an integer + * + * @returns The ExponentialBackoff instance + */ + public static from(config?: Partial): ExponentialBackoff { + const backoff = new ExponentialBackoff(config); + + return backoff; + } + + /** + * Run the function with exponential backoff + * + * @param taskFn - The function to run + * @param onError - The callback to call when an error occurs + * @param options - The configuration for the exponential backoff + * + * @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 + * + * @returns The result of the function + */ + public static run( + taskFn: (callbackParameters: ExponentialBackoffCallbackParameters) => Promise, + onError = (_error: Error): void => {}, + options?: Partial, + ): Promise { + const backoff = ExponentialBackoff.from(options); + + return backoff.run(taskFn, onError); + } + + /** + * Calculate the delay before we should attempt to retry + * + * @param options - The configuration for the exponential backoff + * @param attempt - The current attempt number + * @returns The time in milliseconds before another attempt should be made + */ + public static calculateDelay(options: ExponentialBackoffOptions, attempt: number): number { + // Get the power of the growth rate + const power = options.growthRate ** attempt; + + // Get the delay before jitter or limit + const rawDelay = options.baseDelay * power; + + // Cap the delay to the maximum. Do this before the jitter so jitter does not become larger than delay + const cappedDelay = Math.min(rawDelay, options.maxDelay); + + // Get a random number for the amount to "jitter" the delay by + const jitterAmount = Math.random(); + + // Calculate the jitter + const jitter = jitterAmount * options.jitter * cappedDelay; + + // Subtract the jitter from the delay + return cappedDelay - jitter; + } + + /** + * Validate the options for the exponential backoff + * + * @param options - The options to validate + * + * @throws {@link ExponentialBackoffNumberNotFiniteError} if a provided option is not a finite number + * @throws {@link ExponentialBackoffNonIntegerError} if a provided option is not an integer + * @throws {@link ExponentialBackoffNumberOutOfBoundsError} if a provided option is out of bounds + * @throws {@link ExponentialBackoffNumberTooSmallError} if a provided option is too small + */ + public static validateOptions(options: ExponentialBackoffOptions): void { + /** Validate the value is finite, throwing an {@link ExponentialBackoffInvalidInfiniteIntegerError} if the value is infinite */ + const assertIsFinite = (key: string, value: number): void => { + if (!Number.isFinite(value)) { + throw new ExponentialBackoffNumberNotFiniteError(key, value); + } + }; + + /** Validate the value is an integer, throwing a {@link ExponentialBackoffNonIntegerError} if it is not an integer */ + const assertIsInteger = (key: string, value: number): void => { + if (!Number.isInteger(value)) { + throw new ExponentialBackoffNonIntegerError(key, value); + } + }; + + /** Validate the value is greater than the minimum, throwing a {@link ExponentialBackoffNumberTooSmallError} if it is not */ + const assertIsHigherThan = (key: string, value: number, min: number): void => { + if (value < min) { + throw new ExponentialBackoffNumberTooSmallError(key, value, min); + } + }; + + /** Validate the value is within the bounds, throwing a {@link ExponentialBackoffNumberOutOfBoundsError} if it is not within the bounds */ + const assertIsWithinBounds = (key: string, value: number, min: number, max: number): void => { + if (!isWithinBounds(value, min, max)) { + throw new ExponentialBackoffNumberOutOfBoundsError(key, value, min, max); + } + }; + + // Validate the max delay + assertIsFinite('maxDelay', options.maxDelay); + assertIsHigherThan('maxDelay', options.maxDelay, 0); + + // Validate the max attempts + assertIsFinite('maxAttempts', options.maxAttempts); + assertIsInteger('maxAttempts', options.maxAttempts); + assertIsHigherThan('maxAttempts', options.maxAttempts, 0); + + // Validate the base delay + assertIsFinite('baseDelay', options.baseDelay); + assertIsHigherThan('baseDelay', options.baseDelay, 0); + + // Validate the growth rate + assertIsFinite('growthRate', options.growthRate); + assertIsHigherThan('growthRate', options.growthRate, 0); + + // Validate the jitter + assertIsFinite('jitter', options.jitter); + assertIsWithinBounds('jitter', options.jitter, 0, 1); + } + + /** + * 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 the function fails and we have hit the max attempts, an ExponentialBackoffMaxRetriesHitError will be thrown with all the errors that were thrown by the task function + * + * @param taskFn - The function to run + * @param onError - The callback to call when an error occurs + * + * @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 + * + * @returns The result of the function + */ + public async run( + taskFn: (callbackParameters: ExponentialBackoffCallbackParameters) => Promise, + onError = (_error: Error): void => {}, + ): Promise { + // Initialize an abort signal to allow the task function to be aborted + const abortController = new AbortController(); + const stopRetries = abortController.abort.bind(abortController); + + // Initialize an empty array to store the errors + const errors: Error[] = []; + + // Initialize the attempt counter + let attempt = 0; + + // If the max attempts is 0, we should continue indefinitely. + const unlimitedAttempts = this.#options.maxAttempts === 0; + + // Loop until we succeed, hit the max attempts, or the abort signal is activated + while (true) { + try { + // Await the promise before returning so its execution context remains in the try-catch + // If we didn't await, this `run` function would successfully return and any errors would not be caught here. + return await taskFn({ stopRetries }); + } catch (error) { + // Store the error in case we fail every attempt + const errorInstance = error instanceof Error ? error : new Error(`${error}`); + onError(errorInstance); + + // If we have unlimited attempts, don't append this to the errors array to prevent a memory leak. + if (!unlimitedAttempts) { + errors.push(errorInstance); + } + } + + // Calculate the count for next attempt. Do this now so we can exit before waiting and before running the next attempt. + const nextAttemptCount = attempt + 1; + const nextAttemptExceedsMaxAttempts = nextAttemptCount >= this.#options.maxAttempts; + + // If the next attempt exceeds the max attempts, break out of the loop + if (!unlimitedAttempts && nextAttemptExceedsMaxAttempts) { + break; + } + + // Check if the abort signal has been activated + if (abortController.signal.aborted) { + // Throw an error if the abort signal has been activated + throw new ExponentialBackoffStoppedRetriesError(abortController.signal.reason); + } + + // Wait before going to the next attempt + const delay = ExponentialBackoff.calculateDelay(this.#options, attempt); + await new Promise((resolve) => setTimeout(resolve, delay)); + + attempt++; + } + + // We completed the loop without ever succeeding. Throw an ExponentialBackoffMaxRetriesHitError with all the errors we got + throw new ExponentialBackoffMaxRetriesHitError(errors); + } +} diff --git a/source/index.ts b/source/index.ts index de07278..571a097 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,3 +1,4 @@ +export * from './errors.ts'; export * from './event-emitter.ts'; export * from './exponential-backoff.ts'; export * from './extended-json.ts'; diff --git a/source/misc.ts b/source/misc.ts index eae25c9..2b67d1a 100644 --- a/source/misc.ts +++ b/source/misc.ts @@ -1,3 +1,20 @@ +/** + * Validate the value is within the bounds, returning true if it is within the bounds, false otherwise + * + * @param value - The value to validate + * @param min - The minimum value + * @param max - The maximum value + * + * @returns True if the value is within the bounds, false otherwise + */ +export const isWithinBounds = (value: number, min: number, max: number): boolean => { + if (value < min || value > max) { + return false; + } + + return true; +}; + /** * Tries to execute an async function and handles any errors that occur. * @param fn - The function to execute. diff --git a/test/exponential-backoff.test.ts b/test/exponential-backoff.test.ts index 7c29a96..a27b374 100644 --- a/test/exponential-backoff.test.ts +++ b/test/exponential-backoff.test.ts @@ -1,6 +1,10 @@ import { expect, test, vi } from 'vitest'; import { ExponentialBackoff } from '../source/exponential-backoff.ts'; -import { ExponentialBackoffMaxRetriesHitError, ExponentialBackoffStoppedRetriesError } from '../source/errors.ts'; +import { + ExponentialBackoffMaxRetriesHitError, + ExponentialBackoffNumberNotFiniteError, + ExponentialBackoffStoppedRetriesError, +} from '../source/errors.ts'; /** * A valid options object that satisfies {@link ExponentialBackoff.validateOptions}. @@ -498,7 +502,7 @@ const testExponentialBackoffValidateOptionsRejectsNegativeValues = (): void => { ExponentialBackoff.validateOptions({ ...validExponentialBackoffOptions, [field]: value, - })).toThrow(`${field} must be not less than 0`); + })).toThrow(`Exponential backoff option "${field}" is too small. Must be at least 0`); } }; @@ -515,7 +519,7 @@ const testExponentialBackoffValidateOptionsRejectsInvalidJitter = (): void => { ExponentialBackoff.validateOptions({ ...validExponentialBackoffOptions, jitter: value, - })).toThrow('jitter must be not less than 0 or greater than 1'); + })).toThrow('Exponential backoff option "jitter" is out of bounds. Must be between 0 and 1'); } }; @@ -538,7 +542,24 @@ const testExponentialBackoffValidateOptionsRejectsNonFiniteValues = (): void => ExponentialBackoff.validateOptions({ ...validExponentialBackoffOptions, [field]: value, - })).toThrow(`${field} must be a finite number`); + })).toThrow(`Exponential backoff option "${field}" is invalid. Must be a finite number`); + } +}; + +/** + * Tests that {@link ExponentialBackoff.validateOptions} rejects non-integer values. + */ +const testExponentialBackoffValidateOptionsRejectsNonIntegerValues = (): void => { + // Define our test cases with each value being a non-integer + const nonIntegerCases = [{ field: 'maxAttempts', value: 1.5 }] as const; + + // Iterate through the test cases and expect an error to be thrown + for (const { field, value } of nonIntegerCases) { + expect(() => + ExponentialBackoff.validateOptions({ + ...validExponentialBackoffOptions, + [field]: value, + })).toThrow(`Exponential backoff option "${field}" is invalid. Must be an integer`); } }; @@ -561,10 +582,47 @@ const testExponentialBackoffValidateOptionsRejectsNaN = (): void => { ExponentialBackoff.validateOptions({ ...validExponentialBackoffOptions, [field]: value, - })).toThrow(`${field} must be a finite number`); + })).toThrow(`Exponential backoff option "${field}" is invalid. Must be a finite number`); } }; +/** Tests that calculateDelay will not result in NaN from extremely large growth rates and attempts */ +const testExponentialBackoffCalculateDelayDoesNotResultInNaN = (): void => { + // Large number, 1 trillion. + // Theory being that 1 trillion to the power of 1 trillion should be a very large number and cause either an unsafe value or a NaN. + const largeNumber = 1_000_000_000_000; + + // Test the calculateDelay function + const result = ExponentialBackoff.calculateDelay( + { + baseDelay: 10000, + growthRate: largeNumber, + jitter: 0, + maxDelay: 10_000, + maxAttempts: largeNumber, + }, + largeNumber, + ); + + // Test to ensure it was bounded to the max delay + expect(result).toBe(10_000); +}; + +/** Tests that passing undefined into the constructor does not cause an error during spread */ +const testExponentialBackoffConstructorDoesNotCauseErrorDuringSpread = async (): Promise => { + const options = { + baseDelay: undefined, + growthRate: undefined, + jitter: undefined, + maxDelay: undefined, + maxAttempts: undefined, + }; + + // We expect an error during validation as undefined is not a finite number, not an issue with the spread operator + // @ts-expect-error - Passing undefined is allowed if the exactOptionalPropertyTypes option is set to false in TS Compiler options. + expect(() => new ExponentialBackoff(options)).toThrow(ExponentialBackoffNumberNotFiniteError); +}; + const runTests = async (): Promise => { test('ExponentialBackoff.run: delegates to a new instance using default options', testExponentialBackoffRunUsesDefaultOptions); test('ExponentialBackoff.run: retries and succeeds with partial options', testExponentialBackoffRunWithPartialOptions); @@ -589,7 +647,13 @@ const runTests = async (): Promise => { test('ExponentialBackoff.validateOptions: rejects negative values', testExponentialBackoffValidateOptionsRejectsNegativeValues); test('ExponentialBackoff.validateOptions: rejects invalid jitter', testExponentialBackoffValidateOptionsRejectsInvalidJitter); test('ExponentialBackoff.validateOptions: rejects Infinity', testExponentialBackoffValidateOptionsRejectsNonFiniteValues); + test('ExponentialBackoff.validateOptions: rejects non-integer values', testExponentialBackoffValidateOptionsRejectsNonIntegerValues); test('ExponentialBackoff.validateOptions: rejects NaN', testExponentialBackoffValidateOptionsRejectsNaN); + test( + 'ExponentialBackoff: calculateDelay does not result in NaN from extremely large growth rates and attempts', + testExponentialBackoffCalculateDelayDoesNotResultInNaN, + ); + test('ExponentialBackoff: constructor does not cause an error during spread', testExponentialBackoffConstructorDoesNotCauseErrorDuringSpread); }; await runTests(); diff --git a/tsconfig.json b/tsconfig.json index 106b15f..e6b7c1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "allowImportingTsExtensions": true, + "exactOptionalPropertyTypes": true, "noEmit": true, "declaration": true, "declarationMap": true