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