index.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. 'use strict';
  2. var Promise = require('any-promise');
  3. var util = require('util');
  4. var format = util.format;
  5. function TimeoutError(message, err) {
  6. Error.call(this);
  7. Error.captureStackTrace(this, TimeoutError);
  8. this.name = 'TimeoutError';
  9. this.message = message;
  10. this.previous = err;
  11. }
  12. util.inherits(TimeoutError, Error);
  13. function matches(match, err) {
  14. if (match === true) return true;
  15. if (typeof match === 'function') {
  16. try {
  17. if (err instanceof match) return true;
  18. } catch (_) {
  19. return !!match(err);
  20. }
  21. }
  22. if (match === err.toString()) return true;
  23. if (match === err.message) return true;
  24. return match instanceof RegExp
  25. && (match.test(err.message) || match.test(err.toString()));
  26. }
  27. module.exports = function retryAsPromised(callback, options) {
  28. if (!callback || !options) {
  29. throw new Error(
  30. 'retry-as-promised must be passed a callback and a options set or a number'
  31. );
  32. }
  33. if (typeof options === 'number') {
  34. options = {
  35. max: options
  36. };
  37. }
  38. // Super cheap clone
  39. options = {
  40. $current: options.$current || 1,
  41. max: options.max,
  42. timeout: options.timeout || undefined,
  43. match: options.match || [],
  44. backoffBase: options.backoffBase === undefined ? 100 : options.backoffBase,
  45. backoffExponent: options.backoffExponent || 1.1,
  46. report: options.report || function () {},
  47. name: options.name || callback.name || 'unknown'
  48. };
  49. if (!Array.isArray(options.match)) options.match = [options.match];
  50. options.report('Trying ' + options.name + ' #' + options.$current + ' at ' + new Date().toLocaleTimeString(), options);
  51. return new Promise(function(resolve, reject) {
  52. var timeout, backoffTimeout, lastError;
  53. if (options.timeout) {
  54. timeout = setTimeout(function() {
  55. if (backoffTimeout) clearTimeout(backoffTimeout);
  56. reject(new TimeoutError(options.name + ' timed out', lastError));
  57. }, options.timeout);
  58. }
  59. Promise.resolve(callback({ current: options.$current }))
  60. .then(resolve)
  61. .then(function() {
  62. if (timeout) clearTimeout(timeout);
  63. if (backoffTimeout) clearTimeout(backoffTimeout);
  64. })
  65. .catch(function(err) {
  66. if (timeout) clearTimeout(timeout);
  67. if (backoffTimeout) clearTimeout(backoffTimeout);
  68. lastError = err;
  69. options.report((err && err.toString()) || err, options);
  70. // Should not retry if max has been reached
  71. var shouldRetry = options.$current < options.max;
  72. if (!shouldRetry) return reject(err);
  73. shouldRetry = options.match.length === 0 || options.match.some(function (match) {
  74. return matches(match, err)
  75. });
  76. if (!shouldRetry) return reject(err);
  77. var retryDelay = Math.pow(
  78. options.backoffBase,
  79. Math.pow(options.backoffExponent, options.$current - 1)
  80. );
  81. // Do some accounting
  82. options.$current++;
  83. options.report(format('Retrying %s (%s)', options.name, options.$current), options);
  84. if (retryDelay) {
  85. // Use backoff function to ease retry rate
  86. options.report(format('Delaying retry of %s by %s', options.name, retryDelay), options);
  87. backoffTimeout = setTimeout(function() {
  88. retryAsPromised(callback, options)
  89. .then(resolve)
  90. .catch(reject);
  91. }, retryDelay);
  92. } else {
  93. retryAsPromised(callback, options)
  94. .then(resolve)
  95. .catch(reject);
  96. }
  97. });
  98. });
  99. };
  100. module.exports.TimeoutError = TimeoutError;