qhttpx 1.8.5 → 1.8.11
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/CHANGELOG.md +52 -0
- package/README.md +72 -52
- package/binding.gyp +18 -0
- package/dist/examples/api-server.js +29 -8
- package/dist/examples/basic.d.ts +1 -0
- package/dist/examples/basic.js +10 -0
- package/dist/examples/compression.d.ts +1 -0
- package/dist/examples/compression.js +17 -0
- package/dist/examples/cors.d.ts +1 -0
- package/dist/examples/cors.js +19 -0
- package/dist/examples/errors.d.ts +1 -0
- package/dist/examples/errors.js +25 -0
- package/dist/examples/file-upload.d.ts +1 -0
- package/dist/examples/file-upload.js +24 -0
- package/dist/examples/fusion.d.ts +1 -0
- package/dist/examples/fusion.js +21 -0
- package/dist/examples/rate-limiting.d.ts +1 -0
- package/dist/examples/rate-limiting.js +17 -0
- package/dist/examples/validation.d.ts +1 -0
- package/dist/examples/validation.js +23 -0
- package/dist/examples/websockets.d.ts +1 -0
- package/dist/examples/websockets.js +20 -0
- package/dist/package.json +11 -1
- package/dist/src/benchmarks/simple-json.js +6 -4
- package/dist/src/cli/index.js +33 -11
- package/dist/src/core/errors.d.ts +34 -0
- package/dist/src/core/errors.js +70 -0
- package/dist/src/core/native-adapter.d.ts +11 -0
- package/dist/src/core/native-adapter.js +211 -0
- package/dist/src/core/server.d.ts +52 -4
- package/dist/src/core/server.js +389 -261
- package/dist/src/core/types.d.ts +37 -0
- package/dist/src/index.d.ts +6 -1
- package/dist/src/index.js +19 -3
- package/dist/src/middleware/compression.d.ts +1 -5
- package/dist/src/middleware/cors.d.ts +1 -10
- package/dist/src/middleware/presets.d.ts +4 -1
- package/dist/src/middleware/presets.js +22 -3
- package/dist/src/middleware/rate-limit.d.ts +1 -19
- package/dist/src/middleware/rate-limit.js +6 -0
- package/dist/src/middleware/security.d.ts +1 -2
- package/dist/src/native/index.d.ts +29 -0
- package/dist/src/native/index.js +64 -0
- package/dist/src/router/radix-tree.d.ts +2 -0
- package/dist/src/router/radix-tree.js +54 -4
- package/dist/src/router/router.d.ts +1 -0
- package/dist/src/router/router.js +42 -2
- package/dist/tests/native-adapter.test.d.ts +1 -0
- package/dist/tests/native-adapter.test.js +71 -0
- package/dist/tests/resources.test.js +3 -0
- package/dist/tests/security.test.js +2 -2
- package/docs/AEGIS.md +34 -9
- package/docs/BENCHMARKS.md +8 -7
- package/docs/ERRORS.md +112 -0
- package/docs/FUSION.md +68 -0
- package/docs/MIDDLEWARE.md +65 -0
- package/docs/ROUTING.md +70 -0
- package/docs/STATIC.md +61 -0
- package/docs/WEBSOCKETS.md +76 -0
- package/package.json +11 -1
- package/src/native/README.md +31 -0
- package/src/native/addon.cc +8 -0
- package/src/native/index.ts +78 -0
- package/src/native/picohttpparser.c +608 -0
- package/src/native/picohttpparser.h +71 -0
- package/src/native/server.cc +262 -0
- package/src/native/server.h +30 -0
- package/.eslintrc.json +0 -22
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/npm-publish.yml +0 -37
- package/.github/workflows/release.yml +0 -21
- package/.prettierrc +0 -7
- package/assets/logo.svg +0 -25
- package/eslint.config.cjs +0 -26
- package/examples/api-server.ts +0 -62
- package/src/benchmarks/quantam-users.ts +0 -70
- package/src/benchmarks/simple-json.ts +0 -71
- package/src/benchmarks/ultra-mode.ts +0 -127
- package/src/cli/index.ts +0 -214
- package/src/client/index.ts +0 -93
- package/src/core/batch.ts +0 -110
- package/src/core/body-parser.ts +0 -151
- package/src/core/buffer-pool.ts +0 -96
- package/src/core/config.ts +0 -60
- package/src/core/fusion.ts +0 -210
- package/src/core/logger.ts +0 -70
- package/src/core/metrics.ts +0 -166
- package/src/core/resources.ts +0 -38
- package/src/core/scheduler.ts +0 -126
- package/src/core/scope.ts +0 -87
- package/src/core/serializer.ts +0 -41
- package/src/core/server.ts +0 -1234
- package/src/core/stream.ts +0 -111
- package/src/core/tasks.ts +0 -138
- package/src/core/types.ts +0 -192
- package/src/core/websocket.ts +0 -112
- package/src/core/worker-queue.ts +0 -90
- package/src/database/adapters/memory.ts +0 -99
- package/src/database/adapters/mongo.ts +0 -116
- package/src/database/adapters/postgres.ts +0 -86
- package/src/database/adapters/sqlite.ts +0 -44
- package/src/database/coalescer.ts +0 -153
- package/src/database/manager.ts +0 -97
- package/src/database/types.ts +0 -24
- package/src/index.ts +0 -58
- package/src/middleware/compression.ts +0 -147
- package/src/middleware/cors.ts +0 -98
- package/src/middleware/presets.ts +0 -50
- package/src/middleware/rate-limit.ts +0 -106
- package/src/middleware/security.ts +0 -109
- package/src/middleware/static.ts +0 -216
- package/src/openapi/generator.ts +0 -167
- package/src/router/radix-router.ts +0 -119
- package/src/router/radix-tree.ts +0 -106
- package/src/router/router.ts +0 -190
- package/src/testing/index.ts +0 -104
- package/src/utils/cookies.ts +0 -67
- package/src/utils/logger.ts +0 -59
- package/src/utils/signals.ts +0 -45
- package/src/utils/sse.ts +0 -41
- package/src/validation/index.ts +0 -3
- package/src/validation/simple.ts +0 -93
- package/src/validation/types.ts +0 -38
- package/src/validation/zod.ts +0 -14
- package/src/views/index.ts +0 -1
- package/src/views/types.ts +0 -4
- package/tests/adapters.test.ts +0 -120
- package/tests/batch.test.ts +0 -139
- package/tests/body-parser.test.ts +0 -83
- package/tests/compression-sse.test.ts +0 -98
- package/tests/cookies.test.ts +0 -74
- package/tests/cors.test.ts +0 -79
- package/tests/database.test.ts +0 -90
- package/tests/dx.test.ts +0 -130
- package/tests/ecosystem.test.ts +0 -156
- package/tests/features.test.ts +0 -51
- package/tests/fusion.test.ts +0 -121
- package/tests/http-basic.test.ts +0 -161
- package/tests/logger.test.ts +0 -48
- package/tests/middleware.test.ts +0 -137
- package/tests/observability.test.ts +0 -91
- package/tests/openapi.test.ts +0 -74
- package/tests/plugin.test.ts +0 -85
- package/tests/plugins.test.ts +0 -93
- package/tests/rate-limit.test.ts +0 -97
- package/tests/resources.test.ts +0 -64
- package/tests/scheduler.test.ts +0 -71
- package/tests/schema-routes.test.ts +0 -89
- package/tests/security.test.ts +0 -128
- package/tests/server-db.test.ts +0 -72
- package/tests/smoke.test.ts +0 -9
- package/tests/sqlite-fusion.test.ts +0 -106
- package/tests/static.test.ts +0 -111
- package/tests/stream.test.ts +0 -58
- package/tests/task-metrics.test.ts +0 -78
- package/tests/tasks.test.ts +0 -90
- package/tests/testing.test.ts +0 -53
- package/tests/validation.test.ts +0 -126
- package/tests/websocket.test.ts +0 -132
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -9
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#include "server.h"
|
|
2
|
+
#include "picohttpparser.h"
|
|
3
|
+
#include <iostream>
|
|
4
|
+
|
|
5
|
+
#ifdef _WIN32
|
|
6
|
+
#include <io.h>
|
|
7
|
+
#include <windows.h>
|
|
8
|
+
#else
|
|
9
|
+
#include <unistd.h>
|
|
10
|
+
#include <sys/uio.h>
|
|
11
|
+
#include <sched.h>
|
|
12
|
+
#include <pthread.h>
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
Napi::Object NativeServer::Init(Napi::Env env, Napi::Object exports) {
|
|
16
|
+
Napi::Function func = DefineClass(env, "NativeServer", {
|
|
17
|
+
InstanceMethod("parse", &NativeServer::Parse),
|
|
18
|
+
InstanceMethod("createResponse", &NativeServer::CreateResponse),
|
|
19
|
+
InstanceMethod("createJSONResponse", &NativeServer::CreateJSONResponse),
|
|
20
|
+
InstanceMethod("writeResponse", &NativeServer::WriteResponse),
|
|
21
|
+
InstanceMethod("setCPUAffinity", &NativeServer::SetCPUAffinity),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
Napi::FunctionReference* constructor = new Napi::FunctionReference();
|
|
25
|
+
*constructor = Napi::Persistent(func);
|
|
26
|
+
env.SetInstanceData(constructor);
|
|
27
|
+
|
|
28
|
+
exports.Set("NativeServer", func);
|
|
29
|
+
return exports;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
NativeServer::NativeServer(const Napi::CallbackInfo& info) : Napi::ObjectWrap<NativeServer>(info) {
|
|
33
|
+
// Constructor logic if needed
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parses HTTP request buffer
|
|
37
|
+
// Returns object: { method, path, headers, version, bodyOffset }
|
|
38
|
+
Napi::Value NativeServer::Parse(const Napi::CallbackInfo& info) {
|
|
39
|
+
Napi::Env env = info.Env();
|
|
40
|
+
|
|
41
|
+
if (info.Length() < 1 || !info[0].IsBuffer()) {
|
|
42
|
+
Napi::TypeError::New(env, "Buffer expected").ThrowAsJavaScriptException();
|
|
43
|
+
return env.Null();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Napi::Buffer<char> buffer = info[0].As<Napi::Buffer<char>>();
|
|
47
|
+
const char* buf = buffer.Data();
|
|
48
|
+
size_t len = buffer.Length();
|
|
49
|
+
|
|
50
|
+
const char *method, *path;
|
|
51
|
+
size_t method_len, path_len;
|
|
52
|
+
int minor_version;
|
|
53
|
+
struct phr_header headers[100];
|
|
54
|
+
size_t num_headers = sizeof(headers) / sizeof(headers[0]);
|
|
55
|
+
|
|
56
|
+
int ret = phr_parse_request(buf, len, &method, &method_len, &path, &path_len, &minor_version, headers, &num_headers, 0);
|
|
57
|
+
|
|
58
|
+
if (ret > 0) {
|
|
59
|
+
Napi::Object result = Napi::Object::New(env);
|
|
60
|
+
result.Set("method", Napi::String::New(env, method, method_len));
|
|
61
|
+
result.Set("path", Napi::String::New(env, path, path_len));
|
|
62
|
+
result.Set("version", Napi::Number::New(env, minor_version));
|
|
63
|
+
result.Set("bodyOffset", Napi::Number::New(env, ret));
|
|
64
|
+
|
|
65
|
+
Napi::Object headersObj = Napi::Object::New(env);
|
|
66
|
+
for (size_t i = 0; i < num_headers; ++i) {
|
|
67
|
+
// Lowercase header names? Typically yes for Node.js compat
|
|
68
|
+
// For now, we just pass raw. Optimization: In C++ we could lowercase here.
|
|
69
|
+
std::string name(headers[i].name, headers[i].name_len);
|
|
70
|
+
// Simple lowercase
|
|
71
|
+
for(auto& c : name) c = tolower(c);
|
|
72
|
+
|
|
73
|
+
Napi::String key = Napi::String::New(env, name);
|
|
74
|
+
Napi::String value = Napi::String::New(env, headers[i].value, headers[i].value_len);
|
|
75
|
+
headersObj.Set(key, value);
|
|
76
|
+
}
|
|
77
|
+
result.Set("headers", headersObj);
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
} else if (ret == -1) {
|
|
81
|
+
// Parse error
|
|
82
|
+
return env.Null();
|
|
83
|
+
} else {
|
|
84
|
+
// Partial (ret == -2)
|
|
85
|
+
return env.Undefined();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fast Response Builder
|
|
90
|
+
// createResponse(statusCode, headers, body) -> Buffer
|
|
91
|
+
Napi::Value NativeServer::CreateResponse(const Napi::CallbackInfo& info) {
|
|
92
|
+
Napi::Env env = info.Env();
|
|
93
|
+
|
|
94
|
+
int statusCode = 200;
|
|
95
|
+
if (info.Length() > 0 && info[0].IsNumber()) {
|
|
96
|
+
statusCode = info[0].As<Napi::Number>().Int32Value();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
std::string statusMsg = "OK";
|
|
100
|
+
if (statusCode == 404) statusMsg = "Not Found";
|
|
101
|
+
else if (statusCode == 500) statusMsg = "Internal Server Error";
|
|
102
|
+
|
|
103
|
+
std::string response;
|
|
104
|
+
response.reserve(1024);
|
|
105
|
+
|
|
106
|
+
response += "HTTP/1.1 " + std::to_string(statusCode) + " " + statusMsg + "\r\n";
|
|
107
|
+
response += "Connection: keep-alive\r\n";
|
|
108
|
+
response += "Date: Mon, 27 Jul 2029 12:28:53 GMT\r\n"; // Mock date for speed, real impl needs date cache
|
|
109
|
+
|
|
110
|
+
if (info.Length() > 1 && info[1].IsObject()) {
|
|
111
|
+
Napi::Object headers = info[1].As<Napi::Object>();
|
|
112
|
+
Napi::Array props = headers.GetPropertyNames();
|
|
113
|
+
uint32_t len = props.Length();
|
|
114
|
+
for (uint32_t i = 0; i < len; ++i) {
|
|
115
|
+
Napi::Value key = props.Get(i);
|
|
116
|
+
Napi::Value val = headers.Get(key);
|
|
117
|
+
response += key.ToString().Utf8Value() + ": " + val.ToString().Utf8Value() + "\r\n";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
std::string body = "";
|
|
122
|
+
if (info.Length() > 2) {
|
|
123
|
+
if (info[2].IsString()) {
|
|
124
|
+
body = info[2].As<Napi::String>().Utf8Value();
|
|
125
|
+
} else if (info[2].IsBuffer()) {
|
|
126
|
+
Napi::Buffer<char> b = info[2].As<Napi::Buffer<char>>();
|
|
127
|
+
body.assign(b.Data(), b.Length());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
response += "Content-Length: " + std::to_string(body.length()) + "\r\n";
|
|
132
|
+
response += "\r\n";
|
|
133
|
+
response += body;
|
|
134
|
+
|
|
135
|
+
return Napi::Buffer<char>::Copy(env, response.data(), response.length());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// createJSONResponse(obj) -> Buffer
|
|
139
|
+
Napi::Value NativeServer::CreateJSONResponse(const Napi::CallbackInfo& info) {
|
|
140
|
+
Napi::Env env = info.Env();
|
|
141
|
+
|
|
142
|
+
if (info.Length() < 1) {
|
|
143
|
+
Napi::TypeError::New(env, "Object expected").ThrowAsJavaScriptException();
|
|
144
|
+
return env.Null();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 1. Serialize to JSON string
|
|
148
|
+
Napi::Object json = env.Global().Get("JSON").As<Napi::Object>();
|
|
149
|
+
Napi::Function stringify = json.Get("stringify").As<Napi::Function>();
|
|
150
|
+
Napi::String jsonStr = stringify.Call({ info[0] }).As<Napi::String>();
|
|
151
|
+
std::string body = jsonStr.Utf8Value();
|
|
152
|
+
|
|
153
|
+
// 2. Wrap in HTTP
|
|
154
|
+
std::string response;
|
|
155
|
+
response.reserve(body.length() + 128);
|
|
156
|
+
|
|
157
|
+
// Minimal headers for speed
|
|
158
|
+
response += "HTTP/1.1 200 OK\r\n";
|
|
159
|
+
response += "Content-Type: application/json\r\n";
|
|
160
|
+
response += "Content-Length: " + std::to_string(body.length()) + "\r\n";
|
|
161
|
+
response += "Date: Mon, 27 Jul 2029 12:28:53 GMT\r\n";
|
|
162
|
+
response += "Connection: keep-alive\r\n\r\n";
|
|
163
|
+
response += body;
|
|
164
|
+
|
|
165
|
+
return Napi::Buffer<char>::Copy(env, response.data(), response.length());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// writeResponse(fd, iovecs[]) -> void
|
|
169
|
+
Napi::Value NativeServer::WriteResponse(const Napi::CallbackInfo& info) {
|
|
170
|
+
Napi::Env env = info.Env();
|
|
171
|
+
|
|
172
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsArray()) {
|
|
173
|
+
Napi::TypeError::New(env, "FD (number) and iovecs (array) expected").ThrowAsJavaScriptException();
|
|
174
|
+
return env.Null();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
int fd = info[0].As<Napi::Number>().Int32Value();
|
|
178
|
+
Napi::Array chunks = info[1].As<Napi::Array>();
|
|
179
|
+
uint32_t len = chunks.Length();
|
|
180
|
+
|
|
181
|
+
#ifdef _WIN32
|
|
182
|
+
// Windows implementation using write() loop for now
|
|
183
|
+
// Since WSASend requires WSA structures, keeping it simple for "ABI lock"
|
|
184
|
+
for (uint32_t i = 0; i < len; i++) {
|
|
185
|
+
Napi::Value val = chunks.Get(i);
|
|
186
|
+
if (val.IsBuffer()) {
|
|
187
|
+
Napi::Buffer<char> buf = val.As<Napi::Buffer<char>>();
|
|
188
|
+
_write(fd, buf.Data(), buf.Length());
|
|
189
|
+
} else if (val.IsString()) {
|
|
190
|
+
std::string str = val.As<Napi::String>().Utf8Value();
|
|
191
|
+
_write(fd, str.data(), str.length());
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
#else
|
|
195
|
+
// Linux/Unix implementation using writev if possible, or loop
|
|
196
|
+
// To properly use writev, we need to convert Napi::Array to struct iovec[]
|
|
197
|
+
// For large arrays, dynamic allocation is needed.
|
|
198
|
+
|
|
199
|
+
std::vector<struct iovec> iov;
|
|
200
|
+
iov.reserve(len);
|
|
201
|
+
|
|
202
|
+
// Keep buffers alive? No, Napi handles are valid during call.
|
|
203
|
+
// But we need to make sure we don't copy if possible.
|
|
204
|
+
// Napi::Buffer::Data returns pointer.
|
|
205
|
+
|
|
206
|
+
for (uint32_t i = 0; i < len; i++) {
|
|
207
|
+
Napi::Value val = chunks.Get(i);
|
|
208
|
+
if (val.IsBuffer()) {
|
|
209
|
+
Napi::Buffer<char> buf = val.As<Napi::Buffer<char>>();
|
|
210
|
+
struct iovec v;
|
|
211
|
+
v.iov_base = buf.Data();
|
|
212
|
+
v.iov_len = buf.Length();
|
|
213
|
+
iov.push_back(v);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!iov.empty()) {
|
|
218
|
+
ssize_t written = writev(fd, iov.data(), iov.size());
|
|
219
|
+
if (written < 0) {
|
|
220
|
+
// TODO: Better error handling for writev failure
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
#endif
|
|
224
|
+
|
|
225
|
+
return env.Undefined();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// setCPUAffinity(cpuId) -> boolean
|
|
229
|
+
Napi::Value NativeServer::SetCPUAffinity(const Napi::CallbackInfo& info) {
|
|
230
|
+
Napi::Env env = info.Env();
|
|
231
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
232
|
+
Napi::TypeError::New(env, "CPU ID (number) expected").ThrowAsJavaScriptException();
|
|
233
|
+
return env.Null();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
int cpuId = info[0].As<Napi::Number>().Int32Value();
|
|
237
|
+
|
|
238
|
+
#ifdef _WIN32
|
|
239
|
+
HANDLE process = GetCurrentProcess();
|
|
240
|
+
DWORD_PTR mask = (DWORD_PTR)1 << cpuId;
|
|
241
|
+
BOOL res = SetProcessAffinityMask(process, mask);
|
|
242
|
+
return Napi::Boolean::New(env, res != 0);
|
|
243
|
+
#else
|
|
244
|
+
// Linux/Unix implementation
|
|
245
|
+
// Note: sched_setaffinity is Linux specific.
|
|
246
|
+
// For macOS/BSD, it might be different (thread_policy_set), but we target Linux for production usually.
|
|
247
|
+
// Using pthread_setaffinity_np if available (common in Linux pthreads)
|
|
248
|
+
|
|
249
|
+
#if defined(__linux__)
|
|
250
|
+
cpu_set_t cpuset;
|
|
251
|
+
CPU_ZERO(&cpuset);
|
|
252
|
+
CPU_SET(cpuId, &cpuset);
|
|
253
|
+
pthread_t thread = pthread_self();
|
|
254
|
+
int res = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
|
|
255
|
+
return Napi::Boolean::New(env, res == 0);
|
|
256
|
+
#else
|
|
257
|
+
// Not implemented for this OS (e.g. macOS)
|
|
258
|
+
return Napi::Boolean::New(env, false);
|
|
259
|
+
#endif
|
|
260
|
+
|
|
261
|
+
#endif
|
|
262
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#ifndef NATIVE_SERVER_H
|
|
2
|
+
#define NATIVE_SERVER_H
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
#include <vector>
|
|
6
|
+
#include <string>
|
|
7
|
+
|
|
8
|
+
class NativeServer : public Napi::ObjectWrap<NativeServer> {
|
|
9
|
+
public:
|
|
10
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
11
|
+
NativeServer(const Napi::CallbackInfo& info);
|
|
12
|
+
|
|
13
|
+
private:
|
|
14
|
+
Napi::Value Parse(const Napi::CallbackInfo& info);
|
|
15
|
+
Napi::Value SerializeResponse(const Napi::CallbackInfo& info);
|
|
16
|
+
|
|
17
|
+
// Helper to create a response buffer directly
|
|
18
|
+
Napi::Value CreateResponse(const Napi::CallbackInfo& info);
|
|
19
|
+
|
|
20
|
+
// Zero-copy JSON fast path
|
|
21
|
+
Napi::Value CreateJSONResponse(const Napi::CallbackInfo& info);
|
|
22
|
+
|
|
23
|
+
// Direct socket write (writev wrapper)
|
|
24
|
+
Napi::Value WriteResponse(const Napi::CallbackInfo& info);
|
|
25
|
+
|
|
26
|
+
// CPU Affinity
|
|
27
|
+
Napi::Value SetCPUAffinity(const Napi::CallbackInfo& info);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
#endif
|
package/.eslintrc.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"root": true,
|
|
3
|
-
"env": {
|
|
4
|
-
"es2020": true,
|
|
5
|
-
"node": true
|
|
6
|
-
},
|
|
7
|
-
"parser": "@typescript-eslint/parser",
|
|
8
|
-
"parserOptions": {
|
|
9
|
-
"project": "./tsconfig.json",
|
|
10
|
-
"sourceType": "module"
|
|
11
|
-
},
|
|
12
|
-
"plugins": ["@typescript-eslint"],
|
|
13
|
-
"extends": [
|
|
14
|
-
"eslint:recommended",
|
|
15
|
-
"plugin:@typescript-eslint/recommended",
|
|
16
|
-
"plugin:prettier/recommended"
|
|
17
|
-
],
|
|
18
|
-
"rules": {
|
|
19
|
-
"@typescript-eslint/no-explicit-any": "off"
|
|
20
|
-
},
|
|
21
|
-
"ignorePatterns": ["dist/**", "node_modules/**"]
|
|
22
|
-
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
- master
|
|
8
|
-
pull_request:
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
build-and-test:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout
|
|
16
|
-
uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Setup Node.js
|
|
19
|
-
uses: actions/setup-node@v4
|
|
20
|
-
with:
|
|
21
|
-
node-version: 20
|
|
22
|
-
cache: "npm"
|
|
23
|
-
|
|
24
|
-
- name: Install dependencies
|
|
25
|
-
run: npm ci
|
|
26
|
-
|
|
27
|
-
- name: Lint
|
|
28
|
-
run: npm run lint
|
|
29
|
-
|
|
30
|
-
- name: Test
|
|
31
|
-
run: npm test
|
|
32
|
-
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [published]
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
publish:
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
permissions:
|
|
11
|
-
contents: read
|
|
12
|
-
id-token: write
|
|
13
|
-
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout
|
|
16
|
-
uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Setup Node.js
|
|
19
|
-
uses: actions/setup-node@v4
|
|
20
|
-
with:
|
|
21
|
-
node-version: 20
|
|
22
|
-
registry-url: 'https://registry.npmjs.org'
|
|
23
|
-
cache: 'npm'
|
|
24
|
-
|
|
25
|
-
- name: Install dependencies
|
|
26
|
-
run: npm ci
|
|
27
|
-
|
|
28
|
-
- name: Build
|
|
29
|
-
run: npm run build
|
|
30
|
-
|
|
31
|
-
- name: Test
|
|
32
|
-
run: npm test
|
|
33
|
-
|
|
34
|
-
- name: Publish to NPM
|
|
35
|
-
run: npm publish --access public
|
|
36
|
-
env:
|
|
37
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
name: Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- "v*"
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: write
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
release:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout
|
|
16
|
-
uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Create Release
|
|
19
|
-
uses: softprops/action-gh-release@v1
|
|
20
|
-
with:
|
|
21
|
-
generate_release_notes: true
|
package/.prettierrc
DELETED
package/assets/logo.svg
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
3
|
-
<!-- Creator: CorelDRAW -->
|
|
4
|
-
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="28.726mm" height="11.2072mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
|
5
|
-
viewBox="0 0 2872.6 1120.72"
|
|
6
|
-
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
7
|
-
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
|
8
|
-
<defs>
|
|
9
|
-
<style type="text/css">
|
|
10
|
-
<![CDATA[
|
|
11
|
-
.fil1 {fill:#2B2A29;fill-rule:nonzero}
|
|
12
|
-
.fil0 {fill:#00A0E3;fill-rule:nonzero}
|
|
13
|
-
]]>
|
|
14
|
-
</style>
|
|
15
|
-
</defs>
|
|
16
|
-
<g id="Layer_x0020_1">
|
|
17
|
-
<metadata id="CorelCorpID_0Corel-Layer"/>
|
|
18
|
-
<path class="fil0" d="M414.71 674.26l197.01 0 87.46 110.27 107.44 123.19 171.39 213 -219.58 0 -120.61 -144.12 -68.18 -98.97 -154.93 -203.37zm74.29 379.69c-92.16,0 -175.15,-20.45 -248.97,-61.36 -73.59,-40.91 -132.13,-100.62 -175.15,-179.15 -43.26,-78.52 -64.89,-173.74 -64.89,-285.88 0,-112.85 21.63,-208.53 64.89,-287.06 43.02,-78.52 101.56,-138.23 175.15,-179.14 73.82,-40.91 156.81,-61.36 248.97,-61.36 91.93,0 174.68,20.45 248.27,61.36 73.82,40.91 132.12,100.62 175.15,179.14 42.79,78.53 64.41,174.21 64.41,287.06 0,112.61 -21.62,208.07 -64.41,286.59 -43.03,78.52 -101.33,138 -175.15,178.67 -73.59,40.68 -156.34,61.13 -248.27,61.13zm0 -213.7c49.61,0 92.4,-11.76 128.13,-35.74 35.97,-23.98 63.48,-59.01 82.76,-105.8 19.27,-46.54 28.91,-103.67 28.91,-171.15 0,-67.94 -9.64,-125.31 -28.91,-172.09 -19.28,-46.79 -46.79,-82.29 -82.76,-106.03 -35.73,-23.98 -78.52,-35.97 -128.13,-35.97 -50.07,0 -92.86,12.22 -128.83,36.2 -35.74,24.22 -63.24,59.48 -82.52,106.03 -19.28,46.79 -28.92,103.92 -28.92,171.86 0,67.48 9.64,124.61 28.92,170.92 19.28,46.31 46.78,81.58 82.52,105.79 35.97,23.98 78.76,35.98 128.83,35.98z"/>
|
|
19
|
-
<path class="fil1" d="M1106.72 521.64l0 310.02 -19.05 0 0 -615.95 19.05 0 0 253.01 -4.93 0c9.87,-35.28 29.49,-61.81 58.84,-79.45 29.35,-17.63 61.52,-26.38 96.52,-26.38 31.75,0 59.69,6.63 83.82,20.03 23.99,13.41 43.04,31.9 56.73,55.6 13.83,23.71 20.74,51.37 20.74,83.12l0 310.02 -19.05 0 0 -310.02c0,-41.91 -13.26,-75.64 -39.65,-101.32 -26.53,-25.68 -60.68,-38.38 -102.59,-38.52 -28.65,0.14 -54.33,6.06 -77.05,18.06 -22.72,11.99 -40.64,28.5 -53.76,49.67 -13.12,21.03 -19.62,45.01 -19.62,72.11z"/>
|
|
20
|
-
<path id="_1" class="fil1" d="M1687.12 369.52l0 18.91 -182.74 0 0 -18.91 182.74 0zm-114.86 -110.07l19.05 0 0 484.16c0,27.51 6.91,47.83 20.74,60.96 13.97,13.12 32.74,16.65 56.45,10.58 2.25,-0.56 4.65,-1.13 7.05,-1.83 2.54,-0.71 4.8,-1.42 7.06,-1.98l4.51 18.63c-2.4,0.56 -4.94,1.27 -7.62,1.83 -2.68,0.71 -5.22,1.41 -7.62,1.98 -30.9,7.33 -55.17,2.25 -72.95,-15.53 -17.78,-17.78 -26.67,-42.61 -26.67,-74.64l0 -484.16z"/>
|
|
21
|
-
<path id="_2" class="fil1" d="M1890.46 369.52l0 18.91 -182.74 0 0 -18.91 182.74 0zm-114.86 -110.07l19.05 0 0 484.16c0,27.51 6.91,47.83 20.74,60.96 13.97,13.12 32.74,16.65 56.45,10.58 2.25,-0.56 4.65,-1.13 7.05,-1.83 2.54,-0.71 4.8,-1.42 7.06,-1.98l4.51 18.63c-2.4,0.56 -4.94,1.27 -7.62,1.83 -2.68,0.71 -5.22,1.41 -7.62,1.98 -30.9,7.33 -55.17,2.25 -72.95,-15.53 -17.78,-17.78 -26.67,-42.61 -26.67,-74.64l0 -484.16z"/>
|
|
22
|
-
<path id="_3" class="fil1" d="M1988.81 1004.52l0 -635 19.05 0 0 118.11 2.4 0c8.05,-25.54 19.76,-47.7 35,-66.46 15.24,-18.77 33.3,-33.17 54.19,-43.18 20.74,-10.16 43.46,-15.1 68.01,-15.1 36.13,0 67.45,10.44 93.98,31.32 26.67,21.03 47.27,49.53 61.81,85.66 14.68,36.12 21.87,76.76 21.87,121.92 0,45.44 -7.19,86.36 -21.59,122.63 -14.53,36.26 -34.99,64.77 -61.66,85.79 -26.53,20.89 -58,31.33 -94.41,31.33 -24.83,0 -47.55,-5.08 -68.3,-15.24 -20.6,-10.16 -38.52,-24.7 -53.76,-43.61 -15.1,-18.9 -26.81,-41.2 -35.14,-66.74l-2.4 0 0 288.57 -19.05 0zm178.65 -181.89c32.46,0 60.54,-9.74 84.25,-29.21 23.7,-19.47 42.05,-45.86 55.03,-79.16 12.98,-33.31 19.47,-70.84 19.47,-112.47 0,-41.63 -6.49,-79.02 -19.47,-112.18 -12.98,-33.31 -31.33,-59.55 -55.03,-78.74 -23.71,-19.34 -51.79,-28.93 -84.25,-29.07 -32.59,0.14 -60.68,9.73 -84.52,29.07 -23.85,19.19 -42.34,45.43 -55.46,78.74 -13.12,33.16 -19.62,70.55 -19.62,112.18 0,41.63 6.5,79.16 19.48,112.47 12.84,33.3 31.33,59.69 55.17,79.16 23.85,19.47 52.07,29.21 84.95,29.21z"/>
|
|
23
|
-
<path id="_4" class="fil1" d="M2402.13 831.66l228.61 -319.62 0 13.27 -221.97 -309.6 23.14 0 138.15 192.61c8.18,11.29 16.37,22.58 24.41,33.73 7.9,11.15 15.95,22.44 23.99,33.87 7.9,11.43 15.8,23.28 23.56,35.28l-8.74 0c8.04,-12 15.94,-23.85 23.84,-35.28 7.76,-11.43 15.67,-22.72 23.71,-33.87 8.04,-11.15 15.95,-22.44 23.99,-33.73l138.01 -192.61 23.14 0 -221.97 310.44 0 -14.11 228.6 319.62 -23.14 0 -146.76 -205.04c-7.62,-11 -15.38,-22.01 -23.14,-32.87 -7.76,-10.87 -15.38,-21.88 -23.14,-33.02 -7.76,-11.29 -15.38,-22.58 -23.14,-34.15l8.32 0c-7.76,11.57 -15.38,22.86 -23,34.15 -7.62,11.14 -15.1,22.15 -22.72,33.02 -7.62,10.86 -15.38,21.87 -23.42,32.87l-147.18 205.04 -23.15 0z"/>
|
|
24
|
-
</g>
|
|
25
|
-
</svg>
|
package/eslint.config.cjs
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const tsParser = require("@typescript-eslint/parser");
|
|
2
|
-
const tsPlugin = require("@typescript-eslint/eslint-plugin");
|
|
3
|
-
const prettierPlugin = require("eslint-plugin-prettier");
|
|
4
|
-
|
|
5
|
-
module.exports = [
|
|
6
|
-
{
|
|
7
|
-
files: ["**/*.ts"],
|
|
8
|
-
ignores: ["dist/**", "node_modules/**"],
|
|
9
|
-
languageOptions: {
|
|
10
|
-
parser: tsParser,
|
|
11
|
-
parserOptions: {
|
|
12
|
-
project: "./tsconfig.json",
|
|
13
|
-
tsconfigRootDir: __dirname,
|
|
14
|
-
sourceType: "module",
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
plugins: {
|
|
18
|
-
"@typescript-eslint": tsPlugin,
|
|
19
|
-
prettier: prettierPlugin,
|
|
20
|
-
},
|
|
21
|
-
rules: {
|
|
22
|
-
...tsPlugin.configs.recommended.rules,
|
|
23
|
-
"prettier/prettier": "off",
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
];
|
package/examples/api-server.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { createHttpApp } from '../src/index';
|
|
2
|
-
|
|
3
|
-
// 1. Initialize App (Fusion + Aegis enabled)
|
|
4
|
-
const app = createHttpApp({
|
|
5
|
-
enableRequestFusion: true, // ⚡ Auto-coalesce duplicate requests
|
|
6
|
-
metricsEnabled: true // 📊 Expose /__qhttpx/metrics
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
// 2. Global Middleware (Logging & Rate Limit)
|
|
10
|
-
app.use(async ({ req, next }) => {
|
|
11
|
-
console.log(`[${req.method}] ${req.url}`);
|
|
12
|
-
if (next) await next();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// 3. Validation Schema
|
|
16
|
-
const UserSchema = {
|
|
17
|
-
body: {
|
|
18
|
-
type: 'object',
|
|
19
|
-
required: ['name', 'role'],
|
|
20
|
-
properties: { name: { type: 'string' }, role: { type: 'string' } }
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// 4. Routes (Clean & Destructured)
|
|
25
|
-
app.get('/', ({ json }) => json({ status: 'online', fusion: true }));
|
|
26
|
-
|
|
27
|
-
// ⚡ Fused Endpoint: 1000 concurrent requests -> 1 DB execution
|
|
28
|
-
app.get('/heavy', async ({ json }) => {
|
|
29
|
-
await new Promise(r => setTimeout(r, 100)); // Simulate DB
|
|
30
|
-
json({ data: 'Expensive Result', timestamp: Date.now() });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// 🛡️ Validated & Typed Route
|
|
34
|
-
app.post('/users', { schema: UserSchema }, async ({ body, json }) => {
|
|
35
|
-
// Body is already validated and typed here
|
|
36
|
-
json({ created: true, user: body }, 201);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// 📂 File Uploads (Typed)
|
|
40
|
-
app.post('/upload', ({ files, json }) => {
|
|
41
|
-
if (!files?.doc) return json({ error: 'No file' }, 400);
|
|
42
|
-
const doc = Array.isArray(files.doc) ? files.doc[0] : files.doc;
|
|
43
|
-
json({ filename: doc.filename, size: doc.size });
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// 📡 WebSockets (Pub/Sub)
|
|
47
|
-
app.upgrade('/chat', (ws) => {
|
|
48
|
-
ws.join('general'); // Auto-join room
|
|
49
|
-
ws.on('message', (msg) => {
|
|
50
|
-
// Broadcast to room
|
|
51
|
-
app.websocket.to('general').emit(`Echo: ${msg}`);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// 5. Unified Error Handling
|
|
56
|
-
app.onError(({ error, json }) => {
|
|
57
|
-
console.error(error); // Log internal error
|
|
58
|
-
json({ error: 'Internal Server Error', handled: true }, 500);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// 6. Start Server
|
|
62
|
-
app.listen(3000, () => console.log('🚀 Server running on http://localhost:3000'));
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { quantam } from 'quantam-async';
|
|
2
|
-
import { QHTTPX } from '../index';
|
|
3
|
-
|
|
4
|
-
async function run() {
|
|
5
|
-
const app = new QHTTPX({
|
|
6
|
-
maxConcurrency: 512,
|
|
7
|
-
requestTimeoutMs: 10_000,
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
app.get('/json', (ctx) => {
|
|
11
|
-
ctx.json({ message: 'hello from qhttpx' });
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
15
|
-
|
|
16
|
-
const url = `http://127.0.0.1:${port}/json`;
|
|
17
|
-
|
|
18
|
-
const requestsPerUser = 10;
|
|
19
|
-
const userCount = 10_000;
|
|
20
|
-
const maxConcurrentUsers = 1_000;
|
|
21
|
-
|
|
22
|
-
const flow = quantam<number>()
|
|
23
|
-
.name('user-flow')
|
|
24
|
-
.step(async (userId) => {
|
|
25
|
-
for (let i = 0; i < requestsPerUser; i += 1) {
|
|
26
|
-
const response = await fetch(url);
|
|
27
|
-
if (!response.ok) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`user ${userId} request ${i} failed with status ${response.status}`,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
await response.text();
|
|
33
|
-
}
|
|
34
|
-
return userId;
|
|
35
|
-
})
|
|
36
|
-
.retry(3, 50)
|
|
37
|
-
.timeout(30_000);
|
|
38
|
-
|
|
39
|
-
const inputs: number[] = [];
|
|
40
|
-
for (let i = 0; i < userCount; i += 1) {
|
|
41
|
-
inputs.push(i);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const start = Date.now();
|
|
45
|
-
try {
|
|
46
|
-
await flow.runMany(inputs, {
|
|
47
|
-
concurrency: maxConcurrentUsers,
|
|
48
|
-
});
|
|
49
|
-
} catch (err) {
|
|
50
|
-
console.error('Quantam runMany error', err);
|
|
51
|
-
}
|
|
52
|
-
const durationSec = (Date.now() - start) / 1000;
|
|
53
|
-
const totalRequests = userCount * requestsPerUser;
|
|
54
|
-
const rps = totalRequests / durationSec;
|
|
55
|
-
|
|
56
|
-
console.log(
|
|
57
|
-
`Quantam bench: users=${userCount}, requestsPerUser=${requestsPerUser}, ` +
|
|
58
|
-
`totalRequests=${totalRequests}, duration=${durationSec.toFixed(
|
|
59
|
-
2,
|
|
60
|
-
)}s, rps=${rps.toFixed(0)}, concurrency=${maxConcurrentUsers}`,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
await app.close();
|
|
64
|
-
process.exit(0);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
run().catch((err) => {
|
|
68
|
-
console.error(err);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import autocannon from 'autocannon';
|
|
2
|
-
import { QHTTPX } from '../index';
|
|
3
|
-
|
|
4
|
-
function runAutocannon(url: string): Promise<autocannon.Result> {
|
|
5
|
-
return new Promise((resolve, reject) => {
|
|
6
|
-
const instance = autocannon(
|
|
7
|
-
{
|
|
8
|
-
url,
|
|
9
|
-
amount: 10_000,
|
|
10
|
-
connections: 10_000,
|
|
11
|
-
pipelining: 1,
|
|
12
|
-
duration: 10,
|
|
13
|
-
},
|
|
14
|
-
(err, result) => {
|
|
15
|
-
if (err) {
|
|
16
|
-
reject(err);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
resolve(result);
|
|
20
|
-
},
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
instance.on('error', (err) => {
|
|
24
|
-
reject(err);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function run() {
|
|
30
|
-
const payloadBuffer = Buffer.from(
|
|
31
|
-
JSON.stringify({ message: 'hello from qhttpx' }),
|
|
32
|
-
);
|
|
33
|
-
const app = new QHTTPX({
|
|
34
|
-
maxConcurrency: 512,
|
|
35
|
-
metricsEnabled: false,
|
|
36
|
-
jsonSerializer: () => payloadBuffer,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
app.get('/json', (ctx) => {
|
|
40
|
-
ctx.json({ message: 'hello from qhttpx' });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
44
|
-
|
|
45
|
-
const url = `http://127.0.0.1:${port}/json`;
|
|
46
|
-
|
|
47
|
-
const result = await runAutocannon(url);
|
|
48
|
-
|
|
49
|
-
autocannon.printResult(result);
|
|
50
|
-
|
|
51
|
-
const totalRequests = result.requests.total;
|
|
52
|
-
const sent = result.requests.sent;
|
|
53
|
-
const avgRps = result.requests.average.toFixed(0);
|
|
54
|
-
const p99 = result.latency.p99.toFixed(1);
|
|
55
|
-
const connections = result.connections;
|
|
56
|
-
const pipelining = result.pipelining;
|
|
57
|
-
// Summary line that shows up clearly in CI/dev logs
|
|
58
|
-
console.log(
|
|
59
|
-
`QHTTPX bench: total=${totalRequests} (sent=${sent}) req, ` +
|
|
60
|
-
`${avgRps} req/sec, p99=${p99}ms, connections=${connections}, ` +
|
|
61
|
-
`pipelining=${pipelining}`,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
await app.close();
|
|
65
|
-
process.exit(0);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
run().catch((err) => {
|
|
69
|
-
console.error(err);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
});
|