hyperttp 0.2.4 → 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 (105) 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 +307 -211
  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/UrlExtractor.d.ts +1 -1
  42. package/dist/Hyperttp/UrlExtractor.d.ts.map +1 -1
  43. package/dist/Hyperttp/index.d.ts +1 -3
  44. package/dist/Hyperttp/index.d.ts.map +1 -1
  45. package/dist/Hyperttp/index.js +7 -8
  46. package/dist/Hyperttp/index.js.map +1 -1
  47. package/dist/Types/cache.d.ts +10 -0
  48. package/dist/Types/cache.d.ts.map +1 -0
  49. package/dist/Types/cache.js +3 -0
  50. package/dist/Types/cache.js.map +1 -0
  51. package/dist/Types/errors.d.ts +15 -0
  52. package/dist/Types/errors.d.ts.map +1 -0
  53. package/dist/Types/errors.js +34 -0
  54. package/dist/Types/errors.js.map +1 -0
  55. package/dist/Types/http-client.d.ts +39 -0
  56. package/dist/Types/http-client.d.ts.map +1 -0
  57. package/dist/Types/http-client.js +3 -0
  58. package/dist/Types/http-client.js.map +1 -0
  59. package/dist/Types/http.d.ts +5 -0
  60. package/dist/Types/http.d.ts.map +1 -0
  61. package/dist/Types/http.js +3 -0
  62. package/dist/Types/http.js.map +1 -0
  63. package/dist/Types/index.d.ts +12 -127
  64. package/dist/Types/index.d.ts.map +1 -1
  65. package/dist/Types/index.js +12 -39
  66. package/dist/Types/index.js.map +1 -1
  67. package/dist/Types/interceptors.d.ts +13 -0
  68. package/dist/Types/interceptors.d.ts.map +1 -0
  69. package/dist/Types/interceptors.js +3 -0
  70. package/dist/Types/interceptors.js.map +1 -0
  71. package/dist/Types/metrics.d.ts +67 -0
  72. package/dist/Types/metrics.d.ts.map +1 -0
  73. package/dist/Types/metrics.js +3 -0
  74. package/dist/Types/metrics.js.map +1 -0
  75. package/dist/Types/options.d.ts +233 -0
  76. package/dist/Types/options.d.ts.map +1 -0
  77. package/dist/Types/options.js +3 -0
  78. package/dist/Types/options.js.map +1 -0
  79. package/dist/Types/queue.d.ts +8 -0
  80. package/dist/Types/queue.d.ts.map +1 -0
  81. package/dist/Types/queue.js +3 -0
  82. package/dist/Types/queue.js.map +1 -0
  83. package/dist/Types/request.d.ts +148 -9
  84. package/dist/Types/request.d.ts.map +1 -1
  85. package/dist/Types/response.d.ts +28 -0
  86. package/dist/Types/response.d.ts.map +1 -0
  87. package/dist/Types/response.js +3 -0
  88. package/dist/Types/response.js.map +1 -0
  89. package/dist/Types/stream.d.ts +39 -0
  90. package/dist/Types/stream.d.ts.map +1 -0
  91. package/dist/Types/stream.js +3 -0
  92. package/dist/Types/stream.js.map +1 -0
  93. package/dist/Types/url-extractor.d.ts +10 -0
  94. package/dist/Types/url-extractor.d.ts.map +1 -0
  95. package/dist/Types/url-extractor.js +3 -0
  96. package/dist/Types/url-extractor.js.map +1 -0
  97. package/dist/index.d.ts +1 -2
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +7 -3
  100. package/dist/index.js.map +1 -1
  101. package/package.json +7 -5
  102. package/dist/Hyperttp/Core/ResponseTransformer.d.ts +0 -35
  103. package/dist/Hyperttp/Core/ResponseTransformer.d.ts.map +0 -1
  104. package/dist/Hyperttp/Core/ResponseTransformer.js +0 -171
  105. 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,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,
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
  });
72
96
  this.defaultHeaders = {
73
97
  Accept: "application/json, text/plain, */*",
74
98
  "Accept-Encoding": "gzip, deflate, br",
75
- "User-Agent": this.options.userAgent,
99
+ "User-Agent": this.config.network?.userAgent,
76
100
  };
