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.
Files changed (197) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/.github/workflows/npm-publish.yml +37 -0
  4. package/.github/workflows/release.yml +21 -0
  5. package/.prettierrc +7 -0
  6. package/CHANGELOG.md +145 -0
  7. package/LICENSE +21 -0
  8. package/README.md +343 -0
  9. package/dist/package.json +61 -0
  10. package/dist/src/benchmarks/compare-frameworks.js +119 -0
  11. package/dist/src/benchmarks/quantam-users.js +56 -0
  12. package/dist/src/benchmarks/simple-json.js +58 -0
  13. package/dist/src/benchmarks/ultra-mode.js +122 -0
  14. package/dist/src/cli/index.js +200 -0
  15. package/dist/src/client/index.js +72 -0
  16. package/dist/src/core/batch.js +97 -0
  17. package/dist/src/core/body-parser.js +121 -0
  18. package/dist/src/core/buffer-pool.js +70 -0
  19. package/dist/src/core/config.js +50 -0
  20. package/dist/src/core/fusion.js +183 -0
  21. package/dist/src/core/logger.js +49 -0
  22. package/dist/src/core/metrics.js +111 -0
  23. package/dist/src/core/resources.js +25 -0
  24. package/dist/src/core/scheduler.js +85 -0
  25. package/dist/src/core/scope.js +68 -0
  26. package/dist/src/core/serializer.js +44 -0
  27. package/dist/src/core/server.js +905 -0
  28. package/dist/src/core/stream.js +71 -0
  29. package/dist/src/core/tasks.js +87 -0
  30. package/dist/src/core/types.js +19 -0
  31. package/dist/src/core/websocket.js +86 -0
  32. package/dist/src/core/worker-queue.js +73 -0
  33. package/dist/src/database/adapters/memory.js +90 -0
  34. package/dist/src/database/adapters/mongo.js +141 -0
  35. package/dist/src/database/adapters/postgres.js +111 -0
  36. package/dist/src/database/adapters/sqlite.js +42 -0
  37. package/dist/src/database/coalescer.js +134 -0
  38. package/dist/src/database/manager.js +87 -0
  39. package/dist/src/database/types.js +2 -0
  40. package/dist/src/index.js +61 -0
  41. package/dist/src/middleware/compression.js +133 -0
  42. package/dist/src/middleware/cors.js +66 -0
  43. package/dist/src/middleware/presets.js +33 -0
  44. package/dist/src/middleware/rate-limit.js +77 -0
  45. package/dist/src/middleware/security.js +69 -0
  46. package/dist/src/middleware/static.js +191 -0
  47. package/dist/src/openapi/generator.js +149 -0
  48. package/dist/src/router/radix-router.js +89 -0
  49. package/dist/src/router/radix-tree.js +81 -0
  50. package/dist/src/router/router.js +146 -0
  51. package/dist/src/testing/index.js +84 -0
  52. package/dist/src/utils/cookies.js +59 -0
  53. package/dist/src/utils/logger.js +45 -0
  54. package/dist/src/utils/signals.js +31 -0
  55. package/dist/src/utils/sse.js +32 -0
  56. package/dist/src/validation/index.js +19 -0
  57. package/dist/src/validation/simple.js +102 -0
  58. package/dist/src/validation/types.js +12 -0
  59. package/dist/src/validation/zod.js +18 -0
  60. package/dist/src/views/index.js +17 -0
  61. package/dist/src/views/types.js +2 -0
  62. package/dist/tests/adapters.test.js +106 -0
  63. package/dist/tests/batch.test.js +117 -0
  64. package/dist/tests/body-parser.test.js +52 -0
  65. package/dist/tests/compression-sse.test.js +87 -0
  66. package/dist/tests/cookies.test.js +63 -0
  67. package/dist/tests/cors.test.js +55 -0
  68. package/dist/tests/database.test.js +80 -0
  69. package/dist/tests/dx.test.js +64 -0
  70. package/dist/tests/ecosystem.test.js +133 -0
  71. package/dist/tests/features.test.js +47 -0
  72. package/dist/tests/fusion.test.js +92 -0
  73. package/dist/tests/http-basic.test.js +124 -0
  74. package/dist/tests/logger.test.js +33 -0
  75. package/dist/tests/middleware.test.js +109 -0
  76. package/dist/tests/observability.test.js +59 -0
  77. package/dist/tests/openapi.test.js +64 -0
  78. package/dist/tests/plugin.test.js +65 -0
  79. package/dist/tests/plugins.test.js +71 -0
  80. package/dist/tests/rate-limit.test.js +77 -0
  81. package/dist/tests/resources.test.js +44 -0
  82. package/dist/tests/scheduler.test.js +46 -0
  83. package/dist/tests/schema-routes.test.js +77 -0
  84. package/dist/tests/security.test.js +83 -0
  85. package/dist/tests/server-db.test.js +72 -0
  86. package/dist/tests/smoke.test.js +10 -0
  87. package/dist/tests/sqlite-fusion.test.js +92 -0
  88. package/dist/tests/static.test.js +102 -0
  89. package/dist/tests/stream.test.js +44 -0
  90. package/dist/tests/task-metrics.test.js +53 -0
  91. package/dist/tests/tasks.test.js +62 -0
  92. package/dist/tests/testing.test.js +47 -0
  93. package/dist/tests/validation.test.js +107 -0
  94. package/dist/tests/websocket.test.js +146 -0
  95. package/dist/vitest.config.js +9 -0
  96. package/docs/AEGIS.md +76 -0
  97. package/docs/BENCHMARKS.md +36 -0
  98. package/docs/CAPABILITIES.md +70 -0
  99. package/docs/CLI.md +43 -0
  100. package/docs/DATABASE.md +142 -0
  101. package/docs/ECOSYSTEM.md +146 -0
  102. package/docs/NEXT_STEPS.md +99 -0
  103. package/docs/OPENAPI.md +99 -0
  104. package/docs/PLUGINS.md +59 -0
  105. package/docs/REAL_WORLD_EXAMPLES.md +109 -0
  106. package/docs/ROADMAP.md +366 -0
  107. package/docs/VALIDATION.md +136 -0
  108. package/eslint.config.cjs +26 -0
  109. package/examples/api-server.ts +254 -0
  110. package/package.json +61 -0
  111. package/src/benchmarks/compare-frameworks.ts +149 -0
  112. package/src/benchmarks/quantam-users.ts +70 -0
  113. package/src/benchmarks/simple-json.ts +71 -0
  114. package/src/benchmarks/ultra-mode.ts +159 -0
  115. package/src/cli/index.ts +214 -0
  116. package/src/client/index.ts +93 -0
  117. package/src/core/batch.ts +110 -0
  118. package/src/core/body-parser.ts +151 -0
  119. package/src/core/buffer-pool.ts +96 -0
  120. package/src/core/config.ts +60 -0
  121. package/src/core/fusion.ts +210 -0
  122. package/src/core/logger.ts +70 -0
  123. package/src/core/metrics.ts +166 -0
  124. package/src/core/resources.ts +38 -0
  125. package/src/core/scheduler.ts +126 -0
  126. package/src/core/scope.ts +87 -0
  127. package/src/core/serializer.ts +41 -0
  128. package/src/core/server.ts +1113 -0
  129. package/src/core/stream.ts +111 -0
  130. package/src/core/tasks.ts +138 -0
  131. package/src/core/types.ts +178 -0
  132. package/src/core/websocket.ts +112 -0
  133. package/src/core/worker-queue.ts +90 -0
  134. package/src/database/adapters/memory.ts +99 -0
  135. package/src/database/adapters/mongo.ts +116 -0
  136. package/src/database/adapters/postgres.ts +86 -0
  137. package/src/database/adapters/sqlite.ts +44 -0
  138. package/src/database/coalescer.ts +153 -0
  139. package/src/database/manager.ts +97 -0
  140. package/src/database/types.ts +24 -0
  141. package/src/index.ts +42 -0
  142. package/src/middleware/compression.ts +147 -0
  143. package/src/middleware/cors.ts +98 -0
  144. package/src/middleware/presets.ts +50 -0
  145. package/src/middleware/rate-limit.ts +106 -0
  146. package/src/middleware/security.ts +109 -0
  147. package/src/middleware/static.ts +216 -0
  148. package/src/openapi/generator.ts +167 -0
  149. package/src/router/radix-router.ts +119 -0
  150. package/src/router/radix-tree.ts +106 -0
  151. package/src/router/router.ts +190 -0
  152. package/src/testing/index.ts +104 -0
  153. package/src/utils/cookies.ts +67 -0
  154. package/src/utils/logger.ts +59 -0
  155. package/src/utils/signals.ts +45 -0
  156. package/src/utils/sse.ts +41 -0
  157. package/src/validation/index.ts +3 -0
  158. package/src/validation/simple.ts +93 -0
  159. package/src/validation/types.ts +38 -0
  160. package/src/validation/zod.ts +14 -0
  161. package/src/views/index.ts +1 -0
  162. package/src/views/types.ts +4 -0
  163. package/tests/adapters.test.ts +120 -0
  164. package/tests/batch.test.ts +139 -0
  165. package/tests/body-parser.test.ts +83 -0
  166. package/tests/compression-sse.test.ts +98 -0
  167. package/tests/cookies.test.ts +74 -0
  168. package/tests/cors.test.ts +79 -0
  169. package/tests/database.test.ts +90 -0
  170. package/tests/dx.test.ts +78 -0
  171. package/tests/ecosystem.test.ts +156 -0
  172. package/tests/features.test.ts +51 -0
  173. package/tests/fusion.test.ts +121 -0
  174. package/tests/http-basic.test.ts +161 -0
  175. package/tests/logger.test.ts +48 -0
  176. package/tests/middleware.test.ts +137 -0
  177. package/tests/observability.test.ts +91 -0
  178. package/tests/openapi.test.ts +74 -0
  179. package/tests/plugin.test.ts +85 -0
  180. package/tests/plugins.test.ts +93 -0
  181. package/tests/rate-limit.test.ts +97 -0
  182. package/tests/resources.test.ts +64 -0
  183. package/tests/scheduler.test.ts +71 -0
  184. package/tests/schema-routes.test.ts +89 -0
  185. package/tests/security.test.ts +128 -0
  186. package/tests/server-db.test.ts +72 -0
  187. package/tests/smoke.test.ts +9 -0
  188. package/tests/sqlite-fusion.test.ts +106 -0
  189. package/tests/static.test.ts +111 -0
  190. package/tests/stream.test.ts +58 -0
  191. package/tests/task-metrics.test.ts +78 -0
  192. package/tests/tasks.test.ts +90 -0
  193. package/tests/testing.test.ts +53 -0
  194. package/tests/validation.test.ts +126 -0
  195. package/tests/websocket.test.ts +132 -0
  196. package/tsconfig.json +16 -0
  197. 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
+ });