hyperttp 0.1.5 → 0.1.7

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