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));
13 changes: 13 additions & 0 deletions lib/retry/Counter.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
24 changes: 13 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,18 @@ 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_ERROR = 'Retry failed.';
export const DEFAULT_OPTIONS: RetryOptions = {
errorFilter: () => true,
};
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, 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(this.options);
return raise(new Error(DEFAULT_ERROR));
}
}
16 changes: 16 additions & 0 deletions lib/retry/ScopeCounter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Counter } from './Counter';

export class ScopeCounter {
private readonly map: WeakMap<any, Counter> = 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;
}
}
35 changes: 35 additions & 0 deletions lib/retry/WaitStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { WaitPattern } from './RetryOptions';

export class WaitStrategy {

constructor(
private readonly waitPattern: WaitPattern,
) { }

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

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

private getTimeout(index: number): number {
if (Array.isArray(this.waitPattern)) {
const values = this.waitPattern as number[];
const count = values.length;
return index > count ? values[count - 1] : values[index];
nicolaecaliman marked this conversation as resolved.
Show resolved Hide resolved
}

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'.`);
}
}
57 changes: 57 additions & 0 deletions lib/retry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Retryer } from './Retryer';
import { DEFAULT_OPTIONS, MethodOptions, RetryOptions } from './RetryOptions';
import { ScopeCounter } from './ScopeCounter';
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);
const scope = new ScopeCounter();

descriptor.value = function () {
const counter = scope.getCounter(this);
nicolaecaliman marked this conversation as resolved.
Show resolved Hide resolved

let count = counter.get();

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 {
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);
}
nicolaecaliman marked this conversation as resolved.
Show resolved Hide resolved
};

return descriptor;
};
}
20 changes: 20 additions & 0 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { RetryOptions } from '../retry';

const DEFAULT_ON_ERROR = 'throw';

export function raiseStrategy(options: RetryOptions) {
const value = options && options.onError || DEFAULT_ON_ERROR;

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