qhttpx 1.8.5 → 1.8.11
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 +52 -0
- package/README.md +72 -52
- package/binding.gyp +18 -0
- package/dist/examples/api-server.js +29 -8
- package/dist/examples/basic.d.ts +1 -0
- package/dist/examples/basic.js +10 -0
- package/dist/examples/compression.d.ts +1 -0
- package/dist/examples/compression.js +17 -0
- package/dist/examples/cors.d.ts +1 -0
- package/dist/examples/cors.js +19 -0
- package/dist/examples/errors.d.ts +1 -0
- package/dist/examples/errors.js +25 -0
- package/dist/examples/file-upload.d.ts +1 -0
- package/dist/examples/file-upload.js +24 -0
- package/dist/examples/fusion.d.ts +1 -0
- package/dist/examples/fusion.js +21 -0
- package/dist/examples/rate-limiting.d.ts +1 -0
- package/dist/examples/rate-limiting.js +17 -0
- package/dist/examples/validation.d.ts +1 -0
- package/dist/examples/validation.js +23 -0
- package/dist/examples/websockets.d.ts +1 -0
- package/dist/examples/websockets.js +20 -0
- package/dist/package.json +11 -1
- package/dist/src/benchmarks/simple-json.js +6 -4
- package/dist/src/cli/index.js +33 -11
- package/dist/src/core/errors.d.ts +34 -0
- package/dist/src/core/errors.js +70 -0
- package/dist/src/core/native-adapter.d.ts +11 -0
- package/dist/src/core/native-adapter.js +211 -0
- package/dist/src/core/server.d.ts +52 -4
- package/dist/src/core/server.js +389 -261
- package/dist/src/core/types.d.ts +37 -0
- package/dist/src/index.d.ts +6 -1
- package/dist/src/index.js +19 -3
- package/dist/src/middleware/compression.d.ts +1 -5
- package/dist/src/middleware/cors.d.ts +1 -10
- package/dist/src/middleware/presets.d.ts +4 -1
- package/dist/src/middleware/presets.js +22 -3
- package/dist/src/middleware/rate-limit.d.ts +1 -19
- package/dist/src/middleware/rate-limit.js +6 -0
- package/dist/src/middleware/security.d.ts +1 -2
- package/dist/src/native/index.d.ts +29 -0
- package/dist/src/native/index.js +64 -0
- package/dist/src/router/radix-tree.d.ts +2 -0
- package/dist/src/router/radix-tree.js +54 -4
- package/dist/src/router/router.d.ts +1 -0
- package/dist/src/router/router.js +42 -2
- package/dist/tests/native-adapter.test.d.ts +1 -0
- package/dist/tests/native-adapter.test.js +71 -0
- package/dist/tests/resources.test.js +3 -0
- package/dist/tests/security.test.js +2 -2
- package/docs/AEGIS.md +34 -9
- package/docs/BENCHMARKS.md +8 -7
- package/docs/ERRORS.md +112 -0
- package/docs/FUSION.md +68 -0
- package/docs/MIDDLEWARE.md +65 -0
- package/docs/ROUTING.md +70 -0
- package/docs/STATIC.md +61 -0
- package/docs/WEBSOCKETS.md +76 -0
- package/package.json +11 -1
- package/src/native/README.md +31 -0
- package/src/native/addon.cc +8 -0
- package/src/native/index.ts +78 -0
- package/src/native/picohttpparser.c +608 -0
- package/src/native/picohttpparser.h +71 -0
- package/src/native/server.cc +262 -0
- package/src/native/server.h +30 -0
- package/.eslintrc.json +0 -22
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/npm-publish.yml +0 -37
- package/.github/workflows/release.yml +0 -21
- package/.prettierrc +0 -7
- package/assets/logo.svg +0 -25
- package/eslint.config.cjs +0 -26
- package/examples/api-server.ts +0 -62
- package/src/benchmarks/quantam-users.ts +0 -70
- package/src/benchmarks/simple-json.ts +0 -71
- package/src/benchmarks/ultra-mode.ts +0 -127
- package/src/cli/index.ts +0 -214
- package/src/client/index.ts +0 -93
- package/src/core/batch.ts +0 -110
- package/src/core/body-parser.ts +0 -151
- package/src/core/buffer-pool.ts +0 -96
- package/src/core/config.ts +0 -60
- package/src/core/fusion.ts +0 -210
- package/src/core/logger.ts +0 -70
- package/src/core/metrics.ts +0 -166
- package/src/core/resources.ts +0 -38
- package/src/core/scheduler.ts +0 -126
- package/src/core/scope.ts +0 -87
- package/src/core/serializer.ts +0 -41
- package/src/core/server.ts +0 -1234
- package/src/core/stream.ts +0 -111
- package/src/core/tasks.ts +0 -138
- package/src/core/types.ts +0 -192
- package/src/core/websocket.ts +0 -112
- package/src/core/worker-queue.ts +0 -90
- package/src/database/adapters/memory.ts +0 -99
- package/src/database/adapters/mongo.ts +0 -116
- package/src/database/adapters/postgres.ts +0 -86
- package/src/database/adapters/sqlite.ts +0 -44
- package/src/database/coalescer.ts +0 -153
- package/src/database/manager.ts +0 -97
- package/src/database/types.ts +0 -24
- package/src/index.ts +0 -58
- package/src/middleware/compression.ts +0 -147
- package/src/middleware/cors.ts +0 -98
- package/src/middleware/presets.ts +0 -50
- package/src/middleware/rate-limit.ts +0 -106
- package/src/middleware/security.ts +0 -109
- package/src/middleware/static.ts +0 -216
- package/src/openapi/generator.ts +0 -167
- package/src/router/radix-router.ts +0 -119
- package/src/router/radix-tree.ts +0 -106
- package/src/router/router.ts +0 -190
- package/src/testing/index.ts +0 -104
- package/src/utils/cookies.ts +0 -67
- package/src/utils/logger.ts +0 -59
- package/src/utils/signals.ts +0 -45
- package/src/utils/sse.ts +0 -41
- package/src/validation/index.ts +0 -3
- package/src/validation/simple.ts +0 -93
- package/src/validation/types.ts +0 -38
- package/src/validation/zod.ts +0 -14
- package/src/views/index.ts +0 -1
- package/src/views/types.ts +0 -4
- package/tests/adapters.test.ts +0 -120
- package/tests/batch.test.ts +0 -139
- package/tests/body-parser.test.ts +0 -83
- package/tests/compression-sse.test.ts +0 -98
- package/tests/cookies.test.ts +0 -74
- package/tests/cors.test.ts +0 -79
- package/tests/database.test.ts +0 -90
- package/tests/dx.test.ts +0 -130
- package/tests/ecosystem.test.ts +0 -156
- package/tests/features.test.ts +0 -51
- package/tests/fusion.test.ts +0 -121
- package/tests/http-basic.test.ts +0 -161
- package/tests/logger.test.ts +0 -48
- package/tests/middleware.test.ts +0 -137
- package/tests/observability.test.ts +0 -91
- package/tests/openapi.test.ts +0 -74
- package/tests/plugin.test.ts +0 -85
- package/tests/plugins.test.ts +0 -93
- package/tests/rate-limit.test.ts +0 -97
- package/tests/resources.test.ts +0 -64
- package/tests/scheduler.test.ts +0 -71
- package/tests/schema-routes.test.ts +0 -89
- package/tests/security.test.ts +0 -128
- package/tests/server-db.test.ts +0 -72
- package/tests/smoke.test.ts +0 -9
- package/tests/sqlite-fusion.test.ts +0 -106
- package/tests/static.test.ts +0 -111
- package/tests/stream.test.ts +0 -58
- package/tests/task-metrics.test.ts +0 -78
- package/tests/tasks.test.ts +0 -90
- package/tests/testing.test.ts +0 -53
- package/tests/validation.test.ts +0 -126
- package/tests/websocket.test.ts +0 -132
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -9
package/docs/AEGIS.md
CHANGED
|
@@ -12,7 +12,25 @@
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
### Zero-Config (Recommended)
|
|
16
|
+
|
|
17
|
+
The easiest way to enable Aegis is via the `createHttpApp` factory:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { createHttpApp } from 'qhttpx';
|
|
21
|
+
|
|
22
|
+
const app = createHttpApp({
|
|
23
|
+
rateLimit: {
|
|
24
|
+
windowMs: 60 * 1000, // 1 minute
|
|
25
|
+
max: 100, // Limit each IP to 100 requests per window
|
|
26
|
+
trustProxy: true // Enable if behind Nginx/Cloudflare
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Manual Middleware
|
|
32
|
+
|
|
33
|
+
For more control, import `rateLimit` from the middleware package:
|
|
16
34
|
|
|
17
35
|
```typescript
|
|
18
36
|
import { QHTTPX } from 'qhttpx';
|
|
@@ -26,17 +44,24 @@ app.use(rateLimit({
|
|
|
26
44
|
max: 100, // Limit each IP to 100 requests per window
|
|
27
45
|
message: { error: 'Too many requests, slow down!' }
|
|
28
46
|
}));
|
|
47
|
+
```
|
|
29
48
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
max: 5000,
|
|
34
|
-
keyGenerator: (ctx) => ctx.req.headers['x-api-key'] || ctx.req.socket.remoteAddress
|
|
35
|
-
}));
|
|
49
|
+
## Proxy Support
|
|
50
|
+
|
|
51
|
+
If your app sits behind a reverse proxy (Nginx, AWS ALB, Cloudflare), you must configure **Proxy Trust** so Aegis sees the real user IP instead of the load balancer's IP.
|
|
36
52
|
|
|
37
|
-
|
|
53
|
+
```typescript
|
|
54
|
+
// In createHttpApp
|
|
55
|
+
const app = createHttpApp({
|
|
56
|
+
rateLimit: {
|
|
57
|
+
trustProxy: true // Trusts X-Forwarded-For header
|
|
58
|
+
}
|
|
59
|
+
});
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
// Or manually
|
|
62
|
+
app.use(rateLimit({
|
|
63
|
+
trustProxy: true
|
|
64
|
+
}));
|
|
40
65
|
```
|
|
41
66
|
|
|
42
67
|
## Options
|
package/docs/BENCHMARKS.md
CHANGED
|
@@ -23,14 +23,15 @@ This will:
|
|
|
23
23
|
- Run an `autocannon` benchmark against `GET /json`
|
|
24
24
|
- Shut down the server when the run completes
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Latest Results (Local Windows Environment)
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Configuration: 100 connections, 10 pipelining, 40s duration (Average of 2 runs, taking the second result)
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
| Framework | Req/Sec | Latency (Avg) | Throughput |
|
|
31
|
+
| :--- | :--- | :--- | :--- |
|
|
32
|
+
| **Fastify** | 16,050 | 256.78 ms | 3.00 Mb/s |
|
|
33
|
+
| **QHTTPX** | **14,179** | **291.13 ms** | **3.02 Mb/s** |
|
|
34
|
+
| **Hono** | 14,176 | 294.09 ms | 2.43 Mb/s |
|
|
35
|
+
| **Express** | 10,450 | 407.12 ms | 1.94 Mb/s |
|
|
34
36
|
|
|
35
|
-
You can adjust these parameters in `src/benchmarks/simple-json.ts`.
|
|
36
37
|
|
package/docs/ERRORS.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
QHTTPX provides a robust and unified error handling system designed to make building reliable APIs simple and consistent.
|
|
4
|
+
|
|
5
|
+
## Standard Exceptions
|
|
6
|
+
|
|
7
|
+
Gone are the days of manually setting status codes and message strings. QHTTPX exports a set of **Standard HTTP Exceptions** that cover most common API scenarios.
|
|
8
|
+
|
|
9
|
+
These exceptions automatically set the correct HTTP status code and structure the JSON response.
|
|
10
|
+
|
|
11
|
+
### Available Exceptions
|
|
12
|
+
|
|
13
|
+
Import them directly from `qhttpx`:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import {
|
|
17
|
+
BadRequestException,
|
|
18
|
+
UnauthorizedException,
|
|
19
|
+
ForbiddenException,
|
|
20
|
+
NotFoundException,
|
|
21
|
+
MethodNotAllowedException,
|
|
22
|
+
ConflictException,
|
|
23
|
+
PayloadTooLargeException,
|
|
24
|
+
UnsupportedMediaTypeException,
|
|
25
|
+
TooManyRequestsException,
|
|
26
|
+
InternalServerErrorException,
|
|
27
|
+
ServiceUnavailableException
|
|
28
|
+
} from 'qhttpx';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Usage in Routes
|
|
32
|
+
|
|
33
|
+
Simply `throw` an exception within your handler. QHTTPX will catch it and format the response.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
app.get('/users/:id', ({ params }) => {
|
|
37
|
+
const user = findUser(params.id);
|
|
38
|
+
|
|
39
|
+
if (!user) {
|
|
40
|
+
throw new NotFoundException(`User with ID ${params.id} not found`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (user.isLocked) {
|
|
44
|
+
throw new ForbiddenException('This account is locked');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return user;
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Response Example (`NotFoundException`):**
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"error": "Not Found",
|
|
55
|
+
"code": "NOT_FOUND",
|
|
56
|
+
"details": "User with ID 123 not found",
|
|
57
|
+
"statusCode": 404
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Global Error Handler
|
|
62
|
+
|
|
63
|
+
You can define a global error handler to intercept all errors (both standard exceptions and unexpected runtime errors) to customize logging or response formatting.
|
|
64
|
+
|
|
65
|
+
Use `app.onError()`:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
app.onError(({ error, json }) => {
|
|
69
|
+
console.error('Captured Error:', error);
|
|
70
|
+
|
|
71
|
+
// Check if it's a known HTTP error
|
|
72
|
+
if (error instanceof HttpError) {
|
|
73
|
+
return json({
|
|
74
|
+
status: 'error',
|
|
75
|
+
message: error.message,
|
|
76
|
+
code: error.code
|
|
77
|
+
}, error.statusCode);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle unknown/unexpected errors
|
|
81
|
+
return json({
|
|
82
|
+
status: 'critical',
|
|
83
|
+
message: 'Internal Server Error'
|
|
84
|
+
}, 500);
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Async Error Handling
|
|
89
|
+
|
|
90
|
+
QHTTPX handles `async` errors automatically. You don't need `try/catch` blocks for every route unless you want specific local handling. If an async function rejects, the Global Error Handler will catch it.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// This works automatically!
|
|
94
|
+
app.get('/db-query', async () => {
|
|
95
|
+
const data = await db.query('SELECT * FROM users'); // If this throws, it's caught.
|
|
96
|
+
return data;
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Validation Errors
|
|
101
|
+
|
|
102
|
+
When using the built-in validation system, validation failures automatically throw a `BadRequestException` containing detailed error messages about which fields failed.
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"error": "Bad Request",
|
|
107
|
+
"code": "BAD_REQUEST",
|
|
108
|
+
"details": [
|
|
109
|
+
{ "field": "email", "message": "Invalid email format" }
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
```
|
package/docs/FUSION.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Request Fusion Engine (Layer 2 Coalescing)
|
|
2
|
+
|
|
3
|
+
**Request Fusion** is QHTTPX's flagship performance feature. It is a "Layer 2 Coalescing" engine designed to protect your database and downstream services from the "Thundering Herd" problem.
|
|
4
|
+
|
|
5
|
+
## The Problem: The Thundering Herd
|
|
6
|
+
|
|
7
|
+
Imagine your API serves a popular resource, like "Trending News" (`GET /api/trending`).
|
|
8
|
+
During a viral event, you might receive **10,000 requests per second** for this *exact same resource*.
|
|
9
|
+
|
|
10
|
+
In traditional frameworks (Express/Fastify):
|
|
11
|
+
1. Node.js accepts 10,000 requests.
|
|
12
|
+
2. The handler runs 10,000 times.
|
|
13
|
+
3. Your database receives 10,000 identical queries.
|
|
14
|
+
4. **Result**: Database CPU spikes to 100%, latency skyrockets, and the site crashes.
|
|
15
|
+
|
|
16
|
+
## The Solution: Request Fusion
|
|
17
|
+
|
|
18
|
+
With Request Fusion enabled, QHTTPX acts intelligently:
|
|
19
|
+
|
|
20
|
+
1. **Request 1** arrives: QHTTPX marks it as the **Leader** and starts processing (querying the DB).
|
|
21
|
+
2. **Requests 2 through 10,000** arrive *while Request 1 is still processing*.
|
|
22
|
+
3. QHTTPX identifies them as **Followers** (identical semantic fingerprint).
|
|
23
|
+
4. These requests **wait** for the Leader. They do *not* execute the handler.
|
|
24
|
+
5. **Leader finishes**: The result is broadcast to all 9,999 Followers instantly.
|
|
25
|
+
|
|
26
|
+
**Result**:
|
|
27
|
+
- **1** Database Query.
|
|
28
|
+
- **10,000** Happy Users.
|
|
29
|
+
- **Zero** Database Load.
|
|
30
|
+
|
|
31
|
+
## Enabling Fusion
|
|
32
|
+
|
|
33
|
+
Request Fusion is optional but recommended for read-heavy public APIs.
|
|
34
|
+
|
|
35
|
+
### Using `createHttpApp` (Zero-Config)
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const app = createHttpApp({
|
|
39
|
+
enableRequestFusion: true
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Manual Configuration
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const app = new QHTTPX({
|
|
47
|
+
enableRequestFusion: true
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Semantic Hashing
|
|
52
|
+
|
|
53
|
+
How does QHTTPX know requests are identical? It generates a **Semantic Key** based on:
|
|
54
|
+
|
|
55
|
+
1. **HTTP Method** (e.g., `GET`)
|
|
56
|
+
2. **URL Path** (e.g., `/api/trending`)
|
|
57
|
+
3. **Query Parameters** (sorted) (e.g., `?category=tech&page=1`)
|
|
58
|
+
4. **Request Body** (if JSON)
|
|
59
|
+
5. **Vary Headers** (Configurable, e.g., `Accept-Language`)
|
|
60
|
+
|
|
61
|
+
## Micro-TTL
|
|
62
|
+
|
|
63
|
+
For even higher performance, Fusion includes a "Micro-TTL" mechanism. Even after a request finishes, the result can be held for a tiny window (e.g., 100ms) to satisfy immediately subsequent requests, acting as an ultra-short-term cache for burst traffic.
|
|
64
|
+
|
|
65
|
+
## Best Practices
|
|
66
|
+
|
|
67
|
+
- **Idempotency**: Fusion is safest for `GET` requests. QHTTPX automatically disables Fusion for `POST`, `PUT`, `DELETE`, and `PATCH` by default to prevent side-effect duplication issues, though this can be overridden if you know what you are doing.
|
|
68
|
+
- **Personalized Content**: Be careful with endpoints that return user-specific data (like "My Profile"). If the response depends on the `Authorization` header, ensure that header is part of the Fusion Key (or disable Fusion for that route).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Middleware
|
|
2
|
+
|
|
3
|
+
Middleware in QHTTPX functions similarly to Express or Koa but is optimized for the async scheduler. It allows you to execute code before or after the route handler.
|
|
4
|
+
|
|
5
|
+
## Global Middleware
|
|
6
|
+
|
|
7
|
+
Register middleware using `app.use()`. It applies to all routes registered *after* it.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
app.use(async (ctx, next) => {
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
|
|
13
|
+
await next(); // Wait for downstream middleware/handler
|
|
14
|
+
|
|
15
|
+
const ms = Date.now() - start;
|
|
16
|
+
ctx.res.setHeader('X-Response-Time', `${ms}ms`);
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Destructuring Syntax
|
|
21
|
+
|
|
22
|
+
You can destructure the context arguments for cleaner code:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
app.use(async ({ req, res }, next) => {
|
|
26
|
+
console.log(`${req.method} ${req.url}`);
|
|
27
|
+
await next();
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Built-in Middleware
|
|
32
|
+
|
|
33
|
+
QHTTPX comes with essential middleware optimized for its core.
|
|
34
|
+
|
|
35
|
+
### Rate Limiting (Aegis)
|
|
36
|
+
|
|
37
|
+
See [Aegis Documentation](./AEGIS.md).
|
|
38
|
+
|
|
39
|
+
### CORS
|
|
40
|
+
|
|
41
|
+
Enabled via `createHttpApp` or manually.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const app = createHttpApp({ cors: true });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Compression
|
|
48
|
+
|
|
49
|
+
Automatically compresses responses using Gzip/Brotli.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const app = createHttpApp({ compression: true });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Body Parser
|
|
56
|
+
|
|
57
|
+
The body parser is **built-in** and lazy-loaded. You do not need to `app.use(bodyParser())`.
|
|
58
|
+
Just access `ctx.body` and it will be parsed on demand.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
app.post('/data', ({ body }) => {
|
|
62
|
+
// body is already parsed (JSON/URL-encoded)
|
|
63
|
+
return { received: body };
|
|
64
|
+
});
|
|
65
|
+
```
|
package/docs/ROUTING.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
QHTTPX employs a high-performance **Radix Tree** (prefix tree) router. This data structure allows for `O(k)` route lookup time (where `k` is the length of the URL), making it significantly faster than the linear regex matching used by Express or Koa.
|
|
4
|
+
|
|
5
|
+
## Basic Routing
|
|
6
|
+
|
|
7
|
+
Supported methods: `get`, `post`, `put`, `delete`, `patch`, `options`, `head`.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
app.get('/', (ctx) => {
|
|
11
|
+
return ctx.json({ hello: 'world' });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.post('/items', (ctx) => {
|
|
15
|
+
// ...
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Route Parameters
|
|
20
|
+
|
|
21
|
+
Capture dynamic segments of the URL using colons (`:`).
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
app.get('/users/:id', (ctx) => {
|
|
25
|
+
const { id } = ctx.params;
|
|
26
|
+
return ctx.json({ userId: id });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Multiple parameters
|
|
30
|
+
app.get('/posts/:year/:month/:slug', (ctx) => {
|
|
31
|
+
const { year, month, slug } = ctx.params;
|
|
32
|
+
// ...
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Wildcards
|
|
37
|
+
|
|
38
|
+
Match everything following a path using `*`.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// Matches /files/image.png, /files/docs/report.pdf, etc.
|
|
42
|
+
app.get('/files/*', (ctx) => {
|
|
43
|
+
const path = ctx.params['*']; // "image.png"
|
|
44
|
+
// ...
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Route Destructuring
|
|
49
|
+
|
|
50
|
+
The handler context can be destructured for cleaner code:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
app.get('/search', ({ query, json }) => {
|
|
54
|
+
const { q } = query;
|
|
55
|
+
return json({ results: performSearch(q) });
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Scoped Routes (Groups)
|
|
60
|
+
|
|
61
|
+
Use plugins to create route groups with prefixes (similar to `express.Router` or Fastify encapsulation).
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const apiV1 = async (app: QHTTPX, opts: any) => {
|
|
65
|
+
app.get('/users', () => { ... }); // Becomes /api/v1/users
|
|
66
|
+
app.get('/posts', () => { ... }); // Becomes /api/v1/posts
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
app.register(apiV1, { prefix: '/api/v1' });
|
|
70
|
+
```
|
package/docs/STATIC.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Static Files & Streaming
|
|
2
|
+
|
|
3
|
+
QHTTPX includes a high-performance static file server capable of handling media streaming (video/audio) and partial content requests out of the box.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
To serve static files (like images, CSS, JS) from a directory:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
// Serve files from the 'public' folder at the root URL
|
|
13
|
+
app.static('/', path.join(__dirname, 'public'));
|
|
14
|
+
|
|
15
|
+
// Serve files from 'assets' under the '/static' prefix
|
|
16
|
+
app.static('/static', path.join(__dirname, 'assets'));
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
### 1. Range Requests (Video Streaming)
|
|
22
|
+
|
|
23
|
+
The static handler automatically supports `Range` headers (HTTP 206 Partial Content). This means you can serve video files directly to browsers, and users can seek (jump) to different parts of the video without downloading the whole file.
|
|
24
|
+
|
|
25
|
+
### 2. Smart Caching
|
|
26
|
+
|
|
27
|
+
It automatically handles:
|
|
28
|
+
- `Last-Modified` headers.
|
|
29
|
+
- `ETag` generation.
|
|
30
|
+
- `304 Not Modified` responses to save bandwidth.
|
|
31
|
+
|
|
32
|
+
### 3. Streaming (Low Memory)
|
|
33
|
+
|
|
34
|
+
Files are streamed to the response using Node.js streams. This ensures that serving a large file (e.g., 1GB video) does not spike your server's RAM usage.
|
|
35
|
+
|
|
36
|
+
## Configuration Options
|
|
37
|
+
|
|
38
|
+
You can customize the static handler:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
app.static('/public', './public', {
|
|
42
|
+
maxAge: 86400, // Cache-Control max-age in seconds (default: 0)
|
|
43
|
+
index: 'index.html', // Default file for directories (default: index.html)
|
|
44
|
+
hidden: false // Serve hidden files starting with . (default: false)
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Manual Streaming
|
|
49
|
+
|
|
50
|
+
If you need to stream generated content or files manually within a route:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import fs from 'fs';
|
|
54
|
+
|
|
55
|
+
app.get('/download', ({ res }) => {
|
|
56
|
+
const stream = fs.createReadStream('./large-file.zip');
|
|
57
|
+
|
|
58
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
59
|
+
stream.pipe(res);
|
|
60
|
+
});
|
|
61
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# WebSockets
|
|
2
|
+
|
|
3
|
+
QHTTPX includes a first-class, high-performance WebSocket implementation with built-in Pub/Sub capabilities. It handles protocol upgrades automatically within the same HTTP server instance.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### 1. Enable WebSockets
|
|
8
|
+
|
|
9
|
+
No special configuration is needed. The WebSocket server is attached to the HTTP server automatically.
|
|
10
|
+
|
|
11
|
+
### 2. Handle Upgrades
|
|
12
|
+
|
|
13
|
+
Use the `app.upgrade` method to handle the initial handshake.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
app.upgrade('/ws', (req, socket, head) => {
|
|
17
|
+
// Perform authentication here if needed
|
|
18
|
+
// ...
|
|
19
|
+
|
|
20
|
+
// Accept the upgrade
|
|
21
|
+
app.websocket.handleUpgrade(req, socket, head, (ws) => {
|
|
22
|
+
app.websocket.emit('connection', ws, req);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 3. Handle Connections
|
|
28
|
+
|
|
29
|
+
Listen for the `connection` event on the `app.websocket` instance.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
app.websocket.on('connection', (ws, req) => {
|
|
33
|
+
console.log('New client connected!');
|
|
34
|
+
|
|
35
|
+
ws.on('message', (msg) => {
|
|
36
|
+
console.log('Received:', msg.toString());
|
|
37
|
+
ws.send(`Echo: ${msg}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
ws.on('close', () => {
|
|
41
|
+
console.log('Client disconnected');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Pub/Sub (Rooms)
|
|
47
|
+
|
|
48
|
+
QHTTPX allows you to broadcast messages to specific "rooms" or groups of clients.
|
|
49
|
+
|
|
50
|
+
### Joining a Room
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
app.websocket.on('connection', (ws) => {
|
|
54
|
+
// Client joins 'chat-room-1'
|
|
55
|
+
ws.subscribe('chat-room-1');
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Broadcasting to a Room
|
|
60
|
+
|
|
61
|
+
You can broadcast from anywhere in your application (even outside the socket handler).
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Send to everyone in 'chat-room-1'
|
|
65
|
+
app.websocket.to('chat-room-1').emit('New Message!');
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Leaving a Room
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
ws.unsubscribe('chat-room-1');
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Performance Note
|
|
75
|
+
|
|
76
|
+
The WebSocket implementation shares the same efficient async scheduler as the HTTP layer, allowing thousands of concurrent connections with minimal overhead.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qhttpx",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.11",
|
|
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",
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
"bin": {
|
|
15
15
|
"qhttpx": "./dist/src/cli/index.js"
|
|
16
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src/native",
|
|
20
|
+
"binding.gyp",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"docs"
|
|
25
|
+
],
|
|
17
26
|
"directories": {
|
|
18
27
|
"doc": "docs"
|
|
19
28
|
},
|
|
@@ -82,6 +91,7 @@
|
|
|
82
91
|
"better-sqlite3": "^12.6.2",
|
|
83
92
|
"busboy": "^1.6.0",
|
|
84
93
|
"fast-json-stringify": "^5.15.1",
|
|
94
|
+
"node-addon-api": "^8.5.0",
|
|
85
95
|
"pino": "^10.2.0",
|
|
86
96
|
"pino-pretty": "^13.1.3",
|
|
87
97
|
"quantam-async": "^0.1.1",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# QHTTPX Native Addon
|
|
2
|
+
|
|
3
|
+
This directory contains the C++ native addon for QHTTPX to accelerate HTTP parsing and response generation.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
To build this addon, you need:
|
|
8
|
+
- **Windows**: Visual Studio Build Tools (Desktop development with C++).
|
|
9
|
+
- **Linux/macOS**: Python 3, make, and a C++ compiler (GCC/Clang).
|
|
10
|
+
|
|
11
|
+
## Building
|
|
12
|
+
|
|
13
|
+
The build is handled automatically by `npm install` if `node-gyp` and build tools are present.
|
|
14
|
+
|
|
15
|
+
To manually rebuild:
|
|
16
|
+
```bash
|
|
17
|
+
npx node-gyp rebuild
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Structure
|
|
21
|
+
|
|
22
|
+
- `addon.cc`: Entry point, registers the module.
|
|
23
|
+
- `server.cc`: `NativeServer` class implementation.
|
|
24
|
+
- `picohttpparser.c/h`: The HTTP parser library (vendored).
|
|
25
|
+
- `index.ts`: TypeScript wrapper and fallback logic.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
The `NativeAdapter` in `src/core/native-adapter.ts` automatically detects if the native addon is available.
|
|
30
|
+
If available, it uses `net.Server` + `NativeServer` for handling requests.
|
|
31
|
+
If not, it falls back to the standard `http.Server`.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export interface NativeServerBinding {
|
|
4
|
+
parse(buffer: Buffer): {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
version: number;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
bodyOffset: number;
|
|
10
|
+
} | null;
|
|
11
|
+
createResponse(statusCode: number, headers: Record<string, string>, body?: string | Buffer): Buffer;
|
|
12
|
+
createJSONResponse(obj: unknown): Buffer;
|
|
13
|
+
writeResponse(fd: number, chunks: (Buffer | string)[]): void;
|
|
14
|
+
setCPUAffinity(cpuId: number): boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
let nativeBinding: any = null;
|
|
19
|
+
|
|
20
|
+
// Use try/catch with module.createRequire if import.meta is not available in some build targets
|
|
21
|
+
// However, since we are in a TS module that might be commonjs or esm
|
|
22
|
+
// We'll use a safer approach for require
|
|
23
|
+
try {
|
|
24
|
+
const req = require;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
nativeBinding = req('../../build/Release/qhttpx_native.node');
|
|
28
|
+
} catch {
|
|
29
|
+
try {
|
|
30
|
+
nativeBinding = req('../../build/Debug/qhttpx_native.node');
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignored
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignored
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class NativeServer {
|
|
40
|
+
private binding: NativeServerBinding | null;
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
if (nativeBinding && nativeBinding.NativeServer) {
|
|
44
|
+
this.binding = new nativeBinding.NativeServer();
|
|
45
|
+
} else {
|
|
46
|
+
this.binding = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public get isAvailable(): boolean {
|
|
51
|
+
return this.binding !== null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public parse(buffer: Buffer) {
|
|
55
|
+
if (!this.binding) throw new Error('Native bindings not available');
|
|
56
|
+
return this.binding.parse(buffer);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public createResponse(statusCode: number, headers: Record<string, string>, body?: string | Buffer) {
|
|
60
|
+
if (!this.binding) throw new Error('Native bindings not available');
|
|
61
|
+
return this.binding.createResponse(statusCode, headers, body);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public createJSONResponse(obj: unknown) {
|
|
65
|
+
if (!this.binding) throw new Error('Native bindings not available');
|
|
66
|
+
return this.binding.createJSONResponse(obj);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public writeResponse(fd: number, chunks: (Buffer | string)[]) {
|
|
70
|
+
if (!this.binding) throw new Error('Native bindings not available');
|
|
71
|
+
return this.binding.writeResponse(fd, chunks);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public setCPUAffinity(cpuId: number) {
|
|
75
|
+
if (!this.binding) throw new Error('Native bindings not available');
|
|
76
|
+
return this.binding.setCPUAffinity(cpuId);
|
|
77
|
+
}
|
|
78
|
+
}
|