From b36183ce0cda3cbe1a625593fed4d2452245a3f9 Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Mon, 11 Nov 2019 12:31:47 +0200 Subject: [PATCH 01/13] Retry decorator --- examples/retry.ts | 12 ++++ lib/retry.ts | 69 ++++++++++++++++++++++- lib/utils/index.ts | 21 +++++++ test/retry.spec.ts | 137 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 examples/retry.ts create mode 100644 lib/utils/index.ts create mode 100644 test/retry.spec.ts diff --git a/examples/retry.ts b/examples/retry.ts new file mode 100644 index 0000000..72aae12 --- /dev/null +++ b/examples/retry.ts @@ -0,0 +1,12 @@ +import { retry } from '../lib'; + +class Service { + @retry(3) + do(): Promise { + return new Promise((res, rej) => { + setTimeout(res, 1000); + }); + } +} + +const t = new Service().do().catch(err => console.log(err.message)); diff --git a/lib/retry.ts b/lib/retry.ts index 356e300..9e4a8a4 100644 --- a/lib/retry.ts +++ b/lib/retry.ts @@ -1,3 +1,5 @@ +import { raiseStrategy } from './utils'; + export type RetryOptions = { /** * Sets the behavior of handling the case when all retrials failed. @@ -26,9 +28,11 @@ export type RetryOptions = { * A custom function can be used to provide custom interval (in milliseconds) * based on attempt number (indexed from one). */ - waitPattern?: number | number[] | ((attempt: number) => number), + waitPattern?: WaitPattern, }; +type WaitPattern = number | number[] | ((attempt: number) => number); + /** * Retries the execution of a method for a given number of attempts. * If the method fails to succeed after `attempts` retries, it fails @@ -36,6 +40,65 @@ export type RetryOptions = { * @param attempts max number of attempts to retry execution * @param options (optional) retry options */ -export function retry(attempts: number, options?: number): any { - throw new Error('Not implemented.'); +export function retry(attempts: number, options?: RetryOptions): any { + return function (target: any, propertyKey: any, descriptor: PropertyDescriptor) { + const method: Function = descriptor.value; + const defaultOptions: RetryOptions = { + errorFilter: () => { return true; }, + }; + const retryOptions = { + ...defaultOptions, + ...options, + }; + + let attemptIndex = 1; + + descriptor.value = function () { + return method.apply(this, arguments) + .then((result) => { + return result; + }) + .catch(async (error) => { + const shouldRetry = attemptIndex < attempts && retryOptions.errorFilter(error); + + if (shouldRetry) { + if (retryOptions.waitPattern) { + await waitByStrategy(attemptIndex, retryOptions.waitPattern); + } + attemptIndex += 1; + return target[propertyKey](); + } + + const raise = raiseStrategy({ onError: retryOptions.onError }, 'throw'); + return raise(new Error('Retry failed.')); + }); + }; + + return descriptor; + }; +} + +function waitByStrategy(attemptIndex: number, waitPattern: WaitPattern): Promise { + const isPatternTypeArray = Array.isArray(waitPattern); + const patternType = isPatternTypeArray ? 'array' : typeof waitPattern; + + switch (patternType) { + case 'number': + return wait(waitPattern as number); + case 'array': + const attemptValues = waitPattern as number[]; + const shouldWaitValue = attemptIndex > attemptValues.length + ? attemptValues[attemptValues.length - 1] + : attemptValues[attemptIndex]; + + return wait(shouldWaitValue); + case 'function': + return wait((waitPattern as Function)(attemptIndex)); + default: + throw new Error(`Option ${patternType} is not supported for 'waitPattern'.`); + } +} + +function wait(timeout: number = 0): Promise { + return new Promise(resolve => setTimeout(resolve, timeout)); } diff --git a/lib/utils/index.ts b/lib/utils/index.ts new file mode 100644 index 0000000..6524160 --- /dev/null +++ b/lib/utils/index.ts @@ -0,0 +1,21 @@ + +type StrategyOptions = { + onError?: 'throw' | 'reject' | 'ignore' | 'ignoreAsync'; +}; + +export function raiseStrategy(options: StrategyOptions, defaultStrategy: string) { + const value = options && options.onError || defaultStrategy; + + switch (value) { + case 'reject': + return err => Promise.reject(err); + case 'throw': + return (err) => { throw err; }; + case 'ignore': + return () => { }; + case 'ignoreAsync': + return () => Promise.resolve(); + default: + throw new Error(`Option ${value} is not supported for 'behavior'.`); + } +} diff --git a/test/retry.spec.ts b/test/retry.spec.ts new file mode 100644 index 0000000..cec8a0f --- /dev/null +++ b/test/retry.spec.ts @@ -0,0 +1,137 @@ +import { expect } from 'chai'; +import { retry } from '../lib'; +import { delay } from './utils'; + +describe.only('@retry', () => { + describe('When method don\'t have retry options.', () => { + it('should resolve with expected result.', async () => { + class TestClass { + @retry(3) + async test() { + return Promise.resolve('Success 42.'); + } + } + + const target = new TestClass(); + const result = await target.test(); + + expect(result).to.equal('Success 42.'); + }); + + it('should throw default error.', async () => { + class TestClass { + @retry(3) + async test() { + return Promise.reject('Error 42.'); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + }); + + describe('When method have empty retry options object.', () => { + it('should resolve with expected result.', async () => { + class TestClass { + @retry(3) + async test() { + return Promise.resolve('Success 42.'); + } + } + + const target = new TestClass(); + const result = await target.test(); + + expect(result).to.equal('Success 42.'); + }); + + it('should retry until function will resolve with expected result.', async () => { + class TestClass { + public retryIndex = 1; + @retry(3) + async test() { + if (this.retryIndex === 1) { + return Promise.reject('Error 42.'); + } + return Promise.resolve('Success 42.'); + } + } + + const target = new TestClass(); + const result = await target.test(); + + expect(result).to.equal('Success 42.'); + }); + + it('should throw default error.', async () => { + class TestClass { + @retry(3) + async test() { + return Promise.reject('Error 42.'); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + }); + + describe('When method is called with retry options.', () => { + it('should throw with expected \'throw\' error.', async () => { + class TestClass { + @retry(3, { onError: 'throw' }) + async test() { + return Promise.reject('Error 42.'); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should throw with expected \'reject\' error.', async () => { + class TestClass { + @retry(3, { onError: 'reject' }) + async test() { + return Promise.reject('Error 42.'); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should ignore with \'ignore\' error.', async () => { + class TestClass { + @retry(3, { onError: 'ignore' }) + async test() { + return Promise.reject('Error 42.'); + } + } + + const target = new TestClass(); + const response = await target.test(); + + expect(response).to.equal(undefined); + }); + + it('should ignore with \'ignoreAsync\' error.', async () => { + class TestClass { + @retry(3, { onError: 'ignoreAsync' }) + async test() { + return Promise.reject('Error 42.'); + } + } + + const target = new TestClass(); + const response = await target.test(); + + expect(response).to.equal(undefined); + }); + }); +}); From 50a3187b43bbe478c40ccdce66c9c686739d7cb0 Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Tue, 12 Nov 2019 13:15:02 +0200 Subject: [PATCH 02/13] Retry decorator tests --- test/retry.spec.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/retry.spec.ts b/test/retry.spec.ts index cec8a0f..c3f8699 100644 --- a/test/retry.spec.ts +++ b/test/retry.spec.ts @@ -133,5 +133,47 @@ describe.only('@retry', () => { expect(response).to.equal(undefined); }); + + it('should continue retring if error is filtered as expected', async () => { + class TestClass { + public retryIndex = 1; + @retry(3, { + errorFilter: (error) => { + return error.message === 'Error 42.'; + }, + }) + async test() { + if (this.retryIndex === 1) { + return Promise.reject('Error 42.'); + } + return Promise.reject('42'); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should stop retring if error is filtered as not expected', async () => { + class TestClass { + public retryIndex = 1; + @retry(3, { + errorFilter: (error) => { + return error.message === '42'; + }, + }) + async test() { + if (this.retryIndex === 1) { + return Promise.reject('Error 42.'); + } + return Promise.resolve('Success 42!'); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); }); }); From 6afefaf712958ea7ee10bd2f84517f3bd2063afc Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Tue, 12 Nov 2019 13:25:13 +0200 Subject: [PATCH 03/13] Retry decorator split code into files --- lib/retry.ts | 66 ++++----------------------------------- lib/retry/RetryOptions.ts | 35 +++++++++++++++++++++ lib/retry/WaitStrategy.ts | 27 ++++++++++++++++ 3 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 lib/retry/RetryOptions.ts create mode 100644 lib/retry/WaitStrategy.ts diff --git a/lib/retry.ts b/lib/retry.ts index 9e4a8a4..7abfa3e 100644 --- a/lib/retry.ts +++ b/lib/retry.ts @@ -1,37 +1,8 @@ +import { DEFAULT_ERROR, DEFAULT_ON_ERROR, RetryOptions } from './retry/RetryOptions'; +import { waitStrategy } from './retry/WaitStrategy'; import { raiseStrategy } from './utils'; -export type RetryOptions = { - /** - * Sets the behavior of handling the case when all retrials failed. - * When `throw` (default) then throws immediately with an error. - * When `reject` then returns a rejected promise with an error. - * When `ignore` then doesn't throw any error and immediately - * terminates execution (returns undefined). - * When `ignoreAsync` then doesn't throw any error and immediately - * returns a resolved promise. - */ - onError?: 'throw' | 'reject' | 'ignore' | 'ignoreAsync', - - /** - * Allows to filter only for specific errors. - * By default all errors are retried. - */ - errorFilter?: (err: Error) => boolean, - - /** - * Allows to delay a retry execution. By default when `waitPattern` is not specified - * then a failing method is retried immediately. - * A number is used as a milliseconds interval to wait until next attempt. - * An array is used to apply timeouts between attempts according to array values - * (in milliseconds). Last array value is used in case array length is less than - * number of attempts. - * A custom function can be used to provide custom interval (in milliseconds) - * based on attempt number (indexed from one). - */ - waitPattern?: WaitPattern, -}; - -type WaitPattern = number | number[] | ((attempt: number) => number); +export { RetryOptions }; /** * Retries the execution of a method for a given number of attempts. @@ -63,42 +34,17 @@ export function retry(attempts: number, options?: RetryOptions): any { if (shouldRetry) { if (retryOptions.waitPattern) { - await waitByStrategy(attemptIndex, retryOptions.waitPattern); + await waitStrategy(attemptIndex, retryOptions.waitPattern); } attemptIndex += 1; return target[propertyKey](); } - const raise = raiseStrategy({ onError: retryOptions.onError }, 'throw'); - return raise(new Error('Retry failed.')); + const raise = raiseStrategy({ onError: retryOptions.onError }, DEFAULT_ON_ERROR); + return raise(new Error(DEFAULT_ERROR)); }); }; return descriptor; }; } - -function waitByStrategy(attemptIndex: number, waitPattern: WaitPattern): Promise { - const isPatternTypeArray = Array.isArray(waitPattern); - const patternType = isPatternTypeArray ? 'array' : typeof waitPattern; - - switch (patternType) { - case 'number': - return wait(waitPattern as number); - case 'array': - const attemptValues = waitPattern as number[]; - const shouldWaitValue = attemptIndex > attemptValues.length - ? attemptValues[attemptValues.length - 1] - : attemptValues[attemptIndex]; - - return wait(shouldWaitValue); - case 'function': - return wait((waitPattern as Function)(attemptIndex)); - default: - throw new Error(`Option ${patternType} is not supported for 'waitPattern'.`); - } -} - -function wait(timeout: number = 0): Promise { - return new Promise(resolve => setTimeout(resolve, timeout)); -} diff --git a/lib/retry/RetryOptions.ts b/lib/retry/RetryOptions.ts new file mode 100644 index 0000000..6144673 --- /dev/null +++ b/lib/retry/RetryOptions.ts @@ -0,0 +1,35 @@ +export type RetryOptions = { + /** + * Sets the behavior of handling the case when all retrials failed. + * When `throw` (default) then throws immediately with an error. + * When `reject` then returns a rejected promise with an error. + * When `ignore` then doesn't throw any error and immediately + * terminates execution (returns undefined). + * When `ignoreAsync` then doesn't throw any error and immediately + * returns a resolved promise. + */ + onError?: 'throw' | 'reject' | 'ignore' | 'ignoreAsync', + + /** + * Allows to filter only for specific errors. + * By default all errors are retried. + */ + errorFilter?: (err: Error) => boolean, + + /** + * Allows to delay a retry execution. By default when `waitPattern` is not specified + * then a failing method is retried immediately. + * A number is used as a milliseconds interval to wait until next attempt. + * An array is used to apply timeouts between attempts according to array values + * (in milliseconds). Last array value is used in case array length is less than + * number of attempts. + * A custom function can be used to provide custom interval (in milliseconds) + * based on attempt number (indexed from one). + */ + waitPattern?: WaitPattern, +}; + +export type WaitPattern = number | number[] | ((attempt: number) => number); + +export const DEFAULT_ON_ERROR = 'throw'; +export const DEFAULT_ERROR = 'Retry failed.'; diff --git a/lib/retry/WaitStrategy.ts b/lib/retry/WaitStrategy.ts new file mode 100644 index 0000000..306b2ff --- /dev/null +++ b/lib/retry/WaitStrategy.ts @@ -0,0 +1,27 @@ +import { WaitPattern } from './RetryOptions'; + +export function waitStrategy(attemptIndex: number, waitPattern: WaitPattern): Promise { + const patternType = Array.isArray(waitPattern) + ? 'array' + : typeof waitPattern; + + switch (patternType) { + case 'number': + return wait(waitPattern as number); + case 'array': + const attemptValues = waitPattern as number[]; + const shouldWaitValue = attemptIndex > attemptValues.length + ? attemptValues[attemptValues.length - 1] + : attemptValues[attemptIndex]; + + return wait(shouldWaitValue); + case 'function': + return wait((waitPattern as Function)(attemptIndex)); + default: + throw new Error(`Option ${patternType} is not supported for 'waitPattern'.`); + } +} + +function wait(timeout: number = 0): Promise { + return new Promise(resolve => setTimeout(resolve, timeout)); +} From 1d0e59c125354b97e354a5d71098dceee4ddceff Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Wed, 13 Nov 2019 16:57:27 +0200 Subject: [PATCH 04/13] Retry decorator --- lib/retry.ts | 50 --- lib/retry/RetryOptions.ts | 9 + lib/retry/Retryer.ts | 25 ++ lib/retry/WaitStrategy.ts | 52 ++-- lib/retry/index.ts | 52 ++++ test/retry.spec.ts | 630 +++++++++++++++++++++++++++++--------- 6 files changed, 607 insertions(+), 211 deletions(-) delete mode 100644 lib/retry.ts create mode 100644 lib/retry/Retryer.ts create mode 100644 lib/retry/index.ts diff --git a/lib/retry.ts b/lib/retry.ts deleted file mode 100644 index 7abfa3e..0000000 --- a/lib/retry.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { DEFAULT_ERROR, DEFAULT_ON_ERROR, RetryOptions } from './retry/RetryOptions'; -import { waitStrategy } from './retry/WaitStrategy'; -import { raiseStrategy } from './utils'; - -export { RetryOptions }; - -/** - * Retries the execution of a method for a given number of attempts. - * If the method fails to succeed after `attempts` retries, it fails - * with error `Retry failed.` - * @param attempts max number of attempts to retry execution - * @param options (optional) retry options - */ -export function retry(attempts: number, options?: RetryOptions): any { - return function (target: any, propertyKey: any, descriptor: PropertyDescriptor) { - const method: Function = descriptor.value; - const defaultOptions: RetryOptions = { - errorFilter: () => { return true; }, - }; - const retryOptions = { - ...defaultOptions, - ...options, - }; - - let attemptIndex = 1; - - descriptor.value = function () { - return method.apply(this, arguments) - .then((result) => { - return result; - }) - .catch(async (error) => { - const shouldRetry = attemptIndex < attempts && retryOptions.errorFilter(error); - - if (shouldRetry) { - if (retryOptions.waitPattern) { - await waitStrategy(attemptIndex, retryOptions.waitPattern); - } - attemptIndex += 1; - return target[propertyKey](); - } - - const raise = raiseStrategy({ onError: retryOptions.onError }, DEFAULT_ON_ERROR); - return raise(new Error(DEFAULT_ERROR)); - }); - }; - - return descriptor; - }; -} diff --git a/lib/retry/RetryOptions.ts b/lib/retry/RetryOptions.ts index 6144673..75b28f4 100644 --- a/lib/retry/RetryOptions.ts +++ b/lib/retry/RetryOptions.ts @@ -31,5 +31,14 @@ export type RetryOptions = { export type WaitPattern = number | number[] | ((attempt: number) => number); +export type MethodOptions = { + method: Function, + instance: any, + args: any, +}; + export const DEFAULT_ON_ERROR = 'throw'; export const DEFAULT_ERROR = 'Retry failed.'; +export const DEFAULT_OPTIONS: RetryOptions = { + errorFilter: () => { return true; }, +}; diff --git a/lib/retry/Retryer.ts b/lib/retry/Retryer.ts new file mode 100644 index 0000000..30440fd --- /dev/null +++ b/lib/retry/Retryer.ts @@ -0,0 +1,25 @@ +import { raiseStrategy } from '../utils'; +import { DEFAULT_ERROR, DEFAULT_ON_ERROR, MethodOptions, RetryOptions } from './RetryOptions'; + +export class Retryer { + constructor( + private readonly options: RetryOptions, + private readonly methodOptions: MethodOptions, + ) { } + + public retry(error: Error, attempts: number, count: number): any { + const { instance } = this.methodOptions; + + if (!attempts || attempts < count || !this.options.errorFilter.bind(instance)(error)) { + return this.error(); + } + + const { method, args } = this.methodOptions; + return method.bind(instance)(args); + } + + private error(): void | Promise { + const raise = raiseStrategy({ onError: this.options.onError }, DEFAULT_ON_ERROR); + return raise(new Error(DEFAULT_ERROR)); + } +} diff --git a/lib/retry/WaitStrategy.ts b/lib/retry/WaitStrategy.ts index 306b2ff..e0843b2 100644 --- a/lib/retry/WaitStrategy.ts +++ b/lib/retry/WaitStrategy.ts @@ -1,27 +1,39 @@ import { WaitPattern } from './RetryOptions'; -export function waitStrategy(attemptIndex: number, waitPattern: WaitPattern): Promise { - const patternType = Array.isArray(waitPattern) - ? 'array' - : typeof waitPattern; +export class WaitStrategy { - switch (patternType) { - case 'number': - return wait(waitPattern as number); - case 'array': - const attemptValues = waitPattern as number[]; - const shouldWaitValue = attemptIndex > attemptValues.length - ? attemptValues[attemptValues.length - 1] - : attemptValues[attemptIndex]; + constructor( + private readonly waitPattern: WaitPattern, + ) { } - return wait(shouldWaitValue); - case 'function': - return wait((waitPattern as Function)(attemptIndex)); - default: - throw new Error(`Option ${patternType} is not supported for 'waitPattern'.`); + public wait(index: number, instance: any): Promise { + if (!this.waitPattern) { + return Promise.resolve(); + } + + const timeout = this.getTimeout(index, instance) || 0; + return new Promise(resolve => setTimeout(resolve, timeout)); } -} -function wait(timeout: number = 0): Promise { - return new Promise(resolve => setTimeout(resolve, timeout)); + private getTimeout(index: number, instance: any): number { + const patternType = Array.isArray(this.waitPattern) + ? 'array' + : typeof this.waitPattern; + + switch (patternType) { + case 'number': + return this.waitPattern as number; + case 'array': + const values = this.waitPattern as number[]; + const timeout = index > values.length + ? values[values.length - 1] + : values[index]; + + return timeout; + case 'function': + return (this.waitPattern as Function)(index); + default: + throw new Error(`Option ${patternType} is not supported for 'waitPattern'.`); + } + } } diff --git a/lib/retry/index.ts b/lib/retry/index.ts new file mode 100644 index 0000000..b343bb3 --- /dev/null +++ b/lib/retry/index.ts @@ -0,0 +1,52 @@ +import { Retryer } from './Retryer'; +import { DEFAULT_OPTIONS, MethodOptions, RetryOptions } from './RetryOptions'; +import { WaitStrategy } from './WaitStrategy'; + +export { RetryOptions }; + +/** + * Retries the execution of a method for a given number of attempts. + * If the method fails to succeed after `attempts` retries, it fails + * with error `Retry failed.` + * @param attempts max number of attempts to retry execution + * @param options (optional) retry options + */ +export function retry(attempts: number, options?: RetryOptions): any { + return function (target: any, propertyKey: any, descriptor: PropertyDescriptor) { + const method: Function = descriptor.value; + const retryOptions = { ...DEFAULT_OPTIONS, ...options }; + const waitStrategy = new WaitStrategy(retryOptions.waitPattern); + + let retryCount: number = 0; + + descriptor.value = function () { + const methodOptions: MethodOptions = { + instance: this, + args: arguments, + method: target[propertyKey], + }; + const retryer = new Retryer(retryOptions, methodOptions); + + try { + const response = method.apply(this, arguments); + const isPromiseLike = response && typeof response.then === 'function'; + + if (isPromiseLike) { + return response.catch((err) => { + retryCount += 1; + + return waitStrategy.wait(retryCount - 1, methodOptions.instance) + .then(() => retryer.retry(err, attempts, retryCount)); + }); + } + + return response; + } catch (err) { + retryCount += 1; + return retryer.retry(err, attempts, retryCount); + } + }; + + return descriptor; + }; +} diff --git a/test/retry.spec.ts b/test/retry.spec.ts index c3f8699..9161bb1 100644 --- a/test/retry.spec.ts +++ b/test/retry.spec.ts @@ -1,179 +1,527 @@ import { expect } from 'chai'; import { retry } from '../lib'; -import { delay } from './utils'; + +function wait(timeout: number = 0): Promise { + return new Promise(resolve => setTimeout(resolve, timeout)); +} describe.only('@retry', () => { - describe('When method don\'t have retry options.', () => { - it('should resolve with expected result.', async () => { - class TestClass { - @retry(3) - async test() { - return Promise.resolve('Success 42.'); - } - } - - const target = new TestClass(); - const result = await target.test(); - - expect(result).to.equal('Success 42.'); - }); + describe('When method is synchrone.', () => { + describe('When method should return value.', () => { + describe('When method should work only with attempts number.', () => { + it('should resolve with expected result and attempts > 0.', () => { + class TestClass { + @retry(3) + test() { + return 'Success 42.'; + } + } - it('should throw default error.', async () => { - class TestClass { - @retry(3) - async test() { - return Promise.reject('Error 42.'); - } - } + const target = new TestClass(); + const result = target.test(); - const target = new TestClass(); + expect(result).to.equal('Success 42.'); + }); - await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); - }); - }); + it('should resolve with expected result and attempts = 0.', () => { + class TestClass { + @retry(0) + test() { + return 'Success 42.'; + } + } - describe('When method have empty retry options object.', () => { - it('should resolve with expected result.', async () => { - class TestClass { - @retry(3) - async test() { - return Promise.resolve('Success 42.'); - } - } + const target = new TestClass(); + const result = target.test(); - const target = new TestClass(); - const result = await target.test(); + expect(result).to.equal('Success 42.'); + }); - expect(result).to.equal('Success 42.'); - }); + it('should resolve with expected result and attempts < 0.', () => { + class TestClass { + @retry(-3) + test() { + return 'Success 42.'; + } + } + + const target = new TestClass(); + const result = target.test(); + + expect(result).to.equal('Success 42.'); + }); + + it('should resolve with expected result and attempts === null.', () => { + class TestClass { + @retry(-3) + test() { + return 'Success 42.'; + } + } - it('should retry until function will resolve with expected result.', async () => { - class TestClass { - public retryIndex = 1; - @retry(3) - async test() { - if (this.retryIndex === 1) { - return Promise.reject('Error 42.'); + const target = new TestClass(); + const result = target.test(); + + expect(result).to.equal('Success 42.'); + }); + + it('should resolve with expected result and attempts === undefined.', () => { + class TestClass { + @retry(undefined) + test() { + return 'Success 42.'; + } } - return Promise.resolve('Success 42.'); - } - } - const target = new TestClass(); - const result = await target.test(); + const target = new TestClass(); + const result = target.test(); - expect(result).to.equal('Success 42.'); + expect(result).to.equal('Success 42.'); + }); + }); }); - it('should throw default error.', async () => { - class TestClass { - @retry(3) - async test() { - return Promise.reject('Error 42.'); - } - } + describe('When method should throw error.', () => { + describe('When method should work only with attempts number.', () => { + it('should reject with expected error and attempts > 0.', () => { + class TestClass { + @retry(3) + test() { + throw new Error('Error 42.'); + } + } + + const target = new TestClass(); + + expect(() => target.test()).to.throw('Retry failed.'); + }); + + it('should resolve with expected result and attempts = 0.', () => { + class TestClass { + @retry(0) + test() { + throw new Error('Error 42.'); + } + } + + const target = new TestClass(); + + expect(() => target.test()).to.throw('Retry failed.'); + }); + + it('should reject with expected error and attempts < 0.', () => { + class TestClass { + @retry(-3) + test() { + throw new Error('Error 42.'); + } + } + + const target = new TestClass(); - const target = new TestClass(); + expect(() => target.test()).to.throw('Retry failed.'); + }); - await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + it('should reject with expected error and attempts === null.', () => { + class TestClass { + @retry(-3) + test() { + throw new Error('Error 42.'); + } + } + + const target = new TestClass(); + + expect(() => target.test()).to.throw('Retry failed.'); + }); + + it('should reject with expected error and attempts === undefined.', () => { + class TestClass { + @retry(undefined) + test() { + throw new Error('Error 42.'); + } + } + + const target = new TestClass(); + + expect(() => target.test()).to.throw('Retry failed.'); + }); + }); + + describe('When method should return a specific error.', () => { + it('should throw with expected error.', () => { + class TestClass { + @retry(3, { onError: 'throw' }) + test() { + throw new Error('Error 42.'); + } + } + + const target = new TestClass(); + + expect(() => target.test()).to.throw('Retry failed.'); + }); + + it('should return undefined.', () => { + class TestClass { + @retry(3, { onError: 'ignore' }) + test() { + throw new Error('Error 42.'); + } + } + + const target = new TestClass(); + const response = target.test(); + + expect(response).to.equal(undefined); + }); + }); + + describe('When method should throw for a specific error until attempts completted.', () => { + it('should throw with expected error.', () => { + class TestClass { + @retry(3, { errorFilter: (err: Error) => err.message === 'Error 42.' }) + test() { + throw new Error('Error 42.'); + } + } + + const target = new TestClass(); + + expect(() => target.test()).to.throw('Retry failed.'); + }); + + it('should throw with default error and throw when error is expected.', () => { + class TestClass { + public count = 1; + @retry(3, { + errorFilter: (err: Error) => { + return err.message === 'error'; + }, + }) + test() { + if (this.count === 2) { + throw new Error('error'); + } + + this.count += 1; + throw new Error('Error 42.'); + } + } + + const target = new TestClass(); + + expect(() => target.test()).to.throw('Retry failed.'); + expect(target.count).to.equal(2); + }); + }); }); }); - describe('When method is called with retry options.', () => { - it('should throw with expected \'throw\' error.', async () => { - class TestClass { - @retry(3, { onError: 'throw' }) - async test() { - return Promise.reject('Error 42.'); - } - } + describe('When method is asynchrone.', () => { + describe('When method should return value.', () => { + describe('When method should work only with attempts number.', () => { + it('should resolve with expected result and attempts > 0.', async () => { + class TestClass { + @retry(3) + async test() { + return Promise.resolve('Success 42.'); + } + } - const target = new TestClass(); + const target = new TestClass(); + const result = await target.test(); - await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); - }); + expect(result).to.equal('Success 42.'); + }); - it('should throw with expected \'reject\' error.', async () => { - class TestClass { - @retry(3, { onError: 'reject' }) - async test() { - return Promise.reject('Error 42.'); - } - } + it('should resolve with expected result and attempts = 0.', async () => { + class TestClass { + @retry(0) + async test() { + return Promise.resolve('Success 42.'); + } + } - const target = new TestClass(); + const target = new TestClass(); + const result = await target.test(); - await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); - }); + expect(result).to.equal('Success 42.'); + }); - it('should ignore with \'ignore\' error.', async () => { - class TestClass { - @retry(3, { onError: 'ignore' }) - async test() { - return Promise.reject('Error 42.'); - } - } + it('should resolve with expected result and attempts < 0.', async () => { + class TestClass { + @retry(-3) + async test() { + return Promise.resolve('Success 42.'); + } + } - const target = new TestClass(); - const response = await target.test(); + const target = new TestClass(); + const result = await target.test(); - expect(response).to.equal(undefined); - }); + expect(result).to.equal('Success 42.'); + }); - it('should ignore with \'ignoreAsync\' error.', async () => { - class TestClass { - @retry(3, { onError: 'ignoreAsync' }) - async test() { - return Promise.reject('Error 42.'); - } - } + it('should resolve with expected result and attempts === null.', async () => { + class TestClass { + @retry(-3) + async test() { + return Promise.resolve('Success 42.'); + } + } - const target = new TestClass(); - const response = await target.test(); + const target = new TestClass(); + const result = await target.test(); - expect(response).to.equal(undefined); - }); + expect(result).to.equal('Success 42.'); + }); - it('should continue retring if error is filtered as expected', async () => { - class TestClass { - public retryIndex = 1; - @retry(3, { - errorFilter: (error) => { - return error.message === 'Error 42.'; - }, - }) - async test() { - if (this.retryIndex === 1) { - return Promise.reject('Error 42.'); - } - return Promise.reject('42'); - } - } - - const target = new TestClass(); - - await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + it('should resolve with expected result and attempts === undefined.', async () => { + class TestClass { + @retry(undefined) + async test() { + return Promise.resolve('Success 42.'); + } + } + + const target = new TestClass(); + const result = await target.test(); + + expect(result).to.equal('Success 42.'); + }); + }); }); - it('should stop retring if error is filtered as not expected', async () => { - class TestClass { - public retryIndex = 1; - @retry(3, { - errorFilter: (error) => { - return error.message === '42'; - }, - }) - async test() { - if (this.retryIndex === 1) { - return Promise.reject('Error 42.'); - } - return Promise.resolve('Success 42!'); - } - } - - const target = new TestClass(); - - await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + describe('When method should throw error.', () => { + describe('When method should work only with attempts number.', () => { + it('should reject with expected error and attempts > 0.', async () => { + class TestClass { + @retry(3) + test() { + return Promise.reject('Error 42.'); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should resolve with expected result and attempts = 0.', async () => { + class TestClass { + @retry(0) + async test() { + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should reject with expected error and attempts < 0.', async () => { + class TestClass { + @retry(-3) + async test() { + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should reject with expected error and attempts === null.', async () => { + class TestClass { + @retry(-3) + async test() { + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should reject with expected error and attempts === undefined.', async () => { + class TestClass { + @retry(undefined) + async test() { + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + }); + + describe('When method should return a specific error.', () => { + it('should throw with expected error.', async () => { + class TestClass { + @retry(3, { onError: 'reject' }) + async test() { + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should return undefined.', async () => { + class TestClass { + @retry(3, { onError: 'ignoreAsync' }) + async test() { + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + const response = await target.test(); + + expect(response).to.equal(undefined); + }); + }); + + // tslint:disable-next-line:max-line-length + describe('When method should reject for a specific error until attempts completted.', () => { + it('should throw with expected error.', async () => { + class TestClass { + @retry(3, { errorFilter: (err: Error) => err.message === 'Error 42.' }) + async test() { + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should throw with default error and throw when error is expected.', async () => { + class TestClass { + public count = 1; + @retry(3, { + errorFilter: (err: Error) => { + return err.message === 'error'; + }, + }) + async test() { + if (this.count === 2) { + return Promise.reject(new Error('error')); + } + + this.count += 1; + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + expect(target.count).to.equal(2); + }); + }); + + describe('When method should work correctly with wait pattern', () => { + it('should wait for a specific time when pattern is number', async () => { + class TestClass { + private count = 1; + private now; + public times = []; + @retry(3, { + waitPattern: 100, + }) + async test() { + if (this.count === 1) { + this.now = new Date().getTime(); + } else { + const newTime = new Date().getTime() - this.now; + + this.times.push(newTime); + this.now = new Date().getTime(); + } + this.count += 1; + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + target.times.forEach((time) => { + expect(Math.floor(time / 100) * 100).to.equal(100); + }); + }); + + it('should wait for a specific time when pattern is array of numbers', async () => { + class TestClass { + private count = 1; + private now; + public times = []; + @retry(3, { + waitPattern: [100, 200, 300], + }) + async test() { + if (this.count === 1) { + this.now = new Date().getTime(); + } else { + const newTime = new Date().getTime() - this.now; + + this.times.push(newTime); + this.now = new Date().getTime(); + } + this.count += 1; + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + target.times.forEach((time, index) => { + expect(Math.floor(time / 100) * 100).to.equal(100 * (index + 1)); + }); + }); + + it('should wait for a specific time when pattern is function', async () => { + class TestClass { + private count = 0; + private now; + public times = []; + @retry(3, { + waitPattern: attempt => attempt * 100, + }) + async test() { + if (this.count <= 1) { + this.now = new Date().getTime(); + } else { + const newTime = new Date().getTime() - this.now; + + this.times.push(newTime); + this.now = new Date().getTime(); + } + this.count += 1; + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); + target.times.forEach((time, index) => { + expect(Math.floor(time / 100) * 100).to.equal(100 * (index + 1)); + }); + }); + }); }); }); }); From 5c1088e8b312d9bb92b08e317c166aea381e9fe9 Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Wed, 13 Nov 2019 17:01:18 +0200 Subject: [PATCH 05/13] Retry decorator fix test --- test/retry.spec.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/retry.spec.ts b/test/retry.spec.ts index 9161bb1..8dc3dc1 100644 --- a/test/retry.spec.ts +++ b/test/retry.spec.ts @@ -521,6 +521,25 @@ describe.only('@retry', () => { expect(Math.floor(time / 100) * 100).to.equal(100 * (index + 1)); }); }); + + it('should reject error if type of wait pattern is wrong', async () => { + class TestClass { + private count = 0; + private now; + public times = []; + @retry(3, { + waitPattern: 'wait' as any, + }) + async test() { + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + + // tslint:disable-next-line:max-line-length + await expect(target.test()).to.eventually.be.rejectedWith('Option string is not supported for \'waitPattern\'.'); + }); }); }); }); From b3a4ab573d4954e486f684c042dc8e197f2a4fae Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Thu, 14 Nov 2019 21:18:52 +0200 Subject: [PATCH 06/13] add new test, remove unused code, simplifying function --- lib/retry/RetryOptions.ts | 3 +-- lib/retry/Retryer.ts | 4 ++-- lib/retry/WaitStrategy.ts | 13 +++++-------- lib/retry/index.ts | 2 +- lib/utils/index.ts | 9 ++++----- test/retry.spec.ts | 24 ++++++++++++++++++++++-- 6 files changed, 35 insertions(+), 20 deletions(-) diff --git a/lib/retry/RetryOptions.ts b/lib/retry/RetryOptions.ts index 75b28f4..9c2d861 100644 --- a/lib/retry/RetryOptions.ts +++ b/lib/retry/RetryOptions.ts @@ -37,8 +37,7 @@ export type MethodOptions = { args: any, }; -export const DEFAULT_ON_ERROR = 'throw'; export const DEFAULT_ERROR = 'Retry failed.'; export const DEFAULT_OPTIONS: RetryOptions = { - errorFilter: () => { return true; }, + errorFilter: () => true, }; diff --git a/lib/retry/Retryer.ts b/lib/retry/Retryer.ts index 30440fd..80e2077 100644 --- a/lib/retry/Retryer.ts +++ b/lib/retry/Retryer.ts @@ -1,5 +1,5 @@ import { raiseStrategy } from '../utils'; -import { DEFAULT_ERROR, DEFAULT_ON_ERROR, MethodOptions, RetryOptions } from './RetryOptions'; +import { DEFAULT_ERROR, MethodOptions, RetryOptions } from './RetryOptions'; export class Retryer { constructor( @@ -19,7 +19,7 @@ export class Retryer { } private error(): void | Promise { - const raise = raiseStrategy({ onError: this.options.onError }, DEFAULT_ON_ERROR); + const raise = raiseStrategy(this.options); return raise(new Error(DEFAULT_ERROR)); } } diff --git a/lib/retry/WaitStrategy.ts b/lib/retry/WaitStrategy.ts index e0843b2..8eb1eaa 100644 --- a/lib/retry/WaitStrategy.ts +++ b/lib/retry/WaitStrategy.ts @@ -6,16 +6,16 @@ export class WaitStrategy { private readonly waitPattern: WaitPattern, ) { } - public wait(index: number, instance: any): Promise { + public wait(index: number): Promise { if (!this.waitPattern) { return Promise.resolve(); } - const timeout = this.getTimeout(index, instance) || 0; + const timeout = this.getTimeout(index) || 0; return new Promise(resolve => setTimeout(resolve, timeout)); } - private getTimeout(index: number, instance: any): number { + private getTimeout(index: number): number { const patternType = Array.isArray(this.waitPattern) ? 'array' : typeof this.waitPattern; @@ -25,11 +25,8 @@ export class WaitStrategy { return this.waitPattern as number; case 'array': const values = this.waitPattern as number[]; - const timeout = index > values.length - ? values[values.length - 1] - : values[index]; - - return timeout; + const count = values.length; + return index > count ? values[count - 1] : values[index]; case 'function': return (this.waitPattern as Function)(index); default: diff --git a/lib/retry/index.ts b/lib/retry/index.ts index b343bb3..357505e 100644 --- a/lib/retry/index.ts +++ b/lib/retry/index.ts @@ -35,7 +35,7 @@ export function retry(attempts: number, options?: RetryOptions): any { return response.catch((err) => { retryCount += 1; - return waitStrategy.wait(retryCount - 1, methodOptions.instance) + return waitStrategy.wait(retryCount - 1) .then(() => retryer.retry(err, attempts, retryCount)); }); } diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 6524160..a8e46ad 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1,10 +1,9 @@ +import { RetryOptions } from '../retry'; -type StrategyOptions = { - onError?: 'throw' | 'reject' | 'ignore' | 'ignoreAsync'; -}; +const DEFAULT_ON_ERROR = 'throw'; -export function raiseStrategy(options: StrategyOptions, defaultStrategy: string) { - const value = options && options.onError || defaultStrategy; +export function raiseStrategy(options: RetryOptions) { + const value = options && options.onError || DEFAULT_ON_ERROR; switch (value) { case 'reject': diff --git a/test/retry.spec.ts b/test/retry.spec.ts index 8dc3dc1..b0b6351 100644 --- a/test/retry.spec.ts +++ b/test/retry.spec.ts @@ -360,6 +360,28 @@ describe.only('@retry', () => { await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); }); + + // tslint:disable-next-line:max-line-length + it('should work if the method fails 2 times and succeeds only on the third time.', async () => { + class TestClass { + public count = 1; + @retry(3) + async test() { + if (this.count === 3) { + return Promise.resolve('Success 42!'); + } + + this.count += 1; + return Promise.reject(new Error('Error 42.')); + } + } + + const target = new TestClass(); + const response = await target.test(); + + expect(response).to.equal('Success 42!'); + expect(target.count).to.equal(3); + }); }); describe('When method should return a specific error.', () => { @@ -524,8 +546,6 @@ describe.only('@retry', () => { it('should reject error if type of wait pattern is wrong', async () => { class TestClass { - private count = 0; - private now; public times = []; @retry(3, { waitPattern: 'wait' as any, From fdfc7683b8975e04e10d29bf7b4068369bd4f5b3 Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Fri, 15 Nov 2019 12:11:58 +0200 Subject: [PATCH 07/13] Retry decorator, fix count scope --- lib/retry/Counter.ts | 13 +++++++++++++ lib/retry/ScopeCounter.ts | 16 ++++++++++++++++ lib/retry/index.ts | 27 ++++++++++++++++----------- 3 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 lib/retry/Counter.ts create mode 100644 lib/retry/ScopeCounter.ts diff --git a/lib/retry/Counter.ts b/lib/retry/Counter.ts new file mode 100644 index 0000000..0429ea1 --- /dev/null +++ b/lib/retry/Counter.ts @@ -0,0 +1,13 @@ +export class Counter { + private count: number = 0; + + public get(): number { + return this.count; + } + + public next(): number { + this.count += 1; + + return this.count; + } +} diff --git a/lib/retry/ScopeCounter.ts b/lib/retry/ScopeCounter.ts new file mode 100644 index 0000000..d110074 --- /dev/null +++ b/lib/retry/ScopeCounter.ts @@ -0,0 +1,16 @@ +import { Counter } from './Counter'; + +export class ScopeCounter { + private readonly map: WeakMap = new WeakMap(); + + public getCounter(instance): Counter { + return this.map.get(instance) || this.createCounter(instance); + } + + private createCounter(instance): Counter { + const counter = new Counter(); + this.map.set(instance, counter); + + return counter; + } +} diff --git a/lib/retry/index.ts b/lib/retry/index.ts index 357505e..7efd5d4 100644 --- a/lib/retry/index.ts +++ b/lib/retry/index.ts @@ -1,5 +1,6 @@ import { Retryer } from './Retryer'; import { DEFAULT_OPTIONS, MethodOptions, RetryOptions } from './RetryOptions'; +import { ScopeCounter } from './ScopeCounter'; import { WaitStrategy } from './WaitStrategy'; export { RetryOptions }; @@ -13,13 +14,17 @@ export { RetryOptions }; */ export function retry(attempts: number, options?: RetryOptions): any { return function (target: any, propertyKey: any, descriptor: PropertyDescriptor) { + const method: Function = descriptor.value; const retryOptions = { ...DEFAULT_OPTIONS, ...options }; const waitStrategy = new WaitStrategy(retryOptions.waitPattern); - - let retryCount: number = 0; + const scope = new ScopeCounter(); descriptor.value = function () { + const counter = scope.getCounter(this); + + let count = counter.get(); + const methodOptions: MethodOptions = { instance: this, args: arguments, @@ -28,22 +33,22 @@ export function retry(attempts: number, options?: RetryOptions): any { const retryer = new Retryer(retryOptions, methodOptions); try { - const response = method.apply(this, arguments); + let response = method.apply(this, arguments); const isPromiseLike = response && typeof response.then === 'function'; if (isPromiseLike) { - return response.catch((err) => { - retryCount += 1; - - return waitStrategy.wait(retryCount - 1) - .then(() => retryer.retry(err, attempts, retryCount)); - }); + response = response.catch(err => + waitStrategy.wait(count) + .then(() => { + count = counter.next(); + return retryer.retry(err, attempts, count); + })); } return response; } catch (err) { - retryCount += 1; - return retryer.retry(err, attempts, retryCount); + count = counter.next(); + return retryer.retry(err, attempts, count); } }; From 8385a84e79cd4a56db3a520d704c3ebb0680db55 Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Fri, 15 Nov 2019 14:24:17 +0200 Subject: [PATCH 08/13] remove switch case statement --- lib/retry/WaitStrategy.ts | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/retry/WaitStrategy.ts b/lib/retry/WaitStrategy.ts index 8eb1eaa..2e6884a 100644 --- a/lib/retry/WaitStrategy.ts +++ b/lib/retry/WaitStrategy.ts @@ -16,21 +16,20 @@ export class WaitStrategy { } private getTimeout(index: number): number { - const patternType = Array.isArray(this.waitPattern) - ? 'array' - : typeof this.waitPattern; - - switch (patternType) { - case 'number': - return this.waitPattern as number; - case 'array': - const values = this.waitPattern as number[]; - const count = values.length; - return index > count ? values[count - 1] : values[index]; - case 'function': - return (this.waitPattern as Function)(index); - default: - throw new Error(`Option ${patternType} is not supported for 'waitPattern'.`); + if (Array.isArray(this.waitPattern)) { + const values = this.waitPattern as number[]; + const count = values.length; + return index > count ? values[count - 1] : values[index]; } + + if (typeof this.waitPattern === 'number') { + return this.waitPattern as number; + } + + if (typeof this.waitPattern === 'function') { + return (this.waitPattern as Function)(index); + } + + throw new Error(`Option ${typeof this.waitPattern} is not supported for 'waitPattern'.`); } } From 66fc5e855be4083208785d772dc974bc69d938ba Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Fri, 15 Nov 2019 15:40:20 +0200 Subject: [PATCH 09/13] remove unused code --- test/retry.spec.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/retry.spec.ts b/test/retry.spec.ts index b0b6351..74a055b 100644 --- a/test/retry.spec.ts +++ b/test/retry.spec.ts @@ -1,11 +1,7 @@ import { expect } from 'chai'; import { retry } from '../lib'; -function wait(timeout: number = 0): Promise { - return new Promise(resolve => setTimeout(resolve, timeout)); -} - -describe.only('@retry', () => { +describe('@retry', () => { describe('When method is synchrone.', () => { describe('When method should return value.', () => { describe('When method should work only with attempts number.', () => { From 7f832a163fcad173908bcbbf4c36bfb18e2d61a6 Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Sat, 16 Nov 2019 19:18:49 +0200 Subject: [PATCH 10/13] Retry decorator tests --- test/retry.spec.ts | 101 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/test/retry.spec.ts b/test/retry.spec.ts index 74a055b..1f25a42 100644 --- a/test/retry.spec.ts +++ b/test/retry.spec.ts @@ -1,7 +1,11 @@ import { expect } from 'chai'; import { retry } from '../lib'; +import { Counter } from '../lib/retry/Counter'; +import { Retryer } from '../lib/retry/Retryer'; +import { WaitStrategy } from '../lib/retry/WaitStrategy'; +import { MethodOptions } from '../lib/retry/RetryOptions'; -describe('@retry', () => { +describe.only('@retry', () => { describe('When method is synchrone.', () => { describe('When method should return value.', () => { describe('When method should work only with attempts number.', () => { @@ -476,7 +480,7 @@ describe('@retry', () => { await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); target.times.forEach((time) => { - expect(Math.floor(time / 100) * 100).to.equal(100); + expect(time).to.be.approximately(100, 5); }); }); @@ -506,7 +510,7 @@ describe('@retry', () => { await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); target.times.forEach((time, index) => { - expect(Math.floor(time / 100) * 100).to.equal(100 * (index + 1)); + expect(time).to.be.approximately(100 * (index + 1), 5); }); }); @@ -536,7 +540,7 @@ describe('@retry', () => { await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); target.times.forEach((time, index) => { - expect(Math.floor(time / 100) * 100).to.equal(100 * (index + 1)); + expect(time).to.be.approximately(100 * (index + 1), 5); }); }); @@ -559,4 +563,93 @@ describe('@retry', () => { }); }); }); + + describe('Counter class', () => { + let counter: Counter; + + beforeEach(() => { + counter = new Counter(); + }); + + it('should return count of counter when inited', () => { + const count = counter.get(); + + expect(count).to.equal(0); + }); + + it('should return incremented count of counter', () => { + let count = counter.get(); + counter.next(); + count = counter.next(); + + expect(count).to.equal(2); + }); + }); + + describe('WaitStrategy class', () => { + let strategy: WaitStrategy; + + it('should delay expected time when pattern is of type number', async () => { + strategy = new WaitStrategy(400); + const delay = await getFunctionDelay(() => strategy.wait(0)); + expect(delay).to.be.approximately(400, 5); + }); + + it('should delay expected time when pattern is of type function', async () => { + strategy = new WaitStrategy(() => { return 300; }); + const delay = await getFunctionDelay(() => strategy.wait(1)); + expect(delay).to.be.approximately(300, 5); + }); + + it('should delay expected time when pattern is of type array', async () => { + strategy = new WaitStrategy([100, 300, 200]); + + let delay = await getFunctionDelay(() => strategy.wait(0)); + expect(delay).to.be.approximately(100, 5); + + delay = await getFunctionDelay(() => strategy.wait(1)); + expect(delay).to.be.approximately(300, 5); + + delay = await getFunctionDelay(() => strategy.wait(2)); + expect(delay).to.be.approximately(200, 5); + }); + }); + + describe('Retryer class', () => { + let retryer: Retryer; + + it('should throw error if no attempts', () => { + const options = {}; + const methodOptions: MethodOptions = { + instance: {}, + args: {}, + method: () => { }, + }; + + retryer = new Retryer(options, methodOptions); + + expect(() => retryer.retry(new Error(), null as any, 1)).to.throw('Retry failed.'); + }); + + it('should throw error if retry count exeeded attempts count', () => { + const options = {}; + const methodOptions: MethodOptions = { + instance: {}, + args: {}, + method: () => { }, + }; + + retryer = new Retryer(options, methodOptions); + + expect(() => retryer.retry(new Error(), 3, 4)).to.throw('Retry failed.'); + }); + }); }); + +async function getFunctionDelay(method: Function): Promise { + const time = new Date().getTime(); + + await method(); + + return new Date().getTime() - time; +} From 6db706874dda74f012ea4d5680e9073412c63b28 Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Sat, 16 Nov 2019 19:23:11 +0200 Subject: [PATCH 11/13] ReMove .only --- test/retry.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/retry.spec.ts b/test/retry.spec.ts index 1f25a42..fb3009a 100644 --- a/test/retry.spec.ts +++ b/test/retry.spec.ts @@ -5,7 +5,7 @@ import { Retryer } from '../lib/retry/Retryer'; import { WaitStrategy } from '../lib/retry/WaitStrategy'; import { MethodOptions } from '../lib/retry/RetryOptions'; -describe.only('@retry', () => { +describe('@retry', () => { describe('When method is synchrone.', () => { describe('When method should return value.', () => { describe('When method should work only with attempts number.', () => { From bc449ff66b7cac4e6367fc791538e9071413de0f Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Wed, 20 Nov 2019 15:16:39 +0200 Subject: [PATCH 12/13] Retry decorator, fixes --- lib/retry/Counter.ts | 13 --- lib/retry/RetryOptions.ts | 6 -- lib/retry/Retryer.ts | 73 ++++++++++++--- lib/retry/ScopeCounter.ts | 16 ---- lib/retry/index.ts | 38 +------- test/retry/Retryer.spec.ts | 108 ++++++++++++++++++++++ test/retry/WaitStrategy.spec.ts | 39 ++++++++ test/{ => retry}/retry.spec.ts | 159 +++++--------------------------- 8 files changed, 235 insertions(+), 217 deletions(-) delete mode 100644 lib/retry/Counter.ts delete mode 100644 lib/retry/ScopeCounter.ts create mode 100644 test/retry/Retryer.spec.ts create mode 100644 test/retry/WaitStrategy.spec.ts rename test/{ => retry}/retry.spec.ts (73%) diff --git a/lib/retry/Counter.ts b/lib/retry/Counter.ts deleted file mode 100644 index 0429ea1..0000000 --- a/lib/retry/Counter.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class Counter { - private count: number = 0; - - public get(): number { - return this.count; - } - - public next(): number { - this.count += 1; - - return this.count; - } -} diff --git a/lib/retry/RetryOptions.ts b/lib/retry/RetryOptions.ts index 9c2d861..de36b45 100644 --- a/lib/retry/RetryOptions.ts +++ b/lib/retry/RetryOptions.ts @@ -31,12 +31,6 @@ export type RetryOptions = { export type WaitPattern = number | number[] | ((attempt: number) => number); -export type MethodOptions = { - method: Function, - instance: any, - args: any, -}; - export const DEFAULT_ERROR = 'Retry failed.'; export const DEFAULT_OPTIONS: RetryOptions = { errorFilter: () => true, diff --git a/lib/retry/Retryer.ts b/lib/retry/Retryer.ts index 80e2077..8402c57 100644 --- a/lib/retry/Retryer.ts +++ b/lib/retry/Retryer.ts @@ -1,25 +1,76 @@ import { raiseStrategy } from '../utils'; -import { DEFAULT_ERROR, MethodOptions, RetryOptions } from './RetryOptions'; +import { DEFAULT_ERROR, DEFAULT_OPTIONS, RetryOptions } from './RetryOptions'; +import { WaitStrategy } from './WaitStrategy'; export class Retryer { + private attempts: number = 0; + private retryOptions: RetryOptions = DEFAULT_OPTIONS; + constructor( private readonly options: RetryOptions, - private readonly methodOptions: MethodOptions, - ) { } + private readonly method: any, + private readonly instance: any, + private readonly retryCount: number, + ) { + this.attempts = (!retryCount || retryCount < 0) ? 0 : retryCount; + this.retryOptions = { ...this.retryOptions, ...options }; + } + + public getResponse(): any | Promise { + try { + const response = this.method(); + const isPromiseLike = response && typeof response.then === 'function'; + + return isPromiseLike ? this.getAsyncResponse(response) : response; + } catch (err) { + const isFiltered = this.retryOptions.errorFilter.bind(this.instance)(err); + + return !isFiltered ? this.error() : this.retryGetSyncResponse(); + } + } + + private retryGetSyncResponse(): any { + for (let index = 0; index < this.attempts; index += 1) { + try { + return this.method(); + } catch (err) { + const isFiltered = this.retryOptions.errorFilter.bind(this.instance)(err); + + if (!isFiltered) { + return this.error(); + } + } + } + + return this.error(); + } + + // tslint:disable-next-line:max-line-length + private async getAsyncResponse(asyncResponse: any): Promise { + for (let index = 0; index <= this.attempts; index += 1) { + if (index > 0) { + const waitStrategy = new WaitStrategy(this.retryOptions.waitPattern); + await waitStrategy.wait(index - 1); + } + + try { + const response = index === 0 ? await asyncResponse : await this.method(); - public retry(error: Error, attempts: number, count: number): any { - const { instance } = this.methodOptions; + return response; + } catch (err) { + const isFiltered = this.retryOptions.errorFilter.bind(this.instance)(err); - if (!attempts || attempts < count || !this.options.errorFilter.bind(instance)(error)) { - return this.error(); + if (!isFiltered) { + return this.error(); + } + } } - const { method, args } = this.methodOptions; - return method.bind(instance)(args); + return this.error(); } - private error(): void | Promise { - const raise = raiseStrategy(this.options); + private error() { + const raise = raiseStrategy(this.retryOptions); return raise(new Error(DEFAULT_ERROR)); } } diff --git a/lib/retry/ScopeCounter.ts b/lib/retry/ScopeCounter.ts deleted file mode 100644 index d110074..0000000 --- a/lib/retry/ScopeCounter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Counter } from './Counter'; - -export class ScopeCounter { - private readonly map: WeakMap = new WeakMap(); - - public getCounter(instance): Counter { - return this.map.get(instance) || this.createCounter(instance); - } - - private createCounter(instance): Counter { - const counter = new Counter(); - this.map.set(instance, counter); - - return counter; - } -} diff --git a/lib/retry/index.ts b/lib/retry/index.ts index 7efd5d4..84f566e 100644 --- a/lib/retry/index.ts +++ b/lib/retry/index.ts @@ -1,7 +1,5 @@ import { Retryer } from './Retryer'; -import { DEFAULT_OPTIONS, MethodOptions, RetryOptions } from './RetryOptions'; -import { ScopeCounter } from './ScopeCounter'; -import { WaitStrategy } from './WaitStrategy'; +import { DEFAULT_OPTIONS, RetryOptions } from './RetryOptions'; export { RetryOptions }; @@ -16,40 +14,12 @@ export function retry(attempts: number, options?: RetryOptions): any { return function (target: any, propertyKey: any, descriptor: PropertyDescriptor) { const method: Function = descriptor.value; - const retryOptions = { ...DEFAULT_OPTIONS, ...options }; - const waitStrategy = new WaitStrategy(retryOptions.waitPattern); - const scope = new ScopeCounter(); descriptor.value = function () { - const counter = scope.getCounter(this); + const args = arguments; + const retryer = new Retryer(options, () => method.apply(this, args), this, attempts); - let count = counter.get(); - - const methodOptions: MethodOptions = { - instance: this, - args: arguments, - method: target[propertyKey], - }; - const retryer = new Retryer(retryOptions, methodOptions); - - try { - let response = method.apply(this, arguments); - const isPromiseLike = response && typeof response.then === 'function'; - - if (isPromiseLike) { - response = response.catch(err => - waitStrategy.wait(count) - .then(() => { - count = counter.next(); - return retryer.retry(err, attempts, count); - })); - } - - return response; - } catch (err) { - count = counter.next(); - return retryer.retry(err, attempts, count); - } + return retryer.getResponse(); }; return descriptor; diff --git a/test/retry/Retryer.spec.ts b/test/retry/Retryer.spec.ts new file mode 100644 index 0000000..1db271c --- /dev/null +++ b/test/retry/Retryer.spec.ts @@ -0,0 +1,108 @@ +import { expect } from 'chai'; +import { RetryOptions } from '../../lib'; +import { Retryer } from '../../lib/retry/Retryer'; + +describe('Retryer class', () => { + let retryer: Retryer; + + describe('when called method is synchrone', () => { + it('should return result', () => { + retryer = new Retryer({} as RetryOptions, () => 'Success 42!', {} as any, 3); + + expect(retryer.getResponse()).to.equal('Success 42!'); + }); + + it('should throw error with message \'Retry failed\'', () => { + retryer = new Retryer( + {} as RetryOptions, + () => { throw new Error('Failed 42'); }, + {} as any, + 3, + ); + + expect(() => retryer.getResponse()).to.throw('Retry failed.'); + }); + + it('should return result if throw\'n error is not filtered as expected', () => { + retryer = new Retryer( + { errorFilter: (err: Error) => err.message === 'Error 42.' } as RetryOptions, + () => { throw new Error('Error.'); }, + {} as any, + 3, + ); + + expect(() => retryer.getResponse()).to.throw('Retry failed.'); + }); + }); + + describe('when called method is asynchrone', () => { + it('should return result', async () => { + retryer = new Retryer({} as RetryOptions, () => Promise.resolve('Success 42!'), {} as any, 3); + const response = await retryer.getResponse(); + + expect(response).to.equal('Success 42!'); + }); + + it('should throw error with message \'Retry failed\'', async () => { + retryer = new Retryer( + {} as RetryOptions, + () => Promise.reject('Failed 42.'), + {} as any, + 3, + ); + + await expect(retryer.getResponse()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + it('should return result if throw\'n error is not filtered as expected', async () => { + retryer = new Retryer( + { errorFilter: (err: Error) => err.message === 'Error 42.' } as RetryOptions, + () => Promise.reject('Error 42.'), + {} as any, + 3, + ); + + await expect(retryer.getResponse()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + describe('when method should wait before retry', () => { + it('should delay expected time when pattern is of type number', async () => { + retryer = new Retryer( + { waitPattern: 400 } as RetryOptions, + () => Promise.reject('Error 42.'), + {} as any, + 3, + ); + + const delay = await getFunctionDelay(async () => { + return await expect(retryer.getResponse()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + expect(delay).to.be.approximately(1200, 15); + }); + + it('should delay expected time when pattern is of type function', async () => { + retryer = new Retryer( + { waitPattern: () => { return 300; } } as RetryOptions, + () => Promise.reject('Error 42.'), + {} as any, + 3, + ); + + const delay = await getFunctionDelay(async () => { + return await expect(retryer.getResponse()).to.eventually.be.rejectedWith('Retry failed.'); + }); + + expect(delay).to.be.approximately(900, 15); + }); + }); + }); +}); + +async function getFunctionDelay(method: Function): Promise { + const time = new Date().getTime(); + + await method(); + + return new Date().getTime() - time; +} diff --git a/test/retry/WaitStrategy.spec.ts b/test/retry/WaitStrategy.spec.ts new file mode 100644 index 0000000..cade490 --- /dev/null +++ b/test/retry/WaitStrategy.spec.ts @@ -0,0 +1,39 @@ +import { expect } from 'chai'; +import { WaitStrategy } from '../../lib/retry/WaitStrategy'; + +describe('WaitStrategy class', () => { + let strategy: WaitStrategy; + + it('should delay expected time when pattern is of type number', async () => { + strategy = new WaitStrategy(400); + const delay = await getFunctionDelay(() => strategy.wait(0)); + expect(delay).to.be.approximately(400, 5); + }); + + it('should delay expected time when pattern is of type function', async () => { + strategy = new WaitStrategy(() => { return 300; }); + const delay = await getFunctionDelay(() => strategy.wait(1)); + expect(delay).to.be.approximately(300, 5); + }); + + it('should delay expected time when pattern is of type array', async () => { + strategy = new WaitStrategy([100, 300, 200]); + + let delay = await getFunctionDelay(() => strategy.wait(0)); + expect(delay).to.be.approximately(100, 5); + + delay = await getFunctionDelay(() => strategy.wait(1)); + expect(delay).to.be.approximately(300, 5); + + delay = await getFunctionDelay(() => strategy.wait(2)); + expect(delay).to.be.approximately(200, 5); + }); +}); + +async function getFunctionDelay(method: Function): Promise { + const time = new Date().getTime(); + + await method(); + + return new Date().getTime() - time; +} diff --git a/test/retry.spec.ts b/test/retry/retry.spec.ts similarity index 73% rename from test/retry.spec.ts rename to test/retry/retry.spec.ts index fb3009a..245d739 100644 --- a/test/retry.spec.ts +++ b/test/retry/retry.spec.ts @@ -1,15 +1,11 @@ import { expect } from 'chai'; -import { retry } from '../lib'; -import { Counter } from '../lib/retry/Counter'; -import { Retryer } from '../lib/retry/Retryer'; -import { WaitStrategy } from '../lib/retry/WaitStrategy'; -import { MethodOptions } from '../lib/retry/RetryOptions'; +import { retry } from '../../lib'; describe('@retry', () => { describe('When method is synchrone.', () => { describe('When method should return value.', () => { describe('When method should work only with attempts number.', () => { - it('should resolve with expected result and attempts > 0.', () => { + it('should resolve with expected result and attempts greater than 0.', () => { class TestClass { @retry(3) test() { @@ -23,7 +19,7 @@ describe('@retry', () => { expect(result).to.equal('Success 42.'); }); - it('should resolve with expected result and attempts = 0.', () => { + it('should resolve with expected result and attempts equal with 0.', () => { class TestClass { @retry(0) test() { @@ -37,7 +33,7 @@ describe('@retry', () => { expect(result).to.equal('Success 42.'); }); - it('should resolve with expected result and attempts < 0.', () => { + it('should resolve with expected result and attempts less than 0.', () => { class TestClass { @retry(-3) test() { @@ -51,7 +47,7 @@ describe('@retry', () => { expect(result).to.equal('Success 42.'); }); - it('should resolve with expected result and attempts === null.', () => { + it('should resolve with expected result and attempts equal with null.', () => { class TestClass { @retry(-3) test() { @@ -65,7 +61,7 @@ describe('@retry', () => { expect(result).to.equal('Success 42.'); }); - it('should resolve with expected result and attempts === undefined.', () => { + it('should resolve with expected result and attempts equal with undefined.', () => { class TestClass { @retry(undefined) test() { @@ -83,7 +79,7 @@ describe('@retry', () => { describe('When method should throw error.', () => { describe('When method should work only with attempts number.', () => { - it('should reject with expected error and attempts > 0.', () => { + it('should reject with expected error and attempts greater than 0.', () => { class TestClass { @retry(3) test() { @@ -96,7 +92,7 @@ describe('@retry', () => { expect(() => target.test()).to.throw('Retry failed.'); }); - it('should resolve with expected result and attempts = 0.', () => { + it('should resolve with expected result and attempts equal with 0.', () => { class TestClass { @retry(0) test() { @@ -109,7 +105,7 @@ describe('@retry', () => { expect(() => target.test()).to.throw('Retry failed.'); }); - it('should reject with expected error and attempts < 0.', () => { + it('should reject with expected error and attempts less than 0.', () => { class TestClass { @retry(-3) test() { @@ -122,7 +118,7 @@ describe('@retry', () => { expect(() => target.test()).to.throw('Retry failed.'); }); - it('should reject with expected error and attempts === null.', () => { + it('should reject with expected error and attempts equal with null.', () => { class TestClass { @retry(-3) test() { @@ -135,7 +131,7 @@ describe('@retry', () => { expect(() => target.test()).to.throw('Retry failed.'); }); - it('should reject with expected error and attempts === undefined.', () => { + it('should reject with expected error and attempts equal with undefined.', () => { class TestClass { @retry(undefined) test() { @@ -222,7 +218,7 @@ describe('@retry', () => { describe('When method is asynchrone.', () => { describe('When method should return value.', () => { describe('When method should work only with attempts number.', () => { - it('should resolve with expected result and attempts > 0.', async () => { + it('should resolve with expected result and attempts greater than 0.', async () => { class TestClass { @retry(3) async test() { @@ -236,7 +232,7 @@ describe('@retry', () => { expect(result).to.equal('Success 42.'); }); - it('should resolve with expected result and attempts = 0.', async () => { + it('should resolve with expected result and attempts equal with 0.', async () => { class TestClass { @retry(0) async test() { @@ -250,7 +246,7 @@ describe('@retry', () => { expect(result).to.equal('Success 42.'); }); - it('should resolve with expected result and attempts < 0.', async () => { + it('should resolve with expected result and attempts less than 0.', async () => { class TestClass { @retry(-3) async test() { @@ -264,7 +260,7 @@ describe('@retry', () => { expect(result).to.equal('Success 42.'); }); - it('should resolve with expected result and attempts === null.', async () => { + it('should resolve with expected result and attempts equal with null.', async () => { class TestClass { @retry(-3) async test() { @@ -278,7 +274,7 @@ describe('@retry', () => { expect(result).to.equal('Success 42.'); }); - it('should resolve with expected result and attempts === undefined.', async () => { + it('should resolve with expected result and attempts equal with undefined.', async () => { class TestClass { @retry(undefined) async test() { @@ -296,10 +292,10 @@ describe('@retry', () => { describe('When method should throw error.', () => { describe('When method should work only with attempts number.', () => { - it('should reject with expected error and attempts > 0.', async () => { + it('should reject with expected error and attempts greater than 0.', async () => { class TestClass { @retry(3) - test() { + async test() { return Promise.reject('Error 42.'); } } @@ -309,7 +305,7 @@ describe('@retry', () => { await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); }); - it('should resolve with expected result and attempts = 0.', async () => { + it('should resolve with expected result and attempts equal with 0.', async () => { class TestClass { @retry(0) async test() { @@ -322,7 +318,7 @@ describe('@retry', () => { await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); }); - it('should reject with expected error and attempts < 0.', async () => { + it('should reject with expected error and attempts less than 0.', async () => { class TestClass { @retry(-3) async test() { @@ -335,7 +331,7 @@ describe('@retry', () => { await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); }); - it('should reject with expected error and attempts === null.', async () => { + it('should reject with expected error and attempts equal with null.', async () => { class TestClass { @retry(-3) async test() { @@ -348,7 +344,7 @@ describe('@retry', () => { await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); }); - it('should reject with expected error and attempts === undefined.', async () => { + it('should reject with expected error and attempts equal with undefined.', async () => { class TestClass { @retry(undefined) async test() { @@ -360,28 +356,6 @@ describe('@retry', () => { await expect(target.test()).to.eventually.be.rejectedWith('Retry failed.'); }); - - // tslint:disable-next-line:max-line-length - it('should work if the method fails 2 times and succeeds only on the third time.', async () => { - class TestClass { - public count = 1; - @retry(3) - async test() { - if (this.count === 3) { - return Promise.resolve('Success 42!'); - } - - this.count += 1; - return Promise.reject(new Error('Error 42.')); - } - } - - const target = new TestClass(); - const response = await target.test(); - - expect(response).to.equal('Success 42!'); - expect(target.count).to.equal(3); - }); }); describe('When method should return a specific error.', () => { @@ -563,93 +537,4 @@ describe('@retry', () => { }); }); }); - - describe('Counter class', () => { - let counter: Counter; - - beforeEach(() => { - counter = new Counter(); - }); - - it('should return count of counter when inited', () => { - const count = counter.get(); - - expect(count).to.equal(0); - }); - - it('should return incremented count of counter', () => { - let count = counter.get(); - counter.next(); - count = counter.next(); - - expect(count).to.equal(2); - }); - }); - - describe('WaitStrategy class', () => { - let strategy: WaitStrategy; - - it('should delay expected time when pattern is of type number', async () => { - strategy = new WaitStrategy(400); - const delay = await getFunctionDelay(() => strategy.wait(0)); - expect(delay).to.be.approximately(400, 5); - }); - - it('should delay expected time when pattern is of type function', async () => { - strategy = new WaitStrategy(() => { return 300; }); - const delay = await getFunctionDelay(() => strategy.wait(1)); - expect(delay).to.be.approximately(300, 5); - }); - - it('should delay expected time when pattern is of type array', async () => { - strategy = new WaitStrategy([100, 300, 200]); - - let delay = await getFunctionDelay(() => strategy.wait(0)); - expect(delay).to.be.approximately(100, 5); - - delay = await getFunctionDelay(() => strategy.wait(1)); - expect(delay).to.be.approximately(300, 5); - - delay = await getFunctionDelay(() => strategy.wait(2)); - expect(delay).to.be.approximately(200, 5); - }); - }); - - describe('Retryer class', () => { - let retryer: Retryer; - - it('should throw error if no attempts', () => { - const options = {}; - const methodOptions: MethodOptions = { - instance: {}, - args: {}, - method: () => { }, - }; - - retryer = new Retryer(options, methodOptions); - - expect(() => retryer.retry(new Error(), null as any, 1)).to.throw('Retry failed.'); - }); - - it('should throw error if retry count exeeded attempts count', () => { - const options = {}; - const methodOptions: MethodOptions = { - instance: {}, - args: {}, - method: () => { }, - }; - - retryer = new Retryer(options, methodOptions); - - expect(() => retryer.retry(new Error(), 3, 4)).to.throw('Retry failed.'); - }); - }); }); - -async function getFunctionDelay(method: Function): Promise { - const time = new Date().getTime(); - - await method(); - - return new Date().getTime() - time; -} From 6e50717e8869caf634026af498085679afdf65f5 Mon Sep 17 00:00:00 2001 From: Nicolae Caliman Date: Mon, 13 Apr 2020 12:17:58 +0300 Subject: [PATCH 13/13] Fix intendations --- lib/retry/Retryer.ts | 33 +++++++++++++++++---------------- lib/retry/WaitStrategy.ts | 2 ++ lib/retry/index.ts | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/retry/Retryer.ts b/lib/retry/Retryer.ts index 8402c57..84d5644 100644 --- a/lib/retry/Retryer.ts +++ b/lib/retry/Retryer.ts @@ -4,7 +4,7 @@ import { WaitStrategy } from './WaitStrategy'; export class Retryer { private attempts: number = 0; - private retryOptions: RetryOptions = DEFAULT_OPTIONS; + private readonly retryOptions: RetryOptions = { ...DEFAULT_OPTIONS, ...this.options }; constructor( private readonly options: RetryOptions, @@ -12,8 +12,7 @@ export class Retryer { private readonly instance: any, private readonly retryCount: number, ) { - this.attempts = (!retryCount || retryCount < 0) ? 0 : retryCount; - this.retryOptions = { ...this.retryOptions, ...options }; + this.attempts = (!this.retryCount || this.retryCount < 0) ? 0 : this.retryCount; } public getResponse(): any | Promise { @@ -25,7 +24,7 @@ export class Retryer { } catch (err) { const isFiltered = this.retryOptions.errorFilter.bind(this.instance)(err); - return !isFiltered ? this.error() : this.retryGetSyncResponse(); + return isFiltered ? this.retryGetSyncResponse() : this.error(); } } @@ -34,9 +33,9 @@ export class Retryer { try { return this.method(); } catch (err) { - const isFiltered = this.retryOptions.errorFilter.bind(this.instance)(err); + const filteredError = this.retryOptions.errorFilter.bind(this.instance)(err); - if (!isFiltered) { + if (!filteredError) { return this.error(); } } @@ -45,22 +44,16 @@ export class Retryer { return this.error(); } - // tslint:disable-next-line:max-line-length private async getAsyncResponse(asyncResponse: any): Promise { for (let index = 0; index <= this.attempts; index += 1) { - if (index > 0) { - const waitStrategy = new WaitStrategy(this.retryOptions.waitPattern); - await waitStrategy.wait(index - 1); - } + await this.waitBeforeResponse(index); try { - const response = index === 0 ? await asyncResponse : await this.method(); - - return response; + return index === 0 ? await asyncResponse : await this.method(); } catch (err) { - const isFiltered = this.retryOptions.errorFilter.bind(this.instance)(err); + const filteredError = this.retryOptions.errorFilter.bind(this.instance)(err); - if (!isFiltered) { + if (!filteredError) { return this.error(); } } @@ -69,8 +62,16 @@ export class Retryer { return this.error(); } + private async waitBeforeResponse(attemptIndex: number): Promise { + if (attemptIndex > 0) { + const waitStrategy = new WaitStrategy(this.retryOptions.waitPattern); + await waitStrategy.wait(attemptIndex - 1); + } + } + private error() { const raise = raiseStrategy(this.retryOptions); + return raise(new Error(DEFAULT_ERROR)); } } diff --git a/lib/retry/WaitStrategy.ts b/lib/retry/WaitStrategy.ts index 2e6884a..7efbf59 100644 --- a/lib/retry/WaitStrategy.ts +++ b/lib/retry/WaitStrategy.ts @@ -19,6 +19,7 @@ export class WaitStrategy { if (Array.isArray(this.waitPattern)) { const values = this.waitPattern as number[]; const count = values.length; + return index > count ? values[count - 1] : values[index]; } @@ -32,4 +33,5 @@ export class WaitStrategy { throw new Error(`Option ${typeof this.waitPattern} is not supported for 'waitPattern'.`); } + } diff --git a/lib/retry/index.ts b/lib/retry/index.ts index 84f566e..8ee9457 100644 --- a/lib/retry/index.ts +++ b/lib/retry/index.ts @@ -1,5 +1,5 @@ import { Retryer } from './Retryer'; -import { DEFAULT_OPTIONS, RetryOptions } from './RetryOptions'; +import { RetryOptions } from './RetryOptions'; export { RetryOptions };