├── .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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
--------------------------------------------------------------------------------