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.
Files changed (101) hide show
  1. package/README.md +5 -18
  2. package/dist/browser/core/client.d.ts +14 -8
  3. package/dist/browser/core/client.js +199 -17
  4. package/dist/browser/core/errors.d.ts +15 -1
  5. package/dist/browser/core/errors.js +140 -9
  6. package/dist/browser/core/request.d.ts +5 -0
  7. package/dist/browser/core/request.js +33 -2
  8. package/dist/browser/core-runtime/plugin-manifest.d.ts +24 -0
  9. package/dist/browser/core-runtime/plugin-manifest.js +159 -0
  10. package/dist/browser/core-runtime/request-context.d.ts +13 -0
  11. package/dist/browser/core-runtime/request-context.js +24 -0
  12. package/dist/browser/core-runtime/typed-events.d.ts +89 -0
  13. package/dist/browser/core-runtime/typed-events.js +34 -0
  14. package/dist/browser/index.iife.min.js +79 -79
  15. package/dist/browser/index.min.js +79 -79
  16. package/dist/browser/index.mini.iife.js +913 -97
  17. package/dist/browser/index.mini.iife.min.js +46 -46
  18. package/dist/browser/index.mini.min.js +46 -46
  19. package/dist/browser/index.mini.umd.js +913 -97
  20. package/dist/browser/index.mini.umd.min.js +46 -46
  21. package/dist/browser/index.umd.min.js +79 -79
  22. package/dist/browser/plugins/auth/aws-sigv4.d.ts +1 -0
  23. package/dist/browser/plugins/auth/aws-sigv4.js +19 -2
  24. package/dist/browser/plugins/retry.js +29 -1
  25. package/dist/browser/presets/aws.d.ts +1 -0
  26. package/dist/browser/presets/aws.js +62 -1
  27. package/dist/browser/runner/request-runner.d.ts +15 -5
  28. package/dist/browser/runner/request-runner.js +164 -30
  29. package/dist/browser/scrape/parser/nodes/html.d.ts +6 -0
  30. package/dist/browser/scrape/parser/nodes/html.js +70 -18
  31. package/dist/browser/scrape/parser/nodes/node.d.ts +1 -0
  32. package/dist/browser/scrape/parser/nodes/node.js +5 -0
  33. package/dist/browser/scrape/spider.d.ts +1 -0
  34. package/dist/browser/scrape/spider.js +39 -26
  35. package/dist/browser/seo/analyzer.d.ts +1 -1
  36. package/dist/browser/seo/analyzer.js +73 -42
  37. package/dist/browser/seo/index.d.ts +1 -1
  38. package/dist/browser/seo/rules/types.d.ts +2 -0
  39. package/dist/browser/seo/seo-spider.d.ts +2 -3
  40. package/dist/browser/seo/seo-spider.js +26 -202
  41. package/dist/browser/seo/types.d.ts +4 -0
  42. package/dist/browser/seo/validators/sitemap.js +9 -2
  43. package/dist/browser/transport/fetch.js +38 -5
  44. package/dist/browser/transport/undici.js +73 -11
  45. package/dist/browser/transport/worker.d.ts +0 -1
  46. package/dist/browser/transport/worker.js +1 -3
  47. package/dist/browser/types/index.d.ts +24 -0
  48. package/dist/cli/commands/mcp.js +5 -3
  49. package/dist/core/client.d.ts +14 -8
  50. package/dist/core/client.js +199 -17
  51. package/dist/core/errors.d.ts +15 -1
  52. package/dist/core/errors.js +140 -9
  53. package/dist/core/request.d.ts +5 -0
  54. package/dist/core/request.js +33 -2
  55. package/dist/core-runtime/plugin-manifest.d.ts +24 -0
  56. package/dist/core-runtime/plugin-manifest.js +159 -0
  57. package/dist/core-runtime/request-context.d.ts +13 -0
  58. package/dist/core-runtime/request-context.js +24 -0
  59. package/dist/core-runtime/typed-events.d.ts +89 -0
  60. package/dist/core-runtime/typed-events.js +34 -0
  61. package/dist/index.d.ts +2 -1
  62. package/dist/index.js +2 -1
  63. package/dist/mcp/cli.js +10 -8
  64. package/dist/mcp/profiles.d.ts +1 -1
  65. package/dist/mcp/profiles.js +31 -6
  66. package/dist/mcp/tools/categories.js +0 -1
  67. package/dist/mcp/tools/seo.js +320 -4
  68. package/dist/plugins/auth/aws-sigv4.d.ts +1 -0
  69. package/dist/plugins/auth/aws-sigv4.js +19 -2
  70. package/dist/plugins/retry.js +29 -1
  71. package/dist/presets/aws.d.ts +1 -0
  72. package/dist/presets/aws.js +62 -1
  73. package/dist/recker.d.ts +3 -0
  74. package/dist/recker.js +5 -0
  75. package/dist/runner/request-runner.d.ts +15 -5
  76. package/dist/runner/request-runner.js +164 -30
  77. package/dist/scrape/parser/nodes/html.d.ts +6 -0
  78. package/dist/scrape/parser/nodes/html.js +70 -18
  79. package/dist/scrape/parser/nodes/node.d.ts +1 -0
  80. package/dist/scrape/parser/nodes/node.js +5 -0
  81. package/dist/scrape/spider.d.ts +1 -0
  82. package/dist/scrape/spider.js +39 -26
  83. package/dist/search/google.d.ts +67 -0
  84. package/dist/search/google.js +480 -0
  85. package/dist/search/index.d.ts +3 -0
  86. package/dist/search/index.js +1 -0
  87. package/dist/seo/analyzer.d.ts +1 -1
  88. package/dist/seo/analyzer.js +73 -42
  89. package/dist/seo/index.d.ts +1 -1
  90. package/dist/seo/rules/types.d.ts +2 -0
  91. package/dist/seo/seo-spider.d.ts +2 -3
  92. package/dist/seo/seo-spider.js +26 -202
  93. package/dist/seo/types.d.ts +4 -0
  94. package/dist/seo/validators/sitemap.js +9 -2
  95. package/dist/transport/fetch.js +38 -5
  96. package/dist/transport/undici.js +73 -11
  97. package/dist/transport/worker.d.ts +0 -1
  98. package/dist/transport/worker.js +1 -3
  99. package/dist/types/index.d.ts +24 -0
  100. package/dist/version.js +1 -1
  101. 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: Object.fromEntries([...res.headers.entries()]),
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) && p.depth > 0)
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.reduce((sum, s) => sum + 1, 0),
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: Object.fromEntries(fetchResponse.headers.entries()),
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(() => abortController.abort(), timeoutMs);
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' && abortController) {
152
- const timeoutError = new Error(`Request timeout after ${timeoutMs}ms`);
153
- timeoutError.name = 'TimeoutError';
154
- throw timeoutError;
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((message) => {
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
- timeoutController.abort();
388
+ timeoutReason = originalSignal.reason ?? new Error('Request aborted by external signal');
389
+ timeoutController.abort(timeoutReason);
348
390
  };
349
391
  if (originalSignal.aborted) {
350
- timeoutController.abort();
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
- timeoutController.abort();
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.name === 'AbortError' || error.code === 'ABORT_ERR') {
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
- timeoutController.abort();
690
+ timeoutReason = originalSignal.reason ?? new Error('Request aborted by external signal');
691
+ timeoutController.abort(timeoutReason);
636
692
  };
637
693
  if (originalSignal.aborted) {
638
- timeoutController.abort();
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
- timeoutController.abort();
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.name === 'AbortError' || error.code === 'ABORT_ERR') {
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
  }
@@ -3,7 +3,6 @@ export interface WorkerTransportOptions {
3
3
  poolSize?: number;
4
4
  }
5
5
  export declare class WorkerTransport implements Transport {
6
- private options;
7
6
  private workers;
8
7
  private workerIndex;
9
8
  private pendingRequests;
@@ -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 [id, pending] of this.pendingRequests) {
167
+ for (const pending of this.pendingRequests.values()) {
170
168
  pending.reject(new Error('Transport terminated'));
171
169
  }
172
170
  this.pendingRequests.clear();