Skip to content

Commit 4bcacde

Browse files
committed
adding src and test files
1 parent 08776d4 commit 4bcacde

6 files changed

+356
-1
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
dist/

package-lock.json

+130
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,11 @@
2020
"bugs": {
2121
"url": "https://github.com/coveo/exponential-backoff/issues"
2222
},
23-
"homepage": "https://github.com/coveo/exponential-backoff#readme"
23+
"homepage": "https://github.com/coveo/exponential-backoff#readme",
24+
"devDependencies": {
25+
"@types/jasmine": "^2.8.8",
26+
"@types/node": "^10.5.1",
27+
"jasmine": "^3.1.0",
28+
"typescript": "^2.9.2"
29+
}
2430
}

src/backoff.spec.ts

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { backOff, IBackOffRequest, IBackOffOptions } from './backoff';
2+
3+
export function BackOffTest() {
4+
describe('BackOff', () => {
5+
const mockSuccessResponse = { success: true };
6+
const mockFailResponse = { success: false };
7+
let backOffRequest: IBackOffRequest<{}>;
8+
let backOffOptions: Partial<IBackOffOptions>;
9+
10+
function initBackOff() {
11+
return backOff(backOffRequest, backOffOptions);
12+
}
13+
14+
function promiseThatIsResolved() {
15+
return () => Promise.resolve(mockSuccessResponse);
16+
}
17+
18+
function promiseThatIsRejected() {
19+
return () => Promise.reject(mockFailResponse);
20+
}
21+
22+
beforeEach(() => {
23+
backOffOptions = { startingDelay: 0 };
24+
backOffRequest = { fn: promiseThatIsResolved() };
25+
});
26+
27+
describe('when #backOffRequest.fn is a promise that resolves', () => {
28+
it('returns the resolved value', done => {
29+
const request = initBackOff();
30+
31+
request.then(response => {
32+
expect(response).toBe(mockSuccessResponse);
33+
done();
34+
});
35+
});
36+
37+
it('calls the #backOffRequest.fn only once', done => {
38+
spyOn(backOffRequest, 'fn');
39+
const request = initBackOff();
40+
41+
request.then(() => {
42+
expect(backOffRequest.fn).toHaveBeenCalledTimes(1);
43+
done();
44+
});
45+
});
46+
47+
it(`when the #backOffOptions.numOfAttempts is 0,
48+
it overrides the value and calls the method only once`, done => {
49+
backOffOptions.numOfAttempts = 0;
50+
spyOn(backOffRequest, 'fn');
51+
52+
const request = initBackOff();
53+
54+
request.then(() => {
55+
expect(backOffRequest.fn).toHaveBeenCalledTimes(1);
56+
done();
57+
});
58+
});
59+
});
60+
61+
describe('when #BackOffRequest.fn is a promise that is rejected', () => {
62+
beforeEach(() => (backOffRequest.fn = promiseThatIsRejected()));
63+
64+
it('returns the rejected value', done => {
65+
const request = initBackOff();
66+
request.catch(err => {
67+
expect(err).toBe(mockFailResponse);
68+
done();
69+
});
70+
});
71+
72+
it('retries the request as many times as specified in #BackOffOptions.numOfAttempts', done => {
73+
backOffOptions.numOfAttempts = 2;
74+
spyOn(backOffRequest, 'fn').and.returnValue(Promise.reject(mockFailResponse));
75+
76+
const request = initBackOff();
77+
78+
request.catch(() => {
79+
expect(backOffRequest.fn).toHaveBeenCalledTimes(backOffOptions.numOfAttempts as number);
80+
done();
81+
});
82+
});
83+
84+
it(`when the #BackOffRequest.retry method is set to always return false,
85+
it only calls #BackOffRequest.fn one time`, done => {
86+
backOffRequest.retry = () => false;
87+
backOffOptions.numOfAttempts = 2;
88+
spyOn(backOffRequest, 'fn').and.returnValue(Promise.reject(mockFailResponse));
89+
90+
const request = initBackOff();
91+
92+
request.catch(() => {
93+
expect(backOffRequest.fn).toHaveBeenCalledTimes(1);
94+
done();
95+
});
96+
});
97+
});
98+
99+
describe(`when calling #backOff with a function that throws an error the first time, and succeeds the second time`, () => {
100+
beforeEach(() => {
101+
spyOn(backOffRequest, 'fn').and.returnValues(Promise.reject(mockFailResponse), Promise.resolve(mockSuccessResponse));
102+
});
103+
104+
it(`returns a successful response`, done => {
105+
const request = initBackOff();
106+
107+
request.then(response => {
108+
expect(response).toBe(mockSuccessResponse);
109+
done();
110+
});
111+
});
112+
113+
it('calls the #BackOffRequest.fn method two times', done => {
114+
const request = initBackOff();
115+
116+
request.then(() => {
117+
expect(backOffRequest.fn).toHaveBeenCalledTimes(2);
118+
done();
119+
});
120+
});
121+
122+
it(`when setting the #BackOffOption.timeMultiple to a value,
123+
it applies a delay between the first and the second call`, done => {
124+
const startingDelay = 100;
125+
const timeMultiple = 3;
126+
const totalExpectedDelay = startingDelay + timeMultiple * startingDelay;
127+
128+
backOffOptions.startingDelay = startingDelay;
129+
backOffOptions.timeMultiple = timeMultiple;
130+
131+
const startTime = Date.now();
132+
const request = initBackOff();
133+
134+
request.then(() => {
135+
const endTime = Date.now();
136+
const duration = endTime - startTime;
137+
const roundedDuration = Math.round(duration / startingDelay) * startingDelay;
138+
139+
expect(roundedDuration).toBe(totalExpectedDelay);
140+
done();
141+
});
142+
});
143+
});
144+
});
145+
}

