aboutsummaryrefslogtreecommitdiffstats
path: root/node_modules/exponential-backoff/src
diff options
context:
space:
mode:
authorpack <pack@packgekko.xyz>2026-08-09 10:37:07 +0000
committerpack <pack@packgekko.xyz>2026-08-09 10:37:07 +0000
commit55a4f1fc869e41aca748c63d3018f0448b1606e0 (patch)
treed132d1d01772b6d53faee3e63c534dea5706b78a /node_modules/exponential-backoff/src
downloadcrud-55a4f1fc869e41aca748c63d3018f0448b1606e0.tar.gz
first commit
Diffstat (limited to '')
-rw-r--r--node_modules/exponential-backoff/src/backoff.spec.ts194
-rw-r--r--node_modules/exponential-backoff/src/backoff.ts60
-rw-r--r--node_modules/exponential-backoff/src/delay/always/always.delay.spec.ts65
-rw-r--r--node_modules/exponential-backoff/src/delay/always/always.delay.ts3
-rw-r--r--node_modules/exponential-backoff/src/delay/delay.base.ts34
-rw-r--r--node_modules/exponential-backoff/src/delay/delay.factory.ts18
-rw-r--r--node_modules/exponential-backoff/src/delay/delay.interface.ts4
-rw-r--r--node_modules/exponential-backoff/src/delay/skip-first/skip-first.delay.ts15
-rw-r--r--node_modules/exponential-backoff/src/jitter/full/full.jitter.spec.ts31
-rw-r--r--node_modules/exponential-backoff/src/jitter/full/full.jitter.ts4
-rw-r--r--node_modules/exponential-backoff/src/jitter/jitter.factory.ts16
-rw-r--r--node_modules/exponential-backoff/src/jitter/no/no.jitter.spec.ts9
-rw-r--r--node_modules/exponential-backoff/src/jitter/no/no.jitter.ts3
-rw-r--r--node_modules/exponential-backoff/src/options.ts76
14 files changed, 532 insertions, 0 deletions
diff --git a/node_modules/exponential-backoff/src/backoff.spec.ts b/node_modules/exponential-backoff/src/backoff.spec.ts
new file mode 100644
index 0000000..24fd95b
--- /dev/null
+++ b/node_modules/exponential-backoff/src/backoff.spec.ts
@@ -0,0 +1,194 @@
+import { backOff } from "./backoff";
+import { BackoffOptions } from "./options";
+
+describe("BackOff", () => {
+ const mockSuccessResponse = { success: true };
+ const mockFailResponse = { success: false };
+ let backOffRequest: () => Promise<any>;
+ let backOffOptions: BackoffOptions;
+
+ function initBackOff() {
+ return backOff(backOffRequest, backOffOptions);
+ }
+
+ function promiseThatIsResolved() {
+ return () => Promise.resolve(mockSuccessResponse);
+ }
+
+ function promiseThatIsRejected() {
+ return () => Promise.reject(mockFailResponse);
+ }
+
+ function promiseThatFailsOnceThenSucceeds() {
+ return (() => {
+ let firstAttempt = true;
+
+ const request = () => {
+ if (firstAttempt) {
+ firstAttempt = false;
+ return Promise.reject(mockFailResponse);
+ }
+
+ return Promise.resolve(mockSuccessResponse);
+ };
+
+ return request;
+ })();
+ }
+
+ beforeEach(() => {
+ backOffOptions = { startingDelay: 0 };
+ backOffRequest = jest.fn(promiseThatIsResolved());
+ });
+
+ describe("when request function is a promise that resolves", () => {
+ it("returns the resolved value", () => {
+ const request = initBackOff();
+ return request.then(response =>
+ expect(response).toBe(mockSuccessResponse)
+ );
+ });
+
+ it("calls the request function only once", () => {
+ const request = initBackOff();
+ return request.then(() =>
+ expect(backOffRequest).toHaveBeenCalledTimes(1)
+ );
+ });
+
+ it(`when the #backOffOptions.numOfAttempts is 0,
+ it overrides the value and calls the method only once`, () => {
+ backOffOptions.numOfAttempts = 0;
+ const request = initBackOff();
+
+ return request.then(() =>
+ expect(backOffRequest).toHaveBeenCalledTimes(1)
+ );
+ });
+ });
+
+ describe(`when the #backOffOptions.startingDelay is 100ms`, () => {
+ const startingDelay = 100;
+
+ beforeEach(() => (backOffOptions.startingDelay = startingDelay));
+
+ it(`does not delay the first attempt`, () => {
+ const startTime = Date.now();
+ const request = initBackOff();
+
+ return request.then(() => {
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+ const roundedDuration = Math.round(duration / 100) * 100;
+
+ expect(roundedDuration).toBe(0);
+ });
+ });
+
+ it(`when #backOffOptions.delayFirstAttempt is 'true',
+ it delays the first attempt`, () => {
+ backOffOptions.delayFirstAttempt = true;
+ const startTime = Date.now();
+ const request = initBackOff();
+
+ return request.then(() => {
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+ const roundedDuration = Math.round(duration / 100) * 100;
+
+ expect(roundedDuration).toBe(startingDelay);
+ });
+ });
+ });
+
+ describe("when request function is a promise that is rejected", () => {
+ beforeEach(() => (backOffRequest = promiseThatIsRejected()));
+
+ it("returns the rejected value", () => {
+ const request = initBackOff();
+ return request.catch(err => expect(err).toBe(mockFailResponse));
+ });
+
+ it("retries the request as many times as specified in #BackOffOptions.numOfAttempts", async () => {
+ const numOfAttemps = 2;
+ backOffOptions.numOfAttempts = numOfAttemps;
+ backOffRequest = jest.fn(() => Promise.reject(mockFailResponse));
+
+ try {
+ await initBackOff();
+ } catch {
+ expect(backOffRequest).toHaveBeenCalledTimes(numOfAttemps);
+ }
+ });
+
+ it(`when the #BackOffOptions.retry function is set to always return false,
+ it only calls request function one time`, async () => {
+ backOffOptions.retry = () => false;
+ backOffOptions.numOfAttempts = 2;
+ backOffRequest = jest.fn(() => Promise.reject(mockFailResponse));
+
+ try {
+ await initBackOff();
+ } catch {
+ expect(backOffRequest).toHaveBeenCalledTimes(1);
+ }
+ });
+ });
+
+ it("when the #BackOffOptions.retry function returns a promise, it awaits it", async () => {
+ const retryDuration = 100;
+ backOffOptions.retry = () =>
+ new Promise(resolve => setTimeout(() => resolve(true), retryDuration));
+ backOffRequest = promiseThatFailsOnceThenSucceeds();
+
+ const start = Date.now();
+ await initBackOff();
+ const end = Date.now();
+
+ const duration = end - start;
+ const roundedDuration =
+ Math.round(duration / retryDuration) * retryDuration;
+
+ expect(roundedDuration).toBe(retryDuration);
+ });
+
+ describe(`when calling #backOff with a function that throws an error the first time, and succeeds the second time`, () => {
+ beforeEach(
+ () => (backOffRequest = jest.fn(promiseThatFailsOnceThenSucceeds()))
+ );
+
+ it(`returns a successful response`, () => {
+ const request = initBackOff();
+ return request.then(response =>
+ expect(response).toBe(mockSuccessResponse)
+ );
+ });
+
+ it("calls the request function two times", async () => {
+ await initBackOff();
+ expect(backOffRequest).toHaveBeenCalledTimes(2);
+ });
+
+ it(`when setting the #BackOffOption.timeMultiple to a value,
+ when setting the #BackOffOption.delayFirstAttempt to true,
+ it applies a delay between the first and the second call`, async () => {
+ const startingDelay = 100;
+ const timeMultiple = 3;
+ const totalExpectedDelay = startingDelay + timeMultiple * startingDelay;
+
+ backOffOptions.startingDelay = startingDelay;
+ backOffOptions.timeMultiple = timeMultiple;
+ backOffOptions.delayFirstAttempt = true;
+
+ const start = Date.now();
+ await initBackOff();
+ const end = Date.now();
+
+ const duration = end - start;
+ const roundedDuration =
+ Math.round(duration / startingDelay) * startingDelay;
+
+ expect(roundedDuration).toBe(totalExpectedDelay);
+ });
+ });
+});
diff --git a/node_modules/exponential-backoff/src/backoff.ts b/node_modules/exponential-backoff/src/backoff.ts
new file mode 100644
index 0000000..549770b
--- /dev/null
+++ b/node_modules/exponential-backoff/src/backoff.ts
@@ -0,0 +1,60 @@
+import {
+ IBackOffOptions,
+ getSanitizedOptions,
+ BackoffOptions
+} from "./options";
+import { DelayFactory } from "./delay/delay.factory";
+
+export { BackoffOptions, IBackOffOptions };
+
+/**
+ * Executes a function with exponential backoff.
+ * @param request the function to be executed
+ * @param options options to customize the backoff behavior
+ * @returns Promise that resolves to the result of the `request` function
+ */
+export async function backOff<T>(
+ request: () => Promise<T>,
+ options: BackoffOptions = {}
+): Promise<T> {
+ const sanitizedOptions = getSanitizedOptions(options);
+ const backOff = new BackOff(request, sanitizedOptions);
+
+ return await backOff.execute();
+}
+
+class BackOff<T> {
+ private attemptNumber = 0;
+
+ constructor(
+ private request: () => Promise<T>,
+ private options: IBackOffOptions
+ ) {}
+
+ public async execute(): Promise<T> {
+ while (!this.attemptLimitReached) {
+ try {
+ await this.applyDelay();
+ return await this.request();
+ } catch (e) {
+ this.attemptNumber++;
+ const shouldRetry = await this.options.retry(e, this.attemptNumber);
+
+ if (!shouldRetry || this.attemptLimitReached) {
+ throw e;
+ }
+ }
+ }
+
+ throw new Error("Something went wrong.");
+ }
+
+ private get attemptLimitReached() {
+ return this.attemptNumber >= this.options.numOfAttempts;
+ }
+
+ private async applyDelay() {
+ const delay = DelayFactory(this.options, this.attemptNumber);
+ await delay.apply();
+ }
+}
diff --git a/node_modules/exponential-backoff/src/delay/always/always.delay.spec.ts b/node_modules/exponential-backoff/src/delay/always/always.delay.spec.ts
new file mode 100644
index 0000000..8a08cd8
--- /dev/null
+++ b/node_modules/exponential-backoff/src/delay/always/always.delay.spec.ts
@@ -0,0 +1,65 @@
+import { AlwaysDelay } from "./always.delay";
+import { IBackOffOptions, getSanitizedOptions } from "../../options";
+
+describe(AlwaysDelay.name, () => {
+ let options: IBackOffOptions;
+ let delay: AlwaysDelay;
+
+ function initClass() {
+ delay = new AlwaysDelay(options);
+ }
+
+ beforeEach(() => {
+ options = getSanitizedOptions({});
+ initClass();
+ jest.useFakeTimers();
+ });
+
+ it(`when calling #apply, the delay is equal to the starting delay`, async () => {
+ const spy = jest.fn();
+ delay.apply().then(spy);
+ jest.runTimersToTime(options.startingDelay);
+ await Promise.resolve();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it(`when the attempt number is 1, when calling #apply,
+ the delay is equal to the starting delay multiplied by the time multiple`, async () => {
+ delay.setAttemptNumber(1);
+
+ const spy = jest.fn();
+ delay.apply().then(spy);
+ jest.runTimersToTime(options.startingDelay * options.timeMultiple);
+ await Promise.resolve();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it(`when the attempt number is 2, when calling #apply,
+ the delay is equal to the starting delay multiplied by the time multiple raised by the attempt number`, async () => {
+ const attemptNumber = 2;
+ delay.setAttemptNumber(attemptNumber);
+
+ const spy = jest.fn();
+ delay.apply().then(spy);
+ jest.runTimersToTime(
+ options.startingDelay * Math.pow(options.timeMultiple, attemptNumber)
+ );
+ await Promise.resolve();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it(`when the #maxDelay is less than #startingDelay, when calling #apply,
+ the delay is equal to the #maxDelay`, async () => {
+ options.maxDelay = options.startingDelay - 1;
+
+ const spy = jest.fn();
+ delay.apply().then(spy);
+ jest.runTimersToTime(options.maxDelay);
+ await Promise.resolve();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/node_modules/exponential-backoff/src/delay/always/always.delay.ts b/node_modules/exponential-backoff/src/delay/always/always.delay.ts
new file mode 100644
index 0000000..ec86c62
--- /dev/null
+++ b/node_modules/exponential-backoff/src/delay/always/always.delay.ts
@@ -0,0 +1,3 @@
+import { Delay } from "../delay.base";
+
+export class AlwaysDelay extends Delay {} \ No newline at end of file
diff --git a/node_modules/exponential-backoff/src/delay/delay.base.ts b/node_modules/exponential-backoff/src/delay/delay.base.ts
new file mode 100644
index 0000000..bed90d6
--- /dev/null
+++ b/node_modules/exponential-backoff/src/delay/delay.base.ts
@@ -0,0 +1,34 @@
+import { IDelay } from "./delay.interface";
+import { IBackOffOptions } from "../options";
+import { JitterFactory } from "../jitter/jitter.factory";
+
+export abstract class Delay implements IDelay {
+ protected attempt = 0;
+ constructor(private options: IBackOffOptions) {}
+
+ public apply() {
+ return new Promise(resolve => setTimeout(resolve, this.jitteredDelay));
+ }
+
+ public setAttemptNumber(attempt: number) {
+ this.attempt = attempt;
+ }
+
+ private get jitteredDelay() {
+ const jitter = JitterFactory(this.options);
+ return jitter(this.delay);
+ }
+
+ private get delay() {
+ const constant = this.options.startingDelay;
+ const base = this.options.timeMultiple;
+ const power = this.numOfDelayedAttempts;
+ const delay = constant * Math.pow(base, power);
+
+ return Math.min(delay, this.options.maxDelay);
+ }
+
+ protected get numOfDelayedAttempts() {
+ return this.attempt;
+ }
+}
diff --git a/node_modules/exponential-backoff/src/delay/delay.factory.ts b/node_modules/exponential-backoff/src/delay/delay.factory.ts
new file mode 100644
index 0000000..95e89fb
--- /dev/null
+++ b/node_modules/exponential-backoff/src/delay/delay.factory.ts
@@ -0,0 +1,18 @@
+import { IBackOffOptions } from "../options";
+import { SkipFirstDelay } from "./skip-first/skip-first.delay";
+import { AlwaysDelay } from "./always/always.delay";
+import { IDelay } from "./delay.interface";
+
+export function DelayFactory(options: IBackOffOptions, attempt: number): IDelay {
+ const delay = initDelayClass(options);
+ delay.setAttemptNumber(attempt);
+ return delay;
+}
+
+function initDelayClass(options: IBackOffOptions) {
+ if (!options.delayFirstAttempt) {
+ return new SkipFirstDelay(options);
+ }
+
+ return new AlwaysDelay(options);
+} \ No newline at end of file
diff --git a/node_modules/exponential-backoff/src/delay/delay.interface.ts b/node_modules/exponential-backoff/src/delay/delay.interface.ts
new file mode 100644
index 0000000..6ecc8da
--- /dev/null
+++ b/node_modules/exponential-backoff/src/delay/delay.interface.ts
@@ -0,0 +1,4 @@
+export interface IDelay {
+ apply: () => Promise<unknown>;
+ setAttemptNumber: (attempt: number) => void;
+}
diff --git a/node_modules/exponential-backoff/src/delay/skip-first/skip-first.delay.ts b/node_modules/exponential-backoff/src/delay/skip-first/skip-first.delay.ts
new file mode 100644
index 0000000..9f2b7f5
--- /dev/null
+++ b/node_modules/exponential-backoff/src/delay/skip-first/skip-first.delay.ts
@@ -0,0 +1,15 @@
+import { Delay } from "../delay.base";
+
+export class SkipFirstDelay extends Delay {
+ public async apply() {
+ return this.isFirstAttempt ? true : super.apply();
+ }
+
+ private get isFirstAttempt() {
+ return this.attempt === 0;
+ }
+
+ protected get numOfDelayedAttempts() {
+ return this.attempt - 1;
+ }
+} \ No newline at end of file
diff --git a/node_modules/exponential-backoff/src/jitter/full/full.jitter.spec.ts b/node_modules/exponential-backoff/src/jitter/full/full.jitter.spec.ts
new file mode 100644
index 0000000..003d08d
--- /dev/null
+++ b/node_modules/exponential-backoff/src/jitter/full/full.jitter.spec.ts
@@ -0,0 +1,31 @@
+import { fullJitter } from './full.jitter';
+
+describe(`Testing ${fullJitter.name}`, () => {
+ const delay = 100;
+
+ function arrayWith5FullJitterDelays() {
+ return Array(5).fill(delay).map(fullJitter)
+ }
+
+ describe(`when calling #fullJitter on the same delay multiple times`, () => {
+ it('all the delays are greater than or equal to 0', () => {
+ arrayWith5FullJitterDelays()
+ .forEach(value => expect(value).toBeGreaterThanOrEqual(0));
+ })
+
+ it('all the delays are less than or equal to the original delay', () => {
+ arrayWith5FullJitterDelays()
+ .forEach(value => expect(value).toBeLessThanOrEqual(delay));
+ })
+
+ it('the delays are not equal to one another', () => {
+ const delays = arrayWith5FullJitterDelays();
+ expect(new Set(delays).size).not.toBe(1);
+ })
+
+ it('the delays are integers', () => {
+ arrayWith5FullJitterDelays()
+ .forEach(value => expect(Number.isInteger(value)).toBe(true))
+ })
+ })
+}) \ No newline at end of file
diff --git a/node_modules/exponential-backoff/src/jitter/full/full.jitter.ts b/node_modules/exponential-backoff/src/jitter/full/full.jitter.ts
new file mode 100644
index 0000000..b3c447b
--- /dev/null
+++ b/node_modules/exponential-backoff/src/jitter/full/full.jitter.ts
@@ -0,0 +1,4 @@
+export function fullJitter(delay: number) {
+ const jitteredDelay = Math.random() * delay;
+ return Math.round(jitteredDelay);
+} \ No newline at end of file
diff --git a/node_modules/exponential-backoff/src/jitter/jitter.factory.ts b/node_modules/exponential-backoff/src/jitter/jitter.factory.ts
new file mode 100644
index 0000000..b549887
--- /dev/null
+++ b/node_modules/exponential-backoff/src/jitter/jitter.factory.ts
@@ -0,0 +1,16 @@
+import { IBackOffOptions } from "../options";
+import { fullJitter } from "./full/full.jitter";
+import { noJitter } from "./no/no.jitter";
+
+export type Jitter = (delay: number) => number;
+
+export function JitterFactory(options: IBackOffOptions): Jitter {
+ switch (options.jitter) {
+ case "full":
+ return fullJitter;
+
+ case "none":
+ default:
+ return noJitter;
+ }
+}
diff --git a/node_modules/exponential-backoff/src/jitter/no/no.jitter.spec.ts b/node_modules/exponential-backoff/src/jitter/no/no.jitter.spec.ts
new file mode 100644
index 0000000..1d56524
--- /dev/null
+++ b/node_modules/exponential-backoff/src/jitter/no/no.jitter.spec.ts
@@ -0,0 +1,9 @@
+import { noJitter } from './no.jitter';
+
+describe(`Testing ${noJitter.name}`, () => {
+ it(`when calling #noJitter with a delay,
+ it returns the same delay`, () => {
+ const delay = 100;
+ expect(noJitter(delay)).toBe(delay);
+ })
+}) \ No newline at end of file
diff --git a/node_modules/exponential-backoff/src/jitter/no/no.jitter.ts b/node_modules/exponential-backoff/src/jitter/no/no.jitter.ts
new file mode 100644
index 0000000..bc1b56f
--- /dev/null
+++ b/node_modules/exponential-backoff/src/jitter/no/no.jitter.ts
@@ -0,0 +1,3 @@
+export function noJitter(delay: number) {
+ return delay;
+} \ No newline at end of file
diff --git a/node_modules/exponential-backoff/src/options.ts b/node_modules/exponential-backoff/src/options.ts
new file mode 100644
index 0000000..ccdee3a
--- /dev/null
+++ b/node_modules/exponential-backoff/src/options.ts
@@ -0,0 +1,76 @@
+/**
+ * Type of jitter to apply to the delay.
+ * - `"none"`: no jitter is applied
+ * - `"full"`: full jitter is applied (random value between `0` and `delay`)
+ */
+export type JitterType = "none" | "full";
+
+export type BackoffOptions = Partial<IBackOffOptions>;
+
+export interface IBackOffOptions {
+ /**
+ * Decides whether the `startingDelay` should be applied before the first call.
+ * If `false`, the first call will occur without a delay.
+ * @defaultValue `false`
+ */
+ delayFirstAttempt: boolean;
+ /**
+ * Decides whether a [jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)
+ * should be applied to the delay. Possible values are `"full"` and `"none"`.
+ * @defaultValue `"none"`
+ */
+ jitter: JitterType;
+ /**
+ * The maximum delay, in milliseconds, between two consecutive attempts.
+ * @defaultValue `Infinity`
+ */
+ maxDelay: number;
+ /**
+ * The maximum number of times to attempt the function.
+ * Must be at least `1`.
+ * @defaultValue `10`
+ */
+ numOfAttempts: number;
+ /**
+ * The `retry` function can be used to run logic after every failed attempt (e.g. logging a message,
+ * assessing the last error, etc.).
+ * It is called with the last error and the upcoming attempt number.
+ * Returning `true` will retry the function as long as the `numOfAttempts` has not been exceeded.
+ * Returning `false` will end the execution.
+ * @defaultValue a function that always returns `true`.
+ * @param e The last error thrown by the function.
+ * @param attemptNumber The upcoming attempt number.
+ * @returns `true` to retry the function, `false` to end the execution
+ */
+ retry: (e: any, attemptNumber: number) => boolean | Promise<boolean>;
+ /**
+ * The delay, in milliseconds, before executing the function for the first time.
+ * @defaultValue `100`
+ */
+ startingDelay: number;
+ /**
+ * The `startingDelay` is multiplied by the `timeMultiple` to increase the delay between reattempts.
+ * @defaultValue `2`
+ */
+ timeMultiple: number;
+}
+
+const defaultOptions: IBackOffOptions = {
+ delayFirstAttempt: false,
+ jitter: "none",
+ maxDelay: Infinity,
+ numOfAttempts: 10,
+ retry: () => true,
+ startingDelay: 100,
+ timeMultiple: 2
+};
+
+export function getSanitizedOptions(options: BackoffOptions) {
+ const sanitized: IBackOffOptions = { ...defaultOptions, ...options };
+
+ if (sanitized.numOfAttempts < 1) {
+ sanitized.numOfAttempts = 1;
+ }
+
+ return sanitized;
+}