qhttpx 1.8.0
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/.eslintrc.json +22 -0
- package/.github/workflows/ci.yml +32 -0
- package/.github/workflows/npm-publish.yml +37 -0
- package/.github/workflows/release.yml +21 -0
- package/.prettierrc +7 -0
- package/CHANGELOG.md +145 -0
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/package.json +61 -0
- package/dist/src/benchmarks/compare-frameworks.js +119 -0
- package/dist/src/benchmarks/quantam-users.js +56 -0
- package/dist/src/benchmarks/simple-json.js +58 -0
- package/dist/src/benchmarks/ultra-mode.js +122 -0
- package/dist/src/cli/index.js +200 -0
- package/dist/src/client/index.js +72 -0
- package/dist/src/core/batch.js +97 -0
- package/dist/src/core/body-parser.js +121 -0
- package/dist/src/core/buffer-pool.js +70 -0
- package/dist/src/core/config.js +50 -0
- package/dist/src/core/fusion.js +183 -0
- package/dist/src/core/logger.js +49 -0
- package/dist/src/core/metrics.js +111 -0
- package/dist/src/core/resources.js +25 -0
- package/dist/src/core/scheduler.js +85 -0
- package/dist/src/core/scope.js +68 -0
- package/dist/src/core/serializer.js +44 -0
- package/dist/src/core/server.js +905 -0
- package/dist/src/core/stream.js +71 -0
- package/dist/src/core/tasks.js +87 -0
- package/dist/src/core/types.js +19 -0
- package/dist/src/core/websocket.js +86 -0
- package/dist/src/core/worker-queue.js +73 -0
- package/dist/src/database/adapters/memory.js +90 -0
- package/dist/src/database/adapters/mongo.js +141 -0
- package/dist/src/database/adapters/postgres.js +111 -0
- package/dist/src/database/adapters/sqlite.js +42 -0
- package/dist/src/database/coalescer.js +134 -0
- package/dist/src/database/manager.js +87 -0
- package/dist/src/database/types.js +2 -0
- package/dist/src/index.js +61 -0
- package/dist/src/middleware/compression.js +133 -0
- package/dist/src/middleware/cors.js +66 -0
- package/dist/src/middleware/presets.js +33 -0
- package/dist/src/middleware/rate-limit.js +77 -0
- package/dist/src/middleware/security.js +69 -0
- package/dist/src/middleware/static.js +191 -0
- package/dist/src/openapi/generator.js +149 -0
- package/dist/src/router/radix-router.js +89 -0
- package/dist/src/router/radix-tree.js +81 -0
- package/dist/src/router/router.js +146 -0
- package/dist/src/testing/index.js +84 -0
- package/dist/src/utils/cookies.js +59 -0
- package/dist/src/utils/logger.js +45 -0
- package/dist/src/utils/signals.js +31 -0
- package/dist/src/utils/sse.js +32 -0
- package/dist/src/validation/index.js +19 -0
- package/dist/src/validation/simple.js +102 -0
- package/dist/src/validation/types.js +12 -0
- package/dist/src/validation/zod.js +18 -0
- package/dist/src/views/index.js +17 -0
- package/dist/src/views/types.js +2 -0
- package/dist/tests/adapters.test.js +106 -0
- package/dist/tests/batch.test.js +117 -0
- package/dist/tests/body-parser.test.js +52 -0
- package/dist/tests/compression-sse.test.js +87 -0
- package/dist/tests/cookies.test.js +63 -0
- package/dist/tests/cors.test.js +55 -0
- package/dist/tests/database.test.js +80 -0
- package/dist/tests/dx.test.js +64 -0
- package/dist/tests/ecosystem.test.js +133 -0
- package/dist/tests/features.test.js +47 -0
- package/dist/tests/fusion.test.js +92 -0
- package/dist/tests/http-basic.test.js +124 -0
- package/dist/tests/logger.test.js +33 -0
- package/dist/tests/middleware.test.js +109 -0
- package/dist/tests/observability.test.js +59 -0
- package/dist/tests/openapi.test.js +64 -0
- package/dist/tests/plugin.test.js +65 -0
- package/dist/tests/plugins.test.js +71 -0
- package/dist/tests/rate-limit.test.js +77 -0
- package/dist/tests/resources.test.js +44 -0
- package/dist/tests/scheduler.test.js +46 -0
- package/dist/tests/schema-routes.test.js +77 -0
- package/dist/tests/security.test.js +83 -0
- package/dist/tests/server-db.test.js +72 -0
- package/dist/tests/smoke.test.js +10 -0
- package/dist/tests/sqlite-fusion.test.js +92 -0
- package/dist/tests/static.test.js +102 -0
- package/dist/tests/stream.test.js +44 -0
- package/dist/tests/task-metrics.test.js +53 -0
- package/dist/tests/tasks.test.js +62 -0
- package/dist/tests/testing.test.js +47 -0
- package/dist/tests/validation.test.js +107 -0
- package/dist/tests/websocket.test.js +146 -0
- package/dist/vitest.config.js +9 -0
- package/docs/AEGIS.md +76 -0
- package/docs/BENCHMARKS.md +36 -0
- package/docs/CAPABILITIES.md +70 -0
- package/docs/CLI.md +43 -0
- package/docs/DATABASE.md +142 -0
- package/docs/ECOSYSTEM.md +146 -0
- package/docs/NEXT_STEPS.md +99 -0
- package/docs/OPENAPI.md +99 -0
- package/docs/PLUGINS.md +59 -0
- package/docs/REAL_WORLD_EXAMPLES.md +109 -0
- package/docs/ROADMAP.md +366 -0
- package/docs/VALIDATION.md +136 -0
- package/eslint.config.cjs +26 -0
- package/examples/api-server.ts +254 -0
- package/package.json +61 -0
- package/src/benchmarks/compare-frameworks.ts +149 -0
- package/src/benchmarks/quantam-users.ts +70 -0
- package/src/benchmarks/simple-json.ts +71 -0
- package/src/benchmarks/ultra-mode.ts +159 -0
- package/src/cli/index.ts +214 -0
- package/src/client/index.ts +93 -0
- package/src/core/batch.ts +110 -0
- package/src/core/body-parser.ts +151 -0
- package/src/core/buffer-pool.ts +96 -0
- package/src/core/config.ts +60 -0
- package/src/core/fusion.ts +210 -0
- package/src/core/logger.ts +70 -0
- package/src/core/metrics.ts +166 -0
- package/src/core/resources.ts +38 -0
- package/src/core/scheduler.ts +126 -0
- package/src/core/scope.ts +87 -0
- package/src/core/serializer.ts +41 -0
- package/src/core/server.ts +1113 -0
- package/src/core/stream.ts +111 -0
- package/src/core/tasks.ts +138 -0
- package/src/core/types.ts +178 -0
- package/src/core/websocket.ts +112 -0
- package/src/core/worker-queue.ts +90 -0
- package/src/database/adapters/memory.ts +99 -0
- package/src/database/adapters/mongo.ts +116 -0
- package/src/database/adapters/postgres.ts +86 -0
- package/src/database/adapters/sqlite.ts +44 -0
- package/src/database/coalescer.ts +153 -0
- package/src/database/manager.ts +97 -0
- package/src/database/types.ts +24 -0
- package/src/index.ts +42 -0
- package/src/middleware/compression.ts +147 -0
- package/src/middleware/cors.ts +98 -0
- package/src/middleware/presets.ts +50 -0
- package/src/middleware/rate-limit.ts +106 -0
- package/src/middleware/security.ts +109 -0
- package/src/middleware/static.ts +216 -0
- package/src/openapi/generator.ts +167 -0
- package/src/router/radix-router.ts +119 -0
- package/src/router/radix-tree.ts +106 -0
- package/src/router/router.ts +190 -0
- package/src/testing/index.ts +104 -0
- package/src/utils/cookies.ts +67 -0
- package/src/utils/logger.ts +59 -0
- package/src/utils/signals.ts +45 -0
- package/src/utils/sse.ts +41 -0
- package/src/validation/index.ts +3 -0
- package/src/validation/simple.ts +93 -0
- package/src/validation/types.ts +38 -0
- package/src/validation/zod.ts +14 -0
- package/src/views/index.ts +1 -0
- package/src/views/types.ts +4 -0
- package/tests/adapters.test.ts +120 -0
- package/tests/batch.test.ts +139 -0
- package/tests/body-parser.test.ts +83 -0
- package/tests/compression-sse.test.ts +98 -0
- package/tests/cookies.test.ts +74 -0
- package/tests/cors.test.ts +79 -0
- package/tests/database.test.ts +90 -0
- package/tests/dx.test.ts +78 -0
- package/tests/ecosystem.test.ts +156 -0
- package/tests/features.test.ts +51 -0
- package/tests/fusion.test.ts +121 -0
- package/tests/http-basic.test.ts +161 -0
- package/tests/logger.test.ts +48 -0
- package/tests/middleware.test.ts +137 -0
- package/tests/observability.test.ts +91 -0
- package/tests/openapi.test.ts +74 -0
- package/tests/plugin.test.ts +85 -0
- package/tests/plugins.test.ts +93 -0
- package/tests/rate-limit.test.ts +97 -0
- package/tests/resources.test.ts +64 -0
- package/tests/scheduler.test.ts +71 -0
- package/tests/schema-routes.test.ts +89 -0
- package/tests/security.test.ts +128 -0
- package/tests/server-db.test.ts +72 -0
- package/tests/smoke.test.ts +9 -0
- package/tests/sqlite-fusion.test.ts +106 -0
- package/tests/static.test.ts +111 -0
- package/tests/stream.test.ts +58 -0
- package/tests/task-metrics.test.ts +78 -0
- package/tests/tasks.test.ts +90 -0
- package/tests/testing.test.ts +53 -0
- package/tests/validation.test.ts +126 -0
- package/tests/websocket.test.ts +132 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { IncomingMessage } from 'http';
|
|
3
|
+
import { Duplex } from 'stream';
|
|
4
|
+
import { createHttpApp, HttpError } from '../src/index';
|
|
5
|
+
|
|
6
|
+
// 1. Initialize the application (includes CORS, Security Headers, Logging)
|
|
7
|
+
const app = createHttpApp({
|
|
8
|
+
metricsEnabled: true,
|
|
9
|
+
workers: 1,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// 2. Mock Database
|
|
13
|
+
const users = new Map<string, { id: string; name: string; role: string }>();
|
|
14
|
+
|
|
15
|
+
// Seed some data
|
|
16
|
+
users.set('1', { id: '1', name: 'Alice', role: 'admin' });
|
|
17
|
+
users.set('2', { id: '2', name: 'Bob', role: 'user' });
|
|
18
|
+
|
|
19
|
+
// 3. Register Background Tasks
|
|
20
|
+
app.task('send-email', async (payload: any) => {
|
|
21
|
+
// Simulate heavy work
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
23
|
+
console.log(`[Background Job] Sending email to ${payload.email}: "${payload.subject}"`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 4. Define Routes
|
|
27
|
+
|
|
28
|
+
// Root
|
|
29
|
+
app.get('/', (ctx) => {
|
|
30
|
+
ctx.json({
|
|
31
|
+
message: 'Welcome to QHTTPX Example API',
|
|
32
|
+
endpoints: [
|
|
33
|
+
'GET /api/users',
|
|
34
|
+
'POST /api/users',
|
|
35
|
+
'GET /api/users/:id',
|
|
36
|
+
'POST /api/jobs/email',
|
|
37
|
+
'WS /ws',
|
|
38
|
+
'GET /api/cookies',
|
|
39
|
+
'GET /api/redirect'
|
|
40
|
+
],
|
|
41
|
+
documentation: 'https://github.com/your-repo/qhttpx'
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.get('/api/cookies', (ctx) => {
|
|
46
|
+
// Read cookies
|
|
47
|
+
const visited = parseInt(ctx.cookies['visited'] || '0');
|
|
48
|
+
|
|
49
|
+
// Set cookie
|
|
50
|
+
ctx.setCookie('visited', (visited + 1).toString(), {
|
|
51
|
+
path: '/',
|
|
52
|
+
maxAge: 3600, // 1 hour
|
|
53
|
+
httpOnly: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
ctx.json({
|
|
57
|
+
message: 'Cookie demo',
|
|
58
|
+
visited: visited + 1,
|
|
59
|
+
allCookies: ctx.cookies
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
app.get('/api/redirect', (ctx) => {
|
|
64
|
+
ctx.redirect('/api/users');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// User Routes using the Chainable Builder
|
|
68
|
+
app.route('/api/users')
|
|
69
|
+
.get((ctx) => {
|
|
70
|
+
const userList = Array.from(users.values());
|
|
71
|
+
ctx.json({ data: userList, count: userList.length });
|
|
72
|
+
})
|
|
73
|
+
.post(async (ctx) => {
|
|
74
|
+
if (!ctx.body || typeof ctx.body !== 'object') {
|
|
75
|
+
ctx.json({ error: 'Invalid body' }, 400);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const body = ctx.body as any;
|
|
80
|
+
if (!body.name || !body.role) {
|
|
81
|
+
ctx.json({ error: 'Missing name or role' }, 400);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const id = (users.size + 1).toString();
|
|
86
|
+
const newUser = { id, name: body.name, role: body.role };
|
|
87
|
+
users.set(id, newUser);
|
|
88
|
+
|
|
89
|
+
ctx.json({ message: 'User created', user: newUser }, 201);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
app.route('/api/users/:id')
|
|
93
|
+
.get((ctx) => {
|
|
94
|
+
const { id } = ctx.params;
|
|
95
|
+
const user = users.get(id);
|
|
96
|
+
|
|
97
|
+
if (!user) {
|
|
98
|
+
ctx.json({ error: 'User not found' }, 404);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ctx.json({ data: user });
|
|
103
|
+
})
|
|
104
|
+
.delete((ctx) => {
|
|
105
|
+
const { id } = ctx.params;
|
|
106
|
+
if (users.delete(id)) {
|
|
107
|
+
ctx.json({ message: 'User deleted' });
|
|
108
|
+
} else {
|
|
109
|
+
ctx.json({ error: 'User not found' }, 404);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Job Route
|
|
114
|
+
app.post('/api/jobs/email', async (ctx) => {
|
|
115
|
+
const body = ctx.body as any;
|
|
116
|
+
if (!body.email) {
|
|
117
|
+
ctx.json({ error: 'Email required' }, 400);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await app.enqueue('send-email', {
|
|
122
|
+
email: body.email,
|
|
123
|
+
subject: 'Welcome to QHTTPX!'
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
ctx.json({ message: 'Email job queued', status: 'pending' }, 202);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// WebSocket Route
|
|
130
|
+
app.upgrade('/ws', handleWebSocketEcho);
|
|
131
|
+
|
|
132
|
+
// Error handling demo
|
|
133
|
+
app.get('/error', () => {
|
|
134
|
+
throw new Error('Something went wrong!');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Custom Error Handlers (optional, defaults are good)
|
|
138
|
+
app.setNotFoundHandler((ctx) => {
|
|
139
|
+
ctx.json({ error: 'Not Found', path: ctx.url.pathname }, 404);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
app.setErrorHandler((err, ctx) => {
|
|
143
|
+
if (err instanceof HttpError) {
|
|
144
|
+
ctx.json({ error: err.message, code: err.code, details: err.details }, err.status);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
ctx.json({ error: 'Internal Server Error' }, 500);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 5. Start Server
|
|
151
|
+
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
|
152
|
+
app.listen(PORT, '0.0.0.0').then(({ port }) => {
|
|
153
|
+
console.log(`\n🚀 Server running at http://localhost:${port}`);
|
|
154
|
+
console.log(` Try: curl http://localhost:${port}/api/users`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// --- Helpers ---
|
|
158
|
+
|
|
159
|
+
function createWebSocketAccept(key: string): string {
|
|
160
|
+
return createHash('sha1')
|
|
161
|
+
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
|
162
|
+
.digest('base64');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handleWebSocketEcho(req: IncomingMessage, socket: Duplex, head: Buffer) {
|
|
166
|
+
void head;
|
|
167
|
+
|
|
168
|
+
const rawKeyHeader = req.headers['sec-websocket-key'];
|
|
169
|
+
let keyHeader: string | undefined;
|
|
170
|
+
if (typeof rawKeyHeader === 'string') {
|
|
171
|
+
keyHeader = rawKeyHeader;
|
|
172
|
+
} else if (Array.isArray(rawKeyHeader)) {
|
|
173
|
+
const values = rawKeyHeader as string[];
|
|
174
|
+
if (values.length > 0) {
|
|
175
|
+
keyHeader = values[0];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const key = keyHeader ?? '';
|
|
180
|
+
const accept = createWebSocketAccept(key);
|
|
181
|
+
const responseLines = [
|
|
182
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
183
|
+
'Upgrade: websocket',
|
|
184
|
+
'Connection: Upgrade',
|
|
185
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
186
|
+
'',
|
|
187
|
+
'',
|
|
188
|
+
];
|
|
189
|
+
socket.write(responseLines.join('\r\n'));
|
|
190
|
+
|
|
191
|
+
socket.on('data', (buffer) => {
|
|
192
|
+
if (buffer.length < 2) return;
|
|
193
|
+
|
|
194
|
+
const opcode = buffer[0] & 0x0f;
|
|
195
|
+
const masked = (buffer[1] & 0x80) !== 0;
|
|
196
|
+
let payloadLength = buffer[1] & 0x7f;
|
|
197
|
+
let offset = 2;
|
|
198
|
+
|
|
199
|
+
if (payloadLength === 126) {
|
|
200
|
+
if (buffer.length < offset + 2) return;
|
|
201
|
+
payloadLength = buffer.readUInt16BE(offset);
|
|
202
|
+
offset += 2;
|
|
203
|
+
} else if (payloadLength === 127) {
|
|
204
|
+
return; // Too large for this demo
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (buffer.length < offset + payloadLength) return;
|
|
208
|
+
|
|
209
|
+
let maskingKey: Buffer | undefined;
|
|
210
|
+
if (masked) {
|
|
211
|
+
maskingKey = buffer.subarray(offset, offset + 4);
|
|
212
|
+
offset += 4;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const payload = buffer.subarray(offset, offset + payloadLength);
|
|
216
|
+
|
|
217
|
+
if (masked && maskingKey) {
|
|
218
|
+
for (let i = 0; i < payload.length; i += 1) {
|
|
219
|
+
payload[i] ^= maskingKey[i % 4];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (opcode === 0x8) { // Close
|
|
224
|
+
socket.end();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (opcode === 0x1) { // Text
|
|
229
|
+
const message = payload.toString('utf8');
|
|
230
|
+
const textBytes = Buffer.from(message, 'utf8');
|
|
231
|
+
const length = textBytes.length;
|
|
232
|
+
|
|
233
|
+
let header: Buffer;
|
|
234
|
+
if (length < 126) {
|
|
235
|
+
header = Buffer.alloc(2);
|
|
236
|
+
header[0] = 0x81;
|
|
237
|
+
header[1] = length;
|
|
238
|
+
} else if (length < 65536) {
|
|
239
|
+
header = Buffer.alloc(4);
|
|
240
|
+
header[0] = 0x81;
|
|
241
|
+
header[1] = 126;
|
|
242
|
+
header.writeUInt16BE(length, 2);
|
|
243
|
+
} else {
|
|
244
|
+
return; // Too large
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const frame = Buffer.concat([header, textBytes]);
|
|
248
|
+
socket.write(frame);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
socket.on('end', () => socket.end());
|
|
253
|
+
socket.on('error', () => socket.destroy());
|
|
254
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qhttpx",
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "**URL:** https://qhttpx.gridrr.com",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"qhttpx": "./dist/src/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"directories": {
|
|
10
|
+
"doc": "docs"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"lint": "eslint src tests --ext .ts",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"example": "npx tsx examples/api-server.ts",
|
|
17
|
+
"bench": "npm run build && node dist/src/benchmarks/simple-json.js",
|
|
18
|
+
"bench:quantam": "npm run build && node dist/src/benchmarks/quantam-users.js",
|
|
19
|
+
"bench:compare": "npm run build && node dist/src/benchmarks/compare-frameworks.js",
|
|
20
|
+
"bench:ultra": "npm run build && node dist/src/benchmarks/ultra-mode.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/autocannon": "^7.12.7",
|
|
28
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
29
|
+
"@types/busboy": "^1.5.4",
|
|
30
|
+
"@types/express": "^5.0.6",
|
|
31
|
+
"@types/ioredis": "^4.28.10",
|
|
32
|
+
"@types/mongodb": "^4.0.6",
|
|
33
|
+
"@types/node": "^25.0.9",
|
|
34
|
+
"@types/pg": "^8.16.0",
|
|
35
|
+
"@types/ws": "^8.18.1",
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
37
|
+
"@typescript-eslint/parser": "^8.53.0",
|
|
38
|
+
"autocannon": "^8.0.0",
|
|
39
|
+
"eslint": "^9.39.2",
|
|
40
|
+
"eslint-config-prettier": "^10.1.8",
|
|
41
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
42
|
+
"mongodb": "^7.0.0",
|
|
43
|
+
"pg": "^8.17.1",
|
|
44
|
+
"prettier": "^3.8.0",
|
|
45
|
+
"tsx": "^4.21.0",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vitest": "^4.0.17"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"better-sqlite3": "^12.6.2",
|
|
51
|
+
"busboy": "^1.6.0",
|
|
52
|
+
"express": "^5.2.1",
|
|
53
|
+
"fast-json-stringify": "^5.15.1",
|
|
54
|
+
"fastify": "^5.7.1",
|
|
55
|
+
"pino": "^10.2.0",
|
|
56
|
+
"pino-pretty": "^13.1.3",
|
|
57
|
+
"quantam-async": "^0.1.1",
|
|
58
|
+
"ws": "^8.19.0",
|
|
59
|
+
"zod": "^4.3.5"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import autocannon from 'autocannon';
|
|
2
|
+
import fastify from 'fastify';
|
|
3
|
+
import express, { Request, Response } from 'express';
|
|
4
|
+
import { QHTTPX } from '../index';
|
|
5
|
+
|
|
6
|
+
type BenchResult = {
|
|
7
|
+
name: string;
|
|
8
|
+
total: number;
|
|
9
|
+
sent: number;
|
|
10
|
+
rps: number;
|
|
11
|
+
p99: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function runAutocannon(name: string, url: string): Promise<BenchResult> {
|
|
15
|
+
const result = await autocannon({
|
|
16
|
+
url,
|
|
17
|
+
connections: 200,
|
|
18
|
+
pipelining: 10,
|
|
19
|
+
duration: 10,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const total = result.requests.total;
|
|
23
|
+
const sent = result.requests.sent;
|
|
24
|
+
const rps = result.requests.average;
|
|
25
|
+
const p99 = result.latency.p99;
|
|
26
|
+
|
|
27
|
+
console.log(
|
|
28
|
+
`${name} bench: total=${total} (sent=${sent}) req, ` +
|
|
29
|
+
`${rps.toFixed(0)} req/sec, p99=${p99.toFixed(
|
|
30
|
+
1,
|
|
31
|
+
)}ms, connections=${result.connections}, pipelining=${
|
|
32
|
+
result.pipelining
|
|
33
|
+
}`,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
total,
|
|
39
|
+
sent,
|
|
40
|
+
rps,
|
|
41
|
+
p99,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function startQHTTPX() {
|
|
46
|
+
const payloadBuffer = Buffer.from(
|
|
47
|
+
JSON.stringify({ message: 'hello from qhttpx' }),
|
|
48
|
+
);
|
|
49
|
+
const app = new QHTTPX({
|
|
50
|
+
maxConcurrency: 1024,
|
|
51
|
+
metricsEnabled: false,
|
|
52
|
+
jsonSerializer: () => payloadBuffer,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
app.get('/json', (ctx) => {
|
|
56
|
+
ctx.json({ message: 'hello from qhttpx' });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
60
|
+
const url = `http://127.0.0.1:${port}/json`;
|
|
61
|
+
return { app, url };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function startFastify() {
|
|
65
|
+
const app = fastify();
|
|
66
|
+
|
|
67
|
+
app.get('/json', async () => {
|
|
68
|
+
return { message: 'hello from fastify' };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await app.listen({ port: 0, host: '127.0.0.1' });
|
|
72
|
+
const address = app.server.address();
|
|
73
|
+
if (!address || typeof address === 'string') {
|
|
74
|
+
throw new Error('Fastify address not available');
|
|
75
|
+
}
|
|
76
|
+
const url = `http://127.0.0.1:${address.port}/json`;
|
|
77
|
+
return { app, url };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function startExpress() {
|
|
81
|
+
const app = express();
|
|
82
|
+
app.get('/json', (_req: Request, res: Response) => {
|
|
83
|
+
res.json({ message: 'hello from express' });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const server = await new Promise<import('http').Server>((resolve) => {
|
|
87
|
+
const s = app.listen(0, '127.0.0.1', () => {
|
|
88
|
+
resolve(s);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
const address = server.address();
|
|
92
|
+
if (!address || typeof address === 'string') {
|
|
93
|
+
throw new Error('Express address not available');
|
|
94
|
+
}
|
|
95
|
+
const url = `http://127.0.0.1:${address.port}/json`;
|
|
96
|
+
return { app, url, server };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function run() {
|
|
100
|
+
const results: BenchResult[] = [];
|
|
101
|
+
|
|
102
|
+
const qhttpx = await startQHTTPX();
|
|
103
|
+
try {
|
|
104
|
+
const r = await runAutocannon('QHTTPX', qhttpx.url);
|
|
105
|
+
results.push(r);
|
|
106
|
+
} finally {
|
|
107
|
+
await qhttpx.app.close();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const fast = await startFastify();
|
|
111
|
+
try {
|
|
112
|
+
const r = await runAutocannon('Fastify', fast.url);
|
|
113
|
+
results.push(r);
|
|
114
|
+
} finally {
|
|
115
|
+
await fast.app.close();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const exp = await startExpress();
|
|
119
|
+
try {
|
|
120
|
+
const r = await runAutocannon('Express', exp.url);
|
|
121
|
+
results.push(r);
|
|
122
|
+
} finally {
|
|
123
|
+
await new Promise<void>((resolve, reject) => {
|
|
124
|
+
exp.server.close((err: unknown) => {
|
|
125
|
+
if (err) {
|
|
126
|
+
reject(err);
|
|
127
|
+
} else {
|
|
128
|
+
resolve();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log('\nSummary:');
|
|
135
|
+
for (const r of results) {
|
|
136
|
+
console.log(
|
|
137
|
+
`${r.name}: ${r.rps.toFixed(0)} req/sec, p99=${r.p99.toFixed(
|
|
138
|
+
1,
|
|
139
|
+
)}ms, total=${r.total}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
run().catch((err) => {
|
|
147
|
+
console.error(err);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
});
|