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/dist/src/core/types.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export type QHTTPXContext = {
|
|
|
44
44
|
readonly res: ServerResponse;
|
|
45
45
|
readonly headers: IncomingHttpHeaders;
|
|
46
46
|
readonly url: URL;
|
|
47
|
+
readonly method: HTTPMethod;
|
|
48
|
+
readonly ip: string;
|
|
47
49
|
readonly params: Record<string, string>;
|
|
48
50
|
readonly query: Record<string, string | string[]>;
|
|
49
51
|
body: unknown;
|
|
@@ -102,6 +104,25 @@ export type QHTTPXTaskOptions = {
|
|
|
102
104
|
backoffMs?: number;
|
|
103
105
|
};
|
|
104
106
|
export type PerformanceMode = 'balanced' | 'ultra';
|
|
107
|
+
export interface RateLimitStore {
|
|
108
|
+
increment(key: string, windowMs: number): Promise<{
|
|
109
|
+
total: number;
|
|
110
|
+
resetTime: number;
|
|
111
|
+
}>;
|
|
112
|
+
decrement(key: string): Promise<void>;
|
|
113
|
+
reset(key: string): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
export interface RateLimitOptions {
|
|
116
|
+
windowMs?: number;
|
|
117
|
+
max?: number;
|
|
118
|
+
message?: string | object;
|
|
119
|
+
statusCode?: number;
|
|
120
|
+
headers?: boolean;
|
|
121
|
+
keyGenerator?: (ctx: QHTTPXContext) => string;
|
|
122
|
+
skip?: (ctx: QHTTPXContext) => boolean;
|
|
123
|
+
store?: RateLimitStore;
|
|
124
|
+
trustProxy?: boolean;
|
|
125
|
+
}
|
|
105
126
|
export type QHTTPXOptions = {
|
|
106
127
|
name?: string;
|
|
107
128
|
workers?: 'auto' | number;
|
|
@@ -127,6 +148,22 @@ export type QHTTPXOptions = {
|
|
|
127
148
|
enableRequestFusion?: boolean | RequestFusionOptions;
|
|
128
149
|
viewEngine?: ViewEngine;
|
|
129
150
|
viewsPath?: string;
|
|
151
|
+
rateLimit?: RateLimitOptions;
|
|
152
|
+
cors?: CorsOptions | boolean;
|
|
153
|
+
compression?: CompressionOptions | boolean;
|
|
154
|
+
};
|
|
155
|
+
export type CorsOrigin = string | string[] | ((origin: string | undefined) => string | null | undefined);
|
|
156
|
+
export type CorsOptions = {
|
|
157
|
+
origin?: CorsOrigin;
|
|
158
|
+
methods?: string[];
|
|
159
|
+
allowedHeaders?: string[];
|
|
160
|
+
exposedHeaders?: string[];
|
|
161
|
+
credentials?: boolean;
|
|
162
|
+
maxAgeSeconds?: number;
|
|
163
|
+
};
|
|
164
|
+
export type CompressionOptions = {
|
|
165
|
+
threshold?: number;
|
|
166
|
+
level?: number;
|
|
130
167
|
};
|
|
131
168
|
export type QHTTPXPlugin<Options = any> = (app: any, // We use 'any' here to avoid circular dependency with QHTTPX class, or we could use an interface
|
|
132
169
|
options: Options) => void | Promise<void>;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { QHTTPX } from './core/server';
|
|
2
2
|
import type { QHTTPXOptions } from './core/types';
|
|
3
|
+
import { NativeAdapter } from './core/native-adapter';
|
|
3
4
|
export { QHTTPX } from './core/server';
|
|
4
5
|
export * from './core/types';
|
|
6
|
+
export * from './core/errors';
|
|
5
7
|
export * from './middleware/cors';
|
|
6
|
-
export
|
|
8
|
+
export { createSecurityHeadersMiddleware, type SecurityHeadersOptions, createSecureDefaults, type SecureDefaultsOptions, } from './middleware/security';
|
|
9
|
+
export * from './middleware/rate-limit';
|
|
7
10
|
export * from './middleware/static';
|
|
8
11
|
export * from './middleware/compression';
|
|
9
12
|
export * from './core/stream';
|
|
@@ -28,7 +31,9 @@ export * from './validation/types';
|
|
|
28
31
|
export * from './validation/simple';
|
|
29
32
|
export * from './openapi/generator';
|
|
30
33
|
export * from './client';
|
|
34
|
+
export { NativeAdapter } from './core/native-adapter';
|
|
31
35
|
export declare function createHttpApp(options?: QHTTPXOptions): QHTTPX;
|
|
36
|
+
export declare function createNativeApp(options?: QHTTPXOptions): NativeAdapter;
|
|
32
37
|
/**
|
|
33
38
|
* Singleton instance for quick start
|
|
34
39
|
* @example
|
package/dist/src/index.js
CHANGED
|
@@ -14,15 +14,21 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.app = exports.getStringifier = exports.fastJsonStringify = exports.BufferPool = exports.QHTTPX = void 0;
|
|
17
|
+
exports.app = exports.NativeAdapter = exports.getStringifier = exports.fastJsonStringify = exports.BufferPool = exports.createSecureDefaults = exports.createSecurityHeadersMiddleware = exports.QHTTPX = void 0;
|
|
18
18
|
exports.createHttpApp = createHttpApp;
|
|
19
|
+
exports.createNativeApp = createNativeApp;
|
|
19
20
|
const server_1 = require("./core/server");
|
|
20
21
|
const presets_1 = require("./middleware/presets");
|
|
22
|
+
const native_adapter_1 = require("./core/native-adapter");
|
|
21
23
|
var server_2 = require("./core/server");
|
|
22
24
|
Object.defineProperty(exports, "QHTTPX", { enumerable: true, get: function () { return server_2.QHTTPX; } });
|
|
23
25
|
__exportStar(require("./core/types"), exports);
|
|
26
|
+
__exportStar(require("./core/errors"), exports);
|
|
24
27
|
__exportStar(require("./middleware/cors"), exports);
|
|
25
|
-
|
|
28
|
+
var security_1 = require("./middleware/security");
|
|
29
|
+
Object.defineProperty(exports, "createSecurityHeadersMiddleware", { enumerable: true, get: function () { return security_1.createSecurityHeadersMiddleware; } });
|
|
30
|
+
Object.defineProperty(exports, "createSecureDefaults", { enumerable: true, get: function () { return security_1.createSecureDefaults; } });
|
|
31
|
+
__exportStar(require("./middleware/rate-limit"), exports);
|
|
26
32
|
__exportStar(require("./middleware/static"), exports);
|
|
27
33
|
__exportStar(require("./middleware/compression"), exports);
|
|
28
34
|
__exportStar(require("./core/stream"), exports);
|
|
@@ -50,15 +56,25 @@ __exportStar(require("./validation/types"), exports);
|
|
|
50
56
|
__exportStar(require("./validation/simple"), exports);
|
|
51
57
|
__exportStar(require("./openapi/generator"), exports);
|
|
52
58
|
__exportStar(require("./client"), exports);
|
|
59
|
+
var native_adapter_2 = require("./core/native-adapter");
|
|
60
|
+
Object.defineProperty(exports, "NativeAdapter", { enumerable: true, get: function () { return native_adapter_2.NativeAdapter; } });
|
|
53
61
|
function createHttpApp(options = {}) {
|
|
54
62
|
const app = new server_1.QHTTPX(options);
|
|
55
63
|
// Skip middleware in ultra mode for maximum performance
|
|
56
64
|
if (options.performanceMode !== 'ultra') {
|
|
57
|
-
const middlewares = (0, presets_1.createApiPreset)(
|
|
65
|
+
const middlewares = (0, presets_1.createApiPreset)({
|
|
66
|
+
rateLimit: options.rateLimit,
|
|
67
|
+
cors: options.cors,
|
|
68
|
+
compression: options.compression,
|
|
69
|
+
});
|
|
58
70
|
middlewares.forEach((mw) => app.use(mw));
|
|
59
71
|
}
|
|
60
72
|
return app;
|
|
61
73
|
}
|
|
74
|
+
function createNativeApp(options = {}) {
|
|
75
|
+
const app = createHttpApp(options);
|
|
76
|
+
return new native_adapter_1.NativeAdapter(app);
|
|
77
|
+
}
|
|
62
78
|
/**
|
|
63
79
|
* Singleton instance for quick start
|
|
64
80
|
* @example
|
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
import { QHTTPXMiddleware } from '../core/types';
|
|
2
|
-
export type CompressionOptions = {
|
|
3
|
-
threshold?: number;
|
|
4
|
-
level?: number;
|
|
5
|
-
};
|
|
1
|
+
import { QHTTPXMiddleware, CompressionOptions } from '../core/types';
|
|
6
2
|
export declare function createCompressionMiddleware(options?: CompressionOptions): QHTTPXMiddleware;
|
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
import { QHTTPXMiddleware } from '../core/types';
|
|
2
|
-
export type CorsOrigin = string | string[] | ((origin: string | undefined) => string | null | undefined);
|
|
3
|
-
export type CorsOptions = {
|
|
4
|
-
origin?: CorsOrigin;
|
|
5
|
-
methods?: string[];
|
|
6
|
-
allowedHeaders?: string[];
|
|
7
|
-
exposedHeaders?: string[];
|
|
8
|
-
credentials?: boolean;
|
|
9
|
-
maxAgeSeconds?: number;
|
|
10
|
-
};
|
|
1
|
+
import { QHTTPXMiddleware, CorsOptions } from '../core/types';
|
|
11
2
|
export declare function createCorsMiddleware(options?: CorsOptions): QHTTPXMiddleware;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { QHTTPXMiddleware } from '../core/types';
|
|
1
|
+
import { QHTTPXMiddleware, RateLimitOptions, CorsOptions, CompressionOptions } from '../core/types';
|
|
2
2
|
import { SecureDefaultsOptions } from './security';
|
|
3
3
|
import { LoggerOptions } from '../utils/logger';
|
|
4
4
|
import { StaticOptions } from './static';
|
|
5
5
|
export type ApiPresetOptions = {
|
|
6
6
|
security?: SecureDefaultsOptions;
|
|
7
7
|
logging?: LoggerOptions | boolean;
|
|
8
|
+
rateLimit?: RateLimitOptions;
|
|
9
|
+
cors?: CorsOptions | boolean;
|
|
10
|
+
compression?: CompressionOptions | boolean;
|
|
8
11
|
};
|
|
9
12
|
export declare function createApiPreset(options?: ApiPresetOptions): QHTTPXMiddleware[];
|
|
10
13
|
export type StaticAppPresetOptions = ApiPresetOptions & {
|
|
@@ -3,17 +3,36 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createApiPreset = createApiPreset;
|
|
4
4
|
exports.createStaticAppPreset = createStaticAppPreset;
|
|
5
5
|
const security_1 = require("./security");
|
|
6
|
+
const cors_1 = require("./cors");
|
|
7
|
+
const compression_1 = require("./compression");
|
|
6
8
|
const logger_1 = require("../utils/logger");
|
|
7
9
|
const static_1 = require("./static");
|
|
10
|
+
const rate_limit_1 = require("./rate-limit");
|
|
8
11
|
function createApiPreset(options = {}) {
|
|
9
12
|
const middlewares = [];
|
|
10
|
-
// 1.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
// 1. CORS
|
|
14
|
+
// Default to true (enabled) if not specified, to match createSecureDefaults behavior
|
|
15
|
+
const corsOpts = options.cors ?? options.security?.cors ?? true;
|
|
16
|
+
if (corsOpts !== false) {
|
|
17
|
+
const opts = corsOpts === true ? {} : corsOpts;
|
|
18
|
+
middlewares.push((0, cors_1.createCorsMiddleware)(opts));
|
|
19
|
+
}
|
|
20
|
+
// 2. Security Headers
|
|
21
|
+
middlewares.push((0, security_1.createSecurityHeadersMiddleware)(options.security?.securityHeaders));
|
|
22
|
+
// 3. Compression
|
|
23
|
+
if (options.compression) {
|
|
24
|
+
const opts = options.compression === true ? {} : options.compression;
|
|
25
|
+
middlewares.push((0, compression_1.createCompressionMiddleware)(opts));
|
|
26
|
+
}
|
|
27
|
+
// 4. Logging
|
|
13
28
|
if (options.logging !== false) {
|
|
14
29
|
const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
|
|
15
30
|
middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
|
|
16
31
|
}
|
|
32
|
+
// 5. Rate Limit
|
|
33
|
+
if (options.rateLimit) {
|
|
34
|
+
middlewares.push((0, rate_limit_1.rateLimit)(options.rateLimit));
|
|
35
|
+
}
|
|
17
36
|
return middlewares;
|
|
18
37
|
}
|
|
19
38
|
function createStaticAppPreset(options) {
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export interface RateLimitStore {
|
|
3
|
-
increment(key: string, windowMs: number): Promise<{
|
|
4
|
-
total: number;
|
|
5
|
-
resetTime: number;
|
|
6
|
-
}>;
|
|
7
|
-
decrement(key: string): Promise<void>;
|
|
8
|
-
reset(key: string): Promise<void>;
|
|
9
|
-
}
|
|
1
|
+
import type { QHTTPXMiddleware, RateLimitOptions, RateLimitStore } from '../core/types';
|
|
10
2
|
export declare class MemoryStore implements RateLimitStore {
|
|
11
3
|
private hits;
|
|
12
4
|
private interval?;
|
|
@@ -19,14 +11,4 @@ export declare class MemoryStore implements RateLimitStore {
|
|
|
19
11
|
reset(key: string): Promise<void>;
|
|
20
12
|
private cleanup;
|
|
21
13
|
}
|
|
22
|
-
export interface RateLimitOptions {
|
|
23
|
-
windowMs?: number;
|
|
24
|
-
max?: number;
|
|
25
|
-
message?: string | object;
|
|
26
|
-
statusCode?: number;
|
|
27
|
-
headers?: boolean;
|
|
28
|
-
keyGenerator?: (ctx: QHTTPXContext) => string;
|
|
29
|
-
skip?: (ctx: QHTTPXContext) => boolean;
|
|
30
|
-
store?: RateLimitStore;
|
|
31
|
-
}
|
|
32
14
|
export declare const rateLimit: (options?: RateLimitOptions) => QHTTPXMiddleware;
|
|
@@ -49,6 +49,12 @@ const rateLimit = (options = {}) => {
|
|
|
49
49
|
const headers = options.headers ?? true;
|
|
50
50
|
const store = options.store ?? new MemoryStore();
|
|
51
51
|
const keyGenerator = options.keyGenerator ?? ((ctx) => {
|
|
52
|
+
if (options.trustProxy) {
|
|
53
|
+
const forwarded = ctx.req.headers['x-forwarded-for'];
|
|
54
|
+
if (forwarded) {
|
|
55
|
+
return Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0].trim();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
52
58
|
return ctx.req.socket.remoteAddress || 'unknown';
|
|
53
59
|
});
|
|
54
60
|
return async (ctx, next) => {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { QHTTPXContext, QHTTPXMiddleware } from '../core/types';
|
|
2
|
-
import { CorsOptions } from './cors';
|
|
1
|
+
import { QHTTPXContext, QHTTPXMiddleware, CorsOptions } from '../core/types';
|
|
3
2
|
export type SecurityHeadersOptions = {
|
|
4
3
|
contentSecurityPolicy?: string | null;
|
|
5
4
|
referrerPolicy?: string | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface NativeServerBinding {
|
|
2
|
+
parse(buffer: Buffer): {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
version: number;
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
bodyOffset: number;
|
|
8
|
+
} | null;
|
|
9
|
+
createResponse(statusCode: number, headers: Record<string, string>, body?: string | Buffer): Buffer;
|
|
10
|
+
createJSONResponse(obj: unknown): Buffer;
|
|
11
|
+
writeResponse(fd: number, chunks: (Buffer | string)[]): void;
|
|
12
|
+
setCPUAffinity(cpuId: number): boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class NativeServer {
|
|
15
|
+
private binding;
|
|
16
|
+
constructor();
|
|
17
|
+
get isAvailable(): boolean;
|
|
18
|
+
parse(buffer: Buffer): {
|
|
19
|
+
method: string;
|
|
20
|
+
path: string;
|
|
21
|
+
version: number;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
bodyOffset: number;
|
|
24
|
+
} | null;
|
|
25
|
+
createResponse(statusCode: number, headers: Record<string, string>, body?: string | Buffer): Buffer<ArrayBufferLike>;
|
|
26
|
+
createJSONResponse(obj: unknown): Buffer<ArrayBufferLike>;
|
|
27
|
+
writeResponse(fd: number, chunks: (Buffer | string)[]): void;
|
|
28
|
+
setCPUAffinity(cpuId: number): boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NativeServer = void 0;
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
let nativeBinding = null;
|
|
6
|
+
// Use try/catch with module.createRequire if import.meta is not available in some build targets
|
|
7
|
+
// However, since we are in a TS module that might be commonjs or esm
|
|
8
|
+
// We'll use a safer approach for require
|
|
9
|
+
try {
|
|
10
|
+
const req = require;
|
|
11
|
+
try {
|
|
12
|
+
nativeBinding = req('../../build/Release/qhttpx_native.node');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
try {
|
|
16
|
+
nativeBinding = req('../../build/Debug/qhttpx_native.node');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Ignored
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Ignored
|
|
25
|
+
}
|
|
26
|
+
class NativeServer {
|
|
27
|
+
constructor() {
|
|
28
|
+
if (nativeBinding && nativeBinding.NativeServer) {
|
|
29
|
+
this.binding = new nativeBinding.NativeServer();
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.binding = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
get isAvailable() {
|
|
36
|
+
return this.binding !== null;
|
|
37
|
+
}
|
|
38
|
+
parse(buffer) {
|
|
39
|
+
if (!this.binding)
|
|
40
|
+
throw new Error('Native bindings not available');
|
|
41
|
+
return this.binding.parse(buffer);
|
|
42
|
+
}
|
|
43
|
+
createResponse(statusCode, headers, body) {
|
|
44
|
+
if (!this.binding)
|
|
45
|
+
throw new Error('Native bindings not available');
|
|
46
|
+
return this.binding.createResponse(statusCode, headers, body);
|
|
47
|
+
}
|
|
48
|
+
createJSONResponse(obj) {
|
|
49
|
+
if (!this.binding)
|
|
50
|
+
throw new Error('Native bindings not available');
|
|
51
|
+
return this.binding.createJSONResponse(obj);
|
|
52
|
+
}
|
|
53
|
+
writeResponse(fd, chunks) {
|
|
54
|
+
if (!this.binding)
|
|
55
|
+
throw new Error('Native bindings not available');
|
|
56
|
+
return this.binding.writeResponse(fd, chunks);
|
|
57
|
+
}
|
|
58
|
+
setCPUAffinity(cpuId) {
|
|
59
|
+
if (!this.binding)
|
|
60
|
+
throw new Error('Native bindings not available');
|
|
61
|
+
return this.binding.setCPUAffinity(cpuId);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.NativeServer = NativeServer;
|
|
@@ -12,5 +12,7 @@ export declare class RadixTree {
|
|
|
12
12
|
private root;
|
|
13
13
|
insert(segments: string[], handler: QHTTPXHandler, priority: RoutePriority): void;
|
|
14
14
|
lookup(segments: string[]): MatchResult | null;
|
|
15
|
+
lookupPath(path: string): MatchResult | null;
|
|
16
|
+
private findPath;
|
|
15
17
|
private find;
|
|
16
18
|
}
|
|
@@ -10,6 +10,8 @@ class Node {
|
|
|
10
10
|
this.paramName = null;
|
|
11
11
|
// Data if this node is a route end
|
|
12
12
|
this.data = null;
|
|
13
|
+
// Optimization: Pre-allocated match result for static matches
|
|
14
|
+
this.staticMatch = null;
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
class RadixTree {
|
|
@@ -38,14 +40,62 @@ class RadixTree {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
node.data = { handler, priority };
|
|
43
|
+
node.staticMatch = { handler, priority, params: {} };
|
|
41
44
|
}
|
|
42
45
|
lookup(segments) {
|
|
43
|
-
// Use a stack-based approach or recursion.
|
|
44
|
-
// For simplicity and correctness with backtracking, we'll use recursion.
|
|
45
|
-
// To minimize allocations, we pass the same params object and only copy on success?
|
|
46
|
-
// Actually, creating a params object is inevitable for the result.
|
|
47
46
|
return this.find(this.root, segments, 0);
|
|
48
47
|
}
|
|
48
|
+
lookupPath(path) {
|
|
49
|
+
if (path === '/' || path === '') {
|
|
50
|
+
if (this.root.staticMatch) {
|
|
51
|
+
return this.root.staticMatch;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const start = path.charCodeAt(0) === 47 ? 1 : 0;
|
|
56
|
+
return this.findPath(this.root, path, start);
|
|
57
|
+
}
|
|
58
|
+
findPath(node, path, start) {
|
|
59
|
+
// Handle trailing slash or end of path
|
|
60
|
+
if (start >= path.length) {
|
|
61
|
+
if (node.staticMatch) {
|
|
62
|
+
return node.staticMatch;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
let end = path.indexOf('/', start);
|
|
67
|
+
if (end === -1) {
|
|
68
|
+
end = path.length;
|
|
69
|
+
}
|
|
70
|
+
const segment = path.substring(start, end);
|
|
71
|
+
const nextStart = end + 1;
|
|
72
|
+
// Skip empty segments (e.g. //) to match normalize behavior
|
|
73
|
+
if (segment.length === 0) {
|
|
74
|
+
return this.findPath(node, path, nextStart);
|
|
75
|
+
}
|
|
76
|
+
// 1. Try exact match
|
|
77
|
+
const child = node.children.get(segment);
|
|
78
|
+
if (child) {
|
|
79
|
+
const result = this.findPath(child, path, nextStart);
|
|
80
|
+
if (result)
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
// 2. Try param match
|
|
84
|
+
if (node.paramChild) {
|
|
85
|
+
const result = this.findPath(node.paramChild, path, nextStart);
|
|
86
|
+
if (result) {
|
|
87
|
+
// Clone params to avoid modifying shared staticMatch
|
|
88
|
+
const newParams = { ...result.params };
|
|
89
|
+
newParams[node.paramChild.paramName] = segment;
|
|
90
|
+
return {
|
|
91
|
+
handler: result.handler,
|
|
92
|
+
priority: result.priority,
|
|
93
|
+
params: newParams
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
49
99
|
find(node, segments, index) {
|
|
50
100
|
if (index === segments.length) {
|
|
51
101
|
if (node.data) {
|
|
@@ -15,6 +15,7 @@ export type RouteMatch = {
|
|
|
15
15
|
export declare class Router {
|
|
16
16
|
private readonly methodBuckets;
|
|
17
17
|
private readonly radixTrees;
|
|
18
|
+
private readonly staticRoutes;
|
|
18
19
|
private isFrozen;
|
|
19
20
|
register(method: HTTPMethod, path: string, handler: QHTTPXHandler, options?: RouteOptions & {
|
|
20
21
|
schema?: RouteSchema | Record<string, unknown>;
|
|
@@ -17,6 +17,16 @@ class Router {
|
|
|
17
17
|
]);
|
|
18
18
|
// Derived structures (built at freeze time)
|
|
19
19
|
this.radixTrees = new Map();
|
|
20
|
+
// Fast lookup for static routes (no params)
|
|
21
|
+
this.staticRoutes = new Map([
|
|
22
|
+
['GET', new Map()],
|
|
23
|
+
['POST', new Map()],
|
|
24
|
+
['PUT', new Map()],
|
|
25
|
+
['DELETE', new Map()],
|
|
26
|
+
['PATCH', new Map()],
|
|
27
|
+
['HEAD', new Map()],
|
|
28
|
+
['OPTIONS', new Map()],
|
|
29
|
+
]);
|
|
20
30
|
// Freeze state
|
|
21
31
|
this.isFrozen = false;
|
|
22
32
|
}
|
|
@@ -42,10 +52,18 @@ class Router {
|
|
|
42
52
|
match(method, path) {
|
|
43
53
|
// Fast path for frozen router
|
|
44
54
|
if (this.isFrozen) {
|
|
55
|
+
// 1. Try static match (O(1))
|
|
56
|
+
const staticMap = this.staticRoutes.get(method);
|
|
57
|
+
if (staticMap) {
|
|
58
|
+
const match = staticMap.get(path);
|
|
59
|
+
if (match) {
|
|
60
|
+
return match;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// 2. Try radix tree match
|
|
45
64
|
const tree = this.radixTrees.get(method);
|
|
46
65
|
if (tree) {
|
|
47
|
-
const
|
|
48
|
-
const match = tree.lookup(segments);
|
|
66
|
+
const match = tree.lookupPath(path);
|
|
49
67
|
if (match) {
|
|
50
68
|
return match;
|
|
51
69
|
}
|
|
@@ -127,8 +145,30 @@ class Router {
|
|
|
127
145
|
// Build derived structures for faster matching
|
|
128
146
|
for (const [method, routes] of this.methodBuckets.entries()) {
|
|
129
147
|
const tree = new radix_tree_1.RadixTree();
|
|
148
|
+
const staticMap = this.staticRoutes.get(method);
|
|
130
149
|
for (const route of routes) {
|
|
131
150
|
tree.insert(route.segments, route.handler, route.priority);
|
|
151
|
+
// Check if route is static (no params)
|
|
152
|
+
let isStatic = true;
|
|
153
|
+
for (const segment of route.segments) {
|
|
154
|
+
if (segment.startsWith(':')) {
|
|
155
|
+
isStatic = false;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (isStatic) {
|
|
160
|
+
// Optimization: Pre-allocate static match result
|
|
161
|
+
// Note: we use route.path directly as key
|
|
162
|
+
staticMap.set(route.path, {
|
|
163
|
+
handler: route.handler,
|
|
164
|
+
params: {},
|
|
165
|
+
priority: route.priority,
|
|
166
|
+
});
|
|
167
|
+
// Also handle trailing slash or no trailing slash?
|
|
168
|
+
// For strict matching, we use exact path.
|
|
169
|
+
// QHTTPX seems to normalize, so /json/ and /json might be different?
|
|
170
|
+
// route.path comes from user.
|
|
171
|
+
}
|
|
132
172
|
}
|
|
133
173
|
this.radixTrees.set(method, tree);
|
|
134
174
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const native_adapter_1 = require("../src/core/native-adapter");
|
|
8
|
+
const index_1 = require("../src/index");
|
|
9
|
+
const net_1 = __importDefault(require("net"));
|
|
10
|
+
// Mock the NativeServer module
|
|
11
|
+
vitest_1.vi.mock('../src/native', () => {
|
|
12
|
+
return {
|
|
13
|
+
NativeServer: class MockNativeServer {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.isAvailable = true;
|
|
16
|
+
}
|
|
17
|
+
parse(buffer) {
|
|
18
|
+
const str = buffer.toString();
|
|
19
|
+
if (str.includes('GET / HTTP/1.1')) {
|
|
20
|
+
return {
|
|
21
|
+
method: 'GET',
|
|
22
|
+
path: '/',
|
|
23
|
+
version: 1,
|
|
24
|
+
headers: { host: 'localhost' },
|
|
25
|
+
bodyOffset: str.indexOf('\r\n\r\n') + 4
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
createResponse(statusCode, headers, body) {
|
|
32
|
+
return Buffer.from(`HTTP/1.1 ${statusCode} OK\r\n\r\n${body || ''}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.describe)('NativeAdapter', () => {
|
|
38
|
+
let app;
|
|
39
|
+
let adapter;
|
|
40
|
+
let server;
|
|
41
|
+
let port = 0;
|
|
42
|
+
(0, vitest_1.beforeEach)(() => {
|
|
43
|
+
app = (0, index_1.createHttpApp)();
|
|
44
|
+
adapter = new native_adapter_1.NativeAdapter(app);
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.afterEach)(async () => {
|
|
47
|
+
if (server) {
|
|
48
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)('should handle request using native parser mock', async () => {
|
|
52
|
+
app.get('/', ({ res }) => { res.end('Native Works'); });
|
|
53
|
+
await new Promise((resolve) => {
|
|
54
|
+
server = adapter.listen(0, () => {
|
|
55
|
+
port = server.address().port;
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
const socket = net_1.default.createConnection(port);
|
|
60
|
+
const responsePromise = new Promise((resolve) => {
|
|
61
|
+
socket.on('data', (data) => {
|
|
62
|
+
resolve(data.toString());
|
|
63
|
+
socket.end();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
socket.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n');
|
|
67
|
+
const response = await responsePromise;
|
|
68
|
+
(0, vitest_1.expect)(response).toContain('HTTP/1.1 200 OK');
|
|
69
|
+
(0, vitest_1.expect)(response).toContain('Native Works');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -32,6 +32,9 @@ const index_1 = require("../src/index");
|
|
|
32
32
|
maxMemoryBytes: 1000000,
|
|
33
33
|
});
|
|
34
34
|
const { port } = await app.listen(0, '127.0.0.1');
|
|
35
|
+
// Force memory check by manipulating requestCounter
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
app.requestCounter = 99;
|
|
35
38
|
app.get('/', (ctx) => {
|
|
36
39
|
ctx.send('ok');
|
|
37
40
|
});
|
|
@@ -67,8 +67,8 @@ const src_1 = require("../src");
|
|
|
67
67
|
});
|
|
68
68
|
(0, vitest_1.it)('applies in-memory rate limiting per remote address', async () => {
|
|
69
69
|
app = new src_1.QHTTPX();
|
|
70
|
-
app.use((0, src_1.
|
|
71
|
-
|
|
70
|
+
app.use((0, src_1.rateLimit)({
|
|
71
|
+
max: 1,
|
|
72
72
|
windowMs: 1000,
|
|
73
73
|
}));
|
|
74
74
|
app.get('/ping', (ctx) => {
|