hyperttp 0.2.2 → 0.2.3

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 (41) hide show
  1. package/dist/Hyperttp/Core/CacheManager.d.ts +26 -2
  2. package/dist/Hyperttp/Core/CacheManager.d.ts.map +1 -1
  3. package/dist/Hyperttp/Core/CacheManager.js +30 -13
  4. package/dist/Hyperttp/Core/CacheManager.js.map +1 -1
  5. package/dist/Hyperttp/Core/HttpClientImproved.d.ts +68 -98
  6. package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
  7. package/dist/Hyperttp/Core/HttpClientImproved.js +313 -631
  8. package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
  9. package/dist/Hyperttp/Core/InterceptorManager.d.ts +62 -0
  10. package/dist/Hyperttp/Core/InterceptorManager.d.ts.map +1 -0
  11. package/dist/Hyperttp/Core/InterceptorManager.js +64 -0
  12. package/dist/Hyperttp/Core/InterceptorManager.js.map +1 -0
  13. package/dist/Hyperttp/Core/MetricsManager.d.ts +50 -2
  14. package/dist/Hyperttp/Core/MetricsManager.d.ts.map +1 -1
  15. package/dist/Hyperttp/Core/MetricsManager.js +48 -4
  16. package/dist/Hyperttp/Core/MetricsManager.js.map +1 -1
  17. package/dist/Hyperttp/Core/QueueManager.d.ts +5 -40
  18. package/dist/Hyperttp/Core/QueueManager.d.ts.map +1 -1
  19. package/dist/Hyperttp/Core/QueueManager.js +41 -54
  20. package/dist/Hyperttp/Core/QueueManager.js.map +1 -1
  21. package/dist/Hyperttp/Core/RateLimiter.d.ts +50 -0
  22. package/dist/Hyperttp/Core/RateLimiter.d.ts.map +1 -1
  23. package/dist/Hyperttp/Core/RateLimiter.js +39 -1
  24. package/dist/Hyperttp/Core/RateLimiter.js.map +1 -1
  25. package/dist/Hyperttp/Core/RequestBuilder.d.ts +44 -73
  26. package/dist/Hyperttp/Core/RequestBuilder.d.ts.map +1 -1
  27. package/dist/Hyperttp/Core/RequestBuilder.js +71 -106
  28. package/dist/Hyperttp/Core/RequestBuilder.js.map +1 -1
  29. package/dist/Hyperttp/Core/RequestExecutor.d.ts +58 -0
  30. package/dist/Hyperttp/Core/RequestExecutor.d.ts.map +1 -0
  31. package/dist/Hyperttp/Core/RequestExecutor.js +160 -0
  32. package/dist/Hyperttp/Core/RequestExecutor.js.map +1 -0
  33. package/dist/Hyperttp/Core/ResponseTransformer.d.ts +35 -0
  34. package/dist/Hyperttp/Core/ResponseTransformer.d.ts.map +1 -0
  35. package/dist/Hyperttp/Core/ResponseTransformer.js +171 -0
  36. package/dist/Hyperttp/Core/ResponseTransformer.js.map +1 -0
  37. package/dist/Hyperttp/Core/index.d.ts +3 -0
  38. package/dist/Hyperttp/Core/index.d.ts.map +1 -1
  39. package/dist/Hyperttp/Core/index.js +7 -1
  40. package/dist/Hyperttp/Core/index.js.map +1 -1
  41. package/package.json +2 -2
@@ -1,757 +1,439 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
2
  Object.defineProperty(exports, "__esModule", { value: true });
39
- const tough_cookie_1 = require("tough-cookie");
40
- const undici_1 = require("undici");
41
- const undici_2 = require("http-cookie-agent/undici");
42
- const zlib = __importStar(require("zlib"));
43
- const util_1 = require("util");
44
- const fast_xml_parser_1 = require("fast-xml-parser");
45
- const fast_xml_builder_1 = __importDefault(require("fast-xml-builder"));
46
- const CacheManager_1 = require("./CacheManager");
47
- const QueueManager_1 = require("./QueueManager");
48
- const RateLimiter_1 = require("./RateLimiter");
49
- const Types_1 = require("../../Types");
50
- const RequestBuilder_1 = require("./RequestBuilder");
51
- const MetricsManager_1 = require("./MetricsManager");
52
- const gunzip = (0, util_1.promisify)(zlib.gunzip);
53
- const inflate = (0, util_1.promisify)(zlib.inflate);
54
- const brotliDecompress = (0, util_1.promisify)(zlib.brotliDecompress);
3
+ const undici_1 = require("http-cookie-agent/undici");
4
+ const CacheManager_js_1 = require("./CacheManager.js");
5
+ const QueueManager_js_1 = require("./QueueManager.js");
6
+ const RateLimiter_js_1 = require("./RateLimiter.js");
7
+ const MetricsManager_js_1 = require("./MetricsManager.js");
8
+ const RequestBuilder_js_1 = require("./RequestBuilder.js");
9
+ const InterceptorManager_js_1 = require("./InterceptorManager.js");
10
+ const ResponseTransformer_js_1 = require("./ResponseTransformer.js");
11
+ const RequestExecutor_js_1 = require("./RequestExecutor.js");
12
+ const index_js_1 = require("../../Types/index.js");
55
13
  /**
56
- * Advanced HTTP client with built-in caching, rate limiting, request queuing,
57
- * automatic retries, cookie management, and response decompression.
14
+ * @class HttpClientImproved
15
+ * @en High-performance HTTP client with built-in caching, queuing, rate limiting, and metrics.
16
+ * @ru Высокопроизводительный HTTP-клиент со встроенным кэшированием, очередями, лимитами и метриками.
58
17
  */
