Files

90 lines
1.9 KiB
TypeScript

/**
* Incremental backoff with decorrelated jitter.
*
* Produces progressively longer delays after consecutive failures,
* capped at a configurable maximum. Uses decorrelated jitter
* (delay = random(base, prev * multiplier)) to prevent thundering-herd
* when many clients retry simultaneously after a shared outage.
*
* The backoff is a pure, stateful object with no timers or side effects.
* Call `next()` to get the next delay, and `reset()` after a success.
*/
export type BackoffOptions = {
/**
* Base delay in milliseconds.
* @default 5_000
*/
baseDelay?: number;
/**
* Maximum delay in milliseconds.
* @default 120_000
*/
maxDelay?: number;
/**
* Growth factor applied to the previous delay on each failure.
* @default 3
*/
multiplier?: number;
/**
* When true, randomize delays between baseDelay and previousDelay * multiplier.
* @default true
*/
jitter?: boolean;
};
export type Backoff = {
next: () => number;
reset: () => void;
attempts: () => number;
};
const DEFAULT_BASE_DELAY = 5_000;
const DEFAULT_MAX_DELAY = 120_000;
const DEFAULT_MULTIPLIER = 3;
const randomBetween = (min: number, max: number) => {
return Math.random() * (max - min) + min;
};
export const createBackoff = (options?: BackoffOptions): Backoff => {
const {
baseDelay = DEFAULT_BASE_DELAY,
jitter = true,
maxDelay = DEFAULT_MAX_DELAY,
multiplier = DEFAULT_MULTIPLIER,
} = options ?? {};
let failures = 0;
let lastDelay = baseDelay;
const next = () => {
failures += 1;
if (jitter) {
lastDelay = Math.min(
maxDelay,
randomBetween(baseDelay, lastDelay * multiplier)
);
return lastDelay;
}
lastDelay = Math.min(maxDelay, baseDelay * multiplier ** (failures - 1));
return lastDelay;
};
const reset = () => {
failures = 0;
lastDelay = baseDelay;
};
return {
attempts: () => failures,
next,
reset,
};
};