type AsyncAttemptFunction < T > = ( ) => Promise < T > ; interface DoneCallback < T extends any [ ] > { ( error , ... args ) : void ; resolve ( ... args ) : void ; reject ( error ) : void ; } type CallbackAttemptFunction < T > = T extends any [ ] ? ( done : DoneCallback < T > ) => any : never ; type AttemptResult < Callback , Value > = Promise < Callback extends AsyncAttemptFunction < infer T > ? T : Value extends ( ... args : infer A ) => any ? A : never > ; export interface ReattemptOptions { times : number ; delay ? : number ; onError ? ( error , done , abort ) : void ; } interface Interceptor { done : any ; abort : boolean ; setDone ( ... args ) : void ; setAbort ( ) : void ; intercept ( error ) : void ; } function isFunction ( value ) { return typeof value === 'function' ; } function isPromise < T > ( value ) { return ( value != null && ( isFunction ( value ) || typeof value === 'object' ) && isFunction ( value . then ) ) ; } function createInterceptor ( callback ) { const interceptor = { abort : false , setAbort ( ) { interceptor . abort = true ; } , setDone ( ) { interceptor . done = Array . from ( arguments ) ; } , intercept ( error ) { callback ( error , interceptor . setDone , interceptor . setAbort ) ; } , } as Interceptor ; return interceptor ; } function runAttempt < T > ( options , callback ) { const delay = options . delay || 0 ; let currentAttempts = options . times ; const interceptor = createInterceptor ( isFunction ( options . onError ) ? options . onError : ( ) => { } ) ; function reattemptAsync ( promise , fn , resolve , reject ) { promise . then ( resolve ) . catch ( error => { interceptor . intercept ( error ) ; if ( interceptor . done ) { return resolve . apply ( null , interceptor . done ) ; } if ( interceptor . abort || currentAttempts <= 0 ) { return reject ( error ) ; } setTimeout ( ( ) => { currentAttempts -- ; reattemptAsync ( fn ( ) , fn , resolve , reject ) ; } , delay ) ; } ) ; } const callbackResolver = { } as any ; function resetCallbackResolver ( ) { callbackResolver . promise = new Promise ( resolve => { function resolveCallback ( ) { resolve ( Array . from ( arguments ) ) ; } resolveCallback . resolve = resolveCallback . bind ( null , null ) ; resolveCallback . reject = resolveCallback . bind ( null ) ; callbackResolver . resolve = resolveCallback ; } ) ; } resetCallbackResolver ( ) ; function reattemptCallback ( fn , resolve , reject ) { callbackResolver . promise . then ( ( args ) => { if ( ! args [ 0 ] ) { return resolve ( args . slice ( 1 ) as any ) ; } interceptor . intercept ( args [ 0 ] ) ; if ( interceptor . done ) { return resolve ( interceptor . done as any ) ; } if ( interceptor . abort || currentAttempts <= 0 ) { return reject ( args [ 0 ] ) ; } resetCallbackResolver ( ) ; setTimeout ( ( ) => { currentAttempts -- ; fn ( callbackResolver . resolve ) ; reattemptCallback ( fn , resolve , reject ) ; } , delay ) ; } ) ; } return new Promise ( ( resolve , reject ) => { currentAttempts -- ; const value = callback ( callbackResolver . resolve as any ) ; if ( isPromise < T > ( value ) ) { callbackResolver . resolve ( null ) ; reattemptAsync ( value , callback as AsyncAttemptFunction < T > , resolve , reject ) ; } else { reattemptCallback ( callback as CallbackAttemptFunction < T > , resolve , reject ) ; } } ) ; } export default { run : runAttempt , } ;