@@ -13,7 +13,8 @@ const DEFAULT_OPTIONS = {
13
13
baseURI : null , // {String|null} a base URI to resolve relative URIs.
14
14
ignore : [ ] , // {Array<String>} URIs to be skipped from availability checks.
15
15
preferGET : [ ] , // {Array<String>} origins to prefer GET over HEAD.
16
- concurrency : 8 // concurrency count of linting link
16
+ concurrency : 8 , // {number} Concurrency count of linting link,
17
+ retry : 3 , // {number} Max retry count
17
18
} ;
18
19
19
20
// Adopted from http://stackoverflow.com/a/3809435/951517
@@ -69,13 +70,33 @@ function isIgnored(uri, ignore = []) {
69
70
return ignore . some ( ( pattern ) => minimatch ( uri , pattern ) ) ;
70
71
}
71
72
73
+ /**
74
+ * wait for ms and resolve the promise
75
+ * @param ms
76
+ * @returns {Promise<any> }
77
+ */
78
+ function waitTimeMs ( ms ) {
79
+ return new Promise ( resolve => {
80
+ setTimeout ( resolve , ms ) ;
81
+ } ) ;
82
+ }
83
+
72
84
/**
73
85
* Checks if a given URI is alive or not.
86
+ *
87
+ * Normally, this method following strategiry about retry
88
+ *
89
+ * 1. Head
90
+ * 2. Get
91
+ * 3. Get
92
+ *
74
93
* @param {string } uri
75
94
* @param {string } method
95
+ * @param {number } maxRetryCount
96
+ * @param {number } currentRetryCount
76
97
* @return {{ ok: boolean, redirect?: string, message: string } }
77
98
*/
78
- async function isAliveURI ( uri , method = 'HEAD' ) {
99
+ async function isAliveURI ( uri , method = 'HEAD' , maxRetryCount = 3 , currentRetryCount = 0 ) {
79
100
const { host } = URL . parse ( uri ) ;
80
101
const opts = {
81
102
method,
@@ -115,10 +136,17 @@ async function isAliveURI(uri, method = 'HEAD') {
115
136
} ;
116
137
}
117
138
118
- if ( ! res . ok && method === 'HEAD' ) {
119
- return isAliveURI ( uri , 'GET' ) ;
139
+ if ( ! res . ok && method === 'HEAD' && currentRetryCount < maxRetryCount ) {
140
+ return isAliveURI ( uri , 'GET' , maxRetryCount , currentRetryCount + 1 ) ;
120
141
}
121
142
143
+ // try to fetch again if not reach max retry count
144
+ if ( currentRetryCount < maxRetryCount ) {
145
+ // exponential retry
146
+ // 0ms -> 100ms -> 200ms -> 400ms -> 800ms ...
147
+ await waitTimeMs ( ( currentRetryCount ** 2 ) * 100 ) ;
148
+ return isAliveURI ( uri , 'GET' , maxRetryCount , currentRetryCount + 1 ) ;
149
+ }
122
150
return {
123
151
ok : res . ok ,
124
152
message : `${ res . status } ${ res . statusText } ` ,
@@ -127,8 +155,8 @@ async function isAliveURI(uri, method = 'HEAD') {
127
155
// Retry with `GET` method if the request failed
128
156
// as some servers don't accept `HEAD` requests but are OK with `GET` requests.
129
157
// https://github.com/textlint-rule/textlint-rule-no-dead-link/pull/86
130
- if ( method === 'HEAD' ) {
131
- return isAliveURI ( uri , 'GET' ) ;
158
+ if ( method === 'HEAD' && currentRetryCount < maxRetryCount ) {
159
+ return isAliveURI ( uri , 'GET' , maxRetryCount , currentRetryCount + 1 ) ;
132
160
}
133
161
134
162
return {
@@ -169,8 +197,9 @@ function reporter(context, options = {}) {
169
197
* @param {TextLintNode } node TextLintNode the URI belongs to.
170
198
* @param {string } uri a URI string to be linted.
171
199
* @param {number } index column number the URI is located at.
200
+ * @param {number } maxRetryCount retry count of linting
172
201
*/
173
- const lint = async ( { node, uri, index } ) => {
202
+ const lint = async ( { node, uri, index } , maxRetryCount ) => {
174
203
if ( isIgnored ( uri , opts . ignore ) ) {
175
204
return ;
176
205
}
@@ -209,12 +238,11 @@ function reporter(context, options = {}) {
209
238
210
239
const result = isLocal ( uri )
211
240
? await isAliveLocalFile ( uri )
212
- : await memorizedIsAliveURI ( uri , method ) ;
241
+ : await memorizedIsAliveURI ( uri , method , maxRetryCount ) ;
213
242
const { ok, redirected, redirectTo, message } = result ;
214
243
215
244
if ( ! ok ) {
216
245
const lintMessage = `${ uri } is dead. (${ message } )` ;
217
-
218
246
report ( node , new RuleError ( lintMessage , { index } ) ) ;
219
247
} else if ( redirected && ! opts . ignoreRedirects ) {
220
248
const lintMessage = `${ uri } is redirected to ${ redirectTo } . (${ message } )` ;
@@ -276,9 +304,9 @@ function reporter(context, options = {}) {
276
304
} ,
277
305
278
306
[ `${ context . Syntax . Document } :exit` ] ( ) {
279
- const linkTasks = URIs . map ( ( item ) => ( ) => lint ( item ) ) ;
307
+ const linkTasks = URIs . map ( ( item ) => ( ) => lint ( item , opts . retry ) ) ;
280
308
return pAll ( linkTasks , {
281
- concurrency : opts . concurrency
309
+ concurrency : opts . concurrency ,
282
310
} ) ;
283
311
} ,
284
312
} ;
0 commit comments