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,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const index_1 = require("../src/index");
|
|
5
|
+
(0, vitest_1.describe)('Task Metrics', () => {
|
|
6
|
+
let app;
|
|
7
|
+
(0, vitest_1.afterEach)(async () => {
|
|
8
|
+
if (app) {
|
|
9
|
+
await app.close();
|
|
10
|
+
app = undefined;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
(0, vitest_1.it)('exposes task metrics in /__qhttpx/metrics', async () => {
|
|
14
|
+
app = new index_1.QHTTPX();
|
|
15
|
+
// Register a simple task
|
|
16
|
+
app.task('simple-task', async () => {
|
|
17
|
+
// do nothing
|
|
18
|
+
});
|
|
19
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
20
|
+
// Enqueue the task a few times
|
|
21
|
+
await app.enqueue('simple-task', {});
|
|
22
|
+
await app.enqueue('simple-task', {});
|
|
23
|
+
// Allow some time for async tasks to complete
|
|
24
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
25
|
+
const response = await fetch(`http://127.0.0.1:${port}/__qhttpx/metrics`);
|
|
26
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
27
|
+
const json = (await response.json());
|
|
28
|
+
(0, vitest_1.expect)(json.tasks).toBeDefined();
|
|
29
|
+
(0, vitest_1.expect)(json.tasks.registeredTasks).toBe(1);
|
|
30
|
+
(0, vitest_1.expect)(json.tasks.totalEnqueued).toBe(2);
|
|
31
|
+
(0, vitest_1.expect)(json.tasks.totalCompleted).toBe(2);
|
|
32
|
+
(0, vitest_1.expect)(json.tasks.totalFailed).toBe(0);
|
|
33
|
+
(0, vitest_1.expect)(json.tasks.totalOverloaded).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
(0, vitest_1.it)('counts failed tasks', async () => {
|
|
36
|
+
app = new index_1.QHTTPX();
|
|
37
|
+
app.task('failing-task', async () => {
|
|
38
|
+
throw new Error('Task failed');
|
|
39
|
+
}, { maxRetries: 1 }); // 1 retry
|
|
40
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
41
|
+
try {
|
|
42
|
+
await app.enqueue('failing-task', {});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
}
|
|
46
|
+
// Allow time for retry and failure
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
48
|
+
const response = await fetch(`http://127.0.0.1:${port}/__qhttpx/metrics`);
|
|
49
|
+
const json = (await response.json());
|
|
50
|
+
(0, vitest_1.expect)(json.tasks.totalFailed).toBe(1);
|
|
51
|
+
(0, vitest_1.expect)(json.tasks.totalRetried).toBe(1); // 1 retry attempt
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const index_1 = require("../src/index");
|
|
5
|
+
(0, vitest_1.describe)('Task engine', () => {
|
|
6
|
+
(0, vitest_1.it)('registers and executes a task', async () => {
|
|
7
|
+
const app = new index_1.QHTTPX();
|
|
8
|
+
const seen = [];
|
|
9
|
+
app.task('echo', async (payload) => {
|
|
10
|
+
seen.push(payload);
|
|
11
|
+
});
|
|
12
|
+
await app.enqueue('echo', { hello: 'world' });
|
|
13
|
+
(0, vitest_1.expect)(seen).toHaveLength(1);
|
|
14
|
+
(0, vitest_1.expect)(seen[0]).toEqual({ hello: 'world' });
|
|
15
|
+
});
|
|
16
|
+
(0, vitest_1.it)('retries failing tasks according to maxRetries', async () => {
|
|
17
|
+
const app = new index_1.QHTTPX();
|
|
18
|
+
const spy = vitest_1.vi.fn();
|
|
19
|
+
let attempts = 0;
|
|
20
|
+
app.task('sometimes-fails', () => {
|
|
21
|
+
spy();
|
|
22
|
+
attempts += 1;
|
|
23
|
+
if (attempts < 3) {
|
|
24
|
+
throw new Error('fail');
|
|
25
|
+
}
|
|
26
|
+
}, { maxRetries: 5 });
|
|
27
|
+
await app.enqueue('sometimes-fails', null);
|
|
28
|
+
(0, vitest_1.expect)(spy).toHaveBeenCalledTimes(3);
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.it)('propagates errors after exceeding retries', async () => {
|
|
31
|
+
const app = new index_1.QHTTPX();
|
|
32
|
+
app.task('always-fails', () => {
|
|
33
|
+
throw new Error('boom');
|
|
34
|
+
}, { maxRetries: 1 });
|
|
35
|
+
await (0, vitest_1.expect)(app.enqueue('always-fails', null)).rejects.toThrow('boom');
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)('integrates with HTTP route that enqueues a job', async () => {
|
|
38
|
+
const app = new index_1.QHTTPX();
|
|
39
|
+
const seen = [];
|
|
40
|
+
app.task('job', async (payload) => {
|
|
41
|
+
seen.push(payload);
|
|
42
|
+
});
|
|
43
|
+
app.post('/enqueue', async (ctx) => {
|
|
44
|
+
const body = ctx.body;
|
|
45
|
+
await app.enqueue('job', body.value);
|
|
46
|
+
ctx.json({ ok: true });
|
|
47
|
+
});
|
|
48
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
49
|
+
const response = await fetch(`http://127.0.0.1:${port}/enqueue`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'content-type': 'application/json',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({ value: 'hello' }),
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
57
|
+
const json = (await response.json());
|
|
58
|
+
(0, vitest_1.expect)(json.ok).toBe(true);
|
|
59
|
+
(0, vitest_1.expect)(seen).toEqual(['hello']);
|
|
60
|
+
await app.close();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const src_1 = require("../src");
|
|
5
|
+
(0, vitest_1.describe)('Testing utilities', () => {
|
|
6
|
+
(0, vitest_1.it)('creates a test client and handles requests', async () => {
|
|
7
|
+
const app = new src_1.QHTTPX();
|
|
8
|
+
app.get('/hello', (ctx) => {
|
|
9
|
+
ctx.json({ message: 'world' });
|
|
10
|
+
});
|
|
11
|
+
app.post('/echo', (ctx) => {
|
|
12
|
+
ctx.json(ctx.body);
|
|
13
|
+
});
|
|
14
|
+
const client = (0, src_1.createTestClient)(app);
|
|
15
|
+
try {
|
|
16
|
+
const res1 = await client.get('/hello');
|
|
17
|
+
(0, vitest_1.expect)(res1.status).toBe(200);
|
|
18
|
+
(0, vitest_1.expect)(await res1.json()).toEqual({ message: 'world' });
|
|
19
|
+
const res2 = await client.post('/echo', { foo: 'bar' });
|
|
20
|
+
(0, vitest_1.expect)(res2.status).toBe(200);
|
|
21
|
+
(0, vitest_1.expect)(await res2.json()).toEqual({ foo: 'bar' });
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await client.stop();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
(0, vitest_1.it)('handles custom headers and raw body', async () => {
|
|
28
|
+
const app = new src_1.QHTTPX();
|
|
29
|
+
app.post('/raw', (ctx) => {
|
|
30
|
+
ctx.send(ctx.req.headers['x-custom']);
|
|
31
|
+
});
|
|
32
|
+
const client = (0, src_1.createTestClient)(app);
|
|
33
|
+
try {
|
|
34
|
+
const res = await client.post('/raw', 'raw-body', {
|
|
35
|
+
headers: {
|
|
36
|
+
'x-custom': 'custom-value',
|
|
37
|
+
'content-type': 'text/plain',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
41
|
+
(0, vitest_1.expect)(await res.text()).toBe('custom-value');
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await client.stop();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const server_1 = require("../src/core/server");
|
|
5
|
+
(0, vitest_1.describe)('Validation System', () => {
|
|
6
|
+
let app;
|
|
7
|
+
(0, vitest_1.afterEach)(async () => {
|
|
8
|
+
if (app)
|
|
9
|
+
await app.close();
|
|
10
|
+
});
|
|
11
|
+
(0, vitest_1.it)('should validate request body and return 400 on failure', async () => {
|
|
12
|
+
app = new server_1.QHTTPX();
|
|
13
|
+
const schema = {
|
|
14
|
+
body: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
name: { type: 'string', min: 3 },
|
|
18
|
+
age: { type: 'number', min: 18 }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
app.post('/user', {
|
|
23
|
+
schema,
|
|
24
|
+
handler: (ctx) => {
|
|
25
|
+
ctx.json({ status: 'ok', body: ctx.body });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const { port } = await app.listen(0);
|
|
29
|
+
// Invalid request (age too low)
|
|
30
|
+
const res1 = await fetch(`http://localhost:${port}/user`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ name: 'Alice', age: 10 })
|
|
34
|
+
});
|
|
35
|
+
(0, vitest_1.expect)(res1.status).toBe(400);
|
|
36
|
+
const data1 = await res1.json();
|
|
37
|
+
(0, vitest_1.expect)(data1.error).toBe('Validation Error');
|
|
38
|
+
(0, vitest_1.expect)(data1.details).toContain('age');
|
|
39
|
+
// Valid request
|
|
40
|
+
const res2 = await fetch(`http://localhost:${port}/user`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ name: 'Alice', age: 20 })
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.expect)(res2.status).toBe(200);
|
|
46
|
+
const data2 = await res2.json();
|
|
47
|
+
(0, vitest_1.expect)(data2.body).toEqual({ name: 'Alice', age: 20 });
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.it)('should validate and coerce query parameters', async () => {
|
|
50
|
+
app = new server_1.QHTTPX();
|
|
51
|
+
const schema = {
|
|
52
|
+
query: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
page: { type: 'number', min: 1 },
|
|
56
|
+
sort: { type: 'string', enum: ['asc', 'desc'] }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
app.get('/items', {
|
|
61
|
+
schema,
|
|
62
|
+
handler: (ctx) => {
|
|
63
|
+
// ctx.query should have numbers now
|
|
64
|
+
ctx.json({
|
|
65
|
+
page: ctx.query.page,
|
|
66
|
+
pageType: typeof ctx.query.page,
|
|
67
|
+
sort: ctx.query.sort
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const { port } = await app.listen(0);
|
|
72
|
+
// Valid query
|
|
73
|
+
const res1 = await fetch(`http://localhost:${port}/items?page=5&sort=desc`);
|
|
74
|
+
(0, vitest_1.expect)(res1.status).toBe(200);
|
|
75
|
+
const data1 = await res1.json();
|
|
76
|
+
(0, vitest_1.expect)(data1.page).toBe(5);
|
|
77
|
+
(0, vitest_1.expect)(data1.pageType).toBe('number'); // Coercion worked
|
|
78
|
+
(0, vitest_1.expect)(data1.sort).toBe('desc');
|
|
79
|
+
// Invalid query
|
|
80
|
+
const res2 = await fetch(`http://localhost:${port}/items?page=0&sort=foo`);
|
|
81
|
+
(0, vitest_1.expect)(res2.status).toBe(400);
|
|
82
|
+
});
|
|
83
|
+
(0, vitest_1.it)('should support legacy response schema (no validation)', async () => {
|
|
84
|
+
app = new server_1.QHTTPX();
|
|
85
|
+
// Legacy schema: just a JSON schema at root
|
|
86
|
+
const schema = {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
hello: { type: 'string' }
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
app.get('/legacy', {
|
|
93
|
+
schema,
|
|
94
|
+
handler: (ctx) => {
|
|
95
|
+
// If validation ran, it might fail because ctx.body is undefined or not matching
|
|
96
|
+
// But here we check if it IGNORES validation and uses it for serialization
|
|
97
|
+
ctx.json({ hello: 'world', extra: 'hidden' });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
const { port } = await app.listen(0);
|
|
101
|
+
const res = await fetch(`http://localhost:${port}/legacy`);
|
|
102
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
103
|
+
const data = await res.json();
|
|
104
|
+
(0, vitest_1.expect)(data.hello).toBe('world');
|
|
105
|
+
(0, vitest_1.expect)(data.extra).toBeUndefined(); // fast-json-stringify filters extra fields
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const vitest_1 = require("vitest");
|
|
40
|
+
const net = __importStar(require("net"));
|
|
41
|
+
const ws_1 = __importDefault(require("ws"));
|
|
42
|
+
const src_1 = require("../src");
|
|
43
|
+
(0, vitest_1.describe)('WebSocket upgrade', () => {
|
|
44
|
+
(0, vitest_1.it)('handles websocket upgrade for registered path', async () => {
|
|
45
|
+
const app = new src_1.QHTTPX();
|
|
46
|
+
let upgraded = false;
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
48
|
+
app.upgrade('/ws', (ws, req) => {
|
|
49
|
+
upgraded = true;
|
|
50
|
+
ws.close();
|
|
51
|
+
});
|
|
52
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
53
|
+
const ws = new ws_1.default(`ws://127.0.0.1:${port}/ws`);
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
ws.on('open', () => {
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
ws.on('error', (err) => {
|
|
59
|
+
reject(err);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.expect)(upgraded).toBe(true);
|
|
63
|
+
ws.close();
|
|
64
|
+
await app.close();
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.it)('destroys socket when no upgrade handler is registered for path', async () => {
|
|
67
|
+
const app = new src_1.QHTTPX();
|
|
68
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
69
|
+
const socket = net.createConnection({ port, host: '127.0.0.1' });
|
|
70
|
+
const done = new Promise((resolve) => {
|
|
71
|
+
let finished = false;
|
|
72
|
+
const finish = () => {
|
|
73
|
+
if (!finished) {
|
|
74
|
+
finished = true;
|
|
75
|
+
resolve();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
socket.on('end', finish);
|
|
79
|
+
socket.on('close', finish);
|
|
80
|
+
socket.on('error', () => {
|
|
81
|
+
finish();
|
|
82
|
+
});
|
|
83
|
+
setTimeout(finish, 500);
|
|
84
|
+
});
|
|
85
|
+
const requestLines = [
|
|
86
|
+
'GET /no-ws HTTP/1.1',
|
|
87
|
+
'Host: 127.0.0.1',
|
|
88
|
+
'Upgrade: websocket',
|
|
89
|
+
'Connection: Upgrade',
|
|
90
|
+
'',
|
|
91
|
+
'',
|
|
92
|
+
];
|
|
93
|
+
socket.write(requestLines.join('\r\n'));
|
|
94
|
+
await done;
|
|
95
|
+
await app.close();
|
|
96
|
+
});
|
|
97
|
+
(0, vitest_1.it)('supports rooms and broadcasting', async () => {
|
|
98
|
+
const app = new src_1.QHTTPX();
|
|
99
|
+
app.upgrade('/chat', (ws, req) => {
|
|
100
|
+
const url = new URL(req.url || '', 'http://localhost');
|
|
101
|
+
const room = url.searchParams.get('room');
|
|
102
|
+
if (room) {
|
|
103
|
+
ws.join(room);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
app.post('/broadcast', (ctx) => {
|
|
107
|
+
const { room, message } = ctx.body;
|
|
108
|
+
app.websocket.to(room).emit(message);
|
|
109
|
+
ctx.json({ success: true });
|
|
110
|
+
});
|
|
111
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
112
|
+
const baseUrl = `ws://127.0.0.1:${port}/chat`;
|
|
113
|
+
// Client A in room1
|
|
114
|
+
const wsA = new ws_1.default(`${baseUrl}?room=room1`);
|
|
115
|
+
const msgsA = [];
|
|
116
|
+
wsA.on('message', (data) => msgsA.push(data.toString()));
|
|
117
|
+
// Client B in room1
|
|
118
|
+
const wsB = new ws_1.default(`${baseUrl}?room=room1`);
|
|
119
|
+
const msgsB = [];
|
|
120
|
+
wsB.on('message', (data) => msgsB.push(data.toString()));
|
|
121
|
+
// Client C in room2
|
|
122
|
+
const wsC = new ws_1.default(`${baseUrl}?room=room2`);
|
|
123
|
+
const msgsC = [];
|
|
124
|
+
wsC.on('message', (data) => msgsC.push(data.toString()));
|
|
125
|
+
await Promise.all([
|
|
126
|
+
new Promise((resolve) => wsA.on('open', () => resolve())),
|
|
127
|
+
new Promise((resolve) => wsB.on('open', () => resolve())),
|
|
128
|
+
new Promise((resolve) => wsC.on('open', () => resolve())),
|
|
129
|
+
]);
|
|
130
|
+
// Broadcast to room1
|
|
131
|
+
// We need to make a POST request.
|
|
132
|
+
// Since we are inside test, we can use fetch or http.request.
|
|
133
|
+
// Or just use app.websocket directly if we can access it?
|
|
134
|
+
// We can access app.websocket directly here since we have app instance.
|
|
135
|
+
app.websocket.to('room1').emit('hello room1');
|
|
136
|
+
// Wait a bit for messages
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
138
|
+
(0, vitest_1.expect)(msgsA).toContain('hello room1');
|
|
139
|
+
(0, vitest_1.expect)(msgsB).toContain('hello room1');
|
|
140
|
+
(0, vitest_1.expect)(msgsC).not.toContain('hello room1');
|
|
141
|
+
wsA.close();
|
|
142
|
+
wsB.close();
|
|
143
|
+
wsC.close();
|
|
144
|
+
await app.close();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_1 = require("vitest/config");
|
|
4
|
+
exports.default = (0, config_1.defineConfig)({
|
|
5
|
+
test: {
|
|
6
|
+
include: ['tests/**/*.test.ts'],
|
|
7
|
+
exclude: ['dist/**', 'node_modules/**'],
|
|
8
|
+
},
|
|
9
|
+
});
|
package/docs/AEGIS.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Aegis Rate Limiter
|
|
2
|
+
|
|
3
|
+
**Aegis** is a high-performance, Token Bucket-based rate limiter built directly into QHTTPX. It is designed to protect your application from DDoS attacks, brute-force attempts, and API abuse while maintaining ultra-low latency.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Token Bucket Algorithm**: Smooths out traffic spikes while enforcing strict limits.
|
|
8
|
+
- **Zero Dependencies**: Uses an efficient in-memory `Map` by default.
|
|
9
|
+
- **Standard Compliance**: Automatically sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `Retry-After` headers.
|
|
10
|
+
- **Flexible Keys**: Limit by IP, API Key, User ID, or any custom logic.
|
|
11
|
+
- **Extensible Storage**: Interface-ready for Redis or other distributed stores.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Import `rateLimit` from the middleware package:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { QHTTPX } from 'qhttpx';
|
|
19
|
+
import { rateLimit } from 'qhttpx/middleware';
|
|
20
|
+
|
|
21
|
+
const app = new QHTTPX();
|
|
22
|
+
|
|
23
|
+
// 1. Global DDoS Protection
|
|
24
|
+
app.use(rateLimit({
|
|
25
|
+
windowMs: 60 * 1000, // 1 minute
|
|
26
|
+
max: 100, // Limit each IP to 100 requests per window
|
|
27
|
+
message: { error: 'Too many requests, slow down!' }
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// 2. API Key Limits (Tiered)
|
|
31
|
+
app.use(rateLimit({
|
|
32
|
+
windowMs: 60 * 60 * 1000, // 1 hour
|
|
33
|
+
max: 5000,
|
|
34
|
+
keyGenerator: (ctx) => ctx.req.headers['x-api-key'] || ctx.req.socket.remoteAddress
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
app.get('/', (ctx) => ctx.json({ message: 'Welcome!' }));
|
|
38
|
+
|
|
39
|
+
app.listen(3000);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
| Option | Type | Default | Description |
|
|
45
|
+
|Params|---|---|---|
|
|
46
|
+
| `windowMs` | `number` | `60000` | Time frame for which requests are checked/remembered. |
|
|
47
|
+
| `max` | `number` | `100` | Max number of connections during `windowMs` before sending a 429 response. |
|
|
48
|
+
| `message` | `string` \| `object` | `"Too many requests..."` | Error message returned to the client. |
|
|
49
|
+
| `statusCode` | `number` | `429` | HTTP status code returned. |
|
|
50
|
+
| `headers` | `boolean` | `true` | Enable/disable `X-RateLimit-*` headers. |
|
|
51
|
+
| `keyGenerator` | `(ctx) => string` | `IP Address` | Function to generate a unique key for the client. |
|
|
52
|
+
| `skip` | `(ctx) => boolean` | `undefined` | Function to bypass the limiter. |
|
|
53
|
+
| `store` | `RateLimitStore` | `MemoryStore` | Custom storage backend (e.g., Redis). |
|
|
54
|
+
|
|
55
|
+
## Custom Storage (Redis Example)
|
|
56
|
+
|
|
57
|
+
You can easily swap the in-memory store for Redis to share limits across a cluster:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { RateLimitStore } from 'qhttpx/middleware';
|
|
61
|
+
|
|
62
|
+
class RedisStore implements RateLimitStore {
|
|
63
|
+
constructor(private redis: RedisClient) {}
|
|
64
|
+
|
|
65
|
+
async increment(key: string, windowMs: number) {
|
|
66
|
+
// Implement Redis INCR + EXPIRE logic
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async decrement(key: string) { ... }
|
|
70
|
+
async reset(key: string) { ... }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
app.use(rateLimit({
|
|
74
|
+
store: new RedisStore(redisClient)
|
|
75
|
+
}));
|
|
76
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# QHTTPX Benchmarks (Phase 8)
|
|
2
|
+
|
|
3
|
+
This project includes a simple benchmark harness using `autocannon` to measure basic JSON response performance.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Install dev dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Run the benchmark
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run bench
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This will:
|
|
20
|
+
|
|
21
|
+
- Build the TypeScript sources
|
|
22
|
+
- Start a temporary QHTTPX server
|
|
23
|
+
- Run an `autocannon` benchmark against `GET /json`
|
|
24
|
+
- Shut down the server when the run completes
|
|
25
|
+
|
|
26
|
+
## Scenario
|
|
27
|
+
|
|
28
|
+
The current scenario measures:
|
|
29
|
+
|
|
30
|
+
- Simple JSON endpoint: `GET /json`
|
|
31
|
+
- Connections: 100
|
|
32
|
+
- Pipelining: 10
|
|
33
|
+
- Duration: 10 seconds
|
|
34
|
+
|
|
35
|
+
You can adjust these parameters in `src/benchmarks/simple-json.ts`.
|
|
36
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# QHTTPX Capabilities & Limitations
|
|
2
|
+
|
|
3
|
+
This document outlines the operational boundaries of the QHTTPX framework, detailing what it handles natively and what lies outside its scope. It is designed to help architects and developers decide if QHTTPX is the right tool for their specific use case.
|
|
4
|
+
|
|
5
|
+
## ✅ What QHTTPX Handles (Core Capabilities)
|
|
6
|
+
|
|
7
|
+
QHTTPX is designed first and foremost as a **high-concurrency runtime**. Its features focus on performance, stability under load, and operational efficiency.
|
|
8
|
+
|
|
9
|
+
### 1. High-Performance Traffic Management
|
|
10
|
+
* **Request Fusion Engine (Layer 2 Coalescing)**: Automatically collapses simultaneous identical requests (same method, route, query, body) into a single execution. Ideal for high-traffic endpoints with "thundering herd" problems.
|
|
11
|
+
* **Aegis Rate Limiter**: A built-in, token-bucket based rate limiter that protects the application layer. Supports custom key generators (IP, API Key) and standard rate-limit headers.
|
|
12
|
+
* **Backpressure & Overload Protection**: Automatically detects CPU and memory pressure. It can queue requests or reject them gracefully (503 Service Unavailable) to prevent crashes, rather than stalling the event loop.
|
|
13
|
+
|
|
14
|
+
### 2. Scalability & Runtime
|
|
15
|
+
* **Auto-Scaling Workers**: The CLI (`qhttpx start`) supports a cluster mode that automatically spawns workers based on available CPU cores (`workers: 'auto'`).
|
|
16
|
+
* **Hot Reload**: The `qhttpx dev` command provides instant feedback during development using `tsx` and filesystem watching.
|
|
17
|
+
* **Async-First Architecture**: The scheduler is designed to prioritize non-blocking tasks, ensuring that background jobs and HTTP requests share resources efficiently without starving each other.
|
|
18
|
+
|
|
19
|
+
### 3. Modular Architecture
|
|
20
|
+
* **The Extension Protocol (Plugins)**: A robust plugin system allowing encapsulated modules. Supports `app.register()` with URL prefixes (scoped routing), making it easy to build modular monoliths or microservices.
|
|
21
|
+
* **Radix-Tree Routing**: Ultra-fast, O(1) static route resolution. Zero regex overhead for standard routes.
|
|
22
|
+
|
|
23
|
+
### 4. Database Optimization
|
|
24
|
+
* **Query Fusion**: When using supported adapters (like SQLite), QHTTPX can automatically batch multiple individual `SELECT` queries into a single `IN (...)` query, reducing database round-trips significantly.
|
|
25
|
+
* **Dynamic Adapters**: Supports a driver-agnostic adapter pattern. Drivers (pg, better-sqlite3, mongodb) are optional peer dependencies, keeping the core bundle small.
|
|
26
|
+
|
|
27
|
+
### 5. Developer Experience & Tooling
|
|
28
|
+
* **RPC Client (`hc`)**: A Hono-like RPC client that provides end-to-end type safety for frontend applications consuming QHTTPX APIs.
|
|
29
|
+
* **Structured Logging**: Built-in integration with `pino` for JSON-structured logs, essential for modern observability stacks.
|
|
30
|
+
* **CLI Scaffolding**: `qhttpx new` command to quickly bootstrap best-practice TypeScript projects.
|
|
31
|
+
|
|
32
|
+
### 7. Extended Web Capabilities
|
|
33
|
+
* **Multipart Support**: Native support for `multipart/form-data` (file uploads) using Busboy integration. Files are automatically parsed and available in `ctx.files`.
|
|
34
|
+
* **View Engine**: Support for template engines via `ViewEngine` interface and `ctx.render()`.
|
|
35
|
+
* **Validation**: Extensible validation system with `ctx.validate()`. Includes a built-in `SimpleValidator` and supports **Zod** integration.
|
|
36
|
+
* **WebSocket Rooms**: Enhanced WebSocket support with Rooms/Channels and broadcasting capabilities.
|
|
37
|
+
* **Compression**: Smart compression middleware (Gzip/Brotli/Deflate) with thresholding and stream piping.
|
|
38
|
+
* **Server-Sent Events (SSE)**: Native support for real-time one-way event streaming via `createSSE()`.
|
|
39
|
+
* **Advanced Static Files**: Streaming support, `Range` requests (partial content/video), and HTTP Caching (`ETag`, `Last-Modified`) for high-performance asset serving.
|
|
40
|
+
|
|
41
|
+
### 8. Ecosystem Compatibility (SaaS Ready)
|
|
42
|
+
QHTTPX is an **Open Runtime**. It does not lock you into specific vendors.
|
|
43
|
+
* **Bring Your Own Stack**: Fully compatible with `jose` (JWT), `ioredis` (Caching), `Prisma`/`Drizzle` (ORM), and `@aws-sdk`.
|
|
44
|
+
* **Middleware Freedom**: The `(ctx, next)` signature allows easy integration of any logic.
|
|
45
|
+
* **[Read the Ecosystem Guide](./ECOSYSTEM.md)** for patterns on integrating Auth, Redis, and S3.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## ❌ What QHTTPX Does NOT Handle (Limitations)
|
|
50
|
+
|
|
51
|
+
QHTTPX is a **runtime/micro-framework**, not a "batteries-included" monolith like NestJS, AdonisJS, or Laravel.
|
|
52
|
+
|
|
53
|
+
### 1. No Built-in ORM
|
|
54
|
+
* **Limitation**: QHTTPX provides *adapters* to connect to databases and run raw queries (or fused queries), but it does **not** provide an Object-Relational Mapper (ORM).
|
|
55
|
+
* **Workaround**: You must use external ORMs like Prisma, Drizzle, or TypeORM if you need entity management, migrations, or complex relationship handling.
|
|
56
|
+
|
|
57
|
+
### 2. No Built-in Authentication Strategies
|
|
58
|
+
* **Limitation**: There is no "Passport.js" equivalent built-in. No native OAuth, JWT signing, or Session management logic beyond setting cookies.
|
|
59
|
+
* **Workaround**: You are responsible for implementing auth logic (e.g., using `jose` for JWTs, or integrating with Auth0/Clerk). Middleware guards can be easily written, but the logic is yours to own.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Summary: When to use QHTTPX?
|
|
64
|
+
|
|
65
|
+
| Use QHTTPX If... | Do NOT Use QHTTPX If... |
|
|
66
|
+
| :--- | :--- |
|
|
67
|
+
| You need **maximum requests/second** on limited hardware. | You need a full-stack MVC with ORM included. |
|
|
68
|
+
| You are building **high-traffic microservices**. | You want "Auth & User Management" pre-built. |
|
|
69
|
+
| You face "thundering herd" issues (Request Fusion solves this). | |
|
|
70
|
+
| You prefer **control** over magic (manual DB queries, custom auth). | |
|
package/docs/CLI.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# QHTTPX CLI
|
|
2
|
+
|
|
3
|
+
The QHTTPX Command Line Interface (QCLI) is your companion for developing and deploying QHTTPX applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
The CLI is included with `qhttpx`. You can run it via `npx` or by adding a script to your `package.json`.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Run directly
|
|
11
|
+
npx qhttpx <command>
|
|
12
|
+
|
|
13
|
+
# Or add to package.json
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "qhttpx dev src/index.ts",
|
|
16
|
+
"start": "qhttpx start dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
### `dev <entry>`
|
|
23
|
+
Starts the application in **Development Mode**.
|
|
24
|
+
- **Hot Reload**: Automatically restarts the server when files change.
|
|
25
|
+
- **TypeScript Support**: Uses `tsx` to run `.ts` files directly.
|
|
26
|
+
- **Watcher**: Recursively watches your source directory.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx qhttpx dev src/server.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `start <entry>`
|
|
33
|
+
Starts the application in **Production Cluster Mode**.
|
|
34
|
+
- **Cluster**: Automatically forks workers equal to the number of CPU cores.
|
|
35
|
+
- **Resilience**: Automatically restarts workers if they crash.
|
|
36
|
+
- **Zero-Downtime**: (Coming soon) Rolling updates.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx qhttpx start dist/server.js
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `new <name>`
|
|
43
|
+
(Coming Soon) Scaffolds a new QHTTPX project with best practices.
|