Merge branch 'exponential-backoff' into sse-and-backoff

This commit is contained in:
2026-08-07 05:48:29 +00:00
6 changed files with 384 additions and 235 deletions
+69 -5
View File
@@ -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<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(() => new ExponentialBackoff(options)).toThrow(ExponentialBackoffNumberNotFiniteError);
};
const runTests = async (): Promise<void> => {
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<void> => {
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();