hyperttp 0.1.7 → 0.1.9

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 (39) hide show
  1. package/dist/Hyperttp/Core/CacheManager.d.ts +13 -54
  2. package/dist/Hyperttp/Core/CacheManager.d.ts.map +1 -1
  3. package/dist/Hyperttp/Core/CacheManager.js +28 -50
  4. package/dist/Hyperttp/Core/CacheManager.js.map +1 -1
  5. package/dist/Hyperttp/Core/HttpClientImproved.d.ts +19 -249
  6. package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
  7. package/dist/Hyperttp/Core/HttpClientImproved.js +393 -173
  8. package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
  9. package/dist/Hyperttp/Core/QueueManager.d.ts.map +1 -1
  10. package/dist/Hyperttp/Core/QueueManager.js +0 -2
  11. package/dist/Hyperttp/Core/QueueManager.js.map +1 -1
  12. package/dist/Hyperttp/Core/RateLimiter.d.ts.map +1 -1
  13. package/dist/Hyperttp/Core/RateLimiter.js.map +1 -1
  14. package/dist/Hyperttp/Core/index.d.ts +7 -7
  15. package/dist/Hyperttp/Core/index.d.ts.map +1 -1
  16. package/dist/Hyperttp/Core/index.js +8 -8
  17. package/dist/Hyperttp/Core/index.js.map +1 -1
  18. package/dist/Hyperttp/Request.d.ts.map +1 -1
  19. package/dist/Hyperttp/Request.js +0 -1
  20. package/dist/Hyperttp/Request.js.map +1 -1
  21. package/dist/Hyperttp/UrlExtractor.d.ts.map +1 -1
  22. package/dist/Hyperttp/UrlExtractor.js +0 -1
  23. package/dist/Hyperttp/UrlExtractor.js.map +1 -1
  24. package/dist/Hyperttp/index.d.ts +4 -4
  25. package/dist/Hyperttp/index.d.ts.map +1 -1
  26. package/dist/Hyperttp/index.js +11 -11
  27. package/dist/Hyperttp/index.js.map +1 -1
  28. package/dist/Types/index.d.ts +256 -0
  29. package/dist/Types/index.d.ts.map +1 -1
  30. package/dist/Types/index.js +47 -0
  31. package/dist/Types/index.js.map +1 -1
  32. package/dist/Types/request.d.ts +1 -1
  33. package/dist/Types/request.d.ts.map +1 -1
  34. package/dist/Types/request.js.map +1 -1
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +8 -8
  38. package/dist/index.js.map +1 -1
  39. package/package.json +5 -5
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RateLimitError = exports.TimeoutError = exports.HttpClientError = void 0;
4
3
  const tough_cookie_1 = require("tough-cookie");
5
4
  const undici_1 = require("undici");
6
5
  const undici_2 = require("http-cookie-agent/undici");
@@ -10,53 +9,8 @@ const crypto_1 = require("crypto");
10
9
  const CacheManager_1 = require("./CacheManager");
11
10
  const QueueManager_1 = require("./QueueManager");
12
11
  const RateLimiter_1 = require("./RateLimiter");
12
+ const Types_1 = require("../../Types");
13
13
  let defaultClient = null;
14
- /**
15
- * Base error class for HTTP client operations.
16
- * Contains additional context about the failed request including status code, URL, and method.
17
- */
18
- class HttpClientError extends Error {
19
- statusCode;
20
- originalError;
21
- url;
22
- method;
23
- constructor(message, statusCode, originalError, url, method) {
24
- super(message);
25
- this.statusCode = statusCode;
26
- this.originalError = originalError;
27
- this.url = url;
28
- this.method = method;
29
- this.name = "HttpClientError";
30
- Object.setPrototypeOf(this, HttpClientError.prototype);
31
- }
32
- }
33
- exports.HttpClientError = HttpClientError;
34
- /**
35
- * Error thrown when an HTTP request exceeds the configured timeout duration.
36
- * Contains information about the URL and timeout value that caused the failure.
37
- */
38
- class TimeoutError extends HttpClientError {
39
- constructor(url, timeout) {
40
- super(`Request timeout after ${timeout}ms for ${url}`);
41
- this.name = "TimeoutError";
42
- Object.setPrototypeOf(this, TimeoutError.prototype);
43
- }
44
- }
45
- exports.TimeoutError = TimeoutError;
46
- /**
47
- * Error thrown when an HTTP request is rate limited by the server.
48
- * Contains information about the URL and optional retry-after duration.
49
- */
50
- class RateLimitError extends HttpClientError {
51
- retryAfter;
52
- constructor(url, retryAfter) {
53
- super(`Rate limited for ${url}${retryAfter ? `, retry after ${retryAfter}ms` : ""}`);
54
- this.retryAfter = retryAfter;
55
- this.name = "RateLimitError";
56
- Object.setPrototypeOf(this, RateLimitError.prototype);
57
- }
58
- }
59
- exports.RateLimitError = RateLimitError;
60
14
  /**
61
15
  * @ru
62
16
  * Улучшенный HTTP-клиент с кэшированием, ограничением скорости, логикой повторных попыток и расширенными функциями.
@@ -119,7 +73,10 @@ class HttpClientImproved {
119
73
  limiter;
120
74
  inflight = new Map();
121
75
  retryOptions;
122
- defaultHeaders = {};
76
+ baseHeaders = Object.freeze({
77
+ Accept: "application/json, text/plain, */*",
78
+ "User-Agent": "Hyperttp/0.1.0 Node.js",
79
+ });
123
80
  options;
