main funcions fixes
This commit is contained in:
194
desktop-operator/node_modules/exponential-backoff/src/backoff.spec.ts
generated
vendored
Normal file
194
desktop-operator/node_modules/exponential-backoff/src/backoff.spec.ts
generated
vendored
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
60
desktop-operator/node_modules/exponential-backoff/src/backoff.ts
generated
vendored
Normal file
60
desktop-operator/node_modules/exponential-backoff/src/backoff.ts
generated
vendored
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
65
desktop-operator/node_modules/exponential-backoff/src/delay/always/always.delay.spec.ts
generated
vendored
Normal file
65
desktop-operator/node_modules/exponential-backoff/src/delay/always/always.delay.spec.ts
generated
vendored
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
3
desktop-operator/node_modules/exponential-backoff/src/delay/always/always.delay.ts
generated
vendored
Normal file
3
desktop-operator/node_modules/exponential-backoff/src/delay/always/always.delay.ts
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Delay } from "../delay.base";
|
||||
|
||||
export class AlwaysDelay extends Delay {}
|
||||
34
desktop-operator/node_modules/exponential-backoff/src/delay/delay.base.ts
generated
vendored
Normal file
34
desktop-operator/node_modules/exponential-backoff/src/delay/delay.base.ts
generated
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
18
desktop-operator/node_modules/exponential-backoff/src/delay/delay.factory.ts
generated
vendored
Normal file
18
desktop-operator/node_modules/exponential-backoff/src/delay/delay.factory.ts
generated
vendored
Normal file
@@ -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);
|
||||
}
|
||||
4
desktop-operator/node_modules/exponential-backoff/src/delay/delay.interface.ts
generated
vendored
Normal file
4
desktop-operator/node_modules/exponential-backoff/src/delay/delay.interface.ts
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IDelay {
|
||||
apply: () => Promise<unknown>;
|
||||
setAttemptNumber: (attempt: number) => void;
|
||||
}
|
||||
15
desktop-operator/node_modules/exponential-backoff/src/delay/skip-first/skip-first.delay.ts
generated
vendored
Normal file
15
desktop-operator/node_modules/exponential-backoff/src/delay/skip-first/skip-first.delay.ts
generated
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
31
desktop-operator/node_modules/exponential-backoff/src/jitter/full/full.jitter.spec.ts
generated
vendored
Normal file
31
desktop-operator/node_modules/exponential-backoff/src/jitter/full/full.jitter.spec.ts
generated
vendored
Normal file
@@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
4
desktop-operator/node_modules/exponential-backoff/src/jitter/full/full.jitter.ts
generated
vendored
Normal file
4
desktop-operator/node_modules/exponential-backoff/src/jitter/full/full.jitter.ts
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export function fullJitter(delay: number) {
|
||||
const jitteredDelay = Math.random() * delay;
|
||||
return Math.round(jitteredDelay);
|
||||
}
|
||||
16
desktop-operator/node_modules/exponential-backoff/src/jitter/jitter.factory.ts
generated
vendored
Normal file
16
desktop-operator/node_modules/exponential-backoff/src/jitter/jitter.factory.ts
generated
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
9
desktop-operator/node_modules/exponential-backoff/src/jitter/no/no.jitter.spec.ts
generated
vendored
Normal file
9
desktop-operator/node_modules/exponential-backoff/src/jitter/no/no.jitter.spec.ts
generated
vendored
Normal file
@@ -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);
|
||||
})
|
||||
})
|
||||
3
desktop-operator/node_modules/exponential-backoff/src/jitter/no/no.jitter.ts
generated
vendored
Normal file
3
desktop-operator/node_modules/exponential-backoff/src/jitter/no/no.jitter.ts
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export function noJitter(delay: number) {
|
||||
return delay;
|
||||
}
|
||||
76
desktop-operator/node_modules/exponential-backoff/src/options.ts
generated
vendored
Normal file
76
desktop-operator/node_modules/exponential-backoff/src/options.ts
generated
vendored
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user