Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry decorator #6

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
12 changes: 12 additions & 0 deletions examples/retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { retry } from '../lib';

class Service {
@retry(3)
do(): Promise<number> {
return new Promise((res, rej) => {
setTimeout(res, 1000);
});
}
}

const t = new Service().do().catch(err => console.log(err.message));
25 changes: 14 additions & 11 deletions lib/retry.ts → lib/retry/RetryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,19 @@ 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,
};

/**
* 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?: number): any {
throw new Error('Not implemented.');
}
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; },
nicolaecaliman marked this conversation as resolved.
Show resolved Hide resolved
};
25 changes: 25 additions & 0 deletions lib/retry/Retryer.ts
Original file line number Diff line number Diff line change
@@ -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<never> {
const raise = raiseStrategy({ onError: this.options.onError }, DEFAULT_ON_ERROR);
return raise(new Error(DEFAULT_ERROR));
}
}
39 changes: 39 additions & 0 deletions lib/retry/WaitStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { WaitPattern } from './RetryOptions';

export class WaitStrategy {

constructor(
private readonly waitPattern: WaitPattern,
) { }

public wait(index: number, instance: any): Promise<void> {
if (!this.waitPattern) {
return Promise.resolve();
}

const timeout = this.getTimeout(index, instance) || 0;
return new Promise(resolve => setTimeout(resolve, timeout));
}

private getTimeout(index: number, instance: any): number {
nicolaecaliman marked this conversation as resolved.
Show resolved Hide resolved
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'.`);
nicolaecaliman marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
52 changes: 52 additions & 0 deletions lib/retry/index.ts
Original file line number Diff line number Diff line change
@@ -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;
nicolaecaliman marked this conversation as resolved.
Show resolved Hide resolved

descriptor.value = function () {
const methodOptions: MethodOptions = {
instance: this,
args: arguments,
method: target[propertyKey],
dimadeveatii marked this conversation as resolved.
Show resolved Hide resolved
};
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);
nicolaecaliman marked this conversation as resolved.
Show resolved Hide resolved
}
};

return descriptor;
};
}
21 changes: 21 additions & 0 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

type StrategyOptions = {
onError?: 'throw' | 'reject' | 'ignore' | 'ignoreAsync';
};

export function raiseStrategy(options: StrategyOptions, defaultStrategy: string) {
nicolaecaliman marked this conversation as resolved.
Show resolved Hide resolved
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'.`);
}
}
Loading