hyperttp 0.2.3 → 0.2.5

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