hyperttp 0.1.8 → 0.2.0

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 (37) hide show
  1. package/dist/Hyperttp/Core/CacheManager.d.ts +13 -54
  2. package/dist/Hyperttp/Core/CacheManager.d.ts.map +1 -1
  3. package/dist/Hyperttp/Core/CacheManager.js +28 -50
  4. package/dist/Hyperttp/Core/CacheManager.js.map +1 -1
  5. package/dist/Hyperttp/Core/HttpClientImproved.d.ts +64 -285
  6. package/dist/Hyperttp/Core/HttpClientImproved.d.ts.map +1 -1
  7. package/dist/Hyperttp/Core/HttpClientImproved.js +282 -584
  8. package/dist/Hyperttp/Core/HttpClientImproved.js.map +1 -1
  9. package/dist/Hyperttp/Core/QueueManager.d.ts.map +1 -1
  10. package/dist/Hyperttp/Core/QueueManager.js.map +1 -1
  11. package/dist/Hyperttp/Core/RateLimiter.d.ts.map +1 -1
  12. package/dist/Hyperttp/Core/RateLimiter.js.map +1 -1
  13. package/dist/Hyperttp/Core/RequestBuilder.d.ts +97 -0
  14. package/dist/Hyperttp/Core/RequestBuilder.d.ts.map +1 -0
  15. package/dist/Hyperttp/Core/RequestBuilder.js +174 -0
  16. package/dist/Hyperttp/Core/RequestBuilder.js.map +1 -0
  17. package/dist/Hyperttp/Core/index.d.ts +1 -0
  18. package/dist/Hyperttp/Core/index.d.ts.map +1 -1
  19. package/dist/Hyperttp/Core/index.js +3 -1
  20. package/dist/Hyperttp/Core/index.js.map +1 -1
  21. package/dist/Hyperttp/Request.d.ts.map +1 -1
  22. package/dist/Hyperttp/Request.js.map +1 -1
  23. package/dist/Hyperttp/UrlExtractor.d.ts.map +1 -1
  24. package/dist/Hyperttp/UrlExtractor.js.map +1 -1
  25. package/dist/Hyperttp/index.d.ts +1 -1
  26. package/dist/Hyperttp/index.d.ts.map +1 -1
  27. package/dist/Hyperttp/index.js +2 -1
  28. package/dist/Hyperttp/index.js.map +1 -1
  29. package/dist/Types/index.d.ts.map +1 -1
  30. package/dist/Types/index.js.map +1 -1
  31. package/dist/Types/request.d.ts.map +1 -1
  32. package/dist/Types/request.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -1
  36. package/dist/index.js.map +1 -1
  37. package/package.json +4 -4
@@ -1,69 +1,55 @@
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  const tough_cookie_1 = require("tough-cookie");
4
37
  const undici_1 = require("undici");
5
38
  const undici_2 = require("http-cookie-agent/undici");
39
+ const zlib = __importStar(require("zlib"));
40
+ const util_1 = require("util");
6
41
  const fast_xml_parser_1 = require("fast-xml-parser");
7
- const url_1 = require("url");
8
- const crypto_1 = require("crypto");
9
42
  const CacheManager_1 = require("./CacheManager");
10
43
  const QueueManager_1 = require("./QueueManager");
11
44
  const RateLimiter_1 = require("./RateLimiter");
12
45
  const Types_1 = require("../../Types");
13
- let defaultClient = null;
46
+ const RequestBuilder_1 = require("./RequestBuilder");
47
+ const gunzip = (0, util_1.promisify)(zlib.gunzip);
48
+ const inflate = (0, util_1.promisify)(zlib.inflate);
49
+ const brotliDecompress = (0, util_1.promisify)(zlib.brotliDecompress);
14
50
  /**
15
- * @ru
16
- * Улучшенный HTTP-клиент с кэшированием, ограничением скорости, логикой повторных попыток и расширенными функциями.
17
- * Предоставляет надежный интерфейс для выполнения HTTP-запросов с автоматической обработкой
18
- * распространенных паттернов, таких как повторные попытки, кэширование и перехват запросов/ответов.
19
- * @en
20
- * Enhanced HTTP client with caching, rate limiting, retry logic, and advanced features.
21
- * Provides a robust interface for making HTTP requests with automatic handling of
22
- * common patterns like retries, caching, and request/response interception.
23
- *
24
- * @example
25
- * ```ts
26
- * const client = new HttpClientImproved({
27
- * timeout: 10000,
28
- * maxRetries: 3,
29
- * cacheTTL: 300000,
30
- * rateLimit: { maxRequests: 100, windowMs: 60000 }
31
- * });
32
- *
33
- * const response = await client.get('https://api.example.com/data');
34
- * ```
35
- *
36
- * @example
37
- * ```ts
38
- * // Using the fluent request builder
39
- * const client = new HttpClientImproved();
40
- * const response = await client.request('https://api.example.com/data')
41
- * .headers({ 'Authorization': 'Bearer token' })
42
- * .json()
43
- * .send();
44
- * ```
45
- *
46
- * @example
47
- * ```ts
48
- * // Using RequestInterface for complex requests
49
- * import { RequestInterface } from './src';
50
- *
51
- * class ApiRequest implements RequestInterface {
52
- * constructor(
53
- * private url: string,
54
- * private headers: Record<string, string> = {},
55
- * private body?: any
56
- * ) {}
57
- *
58
- * getURL(): string { return this.url; }
59
- * getHeaders(): Record<string, string> { return this.headers; }
60
- * getBodyData(): any { return this.body; }
61
- * }
62
- *
63
- * const client = new HttpClientImproved();
64
- * const request = new ApiRequest('https://api.example.com/data');
65
- * const response = await client.get(request);
66
- * ```
51
+ * Advanced HTTP client with built-in caching, rate limiting, request queuing,
52
+ * automatic retries, cookie management, and response decompression.
67
53
  */
