promise-again
Advanced tools
Comparing version 1.2.0 to 1.3.0
export interface IOptions { | ||
delay?: number | ((attempt: number, ...args: any[]) => number); | ||
attempts: number | ((attempt: number, ...args: any[]) => boolean); | ||
retryArgumentsInterceptor?: (reason: any, attempt: number, ...args: any[]) => any[]; | ||
delay?: number | ((reason: any, attempt: number, ...args: any[]) => number | Promise<number>); | ||
attempts: number | ((reason: any, attempt: number, ...args: any[]) => boolean | Promise<boolean>); | ||
retryArgumentsInterceptor?: (reason: any, attempt: number, ...args: any[]) => any[] | Promise<any[]>; | ||
} | ||
export default function promiseAgain<T>(func: (...args: any[]) => Promise<T>, options: IOptions): (...args: any[]) => Promise<T>; |
@@ -23,3 +23,3 @@ import * as promiseDelay from 'delay'; | ||
else { | ||
shouldRetry = options.attempts.apply(options, [usedAttempts].concat(innerArgs)); | ||
shouldRetry = options.attempts.apply(options, [reason, usedAttempts].concat(innerArgs)); | ||
} | ||
@@ -31,12 +31,15 @@ var nextDelay = 0; | ||
else if (typeof options.delay === 'function') { | ||
nextDelay = options.delay.apply(options, [usedAttempts].concat(innerArgs)); | ||
nextDelay = options.delay.apply(options, [reason, usedAttempts].concat(innerArgs)); | ||
} | ||
if (shouldRetry) { | ||
return promiseDelay(nextDelay) | ||
.then(function () { return attempt.apply(void 0, newArguments); }) | ||
.catch(function (subReason) { return Promise.reject(subReason); }); | ||
} | ||
else { | ||
return Promise.reject(reason); | ||
} | ||
return Promise.all([shouldRetry, nextDelay, newArguments]).then(function (_a) { | ||
var resolvedShouldRetry = _a[0], resolvedNextDelay = _a[1], _b = _a[2], resolvedNewArguments = _b === void 0 ? [] : _b; | ||
if (resolvedShouldRetry) { | ||
return promiseDelay(resolvedNextDelay) | ||
.then(function () { return attempt.apply(void 0, resolvedNewArguments); }) | ||
.catch(function (subReason) { return Promise.reject(subReason); }); | ||
} | ||
else { | ||
return Promise.reject(reason); | ||
} | ||
}); | ||
}); | ||
@@ -43,0 +46,0 @@ }.apply(void 0, args); |
export interface IOptions { | ||
delay?: number | ((attempt: number, ...args: any[]) => number); | ||
attempts: number | ((attempt: number, ...args: any[]) => boolean); | ||
retryArgumentsInterceptor?: (reason: any, attempt: number, ...args: any[]) => any[]; | ||
delay?: number | ((reason: any, attempt: number, ...args: any[]) => number | Promise<number>); | ||
attempts: number | ((reason: any, attempt: number, ...args: any[]) => boolean | Promise<boolean>); | ||
retryArgumentsInterceptor?: (reason: any, attempt: number, ...args: any[]) => any[] | Promise<any[]>; | ||
} | ||
export default function promiseAgain<T>(func: (...args: any[]) => Promise<T>, options: IOptions): (...args: any[]) => Promise<T>; |
@@ -34,3 +34,3 @@ (function (factory) { | ||
else { | ||
shouldRetry = options.attempts.apply(options, [usedAttempts].concat(innerArgs)); | ||
shouldRetry = options.attempts.apply(options, [reason, usedAttempts].concat(innerArgs)); | ||
} | ||
@@ -42,12 +42,15 @@ var nextDelay = 0; | ||
else if (typeof options.delay === 'function') { | ||
nextDelay = options.delay.apply(options, [usedAttempts].concat(innerArgs)); | ||
nextDelay = options.delay.apply(options, [reason, usedAttempts].concat(innerArgs)); | ||
} | ||
if (shouldRetry) { | ||
return promiseDelay(nextDelay) | ||
.then(function () { return attempt.apply(void 0, newArguments); }) | ||
.catch(function (subReason) { return Promise.reject(subReason); }); | ||
} | ||
else { | ||
return Promise.reject(reason); | ||
} | ||
return Promise.all([shouldRetry, nextDelay, newArguments]).then(function (_a) { | ||
var resolvedShouldRetry = _a[0], resolvedNextDelay = _a[1], _b = _a[2], resolvedNewArguments = _b === void 0 ? [] : _b; | ||
if (resolvedShouldRetry) { | ||
return promiseDelay(resolvedNextDelay) | ||
.then(function () { return attempt.apply(void 0, resolvedNewArguments); }) | ||
.catch(function (subReason) { return Promise.reject(subReason); }); | ||
} | ||
else { | ||
return Promise.reject(reason); | ||
} | ||
}); | ||
}); | ||
@@ -54,0 +57,0 @@ }.apply(void 0, args); |
@@ -7,3 +7,31 @@ import {expect} from 'chai'; | ||
function freePromisesQueue(clock: sinon.SinonFakeTimers, n: number = 10) { | ||
return () => { | ||
let result = promiseDelay(0); | ||
for (let i = 0; i < n; i++) { | ||
result = result.then(() => { const pr = promiseDelay(0); clock.tick(0); return pr; }); | ||
} | ||
clock.tick(0); | ||
return result; | ||
}; | ||
} | ||
function promiseDelayAndFreeQueue(delay: number, clock: sinon.SinonFakeTimers, n: number = 10): Promise<any> { | ||
return freePromisesQueue(clock, n)().then(promiseDelay(delay)).then(freePromisesQueue(clock, n)); | ||
} | ||
describe('promiseAgain', () => { | ||
let clock: sinon.SinonFakeTimers; | ||
beforeEach(() => { | ||
clock = sinon.useFakeTimers(); | ||
}); | ||
afterEach(() => { | ||
clock.restore(); | ||
}); | ||
it('should get value using attempts', (done) => { | ||
@@ -21,2 +49,4 @@ const func = sinon.stub(); | ||
}); | ||
freePromisesQueue(clock)(); | ||
}); | ||
@@ -38,4 +68,85 @@ | ||
}); | ||
freePromisesQueue(clock)(); | ||
}); | ||
it('should use attempts function to decide if retry needed', (done) => { | ||
const func = sinon.stub(); | ||
func.onCall(0).rejects('Some reason 1'); | ||
func.onCall(1).rejects('Some reason 2'); | ||
func.onCall(2).resolves('Needed value'); | ||
const attempts = sinon.stub(); | ||
attempts.onCall(0).returns(true); | ||
attempts.onCall(1).returns(false); | ||
const composedFunction = promiseAgain(func, {attempts}); | ||
composedFunction().then(() => { | ||
throw new Error('Should not succeed'); | ||
}).catch((reason) => { | ||
expect(reason.toString()).to.equal('Some reason 2'); | ||
done(); | ||
}); | ||
freePromisesQueue(clock)(); | ||
}); | ||
it('should use attempts function returning a promise to decide if retry needed', (done) => { | ||
const func = sinon.stub(); | ||
func.onCall(0).rejects('Some reason 1'); | ||
func.onCall(1).rejects('Some reason 2'); | ||
func.onCall(2).resolves('Needed value'); | ||
const attempts = sinon.stub(); | ||
attempts.onCall(0).resolves(true); | ||
attempts.onCall(1).resolves(false); | ||
const composedFunction = promiseAgain(func, {attempts}); | ||
composedFunction().then(() => { | ||
throw new Error('Should not succeed'); | ||
}).catch((reason) => { | ||
expect(reason.toString()).to.equal('Some reason 2'); | ||
done(); | ||
}); | ||
freePromisesQueue(clock)(); | ||
}); | ||
it('should call attempts dunction with reason, sequentially changing args and attempts count', (done) => { | ||
const func = sinon.stub(); | ||
func.onCall(0).rejects('Some reason 1'); | ||
func.onCall(1).rejects('Some reason 2'); | ||
func.onCall(2).resolves('Needed value'); | ||
const attempts = sinon.stub(); | ||
attempts.onCall(0).returns(true); | ||
attempts.onCall(1).returns(true); | ||
const composedFunction = promiseAgain(func, {attempts}); | ||
composedFunction(1, 2, 3, 'some arg').then(() => { | ||
expect(attempts.callCount).to.equal(2); | ||
expect( | ||
attempts.calledWithExactly( | ||
sinon.match({name: 'Some reason 1'}), 1, 1, 2, 3, 'some arg', | ||
), | ||
).to.equal(true); | ||
expect( | ||
attempts.calledWithExactly( | ||
sinon.match({name: 'Some reason 2'}), 2, 1, 2, 3, 'some arg', | ||
), | ||
).to.equal(true); | ||
done(); | ||
}); | ||
freePromisesQueue(clock)(); | ||
}); | ||
it('should pass all function arguments to wrapped function', (done) => { | ||
@@ -53,2 +164,4 @@ const func = sinon.stub(); | ||
}); | ||
freePromisesQueue(clock)(); | ||
}); | ||
@@ -75,5 +188,7 @@ | ||
}); | ||
freePromisesQueue(clock)(); | ||
}); | ||
it('should call retryArgumentsInterceptor with sequentially changing args and attempts count', (done) => { | ||
it('should call the original function with result of retryArgumentsInterceptor returning a promise', (done) => { | ||
const func = sinon.stub(); | ||
@@ -86,2 +201,25 @@ | ||
const retryArgumentsInterceptor = sinon.stub(); | ||
retryArgumentsInterceptor.onCall(0).resolves([5, 6, 7]); | ||
retryArgumentsInterceptor.onCall(1).resolves([8, 9, 10]); | ||
const composedFunction = promiseAgain(func, {attempts: 5, retryArgumentsInterceptor}); | ||
composedFunction(1, 2, 3, 'some arg').then(() => { | ||
expect(func.callCount).to.equal(3); | ||
expect(func.calledWithExactly(1, 2, 3, 'some arg')).to.equal(true); | ||
expect(func.calledWithExactly(5, 6, 7)).to.equal(true); | ||
expect(func.calledWithExactly(8, 9, 10)).to.equal(true); | ||
done(); | ||
}); | ||
freePromisesQueue(clock)(); | ||
}); | ||
it('should call retryArgumentsInterceptor with reason, sequentially changing args and attempts count', (done) => { | ||
const func = sinon.stub(); | ||
func.onCall(0).rejects('Some reason 1'); | ||
func.onCall(1).rejects('Some reason 2'); | ||
func.onCall(2).resolves('Needed value'); | ||
const retryArgumentsInterceptor = sinon.stub(); | ||
retryArgumentsInterceptor.onCall(0).returns([5, 6, 7]); | ||
@@ -108,6 +246,7 @@ retryArgumentsInterceptor.onCall(1).returns([8, 9, 10]); | ||
}); | ||
freePromisesQueue(clock)(); | ||
}); | ||
it('should use a delay between retry calls', (done) => { | ||
const clock = sinon.useFakeTimers(); | ||
const func = sinon.stub(); | ||
@@ -121,3 +260,2 @@ | ||
composedFunction(1, 2, 3, 'some arg').then(() => { | ||
clock.restore(); | ||
done(); | ||
@@ -130,3 +268,3 @@ }).catch(() => { | ||
promiseDelay(50).then(() => { | ||
promiseDelayAndFreeQueue(50, clock).then(() => { | ||
expect(func.callCount).to.equal(1); | ||
@@ -136,3 +274,3 @@ }) | ||
promiseDelay(80).then(() => { | ||
promiseDelayAndFreeQueue(80, clock).then(() => { | ||
expect(func.callCount).to.equal(1); | ||
@@ -142,3 +280,3 @@ }) | ||
promiseDelay(155).then(() => { | ||
promiseDelayAndFreeQueue(155, clock).then(() => { | ||
expect(func.callCount).to.equal(2); | ||
@@ -148,3 +286,3 @@ }) | ||
promiseDelay(260).then(() => { | ||
promiseDelayAndFreeQueue(260, clock).then(() => { | ||
expect(func.callCount).to.equal(3); | ||
@@ -158,3 +296,2 @@ }) | ||
it('should use a delay function to get delay if provided', (done) => { | ||
const clock = sinon.useFakeTimers(); | ||
const func = sinon.stub(); | ||
@@ -174,3 +311,2 @@ | ||
composedFunction(1, 2, 3, 'some arg').then(() => { | ||
clock.restore(); | ||
done(); | ||
@@ -183,3 +319,3 @@ }).catch(() => { | ||
promiseDelay(50).then(() => { | ||
promiseDelayAndFreeQueue(50, clock).then(() => { | ||
expect(func.callCount).to.equal(1); | ||
@@ -189,3 +325,3 @@ }) | ||
promiseDelay(80).then(() => { | ||
promiseDelayAndFreeQueue(80, clock).then(() => { | ||
expect(func.callCount).to.equal(1); | ||
@@ -195,3 +331,3 @@ }) | ||
promiseDelay(155).then(() => { | ||
promiseDelayAndFreeQueue(155, clock).then(() => { | ||
expect(func.callCount).to.equal(2); | ||
@@ -201,3 +337,3 @@ }) | ||
promiseDelay(360).then(() => { | ||
promiseDelayAndFreeQueue(360, clock).then(() => { | ||
expect(func.callCount).to.equal(3); | ||
@@ -210,4 +346,3 @@ }) | ||
it('should pass attempts count and sequential changing arguments to delay function', (done) => { | ||
const clock = sinon.useFakeTimers(); | ||
it('should use a delay function returning a promise to get delay if provided', (done) => { | ||
const func = sinon.stub(); | ||
@@ -219,2 +354,47 @@ | ||
const delayFunction = sinon.stub(); | ||
delayFunction.onCall(0).resolves(100); | ||
delayFunction.onCall(1).resolves(150); | ||
delayFunction.onCall(2).resolves(250); | ||
const composedFunction = promiseAgain(func, {attempts: 5, delay: delayFunction}); | ||
composedFunction(1, 2, 3, 'some arg').then(() => { | ||
done(); | ||
}).catch(() => { | ||
throw new Error('Catch'); | ||
}); | ||
expect(func.callCount).to.equal(1); | ||
promiseDelayAndFreeQueue(50, clock).then(() => { | ||
expect(func.callCount).to.equal(1); | ||
}) | ||
.then(() => clock.tick(30)); | ||
promiseDelayAndFreeQueue(80, clock).then(() => { | ||
expect(func.callCount).to.equal(1); | ||
}) | ||
.then(() => clock.tick(75)); | ||
promiseDelayAndFreeQueue(155, clock).then(() => { | ||
expect(func.callCount).to.equal(2); | ||
}) | ||
.then(() => clock.tick(205)); | ||
promiseDelayAndFreeQueue(360, clock).then(() => { | ||
expect(func.callCount).to.equal(3); | ||
}) | ||
.then(() => clock.tick(105)); | ||
clock.tick(50); | ||
}); | ||
it('should pass reason, attempts count and sequential changing arguments to delay function', (done) => { | ||
const func = sinon.stub(); | ||
func.onCall(0).rejects('Some reason 1'); | ||
func.onCall(1).rejects('Some reason 2'); | ||
func.onCall(2).resolves('Needed value'); | ||
const retryArgumentsInterceptor = sinon.stub(); | ||
@@ -233,8 +413,11 @@ retryArgumentsInterceptor.onCall(0).returns([5, 6, 7]); | ||
composedFunction(1, 2, 3, 'some arg').then(() => { | ||
clock.restore(); | ||
expect(delayFunction.callCount).to.equal(2); | ||
expect(delayFunction.calledWithExactly(1, 1, 2, 3, 'some arg')).to.equal(true); | ||
expect(delayFunction.calledWithExactly(2, 5, 6, 7)).to.equal(true); | ||
expect( | ||
delayFunction.calledWithExactly(sinon.match({name: 'Some reason 1'}), 1, 1, 2, 3, 'some arg'), | ||
).to.equal(true); | ||
expect( | ||
delayFunction.calledWithExactly(sinon.match({name: 'Some reason 2'}), 2, 5, 6, 7), | ||
).to.equal(true); | ||
done(); | ||
@@ -247,12 +430,12 @@ }).catch(() => { | ||
promiseDelay(50) | ||
promiseDelayAndFreeQueue(50, clock) | ||
.then(() => clock.tick(30)); | ||
promiseDelay(80) | ||
promiseDelayAndFreeQueue(80, clock) | ||
.then(() => clock.tick(75)); | ||
promiseDelay(155) | ||
promiseDelayAndFreeQueue(155, clock) | ||
.then(() => clock.tick(105)); | ||
promiseDelay(260) | ||
promiseDelayAndFreeQueue(260, clock) | ||
.then(() => clock.tick(105)); | ||
@@ -259,0 +442,0 @@ |
32
index.ts
import * as promiseDelay from 'delay'; | ||
export interface IOptions { | ||
delay?: number | ((attempt: number, ...args: any[]) => number); | ||
attempts: number | ((attempt: number, ...args: any[]) => boolean); | ||
retryArgumentsInterceptor?: (reason: any, attempt: number, ...args: any[]) => any[]; | ||
delay?: number | ((reason: any, attempt: number, ...args: any[]) => number | Promise<number>); | ||
attempts: number | ((reason: any, attempt: number, ...args: any[]) => boolean | Promise<boolean>); | ||
retryArgumentsInterceptor?: (reason: any, attempt: number, ...args: any[]) => any[] | Promise<any[]>; | ||
} | ||
@@ -23,3 +23,3 @@ | ||
let shouldRetry = false; | ||
let shouldRetry: boolean | Promise<boolean> = false; | ||
@@ -29,6 +29,6 @@ if (typeof options.attempts === 'number') { | ||
} else { | ||
shouldRetry = options.attempts(usedAttempts, ...innerArgs); | ||
shouldRetry = options.attempts(reason, usedAttempts, ...innerArgs); | ||
} | ||
let nextDelay: number = 0; | ||
let nextDelay: number | Promise<number> = 0; | ||
@@ -38,12 +38,16 @@ if (typeof options.delay === 'number') { | ||
} else if (typeof options.delay === 'function') { | ||
nextDelay = options.delay(usedAttempts, ...innerArgs); | ||
nextDelay = options.delay(reason, usedAttempts, ...innerArgs); | ||
} | ||
if (shouldRetry) { | ||
return promiseDelay(nextDelay) | ||
.then(() => attempt(...newArguments)) | ||
.catch((subReason: any) => Promise.reject(subReason)); | ||
} else { | ||
return Promise.reject(reason); | ||
} | ||
return Promise.all([shouldRetry, nextDelay, newArguments]).then( | ||
([resolvedShouldRetry, resolvedNextDelay, resolvedNewArguments = []]) => { | ||
if (resolvedShouldRetry) { | ||
return promiseDelay(resolvedNextDelay) | ||
.then(() => attempt(...resolvedNewArguments)) | ||
.catch((subReason: any) => Promise.reject(subReason)); | ||
} else { | ||
return Promise.reject(reason); | ||
} | ||
}, | ||
); | ||
}); | ||
@@ -50,0 +54,0 @@ }(...args); |
{ | ||
"name": "promise-again", | ||
"version": "1.2.0", | ||
"version": "1.3.0", | ||
"main": "./dist/umd/index.js", | ||
@@ -5,0 +5,0 @@ "module": "./dist/es6/index.js", |
# promise-again | ||
Composing util method which reruns promise-returning function with possible delay and arguments interceptor | ||
Yet another wrapper for functions that return promise to retry rejected attempts. | ||
But this one with advanced **flexibility**. | ||
--- | ||
## Usage | ||
``` | ||
import promiseAgain from 'promise-again'; | ||
function someFunctionReturningAPromise() { | ||
... | ||
} | ||
const wrappedFunction = promiseAgain( | ||
/** | ||
* A function that returns a promise | ||
**/ | ||
someFunctionReturningAPromise, | ||
{ | ||
/** | ||
* Optional. Delay in milisecconds or a function that returns a delay | ||
* or a function that returns a promise that resolves to a delay. | ||
* | ||
* @param reason - reason of the last rejection; | ||
* @param attempt - number of used attempts; | ||
* @param ...args - last attempt arguments; | ||
* | ||
* @returns {number | Promise<number>} - modified arguments to be used in the next attempt or a promise that is resolved to such arguments; | ||
**/ | ||
delay: number | ((attempt: number, ...args: any[]) => number | Promise<number>); | ||
/** | ||
* Required. Number of attempts or function that returns true or a Promise that resolved to true if retry is needed; | ||
* | ||
* @param reason - reason of the last rejection; | ||
* @param attempt - number of used attempts; | ||
* @param ...args - last attempt arguments; | ||
* | ||
* @returns {boolean | Promise<any[]>} - modified arguments to be used in the next attempt or a promise that is resolved to such arguments; | ||
**/ | ||
attempts: number | ((attempt: number, ...args: any[]) => boolean | Promise<boolean>); | ||
/** | ||
* Optional. Function that is called before every retry attempt to modify next attempt arguments; | ||
* | ||
* @param reason - reason of the last rejection; | ||
* @param attempt - number of used attempts; | ||
* @param ...args - last attempt arguments; | ||
* | ||
* @returns {any[] | Promise<any[]>} - modified arguments to be used in the next attempt or a promise that is resolved to such arguments; | ||
**/ | ||
retryArgumentsInterceptor: (reason: any, attempt: number, ...args: any[]) => any[] | Promise<any[]>; | ||
} | ||
) | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
56876
520
60