hyperttp 0.2.4 → 0.2.6

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 (110) hide show
  1. package/README.md +129 -16
  2. package/dist/Hyperttp/Core/CacheManager.d.ts +63 -40
  3. package/dist/Hyperttp/Core/CacheManager.d.ts.map +1 -1
  4. package/dist/Hyperttp/Core/CacheManager.js +64 -62
  5. package/dist/Hyperttp/Core/CacheManager.js.map +1 -1
  6. package/dist/Hyperttp/Core/HttpClientImproved.d.ts +55 -33
  7. package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
  8. package/dist/Hyperttp/Core/HttpClientImproved.js +315 -211
  9. package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
  10. package/dist/Hyperttp/Core/InterceptorManager.d.ts +11 -11
  11. package/dist/Hyperttp/Core/InterceptorManager.d.ts.map +1 -1
  12. package/dist/Hyperttp/Core/InterceptorManager.js +10 -10
  13. package/dist/Hyperttp/Core/InterceptorManager.js.map +1 -1
  14. package/dist/Hyperttp/Core/MetricsManager.d.ts +33 -42
  15. package/dist/Hyperttp/Core/MetricsManager.d.ts.map +1 -1
  16. package/dist/Hyperttp/Core/MetricsManager.js +165 -59
  17. package/dist/Hyperttp/Core/MetricsManager.js.map +1 -1
  18. package/dist/Hyperttp/Core/QueueManager.d.ts +6 -4
  19. package/dist/Hyperttp/Core/QueueManager.d.ts.map +1 -1
  20. package/dist/Hyperttp/Core/QueueManager.js +42 -34
  21. package/dist/Hyperttp/Core/QueueManager.js.map +1 -1
  22. package/dist/Hyperttp/Core/RateLimiter.d.ts +29 -36
  23. package/dist/Hyperttp/Core/RateLimiter.d.ts.map +1 -1
  24. package/dist/Hyperttp/Core/RateLimiter.js +100 -36
  25. package/dist/Hyperttp/Core/RateLimiter.js.map +1 -1
  26. package/dist/Hyperttp/Core/RequestBuilder.d.ts +4 -2
  27. package/dist/Hyperttp/Core/RequestBuilder.d.ts.map +1 -1
  28. package/dist/Hyperttp/Core/RequestBuilder.js +10 -3
  29. package/dist/Hyperttp/Core/RequestBuilder.js.map +1 -1
  30. package/dist/Hyperttp/Core/RequestExecutor.d.ts +7 -34
  31. package/dist/Hyperttp/Core/RequestExecutor.d.ts.map +1 -1
  32. package/dist/Hyperttp/Core/RequestExecutor.js +121 -114
  33. package/dist/Hyperttp/Core/RequestExecutor.js.map +1 -1
  34. package/dist/Hyperttp/Core/RequestProfiler.d.ts +10 -0
  35. package/dist/Hyperttp/Core/RequestProfiler.d.ts.map +1 -0
  36. package/dist/Hyperttp/Core/RequestProfiler.js +55 -0
  37. package/dist/Hyperttp/Core/RequestProfiler.js.map +1 -0
  38. package/dist/Hyperttp/Core/ResponseConverter.d.ts +23 -0
  39. package/dist/Hyperttp/Core/ResponseConverter.d.ts.map +1 -0
  40. package/dist/Hyperttp/Core/ResponseConverter.js +369 -0
  41. package/dist/Hyperttp/Core/ResponseConverter.js.map +1 -0
  42. package/dist/Hyperttp/Core/index.d.ts +8 -10
  43. package/dist/Hyperttp/Core/index.d.ts.map +1 -1
  44. package/dist/Hyperttp/Core/index.js +28 -15
  45. package/dist/Hyperttp/Core/index.js.map +1 -1
  46. package/dist/Hyperttp/UrlExtractor.d.ts +1 -1
  47. package/dist/Hyperttp/UrlExtractor.d.ts.map +1 -1
  48. package/dist/Hyperttp/index.d.ts +1 -3
  49. package/dist/Hyperttp/index.d.ts.map +1 -1
  50. package/dist/Hyperttp/index.js +8 -8
  51. package/dist/Hyperttp/index.js.map +1 -1
  52. package/dist/Types/cache.d.ts +10 -0
  53. package/dist/Types/cache.d.ts.map +1 -0
  54. package/dist/Types/cache.js +3 -0
  55. package/dist/Types/cache.js.map +1 -0
  56. package/dist/Types/errors.d.ts +15 -0
  57. package/dist/Types/errors.d.ts.map +1 -0
  58. package/dist/Types/errors.js +34 -0
  59. package/dist/Types/errors.js.map +1 -0
  60. package/dist/Types/http-client.d.ts +39 -0
  61. package/dist/Types/http-client.d.ts.map +1 -0
  62. package/dist/Types/http-client.js +3 -0
  63. package/dist/Types/http-client.js.map +1 -0
  64. package/dist/Types/http.d.ts +5 -0
  65. package/dist/Types/http.d.ts.map +1 -0
  66. package/dist/Types/http.js +3 -0
  67. package/dist/Types/http.js.map +1 -0
  68. package/dist/Types/index.d.ts +12 -127
  69. package/dist/Types/index.d.ts.map +1 -1
  70. package/dist/Types/index.js +12 -39
  71. package/dist/Types/index.js.map +1 -1
  72. package/dist/Types/interceptors.d.ts +13 -0
  73. package/dist/Types/interceptors.d.ts.map +1 -0
  74. package/dist/Types/interceptors.js +3 -0
  75. package/dist/Types/interceptors.js.map +1 -0
  76. package/dist/Types/metrics.d.ts +90 -0
  77. package/dist/Types/metrics.d.ts.map +1 -0
  78. package/dist/Types/metrics.js +3 -0
  79. package/dist/Types/metrics.js.map +1 -0
  80. package/dist/Types/options.d.ts +233 -0
  81. package/dist/Types/options.d.ts.map +1 -0
  82. package/dist/Types/options.js +3 -0
  83. package/dist/Types/options.js.map +1 -0
  84. package/dist/Types/queue.d.ts +8 -0
  85. package/dist/Types/queue.d.ts.map +1 -0
  86. package/dist/Types/queue.js +3 -0
  87. package/dist/Types/queue.js.map +1 -0
  88. package/dist/Types/request.d.ts +148 -9
  89. package/dist/Types/request.d.ts.map +1 -1
  90. package/dist/Types/response.d.ts +28 -0
  91. package/dist/Types/response.d.ts.map +1 -0
  92. package/dist/Types/response.js +3 -0
  93. package/dist/Types/response.js.map +1 -0
  94. package/dist/Types/stream.d.ts +39 -0
  95. package/dist/Types/stream.d.ts.map +1 -0
  96. package/dist/Types/stream.js +3 -0
  97. package/dist/Types/stream.js.map +1 -0
  98. package/dist/Types/url-extractor.d.ts +10 -0
  99. package/dist/Types/url-extractor.d.ts.map +1 -0
  100. package/dist/Types/url-extractor.js +3 -0
  101. package/dist/Types/url-extractor.js.map +1 -0
  102. package/dist/index.d.ts +1 -2
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +8 -3
  105. package/dist/index.js.map +1 -1
  106. package/package.json +7 -5
  107. package/dist/Hyperttp/Core/ResponseTransformer.d.ts +0 -35
  108. package/dist/Hyperttp/Core/ResponseTransformer.d.ts.map +0 -1
  109. package/dist/Hyperttp/Core/ResponseTransformer.js +0 -171
  110. package/dist/Hyperttp/Core/ResponseTransformer.js.map +0 -1
