retry.js
  1  'use strict';
  2  
  3  Object.defineProperty(exports, "__esModule", {
  4      value: true
  5  });
  6  exports.default = retry;
  7  
  8  var _wrapAsync = require('./internal/wrapAsync');
  9  
 10  var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
 11  
 12  var _promiseCallback = require('./internal/promiseCallback');
 13  
 14  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 15  
 16  function constant(value) {
 17      return function () {
 18          return value;
 19      };
 20  }
 21  
 22  /**
 23   * Attempts to get a successful response from `task` no more than `times` times
 24   * before returning an error. If the task is successful, the `callback` will be
 25   * passed the result of the successful task. If all attempts fail, the callback
 26   * will be passed the error and result (if any) of the final attempt.
 27   *
 28   * @name retry
 29   * @static
 30   * @memberOf module:ControlFlow
 31   * @method
 32   * @category Control Flow
 33   * @see [async.retryable]{@link module:ControlFlow.retryable}
 34   * @param {Object|number} [opts = {times: 5, interval: 0}| 5] - Can be either an
 35   * object with `times` and `interval` or a number.
 36   * * `times` - The number of attempts to make before giving up.  The default
 37   *   is `5`.
 38   * * `interval` - The time to wait between retries, in milliseconds.  The
 39   *   default is `0`. The interval may also be specified as a function of the
 40   *   retry count (see example).
 41   * * `errorFilter` - An optional synchronous function that is invoked on
 42   *   erroneous result. If it returns `true` the retry attempts will continue;
 43   *   if the function returns `false` the retry flow is aborted with the current
 44   *   attempt's error and result being returned to the final callback.
 45   *   Invoked with (err).
 46   * * If `opts` is a number, the number specifies the number of times to retry,
 47   *   with the default interval of `0`.
 48   * @param {AsyncFunction} task - An async function to retry.
 49   * Invoked with (callback).
 50   * @param {Function} [callback] - An optional callback which is called when the
 51   * task has succeeded, or after the final failed attempt. It receives the `err`
 52   * and `result` arguments of the last attempt at completing the `task`. Invoked
 53   * with (err, results).
 54   * @returns {Promise} a promise if no callback provided
 55   *
 56   * @example
 57   *
 58   * // The `retry` function can be used as a stand-alone control flow by passing
 59   * // a callback, as shown below:
 60   *
 61   * // try calling apiMethod 3 times
 62   * async.retry(3, apiMethod, function(err, result) {
 63   *     // do something with the result
 64   * });
 65   *
 66   * // try calling apiMethod 3 times, waiting 200 ms between each retry
 67   * async.retry({times: 3, interval: 200}, apiMethod, function(err, result) {
 68   *     // do something with the result
 69   * });
 70   *
 71   * // try calling apiMethod 10 times with exponential backoff
 72   * // (i.e. intervals of 100, 200, 400, 800, 1600, ... milliseconds)
 73   * async.retry({
 74   *   times: 10,
 75   *   interval: function(retryCount) {
 76   *     return 50 * Math.pow(2, retryCount);
 77   *   }
 78   * }, apiMethod, function(err, result) {
 79   *     // do something with the result
 80   * });
 81   *
 82   * // try calling apiMethod the default 5 times no delay between each retry
 83   * async.retry(apiMethod, function(err, result) {
 84   *     // do something with the result
 85   * });
 86   *
 87   * // try calling apiMethod only when error condition satisfies, all other
 88   * // errors will abort the retry control flow and return to final callback
 89   * async.retry({
 90   *   errorFilter: function(err) {
 91   *     return err.message === 'Temporary error'; // only retry on a specific error
 92   *   }
 93   * }, apiMethod, function(err, result) {
 94   *     // do something with the result
 95   * });
 96   *
 97   * // to retry individual methods that are not as reliable within other
 98   * // control flow functions, use the `retryable` wrapper:
 99   * async.auto({
100   *     users: api.getUsers.bind(api),
101   *     payments: async.retryable(3, api.getPayments.bind(api))
102   * }, function(err, results) {
103   *     // do something with the results
104   * });
105   *
106   */
107  const DEFAULT_TIMES = 5;
108  const DEFAULT_INTERVAL = 0;
109  
110  function retry(opts, task, callback) {
111      var options = {
112          times: DEFAULT_TIMES,
113          intervalFunc: constant(DEFAULT_INTERVAL)
114      };
115  
116      if (arguments.length < 3 && typeof opts === 'function') {
117          callback = task || (0, _promiseCallback.promiseCallback)();
118          task = opts;
119      } else {
120          parseTimes(options, opts);
121          callback = callback || (0, _promiseCallback.promiseCallback)();
122      }
123  
124      if (typeof task !== 'function') {
125          throw new Error("Invalid arguments for async.retry");
126      }
127  
128      var _task = (0, _wrapAsync2.default)(task);
129  
130      var attempt = 1;
131      function retryAttempt() {
132          _task((err, ...args) => {
133              if (err === false) return;
134              if (err && attempt++ < options.times && (typeof options.errorFilter != 'function' || options.errorFilter(err))) {
135                  setTimeout(retryAttempt, options.intervalFunc(attempt - 1));
136              } else {
137                  callback(err, ...args);
138              }
139          });
140      }
141  
142      retryAttempt();
143      return callback[_promiseCallback.PROMISE_SYMBOL];
144  }
145  
146  function parseTimes(acc, t) {
147      if (typeof t === 'object') {
148          acc.times = +t.times || DEFAULT_TIMES;
149  
150          acc.intervalFunc = typeof t.interval === 'function' ? t.interval : constant(+t.interval || DEFAULT_INTERVAL);
151  
152          acc.errorFilter = t.errorFilter;
153      } else if (typeof t === 'number' || typeof t === 'string') {
154          acc.times = +t || DEFAULT_TIMES;
155      } else {
156          throw new Error("Invalid arguments for async.retry");
157      }
158  }
159  module.exports = exports['default'];