77
101
  }
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
102
  /**
149
103
  * @en Performs an HTTP GET request.
150
104
  * @ru Выполняет HTTP GET запрос.
@@ -163,78 +117,62 @@ class HttpClientImproved {
163
117
  * @param responseType Expected response format
164
118
  */
165
119
  post(req, body, responseType = "auto") {
166
- const requestObj = this.normalizeRequest(req, body);
167
- return this.requestInternal("POST", requestObj, false, responseType);
120
+ return this.requestInternal("POST", this.normalizeRequest(req, body), false, responseType);
168
121
  }
169
122
  /**
170
123
  * @en Performs an HTTP PUT request.
171
124
  * @ru Выполняет HTTP PUT запрос.
172
125
  */
173
126
  put(req, body, responseType = "auto") {
174
- const requestObj = this.normalizeRequest(req, body);
175
- return this.requestInternal("PUT", requestObj, false, responseType);
127
+ return this.requestInternal("PUT", this.normalizeRequest(req, body), false, responseType);
176
128
  }
177
129
  /**
178
130
  * @en Performs an HTTP DELETE request.
179
131
  * @ru Выполняет HTTP DELETE запрос.
180
132
  */
181
133
  delete(req, responseType = "auto") {
182
- const requestObj = this.normalizeRequest(req);
183
- return this.requestInternal("DELETE", requestObj, false, responseType);
134
+ return this.requestInternal("DELETE", this.normalizeRequest(req), false, responseType);
184
135
  }
185
136
  /**
186
137
  * @en Performs an HTTP PATCH request.
187
138
  * @ru Выполняет HTTP PATCH запрос.
188
139
  */
189
140
  patch(req, body, responseType = "auto") {
190
- const requestObj = this.normalizeRequest(req, body);
191
- return this.requestInternal("PATCH", requestObj, false, responseType);
192
- }
193
- /**
194
- * @en Creates a RequestBuilder for a fluent API approach.
195
- * @ru Создает RequestBuilder для использования Fluent API.
196
- * @example client.request('url').get().send();
197
- */
198
- request(url) {
199
- return new RequestBuilder_js_1.RequestBuilder(url, this);
141
+ return this.requestInternal("PATCH", this.normalizeRequest(req, body), false, responseType);
200
142
  }
201
143
  /**
202
- * @en Releases all resources, aborts active requests, and closes connections.
203
- * @ru Освобождает ресурсы клиента, отменяет активные запросы и закрывает соединения.
144
+ * @en Performs an HTTP OPTIONS request.
145
+ * @ru Выполняет HTTP OPTIONS запрос.
204
146
  */
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();
147
+ options(req, body, responseType = "auto") {
148
+ return this.requestInternal("OPTIONS", this.normalizeRequest(req, body), false, responseType);
230
149
  }
231
150
  /**
232
151
  * @en Performs an HTTP HEAD request.
233
152
  * @ru Выполняет HTTP HEAD запрос.
234
153
  */
235
154
  async head(req) {
236
- const requestObj = this.normalizeRequest(req);
237
- 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);
238
176
  }
239
177
  /**
240
178
  * @en Executes a request and returns an AsyncIterable stream.
@@ -244,47 +182,57 @@ class HttpClientImproved {
244
182
  const requestObj = this.normalizeRequest(req);
245
183
  const url = requestObj.getURL();
246
184
  const { body, headers } = this.prepareRequestData("GET", requestObj);
247
- const key = `STREAM:GET:${url}`;
248
- const internalController = new AbortController();
185
+ const controller = new AbortController();
249
186
  const userSignal = requestObj.getSignal?.();
250
- const abortHandler = () => internalController.abort();
251
- if (userSignal) {
252
- 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();
253
219
  }
220
+ this.inflight.clear();
254
221
  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
- };
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
+ }
273
228
  }
274
- catch (error) {
275
- if (userSignal)
276
- userSignal.removeEventListener("abort", abortHandler);
277
- throw error;
229
+ catch {
230
+ /* ignore */
278
231
  }
232
+ this.cache?.clear();
279
233
  }
