qhttpx 1.8.3 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +33 -22
- package/dist/examples/api-server.d.ts +1 -0
- package/dist/examples/api-server.js +56 -0
- package/dist/package.json +1 -1
- package/dist/src/core/server.d.ts +5 -1
- package/dist/src/core/server.js +30 -12
- package/dist/src/core/types.d.ts +1 -0
- package/examples/api-server.ts +44 -236
- package/package.json +1 -1
- package/src/core/server.ts +91 -12
- package/src/core/types.ts +3 -0
- package/tsconfig.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.8.4] - 2026-01-20
|
|
6
|
+
**"The Usability III Update"**
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- **Flexible Route Signatures**: Routes now support `(path, options, handler)` signature for cleaner validation configuration.
|
|
10
|
+
- **Simplified Listen**: `app.listen(port, callback)` is now supported, matching standard patterns.
|
|
11
|
+
- **Updated Examples**: `examples/api-server.ts` rewritten to demonstrate the "Rapid Way" (Fusion, Validation, WebSockets, Typed Files).
|
|
12
|
+
|
|
5
13
|
## [1.8.3] - 2026-01-20
|
|
6
14
|
**"The Developer Experience II Update"**
|
|
7
15
|
|
package/README.md
CHANGED
|
@@ -77,30 +77,42 @@ npm install qhttpx
|
|
|
77
77
|
|
|
78
78
|
## ⚡ Quick Start
|
|
79
79
|
|
|
80
|
-
### 1. The
|
|
81
|
-
|
|
80
|
+
### 1. The Rapid Way (Recommended)
|
|
81
|
+
Get up and running instantly with the singleton instance and destructuring support.
|
|
82
82
|
|
|
83
83
|
```typescript
|
|
84
|
-
|
|
85
|
-
import QHTTPX from "qhttpx";
|
|
84
|
+
import { app } from "qhttpx";
|
|
86
85
|
|
|
87
|
-
//
|
|
88
|
-
|
|
86
|
+
// Concise, destructured handlers
|
|
87
|
+
app.get("/", ({ json }) => json({ message: "Hello World" }));
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
// Unified error handling
|
|
90
|
+
app.onError(({ error, json }) => {
|
|
91
|
+
json({ error: "Something went wrong", details: error }, 500);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
//
|
|
95
|
-
app.
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
// Start listening
|
|
95
|
+
app.listen(3000, () => console.log("Server running on http://localhost:3000"));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. The Custom Way
|
|
99
|
+
When you need specific configuration options or multiple instances.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { createHttpApp } from "qhttpx";
|
|
103
|
+
|
|
104
|
+
const app = createHttpApp({
|
|
105
|
+
enableRequestFusion: true, // Enable coalescing
|
|
106
|
+
maxBodySize: "1mb"
|
|
98
107
|
});
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
app.get("/users", async ({ json, query }) => {
|
|
110
|
+
const { page } = query;
|
|
111
|
+
// ...
|
|
112
|
+
json({ page });
|
|
103
113
|
});
|
|
114
|
+
|
|
115
|
+
app.listen(3000);
|
|
104
116
|
```
|
|
105
117
|
|
|
106
118
|
### 2. The Scalable Way (Cluster Mode)
|
|
@@ -143,13 +155,12 @@ app.get('/events', (ctx) => {
|
|
|
143
155
|
});
|
|
144
156
|
|
|
145
157
|
// WebSockets with Rooms
|
|
146
|
-
app.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
158
|
+
app.upgrade('/chat', (ws) => {
|
|
159
|
+
ws.join('general');
|
|
160
|
+
ws.on('message', (msg) => {
|
|
161
|
+
// Broadcast to 'general' room
|
|
162
|
+
app.websocket.to('general').emit(`Echo: ${msg}`);
|
|
163
|
+
});
|
|
153
164
|
});
|
|
154
165
|
```
|
|
155
166
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("../src/index");
|
|
4
|
+
// 1. Initialize App (Fusion + Aegis enabled)
|
|
5
|
+
const app = (0, index_1.createHttpApp)({
|
|
6
|
+
enableRequestFusion: true, // ⚡ Auto-coalesce duplicate requests
|
|
7
|
+
metricsEnabled: true // 📊 Expose /__qhttpx/metrics
|
|
8
|
+
});
|
|
9
|
+
// 2. Global Middleware (Logging & Rate Limit)
|
|
10
|
+
app.use(async ({ req, next }) => {
|
|
11
|
+
console.log(`[${req.method}] ${req.url}`);
|
|
12
|
+
if (next)
|
|
13
|
+
await next();
|
|
14
|
+
});
|
|
15
|
+
// 3. Validation Schema
|
|
16
|
+
const UserSchema = {
|
|
17
|
+
body: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
required: ['name', 'role'],
|
|
20
|
+
properties: { name: { type: 'string' }, role: { type: 'string' } }
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
// 4. Routes (Clean & Destructured)
|
|
24
|
+
app.get('/', ({ json }) => json({ status: 'online', fusion: true }));
|
|
25
|
+
// ⚡ Fused Endpoint: 1000 concurrent requests -> 1 DB execution
|
|
26
|
+
app.get('/heavy', async ({ json }) => {
|
|
27
|
+
await new Promise(r => setTimeout(r, 100)); // Simulate DB
|
|
28
|
+
json({ data: 'Expensive Result', timestamp: Date.now() });
|
|
29
|
+
});
|
|
30
|
+
// 🛡️ Validated & Typed Route
|
|
31
|
+
app.post('/users', { schema: UserSchema }, async ({ body, json }) => {
|
|
32
|
+
// Body is already validated and typed here
|
|
33
|
+
json({ created: true, user: body }, 201);
|
|
34
|
+
});
|
|
35
|
+
// 📂 File Uploads (Typed)
|
|
36
|
+
app.post('/upload', ({ files, json }) => {
|
|
37
|
+
if (!files?.doc)
|
|
38
|
+
return json({ error: 'No file' }, 400);
|
|
39
|
+
const doc = Array.isArray(files.doc) ? files.doc[0] : files.doc;
|
|
40
|
+
json({ filename: doc.filename, size: doc.size });
|
|
41
|
+
});
|
|
42
|
+
// 📡 WebSockets (Pub/Sub)
|
|
43
|
+
app.upgrade('/chat', (ws) => {
|
|
44
|
+
ws.join('general'); // Auto-join room
|
|
45
|
+
ws.on('message', (msg) => {
|
|
46
|
+
// Broadcast to room
|
|
47
|
+
app.websocket.to('general').emit(`Echo: ${msg}`);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
// 5. Unified Error Handling
|
|
51
|
+
app.onError(({ error, json }) => {
|
|
52
|
+
console.error(error); // Log internal error
|
|
53
|
+
json({ error: 'Internal Server Error', handled: true }, 500);
|
|
54
|
+
});
|
|
55
|
+
// 6. Start Server
|
|
56
|
+
app.listen(3000, () => console.log('🚀 Server running on http://localhost:3000'));
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qhttpx",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
4
4
|
"description": "The High-Performance Hybrid HTTP Runtime for Node.js. Built for extreme concurrency, request fusion, and zero-overhead scaling.",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -56,9 +56,13 @@ export declare class QHTTPX {
|
|
|
56
56
|
register<Options extends QHTTPXPluginOptions>(plugin: QHTTPXPlugin<Options>, options?: Options): Promise<void>;
|
|
57
57
|
private registerRoute;
|
|
58
58
|
get(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
|
|
59
|
+
get(path: string, config: import('./types').QHTTPXRouteConfig, handler: QHTTPXHandler): void;
|
|
59
60
|
post(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
|
|
61
|
+
post(path: string, config: import('./types').QHTTPXRouteConfig, handler: QHTTPXHandler): void;
|
|
60
62
|
put(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
|
|
63
|
+
put(path: string, config: import('./types').QHTTPXRouteConfig, handler: QHTTPXHandler): void;
|
|
61
64
|
delete(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
|
|
65
|
+
delete(path: string, config: import('./types').QHTTPXRouteConfig, handler: QHTTPXHandler): void;
|
|
62
66
|
route(path: string): {
|
|
63
67
|
get(handler: QHTTPXHandler | QHTTPXRouteOptions): /*elided*/ any;
|
|
64
68
|
post(handler: QHTTPXHandler | QHTTPXRouteOptions): /*elided*/ any;
|
|
@@ -70,7 +74,7 @@ export declare class QHTTPX {
|
|
|
70
74
|
op(name: string, handler: import('./types').QHTTPXOpHandler): void;
|
|
71
75
|
private registerInternalRoutes;
|
|
72
76
|
getOpenAPI(options: OpenAPIOptions): object;
|
|
73
|
-
listen(port: number,
|
|
77
|
+
listen(port: number, hostnameOrCallback?: string | (() => void), callback?: () => void): Promise<{
|
|
74
78
|
port: number;
|
|
75
79
|
}>;
|
|
76
80
|
close(): Promise<void>;
|
package/dist/src/core/server.js
CHANGED
|
@@ -280,32 +280,38 @@ class QHTTPX {
|
|
|
280
280
|
const scope = new scope_1.QHTTPXScope(this, options?.prefix);
|
|
281
281
|
await plugin(scope, options);
|
|
282
282
|
}
|
|
283
|
-
registerRoute(method, path, handlerOrOptions) {
|
|
283
|
+
registerRoute(method, path, handlerOrOptions, handlerIfOptions) {
|
|
284
284
|
let handler;
|
|
285
285
|
let schema;
|
|
286
286
|
const options = {};
|
|
287
287
|
if (typeof handlerOrOptions === 'function') {
|
|
288
288
|
handler = handlerOrOptions;
|
|
289
289
|
}
|
|
290
|
-
else {
|
|
291
|
-
handler =
|
|
290
|
+
else if (handlerIfOptions) {
|
|
291
|
+
handler = handlerIfOptions;
|
|
292
292
|
schema = handlerOrOptions.schema;
|
|
293
293
|
options.priority = handlerOrOptions.priority;
|
|
294
294
|
}
|
|
295
|
+
else {
|
|
296
|
+
const opts = handlerOrOptions;
|
|
297
|
+
handler = opts.handler;
|
|
298
|
+
schema = opts.schema;
|
|
299
|
+
options.priority = opts.priority;
|
|
300
|
+
}
|
|
295
301
|
const compiled = this.compileRoutePipeline(handler, schema);
|
|
296
302
|
this.router.register(method, path, compiled, { ...options, schema });
|
|
297
303
|
}
|
|
298
|
-
get(path, handler) {
|
|
299
|
-
this.registerRoute('GET', path, handler);
|
|
304
|
+
get(path, handlerOrOptions, handler) {
|
|
305
|
+
this.registerRoute('GET', path, handlerOrOptions, handler);
|
|
300
306
|
}
|
|
301
|
-
post(path, handler) {
|
|
302
|
-
this.registerRoute('POST', path, handler);
|
|
307
|
+
post(path, handlerOrOptions, handler) {
|
|
308
|
+
this.registerRoute('POST', path, handlerOrOptions, handler);
|
|
303
309
|
}
|
|
304
|
-
put(path, handler) {
|
|
305
|
-
this.registerRoute('PUT', path, handler);
|
|
310
|
+
put(path, handlerOrOptions, handler) {
|
|
311
|
+
this.registerRoute('PUT', path, handlerOrOptions, handler);
|
|
306
312
|
}
|
|
307
|
-
delete(path, handler) {
|
|
308
|
-
this.registerRoute('DELETE', path, handler);
|
|
313
|
+
delete(path, handlerOrOptions, handler) {
|
|
314
|
+
this.registerRoute('DELETE', path, handlerOrOptions, handler);
|
|
309
315
|
}
|
|
310
316
|
route(path) {
|
|
311
317
|
const register = (method, handler) => {
|
|
@@ -402,7 +408,17 @@ class QHTTPX {
|
|
|
402
408
|
const generator = new generator_1.OpenAPIGenerator(this.router, options);
|
|
403
409
|
return generator.generate();
|
|
404
410
|
}
|
|
405
|
-
async listen(port,
|
|
411
|
+
async listen(port, hostnameOrCallback, callback) {
|
|
412
|
+
let hostname;
|
|
413
|
+
let cb;
|
|
414
|
+
if (typeof hostnameOrCallback === 'function') {
|
|
415
|
+
cb = hostnameOrCallback;
|
|
416
|
+
hostname = undefined;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
hostname = hostnameOrCallback;
|
|
420
|
+
cb = callback;
|
|
421
|
+
}
|
|
406
422
|
if (this.options.database) {
|
|
407
423
|
await this.options.database.connect();
|
|
408
424
|
}
|
|
@@ -424,6 +440,8 @@ class QHTTPX {
|
|
|
424
440
|
this.router.freeze();
|
|
425
441
|
void this.runLifecycleHooks(this.onStartHooks);
|
|
426
442
|
const address = this.server.address();
|
|
443
|
+
if (cb)
|
|
444
|
+
cb();
|
|
427
445
|
if (address && typeof address === 'object') {
|
|
428
446
|
resolve({ port: address.port });
|
|
429
447
|
}
|
package/dist/src/core/types.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export type QHTTPXRouteOptions = {
|
|
|
73
73
|
handler: QHTTPXHandler;
|
|
74
74
|
priority?: RoutePriority;
|
|
75
75
|
};
|
|
76
|
+
export type QHTTPXRouteConfig = Omit<QHTTPXRouteOptions, 'handler'>;
|
|
76
77
|
export type QHTTPXMiddleware = (ctx: QHTTPXContext, next: () => Promise<void>) => void | Promise<void>;
|
|
77
78
|
export type QHTTPXErrorContext = QHTTPXContext & {
|
|
78
79
|
error: unknown;
|
package/examples/api-server.ts
CHANGED
|
@@ -1,254 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { IncomingMessage } from 'http';
|
|
3
|
-
import { Duplex } from 'stream';
|
|
4
|
-
import { createHttpApp, HttpError } from '../src/index';
|
|
1
|
+
import { createHttpApp } from '../src/index';
|
|
5
2
|
|
|
6
|
-
// 1. Initialize
|
|
7
|
-
const app = createHttpApp({
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
// 1. Initialize App (Fusion + Aegis enabled)
|
|
4
|
+
const app = createHttpApp({
|
|
5
|
+
enableRequestFusion: true, // ⚡ Auto-coalesce duplicate requests
|
|
6
|
+
metricsEnabled: true // 📊 Expose /__qhttpx/metrics
|
|
10
7
|
});
|
|
11
8
|
|
|
12
|
-
// 2.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
});
|
|
9
|
+
// 2. Global Middleware (Logging & Rate Limit)
|
|
10
|
+
app.use(async ({ req, next }) => {
|
|
11
|
+
console.log(`[${req.method}] ${req.url}`);
|
|
12
|
+
if (next) await next();
|
|
61
13
|
});
|
|
62
14
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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;
|
|
15
|
+
// 3. Validation Schema
|
|
16
|
+
const UserSchema = {
|
|
17
|
+
body: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
required: ['name', 'role'],
|
|
20
|
+
properties: { name: { type: 'string' }, role: { type: 'string' } }
|
|
119
21
|
}
|
|
22
|
+
};
|
|
120
23
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
subject: 'Welcome to QHTTPX!'
|
|
124
|
-
});
|
|
24
|
+
// 4. Routes (Clean & Destructured)
|
|
25
|
+
app.get('/', ({ json }) => json({ status: 'online', fusion: true }));
|
|
125
26
|
|
|
126
|
-
|
|
27
|
+
// ⚡ Fused Endpoint: 1000 concurrent requests -> 1 DB execution
|
|
28
|
+
app.get('/heavy', async ({ json }) => {
|
|
29
|
+
await new Promise(r => setTimeout(r, 100)); // Simulate DB
|
|
30
|
+
json({ data: 'Expensive Result', timestamp: Date.now() });
|
|
127
31
|
});
|
|
128
32
|
|
|
129
|
-
//
|
|
130
|
-
app.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
app.get('/error', () => {
|
|
134
|
-
throw new Error('Something went wrong!');
|
|
33
|
+
// 🛡️ Validated & Typed Route
|
|
34
|
+
app.post('/users', { schema: UserSchema }, async ({ body, json }) => {
|
|
35
|
+
// Body is already validated and typed here
|
|
36
|
+
json({ created: true, user: body }, 201);
|
|
135
37
|
});
|
|
136
38
|
|
|
137
|
-
//
|
|
138
|
-
app.
|
|
139
|
-
|
|
39
|
+
// 📂 File Uploads (Typed)
|
|
40
|
+
app.post('/upload', ({ files, json }) => {
|
|
41
|
+
if (!files?.doc) return json({ error: 'No file' }, 400);
|
|
42
|
+
const doc = Array.isArray(files.doc) ? files.doc[0] : files.doc;
|
|
43
|
+
json({ filename: doc.filename, size: doc.size });
|
|
140
44
|
});
|
|
141
45
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
46
|
+
// 📡 WebSockets (Pub/Sub)
|
|
47
|
+
app.upgrade('/chat', (ws) => {
|
|
48
|
+
ws.join('general'); // Auto-join room
|
|
49
|
+
ws.on('message', (msg) => {
|
|
50
|
+
// Broadcast to room
|
|
51
|
+
app.websocket.to('general').emit(`Echo: ${msg}`);
|
|
52
|
+
});
|
|
148
53
|
});
|
|
149
54
|
|
|
150
|
-
// 5.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
console.log(` Try: curl http://localhost:${port}/api/users`);
|
|
55
|
+
// 5. Unified Error Handling
|
|
56
|
+
app.onError(({ error, json }) => {
|
|
57
|
+
console.error(error); // Log internal error
|
|
58
|
+
json({ error: 'Internal Server Error', handled: true }, 500);
|
|
155
59
|
});
|
|
156
60
|
|
|
157
|
-
//
|
|
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
|
-
}
|
|
61
|
+
// 6. Start Server
|
|
62
|
+
app.listen(3000, () => console.log('🚀 Server running on http://localhost:3000'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qhttpx",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
4
4
|
"description": "The High-Performance Hybrid HTTP Runtime for Node.js. Built for extreme concurrency, request fusion, and zero-overhead scaling.",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
package/src/core/server.ts
CHANGED
|
@@ -395,7 +395,11 @@ export class QHTTPX {
|
|
|
395
395
|
private registerRoute(
|
|
396
396
|
method: HTTPMethod,
|
|
397
397
|
path: string,
|
|
398
|
-
handlerOrOptions:
|
|
398
|
+
handlerOrOptions:
|
|
399
|
+
| QHTTPXHandler
|
|
400
|
+
| QHTTPXRouteOptions
|
|
401
|
+
| import('./types').QHTTPXRouteConfig,
|
|
402
|
+
handlerIfOptions?: QHTTPXHandler,
|
|
399
403
|
): void {
|
|
400
404
|
let handler: QHTTPXHandler;
|
|
401
405
|
let schema: RouteSchema | Record<string, unknown> | undefined;
|
|
@@ -403,30 +407,87 @@ export class QHTTPX {
|
|
|
403
407
|
|
|
404
408
|
if (typeof handlerOrOptions === 'function') {
|
|
405
409
|
handler = handlerOrOptions;
|
|
406
|
-
} else {
|
|
407
|
-
handler =
|
|
410
|
+
} else if (handlerIfOptions) {
|
|
411
|
+
handler = handlerIfOptions;
|
|
408
412
|
schema = handlerOrOptions.schema;
|
|
409
413
|
options.priority = handlerOrOptions.priority;
|
|
414
|
+
} else {
|
|
415
|
+
const opts = handlerOrOptions as QHTTPXRouteOptions;
|
|
416
|
+
handler = opts.handler;
|
|
417
|
+
schema = opts.schema;
|
|
418
|
+
options.priority = opts.priority;
|
|
410
419
|
}
|
|
411
420
|
|
|
412
421
|
const compiled = this.compileRoutePipeline(handler, schema);
|
|
413
422
|
this.router.register(method, path, compiled, { ...options, schema });
|
|
414
423
|
}
|
|
415
424
|
|
|
416
|
-
get(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void
|
|
417
|
-
|
|
425
|
+
get(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
|
|
426
|
+
get(
|
|
427
|
+
path: string,
|
|
428
|
+
config: import('./types').QHTTPXRouteConfig,
|
|
429
|
+
handler: QHTTPXHandler,
|
|
430
|
+
): void;
|
|
431
|
+
get(
|
|
432
|
+
path: string,
|
|
433
|
+
handlerOrOptions:
|
|
434
|
+
| QHTTPXHandler
|
|
435
|
+
| QHTTPXRouteOptions
|
|
436
|
+
| import('./types').QHTTPXRouteConfig,
|
|
437
|
+
handler?: QHTTPXHandler,
|
|
438
|
+
): void {
|
|
439
|
+
this.registerRoute('GET', path, handlerOrOptions, handler);
|
|
418
440
|
}
|
|
419
441
|
|
|
420
|
-
post(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void
|
|
421
|
-
|
|
442
|
+
post(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
|
|
443
|
+
post(
|
|
444
|
+
path: string,
|
|
445
|
+
config: import('./types').QHTTPXRouteConfig,
|
|
446
|
+
handler: QHTTPXHandler,
|
|
447
|
+
): void;
|
|
448
|
+
post(
|
|
449
|
+
path: string,
|
|
450
|
+
handlerOrOptions:
|
|
451
|
+
| QHTTPXHandler
|
|
452
|
+
| QHTTPXRouteOptions
|
|
453
|
+
| import('./types').QHTTPXRouteConfig,
|
|
454
|
+
handler?: QHTTPXHandler,
|
|
455
|
+
): void {
|
|
456
|
+
this.registerRoute('POST', path, handlerOrOptions, handler);
|
|
422
457
|
}
|
|
423
458
|
|
|
424
|
-
put(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void
|
|
425
|
-
|
|
459
|
+
put(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
|
|
460
|
+
put(
|
|
461
|
+
path: string,
|
|
462
|
+
config: import('./types').QHTTPXRouteConfig,
|
|
463
|
+
handler: QHTTPXHandler,
|
|
464
|
+
): void;
|
|
465
|
+
put(
|
|
466
|
+
path: string,
|
|
467
|
+
handlerOrOptions:
|
|
468
|
+
| QHTTPXHandler
|
|
469
|
+
| QHTTPXRouteOptions
|
|
470
|
+
| import('./types').QHTTPXRouteConfig,
|
|
471
|
+
handler?: QHTTPXHandler,
|
|
472
|
+
): void {
|
|
473
|
+
this.registerRoute('PUT', path, handlerOrOptions, handler);
|
|
426
474
|
}
|
|
427
475
|
|
|
428
|
-
delete(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void
|
|
429
|
-
|
|
476
|
+
delete(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
|
|
477
|
+
delete(
|
|
478
|
+
path: string,
|
|
479
|
+
config: import('./types').QHTTPXRouteConfig,
|
|
480
|
+
handler: QHTTPXHandler,
|
|
481
|
+
): void;
|
|
482
|
+
delete(
|
|
483
|
+
path: string,
|
|
484
|
+
handlerOrOptions:
|
|
485
|
+
| QHTTPXHandler
|
|
486
|
+
| QHTTPXRouteOptions
|
|
487
|
+
| import('./types').QHTTPXRouteConfig,
|
|
488
|
+
handler?: QHTTPXHandler,
|
|
489
|
+
): void {
|
|
490
|
+
this.registerRoute('DELETE', path, handlerOrOptions, handler);
|
|
430
491
|
}
|
|
431
492
|
|
|
432
493
|
route(path: string) {
|
|
@@ -541,7 +602,22 @@ export class QHTTPX {
|
|
|
541
602
|
return generator.generate();
|
|
542
603
|
}
|
|
543
604
|
|
|
544
|
-
public async listen(
|
|
605
|
+
public async listen(
|
|
606
|
+
port: number,
|
|
607
|
+
hostnameOrCallback?: string | (() => void),
|
|
608
|
+
callback?: () => void,
|
|
609
|
+
): Promise<{ port: number }> {
|
|
610
|
+
let hostname: string | undefined;
|
|
611
|
+
let cb: (() => void) | undefined;
|
|
612
|
+
|
|
613
|
+
if (typeof hostnameOrCallback === 'function') {
|
|
614
|
+
cb = hostnameOrCallback;
|
|
615
|
+
hostname = undefined;
|
|
616
|
+
} else {
|
|
617
|
+
hostname = hostnameOrCallback;
|
|
618
|
+
cb = callback;
|
|
619
|
+
}
|
|
620
|
+
|
|
545
621
|
if (this.options.database) {
|
|
546
622
|
await this.options.database.connect();
|
|
547
623
|
}
|
|
@@ -567,6 +643,9 @@ export class QHTTPX {
|
|
|
567
643
|
this.router.freeze();
|
|
568
644
|
void this.runLifecycleHooks(this.onStartHooks);
|
|
569
645
|
const address = this.server.address();
|
|
646
|
+
|
|
647
|
+
if (cb) cb();
|
|
648
|
+
|
|
570
649
|
if (address && typeof address === 'object') {
|
|
571
650
|
resolve({ port: address.port });
|
|
572
651
|
} else {
|
package/src/core/types.ts
CHANGED
package/tsconfig.json
CHANGED