59
18
  class HttpClientImproved {
60
- cookieJar = new tough_cookie_1.CookieJar();
61
19
  agent;
20
+ options;
62
21
  cache;
63
22
  queue;
64
23
  limiter;
65
- inflight = new Map();
66
- retryOptions;
67
- defaultHeaders = {};
68
- options;
69
- requestInterceptors = [];
70
- responseInterceptors = [];
71
24
  metricsManager;
25
+ interceptors;
26
+ transformer;
27
+ executor;
72
28
  /**
73
- * Creates a new instance of HttpClientImproved.
74
- * @param options Optional configuration options for the HTTP client
29
+ * @en Internal map to track active requests for deduplication and cancellation.
30
+ * @ru Внутренняя карта для отслеживания активных запросов (дедупликация и отмена).
75
31
  */
32
+ inflight = new Map();
33
+ defaultHeaders = {};
76
34
  constructor(options) {
77
- this.options = {
78
- timeout: options?.timeout ?? 15000,
79
- maxConcurrent: options?.maxConcurrent ?? 50,
80
- maxRetries: options?.maxRetries ?? 3,
81
- cacheTTL: options?.cacheTTL ?? 300000,
82
- cacheMaxSize: options?.cacheMaxSize ?? 500,
83
- followRedirects: options?.followRedirects ?? true,
84
- maxRedirects: options?.maxRedirects ?? 5,
85
- validateStatus: options?.validateStatus ??
86
- ((status) => status >= 200 && status < 300),
87
- cacheMethods: options?.cacheMethods ?? ["GET", "HEAD"],
88
- maxMetricsSize: options?.maxMetricsSize ?? 1000,
89
- rateLimit: options?.rateLimit ?? { maxRequests: 100, windowMs: 60000 },
90
- userAgent: options?.userAgent ?? "Hyperttp/0.1.0 Node.js",
91
- logger: options?.logger ??
92
- ((level, message, meta) => {
93
- const methods = {
94
- debug: console.debug,
95
- info: console.info,
96
- warn: console.warn,
97
- error: console.error,
98
- };
99
- (methods[level] || console.log)(`[${level.toUpperCase()}] ${message}`, meta || "");
100
- }),
101
- retryOptions: options?.retryOptions ?? {},
102
- maxResponseBytes: options?.maxResponseBytes ?? 1024 * 1024,
103
- verbose: false,
104
- enableQueue: options?.enableQueue ?? false,
105
- enableRateLimit: options?.enableRateLimit ?? false,
106
- enableCache: options?.enableCache ?? true,
107
- };
108
- this.metricsManager = new MetricsManager_1.MetricsManager({
35
+ this.options = this.applyDefaultOptions(options);
36
+ this.metricsManager = new MetricsManager_js_1.MetricsManager({
109
37
  maxHistory: this.options.maxMetricsSize,
110
38
  });
39
+ this.interceptors = new InterceptorManager_js_1.InterceptorManager();
40
+ this.transformer = new ResponseTransformer_js_1.ResponseTransformer(this.options.maxResponseBytes, this.options.logger);
111
41
  if (this.options.enableCache) {
112
- this.cache = new CacheManager_1.CacheManager({
42
+ this.cache = new CacheManager_js_1.CacheManager({
113
43
  cacheTTL: this.options.cacheTTL,
114
44
  cacheMaxSize: this.options.cacheMaxSize,
115
45
  });
116
46
  }
117
47
  if (this.options.enableQueue) {
118
- this.queue = new QueueManager_1.QueueManager(this.options.maxConcurrent ?? 500);
48
+ this.queue = new QueueManager_js_1.QueueManager(this.options.maxConcurrent ?? 500);
119
49
  }
120
50
  if (this.options.enableRateLimit) {
121
- this.limiter = new RateLimiter_1.RateLimiter(this.options.rateLimit);
51
+ this.limiter = new RateLimiter_js_1.RateLimiter(this.options.rateLimit);
122
52
  }
123
- this.retryOptions = {
124
- maxRetries: this.options.maxRetries ?? 5,
125
- baseDelay: this.options.retryOptions?.baseDelay ?? 1000,
126
- maxDelay: this.options.retryOptions?.maxDelay ?? 30000,
127
- retryStatusCodes: this.options.retryOptions?.retryStatusCodes ?? [
128
- 408, 429, 500, 502, 503, 504,
129
- ],
130
- jitter: this.options.retryOptions?.jitter ?? true,
131
- };
132
- this.defaultHeaders = {
133
- Accept: "application/json, text/plain, */*",
134
- "Accept-Encoding": "gzip, deflate, br",
135
- "User-Agent": this.options.userAgent ?? "Hyperttp/0.1.0 Node.js",
136
- };
137
- this.agent = new undici_2.CookieAgent({
53
+ this.agent = new undici_1.CookieAgent({
138
54
  connections: 1000,
139
55
  pipelining: 10,
140
56
  keepAliveTimeout: 60000,
141
- keepAliveMaxTimeout: 600000,
142
57
  });
58
+ 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,
66
+ });
67
+ this.defaultHeaders = {
68
+ Accept: "application/json, text/plain, */*",
69
+ "Accept-Encoding": "gzip, deflate, br",
70
+ "User-Agent": this.options.userAgent,
71
+ };
143
72
  }
144
73
  /**
145
- * Sets default headers that will be applied to all outgoing requests.
146
- * @param headers An object containing header names and values
147
- */
148
- setDefaultHeaders(headers) {
149
- Object.assign(this.defaultHeaders, headers);
150
- }
151
- /**
152
- * Returns the cookie jar used for managing HTTP cookies.
153
- * @returns The CookieJar instance
154
- */
155
- getCookieJar() {
156
- return this.cookieJar;
157
- }
158
- /**
159
- * Adds a request interceptor to modify requests before they are sent.
160
- * @param interceptor The interceptor function to add
161
- */
162
- addRequestInterceptor(interceptor) {
163
- this.requestInterceptors.push(interceptor);
164
- }
165
- /**
166
- * Adds a response interceptor to modify responses after they are received.
167
- * @param interceptor The interceptor function to add
168
- */
169
- addResponseInterceptor(interceptor) {
170
- this.responseInterceptors.push(interceptor);
171
- }
172
- /**
173
- * @ru Закрывает агент и освобождает ресурсы (keep-alive соединения).
174
- * @en Closes the HTTP agent and terminates keep-alive connections.
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
175
80
  */
176
- close() {
177
- if (this.agent && typeof this.agent.destroy === "function") {
178
- this.agent.destroy();
179
- }
180
- }
181
- log(level, msg, meta) {
182
- if (this.options.verbose) {
183
- if (this.options.logger)
184
- this.options.logger(level, msg, meta);
185
- }
186
- }
187
- async decompress(buf, enc, charset = "utf-8") {
188
- if (!enc)
189
- return buf.toString(charset);
190
- try {
191
- switch (enc.toLowerCase()) {
192
- case "gzip":
193
- return (await gunzip(buf)).toString(charset);
194
- case "deflate":
195
- return (await inflate(buf)).toString(charset);
196
- case "br":
197
- return (await brotliDecompress(buf)).toString(charset);
198
- default:
199
- return buf.toString(charset);
200
- }
201
- }
202
- catch (error) {
203
- this.log("error", `Decompression failed for encoding ${enc}`, error);
204
- return buf.toString(charset);
205
- }
206
- }
207
- calcDelay(attempt) {
208
- const base = Math.min(this.retryOptions.baseDelay * 2 ** attempt, this.retryOptions.maxDelay);
209
- return this.retryOptions.jitter
210
- ? base * (0.75 + Math.random() * 0.5)
211
- : base;
212
- }
213
- sleep(ms) {
214
- return new Promise((resolve) => setTimeout(resolve, ms));
215
- }
216
- async applyRequestInterceptors(config) {
217
- let result = config;
218
- for (const interceptor of this.requestInterceptors)
219
- result = await interceptor(result);
220
- return result;
221
- }
222
- async applyResponseInterceptors(response) {
223
- let result = response;
224
- for (const interceptor of this.responseInterceptors)
225
- result = await interceptor(result);
226
- return result;
227
- }
228
- resolveRedirect(location, baseUrl) {
229
- try {
230
- return new URL(location, baseUrl).toString();
231
- }
232
- catch {
233
- return location;
234
- }
235
- }
236
- parseRetryAfterMs(retryAfterHeader) {
237
- if (!retryAfterHeader)
238
- return undefined;
239
- const raw = Array.isArray(retryAfterHeader)
240
- ? retryAfterHeader[0]
241
- : String(retryAfterHeader);
242
- const asSeconds = Number(raw);
243
- if (Number.isFinite(asSeconds))
244
- return Math.max(0, Math.floor(asSeconds * 1000));
245
- const asDate = Date.parse(raw);
246
- if (!Number.isNaN(asDate))
247
- return Math.max(0, asDate - Date.now());
248
- return undefined;
249
- }
250
- async readBodyWithLimit(body) {
251
- const limit = this.options.maxResponseBytes;
252
- const chunks = [];
253
- let receivedBytes = 0;
254
- for await (const chunk of body) {
255
- receivedBytes += chunk.length;
256
- if (typeof limit === "number" && limit > 0 && receivedBytes > limit) {
257
- if (typeof body.destroy === "function")
258
- body.destroy();
259
- throw new Types_1.HttpClientError(`Response too large`, "HTTP_ERROR", 0);
260
- }
261
- chunks.push(Buffer.from(chunk));
262
- }
263
- return Buffer.concat(chunks);
264
- }
265
- async sendWithRetry(method, url, headers, body, metrics, signal, redirects = 0) {
266
- let lastError;
267
- for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
268
- const timeoutController = new AbortController();
269
- const timeout = this.options.timeout ?? 15000;
270
- const timer = setTimeout(() => timeoutController.abort(), timeout);
271
- const abortHandler = () => timeoutController.abort();
272
- if (signal) {
273
- if (signal.aborted) {
274
- clearTimeout(timer);
275
- throw new Types_1.HttpClientError("Request aborted by user", "ABORTED", 0, undefined, url, method);
276
- }
277
- signal.addEventListener("abort", abortHandler);
278
- }
279
- try {
280
- if (this.limiter && this.options.enableRateLimit) {
281
- await this.limiter.wait();
282
- }
283
- const finalConfig = await this.applyRequestInterceptors({
284
- url,
285
- method,
286
- headers,
287
- body,
288
- });
289
- try {
290
- const res = await (0, undici_1.request)(finalConfig.url, {
291
- method: finalConfig.method,
292
- headers: finalConfig.headers,
293
- body: finalConfig.body,
294
- dispatcher: this.agent,
295
- signal: timeoutController.signal,
296
- });
297
- clearTimeout(timer);
298
- const buf = await this.readBodyWithLimit(res.body);
299
- let response = await this.applyResponseInterceptors({
300
- status: res.statusCode,
301
- headers: res.headers,
302
- body: buf,
303
- url: finalConfig.url,
304
- });
305
- if (this.options.followRedirects &&
306
- [301, 302, 303, 307, 308].includes(response.status) &&
307
- redirects < (this.options.maxRedirects ?? 5)) {
308
- const location = response.headers.location;
309
- if (location) {
310
- const nextUrl = this.resolveRedirect(location, finalConfig.url);
311
- const redirectMethod = response.status === 303 ? "GET" : method;
312
- const nextHeaders = { ...headers };
313
- let nextBody = body;
314
- if (redirectMethod === "GET") {
315
- nextBody = undefined;
316
- delete nextHeaders["content-type"];
317
- delete nextHeaders["Content-Type"];
318
- delete nextHeaders["content-length"];
319
- delete nextHeaders["Content-Length"];
320
- }
321
- this.log("debug", `Redirecting to ${nextUrl}`);
322
- return this.sendWithRetry(redirectMethod, nextUrl, nextHeaders, nextBody, metrics, signal, redirects + 1);
323
- }
324
- }
325
- if (this.retryOptions.retryStatusCodes.includes(response.status)) {
326
- metrics && (metrics.retries += 1);
327
- if (response.status === 429) {
328
- const ra = this.parseRetryAfterMs(response.headers["retry-after"]);
329
- if (ra !== undefined) {
330
- if (attempt < this.retryOptions.maxRetries) {
331
- await this.sleep(ra);
332
- continue;
333
- }
334
- throw new Types_1.RateLimitError(finalConfig.url, ra);
335
- }
336
- }
337
- if (attempt < this.retryOptions.maxRetries) {
338
- await this.sleep(this.calcDelay(attempt));
339
- continue;
340
- }
341
- }
342
- return response;
343
- }
344
- catch (innerErr) {
345
- clearTimeout(timer);
346
- if (innerErr.name === "AbortError") {
347
- if (signal?.aborted) {
348
- throw new Types_1.HttpClientError("Request aborted by user", "ABORTED", 0, innerErr, url, method);
349
- }
350
- else {
351
- throw new Types_1.TimeoutError(url, timeout);
352
- }
353
- }
354
- throw innerErr;
355
- }
356
- }
357
- catch (err) {
358
- lastError = err;
359
- if (err.code === 'ECONNREFUSED') {
360
- this.log("error", `Соединение отклонено: проверьте, запущен ли сервер на ${url}`);
361
- throw new Types_1.HttpClientError(`Request failed: ${err.message}`, 'REQUEST_FAILED', undefined, err, url, method);
362
- }
363
- if (err.code === "ABORTED" || err instanceof Types_1.TimeoutError) {
364
- throw err;
365
- }
366
- this.log("error", `Request error ${method} ${url}: ${err?.message}`);
367
- metrics && (metrics.retries += 1);
368
- if (attempt < this.retryOptions.maxRetries) {
369
- await this.sleep(this.calcDelay(attempt));
370
- continue;
371
- }
372
- }
373
- finally {
374
- clearTimeout(timer);
375
- if (signal) {
376
- signal.removeEventListener("abort", abortHandler);
377
- }
378
- }
379
- }
380
- if (lastError instanceof Types_1.HttpClientError)
381
- throw lastError;
382
- throw new Types_1.HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, "REQUEST_FAILED", undefined, lastError instanceof Error ? lastError : undefined, url, method);
383
- }
384
- xmlParser = new fast_xml_parser_1.XMLParser({
385
- ignoreAttributes: false,
386
- allowBooleanAttributes: true,
387
- });
388
- async parseResponse(res, responseType = "auto") {
389
- try {
390
- const text = await this.decompress(res.body, res.headers["content-encoding"]);
391
- const trimmed = text.trim();
392
- switch (responseType) {
393
- case "json": {
394
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
395
- return JSON.parse(trimmed);
396
- }
397
- if (trimmed.startsWith("<")) {
398
- return this.xmlParser.parse(trimmed);
399
- }
400
- return { data: trimmed };
401
- }
402
- case "xml": {
403
- if (trimmed.startsWith("<"))
404
- return trimmed;
405
- try {
406
- const obj = JSON.parse(trimmed);
407
- const builder = new fast_xml_builder_1.default({
408
- format: true,
409
- indentBy: " ",
410
- ignoreAttributes: false,
411
- });
412
- return builder.build(obj);
413
- }
414
- catch {
415
- return text;
416
- }
417
- }
418
- case "text":
419
- return text;
420
- case "buffer":
421
- return res.body;
422
- case "auto":
423
- default: {
424
- const contentType = (res.headers["content-type"] || "").toLowerCase();
425
- if (contentType.includes("json") ||
426
- trimmed.startsWith("{") ||
427
- trimmed.startsWith("[")) {
428
- try {
429
- return JSON.parse(trimmed);
430
- }
431
- catch {
432
- return text;
433
- }
434
- }
435
- return text;
436
- }
437
- }
438
- }
439
- catch (err) {
440
- throw new Types_1.HttpClientError(`Parsing failed: ${err?.message ?? String(err)}`, "PARSING_ERROR", res.status);
441
- }
442
- }
443
- async requestInternal(method, req, useCache = true, responseType) {
81
+ async requestInternal(method, req, useCache = true, responseType = "auto") {
444
82
  const url = req.getURL();
445
83
  if (this.metricsManager.isCircuitOpen(url)) {
446
- throw new Types_1.HttpClientError(`Circuit Breaker is OPEN for host: ${new URL(url).host}`, "CIRCUIT_OPEN", 503, undefined, url, method);
84
+ throw new index_js_1.HttpClientError(`Circuit Breaker is OPEN`, "CIRCUIT_OPEN", 503, undefined, url, method);
447
85
  }
448
- const rawBody = req.getBodyData();
449
- const headers = {
450
- ...this.defaultHeaders,
451
- ...req.getHeaders(),
452
- };
453
- const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
454
- let body;
455
- const contentType = headers["content-type"] || headers["Content-Type"] || "";
456
- if (isBodyAllowed && rawBody !== undefined && rawBody !== null) {
457
- if (Buffer.isBuffer(rawBody)) {
458
- body = rawBody;
459
- }
460
- else if (typeof rawBody === "string") {
461
- body = rawBody;
462
- }
463
- else if (contentType.includes("application/x-www-form-urlencoded")) {
464
- body = new URLSearchParams(rawBody).toString();
465
- }
466
- else {
467
- body = JSON.stringify(rawBody);
468
- if (!contentType)
469
- headers["Content-Type"] = "application/json; charset=utf-8";
470
- }
86
+ if (this.limiter && this.options.enableRateLimit) {
87
+ await this.limiter.wait();
471
88
  }
472
- const key = `${method}:${url}:${body ?? ""}`;
89
+ const { body, headers } = this.prepareRequestData(method, req);
90
+ const key = method === "GET"
91
+ ? `GET:${url}`
92
+ : `${method}:${url}:${body ? JSON.stringify(body) : ""}`;
473
93
  if (method === "GET" && useCache && this.cache) {
474
94
  const cached = await this.cache.get(key);
475
- if (cached) {
476
- this.log("debug", `Memory cache hit for ${url}`);
95
+ if (cached)
477
96
  return cached;
478
- }
479
- }
480
- if (this.inflight.has(key)) {
481
- this.log("debug", `Deduplicating request for ${url}`);
482
- return this.inflight.get(key);
483
97
  }
484
- const signal = req.getSignal?.();
485
- if (signal?.aborted) {
486
- throw new Types_1.HttpClientError("Aborted before execution", "ABORTED", 0, undefined, url, method);
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 });
487
105
  }
488
- const executeRequest = async () => {
489
- const metrics = {
490
- startTime: Date.now(),
491
- endTime: 0,
492
- duration: 0,
493
- bytesReceived: 0,
494
- bytesSent: 0,
495
- retries: 0,
496
- cached: false,
497
- url,
498
- method,
499
- };
106
+ const executeAction = async () => {
107
+ const metrics = this.createInitialMetrics(url, method);
500
108
  try {
501
- const res = await this.sendWithRetry(method, url, headers, body, metrics, signal);
502
- if (method === "HEAD") {
503
- metrics.endTime = Date.now();
504
- metrics.duration = metrics.endTime - metrics.startTime;
505
- this.metricsManager.record(metrics);
506
- return { status: res.status, headers: res.headers };
507
- }
508
- const parsed = await this.parseResponse(res, responseType);
509
- if (method === "GET" && useCache && this.cache) {
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) {
510
122
  this.cache.set(key, parsed);
511
123
  }
512
- metrics.endTime = Date.now();
513
- metrics.duration = metrics.endTime - metrics.startTime;
514
- metrics.statusCode = res.status;
515
- this.metricsManager.record(metrics);
124
+ this.recordSuccess(metrics, rawResponse.status);
516
125
  return parsed;
517
126
  }
518
127
  catch (error) {
519
- metrics.endTime = Date.now();
520
- metrics.duration = metrics.endTime - metrics.startTime;
521
- this.metricsManager.record(metrics);
128
+ this.recordError(metrics, error);
522
129
  throw error;
523
130
  }
524
131
  finally {
132
+ if (userSignal)
133
+ userSignal.removeEventListener("abort", abortHandler);
525
134
  this.inflight.delete(key);
526
135
  }
527
136
  };
528
- const promise = this.options.enableQueue
529
- ? this.queue.enqueue(() => executeRequest())
530
- : executeRequest();
531
- this.inflight.set(key, promise);
137
+ const promise = this.options.enableQueue && this.queue
138
+ ? this.queue.enqueue(() => executeAction())
139
+ : executeAction();
140
+ this.inflight.set(key, { promise, controller: internalController });
532
141
  return promise;
533
142
  }
534
143
  /**
535
- * Performs an HTTP GET request.
536
- * @param req The request object containing URL and headers
537
- * @param responseType Optional response parsing type
538
- * @returns A promise that resolves to the parsed response
539
- * @template T The expected response type
144
+ * @en Performs an HTTP GET request.
145
+ * @ru Выполняет HTTP GET запрос.
146
+ * @param req Request URL or Request object
147
+ * @param responseType Expected response format
540
148
  */
541
149
  get(req, responseType = "auto") {
542
- if (typeof req === "string") {
543
- const simpleReq = {
544
- getURL: () => req,
545
- getBodyData: () => undefined,
546
- getHeaders: () => ({}),
547
- };
548
- return this.requestInternal("GET", simpleReq, true, responseType);
549
- }
550
- else {
551
- return this.requestInternal("GET", req, true, responseType);
552
- }
150
+ const requestObj = this.normalizeRequest(req);
151
+ return this.requestInternal("GET", requestObj, true, responseType);
553
152
  }
554
153
  /**
555
- * Performs an HTTP POST request.
556
- * @param req The request object containing URL, body, and headers
557
- * @param responseType Optional response parsing type
558
- * @returns A promise that resolves to the parsed response
559
- * @template T The expected response type
154
+ * @en Performs an HTTP POST request.
155
+ * @ru Выполняет HTTP POST запрос.
156
+ * @param req Request URL or Request object
157
+ * @param body Request body data
158
+ * @param responseType Expected response format
560
159
  */
561
160
  post(req, body, responseType = "auto") {
562
- if (typeof req === "string") {
563
- const simpleReq = {
564
- getURL: () => req,
565
- getBodyData: () => body,
566
- getHeaders: () => ({ "Content-Type": "application/json" }),
567
- };
568
- return this.requestInternal("POST", simpleReq, false, responseType);
569
- }
570
- else {
571
- return this.requestInternal("POST", req, false, responseType);
572
- }
161
+ const requestObj = this.normalizeRequest(req, body);
162
+ return this.requestInternal("POST", requestObj, false, responseType);
573
163
  }
574
164
  /**
575
- * Performs an HTTP PUT request.
576
- * @param req The request object containing URL, body, and headers
577
- * @param responseType Optional response parsing type
578
- * @returns A promise that resolves to the parsed response
579
- * @template T The expected response type
165
+ * @en Performs an HTTP PUT request.
166
+ * @ru Выполняет HTTP PUT запрос.
580
167
  */
581
168
  put(req, body, responseType = "auto") {
582
- if (typeof req === "string") {
583
- const simpleReq = {
584
- getURL: () => req,
585
- getBodyData: () => body,
586
- getHeaders: () => ({ "Content-Type": "application/json" }),
587
- };
588
- return this.requestInternal("PUT", simpleReq, false, responseType);
589
- }
590
- return this.requestInternal("PUT", req, false, responseType);
169
+ const requestObj = this.normalizeRequest(req, body);
170
+ return this.requestInternal("PUT", requestObj, false, responseType);
591
171
  }
592
172
  /**
593
- * Performs an HTTP DELETE request.
594
- * @param req The request object containing URL and headers
595
- * @param responseType Optional response parsing type
596
- * @returns A promise that resolves to the parsed response
597
- * @template T The expected response type
173
+ * @en Performs an HTTP DELETE request.
174
+ * @ru Выполняет HTTP DELETE запрос.
598
175
  */
599
176
  delete(req, responseType = "auto") {
600
- if (typeof req === "string") {
601
- const simpleReq = {
602
- getURL: () => req,
603
- getBodyData: () => undefined,
604
- getHeaders: () => ({}),
605
- };
606
- return this.requestInternal("DELETE", simpleReq, false, responseType);
607
- }
608
- return this.requestInternal("DELETE", req, false, responseType);
177
+ const requestObj = this.normalizeRequest(req);
178
+ return this.requestInternal("DELETE", requestObj, false, responseType);
609
179
  }
610
180
  /**
611
- * Performs an HTTP PATCH request.
612
- * @param req The request object containing URL, body, and headers
613
- * @param responseType Optional response parsing type
614
- * @returns A promise that resolves to the parsed response
615
- * @template T The expected response type
181
+ * @en Performs an HTTP PATCH request.
182
+ * @ru Выполняет HTTP PATCH запрос.
616
183
  */
617
184
  patch(req, body, responseType = "auto") {
618
- if (typeof req === "string") {
619
- const simpleReq = {
620
- getURL: () => req,
621
- getBodyData: () => body,
622
- getHeaders: () => ({ "Content-Type": "application/json" }),
623
- };
624
- return this.requestInternal("PATCH", simpleReq, false, responseType);
625
- }
626
- return this.requestInternal("PATCH", req, false, responseType);
185
+ const requestObj = this.normalizeRequest(req, body);
186
+ return this.requestInternal("PATCH", requestObj, false, responseType);
627
187
  }
628
188
  /**
629
- * @ru Получает потоковый ответ (для SSE, больших файлов).
630
- * @en Gets streaming response (for SSE, large files).
189
+ * @en Creates a RequestBuilder for a fluent API approach.
190
+ * @ru Создает RequestBuilder для использования Fluent API.
191
+ * @example client.request('url').get().send();
631
192
  */
632
- async stream(req) {
633
- const requestObj = typeof req === "string"
634
- ? {
635
- getURL: () => req,
636
- getBodyData: () => undefined,
637
- getHeaders: () => ({}),
638
- getSignal: () => undefined,
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...`);
639
204
  }
640
- : req;
641
- const url = requestObj.getURL();
642
- const signal = requestObj.getSignal?.();
643
- if (signal?.aborted) {
644
- throw new Types_1.HttpClientError("Request aborted before execution", "ABORTED", 0, undefined, url, "GET");
205
+ for (const { controller } of this.inflight.values()) {
206
+ controller.abort();
207
+ }
208
+ this.inflight.clear();
645
209
  }
646
- const executeStream = async () => {
647
- const headers = {
648
- ...this.defaultHeaders,
649
- ...requestObj.getHeaders(),
650
- };
210
+ if (this.agent) {
651
211
  try {
652
- const response = await (0, undici_1.request)(url, {
653
- method: "GET",
654
- headers,
655
- dispatcher: this.agent,
656
- signal,
657
- bodyTimeout: this.options.timeout,
658
- headersTimeout: this.options.timeout,
659
- });
660
- return {
661
- status: response.statusCode,
662
- headers: response.headers,
663
- body: response.body,
664
- url,
665
- };
666
- }
667
- catch (err) {
668
- if (err.name === "AbortError") {
669
- throw new Types_1.HttpClientError("Stream aborted by user", "ABORTED", 0, err, url, "GET");
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();
670
217
  }
671
- throw err;
672
218
  }
673
- };
674
- if (this.queue && this.options.enableQueue) {
675
- return this.queue.enqueue(() => executeStream());
219
+ catch {
220
+ /* ignore */
221
+ }
676
222
  }
677
- return executeStream();
223
+ if (this.cache)
224
+ this.cache.clear();
678
225
  }
679
226
  /**
680
- * Performs an HTTP HEAD request.
681
- * @param req The request object containing URL and headers
682
- * @returns A promise that resolves when the request completes
227
+ * @en Performs an HTTP HEAD request.
228
+ * @ru Выполняет HTTP HEAD запрос.
683
229
  */
684
230
  async head(req) {
685
- if (typeof req === "string") {
686
- const simpleReq = {
687
- getURL: () => req,
688
- getBodyData: () => undefined,
689
- getHeaders: () => ({}),
231
+ const requestObj = this.normalizeRequest(req);
232
+ return this.requestInternal("HEAD", requestObj, false);
233
+ }
234
+ /**
235
+ * @en Executes a request and returns an AsyncIterable stream.
236
+ * @ru Выполняет запрос и возвращает итерируемый поток данных.
237
+ */
238
+ async stream(req) {
239
+ const requestObj = this.normalizeRequest(req);
240
+ const url = requestObj.getURL();
241
+ const { body, headers } = this.prepareRequestData("GET", requestObj);
242
+ const key = `STREAM:GET:${url}`;
243
+ const internalController = new AbortController();
244
+ const userSignal = requestObj.getSignal?.();
245
+ const abortHandler = () => internalController.abort();
246
+ if (userSignal) {
247
+ userSignal.addEventListener("abort", abortHandler, { once: true });
248
+ }
249
+ 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,
690
267
  };
691
- return this.requestInternal("HEAD", simpleReq, false);
692
268
  }
693
- return this.requestInternal("HEAD", req, false);
269
+ catch (error) {
270
+ if (userSignal)
271
+ userSignal.removeEventListener("abort", abortHandler);
272
+ throw error;
273
+ }
694
274
  }
695
275
  /**
696
- * Clears the request cache.
276
+ * @en Clears the internal cache.
277
+ * @ru Полностью очищает внутренний кэш клиента.
697
278
  */
698
- async clearCache() {
279
+ clearCache() {
699
280
  if (this.cache) {
700
- await this.cache.clear();
701
- this.log("info", "Cache cleared");
281
+ this.cache.clear();
702
282
  }
703
283
  }
704
284
  /**
705
- * Clears all collected request metrics.
706
- * Removes performance and timing data from memory.
285
+ * @en Clears all collected performance metrics.
286
+ * @ru Очищает все собранные метрики производительности.
707
287
  */
708
288
  clearMetrics() {
709
289
  this.metricsManager.clear();
710
- this.log("info", "Metrics cleared");
290
+ this.options.logger?.("info", "Metrics cleared");
711
291
  }
712
292
  /**
713
- * Retrieves metrics for a specific request by its URL.
714
- * @param key - The URL or cache key to retrieve metrics for
715
- * @returns Metrics object if found, undefined otherwise
293
+ * @en Retrieves metrics for a specific URL.
294
+ * @ru Получает метрики для конкретного URL.
716
295
  */
717
296
  getMetrics(key) {
718
297
  return this.metricsManager.get(key);
719
298
  }
720
299
  /**
721
- * Retrieves all collected request metrics.
722
- * @returns Array of all metrics objects
300
+ * @en Retrieves all stored request metrics.
301
+ * @ru Получает список всех сохраненных метрик.
723
302
  */
724
303
  getAllMetrics() {
725
304
  return Array.from(this.metricsManager.getAll());
726
305
  }
727
306
  /**
728
- * Creates a fluent request builder for making HTTP requests.
729
- * Provides a chainable API for building and sending requests.
730
- * @param url - The target URL for the request
731
- * @returns RequestBuilder instance for chaining
732
- */
733
- request(url) {
734
- return new RequestBuilder_1.RequestBuilder(url, this);
735
- }
736
- /**
737
- * Returns current statistics about the HTTP client's state.
738
- * @returns An object containing cache size, request counts, and rate limit information
307
+ * @en Returns real-time statistics about the client's internal state.
308
+ * @ru Возвращает статистику состояния клиента в реальном времени.
309
+ * @returns Cache size, active requests, queue state, etc.
739
310
  */
740
311
  getStats() {
741
312
  return {
742
313
  cacheSize: this.cache?.size ?? 0,
743
314
  inflightRequests: this.inflight.size,
744
- queuedRequests: this.queue && this.options.enableQueue
315
+ queuedRequests: this.options.enableQueue && this.queue
745
316
  ? (this.queue.queuedCount ?? 0)
746
317
  : 0,
747
- activeRequests: this.queue && this.options.enableQueue
318
+ activeRequests: this.options.enableQueue && this.queue
748
319
  ? (this.queue.activeCount ?? 0)
749
320
  : 0,
750
- currentRateLimit: this.limiter && this.options.enableRateLimit
321
+ currentRateLimit: this.options.enableRateLimit && this.limiter
751
322
  ? (this.limiter.currentCount ?? 0)
752
323
  : 0,
753
324
  };
754
325
  }
326
+ normalizeRequest(req, body) {
327
+ if (typeof req === "string") {
328
+ return {
329
+ getURL: () => req,
330
+ getBodyData: () => body,
331
+ getHeaders: () => ({}),
332
+ };
333
+ }
334
+ return req;
335
+ }
336
+ applyDefaultOptions(opt) {
337
+ 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: {
350
+ maxRetries: 3,
351
+ baseDelay: 1000,
352
+ maxDelay: 10000,
353
+ retryStatusCodes: [408, 429, 500, 502, 503, 504],
354
+ jitter: true,
355
+ },
356
+ };
357
+ return { ...defaults, ...opt };
358
+ }
359
+ prepareRequestData(method, req) {
360
+ const headers = { ...this.defaultHeaders, ...req.getHeaders() };
361
+ let rawBody = req.getBodyData();
362
+ const methodsWithBody = ["POST", "PUT", "PATCH", "DELETE"];
363
+ if (!methodsWithBody.includes(method)) {
364
+ return { body: undefined, headers };
365
+ }
366
+ if (rawBody &&
367
+ typeof rawBody === "object" &&
368
+ !(rawBody instanceof Buffer)) {
369
+ const contentType = (headers["content-type"] ||
370
+ headers["Content-Type"] ||
371
+ "").toLowerCase();
372
+ if (contentType.includes("application/x-www-form-urlencoded")) {
373
+ const params = new URLSearchParams();
374
+ for (const [key, value] of Object.entries(rawBody)) {
375
+ const finalValue = typeof value === "object" ? JSON.stringify(value) : String(value);
376
+ params.append(key, finalValue);
377
+ }
378
+ rawBody = params.toString();
379
+ }
380
+ 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);
392
+ }
393
+ }
394
+ }
395
+ return {
396
+ body: rawBody === null || rawBody === undefined ? undefined : rawBody,
397
+ headers,
398
+ };
399
+ }
400
+ createInitialMetrics(url, method) {
401
+ return {
402
+ startTime: Date.now(),
403
+ endTime: 0,
404
+ duration: 0,
405
+ bytesReceived: 0,
406
+ bytesSent: 0,
407
+ retries: 0,
408
+ cached: false,
409
+ url,
410
+ method,
411
+ };
412
+ }
413
+ recordSuccess(metrics, status) {
414
+ metrics.endTime = Date.now();
415
+ metrics.duration = metrics.endTime - metrics.startTime;
416
+ metrics.statusCode = status;
417
+ this.metricsManager.record(metrics);
418
+ if (this.options.verbose && this.options.logger) {
419
+ this.options.logger("info", `Request successful: ${metrics.method} ${metrics.url}`, {
420
+ duration: metrics.duration,
421
+ status: metrics.statusCode,
422
+ });
423
+ }
424
+ }
425
+ recordError(metrics, error) {
426
+ metrics.endTime = Date.now();
427
+ metrics.duration = metrics.endTime - metrics.startTime;
428
+ metrics.statusCode = error.statusCode || 0;
429
+ 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,
434
+ });
435
+ }
436
+ }
755
437
  }
756
438
  exports.default = HttpClientImproved;
757
439
  //# sourceMappingURL=HttpClientImproved.js.map