recker 1.0.72 → 1.0.75-next.2e5a94f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -18
- package/dist/browser/core/client.d.ts +14 -8
- package/dist/browser/core/client.js +199 -17
- package/dist/browser/core/errors.d.ts +15 -1
- package/dist/browser/core/errors.js +140 -9
- package/dist/browser/core/request.d.ts +5 -0
- package/dist/browser/core/request.js +33 -2
- package/dist/browser/core-runtime/plugin-manifest.d.ts +24 -0
- package/dist/browser/core-runtime/plugin-manifest.js +159 -0
- package/dist/browser/core-runtime/request-context.d.ts +13 -0
- package/dist/browser/core-runtime/request-context.js +24 -0
- package/dist/browser/core-runtime/typed-events.d.ts +89 -0
- package/dist/browser/core-runtime/typed-events.js +34 -0
- package/dist/browser/index.iife.min.js +79 -79
- package/dist/browser/index.min.js +79 -79
- package/dist/browser/index.mini.iife.js +913 -97
- package/dist/browser/index.mini.iife.min.js +46 -46
- package/dist/browser/index.mini.min.js +46 -46
- package/dist/browser/index.mini.umd.js +913 -97
- package/dist/browser/index.mini.umd.min.js +46 -46
- package/dist/browser/index.umd.min.js +79 -79
- package/dist/browser/plugins/auth/aws-sigv4.d.ts +1 -0
- package/dist/browser/plugins/auth/aws-sigv4.js +19 -2
- package/dist/browser/plugins/retry.js +29 -1
- package/dist/browser/presets/aws.d.ts +1 -0
- package/dist/browser/presets/aws.js +62 -1
- package/dist/browser/runner/request-runner.d.ts +15 -5
- package/dist/browser/runner/request-runner.js +164 -30
- package/dist/browser/scrape/parser/nodes/html.d.ts +6 -0
- package/dist/browser/scrape/parser/nodes/html.js +70 -18
- package/dist/browser/scrape/parser/nodes/node.d.ts +1 -0
- package/dist/browser/scrape/parser/nodes/node.js +5 -0
- package/dist/browser/scrape/spider.d.ts +1 -0
- package/dist/browser/scrape/spider.js +39 -26
- package/dist/browser/seo/analyzer.d.ts +1 -1
- package/dist/browser/seo/analyzer.js +73 -42
- package/dist/browser/seo/index.d.ts +1 -1
- package/dist/browser/seo/rules/types.d.ts +2 -0
- package/dist/browser/seo/seo-spider.d.ts +2 -3
- package/dist/browser/seo/seo-spider.js +26 -202
- package/dist/browser/seo/types.d.ts +4 -0
- package/dist/browser/seo/validators/sitemap.js +9 -2
- package/dist/browser/transport/fetch.js +38 -5
- package/dist/browser/transport/undici.js +73 -11
- package/dist/browser/transport/worker.d.ts +0 -1
- package/dist/browser/transport/worker.js +1 -3
- package/dist/browser/types/index.d.ts +24 -0
- package/dist/cli/commands/mcp.js +5 -3
- package/dist/core/client.d.ts +14 -8
- package/dist/core/client.js +199 -17
- package/dist/core/errors.d.ts +15 -1
- package/dist/core/errors.js +140 -9
- package/dist/core/request.d.ts +5 -0
- package/dist/core/request.js +33 -2
- package/dist/core-runtime/plugin-manifest.d.ts +24 -0
- package/dist/core-runtime/plugin-manifest.js +159 -0
- package/dist/core-runtime/request-context.d.ts +13 -0
- package/dist/core-runtime/request-context.js +24 -0
- package/dist/core-runtime/typed-events.d.ts +89 -0
- package/dist/core-runtime/typed-events.js +34 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mcp/cli.js +10 -8
- package/dist/mcp/profiles.d.ts +1 -1
- package/dist/mcp/profiles.js +31 -6
- package/dist/mcp/tools/categories.js +0 -1
- package/dist/mcp/tools/seo.js +320 -4
- package/dist/plugins/auth/aws-sigv4.d.ts +1 -0
- package/dist/plugins/auth/aws-sigv4.js +19 -2
- package/dist/plugins/retry.js +29 -1
- package/dist/presets/aws.d.ts +1 -0
- package/dist/presets/aws.js +62 -1
- package/dist/recker.d.ts +3 -0
- package/dist/recker.js +5 -0
- package/dist/runner/request-runner.d.ts +15 -5
- package/dist/runner/request-runner.js +164 -30
- package/dist/scrape/parser/nodes/html.d.ts +6 -0
- package/dist/scrape/parser/nodes/html.js +70 -18
- package/dist/scrape/parser/nodes/node.d.ts +1 -0
- package/dist/scrape/parser/nodes/node.js +5 -0
- package/dist/scrape/spider.d.ts +1 -0
- package/dist/scrape/spider.js +39 -26
- package/dist/search/google.d.ts +67 -0
- package/dist/search/google.js +480 -0
- package/dist/search/index.d.ts +3 -0
- package/dist/search/index.js +1 -0
- package/dist/seo/analyzer.d.ts +1 -1
- package/dist/seo/analyzer.js +73 -42
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/rules/types.d.ts +2 -0
- package/dist/seo/seo-spider.d.ts +2 -3
- package/dist/seo/seo-spider.js +26 -202
- package/dist/seo/types.d.ts +4 -0
- package/dist/seo/validators/sitemap.js +9 -2
- package/dist/transport/fetch.js +38 -5
- package/dist/transport/undici.js +73 -11
- package/dist/transport/worker.d.ts +0 -1
- package/dist/transport/worker.js +1 -3
- package/dist/types/index.d.ts +24 -0
- package/dist/version.js +1 -1
- package/package.json +9 -1
|
@@ -7,9 +7,29 @@ import * as fs from 'fs/promises';
|
|
|
7
7
|
export class SeoSpider {
|
|
8
8
|
spider;
|
|
9
9
|
options;
|
|
10
|
-
seoResults = new Map();
|
|
11
10
|
seoPages = [];
|
|
12
11
|
homeHtml = '';
|
|
12
|
+
normalizeUrl(url) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL(url);
|
|
15
|
+
parsed.hash = '';
|
|
16
|
+
parsed.searchParams.sort();
|
|
17
|
+
if (parsed.pathname !== '/' && parsed.pathname.endsWith('/')) {
|
|
18
|
+
parsed.pathname = parsed.pathname.slice(0, -1);
|
|
19
|
+
}
|
|
20
|
+
return parsed.toString();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return url;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
toHeaderRecord(headers) {
|
|
27
|
+
const headerRecord = {};
|
|
28
|
+
headers.forEach((value, key) => {
|
|
29
|
+
headerRecord[key] = value;
|
|
30
|
+
});
|
|
31
|
+
return headerRecord;
|
|
32
|
+
}
|
|
13
33
|
constructor(options = {}) {
|
|
14
34
|
this.options = options;
|
|
15
35
|
this.spider = new Spider({
|
|
@@ -36,11 +56,12 @@ export class SeoSpider {
|
|
|
36
56
|
: undefined;
|
|
37
57
|
const seoReport = await analyzeSeo(html, {
|
|
38
58
|
baseUrl: pageResult.url,
|
|
59
|
+
timings: pageResult.timings,
|
|
60
|
+
htmlSize: pageResult.metrics?.htmlSize,
|
|
39
61
|
rules: rulesOptions,
|
|
40
62
|
});
|
|
41
63
|
const seoPage = { ...pageResult, seoReport };
|
|
42
64
|
this.seoPages.push(seoPage);
|
|
43
|
-
this.seoResults.set(pageResult.url, seoReport);
|
|
44
65
|
this.options.onSeoAnalysis?.(seoPage);
|
|
45
66
|
}
|
|
46
67
|
catch {
|
|
@@ -50,7 +71,6 @@ export class SeoSpider {
|
|
|
50
71
|
}
|
|
51
72
|
async crawl(startUrl) {
|
|
52
73
|
this.seoPages = [];
|
|
53
|
-
this.seoResults.clear();
|
|
54
74
|
this.homeHtml = '';
|
|
55
75
|
const result = await this.spider.crawl(startUrl);
|
|
56
76
|
if (!this.options.seo) {
|
|
@@ -206,7 +226,7 @@ export class SeoSpider {
|
|
|
206
226
|
return {
|
|
207
227
|
status: res.status,
|
|
208
228
|
text,
|
|
209
|
-
headers:
|
|
229
|
+
headers: this.toHeaderRecord(res.headers),
|
|
210
230
|
};
|
|
211
231
|
};
|
|
212
232
|
const result = await fetchAndValidateSitemap(sitemapUrl, fetcher);
|
|
@@ -216,191 +236,6 @@ export class SeoSpider {
|
|
|
216
236
|
return undefined;
|
|
217
237
|
}
|
|
218
238
|
}
|
|
219
|
-
createReportFromPageData(page) {
|
|
220
|
-
const checks = [];
|
|
221
|
-
if (page.title) {
|
|
222
|
-
const titleLength = page.title.length;
|
|
223
|
-
if (titleLength < 30) {
|
|
224
|
-
checks.push({
|
|
225
|
-
id: 'title-length',
|
|
226
|
-
name: 'Title Length',
|
|
227
|
-
category: 'title',
|
|
228
|
-
status: 'warn',
|
|
229
|
-
message: `Title is ${titleLength} characters`,
|
|
230
|
-
value: titleLength,
|
|
231
|
-
recommendation: 'Title should be 50-60 characters',
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
else if (titleLength > 60) {
|
|
235
|
-
checks.push({
|
|
236
|
-
id: 'title-length',
|
|
237
|
-
name: 'Title Length',
|
|
238
|
-
category: 'title',
|
|
239
|
-
status: 'warn',
|
|
240
|
-
message: `Title is too long (${titleLength} chars)`,
|
|
241
|
-
value: titleLength,
|
|
242
|
-
recommendation: 'Title should be 50-60 characters',
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
checks.push({
|
|
247
|
-
id: 'title-length',
|
|
248
|
-
name: 'Title Length',
|
|
249
|
-
category: 'title',
|
|
250
|
-
status: 'pass',
|
|
251
|
-
message: `Good title length (${titleLength} chars)`,
|
|
252
|
-
value: titleLength,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
checks.push({
|
|
258
|
-
id: 'title-missing',
|
|
259
|
-
name: 'Title',
|
|
260
|
-
category: 'title',
|
|
261
|
-
status: 'fail',
|
|
262
|
-
message: 'Page has no title',
|
|
263
|
-
recommendation: 'Add a descriptive <title> tag',
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
const internalLinks = page.links.filter(l => l.type === 'internal').length;
|
|
267
|
-
const externalLinks = page.links.filter(l => l.type === 'external').length;
|
|
268
|
-
if (internalLinks === 0) {
|
|
269
|
-
checks.push({
|
|
270
|
-
id: 'internal-links',
|
|
271
|
-
name: 'Internal Links',
|
|
272
|
-
category: 'links',
|
|
273
|
-
status: 'warn',
|
|
274
|
-
message: 'No internal links found',
|
|
275
|
-
recommendation: 'Add internal links to improve site structure',
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
checks.push({
|
|
280
|
-
id: 'internal-links',
|
|
281
|
-
name: 'Internal Links',
|
|
282
|
-
category: 'links',
|
|
283
|
-
status: 'pass',
|
|
284
|
-
message: `${internalLinks} internal links found`,
|
|
285
|
-
value: internalLinks,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
const scoreSum = checks.reduce((sum, c) => {
|
|
289
|
-
if (c.status === 'pass')
|
|
290
|
-
return sum + 100;
|
|
291
|
-
if (c.status === 'warn')
|
|
292
|
-
return sum + 50;
|
|
293
|
-
return sum;
|
|
294
|
-
}, 0);
|
|
295
|
-
const score = checks.length > 0 ? Math.round(scoreSum / checks.length) : 0;
|
|
296
|
-
const passed = checks.filter(c => c.status === 'pass').length;
|
|
297
|
-
const warnings = checks.filter(c => c.status === 'warn').length;
|
|
298
|
-
const errors = checks.filter(c => c.status === 'fail').length;
|
|
299
|
-
const infos = checks.filter(c => c.status === 'info').length;
|
|
300
|
-
const passRate = checks.length > 0 ? Math.round((passed / checks.length) * 100) : 0;
|
|
301
|
-
return {
|
|
302
|
-
url: page.url,
|
|
303
|
-
timestamp: new Date(),
|
|
304
|
-
grade: this.scoreToGrade(score),
|
|
305
|
-
score,
|
|
306
|
-
summary: {
|
|
307
|
-
totalChecks: checks.length,
|
|
308
|
-
passed,
|
|
309
|
-
warnings,
|
|
310
|
-
errors,
|
|
311
|
-
infos,
|
|
312
|
-
passRate,
|
|
313
|
-
issuesByCategory: {},
|
|
314
|
-
topIssues: checks
|
|
315
|
-
.filter(c => c.status === 'fail' || c.status === 'warn')
|
|
316
|
-
.slice(0, 5)
|
|
317
|
-
.map(c => ({
|
|
318
|
-
name: c.name,
|
|
319
|
-
message: c.message,
|
|
320
|
-
category: 'general',
|
|
321
|
-
severity: c.status === 'fail' ? 'error' : 'warning',
|
|
322
|
-
})),
|
|
323
|
-
quickWins: [],
|
|
324
|
-
vitals: {
|
|
325
|
-
wordCount: 0,
|
|
326
|
-
readingTime: 0,
|
|
327
|
-
imageCount: 0,
|
|
328
|
-
linkCount: page.links.length,
|
|
329
|
-
},
|
|
330
|
-
completeness: {
|
|
331
|
-
meta: 0,
|
|
332
|
-
social: 0,
|
|
333
|
-
technical: 0,
|
|
334
|
-
content: 0,
|
|
335
|
-
images: 0,
|
|
336
|
-
links: 0,
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
checks,
|
|
340
|
-
title: page.title ? { text: page.title, length: page.title.length } : undefined,
|
|
341
|
-
headings: {
|
|
342
|
-
structure: [],
|
|
343
|
-
h1Count: 0,
|
|
344
|
-
hasProperHierarchy: false,
|
|
345
|
-
issues: [],
|
|
346
|
-
},
|
|
347
|
-
content: {
|
|
348
|
-
wordCount: 0,
|
|
349
|
-
characterCount: 0,
|
|
350
|
-
sentenceCount: 0,
|
|
351
|
-
paragraphCount: 0,
|
|
352
|
-
readingTimeMinutes: 0,
|
|
353
|
-
avgWordsPerSentence: 0,
|
|
354
|
-
avgParagraphLength: 0,
|
|
355
|
-
listCount: 0,
|
|
356
|
-
strongTagCount: 0,
|
|
357
|
-
emTagCount: 0,
|
|
358
|
-
},
|
|
359
|
-
links: {
|
|
360
|
-
total: page.links.length,
|
|
361
|
-
internal: internalLinks,
|
|
362
|
-
external: externalLinks,
|
|
363
|
-
nofollow: 0,
|
|
364
|
-
broken: 0,
|
|
365
|
-
withoutText: page.links.filter(l => !l.text?.trim()).length,
|
|
366
|
-
sponsoredLinks: 0,
|
|
367
|
-
ugcLinks: 0,
|
|
368
|
-
},
|
|
369
|
-
images: {
|
|
370
|
-
total: 0,
|
|
371
|
-
withAlt: 0,
|
|
372
|
-
withoutAlt: 0,
|
|
373
|
-
lazy: 0,
|
|
374
|
-
missingDimensions: 0,
|
|
375
|
-
modernFormats: 0,
|
|
376
|
-
altTextLengths: [],
|
|
377
|
-
imageAltTexts: [],
|
|
378
|
-
imageFilenames: [],
|
|
379
|
-
imagesWithAsyncDecoding: 0,
|
|
380
|
-
},
|
|
381
|
-
social: {
|
|
382
|
-
openGraph: {
|
|
383
|
-
present: false, hasTitle: false, hasDescription: false, hasImage: false, hasUrl: false, issues: []
|
|
384
|
-
},
|
|
385
|
-
twitterCard: {
|
|
386
|
-
present: false, hasCard: false, hasTitle: false, hasDescription: false, hasImage: false, issues: []
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
keywords: { totalWords: 0, uniqueWords: 0, topKeywords: [] },
|
|
390
|
-
technical: {
|
|
391
|
-
hasCanonical: false,
|
|
392
|
-
hasRobotsMeta: false,
|
|
393
|
-
hasViewport: false,
|
|
394
|
-
hasCharset: false,
|
|
395
|
-
hasLang: false,
|
|
396
|
-
},
|
|
397
|
-
structuredData: {
|
|
398
|
-
count: 0,
|
|
399
|
-
types: [],
|
|
400
|
-
items: [],
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
239
|
detectSiteWideIssues(pages) {
|
|
405
240
|
const issues = [];
|
|
406
241
|
const titleGroups = new Map();
|
|
@@ -465,12 +300,12 @@ export class SeoSpider {
|
|
|
465
300
|
for (const page of pages) {
|
|
466
301
|
for (const link of page.links) {
|
|
467
302
|
if (link.type === 'internal' && link.href) {
|
|
468
|
-
linkedUrls.add(link.href);
|
|
303
|
+
linkedUrls.add(this.normalizeUrl(link.href));
|
|
469
304
|
}
|
|
470
305
|
}
|
|
471
306
|
}
|
|
472
307
|
const orphanPages = pages
|
|
473
|
-
.filter(p => !linkedUrls.has(p.url)
|
|
308
|
+
.filter(p => p.depth > 0 && !linkedUrls.has(this.normalizeUrl(p.url)))
|
|
474
309
|
.map(p => p.url);
|
|
475
310
|
if (orphanPages.length > 0) {
|
|
476
311
|
issues.push({
|
|
@@ -507,17 +342,6 @@ export class SeoSpider {
|
|
|
507
342
|
orphanPages,
|
|
508
343
|
};
|
|
509
344
|
}
|
|
510
|
-
scoreToGrade(score) {
|
|
511
|
-
if (score >= 90)
|
|
512
|
-
return 'A';
|
|
513
|
-
if (score >= 80)
|
|
514
|
-
return 'B';
|
|
515
|
-
if (score >= 70)
|
|
516
|
-
return 'C';
|
|
517
|
-
if (score >= 60)
|
|
518
|
-
return 'D';
|
|
519
|
-
return 'F';
|
|
520
|
-
}
|
|
521
345
|
async saveReport(result) {
|
|
522
346
|
if (!this.options.output)
|
|
523
347
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { KeywordCloud } from './keywords.js';
|
|
2
2
|
export type { KeywordCloud, KeywordItem } from './keywords.js';
|
|
3
3
|
export type SeoStatus = 'pass' | 'warn' | 'fail' | 'info';
|
|
4
|
+
export type SeoPageType = 'homepage' | 'product' | 'article' | 'category' | 'search' | 'other';
|
|
4
5
|
export interface SeoCheckEvidence {
|
|
5
6
|
found?: string | number | string[];
|
|
6
7
|
expected?: string | number | string[];
|
|
@@ -15,6 +16,7 @@ export interface SeoCheckResult {
|
|
|
15
16
|
name: string;
|
|
16
17
|
category: string;
|
|
17
18
|
status: SeoStatus;
|
|
19
|
+
severity?: 'error' | 'warning' | 'info';
|
|
18
20
|
message: string;
|
|
19
21
|
value?: string | number;
|
|
20
22
|
recommendation?: string;
|
|
@@ -107,6 +109,7 @@ export interface SeoTiming {
|
|
|
107
109
|
download?: number;
|
|
108
110
|
}
|
|
109
111
|
export interface SeoSummary {
|
|
112
|
+
pageType?: SeoPageType;
|
|
110
113
|
totalChecks: number;
|
|
111
114
|
passed: number;
|
|
112
115
|
warnings: number;
|
|
@@ -150,6 +153,7 @@ export interface SeoReport {
|
|
|
150
153
|
timestamp: Date;
|
|
151
154
|
grade: string;
|
|
152
155
|
score: number;
|
|
156
|
+
pageType?: SeoPageType;
|
|
153
157
|
summary: SeoSummary;
|
|
154
158
|
timing?: SeoTiming;
|
|
155
159
|
checks: SeoCheckResult[];
|
|
@@ -2,6 +2,13 @@ import { parse } from '../../scrape/parser/index.js';
|
|
|
2
2
|
const VALID_CHANGEFREQ = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'];
|
|
3
3
|
const MAX_URLS_PER_SITEMAP = 50000;
|
|
4
4
|
const MAX_SITEMAP_SIZE = 50 * 1024 * 1024;
|
|
5
|
+
function toHeaderRecord(headers) {
|
|
6
|
+
const headerRecord = {};
|
|
7
|
+
headers.forEach((value, key) => {
|
|
8
|
+
headerRecord[key] = value;
|
|
9
|
+
});
|
|
10
|
+
return headerRecord;
|
|
11
|
+
}
|
|
5
12
|
export function parseSitemap(content, compressed = false) {
|
|
6
13
|
const errors = [];
|
|
7
14
|
const warnings = [];
|
|
@@ -141,7 +148,7 @@ export function parseSitemap(content, compressed = false) {
|
|
|
141
148
|
warnings,
|
|
142
149
|
urls,
|
|
143
150
|
sitemaps,
|
|
144
|
-
urlCount: type === 'urlset' ? urls.length : sitemaps.
|
|
151
|
+
urlCount: type === 'urlset' ? urls.length : sitemaps.length,
|
|
145
152
|
size: content.length,
|
|
146
153
|
compressed,
|
|
147
154
|
};
|
|
@@ -339,7 +346,7 @@ export async function fetchAndValidateSitemap(url, fetcher) {
|
|
|
339
346
|
response = {
|
|
340
347
|
status: fetchResponse.status,
|
|
341
348
|
text: await fetchResponse.text(),
|
|
342
|
-
headers:
|
|
349
|
+
headers: toHeaderRecord(fetchResponse.headers),
|
|
343
350
|
};
|
|
344
351
|
}
|
|
345
352
|
if (response.status === 404) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createProgressStream } from '../utils/progress.js';
|
|
2
|
+
import { TimeoutError } from '../core/errors.js';
|
|
2
3
|
function parseContentLength(headers) {
|
|
3
4
|
const raw = headers.get('content-length');
|
|
4
5
|
if (!raw)
|
|
@@ -6,6 +7,19 @@ function parseContentLength(headers) {
|
|
|
6
7
|
const parsed = parseInt(raw, 10);
|
|
7
8
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
8
9
|
}
|
|
10
|
+
function getAbortReason(error) {
|
|
11
|
+
return error?.cause ?? error?.reason;
|
|
12
|
+
}
|
|
13
|
+
function isTimeoutReason(reason) {
|
|
14
|
+
if (reason instanceof TimeoutError) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (!reason || typeof reason !== 'object') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const timeoutReason = reason;
|
|
21
|
+
return timeoutReason.name === 'TimeoutError';
|
|
22
|
+
}
|
|
9
23
|
function wrapDownloadResponse(response, onProgress) {
|
|
10
24
|
if (!onProgress || !response.body)
|
|
11
25
|
return response;
|
|
@@ -66,10 +80,16 @@ export class FetchTransport {
|
|
|
66
80
|
? req.timeout
|
|
67
81
|
: req.timeout?.request;
|
|
68
82
|
let signal = req.signal;
|
|
83
|
+
const requestTimeoutError = timeoutMs ? new TimeoutError(req, {
|
|
84
|
+
phase: 'request',
|
|
85
|
+
timeout: timeoutMs
|
|
86
|
+
}) : undefined;
|
|
69
87
|
if (timeoutMs && !signal) {
|
|
70
88
|
abortController = new AbortController();
|
|
71
89
|
signal = abortController.signal;
|
|
72
|
-
timeoutId = setTimeout(() =>
|
|
90
|
+
timeoutId = setTimeout(() => {
|
|
91
|
+
timeoutControllerAbort(abortController, requestTimeoutError);
|
|
92
|
+
}, timeoutMs);
|
|
73
93
|
}
|
|
74
94
|
const followRedirects = req.followRedirects !== false;
|
|
75
95
|
const maxRedirects = req.maxRedirects ?? 20;
|
|
@@ -148,10 +168,13 @@ export class FetchTransport {
|
|
|
148
168
|
}
|
|
149
169
|
}
|
|
150
170
|
catch (error) {
|
|
151
|
-
if (error.name === 'AbortError'
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
171
|
+
if (timeoutMs && (error.name === 'AbortError' || error.name === 'TimeoutError')) {
|
|
172
|
+
const timeoutReason = getAbortReason(error);
|
|
173
|
+
if (isTimeoutReason(timeoutReason) ||
|
|
174
|
+
isTimeoutReason(getAbortReason(signal)) ||
|
|
175
|
+
(abortController && isTimeoutReason(requestTimeoutError))) {
|
|
176
|
+
throw timeoutReason instanceof TimeoutError ? timeoutReason : requestTimeoutError ?? error;
|
|
177
|
+
}
|
|
155
178
|
}
|
|
156
179
|
throw error;
|
|
157
180
|
}
|
|
@@ -161,6 +184,16 @@ export class FetchTransport {
|
|
|
161
184
|
}
|
|
162
185
|
}
|
|
163
186
|
}
|
|
187
|
+
function timeoutControllerAbort(controller, reason) {
|
|
188
|
+
if (!controller.signal.aborted) {
|
|
189
|
+
if (reason) {
|
|
190
|
+
controller.abort(reason);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
controller.abort();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
164
197
|
class FetchResponseWrapper {
|
|
165
198
|
raw;
|
|
166
199
|
timings;
|
|
@@ -15,6 +15,40 @@ const undiciBodySentChannel = channel('undici:request:bodySent');
|
|
|
15
15
|
const undiciHeadersChannel = channel('undici:request:headers');
|
|
16
16
|
const undiciConnectChannel = channel('undici:client:connect');
|
|
17
17
|
const requestStorage = new AsyncLocalStorage();
|
|
18
|
+
function getAbortReason(error) {
|
|
19
|
+
return error?.cause ?? error?.reason;
|
|
20
|
+
}
|
|
21
|
+
function isTimeoutAbortReason(reason) {
|
|
22
|
+
if (reason instanceof TimeoutError) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (!reason || typeof reason !== 'object') {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const timeoutReason = reason;
|
|
29
|
+
return timeoutReason.name === 'TimeoutError';
|
|
30
|
+
}
|
|
31
|
+
function isTotalRequestTimeoutError(error, timeoutReason, timeoutMs) {
|
|
32
|
+
if (!timeoutMs) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (timeoutReason !== undefined) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const causeOrReason = getAbortReason(error);
|
|
39
|
+
const message = error?.message;
|
|
40
|
+
const hasTimeoutMessage = typeof message === 'string' && (message.includes('Request timed out (total time exceeded)') ||
|
|
41
|
+
message.includes('timed out (total time exceeded)'));
|
|
42
|
+
const hasRequestAbortMessage = typeof message === 'string' && message.toLowerCase() === 'request was aborted';
|
|
43
|
+
return (error instanceof TimeoutError ||
|
|
44
|
+
isTimeoutAbortReason(causeOrReason) ||
|
|
45
|
+
isTimeoutAbortReason(error) ||
|
|
46
|
+
error instanceof undiciErrors.RequestAbortedError ||
|
|
47
|
+
hasTimeoutMessage ||
|
|
48
|
+
hasRequestAbortMessage ||
|
|
49
|
+
error?.code === 'ABORT_ERR' ||
|
|
50
|
+
error?.code === 'UND_ERR_REQUEST_TIMEOUT');
|
|
51
|
+
}
|
|
18
52
|
undiciRequestChannel.subscribe((message) => {
|
|
19
53
|
const store = requestStorage.getStore();
|
|
20
54
|
if (store) {
|
|
@@ -36,7 +70,7 @@ undiciRequestChannel.subscribe((message) => {
|
|
|
36
70
|
}
|
|
37
71
|
}
|
|
38
72
|
});
|
|
39
|
-
undiciBodySentChannel.subscribe((
|
|
73
|
+
undiciBodySentChannel.subscribe((_message) => {
|
|
40
74
|
const store = requestStorage.getStore();
|
|
41
75
|
if (store?.hooks && store.hooks.onRequestSent) {
|
|
42
76
|
store.hooks.onRequestSent();
|
|
@@ -317,6 +351,9 @@ export class UndiciTransport {
|
|
|
317
351
|
});
|
|
318
352
|
let timeoutController;
|
|
319
353
|
let timeoutId;
|
|
354
|
+
let timeoutReason;
|
|
355
|
+
let timeoutError;
|
|
356
|
+
let cancelOriginalAbortListener;
|
|
320
357
|
if (!this.observability) {
|
|
321
358
|
return this.dispatchFast(req, headers, currentUrl, timeouts, handleRedirectsManually, maxRedirects, followRedirects, uploadTotal);
|
|
322
359
|
}
|
|
@@ -340,24 +377,32 @@ export class UndiciTransport {
|
|
|
340
377
|
let effectiveSignal = req.signal;
|
|
341
378
|
if (timeouts.totalTimeout) {
|
|
342
379
|
timeoutController = new AbortController();
|
|
380
|
+
timeoutError = new TimeoutError(req, {
|
|
381
|
+
phase: 'request',
|
|
382
|
+
timeout: timeouts.totalTimeout
|
|
383
|
+
});
|
|
343
384
|
if (req.signal) {
|
|
344
385
|
const originalSignal = req.signal;
|
|
345
386
|
effectiveSignal = timeoutController.signal;
|
|
346
387
|
const onOriginalAbort = () => {
|
|
347
|
-
|
|
388
|
+
timeoutReason = originalSignal.reason ?? new Error('Request aborted by external signal');
|
|
389
|
+
timeoutController.abort(timeoutReason);
|
|
348
390
|
};
|
|
349
391
|
if (originalSignal.aborted) {
|
|
350
|
-
|
|
392
|
+
timeoutReason = originalSignal.reason ?? new Error('Request aborted by external signal');
|
|
393
|
+
timeoutController.abort(timeoutReason);
|
|
351
394
|
}
|
|
352
395
|
else {
|
|
353
396
|
originalSignal.addEventListener('abort', onOriginalAbort, { once: true });
|
|
397
|
+
cancelOriginalAbortListener = () => originalSignal.removeEventListener('abort', onOriginalAbort);
|
|
354
398
|
}
|
|
355
399
|
}
|
|
356
400
|
else {
|
|
357
401
|
effectiveSignal = timeoutController.signal;
|
|
358
402
|
}
|
|
359
403
|
timeoutId = setTimeout(() => {
|
|
360
|
-
|
|
404
|
+
timeoutReason = timeoutError;
|
|
405
|
+
timeoutController.abort(timeoutReason);
|
|
361
406
|
}, timeouts.totalTimeout);
|
|
362
407
|
}
|
|
363
408
|
while (true) {
|
|
@@ -593,8 +638,8 @@ export class UndiciTransport {
|
|
|
593
638
|
timeout: timeouts.bodyTimeout
|
|
594
639
|
});
|
|
595
640
|
}
|
|
596
|
-
if (error
|
|
597
|
-
throw new TimeoutError(req, {
|
|
641
|
+
if (isTotalRequestTimeoutError(error, timeoutReason, timeouts.totalTimeout)) {
|
|
642
|
+
throw timeoutError ?? new TimeoutError(req, {
|
|
598
643
|
phase: 'request',
|
|
599
644
|
timeout: timeouts.totalTimeout
|
|
600
645
|
});
|
|
@@ -611,6 +656,9 @@ export class UndiciTransport {
|
|
|
611
656
|
throw new NetworkError(error.message, code, req);
|
|
612
657
|
}
|
|
613
658
|
finally {
|
|
659
|
+
if (typeof cancelOriginalAbortListener === 'function') {
|
|
660
|
+
cancelOriginalAbortListener();
|
|
661
|
+
}
|
|
614
662
|
if (timeoutId) {
|
|
615
663
|
clearTimeout(timeoutId);
|
|
616
664
|
}
|
|
@@ -620,6 +668,9 @@ export class UndiciTransport {
|
|
|
620
668
|
async dispatchFast(req, headers, currentUrl, timeouts, handleRedirectsManually, maxRedirects, followRedirects, uploadTotal) {
|
|
621
669
|
let timeoutController;
|
|
622
670
|
let timeoutId;
|
|
671
|
+
let timeoutReason;
|
|
672
|
+
let timeoutError;
|
|
673
|
+
let cancelOriginalAbortListener;
|
|
623
674
|
try {
|
|
624
675
|
let redirectCount = 0;
|
|
625
676
|
let currentMethod = req.method;
|
|
@@ -628,24 +679,32 @@ export class UndiciTransport {
|
|
|
628
679
|
let effectiveSignal = req.signal;
|
|
629
680
|
if (timeouts.totalTimeout) {
|
|
630
681
|
timeoutController = new AbortController();
|
|
682
|
+
timeoutError = new TimeoutError(req, {
|
|
683
|
+
phase: 'request',
|
|
684
|
+
timeout: timeouts.totalTimeout
|
|
685
|
+
});
|
|
631
686
|
if (req.signal) {
|
|
632
687
|
const originalSignal = req.signal;
|
|
633
688
|
effectiveSignal = timeoutController.signal;
|
|
634
689
|
const onOriginalAbort = () => {
|
|
635
|
-
|
|
690
|
+
timeoutReason = originalSignal.reason ?? new Error('Request aborted by external signal');
|
|
691
|
+
timeoutController.abort(timeoutReason);
|
|
636
692
|
};
|
|
637
693
|
if (originalSignal.aborted) {
|
|
638
|
-
|
|
694
|
+
timeoutReason = originalSignal.reason ?? new Error('Request aborted by external signal');
|
|
695
|
+
timeoutController.abort(timeoutReason);
|
|
639
696
|
}
|
|
640
697
|
else {
|
|
641
698
|
originalSignal.addEventListener('abort', onOriginalAbort, { once: true });
|
|
699
|
+
cancelOriginalAbortListener = () => originalSignal.removeEventListener('abort', onOriginalAbort);
|
|
642
700
|
}
|
|
643
701
|
}
|
|
644
702
|
else {
|
|
645
703
|
effectiveSignal = timeoutController.signal;
|
|
646
704
|
}
|
|
647
705
|
timeoutId = setTimeout(() => {
|
|
648
|
-
|
|
706
|
+
timeoutReason = timeoutError;
|
|
707
|
+
timeoutController.abort(timeoutReason);
|
|
649
708
|
}, timeouts.totalTimeout);
|
|
650
709
|
}
|
|
651
710
|
while (true) {
|
|
@@ -822,8 +881,8 @@ export class UndiciTransport {
|
|
|
822
881
|
timeout: timeouts.bodyTimeout
|
|
823
882
|
});
|
|
824
883
|
}
|
|
825
|
-
if (error
|
|
826
|
-
throw new TimeoutError(req, {
|
|
884
|
+
if (isTotalRequestTimeoutError(error, timeoutReason, timeouts.totalTimeout)) {
|
|
885
|
+
throw timeoutError ?? new TimeoutError(req, {
|
|
827
886
|
phase: 'request',
|
|
828
887
|
timeout: timeouts.totalTimeout
|
|
829
888
|
});
|
|
@@ -840,6 +899,9 @@ export class UndiciTransport {
|
|
|
840
899
|
throw new NetworkError(error.message, code, req);
|
|
841
900
|
}
|
|
842
901
|
finally {
|
|
902
|
+
if (typeof cancelOriginalAbortListener === 'function') {
|
|
903
|
+
cancelOriginalAbortListener();
|
|
904
|
+
}
|
|
843
905
|
if (timeoutId) {
|
|
844
906
|
clearTimeout(timeoutId);
|
|
845
907
|
}
|
|
@@ -58,13 +58,11 @@ self.onmessage = async (event) => {
|
|
|
58
58
|
};
|
|
59
59
|
`;
|
|
60
60
|
export class WorkerTransport {
|
|
61
|
-
options;
|
|
62
61
|
workers = [];
|
|
63
62
|
workerIndex = 0;
|
|
64
63
|
pendingRequests = new Map();
|
|
65
64
|
workerUrl;
|
|
66
65
|
constructor(options = {}) {
|
|
67
|
-
this.options = options;
|
|
68
66
|
const poolSize = options.poolSize ?? (typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : 4) ?? 4;
|
|
69
67
|
const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' });
|
|
70
68
|
this.workerUrl = URL.createObjectURL(blob);
|
|
@@ -166,7 +164,7 @@ export class WorkerTransport {
|
|
|
166
164
|
}
|
|
167
165
|
this.workers = [];
|
|
168
166
|
URL.revokeObjectURL(this.workerUrl);
|
|
169
|
-
for (const
|
|
167
|
+
for (const pending of this.pendingRequests.values()) {
|
|
170
168
|
pending.reject(new Error('Transport terminated'));
|
|
171
169
|
}
|
|
172
170
|
this.pendingRequests.clear();
|