124
81
  requestInterceptors = [];
125
82
  responseInterceptors = [];
@@ -136,7 +93,7 @@ class HttpClientImproved {
136
93
  validateStatus: options?.validateStatus ??
137
94
  ((status) => status >= 200 && status < 300),
138
95
  cacheMethods: options?.cacheMethods ?? ["GET", "HEAD"],
139
- maxMetricsSize: options?.maxMetricsSize ?? 10000,
96
+ maxMetricsSize: options?.maxMetricsSize ?? 1000,
140
97
  rateLimit: options?.rateLimit ?? { maxRequests: 100, windowMs: 60000 },
141
98
  userAgent: options?.userAgent ?? "Hyperttp/0.1.0 Node.js",
142
99
  logger: options?.logger ??
@@ -152,13 +109,22 @@ class HttpClientImproved {
152
109
  retryOptions: options?.retryOptions ?? {},
153
110
  maxResponseBytes: options?.maxResponseBytes ?? 1024 * 1024,
154
111
  verbose: false,
112
+ enableQueue: options?.enableQueue ?? false,
113
+ enableRateLimit: options?.enableRateLimit ?? false,
114
+ enableCache: options?.enableCache ?? true,
155
115
  };
156
- this.cache = new CacheManager_1.CacheManager({
157
- cacheTTL: this.options.cacheTTL,
158
- cacheMaxSize: this.options.cacheMaxSize,
159
- });
160
- this.queue = new QueueManager_1.QueueManager(this.options.maxConcurrent ?? 100);
161
- this.limiter = new RateLimiter_1.RateLimiter(this.options.rateLimit);
116
+ if (this.options.enableCache) {
117
+ this.cache = new CacheManager_1.CacheManager({
118
+ cacheTTL: this.options.cacheTTL,
119
+ cacheMaxSize: this.options.cacheMaxSize,
120
+ });
121
+ }
122
+ if (this.options.enableQueue) {
123
+ this.queue = new QueueManager_1.QueueManager(this.options.maxConcurrent ?? 500);
124
+ }
125
+ if (this.options.enableRateLimit) {
126
+ this.limiter = new RateLimiter_1.RateLimiter(this.options.rateLimit);
127
+ }
162
128
  this.retryOptions = {
163
129
  maxRetries: this.options.maxRetries,
164
130
  baseDelay: this.options.retryOptions?.baseDelay ?? 1000,
@@ -168,13 +134,11 @@ class HttpClientImproved {
168
134
  ],
169
135
  jitter: this.options.retryOptions?.jitter ?? true,
170
136
  };
171
- this.defaultHeaders = {
172
- Accept: "application/json, text/plain, */*",
173
- "User-Agent": this.options.userAgent ?? "Hyperttp/0.1.0 Node.js",
174
- };
175
137
  this.agent = new undici_1.Agent({
176
- connections: 256,
177
- pipelining: 1,
138
+ connections: 1000,
139
+ pipelining: 10,
140
+ keepAliveTimeout: 60000,
141
+ keepAliveMaxTimeout: 600000,
178
142
  interceptors: {
179
143
  Client: [(0, undici_2.cookie)({ jar: this.cookieJar })],
180
144
  },
@@ -305,31 +269,45 @@ class HttpClientImproved {
305
269
  * @param body - Async iterable of response chunks
306
270
  * @returns Complete response body as a Buffer
307
271
  */
308
- async readBodyWithLimit(body) {
309
- const chunks = [];
310
- const limit = this.options.maxResponseBytes;
311
- let total = 0;
312
- for await (const chunk of body) {
313
- const buf = Buffer.from(chunk);
314
- total += buf.length;
315
- if (limit && total > limit) {
316
- throw new HttpClientError(`Response too large (${total} bytes), limit is ${limit}`, 0);
272
+ async readBodyWithLimit(body, maxBytes = this.options.maxResponseBytes) {
273
+ if (!body)
274
+ return Buffer.alloc(0);
275
+ // максимально быстрый парсинг через arrayBuffer
276
+ if (body?.arrayBuffer) {
277
+ const arrayBuffer = await body.arrayBuffer();
278
+ return Buffer.from(arrayBuffer);
279
+ }
280
+ if (body[Symbol.asyncIterator]) {
281
+ const chunks = [];
282
+ let total = 0;
283
+ for await (const chunk of body) {
284
+ chunks.push(Buffer.from(chunk));
285
+ total += chunk.length;
286
+ if (total > maxBytes)
287
+ break;
317
288
  }
318
- chunks.push(buf);
289
+ return Buffer.concat(chunks);
319
290
  }
320
- return Buffer.concat(chunks, total);
291
+ if (Buffer.isBuffer(body))
292
+ return body.subarray(0, maxBytes);
293
+ if (typeof body === "string")
294
+ return Buffer.from(body);
295
+ throw new TypeError(`Unsupported body type: ${typeof body}`);
321
296
  }
322
297
  /**
323
298
  * Removes old metrics entries to prevent memory leaks.
324
299
  * Keeps only metrics from the last 24 hours.
325
300
  */
326
301
  trimMetrics() {
327
- if (this.requestMetrics.size > this.options.maxMetricsSize) {
328
- const cutoff = Date.now() - 24 * 60 * 60 * 1000;
329
- for (const [key, metrics] of this.requestMetrics) {
330
- if (metrics.endTime < cutoff) {
331
- this.requestMetrics.delete(key);
332
- }
302
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
303
+ for (const [key, metrics] of this.requestMetrics) {
304
+ if (metrics.endTime && metrics.endTime < cutoff) {
305
+ this.requestMetrics.delete(key);
306
+ }
307
+ }
308
+ for (const key of this.inflight.keys()) {
309
+ if (this.inflight.size > 1000) {
310
+ this.inflight.delete(key);
333
311
  }
334
312
  }
335
313
  }
@@ -344,11 +322,105 @@ class HttpClientImproved {
344
322
  * @param redirects - Number of redirects followed so far
345
323
  * @returns Promise resolving to the response data
346
324
  */
325
+ async sendOnce(method, url, headers, body, metrics, redirects = 0) {
326
+ let lastError;
327
+ try {
328
+ if (this.limiter && this.options.enableRateLimit) {
329
+ await this.limiter.wait();
330
+ }
331
+ const finalConfig = await this.applyRequestInterceptors({
332
+ url,
333
+ method,
334
+ headers,
335
+ body,
336
+ });
337
+ const controller = new AbortController();
338
+ const timeout = this.options.timeout;
339
+ const timer = setTimeout(() => controller.abort(), timeout);
340
+ try {
341
+ const res = await (0, undici_1.request)(finalConfig.url, {
342
+ method: finalConfig.method,
343
+ headers: finalConfig.headers,
344
+ body: finalConfig.body,
345
+ dispatcher: this.agent,
346
+ signal: controller.signal,
347
+ });
348
+ if (method === "HEAD") {
349
+ clearTimeout(timer);
350
+ return {
351
+ status: res.statusCode,
352
+ headers: res.headers,
353
+ body: Buffer.alloc(0),
354
+ url: finalConfig.url,
355
+ };
356
+ }
357
+ clearTimeout(timer);
358
+ const buf = await this.readBodyWithLimit(res.body);
359
+ let response = await this.applyResponseInterceptors({
360
+ status: res.statusCode,
361
+ headers: res.headers,
362
+ body: buf,
363
+ url: finalConfig.url,
364
+ });
365
+ if (!this.options.validateStatus(response.status)) {
366
+ throw new Types_1.HttpClientError(`Request failed with status ${response.status}`, response.status, undefined, finalConfig.url, finalConfig.method);
367
+ }
368
+ if (this.options.followRedirects &&
369
+ [301, 302, 303, 307, 308].includes(response.status) &&
370
+ redirects < (this.options.maxRedirects ?? 5)) {
371
+ const location = response.headers.location;
372
+ if (location) {
373
+ const nextUrl = this.resolveRedirect(location, finalConfig.url);
374
+ const redirectMethod = response.status === 303 ? "GET" : method;
375
+ const nextHeaders = { ...headers };
376
+ let nextBody = body;
377
+ if (redirectMethod === "GET") {
378
+ nextBody = undefined;
379
+ delete nextHeaders["content-type"];
380
+ delete nextHeaders["Content-Type"];
381
+ delete nextHeaders["content-length"];
382
+ delete nextHeaders["Content-Length"];
383
+ }
384
+ this.log("debug", `Redirecting to ${nextUrl}`, {
385
+ originalUrl: finalConfig.url,
386
+ status: response.status,
387
+ });
388
+ return this.sendOnce(redirectMethod, nextUrl, nextHeaders, nextBody, metrics, redirects + 1);
389
+ }
390
+ }
391
+ return response;
392
+ }
393
+ catch (timeoutErr) {
394
+ clearTimeout(timer);
395
+ if (timeoutErr?.name === "AbortError")
396
+ throw new Types_1.TimeoutError(url, timeout);
397
+ throw timeoutErr;
398
+ }
399
+ finally {
400
+ clearTimeout(timer);
401
+ }
402
+ }
403
+ catch (err) {
404
+ lastError = err;
405
+ this.log("error", `Request error ${method} ${url}: ${err?.message ?? String(err)}`, {
406
+ error: err,
407
+ });
408
+ metrics && (metrics.retries += 1);
409
+ }
410
+ if (lastError instanceof Types_1.HttpClientError)
411
+ throw lastError;
412
+ throw new Types_1.HttpClientError(`Request failed after 1 attempt`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
413
+ }
347
414
  async sendWithRetry(method, url, headers, body, metrics, redirects = 0) {
348
415
  let lastError;
416
+ if (this.retryOptions.maxRetries === 0) {
417
+ return this.sendOnce(method, url, headers, body, metrics, redirects);
418
+ }
349
419
  for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
350
420
  try {
351
- await this.limiter.wait();
421
+ if (this.limiter && this.options.enableRateLimit) {
422
+ await this.limiter.wait();
423
+ }
352
424
  const finalConfig = await this.applyRequestInterceptors({
353
425
  url,
354
426
  method,
@@ -366,6 +438,15 @@ class HttpClientImproved {
366
438
  dispatcher: this.agent,
367
439
  signal: controller.signal,
368
440
  });
441
+ if (method === "HEAD") {
442
+ clearTimeout(timer);
443
+ return {
444
+ status: res.statusCode,
445
+ headers: res.headers,
446
+ body: Buffer.alloc(0),
447
+ url: finalConfig.url,
448
+ };
449
+ }
369
450
  clearTimeout(timer);
370
451
  const buf = await this.readBodyWithLimit(res.body);
371
452
  let response = await this.applyResponseInterceptors({
@@ -375,7 +456,7 @@ class HttpClientImproved {
375
456
  url: finalConfig.url,
376
457
  });
377
458
  if (!this.options.validateStatus(response.status)) {
378
- throw new HttpClientError(`Request failed with status ${response.status}`, response.status, undefined, finalConfig.url, finalConfig.method);
459
+ throw new Types_1.HttpClientError(`Request failed with status ${response.status}`, response.status, undefined, finalConfig.url, finalConfig.method);
379
460
  }
380
461
  if (this.options.followRedirects &&
381
462
  [301, 302, 303, 307, 308].includes(response.status) &&
@@ -410,7 +491,7 @@ class HttpClientImproved {
410
491
  await this.sleep(ra);
411
492
  continue;
412
493
  }
413
- throw new RateLimitError(finalConfig.url, ra);
494
+ throw new Types_1.RateLimitError(finalConfig.url, ra);
414
495
  }
415
496
  }
416
497
  this.log("warn", `Retrying ${method} ${finalConfig.url} due to status ${response.status}`, {
@@ -427,7 +508,7 @@ class HttpClientImproved {
427
508
  catch (timeoutErr) {
428
509
  clearTimeout(timer);
429
510
  if (timeoutErr?.name === "AbortError")
430
- throw new TimeoutError(url, timeout);
511
+ throw new Types_1.TimeoutError(url, timeout);
431
512
  throw timeoutErr;
432
513
  }
433
514
  finally {
@@ -447,9 +528,9 @@ class HttpClientImproved {
447
528
  }
448
529
  }
449
530
  }
450
- if (lastError instanceof HttpClientError)
531
+ if (lastError instanceof Types_1.HttpClientError)
451
532
  throw lastError;
452
- throw new HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
533
+ throw new Types_1.HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
453
534
  }
454
535
  /**
455
536
  * Parses the Content-Type header to extract MIME type and character encoding.
@@ -494,43 +575,33 @@ class HttpClientImproved {
494
575
  * @returns Parsed response data
495
576
  */
496
577
  async parseResponse(res, responseType) {
497
- const { type, charset } = this.parseContentType(res.headers["content-type"]);
498
- const text = res.body.toString(charset);
578
+ const { type } = this.parseContentType(res.headers["content-type"]);
499
579
  const finalType = responseType ?? (type.includes("application/json") ? "json" : "text");
500
580
  try {
501
581
  switch (finalType) {
502
- case "json": {
503
- try {
504
- return JSON.parse(text);
505
- }
506
- catch {
507
- try {
508
- return new fast_xml_parser_1.XMLParser({ ignoreAttributes: false }).parse(text);
509
- }
510
- catch {
511
- return { data: text };
512
- }
513
- }
514
- }
582
+ case "json":
583
+ return JSON.parse(res.body.toString("utf8"));
515
584
  case "xml": {
516
585
  try {
517
- const jsonData = JSON.parse(text);
586
+ const jsonData = JSON.parse(res.body.toString("utf8"));
518
587
  return new fast_xml_parser_1.XMLBuilder({ format: true }).build({ root: jsonData });
519
588
  }
520
589
  catch {
521
- return text;
590
+ return res.body.toString("utf8");
522
591
  }
523
592
  }
524
593
  case "text":
525
- return text;
594
+ return res.body.toString("utf8");
526
595
  case "buffer":
527
596
  return res.body;
597
+ case "stream":
598
+ throw new Error("Stream mode requires raw response. Use stream() method.");
528
599
  default:
529
- return text;
600
+ return res.body.toString("utf8");
530
601
  }
531
602
  }
532
603
  catch (err) {
533
- throw new HttpClientError(`Parsing failed: ${err?.message ?? String(err)}`, res.status);
604
+ throw new Types_1.HttpClientError(`Parsing failed: ${err?.message ?? String(err)}`, res.status);
534
605
  }
535
606
  }
536
607
  /**
@@ -544,10 +615,7 @@ class HttpClientImproved {
544
615
  async requestInternalWithoutCache(method, req, responseType) {
545
616
  const url = req.getURL();
546
617
  const rawBody = req.getBodyData();
547
- const headers = {
548
- ...this.defaultHeaders,
549
- ...req.getHeaders(),
550
- };
618
+ const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
551
619
  const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
552
620
  let body;
553
621
  const contentType = headers["content-type"] || headers["Content-Type"] || "";
@@ -578,19 +646,35 @@ class HttpClientImproved {
578
646
  url,
579
647
  method,
580
648
  };
581
- const result = await this.queue.enqueue(async () => {
582
- const res = await this.sendWithRetry(method, url, headers, body, metrics);
583
- metrics.statusCode = res.status;
584
- metrics.bytesReceived = res.body.length;
585
- metrics.bytesSent = body instanceof Buffer
586
- ? body.length
587
- : Buffer.byteLength(body || "");
588
- if (method === "HEAD") {
589
- return { status: res.status, headers: res.headers };
590
- }
591
- const parsed = await this.parseResponse(res, responseType);
592
- return parsed;
593
- });
649
+ const result = await (this.queue && this.options.enableQueue
650
+ ? this.queue.enqueue(async () => {
651
+ const res = await this.sendWithRetry(method, url, headers, body, metrics);
652
+ metrics.statusCode = res.status;
653
+ metrics.bytesReceived = res.body.length;
654
+ metrics.bytesSent =
655
+ body instanceof Buffer
656
+ ? body.length
657
+ : Buffer.byteLength(body || "");
658
+ if (method === "HEAD") {
659
+ return { status: res.status, headers: res.headers };
660
+ }
661
+ const parsed = await this.parseResponse(res, responseType);
662
+ return parsed;
663
+ })
664
+ : (async () => {
665
+ const res = await this.sendWithRetry(method, url, headers, body, metrics);
666
+ metrics.statusCode = res.status;
667
+ metrics.bytesReceived = res.body.length;
668
+ metrics.bytesSent =
669
+ body instanceof Buffer
670
+ ? body.length
671
+ : Buffer.byteLength(body || "");
672
+ if (method === "HEAD") {
673
+ return { status: res.status, headers: res.headers };
674
+ }
675
+ const parsed = await this.parseResponse(res, responseType);
676
+ return parsed;
677
+ })());
594
678
  metrics.endTime = Date.now();
595
679
  metrics.duration = metrics.endTime - metrics.startTime;
596
680
  this.requestMetrics.set(url, metrics);
@@ -606,16 +690,50 @@ class HttpClientImproved {
606
690
  * @param responseType - Expected response type
607
691
  * @returns Promise resolving to the response data
608
692
  */
693
+ async fastRequest(method, req, responseType) {
694
+ const url = req.getURL();
695
+ const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
696
+ const body = req.getBodyData();
697
+ let finalBody;
698
+ if (body == null) {
699
+ finalBody = undefined;
700
+ }
701
+ else if (typeof body === "string" || Buffer.isBuffer(body)) {
702
+ finalBody = body;
703
+ }
704
+ else {
705
+ finalBody = JSON.stringify(body);
706
+ }
707
+ const res = await (0, undici_1.request)(url, {
708
+ method,
709
+ headers,
710
+ body: finalBody,
711
+ dispatcher: this.agent,
712
+ });
713
+ if (responseType === "json") {
714
+ return res.body.json();
715
+ }
716
+ if (responseType === "text") {
717
+ const text = await res.body.text();
718
+ return text;
719
+ }
720
+ return Buffer.from(await res.body.arrayBuffer());
721
+ }
609
722
  async requestInternal(method, req, useCache = true, responseType) {
610
- if (this.options.cacheTTL === 0) {
723
+ if (!this.cache &&
724
+ !this.options.enableQueue &&
725
+ !this.options.enableRateLimit &&
726
+ this.retryOptions.maxRetries === 0 &&
727
+ this.requestInterceptors.length === 0 &&
728
+ this.responseInterceptors.length === 0) {
729
+ return this.fastRequest(method, req, responseType);
730
+ }
731
+ if (this.options.cacheTTL === 0 || this.options.enableCache === false) {
611
732
  return this.requestInternalWithoutCache(method, req, responseType);
612
733
  }
613
734
  const url = req.getURL();
614
735
  const rawBody = req.getBodyData();
615
- const headers = {
616
- ...this.defaultHeaders,
617
- ...req.getHeaders(),
618
- };
736
+ const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
619
737
  const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
620
738
  let body;
621
739
  const contentType = headers["content-type"] || headers["Content-Type"] || "";
@@ -635,18 +753,21 @@ class HttpClientImproved {
635
753
  headers["Content-Type"] = "application/json; charset=utf-8";
636
754
  }
637
755
  }
638
- const bodyHash = this.hashBody(body);
639
- const cacheKey = (0, crypto_1.createHash)("sha1")
640
- .update(method + url + bodyHash + responseType)
641
- .digest("hex");
642
- if (this.options.cacheMethods.includes(method) && useCache) {
643
- const cached = this.cache.get(cacheKey);
644
- if (cached) {
756
+ let cacheKey;
757
+ if (this.cache && useCache) {
758
+ const bodyHash = this.hashBody(body);
759
+ cacheKey = (0, crypto_1.createHash)("sha1")
760
+ .update(method + url + bodyHash + responseType)
761
+ .digest("hex");
762
+ }
763
+ if (cacheKey && this.options.cacheMethods.includes(method) && useCache && this.cache) {
764
+ const cached = await this.cache.get(cacheKey);
765
+ if (cached != null) {
645
766
  this.log("debug", `Cache hit for ${url}`);
646
767
  return cached;
647
768
  }
648
769
  }
649
- if (this.inflight.has(cacheKey)) {
770
+ if (cacheKey && this.inflight.has(cacheKey)) {
650
771
  this.log("debug", `Deduplicating request for ${url}`);
651
772
  return this.inflight.get(cacheKey);
652
773
  }
@@ -661,31 +782,55 @@ class HttpClientImproved {
661
782
  cached: false,
662
783
  url,
663
784
  method,
664
- bodyHash,
665
785
  };
666
786
  try {
667
787
  this.log("debug", `Starting request: ${method} ${url}`);
668
- const result = await this.queue.enqueue(async () => {
669
- const res = await this.sendWithRetry(method, url, headers, body, metrics);
670
- metrics.statusCode = res.status;
671
- metrics.bytesReceived = res.body.length;
672
- metrics.bytesSent =
673
- body instanceof Buffer
674
- ? body.length
675
- : Buffer.byteLength(body || "");
676
- if (method === "HEAD") {
677
- return { status: res.status, headers: res.headers };
678
- }
679
- const parsed = await this.parseResponse(res, responseType);
680
- if (this.options.cacheMethods.includes(method) && useCache) {
681
- this.cache.set(cacheKey, parsed);
682
- metrics.cached = true;
683
- }
684
- return parsed;
685
- });
788
+ const result = await (this.queue && this.options.enableQueue
789
+ ? this.queue.enqueue(async () => {
790
+ const res = await this.sendWithRetry(method, url, headers, body, metrics);
791
+ metrics.statusCode = res.status;
792
+ metrics.bytesReceived = res.body.length;
793
+ metrics.bytesSent =
794
+ body instanceof Buffer
795
+ ? body.length
796
+ : Buffer.byteLength(body || "");
797
+ if (method === "HEAD") {
798
+ return { status: res.status, headers: res.headers };
799
+ }
800
+ const parsed = await this.parseResponse(res, responseType);
801
+ if (cacheKey &&
802
+ this.options.cacheMethods.includes(method) &&
803
+ useCache &&
804
+ this.cache) {
805
+ this.cache.set(cacheKey, parsed);
806
+ metrics.cached = true;
807
+ }
808
+ return parsed;
809
+ })
810
+ : (async () => {
811
+ const res = await this.sendWithRetry(method, url, headers, body, metrics);
812
+ metrics.statusCode = res.status;
813
+ metrics.bytesReceived = res.body.length;
814
+ metrics.bytesSent =
815
+ body instanceof Buffer
816
+ ? body.length
817
+ : Buffer.byteLength(body || "");
818
+ if (method === "HEAD") {
819
+ return { status: res.status, headers: res.headers };
820
+ }
821
+ const parsed = await this.parseResponse(res, responseType);
822
+ if (cacheKey &&
823
+ this.options.cacheMethods.includes(method) &&
824
+ useCache &&
825
+ this.cache) {
826
+ await this.cache.set(cacheKey, parsed);
827
+ metrics.cached = true;
828
+ }
829
+ return parsed;
830
+ })());
686
831
  metrics.endTime = Date.now();
687
832
  metrics.duration = metrics.endTime - metrics.startTime;
688
- this.requestMetrics.set(cacheKey, metrics);
833
+ this.requestMetrics.set(cacheKey || url, metrics);
689
834
  this.trimMetrics();
690
835
  this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
691
836
  return result;
@@ -693,15 +838,17 @@ class HttpClientImproved {
693
838
  catch (error) {
694
839
  metrics.endTime = Date.now();
695
840
  metrics.duration = metrics.endTime - metrics.startTime;
696
- this.requestMetrics.set(cacheKey, metrics);
841
+ this.requestMetrics.set(cacheKey || url, metrics);
697
842
  this.trimMetrics();
698
843
  throw error;
699
844
  }
700
845
  finally {
701
- this.inflight.delete(cacheKey);
846
+ if (cacheKey)
847
+ this.inflight.delete(cacheKey);
702
848
  }
703
849
  })();
704
- this.inflight.set(cacheKey, promise);
850
+ if (cacheKey)
851
+ this.inflight.set(cacheKey, promise);
705
852
  return promise;
706
853
  }
707
854
  /**
@@ -758,16 +905,60 @@ class HttpClientImproved {
758
905
  */
759
906
  put(req, body, responseType = "json") {
760
907
  if (typeof req === "string") {
761
- const client = defaultClient ?? (defaultClient = new HttpClientImproved());
762
908
  const simpleReq = {
763
909
  getURL: () => req,
764
910
  getBodyData: () => body,
765
911
  getHeaders: () => ({ "Content-Type": "application/json" }),
766
912
  };
767
- return client.put(simpleReq, responseType);
913
+ return this.requestInternal("PUT", simpleReq, false, responseType);
768
914
  }
769
915
  return this.requestInternal("PUT", req, false, responseType);
770
916
  }
917
+ /**
918
+ * @ru Получает потоковый ответ (для SSE, больших файлов).
919
+ * @en Gets streaming response (for SSE, large files).
920
+ */
921
+ stream(req) {
922
+ if (typeof req === "string") {
923
+ const simpleReq = {
924
+ getURL: () => req,
925
+ getBodyData: () => undefined,
926
+ getHeaders: () => ({}),
927
+ };
928
+ return this.stream(simpleReq);
929
+ }
930
+ return (this.queue && this.options.enableQueue
931
+ ? this.queue.enqueue(async function () {
932
+ const url = req.getURL();
933
+ const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
934
+ const response = await (0, undici_1.request)(url, {
935
+ method: "GET",
936
+ headers,
937
+ dispatcher: this.agent,
938
+ });
939
+ return {
940
+ status: response.statusCode,
941
+ headers: response.headers,
942
+ body: response.body,
943
+ url,
944
+ };
945
+ }.bind(this))
946
+ : async function () {
947
+ const url = req.getURL();
948
+ const headers = Object.assign({}, this.baseHeaders, req.getHeaders());
949
+ const response = await (0, undici_1.request)(url, {
950
+ method: "GET",
951
+ headers,
952
+ dispatcher: this.agent,
953
+ });
954
+ return {
955
+ status: response.statusCode,
956
+ headers: response.headers,
957
+ body: response.body,
958
+ url,
959
+ };
960
+ }.bind(this)());
961
+ }
771
962
  /**
772
963
  * Makes an HTTP DELETE request.
773
964
  * Supports both RequestInterface objects and direct URL strings.
@@ -795,7 +986,15 @@ class HttpClientImproved {
795
986
  * @param responseType - Expected response type (default: "json")
796
987
  * @returns Promise resolving to the response data
797
988
  */
798
- patch(req, responseType = "json") {
989
+ patch(req, body, responseType = "json") {
990
+ if (typeof req === "string") {
991
+ const simpleReq = {
992
+ getURL: () => req,
993
+ getBodyData: () => body,
994
+ getHeaders: () => ({ "Content-Type": "application/json" }),
995
+ };
996
+ return this.requestInternal("PATCH", simpleReq, false, responseType);
997
+ }
799
998
  return this.requestInternal("PATCH", req, false, responseType);
800
999
  }
801
1000
  /**
@@ -807,13 +1006,12 @@ class HttpClientImproved {
807
1006
  */
808
1007
  async head(req) {
809
1008
  if (typeof req === "string") {
810
- const client = defaultClient ?? (defaultClient = new HttpClientImproved());
811
1009
  const simpleReq = {
812
1010
  getURL: () => req,
813
1011
  getBodyData: () => undefined,
814
1012
  getHeaders: () => ({}),
815
1013
  };
816
- return client.head(simpleReq);
1014
+ return this.requestInternal("HEAD", simpleReq, false);
817
1015
  }
818
1016
  return this.requestInternal("HEAD", req, false);
819
1017
  }
@@ -821,9 +1019,11 @@ class HttpClientImproved {
821
1019
  * Clears the internal cache of the HTTP client.
822
1020
  * Removes all cached responses and resets the cache state.
823
1021
  */
824
- clearCache() {
825
- this.cache.clear();
826
- this.log("info", "Cache cleared");
1022
+ async clearCache() {
1023
+ if (this.cache) {
1024
+ await this.cache.clear();
1025
+ this.log("info", "Cache cleared");
1026
+ }
827
1027
  }
828
1028
  /**
829
1029
  * Clears all collected request metrics.
@@ -864,11 +1064,17 @@ class HttpClientImproved {
864
1064
  */
865
1065
  getStats() {
866
1066
  return {
867
- cacheSize: this.cache.size,
1067
+ cacheSize: this.cache?.size ?? 0,
868
1068
  inflightRequests: this.inflight.size,
869
- queuedRequests: this.queue.queuedCount,
870
- activeRequests: this.queue.activeCount,
871
- currentRateLimit: this.limiter.currentCount,
1069
+ queuedRequests: this.queue && this.options.enableQueue
1070
+ ? (this.queue.queuedCount ?? 0)
1071
+ : 0,
1072
+ activeRequests: this.queue && this.options.enableQueue
1073
+ ? (this.queue.activeCount ?? 0)
1074
+ : 0,
1075
+ currentRateLimit: this.limiter && this.options.enableRateLimit
1076
+ ? (this.limiter.currentCount ?? 0)
1077
+ : 0,
872
1078
  metricsSize: this.requestMetrics.size,
873
1079
  };
874
1080
  }
@@ -951,6 +1157,14 @@ class RequestBuilder {
951
1157
  this._method = "POST";
952
1158
  return this;
953
1159
  }
1160
+ /**
1161
+ * @ru Устанавливает потоковый режим ответа.
1162
+ * @en Sets streaming response mode.
1163
+ */
1164
+ stream() {
1165
+ this._responseType = "stream";
1166
+ return this;
1167
+ }
954
1168
  /**
955
1169
  * Sets the HTTP method to PUT.
956
1170
  * @returns The builder instance for chaining
@@ -994,7 +1208,7 @@ class RequestBuilder {
994
1208
  */
995
1209
  jsonBody(body) {
996
1210
  this._body = body;
997
- this._headers['Content-Type'] = 'application/json; charset=utf-8';
1211
+ this._headers["Content-Type"] = "application/json; charset=utf-8";
998
1212
  return this;
999
1213
  }
1000
1214
  /**
@@ -1010,16 +1224,22 @@ class RequestBuilder {
1010
1224
  };
1011
1225
  switch (this._method) {
1012
1226
  case "GET":
1227
+ if (this._responseType === "stream") {
1228
+ return client.stream(req);
1229
+ }
1013
1230
  return client.get(req, this._responseType);
1014
1231
  case "POST":
1015
- return client.post(req, this._responseType);
1232
+ return client.post(req, this._body, this._responseType);
1016
1233
  case "PUT":
1017
- return client.put(req, this._responseType);
1234
+ return client.put(req, this._body, this._responseType);
1018
1235
  case "DELETE":
1019
1236
  return client.delete(req, this._responseType);
1020
1237
  case "PATCH":
1021
- return client.patch(req, this._responseType);
1238
+ return client.patch(req, this._body, this._responseType);
1022
1239
  default:
1240
+ if (this._responseType === "stream") {
1241
+ return client.stream(req);
1242
+ }
1023
1243
  return client.get(req, this._responseType);
1024
1244
  }
1025
1245
  }