src/backoff.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export interface IBackOffRequest<T> {
2+
fn: () => Promise<T>;
3+
retry?: (e, attemptNumber: number) => boolean;
4+
}
5+
6+
export interface IBackOffOptions {
7+
numOfAttempts: number;
8+
timeMultiple: number;
9+
startingDelay: number;
10+
}
11+
12+
const defaultOptions: IBackOffOptions = {
13+
numOfAttempts: 10,
14+
timeMultiple: 2,
15+
startingDelay: 100
16+
};
17+
18+
export async function backOff<T>(request: IBackOffRequest<T>, options: Partial<IBackOffOptions> = {}): Promise<T> {
19+
const sanitizedOptions = getSanitizedOptions(options);
20+
21+
let attemptNumber = 0;
22+
let delay = sanitizedOptions.startingDelay;
23+
24+
while (attemptNumber < sanitizedOptions.numOfAttempts) {
25+
try {
26+
await delayBeforeExecuting(delay);
27+
attemptNumber++;
28+
return await request.fn();
29+
} catch (e) {
30+
const shouldRetry = request.retry ? request.retry(e, attemptNumber) : true;
31+
const reachedRetryLimit = attemptNumber >= sanitizedOptions.numOfAttempts;
32+
33+
if (!shouldRetry || reachedRetryLimit) {
34+
throw e;
35+
}
36+
37+
delay *= sanitizedOptions.timeMultiple;
38+
}
39+
}
40+
41+
throw new Error('Something went wrong.');
42+
}
43+
44+
function getSanitizedOptions(options: Partial<IBackOffOptions>) {
45+
const sanitized: IBackOffOptions = { ...defaultOptions, ...options };
46+
const numOfAttempts = sanitized.numOfAttempts;
47+
48+
if (numOfAttempts && numOfAttempts < 1) {
49+
sanitized.numOfAttempts = 1;
50+
}
51+
52+
return sanitized;
53+
}
54+
55+
function delayBeforeExecuting(delay: number) {
56+
return new Promise(resolve => setTimeout(resolve, delay));
57+
}

tsconfig.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"outDir": "./dist/",
4+
"sourceMap": true,
5+
"strictNullChecks": true,
6+
"module": "es6",
7+
"target": "es5",
8+
"lib": [
9+
"es2015"
10+
]
11+
},
12+
"include": [
13+
"./src/"
14+
]
15+
}

0 commit comments

Comments
 (0)