-
Notifications
You must be signed in to change notification settings - Fork 25
/
index.ts
135 lines (116 loc) · 4.54 KB
/
index.ts
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
'use strict';
export class TimeoutError extends Error {
previous: Error | undefined;
constructor(message: string, previousError?: Error) {
super(message);
this.name = "TimeoutError";
this.previous = previousError;
}
}
export type MatchOption =
| string
| RegExp
| Error
| Function;
export interface Options {
max: number;
timeout?: number | undefined;
match?: MatchOption[] | MatchOption | undefined;
backoffBase?: number | undefined;
backoffExponent?: number | undefined;
report?: ((message: string, obj: CoercedOptions, err?: any) => void) | undefined;
name?: string | undefined;
}
type CoercedOptions = {
$current: number;
max: number;
timeout?: number | undefined;
match: MatchOption[];
backoffBase: number;
backoffExponent: number;
report?: ((message: string, obj: CoercedOptions, err?: any) => void) | undefined;
name?: string | undefined;
}
type MaybePromise<T> = PromiseLike<T> | T;
type RetryCallback<T> = ({ current }: { current: CoercedOptions['$current'] }) => MaybePromise<T>;
function matches(match : MatchOption, err: Error) {
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()));
}
export function retryAsPromised<T>(callback : RetryCallback<T>, optionsInput : Options | number | CoercedOptions) : Promise<T> {
if (!callback || !optionsInput) {
throw new Error(
'retry-as-promised must be passed a callback and a options set'
);
}
optionsInput = (typeof optionsInput === "number" ? {max: optionsInput} : optionsInput) as Options | CoercedOptions;
const options : CoercedOptions = {
$current: "$current" in optionsInput ? optionsInput.$current : 1,
max: optionsInput.max,
timeout: optionsInput.timeout || undefined,
match: optionsInput.match ? Array.isArray(optionsInput.match) ? optionsInput.match : [optionsInput.match] : [],
backoffBase: optionsInput.backoffBase === undefined ? 100 : optionsInput.backoffBase,
backoffExponent: optionsInput.backoffExponent || 1.1,
report: optionsInput.report,
name: optionsInput.name || callback.name || 'unknown'
};
if (options.match && !Array.isArray(options.match)) options.match = [options.match];
if (options.report) options.report('Trying ' + options.name + ' #' + options.$current + ' at ' + new Date().toLocaleTimeString(), options);
return new Promise(function(resolve, reject) {
let timeout : NodeJS.Timeout | undefined;
let backoffTimeout : NodeJS.Timeout | undefined;
let lastError : Error | undefined;
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;
if (options.report) options.report((err && err.toString()) || err, options, err);
// 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 = options.backoffBase * Math.pow(options.backoffExponent, options.$current - 1);
// Do some accounting
options.$current++;
if (options.report) options.report(`Retrying ${options.name} (${options.$current})`, options);
if (retryDelay) {
// Use backoff function to ease retry rate
if (options.report) options.report(`Delaying retry of ${options.name} by ${retryDelay}`, options);
backoffTimeout = setTimeout(function() {
retryAsPromised(callback, options)
.then(resolve)
.catch(reject);
}, retryDelay);
} else {
retryAsPromised(callback, options)
.then(resolve)
.catch(reject);
}
});
});
};
export default retryAsPromised;