/* tslint:disable:no-use-before-declare */ import * as _ from 'underscore'; import { createDeferred } from '../deferred'; import { sleep } from '../sleep'; /* tslint:enable:no-use-before-declare */ export type SequenceType = 'constant' | 'fibonacci' | 'exponential' | 'linear'; export type Delayable = { // tslint:disable-next-line:no-reserved-keywords type: SequenceType; beat?: number; maxDuration?: number; maxInterval?: number; maxIteration?: number; }; // tslint:disable /** * retry decorator * * @retry() */ export function retry(config: Delayable, canRetry: (e: any) => Promise) { return function (target: any, key: string, descriptor: PropertyDescriptor) { // tslint:disable-next-line:no-parameter-reassignment descriptor = descriptor || Object.getOwnPropertyDescriptor(target, key); const action = descriptor.value; descriptor.value = function () { const args = _.map(arguments, x => x); const _this = this; return retryAction( function () { // tslint:disable-line try { return action.apply(_this, args); } catch (ex) { // ignore error } }, canRetry, config); }; return descriptor; }; } // tslint:enable export interface Sequence { next(): T; } // tslint:disable-next-line:no-reserved-keywords export function createSequence(type: SequenceType): Sequence { switch (type) { case 'constant': return { next: () => 1 }; case 'exponential': { let prev = 0; return { next: () => { const result = Math.pow(2, prev); prev++; return result; } }; } case 'linear': { let prev = 0; return { next: () => ++prev }; } case 'fibonacci': { let prev = 0; let cur = 1; return { next: () => { const result = prev + cur; prev = cur; cur = result; return result; } }; } default: throw new Error(`unknown sequence type ${type}`); } } const _defaultRetryOption: Delayable = { type: 'constant', beat: 200, maxDuration: 120000, maxInterval: 30000 }; export async function retryAction(action: () => Promise, canRetry: (e: any) => Promise, delay: Delayable) { const start = new Date().getTime(); let { type, beat, maxDuration, maxInterval, maxIteration } = _.extend({}, _defaultRetryOption, delay); let sequence = createSequence(type); let invokeCount = 0; return repeat(action, canRetry, async () => { const now = new Date().getTime(); invokeCount++; const duration = now - start; if (maxIteration > 0 && invokeCount >= maxIteration) { throw new Error(`exceeded maxIteration ${maxIteration}`); } let waitDuration = sequence.next() * beat; if (maxInterval > 0) { waitDuration = Math.min(maxInterval, waitDuration); } if (maxDuration > 0 && duration + waitDuration > maxDuration) { throw new Error(`exceeded maxDuration ${maxDuration}`); } await sleep(waitDuration); }); } /** * repeat action until it resolves to promise * @param action async action that returns a Promise * @param canRetry when action rejects with err, check if the error can be retried * @param wait wait before issue the next repeat */ export async function repeat(action: () => Promise, canRetry: (v: any) => Promise, wait: () => Promise): Promise { const result = createDeferred(); const inner = async () => { try { const x = await action(); result.resolve(x); } catch (innerError) { if (await canRetry(innerError)) { try { await wait(); //tslint:disable-next-line:no-floating-promises inner(); } catch (waitError) { result.reject(waitError); } } else { result.reject(innerError); } } }; //tslint:disable-next-line:no-floating-promises inner(); return result.promise; }