517 lines
20 KiB
TypeScript
517 lines
20 KiB
TypeScript
import { describe, expect, test, vi } from 'vitest';
|
|
|
|
import type {
|
|
ExponentialBackoffCallbackParameters,
|
|
ExponentialBackoffOptions,
|
|
ExponentialBackoffRunOptions,
|
|
} from '../../source/exponential-backoff/index.ts';
|
|
|
|
import {
|
|
ExponentialBackoffMaxRetriesHitError,
|
|
ExponentialBackoffNumberNotFiniteError,
|
|
ExponentialBackoffStoppedRetriesError,
|
|
} from '../../source/exponential-backoff/index.ts';
|
|
|
|
/**
|
|
* The common run interface implemented by ExponentialBackoff and ExponentialBackoffExternallyAbortable.
|
|
*/
|
|
export type ExponentialBackoffInstance = {
|
|
run: <T>(
|
|
taskFn: (callbackParameters: ExponentialBackoffCallbackParameters) => Promise<T>,
|
|
options?: Partial<ExponentialBackoffRunOptions>,
|
|
) => Promise<T>;
|
|
};
|
|
|
|
/**
|
|
* Factory function passed into the tests to create an {@link ExponentialBackoffInstance}.
|
|
*/
|
|
export type ExponentialBackoffFactory = (options?: Partial<ExponentialBackoffOptions>) => ExponentialBackoffInstance;
|
|
|
|
/**
|
|
* Tests the happy path: the wrapped function succeeds immediately and no retry machinery runs.
|
|
*/
|
|
const testExponentialBackoffSucceedsOnFirstAttempt = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
// Always resolves — never enters the catch/retry branch.
|
|
const resolveFn = vi.fn(async () => 'success');
|
|
const onError = vi.fn();
|
|
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
};
|
|
|
|
const result = await exponentialBackoffFactory(options).run(resolveFn, { onError });
|
|
|
|
expect(result).toBe('success');
|
|
expect(resolveFn).toHaveBeenCalledOnce();
|
|
expect(onError).not.toHaveBeenCalled();
|
|
};
|
|
|
|
/**
|
|
* Tests that retries continue across multiple failures until the function eventually resolves.
|
|
*/
|
|
const testExponentialBackoffRetriesUntilSuccess = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
// Three invocations: two rejections then a success on the third call.
|
|
const tripleRejectFn = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error('attempt 1'))
|
|
.mockRejectedValueOnce(new Error('attempt 2'))
|
|
.mockResolvedValueOnce('success');
|
|
|
|
// maxAttempts: 5 is high enough that we stop because fn succeeded, not because we hit the cap.
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
maxAttempts: 5,
|
|
};
|
|
|
|
const result = await exponentialBackoffFactory(options).run(tripleRejectFn);
|
|
|
|
expect(result).toBe('success');
|
|
expect(tripleRejectFn).toHaveBeenCalledTimes(3);
|
|
};
|
|
|
|
/**
|
|
* Tests that the onError callback is invoked once for every failed attempt, including the last one
|
|
* before an ExponentialBackoffMaxRetriesHitError is thrown to the caller.
|
|
*/
|
|
const testExponentialBackoffCallsOnErrorForEachFailure = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
const error = new Error('temporary failure');
|
|
|
|
// Always rejects with the same error — we will exhaust all attempts.
|
|
const rejectFn = vi.fn().mockRejectedValue(error);
|
|
const onError = vi.fn();
|
|
|
|
// maxAttempts: 3 means three tries total, all of which will fail.
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
maxAttempts: 3,
|
|
};
|
|
|
|
await expect(exponentialBackoffFactory(options).run(rejectFn, { onError })).rejects.toThrow(ExponentialBackoffMaxRetriesHitError);
|
|
|
|
expect(onError).toHaveBeenCalledTimes(3);
|
|
expect(onError).toHaveBeenCalledWith(error);
|
|
};
|
|
|
|
/**
|
|
* Tests that when all attempts are exhausted the caller receives an ExponentialBackoffMaxRetriesHitError
|
|
* with every task error preserved in order on the cause.
|
|
*/
|
|
const testExponentialBackoffThrowsMaxRetriesHitErrorWhenExhausted = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
const firstError = new Error('first');
|
|
const lastError = new Error('last');
|
|
|
|
// Two distinct errors so we can prove both are collected, not just the last one.
|
|
const doubleRejectFn = vi.fn().mockRejectedValueOnce(firstError)
|
|
.mockRejectedValueOnce(lastError);
|
|
|
|
try {
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
maxAttempts: 2,
|
|
};
|
|
|
|
await exponentialBackoffFactory(options).run(doubleRejectFn, { onError: vi.fn() });
|
|
expect.fail('Expected ExponentialBackoffMaxRetriesHitError to be thrown');
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(ExponentialBackoffMaxRetriesHitError);
|
|
expect((error as ExponentialBackoffMaxRetriesHitError).cause).toEqual([ firstError, lastError ]);
|
|
}
|
|
|
|
expect(doubleRejectFn).toHaveBeenCalledTimes(2);
|
|
};
|
|
|
|
/**
|
|
* Tests that rejections which are not Error instances are coerced to Error before onError runs,
|
|
* so callers always observe a consistent error type in the callback.
|
|
*/
|
|
const testExponentialBackoffWrapsNonErrorThrows = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
// Reject with a plain string — not an Error subclass.
|
|
const rejectedFn = vi.fn().mockRejectedValue('not-an-error');
|
|
const onError = vi.fn();
|
|
|
|
// Single attempt — we fail fast and inspect what onError received.
|
|
try {
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
maxAttempts: 1,
|
|
};
|
|
|
|
await exponentialBackoffFactory(options).run(rejectedFn, { onError });
|
|
expect.fail('Expected ExponentialBackoffMaxRetriesHitError to be thrown');
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(ExponentialBackoffMaxRetriesHitError);
|
|
const [ wrappedError ] = (error as ExponentialBackoffMaxRetriesHitError).cause as Error[];
|
|
expect(wrappedError).toBeInstanceOf(Error);
|
|
expect(wrappedError.message).toBe('not-an-error');
|
|
}
|
|
|
|
expect(onError).toHaveBeenCalledOnce();
|
|
expect(onError.mock.calls[0][0]).toBeInstanceOf(Error);
|
|
expect(onError.mock.calls[0][0].message).toBe('not-an-error');
|
|
};
|
|
|
|
/**
|
|
* Tests that when the task function succeeds and the abort signal is aborted, the result is returned
|
|
* and the onError callback is not called.
|
|
*/
|
|
const testExponentialBackoffRunSuccessAndAbortSignal = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
// Define the function which aborts the exponential backoff and succeeds
|
|
const abortAndSucceedFn = vi.fn(({ stopRetries }) => {
|
|
stopRetries(new Error('retry me'));
|
|
|
|
return Promise.resolve('success');
|
|
});
|
|
const onErrorFn = vi.fn();
|
|
|
|
// Run the exponential backoff with the function and the onError callback
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
};
|
|
|
|
const result = await exponentialBackoffFactory(options).run(abortAndSucceedFn, { onError: onErrorFn });
|
|
|
|
// Expect the result to be the success message
|
|
expect(result).toBe('success');
|
|
expect(abortAndSucceedFn).toHaveBeenCalledOnce();
|
|
|
|
// Expect the onError callback to not have been called
|
|
expect(onErrorFn).not.toHaveBeenCalled();
|
|
};
|
|
|
|
/**
|
|
* Tests that when the abort signal is aborted with an error, an ExponentialBackoffStoppedRetriesError is thrown
|
|
* with the error as the message.
|
|
*/
|
|
const testExponentialBackoffRunWithAbortSignal = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
// Define the function which aborts the exponential backoff and throws an error
|
|
const abortAndThrowFn = vi.fn(({ stopRetries }) => {
|
|
stopRetries(new Error('exponential backoff aborted'));
|
|
throw new Error('error message');
|
|
});
|
|
const onErrorFn = vi.fn();
|
|
|
|
// Define the expected error
|
|
const expectedError = new ExponentialBackoffStoppedRetriesError(new Error('exponential backoff aborted'));
|
|
|
|
// Run the exponential backoff with the function and the onError callback and expect the error to be thrown
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
};
|
|
|
|
await expect(exponentialBackoffFactory(options).run(abortAndThrowFn, { onError: onErrorFn })).rejects.toThrow(expectedError);
|
|
|
|
// Expect the onError callback to have been called once with the error
|
|
expect(onErrorFn).toHaveBeenCalledOnce();
|
|
expect(onErrorFn.mock.calls[0][0]).toBeInstanceOf(Error);
|
|
expect(onErrorFn.mock.calls[0][0].message).toBe('error message');
|
|
|
|
// Expect the function to have been called once and not to have resolved
|
|
expect(abortAndThrowFn).toHaveBeenCalledOnce();
|
|
expect(abortAndThrowFn).not.toHaveResolved();
|
|
};
|
|
|
|
/**
|
|
* Tests that when the abort signal is aborted with a string, an ExponentialBackoffStoppedRetriesError is thrown
|
|
* with the string as the message.
|
|
*/
|
|
const testExponentialBackoffRunAbortedStringCreatesError = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
// Define the function which aborts the exponential backoff and throws an error
|
|
const abortAndThrowStringFn = vi.fn(({ stopRetries }) => {
|
|
stopRetries('exponential backoff aborted');
|
|
|
|
// eslint-disable-next-line
|
|
throw 'error message';
|
|
});
|
|
const onErrorFn = vi.fn();
|
|
|
|
// Define the expected error, Note that we "stopRetries" with just a string, not an error. They are treated equivalently.
|
|
const expectedError = new ExponentialBackoffStoppedRetriesError(new Error('exponential backoff aborted'));
|
|
|
|
// Run the exponential backoff with the function and the onError callback and expect the error to be thrown
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
};
|
|
|
|
await expect(exponentialBackoffFactory(options).run(abortAndThrowStringFn, { onError: onErrorFn })).rejects.toThrow(expectedError);
|
|
|
|
// Expect the onError callback to have been called once with the error
|
|
expect(onErrorFn).toHaveBeenCalledOnce();
|
|
expect(onErrorFn.mock.calls[0][0]).toBeInstanceOf(Error);
|
|
expect(onErrorFn.mock.calls[0][0].message).toBe('error message');
|
|
|
|
// Expect the function to have been called once and not to have resolved
|
|
expect(abortAndThrowStringFn).toHaveBeenCalledOnce();
|
|
expect(abortAndThrowStringFn).not.toHaveResolved();
|
|
};
|
|
|
|
/**
|
|
* Tests that the delay is aborted when the abort signal is activated.
|
|
*/
|
|
const testExponentialBackoffSignalInterruptsDelay = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
|
|
try {
|
|
let abort: (reason: unknown) => void;
|
|
|
|
const taskFn = vi.fn(async ({ stopRetries }) => {
|
|
abort = stopRetries;
|
|
throw new Error('error message');
|
|
});
|
|
|
|
// Start the exponential backoff and wait for it to complete
|
|
const options = {
|
|
baseDelay: 1000,
|
|
jitter: 0,
|
|
maxAttempts: 0,
|
|
};
|
|
|
|
const result = exponentialBackoffFactory(options).run(taskFn, { onError: vi.fn() });
|
|
|
|
// Advance the timer by 500 (mid delay)
|
|
await vi.advanceTimersByTimeAsync(500);
|
|
|
|
// Make sure the abort function is defined (That the taskFn was called)
|
|
if (!abort!) {
|
|
throw new Error('abort is not defined');
|
|
}
|
|
|
|
// Check that the abort function is defined
|
|
expect(abort).toBeDefined();
|
|
|
|
// Abort the exponential backoff
|
|
abort?.(new Error('exponential backoff aborted'));
|
|
|
|
// Expect the result to be rejected with an ExponentialBackoffStoppedRetriesError
|
|
await expect(result).rejects.toThrow(ExponentialBackoffStoppedRetriesError);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Tests that the exponential backoff is cancelled if the abort signal was activated before run()
|
|
*/
|
|
const testExponentialBackoffAlreadyAbortedSignalPreventsRun = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
const abortController = new AbortController();
|
|
const abortSignal = abortController.signal;
|
|
abortController.abort(new Error('already aborted'));
|
|
|
|
const taskFn = vi.fn();
|
|
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
};
|
|
|
|
const promise = exponentialBackoffFactory(options).run(taskFn, { onError: vi.fn(), signal: abortSignal });
|
|
|
|
await expect(promise).rejects.toThrow(ExponentialBackoffStoppedRetriesError);
|
|
|
|
expect(taskFn).not.toHaveBeenCalled();
|
|
};
|
|
|
|
/**
|
|
* Tests that maxAttempts: 0 disables the attempt cap so retries continue until the function succeeds.
|
|
*/
|
|
const testExponentialBackoffRetriesIndefinitelyWhenMaxAttemptsIsZero = async (factory: ExponentialBackoffFactory): Promise<void> => {
|
|
// Four invocations: three failures then success — would exceed a cap of 3 if one existed.
|
|
const tripleRejectThenResolveFn = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error('attempt 1'))
|
|
.mockRejectedValueOnce(new Error('attempt 2'))
|
|
.mockRejectedValueOnce(new Error('attempt 3'))
|
|
.mockResolvedValueOnce('eventually');
|
|
|
|
const options = {
|
|
baseDelay: 0,
|
|
jitter: 0,
|
|
maxAttempts: 0,
|
|
};
|
|
|
|
const result = await factory(options).run(tripleRejectThenResolveFn, { onError: vi.fn() });
|
|
|
|
expect(result).toBe('eventually');
|
|
expect(tripleRejectThenResolveFn).toHaveBeenCalledTimes(4);
|
|
};
|
|
|
|
/**
|
|
* Tests the delay formula: each retry waits baseDelay * growthRate^attemptIndex milliseconds
|
|
* (with jitter disabled so the math is exact).
|
|
*/
|
|
const testExponentialBackoffIncreasesDelayExponentially = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
|
|
|
try {
|
|
const doubleRejectThenResolveFn = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error('attempt 1'))
|
|
.mockRejectedValueOnce(new Error('attempt 2'))
|
|
.mockResolvedValueOnce('success');
|
|
|
|
const options = {
|
|
baseDelay: 100,
|
|
growthRate: 2,
|
|
jitter: 0,
|
|
maxDelay: 10_000,
|
|
maxAttempts: 5,
|
|
};
|
|
|
|
const promise = exponentialBackoffFactory(options).run(doubleRejectThenResolveFn, { onError: vi.fn() });
|
|
|
|
// Attempt 0 fires synchronously on the first microtask tick.
|
|
await Promise.resolve();
|
|
expect(doubleRejectThenResolveFn).toHaveBeenCalledTimes(1);
|
|
|
|
// After attempt 0 fails, delay = 100 * 2^0 = 100ms before attempt 1.
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
expect(doubleRejectThenResolveFn).toHaveBeenCalledTimes(2);
|
|
|
|
// After attempt 1 fails, delay = 100 * 2^1 = 200ms before attempt 2.
|
|
await vi.advanceTimersByTimeAsync(200);
|
|
expect(doubleRejectThenResolveFn).toHaveBeenCalledTimes(3);
|
|
|
|
await expect(promise).resolves.toBe('success');
|
|
} finally {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Tests that computed delay never exceeds maxDelay even when exponential growth would go higher.
|
|
*/
|
|
const testExponentialBackoffCapsDelayAtMaxDelay = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
|
|
|
try {
|
|
const doubleRejectThenResolveFn = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error('attempt 1'))
|
|
.mockRejectedValueOnce(new Error('attempt 2'))
|
|
.mockResolvedValueOnce('success');
|
|
|
|
const options = {
|
|
baseDelay: 1_000,
|
|
growthRate: 4,
|
|
jitter: 0,
|
|
maxDelay: 2_000,
|
|
maxAttempts: 5,
|
|
};
|
|
|
|
const promise = exponentialBackoffFactory(options).run(doubleRejectThenResolveFn, { onError: vi.fn() });
|
|
|
|
await Promise.resolve();
|
|
expect(doubleRejectThenResolveFn).toHaveBeenCalledTimes(1);
|
|
|
|
// attempt 0: 1000 * 4^0 = 1000ms, below the 2000ms cap.
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
expect(doubleRejectThenResolveFn).toHaveBeenCalledTimes(2);
|
|
|
|
// attempt 1: uncapped would be 4000ms but maxDelay clamps to 2000ms.
|
|
await vi.advanceTimersByTimeAsync(2_000);
|
|
expect(doubleRejectThenResolveFn).toHaveBeenCalledTimes(3);
|
|
|
|
await expect(promise).resolves.toBe('success');
|
|
} finally {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Tests that jitter subtracts up to jitter * cappedDelay from the capped delay based on Math.random.
|
|
*/
|
|
const testExponentialBackoffAppliesJitter = async (exponentialBackoffFactory: ExponentialBackoffFactory): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
// random = 1 → full 10% reduction: 1000 - (1 * 0.1 * 1000) = 900ms.
|
|
vi.spyOn(Math, 'random').mockReturnValue(1);
|
|
|
|
try {
|
|
const rejectThenResolveFn = vi.fn().mockRejectedValueOnce(new Error('attempt 1'))
|
|
.mockResolvedValueOnce('success');
|
|
|
|
const options = {
|
|
baseDelay: 1_000,
|
|
growthRate: 1,
|
|
jitter: 0.1,
|
|
maxDelay: 10_000,
|
|
maxAttempts: 3,
|
|
};
|
|
|
|
const promise = exponentialBackoffFactory(options).run(rejectThenResolveFn, { onError: vi.fn() });
|
|
|
|
await Promise.resolve();
|
|
expect(rejectThenResolveFn).toHaveBeenCalledTimes(1);
|
|
|
|
// Advancing 899ms is one ms short of the jittered delay; 900ms triggers the retry.
|
|
await vi.advanceTimersByTimeAsync(899);
|
|
expect(rejectThenResolveFn).toHaveBeenCalledTimes(1);
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
expect(rejectThenResolveFn).toHaveBeenCalledTimes(2);
|
|
|
|
await expect(promise).resolves.toBe('success');
|
|
} finally {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Tests that passing undefined into the constructor does not cause an error during spread
|
|
*/
|
|
const testExponentialBackoffConstructorDoesNotCauseErrorDuringSpread = async (factory: ExponentialBackoffFactory): Promise<void> => {
|
|
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(() => factory(options)).toThrow(ExponentialBackoffNumberNotFiniteError);
|
|
};
|
|
|
|
export const testExponentialBackoff = (name: string, exponentialBackoffFactory: ExponentialBackoffFactory): void => {
|
|
describe(name, () => {
|
|
test('ExponentialBackoff: returns the result on first success', () =>
|
|
testExponentialBackoffSucceedsOnFirstAttempt(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: retries until the function succeeds', () => testExponentialBackoffRetriesUntilSuccess(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: calls onError for each failed attempt', () =>
|
|
testExponentialBackoffCallsOnErrorForEachFailure(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: throws ExponentialBackoffMaxRetriesHitError when max attempts are exhausted', () =>
|
|
testExponentialBackoffThrowsMaxRetriesHitErrorWhenExhausted(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: wraps non-Error throws before calling onError', () =>
|
|
testExponentialBackoffWrapsNonErrorThrows(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: succeeds and aborts with abort signal', () =>
|
|
testExponentialBackoffRunSuccessAndAbortSignal(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: aborts with abort signal', () => testExponentialBackoffRunWithAbortSignal(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: aborts with aborted string creates error', () =>
|
|
testExponentialBackoffRunAbortedStringCreatesError(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: aborts with abort signal, interrupts delay', () =>
|
|
testExponentialBackoffSignalInterruptsDelay(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: already aborted signal prevents run', () =>
|
|
testExponentialBackoffAlreadyAbortedSignalPreventsRun(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: retries indefinitely when maxAttempts is 0', () =>
|
|
testExponentialBackoffRetriesIndefinitelyWhenMaxAttemptsIsZero(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: increases delay exponentially between attempts', () =>
|
|
testExponentialBackoffIncreasesDelayExponentially(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: caps delay at maxDelay', () => testExponentialBackoffCapsDelayAtMaxDelay(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: subtracts jitter from the capped delay', () => testExponentialBackoffAppliesJitter(exponentialBackoffFactory));
|
|
test('ExponentialBackoff: constructor does not cause an error during spread', () =>
|
|
testExponentialBackoffConstructorDoesNotCauseErrorDuringSpread(exponentialBackoffFactory));
|
|
});
|
|
};
|