├── .gitignore ├── .npmignore ├── src ├── wait.js ├── test.js └── retry.js ├── webpack.config.js ├── dist └── retry.js ├── package.json ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | webpack.config.js 3 | -------------------------------------------------------------------------------- /src/wait.js: -------------------------------------------------------------------------------- 1 | const wait = (secs) => new Promise((resolve) => setTimeout(resolve, secs)) 2 | 3 | module.exports = wait 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: './src/retry.js', 6 | output: { 7 | path: path.resolve('dist'), 8 | filename: 'retry.js', 9 | library: 'tiny-retry', 10 | libraryTarget: 'umd', 11 | globalObject: 'this' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js?$/, 17 | exclude: /(node_modules)/, 18 | use: 'babel-loader', 19 | }, 20 | ], 21 | }, 22 | resolve: { 23 | extensions: ['.js'], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /dist/retry.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports["tiny-retry"]=e():t["tiny-retry"]=e()}(this,(function(){return t={868:(t,e,o)=>{const r=o(572);t.exports=async function(t,e){const{maxTries:o=10,delay:n=1e3,startAfter:s=0,process:i,errorHandler:c,check:a}=e;return new Promise((async e=>{await r(s);let f=0;await async function s(){try{f+=1,"function"==typeof i&&i(f);const o=await t();if("function"==typeof a&&!a(o))throw new Error("Unexpected data");e({success:!0,tries:f,data:o})}catch(t){"function"==typeof c&&c(t),f>=o?e({success:!1,tries:f}):(await r(n),await s())}}()}))}},572:t=>{t.exports=t=>new Promise((e=>setTimeout(e,t)))}},e={},function o(r){var n=e[r];if(void 0!==n)return n.exports;var s=e[r]={exports:{}};return t[r](s,s.exports,o),s.exports}(868);var t,e})); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-retry", 3 | "version": "1.6.5", 4 | "description": "Lightweight function to retry an async job until the job success or stop after a maximum number of tries", 5 | "main": "dist/retry.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "prepublish": "npm run build", 9 | "test": "node ./src/test.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/hta218/tiny-retry.git" 14 | }, 15 | "keywords": [ 16 | "retry", 17 | "tiny-module", 18 | "tiny-retry", 19 | "pure-js" 20 | ], 21 | "author": "Leo Huynh @ https://leohuynh.dev", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/hta218/tiny-retry/issues" 25 | }, 26 | "homepage": "https://github.com/hta218/tiny-retry#readme", 27 | "devDependencies": { 28 | "@babel/core": "^7.14.6", 29 | "babel-loader": "^8.2.2", 30 | "webpack": "^5.40.0", 31 | "webpack-cli": "^4.7.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Leo Huynh @ [https://leohuynh.dev](https://leohuynh.dev) 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 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | const retry = require('../dist/retry') 2 | const wait = require('./wait') 3 | 4 | let count = 0; 5 | const fakeJobThatDoneAfter6Tries = async () => { 6 | await wait(2000); 7 | count += 1; 8 | if (count < 4) { 9 | throw new Error('Job failed!'); 10 | } else if (count < 6) { 11 | return false 12 | } else { 13 | console.log('Job done!'); 14 | return "Job data" 15 | } 16 | }; 17 | 18 | /////////////////////////// TEST 19 | (async () => { 20 | console.log(`/********/ \nTesting a job that need 2s for each run, return unexpect data after 4 tries and success after 6 tries \n/********/`); 21 | 22 | console.log("\nTest 1 (10 tries max)..."); 23 | console.time("TEST_1_TIME"); 24 | const result1 = await retry(fakeJobThatDoneAfter6Tries, { 25 | process: (tries) => console.log(`[TRY]: ${tries} time(s)`), 26 | errorHandler: (err) => console.log(err.toString()), 27 | check: Boolean, 28 | maxTries: 10, 29 | delay: 1000, 30 | startAfter: 0 31 | }); 32 | console.log("\nJob result: ", result1); 33 | console.log("Time expect: 0 + 2*6 + 1*(6-1) = 17s"); 34 | console.timeEnd("TEST_1_TIME"); 35 | 36 | count = 0 // Reset count 37 | console.log("\n****************************"); 38 | console.log("Test 2 (3 tries max)..."); 39 | console.time("TEST_2_TIME"); 40 | const result2 = await retry(fakeJobThatDoneAfter6Tries, { 41 | process: (tries) => console.log(`[TRY]: ${tries} time(s)`), 42 | errorHandler: (err) => console.log(err.toString()), 43 | maxTries: 3, 44 | delay: 2000, 45 | startAfter: 1000 46 | }); 47 | console.log("\nJob result: ", result2); 48 | console.log("Time expect: 1 + 2*3 + 2*(3-1) = 11s"); 49 | console.timeEnd("TEST_2_TIME"); 50 | })(); 51 | -------------------------------------------------------------------------------- /src/retry.js: -------------------------------------------------------------------------------- 1 | const wait = require('./wait') 2 | 3 | /** 4 | * @typedef Result 5 | * @property {Boolean} success True/false - whether the job done or not 6 | * @property {Object} [data] The return data from async job if the job success (If not, there will be no data in the result object) 7 | * @property {Number} [tries] Number of tries 8 | */ 9 | 10 | /** 11 | * Retry an async job until the job success or stop after a maximum number of tries 12 | * 13 | * @param {Promise} asyncJob Async job to try. 14 | * @param {Object} options Options 15 | * @param {Number} [options.maxTries=10] Maximum times to try job. 16 | * @param {Number} [options.delay=1000] Time after between each try in ms. 17 | * @param {Number} [options.startAfter=0] Time to start the 1st try in ms. 18 | * @param {Function} [options.process=void] A process function to run before each try with the "tries" count argument 19 | * @param {Function} [options.errorHandler=void] A function to handle error in each try with the "err" argument 20 | * @param {Function} [options.check=void => Boolean] A function with the job reponse argument to verify whether the job response is expected or not (throw an error if not) 21 | * 22 | * @return {Promise} A promise that contains job data when fulfilled 23 | */ 24 | 25 | module.exports = async function retry(asyncJob, options) { 26 | const { maxTries = 10, delay = 1000, startAfter = 0, process, errorHandler, check } = options 27 | 28 | return new Promise(async (resolve) => { 29 | await wait(startAfter) 30 | 31 | let tries = 0 32 | async function fn() { 33 | try { 34 | tries += 1 35 | if (typeof process === "function") process(tries) 36 | 37 | const data = await asyncJob() 38 | if (typeof check === "function" && !check(data)) throw new Error("Unexpected data") 39 | 40 | resolve({ success: true, tries, data }) 41 | } catch (err) { 42 | if (typeof errorHandler === "function") errorHandler(err) 43 | if (tries >= maxTries) { 44 | resolve({ success: false, tries }) 45 | } else { 46 | await wait(delay) 47 | await fn() 48 | } 49 | } 50 | } 51 | 52 | await fn() 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny `retry` ✨ 2 | 3 | [![npm](https://img.shields.io/npm/v/tiny-retry?color=green)](https://www.npmjs.com/package/tiny-retry) 4 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/tiny-retry)](https://www.npmjs.com/package/tiny-retry) 5 | [![npm](https://img.shields.io/npm/dt/tiny-retry)](https://www.npmjs.com/package/tiny-retry) 6 | ![GitHub](https://img.shields.io/github/license/hta218/tiny-retry) 7 | 8 | A lightweight [module](https://www.npmjs.com/package/tiny-retry) (**~0.5kb**) to retry an async job until the job success or stop after a maximum number of tries 9 | 10 | ![package size](https://i.imgur.com/kPMgkMm.png) 11 | 12 | ## Usage 13 | 14 | - Install package 15 | 16 | ```bash 17 | npm install tiny-retry 18 | # or yarn add tiny-retry 19 | ``` 20 | 21 | - Use it 22 | 23 | ```js 24 | import retry from "tiny-retry"; 25 | 26 | // Async context 27 | const result = await retry(asyncJob, options); 28 | 29 | if (result.success) { 30 | // Do something with job data 31 | console.log(result.data) 32 | } else { 33 | // Do other thing 34 | } 35 | ``` 36 | 37 | ## Parameters 38 | 39 | ```javascript 40 | const result = await retry(asyncJob, { maxTries, delay, startAfter, process, errorHandler, check }); 41 | ``` 42 | 43 | - `asyncJob` [Function]: async function that throw an `Error` if failed 44 | - `options` [Object]: Retry options 45 | - `options.maxTries` [Number]: number of maximum time to try to run job 46 | - `options.delay` [Number]: the number in miliseconds of time after between each tries 47 | - `options.starAfter` [Number]: the number in miliseconds to start the 1st try 48 | - `options.process` [Function]: A process function to run before each try with the `tries` count argument 49 | - `options.errorHandler` [Function]: A function to handle error in each try with the `err` argument 50 | - `options.check` [Function]: A function with the job reponse argument to verify whether the job response is expected or not (throw an `Error` if not) 51 | 52 | ## Return value 53 | 54 | - `result` [Object]: Represent the value of the job done or not and include job's data (if job return) 55 | - `result.success` [Boolean]: Whether the job success or not 56 | - `result.tries` [Number]: Number of tries 57 | - `[result.data]` [Number]: The return value of job if success 58 | 59 | ```javascript 60 | console.log(result) 61 | 62 | // Expect: { success: true, data: "Async job data", tries } 63 | // If job failed: { success: false, tries } 64 | ``` 65 | 66 | ## Example 67 | 68 | [![Edit codesandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/test-tiny-retry-pjbqs?file=/src/index.js:0-1198) 69 | 70 | ```javascript 71 | import retry from "tiny-retry"; 72 | 73 | const wait = (secs) => new Promise((resolve) => setTimeout(resolve, secs)); 74 | 75 | let count = 0; 76 | const fakeJobThatDoneAfter6Tries = async () => { 77 | await wait(2000); 78 | count += 1; 79 | if (count < 4) { 80 | throw new Error('Job failed!'); 81 | } else if (count < 6) { 82 | return false 83 | } else { 84 | console.log('Job done!'); 85 | return "Job data" 86 | } 87 | }; 88 | 89 | (async () => { 90 | console.log("Start Job"); 91 | console.time("JOB_COST"); 92 | const result = await retry(fakeJobThatDoneAfter6Tries, { 93 | process: (tries) => console.log(`[TRY]: ${tries} time(s)`), 94 | errorHandler: (err) => console.log(err.toString()), 95 | check: Boolean, 96 | maxTries: 10, 97 | delay: 1000, 98 | startAfter: 0 99 | }); 100 | console.log("Job result: ", result); 101 | console.log("Time expect: 0 + 2*6 + 1*(6-1) = 17s"); 102 | console.timeEnd("JOB_COST"); // Expect 17s 103 | })(); 104 | 105 | ``` 106 | 107 | ## License 108 | 109 | Bundled by me from the ideas' combination of me and my friend. 110 | 111 | Copyright (c) 2021 Leo Huynh @ [https://leohuynh.dev](https://leohuynh.dev) under [MIT LICENSE](/LICENSE.md) 112 | --------------------------------------------------------------------------------