express-api-stress-tester 2.0.4 → 2.0.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/package.json +1 -1
- package/src/core/httpEngine.js +64 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-api-stress-tester",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "High-performance distributed API stress testing platform for Express.js APIs — simulate up to 10M concurrent virtual users",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/core/httpEngine.js
CHANGED
|
@@ -4,7 +4,31 @@
|
|
|
4
4
|
* Provides keep-alive, pipelining, and precise response-time tracking
|
|
5
5
|
* via process.hrtime.bigint().
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
async function ensureWebStreamsGlobals() {
|
|
8
|
+
if (typeof globalThis.ReadableStream !== 'undefined') return;
|
|
9
|
+
try {
|
|
10
|
+
const web = await import('node:stream/web');
|
|
11
|
+
if (web.ReadableStream && typeof globalThis.ReadableStream === 'undefined') {
|
|
12
|
+
globalThis.ReadableStream = web.ReadableStream;
|
|
13
|
+
}
|
|
14
|
+
if (web.WritableStream && typeof globalThis.WritableStream === 'undefined') {
|
|
15
|
+
globalThis.WritableStream = web.WritableStream;
|
|
16
|
+
}
|
|
17
|
+
if (web.TransformStream && typeof globalThis.TransformStream === 'undefined') {
|
|
18
|
+
globalThis.TransformStream = web.TransformStream;
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
// ignore
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cachedUndici = null;
|
|
26
|
+
async function getUndici() {
|
|
27
|
+
if (cachedUndici) return cachedUndici;
|
|
28
|
+
await ensureWebStreamsGlobals();
|
|
29
|
+
cachedUndici = await import('undici');
|
|
30
|
+
return cachedUndici;
|
|
31
|
+
}
|
|
8
32
|
|
|
9
33
|
const CONTROL_CHARS_REGEX = /[\0\r\n]/g;
|
|
10
34
|
const MAX_WARNED_HEADER_VALUES = 100;
|
|
@@ -36,14 +60,28 @@ export class HttpEngine {
|
|
|
36
60
|
this.timeout = timeout;
|
|
37
61
|
this.invalidHeaderWarningCache = { map: new Map(), queue: [] };
|
|
38
62
|
|
|
39
|
-
this.pool =
|
|
63
|
+
this.pool = null;
|
|
64
|
+
this.poolPromise = null;
|
|
65
|
+
this.poolOptions = {
|
|
40
66
|
connections,
|
|
41
67
|
pipelining,
|
|
42
68
|
keepAliveTimeout: 30_000,
|
|
43
69
|
keepAliveMaxTimeout: 60_000,
|
|
44
70
|
headersTimeout: timeout,
|
|
45
71
|
bodyTimeout: timeout,
|
|
46
|
-
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async ensurePool() {
|
|
76
|
+
if (this.pool) return this.pool;
|
|
77
|
+
if (!this.poolPromise) {
|
|
78
|
+
this.poolPromise = (async () => {
|
|
79
|
+
const { Pool } = await getUndici();
|
|
80
|
+
this.pool = new Pool(this.baseUrl, this.poolOptions);
|
|
81
|
+
return this.pool;
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
84
|
+
return this.poolPromise;
|
|
47
85
|
}
|
|
48
86
|
|
|
49
87
|
/**
|
|
@@ -57,6 +95,7 @@ export class HttpEngine {
|
|
|
57
95
|
* @returns {Promise<{ statusCode: number, headers: object, body: string, responseTime: number }>}
|
|
58
96
|
*/
|
|
59
97
|
async request({ method = 'GET', path = '/', headers = {}, body = null } = {}) {
|
|
98
|
+
const pool = await this.ensurePool();
|
|
60
99
|
const mergedHeaders = normalizeHeaders(
|
|
61
100
|
{ ...this.defaultHeaders, ...headers },
|
|
62
101
|
this.invalidHeaderWarningCache,
|
|
@@ -64,7 +103,7 @@ export class HttpEngine {
|
|
|
64
103
|
|
|
65
104
|
const start = process.hrtime.bigint();
|
|
66
105
|
|
|
67
|
-
const { statusCode, headers: resHeaders, body: resBody } = await
|
|
106
|
+
const { statusCode, headers: resHeaders, body: resBody } = await pool.request({
|
|
68
107
|
method: method.toUpperCase(),
|
|
69
108
|
path,
|
|
70
109
|
headers: mergedHeaders,
|
|
@@ -74,7 +113,16 @@ export class HttpEngine {
|
|
|
74
113
|
});
|
|
75
114
|
|
|
76
115
|
// Consume the body fully (undici requirement to free the socket)
|
|
77
|
-
|
|
116
|
+
// Drain via async iteration to avoid depending on Web Streams globals.
|
|
117
|
+
if (resBody) {
|
|
118
|
+
try {
|
|
119
|
+
for await (const _chunk of resBody) {
|
|
120
|
+
// discard
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// ignore body drain errors
|
|
124
|
+
}
|
|
125
|
+
}
|
|
78
126
|
|
|
79
127
|
const end = process.hrtime.bigint();
|
|
80
128
|
// Convert nanoseconds → milliseconds (floating point)
|
|
@@ -83,7 +131,7 @@ export class HttpEngine {
|
|
|
83
131
|
return {
|
|
84
132
|
statusCode,
|
|
85
133
|
headers: resHeaders,
|
|
86
|
-
body:
|
|
134
|
+
body: '',
|
|
87
135
|
responseTime,
|
|
88
136
|
};
|
|
89
137
|
}
|
|
@@ -92,7 +140,16 @@ export class HttpEngine {
|
|
|
92
140
|
* Gracefully close the connection pool.
|
|
93
141
|
*/
|
|
94
142
|
async close() {
|
|
95
|
-
|
|
143
|
+
if (this.pool) {
|
|
144
|
+
await this.pool.close();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (this.poolPromise) {
|
|
148
|
+
const pool = await this.poolPromise;
|
|
149
|
+
if (pool && typeof pool.close === 'function') {
|
|
150
|
+
await pool.close();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
96
153
|
}
|
|
97
154
|
}
|
|
98
155
|
|