├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── decorator │ └── index.ts └── index.ts ├── package.json ├── test ├── reattempt.decorator.test.ts └── reattempt.test.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .npmrc 3 | *.tgz 4 | *.log 5 | .vscode 6 | .env 7 | .dev 8 | .cache 9 | dist 10 | node_modules 11 | coverage 12 | test/tmp 13 | package-lock.json 14 | .scratchpad 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - node 5 | cache: 6 | yarn: true 7 | directories: 8 | - node_modules 9 | script: 10 | - yarn run prepack 11 | after_success: 12 | - yarn run coveralls 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Waseem Dahman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | reattempt 5 | 6 |

7 | 8 |

9 | 10 | Current Release 11 | 12 | 13 | CI Build 14 | 15 | 16 | Coverage Status 17 | 18 | 19 | Licence 20 | 21 |

22 | 23 | > `reattempt` is a modern JavaScript library for the browser and Node.js that lets you retry asynchronous functions when they fail - because some functions deserve a second chance, or a third or maybe even several dozen or so. 24 | 25 |
26 | 📖 Table of Contents 27 |

28 | 29 | - [Highlights](#highlights) 30 | - [Getting Started](#getting-started) 31 | - [Usage](#usage) 32 | - [Asynchronous Promise-Based Functions](#asynchronous-promise-based-functions) 33 | - [Node.js Error-First Callbacks](#nodejs-error-first-callbacks) 34 | - [Custom Interface Functions](#custom-interface-functions) 35 | - [Intercepting Attempts](#intercepting-attempts) 36 | - [Working with TypeScript](#working-with-typescript) 37 | - [Reattempt As A Decorator](#reattempt-as-a-decorator) 38 | - [Type Safe Callbacks](#type-safe-callbacks) 39 | - [API](#api) 40 | - [Methods](#methods) 41 | - [`run(options: Options, callback: Callback): Promise`](#runoptions-options-callback-callback-promise) 42 | - [Reattempt Options](#reattempt-options) 43 | - [`times: number`](#times-number) 44 | - [`delay?: number`](#delay-number) 45 | - [`onError?(error, done, abort): void`](#onerrorerror-done-abort-void) 46 | - [Reattempt Callback](#reattempt-callback) 47 | - [The `done` Callback](#the-done-callback) 48 | 49 |

50 |
51 | 52 | ## Highlights 53 | 54 | - 🚀 Very lightweight: ~550 bytes minified+gzipped 55 | - ⚡️ Modern asynchronous JavaScript support with Promises and Async/Await 56 | - 💪 Flexible API that covers many cases 57 | - 🛠 Targeted for both the browser and Node.js 58 | - ⛑ Type-safety with TypeScript and a built-in decorator 59 | 60 | ## Getting Started 61 | 62 | To get started, add `reattempt` to your project: 63 | 64 | ``` 65 | npm i --save-dev reattempt 66 | ``` 67 | 68 | ## Usage 69 | 70 | ### Asynchronous Promise-Based Functions 71 | 72 | When an `async` function (or a function that returns a `Promise`) is passed to `Reattempt.run`, the function will be called immediately. If the functions reject with an error, `Reattempt.run` will retry calling that function. The function will be retried until it resolves, or until the maximum retries count is reached, whichever comes first. 73 | 74 | ```js 75 | import Reattempt from 'reattempt'; 76 | 77 | async function doSomethingAsync() { 78 | // doing async operation that may throw 79 | return result; 80 | } 81 | 82 | async function main() { 83 | try { 84 | const result = await Reattempt.run({ times: 3 }, doSomethingAsync); 85 | } catch (error) { 86 | // an error is thrown if the function rejects with an error after 87 | // exhausting all attempts 88 | } 89 | } 90 | ``` 91 | 92 | ### Node.js Error-First Callbacks 93 | 94 | Reattempt also works with functions following the _error-first callbacks_ pattern. When working with these functions, instead of passing an `async` or `Promise` based function, pass a function with a single argument called `done`. Use this argument as the error-first callback of your function. 95 | 96 | The function will be retried until it returns a value without an error, or until the maximum retries count is reached, whichever comes first. 97 | 98 | ```js 99 | import fs from 'fs'; 100 | import Reattempt from 'reattempt'; 101 | 102 | async function main() { 103 | try { 104 | const data = await Reattempt.run({ times: 3 }, done => { 105 | fs.readFile('./path/to/file', 'utf8', done); 106 | }); 107 | } catch (error) { 108 | // an error is thrown if the function rejects with an error after 109 | // exhausting all attempts 110 | } 111 | } 112 | ``` 113 | 114 | ### Custom Interface Functions 115 | 116 | Similar to working with _[Node.js Error-First Callbacks](#nodejs-error-first-callbacks)_, the `done` callback can be used to reattempt any asynchronous function with custom callback interface. For example, some APIs expects an `onSuccess` and `onError` callbacks. 117 | 118 | The properties `done.resolve` and `done.reject` can be used to hook into any custom interface and perform reattempts as needed. 119 | 120 | ```ts 121 | function doSomething(onSuccess, onError) { 122 | // some async operations 123 | } 124 | 125 | async function main() { 126 | try { 127 | const data = await Reattempt.run({ times: 3 }, done => { 128 | doSomething(done.resolve, done.reject); 129 | }); 130 | } catch (error) { 131 | // an error is thrown if the function rejects with an error after 132 | // exhausting all attempts 133 | } 134 | } 135 | ``` 136 | 137 | ### Intercepting Attempts 138 | 139 | There are cases when you need to intercept an attempt call. It's possible to control the reattempt flow, by providing the `onError` option. This option allows you to intercept each attempt and control the reattempt flow. 140 | 141 | 142 | ```js 143 | import Reattempt from 'reattempt'; 144 | 145 | async function doSomething() { 146 | // some async operations 147 | } 148 | 149 | function handleError( 150 | error /* the error object that the function rejected with */, 151 | done /* resolves the function call with a custom value */, 152 | abort /* bail out of remaining attempts and rejects with current error */, 153 | ) { 154 | if (shouldAbortRemainingAttempts) { 155 | abort(); 156 | } else if (shouldSkipAttemptsAndResolve) { 157 | done(defaultValue); 158 | } 159 | } 160 | 161 | async function main() { 162 | try { 163 | const result = await Reattempt.run( 164 | { times: 10, onError: handleError }, 165 | doSomething, 166 | ); 167 | } catch (error) { 168 | // ... 169 | } 170 | } 171 | ``` 172 | 173 | ### Working with TypeScript 174 | 175 | #### Reattempt As A Decorator 176 | 177 | Reattempt also comes as a decorator that can be imported from `reattempt/decorator`. 178 | 179 | ```ts 180 | import Reattempt from 'reattempt/decorator'; 181 | 182 | class Group { 183 | @Reattempt({ times: 3, delay: 5000 }) 184 | private async getUserIds() { 185 | const user = await fakeAPI.getUsers(this.id); // could throw! 186 | return users.map(user => user.id); 187 | } 188 | 189 | public async doSomething() { 190 | try { 191 | const result = await this.getUserIds(); 192 | } catch (error) { 193 | // Only throws after failing 3 attempts with 5 seconds in between 194 | } 195 | } 196 | } 197 | ``` 198 | 199 | #### Type Safe Callbacks 200 | 201 | Reattempt can infer types of async and Promise-based functions automatically. 202 | 203 | However, when working with error-first callbacks, you can enforce type safety by passing a type argument informing Reattempt about the list of success arguments the original function could potentially provide. 204 | 205 | ```ts 206 | Reattempt 207 | .run<[string, string]>({ times: 3 }, done => { 208 | childProcess.exec('cat *.md | wc -w', attempt); 209 | }) 210 | // resolves with an array of success type-safe arguments 211 | .then(([stdout, stderr]) => stdout.trim()) 212 | .catch(error => /* ... */); 213 | ``` 214 | 215 | ## API 216 | 217 | ### Methods 218 | 219 | #### `run(options: Options, callback: Callback): Promise` 220 | 221 | Runs and reattempt the provided callback. If the callback fails, it will be reattempted until it resolves, or until the maximum retries count `options.times` is reached, whichever comes first. 222 | 223 | Returns a `Promise` that resolves with the result of the provided function, and rejects with the same error it could reject with. 224 | 225 | ### Reattempt Options 226 | 227 | All Reattempt methods accept an options object as the first argument with the following properties: 228 | 229 | #### `times: number` 230 | 231 | The number of times a function can be reattempted. 232 | 233 | If this property is not provided Reattempt will perform the provided function once without any additional reattempts on failure. 234 | 235 | #### `delay?: number` 236 | 237 | The duration in milliseconds between each attempt. Defaults to `0`. 238 | 239 | If this property is not provided Reattempt will perform a reattempt as soon as the function fails. 240 | 241 | #### `onError?(error, done, abort): void` 242 | 243 | A callback that fires on each attempt after receiving an error. It allows you to intercept an attempt and gives you access to the error object. It passes the following parameters: 244 | 245 | - `error: any`: the error that the function rejected with 246 | - `done(value: any): void`: a function that allows you to skip remaining reattempts and resolve the attempted function with the value provided. 247 | - `abort(): void`: a function allowing you to bail out of remaining attempts and rejects the attempted function immediately. 248 | 249 | ### Reattempt Callback 250 | 251 | All Reattempt methods take a function as the second argument. 252 | 253 | This function will be reattempted on failure and can be one of three forms: 254 | 255 | - An `async` function. 256 | 257 | ```js 258 | Reattempt.run({ times: 2 }, async () => { 259 | // ... 260 | }); 261 | ``` 262 | 263 | - A function that returns a `Promise` 264 | 265 | ```js 266 | Reattempt.run({ times: 2 }, () => { 267 | return new Promise((resolve, reject) => { 268 | //... 269 | }); 270 | }); 271 | ``` 272 | 273 | - A non-`async`, non-`Promise` function that wraps functions with error-first-callbacks 274 | 275 | ```js 276 | Reattempt.run({ times: 2 }, done => { 277 | fs.readFile('path/to/file', 'utf-8', done); 278 | }); 279 | ``` 280 | 281 | ### The `done` Callback 282 | 283 | If you are reattempting a non-`async` function (or a function that does not return a `Promise`), pass a callback function with one argument `done`. 284 | 285 | This argument controls the reattempt flow and can be used in one of two ways: 286 | 287 | - As an error-first callback that you can pass to any function such as most Node.js APIs 288 | - As a hook to custom interfaces that expects success and error callbacks by utilizing the two properties `done.resolve` and `done.reject`. 289 | 290 | # License 291 | 292 | MIT 293 | -------------------------------------------------------------------------------- /lib/decorator/index.ts: -------------------------------------------------------------------------------- 1 | import Reattempt, { ReattemptOptions } from '..'; 2 | 3 | export default function ReattemptDecorator(options: ReattemptOptions) { 4 | return function withReattempt( 5 | target: any, 6 | propertyKey: string, 7 | descriptor: PropertyDescriptor, 8 | ): any { 9 | const fn = descriptor.value; 10 | descriptor.value = function() { 11 | const args = arguments; 12 | return Reattempt.run(options, () => fn.apply(this, args)); 13 | }; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | type AsyncAttemptFunction = () => Promise; 2 | 3 | interface DoneCallback { 4 | (error: any, ...args: T): void; 5 | resolve(...args: T): void; 6 | reject(error: any): void; 7 | } 8 | 9 | type CallbackAttemptFunction = T extends any[] 10 | ? (done: DoneCallback) => any 11 | : never; 12 | 13 | type AttemptResult = Promise< 14 | Callback extends AsyncAttemptFunction 15 | ? T 16 | : Value extends (...args: infer A) => any 17 | ? A 18 | : never 19 | >; 20 | 21 | export interface ReattemptOptions { 22 | times: number; 23 | delay?: number; 24 | onError?(error: any, done: (value?: any) => void, abort: () => void): void; 25 | } 26 | 27 | interface Interceptor { 28 | done: any; 29 | abort: boolean; 30 | setDone(...args: any[]): void; 31 | setAbort(): void; 32 | intercept(error: any): void; 33 | } 34 | 35 | function isFunction(value: any): value is (...args: any[]) => any { 36 | return typeof value === 'function'; 37 | } 38 | 39 | function isPromise(value: any): value is Promise { 40 | return ( 41 | value != null && 42 | (isFunction(value) || typeof value === 'object') && 43 | isFunction(value.then) 44 | ); 45 | } 46 | 47 | function createInterceptor( 48 | callback: Required['onError'], 49 | ): Interceptor { 50 | const interceptor = { 51 | abort: false, 52 | setAbort() { 53 | interceptor.abort = true; 54 | }, 55 | setDone() { 56 | interceptor.done = Array.from(arguments); 57 | }, 58 | intercept(error: any) { 59 | callback(error, interceptor.setDone, interceptor.setAbort); 60 | }, 61 | } as Interceptor; 62 | return interceptor; 63 | } 64 | 65 | function runAttempt( 66 | options: ReattemptOptions, 67 | callback: AsyncAttemptFunction | CallbackAttemptFunction, 68 | ): AttemptResult { 69 | const delay = options.delay || 0; 70 | let currentAttempts = options.times; 71 | const interceptor = createInterceptor( 72 | isFunction(options.onError) ? options.onError : () => {}, 73 | ); 74 | 75 | function reattemptAsync( 76 | promise: Promise, 77 | fn: AsyncAttemptFunction, 78 | resolve: (value?: T | PromiseLike) => void, 79 | reject: (value?: T | PromiseLike) => void, 80 | ) { 81 | promise.then(resolve).catch(error => { 82 | interceptor.intercept(error); 83 | if (interceptor.done) { 84 | return resolve.apply(null, interceptor.done); 85 | } 86 | 87 | if (interceptor.abort || currentAttempts <= 0) { 88 | return reject(error); 89 | } 90 | 91 | setTimeout(() => { 92 | currentAttempts--; 93 | reattemptAsync(fn(), fn, resolve, reject); 94 | }, delay); 95 | }); 96 | } 97 | 98 | const callbackResolver: { 99 | resolve: DoneCallback; 100 | promise: Promise; 101 | } = {} as any; 102 | 103 | function resetCallbackResolver() { 104 | callbackResolver.promise = new Promise(resolve => { 105 | function resolveCallback() { 106 | resolve(Array.from(arguments)); 107 | } 108 | resolveCallback.resolve = resolveCallback.bind(null, null); 109 | resolveCallback.reject = resolveCallback.bind(null); 110 | callbackResolver.resolve = resolveCallback; 111 | }); 112 | } 113 | 114 | resetCallbackResolver(); 115 | 116 | function reattemptCallback( 117 | fn: CallbackAttemptFunction, 118 | resolve: (value?: T) => void, 119 | reject: (value?: T) => void, 120 | ) { 121 | callbackResolver.promise.then((args: any[]) => { 122 | if (!args[0]) { 123 | return resolve(args.slice(1) as any); 124 | } 125 | 126 | interceptor.intercept(args[0]); 127 | 128 | if (interceptor.done) { 129 | return resolve(interceptor.done as any); 130 | } 131 | 132 | if (interceptor.abort || currentAttempts <= 0) { 133 | return reject(args[0]); 134 | } 135 | 136 | resetCallbackResolver(); 137 | setTimeout(() => { 138 | currentAttempts--; 139 | fn(callbackResolver.resolve); 140 | reattemptCallback(fn, resolve, reject); 141 | }, delay); 142 | }); 143 | } 144 | 145 | return new Promise((resolve, reject) => { 146 | currentAttempts--; 147 | const value = callback(callbackResolver.resolve as any); 148 | if (isPromise(value)) { 149 | callbackResolver.resolve(null); 150 | reattemptAsync( 151 | value, 152 | callback as AsyncAttemptFunction, 153 | resolve, 154 | reject, 155 | ); 156 | } else { 157 | reattemptCallback( 158 | callback as CallbackAttemptFunction, 159 | resolve, 160 | reject, 161 | ); 162 | } 163 | }); 164 | } 165 | 166 | export default { 167 | run: runAttempt, 168 | }; 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reattempt", 3 | "description": "Give your functions another chance", 4 | "version": "0.1.1", 5 | "author": "Waseem Dahman ", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "keywords": [ 10 | "try", 11 | "try-catch", 12 | "retry", 13 | "attempt", 14 | "errors", 15 | "error-handling", 16 | "javascript" 17 | ], 18 | "scripts": { 19 | "test": "jest", 20 | "lint": "tslint --project .", 21 | "typecheck": "tsc --noEmit", 22 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 23 | "test:coverage": "jest --coverage", 24 | "test:all": "yarn typecheck && yarn lint && yarn test:coverage", 25 | "build": "tsc", 26 | "prebuild": "rm -rf dist", 27 | "prepack": "yarn test:all && yarn build && size-limit", 28 | "size": "yarn build --skipLibCheck && size-limit" 29 | }, 30 | "prettier": { 31 | "singleQuote": true, 32 | "trailingComma": "all", 33 | "printWidth": 80 34 | }, 35 | "files": [ 36 | "dist" 37 | ], 38 | "jest": { 39 | "watchPathIgnorePatterns": [ 40 | "dist" 41 | ], 42 | "transform": { 43 | "^.+\\.tsx?$": "ts-jest" 44 | }, 45 | "collectCoverageFrom": [ 46 | "lib/**/*.ts" 47 | ] 48 | }, 49 | "size-limit": [ 50 | { 51 | "path": "dist/index.js", 52 | "limit": "400 B" 53 | } 54 | ], 55 | "devDependencies": { 56 | "@types/jest": "^24.0.11", 57 | "@types/node": "^11.13.4", 58 | "coveralls": "^3.0.3", 59 | "jest": "^24.7.1", 60 | "size-limit": "^1.0.1", 61 | "ts-jest": "^24.0.2", 62 | "tslint": "^5.15.0", 63 | "typescript": "^3.4.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/reattempt.decorator.test.ts: -------------------------------------------------------------------------------- 1 | import { ReattemptOptions } from '../lib'; 2 | import Reattempt from '../lib/decorator'; 3 | 4 | function decorateWithReattempt( 5 | options: ReattemptOptions, 6 | target: T, 7 | property: Extract, 8 | ) { 9 | const desc = Object.getOwnPropertyDescriptor(target, property)!; 10 | const newDesc = Reattempt(options)(target, property, desc) || desc; 11 | Object.defineProperty(target, property, newDesc); 12 | } 13 | 14 | describe('Reattempt decorator', () => { 15 | test('Reattempt decorator is a function', () => { 16 | expect(Reattempt).toBeInstanceOf(Function); 17 | }); 18 | 19 | it('decorates a method', () => { 20 | const spy = jest.fn(() => Promise.resolve()); 21 | const object = { doIt: spy }; 22 | decorateWithReattempt({ times: 2 }, object, 'doIt'); 23 | expect(object.doIt).not.toBe(spy); 24 | expect(object.doIt).toBeInstanceOf(Function); 25 | expect(spy).not.toHaveBeenCalled(); 26 | }); 27 | 28 | test('decorated method throws after attempts', async () => { 29 | const spy = jest.fn(() => Promise.reject('error')); 30 | const foo = { doIt: spy }; 31 | decorateWithReattempt({ times: 2 }, foo, 'doIt'); 32 | await expect(foo.doIt()).rejects.toBe('error'); 33 | expect(spy).toHaveBeenCalledTimes(2); 34 | }); 35 | 36 | test('decorated method resolves after attempts', async () => { 37 | let passes = 2; 38 | const spy = jest.fn(() => 39 | passes-- ? Promise.reject('error') : Promise.resolve('pass'), 40 | ); 41 | const foo = { doIt: spy }; 42 | decorateWithReattempt({ times: 4 }, foo, 'doIt'); 43 | await expect(foo.doIt()).resolves.toBe('pass'); 44 | expect(spy).toHaveBeenCalledTimes(3); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/reattempt.test.ts: -------------------------------------------------------------------------------- 1 | import Reattempt from '../lib'; 2 | 3 | afterEach(() => { 4 | jest.useRealTimers(); 5 | }); 6 | 7 | describe('Reattempt', () => { 8 | test('Reattempt has correct methods', () => { 9 | expect(Reattempt).toHaveProperty('run', expect.any(Function)); 10 | }); 11 | 12 | test('Reattempt.run() returns a promise', () => { 13 | const result = Reattempt.run({ times: 2 }, () => Promise.resolve()); 14 | expect(result).toBeInstanceOf(Promise); 15 | }); 16 | 17 | test('Reattempt.run() resolves with the correct value', async () => { 18 | const result = Reattempt.run({ times: 2 }, () => Promise.resolve('test')); 19 | await expect(result).resolves.toBe('test'); 20 | }); 21 | 22 | test('Reattempt.run() reject with the correct value', async () => { 23 | const result = Reattempt.run({ times: 2 }, () => Promise.reject('error')); 24 | await expect(result).rejects.toEqual('error'); 25 | }); 26 | 27 | test('Reattempt.run() calls an async function once and resolves on first pass', async () => { 28 | const fn = jest.fn(() => Promise.resolve('test')); 29 | const result = await Reattempt.run({ times: 100 }, fn); 30 | expect(fn).toHaveBeenCalledTimes(1); 31 | }); 32 | 33 | test('Reattempt.run() calls an async function multiple times and rejects', async () => { 34 | const fn = jest.fn(() => Promise.reject('test')); 35 | try { 36 | const result = await Reattempt.run({ times: 4 }, fn); 37 | } catch (error) { 38 | expect(error).toBe('test'); 39 | } finally { 40 | expect(fn).toHaveBeenCalledTimes(4); 41 | } 42 | }); 43 | 44 | test('Reattempt.run() calls an async function multiple times and resolves', async () => { 45 | let passes = 3; 46 | const fn = jest.fn(() => { 47 | return passes-- ? Promise.reject('error') : Promise.resolve('pass'); 48 | }); 49 | const result = Reattempt.run({ times: 4 }, fn); 50 | await expect(result).resolves.toBe('pass'); 51 | }); 52 | 53 | test('Reattempt.run() calls an async function multiple times with delays', async () => { 54 | jest.useFakeTimers(); 55 | 56 | const fn = jest.fn(() => Promise.reject('error')); 57 | const promise = Reattempt.run({ times: 2, delay: 1000 }, fn); 58 | 59 | jest.advanceTimersByTime(500); 60 | await Promise.resolve(); 61 | expect(fn).toHaveBeenCalledTimes(1); 62 | 63 | jest.advanceTimersByTime(1000); // 1500ms passed 64 | await Promise.resolve(); 65 | expect(fn).toHaveBeenCalledTimes(2); 66 | 67 | jest.advanceTimersByTime(1000); // 2500ms passed 68 | await Promise.resolve(); 69 | expect(fn).toHaveBeenCalledTimes(2); 70 | 71 | // tslint:disable-next-line: no-empty 72 | promise.catch(() => {}); 73 | }); 74 | 75 | test('Reattempt.run() resolves error first callbacks', async () => { 76 | const fn = jest.fn(callback => { 77 | process.nextTick(() => callback(null, 'pass')); 78 | }); 79 | const promise = Reattempt.run<[string]>({ times: 2 }, done => fn(done)); 80 | await expect(promise).resolves.toEqual(['pass']); 81 | expect(fn).toHaveBeenCalledTimes(1); 82 | }); 83 | 84 | test('Reattempt.run() rejects error first callbacks', async () => { 85 | const fn = jest.fn(callback => { 86 | process.nextTick(() => callback('error')); 87 | }); 88 | const promise = Reattempt.run<[string]>({ times: 2 }, done => fn(done)); 89 | await expect(promise).rejects.toBe('error'); 90 | expect(fn).toHaveBeenCalledTimes(2); 91 | }); 92 | 93 | test('Reattempt.run() resolves custom callbacks manually', async () => { 94 | let passes = 3; 95 | const fn = jest.fn((onSuccess, onError) => { 96 | process.nextTick(() => (passes-- ? onError('error') : onSuccess('pass'))); 97 | }); 98 | const promise = Reattempt.run<[string]>({ times: 4 }, done => { 99 | fn(done.resolve, done.reject); 100 | }); 101 | await expect(promise).resolves.toEqual(['pass']); 102 | }); 103 | 104 | test('Reattempt.run() rejects custom callbacks manually', async () => { 105 | let passes = 3; 106 | const fn = jest.fn((onSuccess, onError) => { 107 | process.nextTick(() => (passes-- ? onError('error') : onSuccess('pass'))); 108 | }); 109 | const promise = Reattempt.run<[string]>({ times: 2 }, done => { 110 | fn(done.resolve, done.reject); 111 | }); 112 | await expect(promise).rejects.toBe('error'); 113 | }); 114 | 115 | describe('intercepting attempts of async functions', () => { 116 | it('Reattempt.run() report async errors via onError', async () => { 117 | const fn = jest.fn(() => Promise.reject('error')); 118 | const handleError = jest.fn(); 119 | const result = Reattempt.run({ times: 3, onError: handleError }, fn); 120 | await expect(result).rejects.toBe('error'); 121 | expect(handleError).toHaveBeenCalledWith( 122 | 'error', // error 123 | expect.any(Function), // done 124 | expect.any(Function), // abort 125 | ); 126 | expect(handleError).toHaveBeenCalledTimes(3); 127 | }); 128 | 129 | it('options.onError can abort an error of async function', async () => { 130 | const fn = jest.fn(() => Promise.reject('error')); 131 | const handleError = jest.fn((error, done, abort) => { 132 | abort(); 133 | }); 134 | const result = Reattempt.run({ times: 3, onError: handleError }, fn); 135 | await expect(result).rejects.toBe('error'); 136 | expect(fn).toHaveBeenCalledTimes(1); 137 | }); 138 | 139 | it('options.onError can skip errors of async function', async () => { 140 | const fn = jest.fn(() => Promise.reject('error')); 141 | const handleError = jest.fn((error, done, abort) => { 142 | done('test'); 143 | }); 144 | const result = Reattempt.run({ times: 3, onError: handleError }, fn); 145 | await expect(result).resolves.toBe('test'); 146 | expect(fn).toHaveBeenCalledTimes(1); 147 | }); 148 | }); 149 | 150 | describe('intercepting attempts of callback functions', () => { 151 | it('Reattempt.run() report callback errors via onError', async () => { 152 | const fn = jest.fn(callback => process.nextTick(() => callback('error'))); 153 | const handleError = jest.fn(); 154 | const result = Reattempt.run<[]>( 155 | { times: 3, onError: handleError }, 156 | done => fn(done), 157 | ); 158 | await expect(result).rejects.toBe('error'); 159 | expect(handleError).toHaveBeenCalledWith( 160 | 'error', // error 161 | expect.any(Function), // done 162 | expect.any(Function), // abort 163 | ); 164 | expect(handleError).toHaveBeenCalledTimes(3); 165 | }); 166 | 167 | it('options.onError can abort an error', async () => { 168 | const fn = jest.fn(callback => process.nextTick(() => callback('error'))); 169 | const handleError = jest.fn((error, done, abort) => { 170 | abort(); 171 | }); 172 | const result = Reattempt.run<[]>({ times: 3, onError: handleError }, fn); 173 | await expect(result).rejects.toBe('error'); 174 | expect(fn).toHaveBeenCalledTimes(1); 175 | }); 176 | 177 | it('options.onError can skip errors and resolve with custom value', async () => { 178 | const fn = jest.fn(callback => process.nextTick(() => callback('error'))); 179 | const handleError = jest.fn((error, done, abort) => { 180 | done('test'); 181 | }); 182 | const result = Reattempt.run<[string]>( 183 | { times: 3, onError: handleError }, 184 | fn, 185 | ); 186 | await expect(result).resolves.toEqual(['test']); 187 | expect(fn).toHaveBeenCalledTimes(1); 188 | }); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es6", "dom"], 6 | "strict": true, 7 | "experimentalDecorators": true, 8 | "esModuleInterop": true, 9 | "declaration": true, 10 | "outDir": "dist" 11 | }, 12 | "include": ["lib"], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": { 6 | "interface-name": false, 7 | "quotemark": false, 8 | "arrow-parens": false, 9 | "unified-signatures": false, 10 | "no-console": false, 11 | "no-empty": false 12 | }, 13 | "rulesDirectory": [] 14 | } 15 | --------------------------------------------------------------------------------