68
54
  class HttpClientImproved {
69
55
  cookieJar = new tough_cookie_1.CookieJar();
@@ -78,6 +64,10 @@ class HttpClientImproved {
78
64
  requestInterceptors = [];
79
65
  responseInterceptors = [];
80
66
  requestMetrics = new Map();
67
+ /**
68
+ * Creates a new instance of HttpClientImproved.
69
+ * @param options Optional configuration options for the HTTP client
70
+ */
81
71
  constructor(options) {
82
72
  this.options = {
83
73
  timeout: options?.timeout ?? 15000,
@@ -106,11 +96,10 @@ class HttpClientImproved {
106
96
  retryOptions: options?.retryOptions ?? {},
107
97
  maxResponseBytes: options?.maxResponseBytes ?? 1024 * 1024,
108
98
  verbose: false,
109
- enableQueue: options?.enableQueue ?? true,
110
- enableRateLimit: options?.enableRateLimit ?? true,
99
+ enableQueue: options?.enableQueue ?? false,
100
+ enableRateLimit: options?.enableRateLimit ?? false,
111
101
  enableCache: options?.enableCache ?? true,
112
102
  };
113
- // Lazy initialization - only create components when needed
114
103
  if (this.options.enableCache) {
115
104
  this.cache = new CacheManager_1.CacheManager({
116
105
  cacheTTL: this.options.cacheTTL,
@@ -124,7 +113,7 @@ class HttpClientImproved {
124
113
  this.limiter = new RateLimiter_1.RateLimiter(this.options.rateLimit);
125
114
  }
126
115
  this.retryOptions = {
127
- maxRetries: this.options.maxRetries,
116
+ maxRetries: this.options.maxRetries ?? 5,
128
117
  baseDelay: this.options.retryOptions?.baseDelay ?? 1000,
129
118
  maxDelay: this.options.retryOptions?.maxDelay ?? 30000,
130
119
  retryStatusCodes: this.options.retryOptions?.retryStatusCodes ?? [
@@ -134,6 +123,7 @@ class HttpClientImproved {
134
123
  };
135
124
  this.defaultHeaders = {
136
125
  Accept: "application/json, text/plain, */*",
126
+ "Accept-Encoding": "gzip, deflate, br",
137
127
  "User-Agent": this.options.userAgent ?? "Hyperttp/0.1.0 Node.js",
138
128
  };
139
129
  this.agent = new undici_1.Agent({
@@ -146,97 +136,85 @@ class HttpClientImproved {
146
136
  },
147
137
  });
148
138
  }
149
- log(level, msg, meta) {
150
- if (!this.options.logger)
151
- return;
152
- const minLevel = process.env.NODE_ENV === "production" ? "warn" : "info";
153
- const levels = ["debug", "info", "warn", "error"];
154
- const currentLevelIndex = levels.indexOf(level);
155
- const minLevelIndex = levels.indexOf(minLevel);
156
- if (currentLevelIndex < minLevelIndex)
157
- return;
158
- if (this.options.verbose || level !== "info") {
159
- this.options.logger(level, msg, meta);
160
- }
139
+ /**
140
+ * Sets default headers that will be applied to all outgoing requests.
141
+ * @param headers An object containing header names and values
142
+ */
143
+ setDefaultHeaders(headers) {
144
+ Object.assign(this.defaultHeaders, headers);
145
+ }
146
+ /**
147
+ * Returns the cookie jar used for managing HTTP cookies.
148
+ * @returns The CookieJar instance
149
+ */
150
+ getCookieJar() {
151
+ return this.cookieJar;
161
152
  }
162
153
  /**
163
- * Creates a hash of the request body for cache key generation.
164
- * @param body - Request body (string or Buffer)
165
- * @returns SHA1 hash of the body, truncated to 8 characters
154
+ * Adds a request interceptor to modify requests before they are sent.
155
+ * @param interceptor The interceptor function to add
166
156
  */
167
- hashBody(body) {
168
- if (!body)
169
- return "";
170
- if (typeof body === "string")
171
- body = Buffer.from(body, "utf-8");
172
- return (0, crypto_1.createHash)("sha1").update(body).digest("hex").slice(0, 8);
157
+ addRequestInterceptor(interceptor) {
158
+ this.requestInterceptors.push(interceptor);
173
159
  }
174
160
  /**
175
- * Calculates the delay for retry attempts using exponential backoff.
176
- * @param attempt - Current retry attempt number (0-based)
177
- * @returns Delay in milliseconds
161
+ * Adds a response interceptor to modify responses after they are received.
162
+ * @param interceptor The interceptor function to add
178
163
  */
164
+ addResponseInterceptor(interceptor) {
165
+ this.responseInterceptors.push(interceptor);
166
+ }
167
+ /** Closes the HTTP agent to properly terminate keep-alive connections. */
168
+ close() {
169
+ this.agent.close();
170
+ }
171
+ log(level, msg, meta) {
172
+ if (this.options.verbose) {
173
+ if (this.options.logger)
174
+ this.options.logger(level, msg, meta);
175
+ }
176
+ }
177
+ async decompress(buf, enc, charset = "utf-8") {
178
+ if (!enc)
179
+ return buf.toString(charset);
180
+ try {
181
+ switch (enc.toLowerCase()) {
182
+ case "gzip":
183
+ return (await gunzip(buf)).toString(charset);
184
+ case "deflate":
185
+ return (await inflate(buf)).toString(charset);
186
+ case "br":
187
+ return (await brotliDecompress(buf)).toString(charset);
188
+ default:
189
+ return buf.toString(charset);
190
+ }
191
+ }
192
+ catch (error) {
193
+ this.log("error", `Decompression failed for encoding ${enc}`, error);
194
+ return buf.toString(charset);
195
+ }
196
+ }
179
197
  calcDelay(attempt) {
180
198
  const base = Math.min(this.retryOptions.baseDelay * 2 ** attempt, this.retryOptions.maxDelay);
181
199
  return this.retryOptions.jitter
182
200
  ? base * (0.75 + Math.random() * 0.5)
183
201
  : base;
184
202
  }
185
- /**
186
- * Creates a promise that resolves after the specified delay.
187
- * @param ms - Delay in milliseconds
188
- * @returns Promise that resolves after the delay
189
- */
190
203
  sleep(ms) {
191
204
  return new Promise((resolve) => setTimeout(resolve, ms));
192
205
  }
193
- /**
194
- * Applies all registered request interceptors to modify the request configuration.
195
- * Interceptors are executed in sequence, with each one receiving the output of the previous.
196
- * @param config - Original request configuration
197
- * @returns Modified request configuration
198
- */
199
206
  async applyRequestInterceptors(config) {
200
- if (!this.requestInterceptors.length)
201
- return config;
202
207
  let result = config;
203
- for (const interceptor of this.requestInterceptors) {
204
- try {
205
- result = await interceptor(result);
206
- }
207
- catch (error) {
208
- this.log("error", "Request interceptor failed", { error });
209
- throw error;
210
- }
211
- }
208
+ for (const interceptor of this.requestInterceptors)
209
+ result = await interceptor(result);
212
210
  return result;
213
211
  }
214
- /**
215
- * Applies all registered response interceptors to modify the response data.
216
- * Interceptors are executed in sequence, with each one receiving the output of the previous.
217
- * @param response - Original response data
218
- * @returns Modified response data
219
- */
220
212
  async applyResponseInterceptors(response) {
221
213
  let result = response;
222
- for (const interceptor of this.responseInterceptors) {
223
- try {
224
- result = await interceptor(result);
225
- }
226
- catch (error) {
227
- this.log("error", "Response interceptor failed", { error });
228
- throw error;
229
- }
230
- }
214
+ for (const interceptor of this.responseInterceptors)
215
+ result = await interceptor(result);
231
216
  return result;
232
217
  }
233
- /**
234
- * Resolves a redirect location relative to the base URL.
235
- * Handles both absolute and relative redirect URLs.
236
- * @param location - The redirect location from the response
237
- * @param baseUrl - The original request URL
238
- * @returns The resolved absolute URL
239
- */
240
218
  resolveRedirect(location, baseUrl) {
241
219
  try {
242
220
  return new URL(location, baseUrl).toString();
@@ -245,12 +223,6 @@ class HttpClientImproved {
245
223
  return location;
246
224
  }
247
225
  }
248
- /**
249
- * Parses the Retry-After header to determine when to retry a request.
250
- * Supports both seconds and HTTP date formats.
251
- * @param retryAfterHeader - The Retry-After header value
252
- * @returns Delay in milliseconds, or undefined if not parseable
253
- */
254
226
  parseRetryAfterMs(retryAfterHeader) {
255
227
  if (!retryAfterHeader)
256
228
  return undefined;
@@ -265,69 +237,19 @@ class HttpClientImproved {
265
237
  return Math.max(0, asDate - Date.now());
266
238
  return undefined;
267
239
  }
268
- /**
269
- * Reads response body with size limit enforcement.
270
- * Collects chunks until the response is complete or the limit is exceeded.
271
- * @param body - Async iterable of response chunks
272
- * @returns Complete response body as a Buffer
273
- */
274
- async readBodyWithLimit(body, maxBytes = this.options.maxResponseBytes) {
275
- if (!body)
276
- return Buffer.alloc(0);
277
- if (body[Symbol.asyncIterator]) {
278
- const chunks = [];
279
- let total = 0;
280
- for await (const chunk of body) {
281
- chunks.push(Buffer.from(chunk));
282
- total += chunk.length;
283
- if (total > maxBytes)
284
- break;
285
- }
286
- return Buffer.concat(chunks);
287
- }
288
- if (body?.arrayBuffer) {
289
- const arrayBuffer = await body.arrayBuffer();
290
- return Buffer.from(arrayBuffer);
240
+ async readBodyWithLimit(body) {
241
+ const buf = Buffer.from(await body.arrayBuffer());
242
+ const limit = this.options.maxResponseBytes;
243
+ if (typeof limit === "number" && limit > 0 && buf.length > limit) {
244
+ throw new Types_1.HttpClientError(`Response too large (${buf.length} bytes), limit is ${limit}`, 0);
291
245
  }
292
- if (Buffer.isBuffer(body))
293
- return body.subarray(0, maxBytes);
294
- if (typeof body === "string")
295
- return Buffer.from(body);
296
- throw new TypeError(`Unsupported body type: ${typeof body}`);
246
+ return buf;
297
247
  }
298
- /**
299
- * Removes old metrics entries to prevent memory leaks.
300
- * Keeps only metrics from the last 24 hours.
301
- */
302
- trimMetrics() {
303
- const cutoff = Date.now() - 24 * 60 * 60 * 1000;
304
- for (const [key, metrics] of this.requestMetrics) {
305
- if (metrics.endTime && metrics.endTime < cutoff) {
306
- this.requestMetrics.delete(key);
307
- }
308
- }
309
- for (const key of this.inflight.keys()) {
310
- if (this.inflight.size > 1000) {
311
- this.inflight.delete(key);
312
- }
313
- }
314
- }
315
- /**
316
- * Sends an HTTP request with retry logic and rate limiting.
317
- * Handles timeouts, redirects, and various retry scenarios.
318
- * @param method - HTTP method (GET, POST, etc.)
319
- * @param url - Target URL
320
- * @param headers - HTTP headers
321
- * @param body - Request body (optional)
322
- * @param metrics - Optional metrics object to track request details
323
- * @param redirects - Number of redirects followed so far
324
- * @returns Promise resolving to the response data
325
- */
326
248
  async sendWithRetry(method, url, headers, body, metrics, redirects = 0) {
327
249
  let lastError;
328
250
  for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
329
251
  try {
330
- if (this.limiter) {
252
+ if (this.limiter && this.options.enableRateLimit) {
331
253
  await this.limiter.wait();
332
254
  }
333
255
  const finalConfig = await this.applyRequestInterceptors({
@@ -337,7 +259,7 @@ class HttpClientImproved {
337
259
  body,
338
260
  });
339
261
  const controller = new AbortController();
340
- const timeout = this.options.timeout;
262
+ const timeout = this.options.timeout ?? 15000;
341
263
  const timer = setTimeout(() => controller.abort(), timeout);
342
264
  try {
343
265
  const res = await (0, undici_1.request)(finalConfig.url, {
@@ -347,15 +269,6 @@ class HttpClientImproved {
347
269
  dispatcher: this.agent,
348
270
  signal: controller.signal,
349
271
  });
350
- if (method === "HEAD") {
351
- clearTimeout(timer);
352
- return {
353
- status: res.statusCode,
354
- headers: res.headers,
355
- body: Buffer.alloc(0),
356
- url: finalConfig.url,
357
- };
358
- }
359
272
  clearTimeout(timer);
360
273
  const buf = await this.readBodyWithLimit(res.body);
361
274
  let response = await this.applyResponseInterceptors({
@@ -364,9 +277,7 @@ class HttpClientImproved {
364
277
  body: buf,
365
278
  url: finalConfig.url,
366
279
  });
367
- if (!this.options.validateStatus(response.status)) {
368
- throw new Types_1.HttpClientError(`Request failed with status ${response.status}`, response.status, undefined, finalConfig.url, finalConfig.method);
369
- }
280
+ // Redirects
370
281
  if (this.options.followRedirects &&
371
282
  [301, 302, 303, 307, 308].includes(response.status) &&
372
283
  redirects < (this.options.maxRedirects ?? 5)) {
@@ -376,6 +287,7 @@ class HttpClientImproved {
376
287
  const redirectMethod = response.status === 303 ? "GET" : method;
377
288
  const nextHeaders = { ...headers };
378
289
  let nextBody = body;
290
+ // If switching to GET, drop body-related headers.
379
291
  if (redirectMethod === "GET") {
380
292
  nextBody = undefined;
381
293
  delete nextHeaders["content-type"];
@@ -390,6 +302,7 @@ class HttpClientImproved {
390
302
  return this.sendWithRetry(redirectMethod, nextUrl, nextHeaders, nextBody, metrics, redirects + 1);
391
303
  }
392
304
  }
305
+ // Retry by status
393
306
  if (this.retryOptions.retryStatusCodes.includes(response.status)) {
394
307
  metrics && (metrics.retries += 1);
395
308
  if (response.status === 429) {
@@ -441,11 +354,6 @@ class HttpClientImproved {
441
354
  throw lastError;
442
355
  throw new Types_1.HttpClientError(`Request failed after ${this.retryOptions.maxRetries + 1} attempts`, undefined, lastError instanceof Error ? lastError : undefined, url, method);
443
356
  }
444
- /**
445
- * Parses the Content-Type header to extract MIME type and character encoding.
446
- * @param contentType - Content-Type header value
447
- * @returns Object containing type and charset information
448
- */
449
357
  parseContentType(contentType) {
450
358
  if (!contentType)
451
359
  return { type: "text/plain", charset: "utf-8" };
@@ -469,35 +377,41 @@ class HttpClientImproved {
469
377
  "base64",
470
378
  "hex",
471
379
  ];
472
- return {
473
- type,
474
- charset: allowed.includes(normalized)
475
- ? normalized
476
- : "utf-8",
477
- };
478
- }
479
- /**
480
- * Parses the HTTP response body based on content type and requested response type.
481
- * Handles JSON, XML, text, and buffer responses with fallback parsing.
482
- * @param res - HTTP response object
483
- * @param responseType - Desired response type
484
- * @returns Parsed response data
485
- */
380
+ const charset = (allowed.includes(normalized)
381
+ ? normalized
382
+ : "utf-8");
383
+ return { type, charset };
384
+ }
385
+ xmlParser = new fast_xml_parser_1.XMLParser({
386
+ ignoreAttributes: false,
387
+ allowBooleanAttributes: true,
388
+ });
486
389
  async parseResponse(res, responseType) {
487
- const { type, charset } = this.parseContentType(res.headers["content-type"]);
488
- const text = res.body.toString(charset);
489
- const finalType = responseType ?? (type.includes("application/json") ? "json" : "text");
490
390
  try {
391
+ const contentType = res.headers["content-type"] || "";
392
+ const text = await this.decompress(res.body, res.headers["content-encoding"]);
393
+ const finalType = responseType ?? "json";
491
394
  switch (finalType) {
492
- case "json":
493
- return JSON.parse(res.body.toString("utf8"));
395
+ case "json": {
396
+ if (contentType.includes("json")) {
397
+ return JSON.parse(text);
398
+ }
399
+ if (text.trim().startsWith("<")) {
400
+ return this.xmlParser.parse(text);
401
+ }
402
+ return { text };
403
+ }
494
404
  case "xml": {
405
+ const text = await this.decompress(res.body, res.headers["content-encoding"]);
406
+ if (text.trim().startsWith("<")) {
407
+ return text;
408
+ }
495
409
  try {
496
- const jsonData = JSON.parse(text);
497
- return new fast_xml_parser_1.XMLBuilder({ format: true }).build({ root: jsonData });
410
+ const json = JSON.parse(text);
411
+ return new fast_xml_parser_1.XMLBuilder({ format: true }).build({ root: json });
498
412
  }
499
413
  catch {
500
- return text;
414
+ return String(text);
501
415
  }
502
416
  }
503
417
  case "text":
@@ -505,91 +419,21 @@ class HttpClientImproved {
505
419
  case "buffer":
506
420
  return res.body;
507
421
  case "stream":
508
- throw new Error("Stream mode requires raw response. Use stream() method.");
422
+ return res.body;
509
423
  default:
510
424
  return text;
511
425
  }
512
426
  }
513
- catch (err) {
514
- throw new Types_1.HttpClientError(`Parsing failed: ${err?.message ?? String(err)}`, res.status);
515
- }
516
- }
517
- /**
518
- * Makes an HTTP request without using the cache.
519
- * Used for methods that shouldn't be cached or when caching is disabled.
520
- * @param method - HTTP method
521
- * @param req - Request configuration
522
- * @param responseType - Expected response type
523
- * @returns Promise resolving to the response data
524
- */
525
- async requestInternalWithoutCache(method, req, responseType) {
526
- const url = req.getURL();
527
- const rawBody = req.getBodyData();
528
- const headers = {
529
- ...this.defaultHeaders,
530
- ...req.getHeaders(),
531
- };
532
- const isBodyAllowed = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
533
- let body;
534
- const contentType = headers["content-type"] || headers["Content-Type"] || "";
535
- if (isBodyAllowed && rawBody !== undefined && rawBody !== null) {
536
- if (Buffer.isBuffer(rawBody)) {
537
- body = rawBody;
538
- }
539
- else if (typeof rawBody === "string") {
540
- body = rawBody;
541
- }
542
- else if (contentType.includes("application/x-www-form-urlencoded")) {
543
- body = new url_1.URLSearchParams(rawBody).toString();
544
- }
545
- else {
546
- body = JSON.stringify(rawBody);
547
- if (!contentType)
548
- headers["Content-Type"] = "application/json; charset=utf-8";
549
- }
427
+ catch (error) {
428
+ this.log("error", "Failed to parse response", {
429
+ error,
430
+ status: res.status,
431
+ contentType: res.headers["content-type"],
432
+ });
433
+ throw new Types_1.HttpClientError(`Response parsing failed: ${error instanceof Error ? error.message : String(error)}`, res.status);
550
434
  }
551
- const metrics = {
552
- startTime: Date.now(),
553
- endTime: 0,
554
- duration: 0,
555
- bytesReceived: 0,
556
- bytesSent: 0,
557
- retries: 0,
558
- cached: false,
559
- url,
560
- method,
561
- };
562
- const result = await (this.queue.enqueue(async () => {
563
- const res = await this.sendWithRetry(method, url, headers, body, metrics);
564
- metrics.statusCode = res.status;
565
- metrics.bytesReceived = res.body.length;
566
- metrics.bytesSent =
567
- body instanceof Buffer ? body.length : Buffer.byteLength(body || "");
568
- if (method === "HEAD") {
569
- return { status: res.status, headers: res.headers };
570
- }
571
- const parsed = await this.parseResponse(res, responseType);
572
- return parsed;
573
- }) ?? Promise.resolve(undefined));
574
- metrics.endTime = Date.now();
575
- metrics.duration = metrics.endTime - metrics.startTime;
576
- this.requestMetrics.set(url, metrics);
577
- this.trimMetrics();
578
- return result;
579
435
  }
580
- /**
581
- * Makes an HTTP request with caching support.
582
- * Handles cache lookups, request deduplication, and automatic cache storage.
583
- * @param method - HTTP method
584
- * @param req - Request configuration
585
- * @param useCache - Whether to use caching (default: true)
586
- * @param responseType - Expected response type
587
- * @returns Promise resolving to the response data
588
- */
589
436
  async requestInternal(method, req, useCache = true, responseType) {
590
- if (this.options.cacheTTL === 0) {
591
- return this.requestInternalWithoutCache(method, req, responseType);
592
- }
593
437
  const url = req.getURL();
594
438
  const rawBody = req.getBodyData();
595
439
  const headers = {
@@ -607,28 +451,26 @@ class HttpClientImproved {
607
451
  body = rawBody;
608
452
  }
609
453
  else if (contentType.includes("application/x-www-form-urlencoded")) {
610
- body = new url_1.URLSearchParams(rawBody).toString();
454
+ body = new URLSearchParams(rawBody).toString();
611
455
  }
612
456
  else {
613
457
  body = JSON.stringify(rawBody);
614
- if (!contentType)
458
+ if (!contentType) {
615
459
  headers["Content-Type"] = "application/json; charset=utf-8";
460
+ }
616
461
  }
617
462
  }
618
- const bodyHash = this.hashBody(body);
619
- const cacheKey = (0, crypto_1.createHash)("sha1")
620
- .update(method + url + bodyHash + responseType)
621
- .digest("hex");
622
- if (this.options.cacheMethods.includes(method) && useCache && this.cache) {
623
- const cached = this.cache.get(cacheKey);
463
+ const key = `${method}:${url}:${body ?? ""}`;
464
+ if (method === "GET" && useCache && this.cache) {
465
+ const cached = await this.cache.get(key);
624
466
  if (cached) {
625
467
  this.log("debug", `Cache hit for ${url}`);
626
468
  return cached;
627
469
  }
628
470
  }
629
- if (this.inflight.has(cacheKey)) {
471
+ if (this.inflight.has(key)) {
630
472
  this.log("debug", `Deduplicating request for ${url}`);
631
- return this.inflight.get(cacheKey);
473
+ return this.inflight.get(key);
632
474
  }
633
475
  const promise = (async () => {
634
476
  const metrics = {
@@ -641,58 +483,55 @@ class HttpClientImproved {
641
483
  cached: false,
642
484
  url,
643
485
  method,
644
- bodyHash,
645
486
  };
646
487
  try {
647
- this.log("debug", `Starting request: ${method} ${url}`);
648
- const result = await this.queue.enqueue(async () => {
649
- const res = await this.sendWithRetry(method, url, headers, body, metrics);
650
- metrics.statusCode = res.status;
651
- metrics.bytesReceived = res.body.length;
652
- metrics.bytesSent =
653
- body instanceof Buffer
654
- ? body.length
655
- : Buffer.byteLength(body || "");
656
- if (method === "HEAD") {
657
- return { status: res.status, headers: res.headers };
658
- }
659
- const parsed = await this.parseResponse(res, responseType);
660
- if (this.options.cacheMethods.includes(method) &&
661
- useCache &&
662
- this.cache) {
663
- this.cache.set(cacheKey, parsed);
664
- metrics.cached = true;
665
- }
666
- return parsed;
667
- });
488
+ let result;
489
+ const res = await this.sendWithRetry(method, url, headers, body, metrics);
490
+ metrics.statusCode = res.status;
491
+ metrics.bytesReceived = res.body.length;
492
+ metrics.bytesSent =
493
+ body instanceof Buffer ? body.length : Buffer.byteLength(body || "");
494
+ if (method === "HEAD") {
495
+ metrics.endTime = Date.now();
496
+ metrics.duration = metrics.endTime - metrics.startTime;
497
+ this.requestMetrics.set(key, metrics);
498
+ this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
499
+ return {
500
+ status: res.status,
501
+ headers: res.headers,
502
+ };
503
+ }
504
+ const parsed = await this.parseResponse(res, responseType);
505
+ if (method === "GET" && useCache && this.cache) {
506
+ this.cache.set(key, parsed);
507
+ metrics.cached = true;
508
+ }
509
+ result = parsed;
668
510
  metrics.endTime = Date.now();
669
511
  metrics.duration = metrics.endTime - metrics.startTime;
670
- this.requestMetrics.set(cacheKey, metrics);
671
- this.trimMetrics();
512
+ this.requestMetrics.set(key, metrics);
672
513
  this.log("info", `${method} ${url} completed in ${metrics.duration}ms`, metrics);
673
514
  return result;
674
515
  }
675
516
  catch (error) {
676
517
  metrics.endTime = Date.now();
677
518
  metrics.duration = metrics.endTime - metrics.startTime;
678
- this.requestMetrics.set(cacheKey, metrics);
679
- this.trimMetrics();
519
+ this.requestMetrics.set(key, metrics);
680
520
  throw error;
681
521
  }
682
522
  finally {
683
- this.inflight.delete(cacheKey);
523
+ this.inflight.delete(key);
684
524
  }
685
525
  })();
686
- this.inflight.set(cacheKey, promise);
526
+ this.inflight.set(key, promise);
687
527
  return promise;
688
528
  }
689
529
  /**
690
- * Makes an HTTP GET request.
691
- * Supports both RequestInterface objects and direct URL strings.
692
- * GET requests are cached by default unless caching is disabled.
693
- * @param req - Request configuration or URL string
694
- * @param responseType - Expected response type (default: "json")
695
- * @returns Promise resolving to the response data
530
+ * Performs an HTTP GET request.
531
+ * @param req The request object containing URL and headers
532
+ * @param responseType Optional response parsing type
533
+ * @returns A promise that resolves to the parsed response
534
+ * @template T The expected response type
696
535
  */
697
536
  get(req, responseType = "json") {
698
537
  if (typeof req === "string") {
@@ -708,13 +547,11 @@ class HttpClientImproved {
708
547
  }
709
548
  }
710
549
  /**
711
- * Makes an HTTP POST request.
712
- * Supports both RequestInterface objects and direct URL strings with body data.
713
- * POST requests are not cached by default due to their side effects.
714
- * @param req - Request configuration or URL string
715
- * @param body - Request body data (optional)
716
- * @param responseType - Expected response type (default: "json")
717
- * @returns Promise resolving to the response data
550
+ * Performs an HTTP POST request.
551
+ * @param req The request object containing URL, body, and headers
552
+ * @param responseType Optional response parsing type
553
+ * @returns A promise that resolves to the parsed response
554
+ * @template T The expected response type
718
555
  */
719
556
  post(req, body, responseType = "json") {
720
557
  if (typeof req === "string") {
@@ -730,13 +567,11 @@ class HttpClientImproved {
730
567
  }
731
568
  }
732
569
  /**
733
- * Makes an HTTP PUT request.
734
- * Supports both RequestInterface objects and direct URL strings with body data.
735
- * PUT requests are not cached by default due to their side effects.
736
- * @param req - Request configuration or URL string
737
- * @param body - Request body data (optional)
738
- * @param responseType - Expected response type (default: "json")
739
- * @returns Promise resolving to the response data
570
+ * Performs an HTTP PUT request.
571
+ * @param req The request object containing URL, body, and headers
572
+ * @param responseType Optional response parsing type
573
+ * @returns A promise that resolves to the parsed response
574
+ * @template T The expected response type
740
575
  */
741
576
  put(req, body, responseType = "json") {
742
577
  if (typeof req === "string") {
@@ -750,70 +585,94 @@ class HttpClientImproved {
750
585
  return this.requestInternal("PUT", req, false, responseType);
751
586
  }
752
587
  /**
753
- * @ru Получает потоковый ответ (для SSE, больших файлов).
754
- * @en Gets streaming response (for SSE, large files).
588
+ * Performs an HTTP DELETE request.
589
+ * @param req The request object containing URL and headers
590
+ * @param responseType Optional response parsing type
591
+ * @returns A promise that resolves to the parsed response
592
+ * @template T The expected response type
755
593
  */
756
- stream(req) {
594
+ delete(req, responseType = "json") {
757
595
  if (typeof req === "string") {
596
+ const client = new HttpClientImproved();
758
597
  const simpleReq = {
759
598
  getURL: () => req,
760
599
  getBodyData: () => undefined,
761
600
  getHeaders: () => ({}),
762
601
  };
763
- return this.stream(simpleReq);
602
+ return client.delete(simpleReq, responseType);
764
603
  }
765
- return this.queue.enqueue(async function () {
766
- const url = req.getURL();
767
- const headers = { ...this.defaultHeaders, ...req.getHeaders() };
768
- const response = await (0, undici_1.request)(url, {
769
- method: "GET",
770
- headers,
771
- dispatcher: this.agent,
772
- });
773
- return {
774
- status: response.statusCode,
775
- headers: response.headers,
776
- body: response.body,
777
- url,
778
- };
779
- }.bind(this));
604
+ return this.requestInternal("DELETE", req, false, responseType);
780
605
  }
781
606
  /**
782
- * Makes an HTTP DELETE request.
783
- * Supports both RequestInterface objects and direct URL strings.
784
- * DELETE requests are not cached by default due to their side effects.
785
- * @param req - Request configuration or URL string
786
- * @param responseType - Expected response type (default: "json")
787
- * @returns Promise resolving to the response data
607
+ * Performs an HTTP PATCH request.
608
+ * @param req The request object containing URL, body, and headers
609
+ * @param responseType Optional response parsing type
610
+ * @returns A promise that resolves to the parsed response
611
+ * @template T The expected response type
788
612
  */
789
- delete(req, responseType = "json") {
613
+ patch(req, body, responseType = "json") {
790
614
  if (typeof req === "string") {
791
- const client = defaultClient ?? (defaultClient = new HttpClientImproved());
792
615
  const simpleReq = {
793
616
  getURL: () => req,
794
- getBodyData: () => undefined,
795
- getHeaders: () => ({}),
617
+ getBodyData: () => body,
618
+ getHeaders: () => ({ "Content-Type": "application/json" }),
796
619
  };
797
- return client.delete(simpleReq, responseType);
620
+ return this.requestInternal("PATCH", simpleReq, false, responseType);
798
621
  }
799
- return this.requestInternal("DELETE", req, false, responseType);
622
+ return this.requestInternal("PATCH", req, false, responseType);
800
623
  }
801
624
  /**
802
- * Makes an HTTP PATCH request.
803
- * PATCH requests are not cached by default due to their side effects.
804
- * @param req - Request configuration
805
- * @param responseType - Expected response type (default: "json")
806
- * @returns Promise resolving to the response data
625
+ * @ru Получает потоковый ответ (для SSE, больших файлов).
626
+ * @en Gets streaming response (for SSE, large files).
807
627
  */
808
- patch(req, responseType = "json") {
809
- return this.requestInternal("PATCH", req, false, responseType);
628
+ stream(req) {
629
+ if (typeof req === "string") {
630
+ const simpleReq = {
631
+ getURL: () => req,
632
+ getBodyData: () => undefined,
633
+ getHeaders: () => ({}),
634
+ };
635
+ return this.stream(simpleReq);
636
+ }
637
+ return (this.queue && this.options.enableQueue
638
+ ? this.queue.enqueue(async function () {
639
+ const url = req.getURL();
640
+ const headers = {
641
+ ...this.defaultHeaders,
642
+ ...req.getHeaders(),
643
+ };
644
+ const response = await (0, undici_1.request)(url, {
645
+ method: "GET",
646
+ headers,
647
+ dispatcher: this.agent,
648
+ });
649
+ return {
650
+ status: response.statusCode,
651
+ headers: response.headers,
652
+ body: response.body,
653
+ url,
654
+ };
655
+ }.bind(this))
656
+ : async function () {
657
+ const url = req.getURL();
658
+ const headers = Object.assign({}, this.defaultHeaders, req.getHeaders());
659
+ const response = await (0, undici_1.request)(url, {
660
+ method: "GET",
661
+ headers,
662
+ dispatcher: this.agent,
663
+ });
664
+ return {
665
+ status: response.statusCode,
666
+ headers: response.headers,
667
+ body: response.body,
668
+ url,
669
+ };
670
+ }.bind(this)());
810
671
  }
811
672
  /**
812
- * Makes an HTTP HEAD request.
813
- * Returns only the status code and headers without the response body.
814
- * HEAD requests are not cached by default.
815
- * @param req - Request configuration or URL string
816
- * @returns Promise resolving to status and headers
673
+ * Performs an HTTP HEAD request.
674
+ * @param req The request object containing URL and headers
675
+ * @returns A promise that resolves when the request completes
817
676
  */
818
677
  async head(req) {
819
678
  if (typeof req === "string") {
@@ -827,12 +686,11 @@ class HttpClientImproved {
827
686
  return this.requestInternal("HEAD", req, false);
828
687
  }
829
688
  /**
830
- * Clears the internal cache of the HTTP client.
831
- * Removes all cached responses and resets the cache state.
689
+ * Clears the request cache.
832
690
  */
833
- clearCache() {
691
+ async clearCache() {
834
692
  if (this.cache) {
835
- this.cache.clear();
693
+ await this.cache.clear();
836
694
  this.log("info", "Cache cleared");
837
695
  }
838
696
  }
@@ -866,187 +724,27 @@ class HttpClientImproved {
866
724
  * @returns RequestBuilder instance for chaining
867
725
  */
868
726
  request(url) {
869
- return new RequestBuilder(url);
727
+ return new RequestBuilder_1.RequestBuilder(url);
870
728
  }
871
729
  /**
872
730
  * Returns current statistics about the HTTP client's state.
873
- * Useful for monitoring and debugging performance.
874
- * @returns Object containing various client statistics
731
+ * @returns An object containing cache size, request counts, and rate limit information
875
732
  */
876
733
  getStats() {
877
734
  return {
878
735
  cacheSize: this.cache?.size ?? 0,
879
736
  inflightRequests: this.inflight.size,
880
- queuedRequests: this.queue.queuedCount ?? 0,
881
- activeRequests: this.queue.activeCount ?? 0,
882
- currentRateLimit: this.limiter?.currentCount ?? 0,
883
- metricsSize: this.requestMetrics.size,
737
+ queuedRequests: this.queue && this.options.enableQueue
738
+ ? (this.queue.queuedCount ?? 0)
739
+ : 0,
740
+ activeRequests: this.queue && this.options.enableQueue
741
+ ? (this.queue.activeCount ?? 0)
742
+ : 0,
743
+ currentRateLimit: this.limiter && this.options.enableRateLimit
744
+ ? (this.limiter.currentCount ?? 0)
745
+ : 0,
884
746
  };
885
747
  }
886
748
  }
887
749
  exports.default = HttpClientImproved;
888
- /**
889
- * Fluent request builder for making HTTP requests with a chainable API.
890
- * Provides a convenient way to build and send HTTP requests with various options.
891
- *
892
- * @example
893
- * ```ts
894
- * const client = new HttpClientImproved();
895
- * const response = await client.request('https://api.example.com/data')
896
- * .headers({ 'Authorization': 'Bearer token' })
897
- * .query({ limit: 10, offset: 0 })
898
- * .json()
899
- * .send();
900
- * ```
901
- */
902
- class RequestBuilder {
903
- _url;
904
- _method = "GET";
905
- _headers = {};
906
- _body;
907
- _responseType = "json";
908
- /**
909
- * Creates a new request builder for the specified URL.
910
- * @param url - The target URL for the request
911
- */
912
- constructor(url) {
913
- this._url = url;
914
- }
915
- /**
916
- * Sets HTTP headers for the request.
917
- * @param headers - Object containing header key-value pairs
918
- * @returns The builder instance for chaining
919
- */
920
- headers(headers) {
921
- this._headers = headers;
922
- return this;
923
- }
924
- /**
925
- * Sets the request body data.
926
- * @param bodyData - The body data to send with the request
927
- * @returns The builder instance for chaining
928
- */
929
- body(bodyData) {
930
- this._body = bodyData;
931
- return this;
932
- }
933
- /**
934
- * Sets the response type to JSON.
935
- * @returns The builder instance for chaining
936
- */
937
- json() {
938
- this._responseType = "json";
939
- return this;
940
- }
941
- /**
942
- * Sets the response type to plain text.
943
- * @returns The builder instance for chaining
944
- */
945
- text() {
946
- this._responseType = "text";
947
- return this;
948
- }
949
- /**
950
- * Sets the response type to XML.
951
- * @returns The builder instance for chaining
952
- */
953
- xml() {
954
- this._responseType = "xml";
955
- return this;
956
- }
957
- /**
958
- * Sets the HTTP method to POST.
959
- * @returns The builder instance for chaining
960
- */
961
- post() {
962
- this._method = "POST";
963
- return this;
964
- }
965
- /**
966
- * @ru Устанавливает потоковый режим ответа.
967
- * @en Sets streaming response mode.
968
- */
969
- stream() {
970
- this._responseType = "stream";
971
- return this;
972
- }
973
- /**
974
- * Sets the HTTP method to PUT.
975
- * @returns The builder instance for chaining
976
- */
977
- put() {
978
- this._method = "PUT";
979
- return this;
980
- }
981
- /**
982
- * Sets the HTTP method to PATCH.
983
- * @returns The builder instance for chaining
984
- */
985
- patch() {
986
- this._method = "PATCH";
987
- return this;
988
- }
989
- /**
990
- * Sets the HTTP method to DELETE.
991
- * @returns The builder instance for chaining
992
- */
993
- delete() {
994
- this._method = "DELETE";
995
- return this;
996
- }
997
- /**
998
- * Adds query parameters to the URL.
999
- * @param params - Object containing query parameter key-value pairs
1000
- * @returns The builder instance for chaining
1001
- */
1002
- query(params) {
1003
- const urlObj = new URL(this._url);
1004
- Object.entries(params).forEach(([k, v]) => urlObj.searchParams.set(k, String(v)));
1005
- this._url = urlObj.toString();
1006
- return this;
1007
- }
1008
- /**
1009
- * Sets a JSON body for the request.
1010
- * Automatically sets the Content-Type header to application/json.
1011
- * @param body - The JSON body data
1012
- * @returns The builder instance for chaining
1013
- */
1014
- jsonBody(body) {
1015
- this._body = body;
1016
- this._headers["Content-Type"] = "application/json; charset=utf-8";
1017
- return this;
1018
- }
1019
- /**
1020
- * Sends the HTTP request and returns the response.
1021
- * @returns Promise resolving to the response data
1022
- */
1023
- async send() {
1024
- const client = defaultClient ?? (defaultClient = new HttpClientImproved());
1025
- const req = {
1026
- getURL: () => this._url,
1027
- getBodyData: () => this._body,
1028
- getHeaders: () => this._headers,
1029
- };
1030
- switch (this._method) {
1031
- case "GET":
1032
- if (this._responseType === "stream") {
1033
- return client.stream(req);
1034
- }
1035
- return client.get(req, this._responseType);
1036
- case "POST":
1037
- return client.post(req, undefined, this._responseType);
1038
- case "PUT":
1039
- return client.put(req, this._responseType);
1040
- case "DELETE":
1041
- return client.delete(req, this._responseType);
1042
- case "PATCH":
1043
- return client.patch(req, this._responseType);
1044
- default:
1045
- if (this._responseType === "stream") {
1046
- return client.stream(req);
1047
- }
1048
- return client.get(req, this._responseType);
1049
- }
1050
- }
1051
- }
1052
750
  //# sourceMappingURL=HttpClientImproved.js.map