280
- /**
281
- * @en Clears the internal cache.
282
- * @ru Полностью очищает внутренний кэш клиента.
283
- */
284
234
  clearCache() {
285
- if (this.cache) {
286
- this.cache.clear();
287
- }
235
+ this.cache?.clear();
288
236
  }
289
237
  /**
290
238
  * @en Clears all collected performance metrics.
@@ -292,7 +240,6 @@ class HttpClientImproved {
292
240
  */
293
241
  clearMetrics() {
294
242
  this.metricsManager.clear();
295
- this.options.logger?.("info", "Metrics cleared");
296
243
  }
297
244
  /**
298
245
  * @en Retrieves metrics for a specific URL.
@@ -317,17 +264,41 @@ class HttpClientImproved {
317
264
  return {
318
265
  cacheSize: this.cache?.size ?? 0,
319
266
  inflightRequests: this.inflight.size,
320
- queuedRequests: this.options.enableQueue && this.queue
267
+ queuedRequests: this.queueEnabled && this.queue
321
268
  ? (this.queue.queuedCount ?? 0)
322
269
  : 0,
323
- activeRequests: this.options.enableQueue && this.queue
270
+ activeRequests: this.queueEnabled && this.queue
324
271
  ? (this.queue.activeCount ?? 0)
325
272
  : 0,
326
- currentRateLimit: this.options.enableRateLimit && this.limiter
273
+ currentRateLimit: this.limiterEnabled && this.limiter
327
274
  ? (this.limiter.currentCount ?? 0)
328
275
  : 0,
329
276
  };
330
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
+ }
331
302
  normalizeRequest(req, body) {
332
303
  if (typeof req === "string") {
333
304
  return {
@@ -338,35 +309,161 @@ class HttpClientImproved {
338
309
  }
339
310
  return req;
340
311
  }
341
- applyDefaultOptions(opt) {
312
+ applyDefaulthcoptions(opt) {
342
313
  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: {
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: {
356
333
  maxRetries: 3,
357
334
  baseDelay: 1000,
358
335
  maxDelay: 10000,
359
336
  retryStatusCodes: [408, 429, 500, 502, 503, 504],
360
337
  jitter: true,
361
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
+ },
362
371
  };
363
- 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;
364
462
  }
365
463
  prepareRequestData(method, req) {
366
464
  const headers = { ...this.defaultHeaders, ...req.getHeaders() };
367
465
  let rawBody = req.getBodyData();
368
- const methodsWithBody = ["POST", "PUT", "PATCH", "DELETE"];
369
- if (!methodsWithBody.includes(method)) {
466
+ if (!["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
370
467
  return { body: undefined, headers };
371
468
  }
372
469
  if (rawBody &&
@@ -378,8 +475,7 @@ class HttpClientImproved {
378
475
  if (contentType.includes("application/x-www-form-urlencoded")) {
379
476
  const params = new URLSearchParams();
380
477
  for (const [key, value] of Object.entries(rawBody)) {
381
- const finalValue = typeof value === "object" ? JSON.stringify(value) : String(value);
382
- params.append(key, finalValue);
478
+ params.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
383
479
  }
384
480
  rawBody = params.toString();
385
481
  }
@@ -413,8 +509,8 @@ class HttpClientImproved {
413
509
  metrics.duration = metrics.endTime - metrics.startTime;
414
510
  metrics.statusCode = status;
415
511
  this.metricsManager.record(metrics);
416
- if (this.options.verbose && this.options.logger) {
417
- 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}`, {
418
514
  duration: metrics.duration,
419
515
  status: metrics.statusCode,
420
516
  });
@@ -423,12 +519,12 @@ class HttpClientImproved {
423
519
  recordError(metrics, error) {
424
520
  metrics.endTime = Date.now();
425
521
  metrics.duration = metrics.endTime - metrics.startTime;
426
- metrics.statusCode = error.statusCode || 0;
522
+ metrics.statusCode = error?.statusCode || 0;
427
523
  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,
524
+ if (this.verboseEnabled) {
525
+ this.config.logger?.("error", `Request failed: ${metrics.method} ${metrics.url}`, {
526
+ error: error?.message,
527
+ code: error?.code,
432
528
  });
433
529
  }
434
530
  }