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