hyperttp 0.2.4 → 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 +307 -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 +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/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 -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 +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 +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 +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,134 +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,
|
|
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
|
});
|
|
72
96
|
this.defaultHeaders = {
|
|
73
97
|
Accept: "application/json, text/plain, */*",
|
|
74
98
|
"Accept-Encoding": "gzip, deflate, br",
|
|
75
|
-
"User-Agent": this.
|
|
99
|
+
"User-Agent": this.config.network?.userAgent,
|
|
76
100
|
};
|
|
77
101
|
}
|
|
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
102
|
/**
|
|
149
103
|
* @en Performs an HTTP GET request.
|
|
150
104
|
* @ru Выполняет HTTP GET запрос.
|
|
@@ -163,78 +117,62 @@ class HttpClientImproved {
|
|
|
163
117
|
* @param responseType Expected response format
|
|
164
118
|
*/
|
|
165
119
|
post(req, body, responseType = "auto") {
|
|
166
|
-
|
|
167
|
-
return this.requestInternal("POST", requestObj, false, responseType);
|
|
120
|
+
return this.requestInternal("POST", this.normalizeRequest(req, body), false, responseType);
|
|
168
121
|
}
|
|
169
122
|
/**
|
|
170
123
|
* @en Performs an HTTP PUT request.
|
|
171
124
|
* @ru Выполняет HTTP PUT запрос.
|
|
172
125
|
*/
|
|
173
126
|
put(req, body, responseType = "auto") {
|
|
174
|
-
|
|
175
|
-
return this.requestInternal("PUT", requestObj, false, responseType);
|
|
127
|
+
return this.requestInternal("PUT", this.normalizeRequest(req, body), false, responseType);
|
|
176
128
|
}
|
|
177
129
|
/**
|
|
178
130
|
* @en Performs an HTTP DELETE request.
|
|
179
131
|
* @ru Выполняет HTTP DELETE запрос.
|
|
180
132
|
*/
|
|
181
133
|
delete(req, responseType = "auto") {
|
|
182
|
-
|
|
183
|
-
return this.requestInternal("DELETE", requestObj, false, responseType);
|
|
134
|
+
return this.requestInternal("DELETE", this.normalizeRequest(req), false, responseType);
|
|
184
135
|
}
|
|
185
136
|
/**
|
|
186
137
|
* @en Performs an HTTP PATCH request.
|
|
187
138
|
* @ru Выполняет HTTP PATCH запрос.
|
|
188
139
|
*/
|
|
189
140
|
patch(req, body, responseType = "auto") {
|
|
190
|
-
|
|
191
|
-
return this.requestInternal("PATCH", requestObj, false, responseType);
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* @en Creates a RequestBuilder for a fluent API approach.
|
|
195
|
-
* @ru Создает RequestBuilder для использования Fluent API.
|
|
196
|
-
* @example client.request('url').get().send();
|
|
197
|
-
*/
|
|
198
|
-
request(url) {
|
|
199
|
-
return new RequestBuilder_js_1.RequestBuilder(url, this);
|
|
141
|
+
return this.requestInternal("PATCH", this.normalizeRequest(req, body), false, responseType);
|
|
200
142
|
}
|
|
201
143
|
/**
|
|
202
|
-
* @en
|
|
203
|
-
* @ru
|
|
144
|
+
* @en Performs an HTTP OPTIONS request.
|
|
145
|
+
* @ru Выполняет HTTP OPTIONS запрос.
|
|
204
146
|
*/
|
|
205
|
-
|
|
206
|
-
|
|
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();
|
|
147
|
+
options(req, body, responseType = "auto") {
|
|
148
|
+
return this.requestInternal("OPTIONS", this.normalizeRequest(req, body), false, responseType);
|
|
230
149
|
}
|
|
231
150
|
/**
|
|
232
151
|
* @en Performs an HTTP HEAD request.
|
|
233
152
|
* @ru Выполняет HTTP HEAD запрос.
|
|
234
153
|
*/
|
|
235
154
|
async head(req) {
|
|
236
|
-
|
|
237
|
-
|
|
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);
|
|
238
176
|
}
|
|
239
177
|
/**
|
|
240
178
|
* @en Executes a request and returns an AsyncIterable stream.
|
|
@@ -244,47 +182,57 @@ class HttpClientImproved {
|
|
|
244
182
|
const requestObj = this.normalizeRequest(req);
|
|
245
183
|
const url = requestObj.getURL();
|
|
246
184
|
const { body, headers } = this.prepareRequestData("GET", requestObj);
|
|
247
|
-
const
|
|
248
|
-
const internalController = new AbortController();
|
|
185
|
+
const controller = new AbortController();
|
|
249
186
|
const userSignal = requestObj.getSignal?.();
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
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();
|
|
253
219
|
}
|
|
220
|
+
this.inflight.clear();
|
|
254
221
|
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
|
-
};
|
|
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
|
+
}
|
|
273
228
|
}
|
|
274
|
-
catch
|
|
275
|
-
|
|
276
|
-
userSignal.removeEventListener("abort", abortHandler);
|
|
277
|
-
throw error;
|
|
229
|
+
catch {
|
|
230
|
+
/* ignore */
|
|
278
231
|
}
|
|
232
|
+
this.cache?.clear();
|
|
279
233
|
}
|
|
280
|
-
/**
|
|
281
|
-
* @en Clears the internal cache.
|
|
282
|
-
* @ru Полностью очищает внутренний кэш клиента.
|
|
283
|
-
*/
|
|
284
234
|
clearCache() {
|
|
285
|
-
|
|
286
|
-
this.cache.clear();
|
|
287
|
-
}
|
|
235
|
+
this.cache?.clear();
|
|
288
236
|
}
|
|
289
237
|
/**
|
|
290
238
|
* @en Clears all collected performance metrics.
|
|
@@ -292,7 +240,6 @@ class HttpClientImproved {
|
|
|
292
240
|
*/
|
|
293
241
|
clearMetrics() {
|
|
294
242
|
this.metricsManager.clear();
|
|
295
|
-
this.options.logger?.("info", "Metrics cleared");
|
|
296
243
|
}
|
|
297
244
|
/**
|
|
298
245
|
* @en Retrieves metrics for a specific URL.
|
|
@@ -317,17 +264,41 @@ class HttpClientImproved {
|
|
|
317
264
|
return {
|
|
318
265
|
cacheSize: this.cache?.size ?? 0,
|
|
319
266
|
inflightRequests: this.inflight.size,
|
|
320
|
-
queuedRequests: this.
|
|
267
|
+
queuedRequests: this.queueEnabled && this.queue
|
|
321
268
|
? (this.queue.queuedCount ?? 0)
|
|
322
269
|
: 0,
|
|
323
|
-
activeRequests: this.
|
|
270
|
+
activeRequests: this.queueEnabled && this.queue
|
|
324
271
|
? (this.queue.activeCount ?? 0)
|
|
325
272
|
: 0,
|
|
326
|
-
currentRateLimit: this.
|
|
273
|
+
currentRateLimit: this.limiterEnabled && this.limiter
|
|
327
274
|
? (this.limiter.currentCount ?? 0)
|
|
328
275
|
: 0,
|
|
329
276
|
};
|
|
330
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
|
+
}
|
|
331
302
|
normalizeRequest(req, body) {
|
|
332
303
|
if (typeof req === "string") {
|
|
333
304
|
return {
|
|
@@ -338,35 +309,161 @@ class HttpClientImproved {
|
|
|
338
309
|
}
|
|
339
310
|
return req;
|
|
340
311
|
}
|
|
341
|
-
|
|
312
|
+
applyDefaulthcoptions(opt) {
|
|
342
313
|
const defaults = {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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: {
|
|
356
333
|
maxRetries: 3,
|
|
357
334
|
baseDelay: 1000,
|
|
358
335
|
maxDelay: 10000,
|
|
359
336
|
retryStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
360
337
|
jitter: true,
|
|
361
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
|
+
},
|
|
362
371
|
};
|
|
363
|
-
|
|
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;
|
|
364
462
|
}
|
|
365
463
|
prepareRequestData(method, req) {
|
|
366
464
|
const headers = { ...this.defaultHeaders, ...req.getHeaders() };
|
|
367
465
|
let rawBody = req.getBodyData();
|
|
368
|
-
|
|
369
|
-
if (!methodsWithBody.includes(method)) {
|
|
466
|
+
if (!["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
|
370
467
|
return { body: undefined, headers };
|
|
371
468
|
}
|
|
372
469
|
if (rawBody &&
|
|
@@ -378,8 +475,7 @@ class HttpClientImproved {
|
|
|
378
475
|
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
379
476
|
const params = new URLSearchParams();
|
|
380
477
|
for (const [key, value] of Object.entries(rawBody)) {
|
|
381
|
-
|
|
382
|
-
params.append(key, finalValue);
|
|
478
|
+
params.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
383
479
|
}
|
|
384
480
|
rawBody = params.toString();
|
|
385
481
|
}
|
|
@@ -413,8 +509,8 @@ class HttpClientImproved {
|
|
|
413
509
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
414
510
|
metrics.statusCode = status;
|
|
415
511
|
this.metricsManager.record(metrics);
|
|
416
|
-
if (this.
|
|
417
|
-
this.
|
|
512
|
+
if (this.verboseEnabled) {
|
|
513
|
+
this.config.logger?.("info", `Request successful: ${metrics.method} ${metrics.url}`, {
|
|
418
514
|
duration: metrics.duration,
|
|
419
515
|
status: metrics.statusCode,
|
|
420
516
|
});
|
|
@@ -423,12 +519,12 @@ class HttpClientImproved {
|
|
|
423
519
|
recordError(metrics, error) {
|
|
424
520
|
metrics.endTime = Date.now();
|
|
425
521
|
metrics.duration = metrics.endTime - metrics.startTime;
|
|
426
|
-
metrics.statusCode = error
|
|
522
|
+
metrics.statusCode = error?.statusCode || 0;
|
|
427
523
|
this.metricsManager.record(metrics);
|
|
428
|
-
if (this.
|
|
429
|
-
this.
|
|
430
|
-
error: error
|
|
431
|
-
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,
|
|
432
528
|
});
|
|
433
529
|
}
|
|
434
530
|
}
|