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,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const autocannon_1 = __importDefault(require("autocannon"));
|
|
7
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
8
|
+
const express_1 = __importDefault(require("express"));
|
|
9
|
+
const index_1 = require("../index");
|
|
10
|
+
async function runAutocannon(name, url) {
|
|
11
|
+
const result = await (0, autocannon_1.default)({
|
|
12
|
+
url,
|
|
13
|
+
connections: 200,
|
|
14
|
+
pipelining: 10,
|
|
15
|
+
duration: 10,
|
|
16
|
+
});
|
|
17
|
+
const total = result.requests.total;
|
|
18
|
+
const sent = result.requests.sent;
|
|
19
|
+
const rps = result.requests.average;
|
|
20
|
+
const p99 = result.latency.p99;
|
|
21
|
+
console.log(`${name} bench: total=${total} (sent=${sent}) req, ` +
|
|
22
|
+
`${rps.toFixed(0)} req/sec, p99=${p99.toFixed(1)}ms, connections=${result.connections}, pipelining=${result.pipelining}`);
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
total,
|
|
26
|
+
sent,
|
|
27
|
+
rps,
|
|
28
|
+
p99,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function startQHTTPX() {
|
|
32
|
+
const payloadBuffer = Buffer.from(JSON.stringify({ message: 'hello from qhttpx' }));
|
|
33
|
+
const app = new index_1.QHTTPX({
|
|
34
|
+
maxConcurrency: 1024,
|
|
35
|
+
metricsEnabled: false,
|
|
36
|
+
jsonSerializer: () => payloadBuffer,
|
|
37
|
+
});
|
|
38
|
+
app.get('/json', (ctx) => {
|
|
39
|
+
ctx.json({ message: 'hello from qhttpx' });
|
|
40
|
+
});
|
|
41
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
42
|
+
const url = `http://127.0.0.1:${port}/json`;
|
|
43
|
+
return { app, url };
|
|
44
|
+
}
|
|
45
|
+
async function startFastify() {
|
|
46
|
+
const app = (0, fastify_1.default)();
|
|
47
|
+
app.get('/json', async () => {
|
|
48
|
+
return { message: 'hello from fastify' };
|
|
49
|
+
});
|
|
50
|
+
await app.listen({ port: 0, host: '127.0.0.1' });
|
|
51
|
+
const address = app.server.address();
|
|
52
|
+
if (!address || typeof address === 'string') {
|
|
53
|
+
throw new Error('Fastify address not available');
|
|
54
|
+
}
|
|
55
|
+
const url = `http://127.0.0.1:${address.port}/json`;
|
|
56
|
+
return { app, url };
|
|
57
|
+
}
|
|
58
|
+
async function startExpress() {
|
|
59
|
+
const app = (0, express_1.default)();
|
|
60
|
+
app.get('/json', (_req, res) => {
|
|
61
|
+
res.json({ message: 'hello from express' });
|
|
62
|
+
});
|
|
63
|
+
const server = await new Promise((resolve) => {
|
|
64
|
+
const s = app.listen(0, '127.0.0.1', () => {
|
|
65
|
+
resolve(s);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
const address = server.address();
|
|
69
|
+
if (!address || typeof address === 'string') {
|
|
70
|
+
throw new Error('Express address not available');
|
|
71
|
+
}
|
|
72
|
+
const url = `http://127.0.0.1:${address.port}/json`;
|
|
73
|
+
return { app, url, server };
|
|
74
|
+
}
|
|
75
|
+
async function run() {
|
|
76
|
+
const results = [];
|
|
77
|
+
const qhttpx = await startQHTTPX();
|
|
78
|
+
try {
|
|
79
|
+
const r = await runAutocannon('QHTTPX', qhttpx.url);
|
|
80
|
+
results.push(r);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await qhttpx.app.close();
|
|
84
|
+
}
|
|
85
|
+
const fast = await startFastify();
|
|
86
|
+
try {
|
|
87
|
+
const r = await runAutocannon('Fastify', fast.url);
|
|
88
|
+
results.push(r);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await fast.app.close();
|
|
92
|
+
}
|
|
93
|
+
const exp = await startExpress();
|
|
94
|
+
try {
|
|
95
|
+
const r = await runAutocannon('Express', exp.url);
|
|
96
|
+
results.push(r);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await new Promise((resolve, reject) => {
|
|
100
|
+
exp.server.close((err) => {
|
|
101
|
+
if (err) {
|
|
102
|
+
reject(err);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
resolve();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
console.log('\nSummary:');
|
|
111
|
+
for (const r of results) {
|
|
112
|
+
console.log(`${r.name}: ${r.rps.toFixed(0)} req/sec, p99=${r.p99.toFixed(1)}ms, total=${r.total}`);
|
|
113
|
+
}
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
run().catch((err) => {
|
|
117
|
+
console.error(err);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const quantam_async_1 = require("quantam-async");
|
|
4
|
+
const index_1 = require("../index");
|
|
5
|
+
async function run() {
|
|
6
|
+
const app = new index_1.QHTTPX({
|
|
7
|
+
maxConcurrency: 512,
|
|
8
|
+
requestTimeoutMs: 10000,
|
|
9
|
+
});
|
|
10
|
+
app.get('/json', (ctx) => {
|
|
11
|
+
ctx.json({ message: 'hello from qhttpx' });
|
|
12
|
+
});
|
|
13
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
14
|
+
const url = `http://127.0.0.1:${port}/json`;
|
|
15
|
+
const requestsPerUser = 10;
|
|
16
|
+
const userCount = 10000;
|
|
17
|
+
const maxConcurrentUsers = 1000;
|
|
18
|
+
const flow = (0, quantam_async_1.quantam)()
|
|
19
|
+
.name('user-flow')
|
|
20
|
+
.step(async (userId) => {
|
|
21
|
+
for (let i = 0; i < requestsPerUser; i += 1) {
|
|
22
|
+
const response = await fetch(url);
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`user ${userId} request ${i} failed with status ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
await response.text();
|
|
27
|
+
}
|
|
28
|
+
return userId;
|
|
29
|
+
})
|
|
30
|
+
.retry(3, 50)
|
|
31
|
+
.timeout(30000);
|
|
32
|
+
const inputs = [];
|
|
33
|
+
for (let i = 0; i < userCount; i += 1) {
|
|
34
|
+
inputs.push(i);
|
|
35
|
+
}
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
try {
|
|
38
|
+
await flow.runMany(inputs, {
|
|
39
|
+
concurrency: maxConcurrentUsers,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error('Quantam runMany error', err);
|
|
44
|
+
}
|
|
45
|
+
const durationSec = (Date.now() - start) / 1000;
|
|
46
|
+
const totalRequests = userCount * requestsPerUser;
|
|
47
|
+
const rps = totalRequests / durationSec;
|
|
48
|
+
console.log(`Quantam bench: users=${userCount}, requestsPerUser=${requestsPerUser}, ` +
|
|
49
|
+
`totalRequests=${totalRequests}, duration=${durationSec.toFixed(2)}s, rps=${rps.toFixed(0)}, concurrency=${maxConcurrentUsers}`);
|
|
50
|
+
await app.close();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
run().catch((err) => {
|
|
54
|
+
console.error(err);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const autocannon_1 = __importDefault(require("autocannon"));
|
|
7
|
+
const index_1 = require("../index");
|
|
8
|
+
function runAutocannon(url) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const instance = (0, autocannon_1.default)({
|
|
11
|
+
url,
|
|
12
|
+
amount: 10000,
|
|
13
|
+
connections: 10000,
|
|
14
|
+
pipelining: 1,
|
|
15
|
+
duration: 10,
|
|
16
|
+
}, (err, result) => {
|
|
17
|
+
if (err) {
|
|
18
|
+
reject(err);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
resolve(result);
|
|
22
|
+
});
|
|
23
|
+
instance.on('error', (err) => {
|
|
24
|
+
reject(err);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function run() {
|
|
29
|
+
const payloadBuffer = Buffer.from(JSON.stringify({ message: 'hello from qhttpx' }));
|
|
30
|
+
const app = new index_1.QHTTPX({
|
|
31
|
+
maxConcurrency: 512,
|
|
32
|
+
metricsEnabled: false,
|
|
33
|
+
jsonSerializer: () => payloadBuffer,
|
|
34
|
+
});
|
|
35
|
+
app.get('/json', (ctx) => {
|
|
36
|
+
ctx.json({ message: 'hello from qhttpx' });
|
|
37
|
+
});
|
|
38
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
39
|
+
const url = `http://127.0.0.1:${port}/json`;
|
|
40
|
+
const result = await runAutocannon(url);
|
|
41
|
+
autocannon_1.default.printResult(result);
|
|
42
|
+
const totalRequests = result.requests.total;
|
|
43
|
+
const sent = result.requests.sent;
|
|
44
|
+
const avgRps = result.requests.average.toFixed(0);
|
|
45
|
+
const p99 = result.latency.p99.toFixed(1);
|
|
46
|
+
const connections = result.connections;
|
|
47
|
+
const pipelining = result.pipelining;
|
|
48
|
+
// Summary line that shows up clearly in CI/dev logs
|
|
49
|
+
console.log(`QHTTPX bench: total=${totalRequests} (sent=${sent}) req, ` +
|
|
50
|
+
`${avgRps} req/sec, p99=${p99}ms, connections=${connections}, ` +
|
|
51
|
+
`pipelining=${pipelining}`);
|
|
52
|
+
await app.close();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
run().catch((err) => {
|
|
56
|
+
console.error(err);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const autocannon_1 = __importDefault(require("autocannon"));
|
|
7
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
8
|
+
const index_1 = require("../index");
|
|
9
|
+
async function runAutocannon(name, url) {
|
|
10
|
+
const result = await (0, autocannon_1.default)({
|
|
11
|
+
url,
|
|
12
|
+
connections: 200,
|
|
13
|
+
pipelining: 10,
|
|
14
|
+
duration: 10,
|
|
15
|
+
});
|
|
16
|
+
const total = result.requests.total;
|
|
17
|
+
const sent = result.requests.sent;
|
|
18
|
+
const rps = result.requests.average;
|
|
19
|
+
const p99 = result.latency.p99;
|
|
20
|
+
console.log(`${name} bench: total=${total} (sent=${sent}) req, ` +
|
|
21
|
+
`${rps.toFixed(0)} req/sec, p99=${p99.toFixed(1)}ms, connections=${result.connections}, pipelining=${result.pipelining}`);
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
total,
|
|
25
|
+
sent,
|
|
26
|
+
rps,
|
|
27
|
+
p99,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function startQHTTPXBalanced() {
|
|
31
|
+
const payloadBuffer = Buffer.from(JSON.stringify({ message: 'hello from qhttpx' }));
|
|
32
|
+
const app = new index_1.QHTTPX({
|
|
33
|
+
maxConcurrency: 1024,
|
|
34
|
+
metricsEnabled: false,
|
|
35
|
+
performanceMode: 'balanced',
|
|
36
|
+
jsonSerializer: () => payloadBuffer,
|
|
37
|
+
});
|
|
38
|
+
app.get('/json', (ctx) => {
|
|
39
|
+
ctx.json({ message: 'hello from qhttpx' });
|
|
40
|
+
});
|
|
41
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
42
|
+
const url = `http://127.0.0.1:${port}/json`;
|
|
43
|
+
return { app, url };
|
|
44
|
+
}
|
|
45
|
+
async function startQHTTPXUltra() {
|
|
46
|
+
const payloadBuffer = Buffer.from(JSON.stringify({ message: 'hello from qhttpx ultra' }));
|
|
47
|
+
const app = new index_1.QHTTPX({
|
|
48
|
+
maxConcurrency: 1024,
|
|
49
|
+
performanceMode: 'ultra',
|
|
50
|
+
jsonSerializer: () => payloadBuffer,
|
|
51
|
+
});
|
|
52
|
+
app.get('/json', (ctx) => {
|
|
53
|
+
ctx.json({ message: 'hello from qhttpx ultra' });
|
|
54
|
+
});
|
|
55
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
56
|
+
const url = `http://127.0.0.1:${port}/json`;
|
|
57
|
+
return { app, url };
|
|
58
|
+
}
|
|
59
|
+
async function startFastify() {
|
|
60
|
+
const app = (0, fastify_1.default)();
|
|
61
|
+
app.get('/json', async () => {
|
|
62
|
+
return { message: 'hello from fastify' };
|
|
63
|
+
});
|
|
64
|
+
await app.listen({ port: 0, host: '127.0.0.1' });
|
|
65
|
+
const address = app.server.address();
|
|
66
|
+
if (!address || typeof address === 'string') {
|
|
67
|
+
throw new Error('Fastify address not available');
|
|
68
|
+
}
|
|
69
|
+
const url = `http://127.0.0.1:${address.port}/json`;
|
|
70
|
+
return { app, url };
|
|
71
|
+
}
|
|
72
|
+
async function run() {
|
|
73
|
+
const results = [];
|
|
74
|
+
console.log('=== QHTTPX Balanced Mode ===');
|
|
75
|
+
const balanced = await startQHTTPXBalanced();
|
|
76
|
+
try {
|
|
77
|
+
const r = await runAutocannon('QHTTPX-Balanced', balanced.url);
|
|
78
|
+
results.push(r);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await balanced.app.close();
|
|
82
|
+
}
|
|
83
|
+
console.log('\n=== QHTTPX Ultra Mode ===');
|
|
84
|
+
const ultra = await startQHTTPXUltra();
|
|
85
|
+
try {
|
|
86
|
+
const r = await runAutocannon('QHTTPX-Ultra', ultra.url);
|
|
87
|
+
results.push(r);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await ultra.app.close();
|
|
91
|
+
}
|
|
92
|
+
console.log('\n=== Fastify Baseline ===');
|
|
93
|
+
const fast = await startFastify();
|
|
94
|
+
try {
|
|
95
|
+
const r = await runAutocannon('Fastify', fast.url);
|
|
96
|
+
results.push(r);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await fast.app.close();
|
|
100
|
+
}
|
|
101
|
+
console.log('\n=== Summary ===');
|
|
102
|
+
for (const r of results) {
|
|
103
|
+
console.log(`${r.name}: ${r.rps.toFixed(0)} req/sec, p99=${r.p99.toFixed(1)}ms, total=${r.total}`);
|
|
104
|
+
}
|
|
105
|
+
// Calculate improvement
|
|
106
|
+
const balanced_rps = results[0]?.rps || 0;
|
|
107
|
+
const ultra_rps = results[1]?.rps || 0;
|
|
108
|
+
const fastify_rps = results[2]?.rps || 0;
|
|
109
|
+
if (ultra_rps > 0 && balanced_rps > 0) {
|
|
110
|
+
const improvement = ((ultra_rps - balanced_rps) / balanced_rps * 100).toFixed(1);
|
|
111
|
+
console.log(`\nUltra vs Balanced improvement: ${improvement}%`);
|
|
112
|
+
}
|
|
113
|
+
if (ultra_rps > 0 && fastify_rps > 0) {
|
|
114
|
+
const ratio = (ultra_rps / fastify_rps).toFixed(2);
|
|
115
|
+
console.log(`Ultra vs Fastify ratio: ${ratio}x`);
|
|
116
|
+
}
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
run().catch((err) => {
|
|
120
|
+
console.error(err);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const cluster_1 = __importDefault(require("cluster"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const command = args[0];
|
|
14
|
+
if (!command) {
|
|
15
|
+
printHelp();
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const targetFile = args[1];
|
|
19
|
+
if (command === 'start') {
|
|
20
|
+
if (!targetFile) {
|
|
21
|
+
console.error('Error: Please provide an entry file (e.g., qhttpx start dist/index.js)');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
runStart(targetFile);
|
|
25
|
+
}
|
|
26
|
+
else if (command === 'dev') {
|
|
27
|
+
if (!targetFile) {
|
|
28
|
+
console.error('Error: Please provide an entry file (e.g., qhttpx dev src/index.ts)');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
runDev(targetFile);
|
|
32
|
+
}
|
|
33
|
+
else if (command === 'new') {
|
|
34
|
+
if (!targetFile) {
|
|
35
|
+
console.error('Error: Please provide a project name (e.g., qhttpx new my-app)');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
runNew(targetFile);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.error(`Unknown command: ${command}`);
|
|
42
|
+
printHelp();
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
function printHelp() {
|
|
46
|
+
console.log('Usage: qhttpx <command> [options]');
|
|
47
|
+
console.log('Commands:');
|
|
48
|
+
console.log(' start <file> Run in production cluster mode (Node.js)');
|
|
49
|
+
console.log(' dev <file> Run in development mode with hot reload (tsx)');
|
|
50
|
+
console.log(' new <name> Create a new QHTTPX project');
|
|
51
|
+
}
|
|
52
|
+
function runStart(entry) {
|
|
53
|
+
const absoluteEntry = path_1.default.resolve(process.cwd(), entry);
|
|
54
|
+
if (cluster_1.default.isPrimary) {
|
|
55
|
+
const numCPUs = os_1.default.cpus().length;
|
|
56
|
+
console.log(`[QCLI] 🚀 Starting Cluster Mode`);
|
|
57
|
+
console.log(`[QCLI] 📂 Entry: ${absoluteEntry}`);
|
|
58
|
+
console.log(`[QCLI] 💻 Workers: ${numCPUs}`);
|
|
59
|
+
for (let i = 0; i < numCPUs; i++) {
|
|
60
|
+
cluster_1.default.fork();
|
|
61
|
+
}
|
|
62
|
+
cluster_1.default.on('exit', (worker) => {
|
|
63
|
+
console.log(`[QCLI] 💀 Worker ${worker.process.pid} died. Restarting...`);
|
|
64
|
+
cluster_1.default.fork();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
try {
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
70
|
+
require(absoluteEntry);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(`[QCLI] Failed to load entry file: ${absoluteEntry}`);
|
|
74
|
+
console.error(err);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function runDev(entry) {
|
|
80
|
+
const absoluteEntry = path_1.default.resolve(process.cwd(), entry);
|
|
81
|
+
console.log(`[QCLI] 🛠️ Starting Dev Mode`);
|
|
82
|
+
console.log(`[QCLI] 📂 Entry: ${absoluteEntry}`);
|
|
83
|
+
let child = null;
|
|
84
|
+
const startChild = () => {
|
|
85
|
+
// Use tsx for dev execution
|
|
86
|
+
child = (0, child_process_1.spawn)('npx', ['tsx', absoluteEntry], {
|
|
87
|
+
stdio: 'inherit',
|
|
88
|
+
shell: true,
|
|
89
|
+
cwd: process.cwd()
|
|
90
|
+
});
|
|
91
|
+
child.on('close', (code) => {
|
|
92
|
+
if (code !== 0 && code !== null) {
|
|
93
|
+
console.log(`[QCLI] App crashed (code ${code}). Waiting for changes...`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
const restartChild = () => {
|
|
98
|
+
if (child) {
|
|
99
|
+
console.log('[QCLI] 🔄 File changed. Restarting...');
|
|
100
|
+
// On Windows, killing a shell-spawned process is tricky.
|
|
101
|
+
// But 'child.kill()' usually works for the handle.
|
|
102
|
+
// For deeper tree killing, we might need a library, but let's try standard kill.
|
|
103
|
+
child.kill();
|
|
104
|
+
child = null;
|
|
105
|
+
}
|
|
106
|
+
startChild();
|
|
107
|
+
};
|
|
108
|
+
startChild();
|
|
109
|
+
// Watcher
|
|
110
|
+
let debounceTimer;
|
|
111
|
+
const watchDir = path_1.default.dirname(absoluteEntry); // Watch the directory of the entry file (usually src)
|
|
112
|
+
console.log(`[QCLI] 👀 Watching: ${watchDir}`);
|
|
113
|
+
// Recursive watch is platform dependent, but 'recursive: true' works on Windows/macOS
|
|
114
|
+
try {
|
|
115
|
+
fs_1.default.watch(watchDir, { recursive: true }, (eventType, filename) => {
|
|
116
|
+
if (!filename)
|
|
117
|
+
return;
|
|
118
|
+
// Ignore node_modules and .git
|
|
119
|
+
if (filename.includes('node_modules') || filename.includes('.git'))
|
|
120
|
+
return;
|
|
121
|
+
clearTimeout(debounceTimer);
|
|
122
|
+
debounceTimer = setTimeout(() => {
|
|
123
|
+
restartChild();
|
|
124
|
+
}, 200); // 200ms debounce
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
console.warn('[QCLI] Recursive watch not supported or failed. Falling back to simple watch.');
|
|
129
|
+
// Fallback or just fail gracefully
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function runNew(projectName) {
|
|
133
|
+
const projectDir = path_1.default.resolve(process.cwd(), projectName);
|
|
134
|
+
if (fs_1.default.existsSync(projectDir)) {
|
|
135
|
+
console.error(`Error: Directory ${projectName} already exists.`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
console.log(`[QCLI] ✨ Creating new QHTTPX project in ${projectName}...`);
|
|
139
|
+
fs_1.default.mkdirSync(projectDir);
|
|
140
|
+
fs_1.default.mkdirSync(path_1.default.join(projectDir, 'src'));
|
|
141
|
+
// package.json
|
|
142
|
+
const pkgJson = {
|
|
143
|
+
name: projectName,
|
|
144
|
+
version: '0.0.1',
|
|
145
|
+
scripts: {
|
|
146
|
+
dev: 'qhttpx dev src/index.ts',
|
|
147
|
+
start: 'qhttpx start dist/index.js',
|
|
148
|
+
build: 'tsc'
|
|
149
|
+
},
|
|
150
|
+
dependencies: {
|
|
151
|
+
'qhttpx': '^1.0.0'
|
|
152
|
+
},
|
|
153
|
+
devDependencies: {
|
|
154
|
+
'typescript': '^5.0.0',
|
|
155
|
+
'@types/node': '^20.0.0',
|
|
156
|
+
'tsx': '^4.0.0'
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
fs_1.default.writeFileSync(path_1.default.join(projectDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
|
|
160
|
+
// tsconfig.json
|
|
161
|
+
const tsConfig = {
|
|
162
|
+
compilerOptions: {
|
|
163
|
+
target: 'ES2020',
|
|
164
|
+
module: 'commonjs',
|
|
165
|
+
rootDir: 'src',
|
|
166
|
+
outDir: 'dist',
|
|
167
|
+
strict: true,
|
|
168
|
+
esModuleInterop: true,
|
|
169
|
+
skipLibCheck: true,
|
|
170
|
+
forceConsistentCasingInFileNames: true
|
|
171
|
+
},
|
|
172
|
+
include: ['src']
|
|
173
|
+
};
|
|
174
|
+
fs_1.default.writeFileSync(path_1.default.join(projectDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
|
|
175
|
+
// src/index.ts
|
|
176
|
+
const indexTs = `import { createHttpApp } from 'qhttpx';
|
|
177
|
+
|
|
178
|
+
const app = createHttpApp();
|
|
179
|
+
|
|
180
|
+
app.get('/', (ctx) => {
|
|
181
|
+
ctx.json({ message: 'Hello from QHTTPX!' });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
app.listen(3000).then(({ port }) => {
|
|
185
|
+
console.log(\`Server running on port \${port}\`);
|
|
186
|
+
});
|
|
187
|
+
`;
|
|
188
|
+
fs_1.default.writeFileSync(path_1.default.join(projectDir, 'src', 'index.ts'), indexTs);
|
|
189
|
+
// .gitignore
|
|
190
|
+
const gitignore = `node_modules
|
|
191
|
+
dist
|
|
192
|
+
.env
|
|
193
|
+
`;
|
|
194
|
+
fs_1.default.writeFileSync(path_1.default.join(projectDir, '.gitignore'), gitignore);
|
|
195
|
+
console.log(`[QCLI] ✅ Project created successfully!`);
|
|
196
|
+
console.log(`\nNext steps:`);
|
|
197
|
+
console.log(` cd ${projectName}`);
|
|
198
|
+
console.log(` npm install`);
|
|
199
|
+
console.log(` npm run dev`);
|
|
200
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
|
4
|
+
// RPC Client for QHTTPX
|
|
5
|
+
// Allows end-to-end type safety similar to Hono's 'hc' or Elysia's 'eden'
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.hc = hc;
|
|
8
|
+
// Proxy-based client generator
|
|
9
|
+
function hc(baseUrl, options = {}) {
|
|
10
|
+
const handler = {
|
|
11
|
+
get(target, prop, receiver) {
|
|
12
|
+
if (typeof prop !== 'string')
|
|
13
|
+
return Reflect.get(target, prop, receiver);
|
|
14
|
+
// If it's a method call like .$get()
|
|
15
|
+
if (prop.startsWith('$')) {
|
|
16
|
+
const method = prop.slice(1).toUpperCase();
|
|
17
|
+
return async (args = {}) => {
|
|
18
|
+
let url = baseUrl;
|
|
19
|
+
const path = target.__path || '';
|
|
20
|
+
// Replace params in path :id
|
|
21
|
+
let finalPath = path;
|
|
22
|
+
if (args.param) {
|
|
23
|
+
Object.entries(args.param).forEach(([key, value]) => {
|
|
24
|
+
finalPath = finalPath.replace(`:${key}`, String(value));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
url += finalPath;
|
|
28
|
+
// Append query params
|
|
29
|
+
if (args.query) {
|
|
30
|
+
const searchParams = new URLSearchParams();
|
|
31
|
+
Object.entries(args.query).forEach(([key, value]) => {
|
|
32
|
+
searchParams.append(key, String(value));
|
|
33
|
+
});
|
|
34
|
+
url += `?${searchParams.toString()}`;
|
|
35
|
+
}
|
|
36
|
+
const fetchOptions = {
|
|
37
|
+
...options,
|
|
38
|
+
method,
|
|
39
|
+
headers: {
|
|
40
|
+
...options.headers,
|
|
41
|
+
...(args.header || {}),
|
|
42
|
+
...(args.json ? { 'Content-Type': 'application/json' } : {}),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
if (args.json) {
|
|
46
|
+
fetchOptions.body = JSON.stringify(args.json);
|
|
47
|
+
}
|
|
48
|
+
else if (args.form) {
|
|
49
|
+
// Handle FormData
|
|
50
|
+
fetchOptions.body = args.form;
|
|
51
|
+
}
|
|
52
|
+
const res = await fetch(url, fetchOptions);
|
|
53
|
+
// Return a wrapper that allows typed access
|
|
54
|
+
return {
|
|
55
|
+
ok: res.ok,
|
|
56
|
+
status: res.status,
|
|
57
|
+
statusText: res.statusText,
|
|
58
|
+
headers: res.headers,
|
|
59
|
+
json: () => res.json(),
|
|
60
|
+
text: () => res.text(),
|
|
61
|
+
blob: () => res.blob(),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// It's a path segment
|
|
66
|
+
const newPath = (target.__path || '') + '/' + prop;
|
|
67
|
+
const proxy = new Proxy({ __path: newPath }, handler);
|
|
68
|
+
return proxy;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
return new Proxy({ __path: '' }, handler);
|
|
72
|
+
}
|