@@ -7,9 +7,10 @@ const RateLimiter_js_1 = require("./RateLimiter.js");
7
7
  const MetricsManager_js_1 = require("./MetricsManager.js");
8
8
  const RequestBuilder_js_1 = require("./RequestBuilder.js");
9
9
  const InterceptorManager_js_1 = require("./InterceptorManager.js");
10
- const ResponseTransformer_js_1 = require("./ResponseTransformer.js");
11
10
  const RequestExecutor_js_1 = require("./RequestExecutor.js");
12
- const index_js_1 = require("../../Types/index.js");
11
+ const ResponseConverter_js_1 = require("./ResponseConverter.js");
12
+ const errors_js_1 = require("../../Types/errors.js");
13
+ const isProd = process.env.NODE_ENV === "production";
13
14
  /**
14
15
  * @class HttpClientImproved
15
16
  * @en High-performance HTTP client with built-in caching, queuing, rate limiting, and metrics.
@@ -17,134 +18,90 @@ const index_js_1 = require("../../Types/index.js");
17
18
  */
18
19
  class HttpClientImproved {
19
20
  agent;
20
- options;
21
+ config;
21
22
  cache;
22
23
  queue;
23
24
  limiter;
24
25
  metricsManager;
25
26
  interceptors;
26
- transformer;
27
27
  executor;
28
- /**
29
- * @en Internal map to track active requests for deduplication and cancellation.
30
- * @ru Внутренняя карта для отслеживания активных запросов (дедупликация и отмена).
31
- */
28
+ converter;
32
29
  inflight = new Map();
33
30
  defaultHeaders = {};
34
- constructor(options) {
35
- this.options = this.applyDefaultOptions(options);
31
+ cacheEnabled;
32
+ queueEnabled;
33
+ limiterEnabled;
34
+ metricsEnabled;
35
+ verboseEnabled;
36
+ constructor(config) {
37
+ this.config = this.applyDefaulthcoptions(config);
38
+ this.cacheEnabled = !!this.config.cache?.enabled;
39
+ this.queueEnabled = !!this.config.queue?.enabled;
40
+ this.limiterEnabled = !!this.config.rateLimit?.enabled;
41
+ this.metricsEnabled = this.config.metrics?.enabled ?? true;
42
+ this.verboseEnabled = !!this.config.verbose && !isProd;
36
43
  this.metricsManager = new MetricsManager_js_1.MetricsManager({
37
- maxHistory: this.options.maxMetricsSize,
44
+ maxHistory: this.config.metrics?.maxHistory,
45
+ });
46
+ this.converter = new ResponseConverter_js_1.ResponseConverter({
47
+ maxBodySize: this.config.responseConverter?.maxBodySize,
48
+ parseHTML: this.config.responseConverter?.parseHTML,
49
+ htmlMode: this.config.responseConverter?.htmlMode,
50
+ charset: this.config.responseConverter?.charset,
38
51
  });
39
52
  this.interceptors = new InterceptorManager_js_1.InterceptorManager();
40
- this.transformer = new ResponseTransformer_js_1.ResponseTransformer(this.options.maxResponseBytes, this.options.logger);
41
- if (this.options.enableCache) {
53
+ if (this.cacheEnabled) {
42
54
  this.cache = new CacheManager_js_1.CacheManager({
43
- cacheTTL: this.options.cacheTTL,
44
- cacheMaxSize: this.options.cacheMaxSize,
55
+ cacheTTL: this.config.cache?.ttl,
56
+ cacheMaxSize: this.config.cache?.maxSize,
45
57
  });
46
58
  }
47
- if (this.options.enableQueue) {
48
- this.queue = new QueueManager_js_1.QueueManager(this.options.maxConcurrent ?? 500);
59
+ const concurrency = this.config.network?.maxConcurrent === 0
60
+ ? Infinity
61
+ : (this.config.network?.maxConcurrent ?? 500);
62
+ if (this.queueEnabled) {
63
+ this.queue = new QueueManager_js_1.QueueManager(concurrency);
49
64
  }
50
- if (this.options.enableRateLimit) {
51
- this.limiter = new RateLimiter_js_1.RateLimiter(this.options.rateLimit);
65
+ if (this.config.rateLimit?.enabled) {
66
+ this.limiter = new RateLimiter_js_1.RateLimiter(this.config.rateLimit);
52
67
  }
53
68
  this.agent = new undici_1.CookieAgent({
54
- connections: 1000,
55
- pipelining: 10,
56
- keepAliveTimeout: 60000,
69
+ connections: concurrency,
70
+ pipelining: this.config.network?.pipelining ?? 10,
71
+ keepAliveTimeout: this.config.network?.keepAliveTimeout ?? 30000,
72
+ keepAliveMaxTimeout: this.config.network?.keepAliveTimeout ?? 30000,
57
73
  connect: {
58
74
  ciphers: "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384",
75
+ rejectUnauthorized: this.config.network?.rejectUnauthorized,
59
76
  },
60
- // @ts-ignore
61
- allowH2: this.options.allowHttp2 ?? false,
77
+ allowH2: this.config.network?.allowHttp2 ?? true,
62
78
  });
63
79
  this.executor = new RequestExecutor_js_1.RequestExecutor(this.agent, this.interceptors, {
64
- timeout: this.options.timeout,
65
- maxRetries: this.options.maxRetries,
66
- followRedirects: this.options.followRedirects,
67
- maxRedirects: this.options.maxRedirects,
68
- retryOptions: this.options.retryOptions,
69
- verbose: this.options.verbose,
70
- logger: this.options.logger,
80
+ timeout: this.config.network?.timeout ?? 30000,
81
+ maxRetries: this.config.retry?.maxRetries ?? 3,
82
+ followRedirects: this.config.network?.followRedirects ?? true,
83
+ maxRedirects: this.config.network?.maxRedirects ?? 5,
84
+ retryOptions: {
85
+ maxRetries: this.config.retry?.maxRetries ?? 3,
86
+ baseDelay: this.config.retry?.baseDelay ?? 1000,
87
+ maxDelay: this.config.retry?.maxDelay ?? 10000,
88
+ retryStatusCodes: this.config.retry?.retryStatusCodes ?? [
89
+ 408, 429, 500, 502, 503, 504,
90
+ ],
91
+ jitter: this.config.retry?.jitter ?? true,
92
+ },
93
+ verbose: this.verboseEnabled,
94
+ logger: this.config.logger,
71
95
  });
96
+ const useragent = this.config.network?.userAgent
97
+ ? this.config.network?.userAgent
98
+ : "Hyperttp/2.0";
72
99
  this.defaultHeaders = {
73
100
  Accept: "application/json, text/plain, */*",
74
101
  "Accept-Encoding": "gzip, deflate, br",
75
- "User-Agent": this.options.userAgent,
102
+ "User-Agent": useragent,
76
103
  };
77
104
  }
78
- /**
79
- * @en Core internal method for handling all HTTP requests.
80
- * @ru Основной внутренний метод для обработки всех HTTP-запросов.
81
- * @param method HTTP method (GET, POST, etc.)
82
- * @param req Request object
83
- * @param useCache Whether to use caching for this request
84
- * @param responseType Expected response format
85
- */
86
- async requestInternal(method, req, useCache = true, responseType = "auto") {
87
- const url = req.getURL();
88
- if (this.metricsManager.isCircuitOpen(url)) {
89
- throw new index_js_1.HttpClientError(`Circuit Breaker is OPEN`, "CIRCUIT_OPEN", 503, undefined, url, method);
90
- }
91
- if (this.limiter && this.options.enableRateLimit) {
92
- await this.limiter.wait();
93
- }
94
- const { body, headers } = this.prepareRequestData(method, req);
95
- const key = method === "GET"
96
- ? `GET:${url}`
97
- : `${method}:${url}:${body ? JSON.stringify(body) : ""}`;
98
- if (method === "GET" && useCache && this.cache) {
99
- const cached = await this.cache.get(key);
100
- if (cached)
101
- return cached;
102
- }
103
- if (this.inflight.has(key))
104
- return this.inflight.get(key).promise;
105
- const internalController = new AbortController();
106
- const userSignal = req.getSignal?.();
107
- const abortHandler = () => internalController.abort();
108
- if (userSignal) {
109
- userSignal.addEventListener("abort", abortHandler, { once: true });
110
- }
111
- const executeAction = async () => {
112
- const metrics = this.createInitialMetrics(url, method);
113
- try {
114
- const rawResponse = await this.executor.execute(method, url, headers, body, metrics, internalController.signal);
115
- const bufferBody = await this.transformer.readBodyWithLimit(rawResponse.body);
116
- this.metricsManager.recordBytes(bufferBody.length);
117
- const parsed = await this.transformer.parseResponse({ ...rawResponse, body: bufferBody }, responseType);
118
- if (method === "HEAD")
119
- return {
120
- status: rawResponse.status,
121
- headers: rawResponse.headers,
122
- };
123
- if (method === "GET" &&
124
- useCache &&
125
- this.cache &&
126
- parsed !== undefined) {
127
- this.cache.set(key, parsed);
128
- }
129
- this.recordSuccess(metrics, rawResponse.status);
130
- return parsed;
131
- }
132
- catch (error) {
133
- this.recordError(metrics, error);
134
- throw error;
135
- }
136
- finally {
137
- if (userSignal)
138
- userSignal.removeEventListener("abort", abortHandler);
139
- this.inflight.delete(key);
140
- }
141
- };
142
- const promise = this.options.enableQueue && this.queue
143
- ? this.queue.enqueue(() => executeAction())
144
- : executeAction();
145
- this.inflight.set(key, { promise, controller: internalController });
146
- return promise;
147
- }
148
105
  /**
149
106
  * @en Performs an HTTP GET request.
150
107
  * @ru Выполняет HTTP GET запрос.
@@ -163,78 +120,62 @@ class HttpClientImproved {
163
120
  * @param responseType Expected response format
164
121
  */
165
122
  post(req, body, responseType = "auto") {
166
- const requestObj = this.normalizeRequest(req, body);
167
- return this.requestInternal("POST", requestObj, false, responseType);
123
+ return this.requestInternal("POST", this.normalizeRequest(req, body), false, responseType);
168
124
  }
169
125
  /**
170
126
  * @en Performs an HTTP PUT request.
171
127
  * @ru Выполняет HTTP PUT запрос.
172
128
  */
173
129
  put(req, body, responseType = "auto") {
174
- const requestObj = this.normalizeRequest(req, body);
175
- return this.requestInternal("PUT", requestObj, false, responseType);
130
+ return this.requestInternal("PUT", this.normalizeRequest(req, body), false, responseType);
176
131
  }
177
132
  /**
178
133
  * @en Performs an HTTP DELETE request.
179
134
  * @ru Выполняет HTTP DELETE запрос.
180
135
  */
181
136
  delete(req, responseType = "auto") {
182
- const requestObj = this.normalizeRequest(req);
183
- return this.requestInternal("DELETE", requestObj, false, responseType);
137
+ return this.requestInternal("DELETE", this.normalizeRequest(req), false, responseType);
184
138
  }
185
139
  /**
186
140
  * @en Performs an HTTP PATCH request.
187
141
  * @ru Выполняет HTTP PATCH запрос.
188
142
  */
189
143
  patch(req, body, responseType = "auto") {
190
- const requestObj = this.normalizeRequest(req, body);
191
- return this.requestInternal("PATCH", requestObj, false, responseType);
144
+ return this.requestInternal("PATCH", this.normalizeRequest(req, body), false, responseType);
192
145
  }
193
146
  /**
194
- * @en Creates a RequestBuilder for a fluent API approach.
195
- * @ru Создает RequestBuilder для использования Fluent API.
196
- * @example client.request('url').get().send();
147
+ * @en Performs an HTTP OPTIONS request.
148
+ * @ru Выполняет HTTP OPTIONS запрос.
197
149
  */
198
- request(url) {
199
- return new RequestBuilder_js_1.RequestBuilder(url, this);
200
- }
201
- /**
202
- * @en Releases all resources, aborts active requests, and closes connections.
203
- * @ru Освобождает ресурсы клиента, отменяет активные запросы и закрывает соединения.
204
- */
205
- async destroy() {
206
- if (this.inflight.size > 0) {
207
- if (this.options.verbose) {
208
- this.options.logger?.("info", `Aborting ${this.inflight.size} active requests...`);
209
- }
210
- for (const { controller } of this.inflight.values()) {
211
- controller.abort();
212
- }
213
- this.inflight.clear();
214
- }
215
- if (this.agent) {
216
- try {
217
- if (typeof this.agent.destroy === "function") {
218
- await this.agent.destroy();
219
- }
220
- else if (typeof this.agent.close === "function") {
221
- await this.agent.close();
222
- }
223
- }
224
- catch {
225
- /* ignore */
226
- }
227
- }
228
- if (this.cache)
229
- this.cache.clear();
150
+ options(req, body, responseType = "auto") {
151
+ return this.requestInternal("OPTIONS", this.normalizeRequest(req, body), false, responseType);
230
152
  }
231
153
  /**
232
154
  * @en Performs an HTTP HEAD request.
233
155
  * @ru Выполняет HTTP HEAD запрос.
234
156
  */
235
157
  async head(req) {
236
- const requestObj = this.normalizeRequest(req);
237
- return this.requestInternal("HEAD", requestObj, false);
158
+ return this.requestInternal("HEAD", this.normalizeRequest(req), false);
159
+ }
160
+ /**
161
+ * @en Creates a new HttpClient instance with merged configuration.
162
+ * @ru Создаёт новый экземпляр HttpClient с объединённой конфигурацией.
163
+ *
164
+ * @param options Partial configuration to override current settings
165
+ * @returns New HttpClientImproved instance
166
+ */
167
+ extend(options) {
168
+ return new HttpClientImproved(this.mergeOptions(this.config, options));
169
+ }
170
+ /**
171
+ * @en Alias for extend(). Creates a new configured client instance.
172
+ * @ru Алиас для extend(). Создаёт новый настроенный экземпляр клиента.
173
+ *
174
+ * @param options Partial configuration overrides
175
+ * @returns New HttpClientImproved instance
176
+ */
177
+ create(options) {
178
+ return this.extend(options);
238
179
  }
239
180
  /**
240
181
  * @en Executes a request and returns an AsyncIterable stream.
@@ -244,47 +185,59 @@ class HttpClientImproved {
244
185
  const requestObj = this.normalizeRequest(req);
245
186
  const url = requestObj.getURL();
246
187
  const { body, headers } = this.prepareRequestData("GET", requestObj);
247
- const key = `STREAM:GET:${url}`;
248
- const internalController = new AbortController();
188
+ const controller = new AbortController();
249
189
  const userSignal = requestObj.getSignal?.();
250
- const abortHandler = () => internalController.abort();
251
- if (userSignal) {
252
- userSignal.addEventListener("abort", abortHandler, { once: true });
190
+ const signal = userSignal
191
+ ? AbortSignal.any([userSignal, controller.signal])
192
+ : controller.signal;
193
+ const rawResponse = await this.executor.execute("GET", url, headers, body, undefined, signal);
194
+ const cleanup = () => {
195
+ this.inflight.delete(url);
196
+ };
197
+ rawResponse.body.once("close", cleanup);
198
+ rawResponse.body.once("error", cleanup);
199
+ rawResponse.body.once("end", cleanup);
200
+ this.inflight.set(url, {
201
+ promise: Promise.resolve(undefined),
202
+ controller,
203
+ });
204
+ return {
205
+ status: rawResponse.status,
206
+ headers: rawResponse.headers,
207
+ body: rawResponse.body,
208
+ url: rawResponse.url,
209
+ };
210
+ }
211
+ /**
212
+ * @en Creates a RequestBuilder for a fluent API approach.
213
+ * @ru Создает RequestBuilder для использования Fluent API.
214
+ * @example client.request('url').get().send();
215
+ */
216
+ request(url) {
217
+ return new RequestBuilder_js_1.RequestBuilder(url, this);
218
+ }
219
+ async destroy() {
220
+ this.queue?.clear();
221
+ this.limiter?.reset();
222
+ for (const { controller } of this.inflight.values()) {
223
+ controller.abort();
253
224
  }
225
+ this.inflight.clear();
254
226
  try {
255
- const rawResponse = await this.executor.execute("GET", url, headers, body, undefined, internalController.signal);
256
- this.inflight.set(key, {
257
- promise: Promise.resolve(),
258
- controller: internalController,
259
- });
260
- const cleanup = () => {
261
- if (userSignal)
262
- userSignal.removeEventListener("abort", abortHandler);
263
- this.inflight.delete(key);
264
- };
265
- rawResponse.body.on("close", cleanup);
266
- rawResponse.body.on("error", cleanup);
267
- return {
268
- status: rawResponse.status,
269
- headers: rawResponse.headers,
270
- body: rawResponse.body,
271
- url: rawResponse.url,
272
- };
227
+ if (typeof this.agent.destroy === "function") {
228
+ await this.agent.destroy();
229
+ }
230
+ else if (typeof this.agent.close === "function") {
231
+ await this.agent.close();
232
+ }
273
233
  }
274
- catch (error) {
275
- if (userSignal)
276
- userSignal.removeEventListener("abort", abortHandler);
277
- throw error;
234
+ catch {
235
+ /* ignore */
278
236
  }
237
+ this.cache?.clear();
279
238
  }
280
- /**
281
- * @en Clears the internal cache.
282
- * @ru Полностью очищает внутренний кэш клиента.
283
- */
284
239
  clearCache() {
285
- if (this.cache) {
286
- this.cache.clear();
287
- }
240
+ this.cache?.clear();
288
241
  }
289
242
  /**
290
243
  * @en Clears all collected performance metrics.
@@ -292,7 +245,6 @@ class HttpClientImproved {
292
245
  */
293
246
  clearMetrics() {
294
247
  this.metricsManager.clear();
295
- this.options.logger?.("info", "Metrics cleared");
296
248
  }
297
249
  /**
298
250
  * @en Retrieves metrics for a specific URL.
@@ -317,17 +269,41 @@ class HttpClientImproved {
317
269
  return {
318
270
  cacheSize: this.cache?.size ?? 0,
319
271
  inflightRequests: this.inflight.size,
320
- queuedRequests: this.options.enableQueue && this.queue
272
+ queuedRequests: this.queueEnabled && this.queue
321
273
  ? (this.queue.queuedCount ?? 0)
322
274
  : 0,
323
- activeRequests: this.options.enableQueue && this.queue
275
+ activeRequests: this.queueEnabled && this.queue
324
276
  ? (this.queue.activeCount ?? 0)
325
277
  : 0,
326
- currentRateLimit: this.options.enableRateLimit && this.limiter
278
+ currentRateLimit: this.limiterEnabled && this.limiter
327
279
  ? (this.limiter.currentCount ?? 0)
328
280
  : 0,
329
281
  };
330
282
  }
283
+ async warmup(urls, count = 10) {
284
+ const tasks = [];
285
+ for (let i = 0; i < count; i++) {
286
+ const url = urls[i % urls.length];
287
+ tasks.push(this.get(url, "text").catch(() => undefined));
288
+ }
289
+ await Promise.all(tasks);
290
+ }
291
+ mergeOptions(base, patch) {
292
+ return {
293
+ ...base,
294
+ ...patch,
295
+ network: { ...base.network, ...patch.network },
296
+ retry: { ...base.retry, ...patch.retry },
297
+ cache: { ...base.cache, ...patch.cache },
298
+ rateLimit: { ...base.rateLimit, ...patch.rateLimit },
299
+ metrics: { ...base.metrics, ...patch.metrics },
300
+ queue: { ...base.queue, ...patch.queue },
301
+ responseConverter: {
302
+ ...base.responseConverter,
303
+ ...patch.responseConverter,
304
+ },
305
+ };
306
+ }
331
307
  normalizeRequest(req, body) {
332
308
  if (typeof req === "string") {
333
309
  return {
@@ -338,35 +314,164 @@ class HttpClientImproved {
338
314
  }
339
315
  return req;
340
316
  }
341
- applyDefaultOptions(opt) {
317
+ applyDefaulthcoptions(opt) {
342
318
  const defaults = {
343
- timeout: 30000,
344
- maxRetries: 3,
345
- followRedirects: true,
346
- maxRedirects: 5,
347
- userAgent: "Hyperttp/2.0",
348
- maxResponseBytes: 10 * 1024 * 1024, // 10MB
349
- cacheTTL: 1000 * 60 * 5,
350
- cacheMaxSize: 500,
351
- enableCache: true,
352
- enableQueue: true,
353
- enableRateLimit: true,
354
- allowHttp2: false,
355
- retryOptions: {
319
+ network: {
320
+ timeout: 30000,
321
+ maxRedirects: 5,
322
+ followRedirects: true,
323
+ maxResponseBytes: 10 * 1024 * 1024,
324
+ userAgent: "Hyperttp/2.0",
325
+ allowHttp2: true,
326
+ pipelining: 10,
327
+ keepAliveTimeout: 30000,
328
+ maxConcurrent: 0,
329
+ rejectUnauthorized: false,
330
+ },
331
+ cache: {
332
+ enabled: true,
333
+ ttl: 1000 * 60 * 5,
334
+ maxSize: 500,
335
+ methods: [],
336
+ },
337
+ retry: {
356
338
  maxRetries: 3,
357
339
  baseDelay: 1000,
358
340
  maxDelay: 10000,
359
341
  retryStatusCodes: [408, 429, 500, 502, 503, 504],
360
342
  jitter: true,
361
343
  },
344
+ rateLimit: {
345
+ enabled: false,
346
+ maxRequests: 100,
347
+ windowMs: 60000,
348
+ },
349
+ metrics: {
350
+ enabled: true,
351
+ maxHistory: 1000,
352
+ },
353
+ queue: {
354
+ enabled: true,
355
+ },
356
+ responseConverter: {
357
+ maxBodySize: 0,
358
+ parseHTML: false,
359
+ htmlMode: "simple",
360
+ charset: "utf-8",
361
+ },
362
+ };
363
+ return {
364
+ ...defaults,
365
+ ...opt,
366
+ network: { ...defaults.network, ...opt?.network },
367
+ cache: { ...defaults.cache, ...opt?.cache },
368
+ retry: { ...defaults.retry, ...opt?.retry },
369
+ rateLimit: { ...defaults.rateLimit, ...opt?.rateLimit },
370
+ metrics: { ...defaults.metrics, ...opt?.metrics },
371
+ queue: { ...defaults.queue, ...opt?.queue },
372
+ responseConverter: {
373
+ ...defaults.responseConverter,
374
+ ...opt?.responseConverter,
375
+ },
362
376
  };
363
- return { ...defaults, ...opt };
377
+ }
378
+ /**
379
+ * @en Core internal method for handling all HTTP requests.
380
+ * @ru Основной внутренний метод для обработки всех HTTP-запросов.
381
+ * @param method HTTP method (GET, POST, etc.)
382
+ * @param req Request object
383
+ * @param useCache Whether to use caching for this request
384
+ * @param responseType Expected response format
385
+ */
386
+ async requestInternal(method, req, useCache = true, responseType = "auto") {
387
+ const url = req.getURL();
388
+ const userSignal = req.getSignal?.();
389
+ if (userSignal?.aborted) {
390
+ throw new errors_js_1.HttpClientError("Request aborted by user", "ABORTED", 0, undefined, url, method);
391
+ }
392
+ if (this.metricsManager.isCircuitOpen(url)) {
393
+ throw new errors_js_1.HttpClientError("Circuit Breaker is OPEN", "CIRCUIT_OPEN", 503, undefined, url, method);
394
+ }
395
+ if (this.limiter) {
396
+ await this.limiter.wait();
397
+ }
398
+ const { body, headers } = this.prepareRequestData(method, req);
399
+ const key = method === "GET"
400
+ ? `GET:${url}`
401
+ : body !== undefined && body !== null
402
+ ? `${method}:${url}:${typeof body === "string" ? body : JSON.stringify(body)}`
403
+ : `${method}:${url}`;
404
+ if (this.cache && method === "GET" && useCache) {
405
+ const cached = await this.cache.get(key);
406
+ if (cached !== undefined) {
407
+ return cached;
408
+ }
409
+ }
410
+ const existing = this.inflight.get(key);
411
+ if (existing) {
412
+ return existing.promise;
413
+ }
414
+ const internalController = new AbortController();
415
+ const abortHandler = () => internalController.abort();
416
+ if (userSignal) {
417
+ userSignal.addEventListener("abort", abortHandler, { once: true });
418
+ }
419
+ const run = () => (async () => {
420
+ let metrics;
421
+ const needMetrics = this.metricsEnabled;
422
+ if (needMetrics) {
423
+ metrics = this.createInitialMetrics(url, method);
424
+ }
425
+ try {
426
+ if (method === "HEAD") {
427
+ const rawResponse = await this.executor.execute(method, url, headers, body, metrics, internalController.signal);
428
+ if (needMetrics && metrics) {
429
+ this.recordSuccess(metrics, rawResponse.status);
430
+ }
431
+ return {
432
+ status: rawResponse.status,
433
+ headers: rawResponse.headers,
434
+ };
435
+ }
436
+ const rawResponse = await this.executor.execute(method, url, headers, body, metrics, internalController.signal);
437
+ const bufferBody = await this.converter.readBody(rawResponse.body);
438
+ const parsed = await this.converter.convert(bufferBody, responseType, {
439
+ contentType: rawResponse.headers["content-type"],
440
+ contentEncoding: rawResponse.headers["content-encoding"],
441
+ url: rawResponse.url,
442
+ });
443
+ if (this.cache &&
444
+ method === "GET" &&
445
+ useCache &&
446
+ parsed !== undefined) {
447
+ this.cache.set(key, parsed);
448
+ }
449
+ if (needMetrics && metrics) {
450
+ this.recordSuccess(metrics, rawResponse.status);
451
+ }
452
+ return parsed;
453
+ }
454
+ catch (error) {
455
+ if (needMetrics && metrics) {
456
+ this.recordError(metrics, error);
457
+ }
458
+ throw error;
459
+ }
460
+ finally {
461
+ if (userSignal) {
462
+ userSignal.removeEventListener("abort", abortHandler);
463
+ }
464
+ this.inflight.delete(key);
465
+ }
466
+ })();
467
+ const promise = this.queueEnabled && this.queue ? this.queue.enqueue(run) : run();
468
+ this.inflight.set(key, { promise, controller: internalController });
469
+ return promise;
364
470
  }
365
471
  prepareRequestData(method, req) {
366
472
  const headers = { ...this.defaultHeaders, ...req.getHeaders() };
367
473
  let rawBody = req.getBodyData();
368
- const methodsWithBody = ["POST", "PUT", "PATCH", "DELETE"];
369
- if (!methodsWithBody.includes(method)) {
474
+ if (!["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
370
475
  return { body: undefined, headers };
371
476
  }
372
477
  if (rawBody &&
@@ -378,8 +483,7 @@ class HttpClientImproved {
378
483
  if (contentType.includes("application/x-www-form-urlencoded")) {
379
484
  const params = new URLSearchParams();
380
485
  for (const [key, value] of Object.entries(rawBody)) {
381
- const finalValue = typeof value === "object" ? JSON.stringify(value) : String(value);
382
- params.append(key, finalValue);
486
+ params.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
383
487
  }
384
488
  rawBody = params.toString();
385
489
  }
@@ -413,8 +517,8 @@ class HttpClientImproved {
413
517
  metrics.duration = metrics.endTime - metrics.startTime;
414
518
  metrics.statusCode = status;
415
519
  this.metricsManager.record(metrics);
416
- if (this.options.verbose && this.options.logger) {
417
- this.options.logger("info", `Request successful: ${metrics.method} ${metrics.url}`, {
520
+ if (this.verboseEnabled) {
521
+ this.config.logger?.("info", `Request successful: ${metrics.method} ${metrics.url}`, {
418
522
  duration: metrics.duration,
419
523
  status: metrics.statusCode,
420
524
  });
@@ -423,12 +527,12 @@ class HttpClientImproved {
423
527
  recordError(metrics, error) {
424
528
  metrics.endTime = Date.now();
425
529
  metrics.duration = metrics.endTime - metrics.startTime;
426
- metrics.statusCode = error.statusCode || 0;
530
+ metrics.statusCode = error?.statusCode || 0;
427
531
  this.metricsManager.record(metrics);
428
- if (this.options.verbose && this.options.logger) {
429
- this.options.logger("error", `Request failed: ${metrics.method} ${metrics.url}`, {
430
- error: error.message,
431
- code: error.code,
532
+ if (this.verboseEnabled) {
533
+ this.config.logger?.("error", `Request failed: ${metrics.method} ${metrics.url}`, {
534
+ error: error?.message,
535
+ code: error?.code,
432
536
  });
433
537
  }
434
538
  }