Skip to content

Commit 86fd2a9

Browse files
authored
feat(rule): add retry option (#123)
* feat(rule): add `retry` option The default retry count is `3`. * fix * feat(rule): implement exponential retry * docs: update
1 parent 9e65ee1 commit 86fd2a9

File tree

3 files changed

+408
-333
lines changed

3 files changed

+408
-333
lines changed

ReadMe.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ The default options are:
6464
"baseURI": null,
6565
"ignore": [],
6666
"preferGET": [],
67-
"ignoreRedirects": false
67+
"ignoreRedirects": false,
68+
"retry": 3
6869
}
6970
}
7071
}
@@ -132,6 +133,11 @@ Example:
132133
This rule checks for redirects (3xx status codes) and consider's them an error by default.
133134
To ignore redirects during checks, set this value to `false`.
134135

136+
### retry
137+
138+
This rule checks the url with retry.
139+
The default max retry count is `3`.
140+
135141
## Tests
136142

137143
```

src/no-dead-link.js

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const DEFAULT_OPTIONS = {
1313
baseURI: null, // {String|null} a base URI to resolve relative URIs.
1414
ignore: [], // {Array<String>} URIs to be skipped from availability checks.
1515
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
1718
};
1819

1920
// Adopted from http://stackoverflow.com/a/3809435/951517
@@ -69,13 +70,33 @@ function isIgnored(uri, ignore = []) {
6970
return ignore.some((pattern) => minimatch(uri, pattern));
7071
}
7172

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+
7284
/**
7385
* 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+
*
7493
* @param {string} uri
7594
* @param {string} method
95+
* @param {number} maxRetryCount
96+
* @param {number} currentRetryCount
7697
* @return {{ ok: boolean, redirect?: string, message: string }}
7798
*/
78-
async function isAliveURI(uri, method = 'HEAD') {
99+
async function isAliveURI(uri, method = 'HEAD', maxRetryCount = 3, currentRetryCount = 0) {
79100
const { host } = URL.parse(uri);
80101
const opts = {
81102
method,
@@ -115,10 +136,17 @@ async function isAliveURI(uri, method = 'HEAD') {
115136
};
116137
}
117138

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);
120141
}
121142

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+
}
122150
return {
123151
ok: res.ok,
124152
message: `${res.status} ${res.statusText}`,
@@ -127,8 +155,8 @@ async function isAliveURI(uri, method = 'HEAD') {
127155
// Retry with `GET` method if the request failed
128156
// as some servers don't accept `HEAD` requests but are OK with `GET` requests.
129157
// 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);
132160
}
133161

134162
return {
@@ -169,8 +197,9 @@ function reporter(context, options = {}) {
169197
* @param {TextLintNode} node TextLintNode the URI belongs to.
170198
* @param {string} uri a URI string to be linted.
171199
* @param {number} index column number the URI is located at.
200+
* @param {number} maxRetryCount retry count of linting
172201
*/
173-
const lint = async ({ node, uri, index }) => {
202+
const lint = async ({ node, uri, index }, maxRetryCount) => {
174203
if (isIgnored(uri, opts.ignore)) {
175204
return;
176205
}
@@ -209,12 +238,11 @@ function reporter(context, options = {}) {
209238

210239
const result = isLocal(uri)
211240
? await isAliveLocalFile(uri)
212-
: await memorizedIsAliveURI(uri, method);
241+
: await memorizedIsAliveURI(uri, method, maxRetryCount);
213242
const { ok, redirected, redirectTo, message } = result;
214243

215244
if (!ok) {
216245
const lintMessage = `${uri} is dead. (${message})`;
217-
218246
report(node, new RuleError(lintMessage, { index }));
219247
} else if (redirected && !opts.ignoreRedirects) {
220248
const lintMessage = `${uri} is redirected to ${redirectTo}. (${message})`;
@@ -276,9 +304,9 @@ function reporter(context, options = {}) {
276304
},
277305

278306
[`${context.Syntax.Document}:exit`]() {
279-
const linkTasks = URIs.map((item) => () => lint(item));
307+
const linkTasks = URIs.map((item) => () => lint(item, opts.retry));
280308
return pAll(linkTasks, {
281-
concurrency: opts.concurrency
309+
concurrency: opts.concurrency,
282310
});
283311
},
284312
};

0 commit comments

Comments
 (0)