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