qhttpx 1.9.2 → 1.9.3
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 +79 -17
- package/dist/examples/api-server.d.ts +1 -0
- package/dist/examples/api-server.js +77 -0
- 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 +112 -0
- package/dist/src/benchmarks/compare-frameworks.js +119 -0
- package/dist/src/benchmarks/compare.d.ts +1 -0
- package/dist/src/benchmarks/compare.js +288 -0
- package/dist/src/benchmarks/quantam-users.d.ts +1 -0
- package/dist/src/benchmarks/quantam-users.js +56 -0
- package/dist/src/benchmarks/simple-json.d.ts +1 -0
- package/dist/src/benchmarks/simple-json.js +60 -0
- package/dist/src/benchmarks/ultra-mode.d.ts +1 -0
- package/dist/src/benchmarks/ultra-mode.js +94 -0
- package/dist/src/buffer-pool.js +70 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +222 -0
- package/dist/src/client/index.d.ts +17 -0
- package/dist/src/client/index.js +72 -0
- package/dist/src/config.js +50 -0
- package/dist/src/cookies.js +59 -0
- package/dist/src/core/batch.d.ts +24 -0
- package/dist/src/core/batch.js +97 -0
- package/dist/src/core/body-parser.d.ts +15 -0
- package/dist/src/core/body-parser.js +121 -0
- package/dist/src/core/buffer-pool.d.ts +41 -0
- package/dist/src/core/buffer-pool.js +70 -0
- package/dist/src/core/config.d.ts +7 -0
- package/dist/src/core/config.js +50 -0
- package/dist/src/core/errors.d.ts +34 -0
- package/dist/src/core/errors.js +70 -0
- package/dist/src/core/fusion.d.ts +20 -0
- package/dist/src/core/fusion.js +193 -0
- package/dist/src/core/logger.d.ts +22 -0
- package/dist/src/core/logger.js +49 -0
- package/dist/src/core/metrics.d.ts +48 -0
- package/dist/src/core/metrics.js +117 -0
- package/dist/src/core/native-adapter.d.ts +11 -0
- package/dist/src/core/native-adapter.js +211 -0
- package/dist/src/core/resources.d.ts +9 -0
- package/dist/src/core/resources.js +25 -0
- package/dist/src/core/scheduler.d.ts +34 -0
- package/dist/src/core/scheduler.js +85 -0
- package/dist/src/core/scope.d.ts +26 -0
- package/dist/src/core/scope.js +68 -0
- package/dist/src/core/serializer.d.ts +10 -0
- package/dist/src/core/serializer.js +44 -0
- package/dist/src/core/server.d.ts +138 -0
- package/dist/src/core/server.js +1082 -0
- package/dist/src/core/stream.d.ts +15 -0
- package/dist/src/core/stream.js +71 -0
- package/dist/src/core/tasks.d.ts +29 -0
- package/dist/src/core/tasks.js +87 -0
- package/dist/src/core/types.d.ts +173 -0
- package/dist/src/core/types.js +19 -0
- package/dist/src/core/websocket.d.ts +25 -0
- package/dist/src/core/websocket.js +86 -0
- package/dist/src/core/worker-queue.d.ts +41 -0
- package/dist/src/core/worker-queue.js +73 -0
- package/dist/src/cors.js +66 -0
- package/dist/src/database/adapters/memory.d.ts +21 -0
- package/dist/src/database/adapters/memory.js +90 -0
- package/dist/src/database/adapters/mongo.d.ts +11 -0
- package/dist/src/database/adapters/mongo.js +141 -0
- package/dist/src/database/adapters/postgres.d.ts +10 -0
- package/dist/src/database/adapters/postgres.js +111 -0
- package/dist/src/database/adapters/sqlite.d.ts +10 -0
- package/dist/src/database/adapters/sqlite.js +42 -0
- package/dist/src/database/coalescer.d.ts +14 -0
- package/dist/src/database/coalescer.js +134 -0
- package/dist/src/database/manager.d.ts +35 -0
- package/dist/src/database/manager.js +87 -0
- package/dist/src/database/types.d.ts +20 -0
- package/dist/src/database/types.js +2 -0
- package/dist/src/index.d.ts +50 -0
- package/dist/src/index.js +91 -0
- package/dist/src/logger.js +45 -0
- package/dist/src/metrics.js +111 -0
- package/dist/src/middleware/compression.d.ts +2 -0
- package/dist/src/middleware/compression.js +133 -0
- package/dist/src/middleware/cors.d.ts +2 -0
- package/dist/src/middleware/cors.js +66 -0
- package/dist/src/middleware/presets.d.ts +16 -0
- package/dist/src/middleware/presets.js +52 -0
- package/dist/src/middleware/rate-limit.d.ts +14 -0
- package/dist/src/middleware/rate-limit.js +83 -0
- package/dist/src/middleware/security.d.ts +21 -0
- package/dist/src/middleware/security.js +69 -0
- package/dist/src/middleware/static.d.ts +11 -0
- package/dist/src/middleware/static.js +191 -0
- package/dist/src/native/index.d.ts +32 -0
- package/dist/src/native/index.js +141 -0
- package/dist/src/openapi/generator.d.ts +19 -0
- package/dist/src/openapi/generator.js +149 -0
- package/dist/src/presets.js +33 -0
- package/dist/src/radix-router.js +89 -0
- package/dist/src/radix-tree.js +81 -0
- package/dist/src/resources.js +25 -0
- package/dist/src/router/radix-router.d.ts +18 -0
- package/dist/src/router/radix-router.js +89 -0
- package/dist/src/router/radix-tree.d.ts +18 -0
- package/dist/src/router/radix-tree.js +131 -0
- package/dist/src/router/router.d.ts +34 -0
- package/dist/src/router/router.js +186 -0
- package/dist/src/router.js +138 -0
- package/dist/src/scheduler.js +85 -0
- package/dist/src/security.js +69 -0
- package/dist/src/server.js +685 -0
- package/dist/src/signals.js +31 -0
- package/dist/src/static.js +107 -0
- package/dist/src/stream.js +71 -0
- package/dist/src/tasks.js +87 -0
- package/dist/src/testing/index.d.ts +25 -0
- package/dist/src/testing/index.js +84 -0
- package/dist/src/testing.js +40 -0
- package/dist/src/types.js +19 -0
- package/dist/src/utils/cookies.d.ts +3 -0
- package/dist/src/utils/cookies.js +59 -0
- package/dist/src/utils/logger.d.ts +12 -0
- package/dist/src/utils/logger.js +45 -0
- package/dist/src/utils/signals.d.ts +6 -0
- package/dist/src/utils/signals.js +31 -0
- package/dist/src/utils/sse.d.ts +6 -0
- package/dist/src/utils/sse.js +32 -0
- package/dist/src/utils/testing.js +40 -0
- package/dist/src/validation/index.d.ts +3 -0
- package/dist/src/validation/index.js +19 -0
- package/dist/src/validation/simple.d.ts +5 -0
- package/dist/src/validation/simple.js +102 -0
- package/dist/src/validation/types.d.ts +32 -0
- package/dist/src/validation/types.js +12 -0
- package/dist/src/validation/zod.d.ts +4 -0
- package/dist/src/validation/zod.js +18 -0
- package/dist/src/views/index.d.ts +1 -0
- package/dist/src/views/index.js +17 -0
- package/dist/src/views/types.d.ts +3 -0
- package/dist/src/views/types.js +2 -0
- package/dist/src/worker-queue.js +73 -0
- package/dist/tests/adapters.test.d.ts +1 -0
- package/dist/tests/adapters.test.js +106 -0
- package/dist/tests/batch.test.d.ts +1 -0
- package/dist/tests/batch.test.js +117 -0
- package/dist/tests/body-parser.test.d.ts +1 -0
- package/dist/tests/body-parser.test.js +52 -0
- package/dist/tests/compression-sse.test.d.ts +1 -0
- package/dist/tests/compression-sse.test.js +87 -0
- package/dist/tests/cookies.test.d.ts +1 -0
- package/dist/tests/cookies.test.js +63 -0
- package/dist/tests/cors.test.d.ts +1 -0
- package/dist/tests/cors.test.js +55 -0
- package/dist/tests/database.test.d.ts +1 -0
- package/dist/tests/database.test.js +80 -0
- package/dist/tests/dx.test.d.ts +1 -0
- package/dist/tests/dx.test.js +114 -0
- package/dist/tests/ecosystem.test.d.ts +1 -0
- package/dist/tests/ecosystem.test.js +133 -0
- package/dist/tests/features.test.d.ts +1 -0
- package/dist/tests/features.test.js +47 -0
- package/dist/tests/fusion.test.d.ts +1 -0
- package/dist/tests/fusion.test.js +92 -0
- package/dist/tests/http-basic.test.d.ts +1 -0
- package/dist/tests/http-basic.test.js +124 -0
- package/dist/tests/logger.test.d.ts +1 -0
- package/dist/tests/logger.test.js +33 -0
- package/dist/tests/middleware.test.d.ts +1 -0
- package/dist/tests/middleware.test.js +109 -0
- package/dist/tests/native-adapter.test.d.ts +1 -0
- package/dist/tests/native-adapter.test.js +71 -0
- package/dist/tests/observability.test.d.ts +1 -0
- package/dist/tests/observability.test.js +59 -0
- package/dist/tests/openapi.test.d.ts +1 -0
- package/dist/tests/openapi.test.js +64 -0
- package/dist/tests/plugin.test.d.ts +1 -0
- package/dist/tests/plugin.test.js +65 -0
- package/dist/tests/plugins.test.d.ts +1 -0
- package/dist/tests/plugins.test.js +71 -0
- package/dist/tests/rate-limit.test.d.ts +1 -0
- package/dist/tests/rate-limit.test.js +77 -0
- package/dist/tests/resources.test.d.ts +1 -0
- package/dist/tests/resources.test.js +47 -0
- package/dist/tests/scheduler.test.d.ts +1 -0
- package/dist/tests/scheduler.test.js +46 -0
- package/dist/tests/schema-routes.test.d.ts +1 -0
- package/dist/tests/schema-routes.test.js +77 -0
- package/dist/tests/security.test.d.ts +1 -0
- package/dist/tests/security.test.js +83 -0
- package/dist/tests/server-db.test.d.ts +1 -0
- package/dist/tests/server-db.test.js +72 -0
- package/dist/tests/smoke.test.d.ts +1 -0
- package/dist/tests/smoke.test.js +10 -0
- package/dist/tests/sqlite-fusion.test.d.ts +1 -0
- package/dist/tests/sqlite-fusion.test.js +92 -0
- package/dist/tests/static.test.d.ts +1 -0
- package/dist/tests/static.test.js +102 -0
- package/dist/tests/stream.test.d.ts +1 -0
- package/dist/tests/stream.test.js +44 -0
- package/dist/tests/task-metrics.test.d.ts +1 -0
- package/dist/tests/task-metrics.test.js +53 -0
- package/dist/tests/tasks.test.d.ts +1 -0
- package/dist/tests/tasks.test.js +62 -0
- package/dist/tests/testing.test.d.ts +1 -0
- package/dist/tests/testing.test.js +47 -0
- package/dist/tests/validation.test.d.ts +1 -0
- package/dist/tests/validation.test.js +107 -0
- package/dist/tests/websocket.test.d.ts +1 -0
- package/dist/tests/websocket.test.js +146 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +9 -0
- package/docs/FUSION.md +19 -0
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/qhttpx.node +0 -0
- package/prebuilds/linux-x64/qhttpx.node +0 -0
- package/prebuilds/win32-x64/qhttpx.node +0 -0
- package/scripts/install-native.js +26 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenAPIGenerator = void 0;
|
|
4
|
+
class OpenAPIGenerator {
|
|
5
|
+
constructor(router, options) {
|
|
6
|
+
this.router = router;
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
generate() {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
const paths = {};
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
const schemas = {};
|
|
14
|
+
const methodMap = this.router.getRoutes();
|
|
15
|
+
for (const [method, routes] of methodMap.entries()) {
|
|
16
|
+
for (const route of routes) {
|
|
17
|
+
// Convert /users/:id to /users/{id}
|
|
18
|
+
const pathKey = route.path.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
|
|
19
|
+
if (!paths[pathKey]) {
|
|
20
|
+
paths[pathKey] = {};
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
const operation = {
|
|
24
|
+
responses: {}
|
|
25
|
+
};
|
|
26
|
+
const schema = route.schema;
|
|
27
|
+
if (schema) {
|
|
28
|
+
// Parameters (Query & Path)
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
const parameters = [];
|
|
31
|
+
// Path Params
|
|
32
|
+
// We can infer path params from the URL segments
|
|
33
|
+
const pathParams = route.path.match(/:([a-zA-Z0-9_]+)/g);
|
|
34
|
+
if (pathParams) {
|
|
35
|
+
pathParams.forEach(p => {
|
|
36
|
+
const name = p.substring(1);
|
|
37
|
+
// Check if we have specific schema for this param
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const paramSchema = schema.params?.properties?.[name];
|
|
40
|
+
parameters.push({
|
|
41
|
+
name,
|
|
42
|
+
in: 'path',
|
|
43
|
+
required: true,
|
|
44
|
+
schema: paramSchema ? this.convertSchema(paramSchema) : { type: 'string' }
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// Query Params
|
|
49
|
+
if (schema.query) {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
const queryProps = schema.query.properties || {};
|
|
52
|
+
for (const [key, prop] of Object.entries(queryProps)) {
|
|
53
|
+
parameters.push({
|
|
54
|
+
name: key,
|
|
55
|
+
in: 'query',
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
required: schema.query.required !== false && prop.required !== false, // simplified
|
|
58
|
+
schema: this.convertSchema(prop)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (parameters.length > 0) {
|
|
63
|
+
operation.parameters = parameters;
|
|
64
|
+
}
|
|
65
|
+
// Request Body
|
|
66
|
+
if (schema.body) {
|
|
67
|
+
operation.requestBody = {
|
|
68
|
+
content: {
|
|
69
|
+
'application/json': {
|
|
70
|
+
schema: this.convertSchema(schema.body)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Responses
|
|
76
|
+
// For now, default to 200 OK
|
|
77
|
+
operation.responses['200'] = {
|
|
78
|
+
description: 'Successful response',
|
|
79
|
+
content: {
|
|
80
|
+
'application/json': {
|
|
81
|
+
schema: schema.response ? this.convertSchema(schema.response) : {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
// 400 Bad Request if validation exists
|
|
86
|
+
if (schema.body || schema.query || schema.params) {
|
|
87
|
+
operation.responses['400'] = {
|
|
88
|
+
description: 'Validation Error'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// No schema, generic response
|
|
94
|
+
operation.responses['200'] = {
|
|
95
|
+
description: 'Successful response'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
paths[pathKey][method.toLowerCase()] = operation;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
openapi: '3.0.0',
|
|
103
|
+
info: this.options.info,
|
|
104
|
+
servers: this.options.servers,
|
|
105
|
+
paths,
|
|
106
|
+
components: {
|
|
107
|
+
schemas
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
+
convertSchema(schema) {
|
|
113
|
+
if (!schema)
|
|
114
|
+
return {};
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
const res = { type: schema.type };
|
|
117
|
+
if (schema.properties) {
|
|
118
|
+
res.properties = {};
|
|
119
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
120
|
+
res.properties[key] = this.convertSchema(prop);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (schema.items) {
|
|
124
|
+
res.items = this.convertSchema(schema.items);
|
|
125
|
+
}
|
|
126
|
+
if (schema.min !== undefined) {
|
|
127
|
+
if (schema.type === 'string')
|
|
128
|
+
res.minLength = schema.min;
|
|
129
|
+
if (schema.type === 'number')
|
|
130
|
+
res.minimum = schema.min;
|
|
131
|
+
if (schema.type === 'array')
|
|
132
|
+
res.minItems = schema.min;
|
|
133
|
+
}
|
|
134
|
+
if (schema.max !== undefined) {
|
|
135
|
+
if (schema.type === 'string')
|
|
136
|
+
res.maxLength = schema.max;
|
|
137
|
+
if (schema.type === 'number')
|
|
138
|
+
res.maximum = schema.max;
|
|
139
|
+
if (schema.type === 'array')
|
|
140
|
+
res.maxItems = schema.max;
|
|
141
|
+
}
|
|
142
|
+
if (schema.pattern)
|
|
143
|
+
res.pattern = schema.pattern;
|
|
144
|
+
if (schema.enum)
|
|
145
|
+
res.enum = schema.enum;
|
|
146
|
+
return res;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.OpenAPIGenerator = OpenAPIGenerator;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createApiPreset = createApiPreset;
|
|
4
|
+
exports.createStaticAppPreset = createStaticAppPreset;
|
|
5
|
+
const security_1 = require("./security");
|
|
6
|
+
const logger_1 = require("./logger");
|
|
7
|
+
const static_1 = require("./static");
|
|
8
|
+
function createApiPreset(options = {}) {
|
|
9
|
+
const middlewares = [];
|
|
10
|
+
// 1. Security (CORS, Headers)
|
|
11
|
+
middlewares.push(...(0, security_1.createSecureDefaults)(options.security));
|
|
12
|
+
// 2. Logging
|
|
13
|
+
if (options.logging !== false) {
|
|
14
|
+
const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
|
|
15
|
+
middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
|
|
16
|
+
}
|
|
17
|
+
return middlewares;
|
|
18
|
+
}
|
|
19
|
+
function createStaticAppPreset(options) {
|
|
20
|
+
const middlewares = [];
|
|
21
|
+
// 1. Security
|
|
22
|
+
middlewares.push(...(0, security_1.createSecureDefaults)(options.security));
|
|
23
|
+
// 2. Logging
|
|
24
|
+
if (options.logging !== false) {
|
|
25
|
+
const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
|
|
26
|
+
middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
|
|
27
|
+
}
|
|
28
|
+
// 3. Static Files
|
|
29
|
+
// We force fallthrough to true so API routes can handle non-static requests
|
|
30
|
+
const staticOptions = { ...options.static, fallthrough: true };
|
|
31
|
+
middlewares.push((0, static_1.createStaticMiddleware)(staticOptions));
|
|
32
|
+
return middlewares;
|
|
33
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Radix tree router compiled into a flat array structure for zero-allocation matching.
|
|
4
|
+
* Built once at server startup (frozen).
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.RadixRouter = void 0;
|
|
8
|
+
class RadixRouter {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.roots = new Map();
|
|
11
|
+
this.isFrozen = false;
|
|
12
|
+
}
|
|
13
|
+
register(method, path, handler) {
|
|
14
|
+
if (this.isFrozen) {
|
|
15
|
+
console.warn(`Radix router is frozen. Late route registration (${method} ${path}) may not be optimized.`);
|
|
16
|
+
}
|
|
17
|
+
if (!this.roots.has(method)) {
|
|
18
|
+
this.roots.set(method, {});
|
|
19
|
+
}
|
|
20
|
+
const root = this.roots.get(method);
|
|
21
|
+
const segments = this.normalize(path);
|
|
22
|
+
let node = root;
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
if (!node.children) {
|
|
25
|
+
node.children = new Map();
|
|
26
|
+
}
|
|
27
|
+
const isParam = segment.startsWith(':');
|
|
28
|
+
const key = isParam ? segment.slice(1) : segment;
|
|
29
|
+
if (!node.children.has(key)) {
|
|
30
|
+
node.children.set(key, {
|
|
31
|
+
paramKey: isParam ? key : undefined,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
node = node.children.get(key);
|
|
35
|
+
}
|
|
36
|
+
node.handler = handler;
|
|
37
|
+
}
|
|
38
|
+
match(method, path) {
|
|
39
|
+
const root = this.roots.get(method);
|
|
40
|
+
if (!root) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const segments = this.normalize(path);
|
|
44
|
+
const params = {};
|
|
45
|
+
let node = root;
|
|
46
|
+
for (const segment of segments) {
|
|
47
|
+
if (!node.children) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
// Try exact match first
|
|
51
|
+
let child = node.children.get(segment);
|
|
52
|
+
// If no exact match, try param match
|
|
53
|
+
if (!child) {
|
|
54
|
+
for (const [key, candidate] of node.children.entries()) {
|
|
55
|
+
if (candidate.paramKey && typeof key === 'string') {
|
|
56
|
+
params[key] = segment;
|
|
57
|
+
child = candidate;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!child) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
node = child;
|
|
66
|
+
}
|
|
67
|
+
if (!node.handler) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return { handler: node.handler, params };
|
|
71
|
+
}
|
|
72
|
+
freeze() {
|
|
73
|
+
if (this.isFrozen) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.isFrozen = true;
|
|
77
|
+
// Future: compile to flat array structure here
|
|
78
|
+
}
|
|
79
|
+
isFrozenRouter() {
|
|
80
|
+
return this.isFrozen;
|
|
81
|
+
}
|
|
82
|
+
normalize(path) {
|
|
83
|
+
if (!path || path === '/') {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return path.split('/').filter((segment) => segment.length > 0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.RadixRouter = RadixRouter;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RadixTree = void 0;
|
|
4
|
+
class Node {
|
|
5
|
+
constructor() {
|
|
6
|
+
// Map segment -> Node
|
|
7
|
+
this.children = new Map();
|
|
8
|
+
// Parameter child node (only one allowed per level for simplicity)
|
|
9
|
+
this.paramChild = null;
|
|
10
|
+
this.paramName = null;
|
|
11
|
+
// Data if this node is a route end
|
|
12
|
+
this.data = null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
class RadixTree {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.root = new Node();
|
|
18
|
+
}
|
|
19
|
+
insert(segments, handler, priority) {
|
|
20
|
+
let node = this.root;
|
|
21
|
+
for (const segment of segments) {
|
|
22
|
+
if (segment.startsWith(':')) {
|
|
23
|
+
const paramName = segment.slice(1);
|
|
24
|
+
if (!node.paramChild) {
|
|
25
|
+
node.paramChild = new Node();
|
|
26
|
+
node.paramChild.paramName = paramName;
|
|
27
|
+
}
|
|
28
|
+
else if (node.paramChild.paramName !== paramName) {
|
|
29
|
+
throw new Error(`Cannot have two different parameter names at the same level: "${node.paramChild.paramName}" and "${paramName}"`);
|
|
30
|
+
}
|
|
31
|
+
node = node.paramChild;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (!node.children.has(segment)) {
|
|
35
|
+
node.children.set(segment, new Node());
|
|
36
|
+
}
|
|
37
|
+
node = node.children.get(segment);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
node.data = { handler, priority };
|
|
41
|
+
}
|
|
42
|
+
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
|
+
return this.find(this.root, segments, 0);
|
|
48
|
+
}
|
|
49
|
+
find(node, segments, index) {
|
|
50
|
+
if (index === segments.length) {
|
|
51
|
+
if (node.data) {
|
|
52
|
+
return {
|
|
53
|
+
handler: node.data.handler,
|
|
54
|
+
priority: node.data.priority,
|
|
55
|
+
params: {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const segment = segments[index];
|
|
61
|
+
// 1. Try exact match
|
|
62
|
+
const child = node.children.get(segment);
|
|
63
|
+
if (child) {
|
|
64
|
+
const result = this.find(child, segments, index + 1);
|
|
65
|
+
if (result) {
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 2. Try param match
|
|
70
|
+
if (node.paramChild) {
|
|
71
|
+
const result = this.find(node.paramChild, segments, index + 1);
|
|
72
|
+
if (result) {
|
|
73
|
+
// If match found down this path, add current param to it
|
|
74
|
+
result.params[node.paramChild.paramName] = segment;
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.RadixTree = RadixTree;
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
exports.calculateWorkerCount = calculateWorkerCount;
|
|
7
|
+
exports.isResourceOverloaded = isResourceOverloaded;
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
function calculateWorkerCount(setting) {
|
|
10
|
+
if (typeof setting === 'number') {
|
|
11
|
+
if (!Number.isFinite(setting) || setting <= 0) {
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
return Math.floor(setting);
|
|
15
|
+
}
|
|
16
|
+
const cpuCount = os_1.default.cpus().length || 1;
|
|
17
|
+
return Math.max(1, cpuCount);
|
|
18
|
+
}
|
|
19
|
+
function isResourceOverloaded(sample, thresholds) {
|
|
20
|
+
if (thresholds.maxRssBytes !== undefined &&
|
|
21
|
+
sample.rssBytes > thresholds.maxRssBytes) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Radix tree router compiled into a flat array structure for zero-allocation matching.
|
|
3
|
+
* Built once at server startup (frozen).
|
|
4
|
+
*/
|
|
5
|
+
import { HTTPMethod, QHTTPXHandler } from '../core/types';
|
|
6
|
+
export type CompiledRadixMatch = {
|
|
7
|
+
handler: QHTTPXHandler;
|
|
8
|
+
params: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
export declare class RadixRouter {
|
|
11
|
+
private roots;
|
|
12
|
+
private isFrozen;
|
|
13
|
+
register(method: HTTPMethod, path: string, handler: QHTTPXHandler): void;
|
|
14
|
+
match(method: HTTPMethod, path: string): CompiledRadixMatch | undefined;
|
|
15
|
+
freeze(): void;
|
|
16
|
+
isFrozenRouter(): boolean;
|
|
17
|
+
private normalize;
|
|
18
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Radix tree router compiled into a flat array structure for zero-allocation matching.
|
|
4
|
+
* Built once at server startup (frozen).
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.RadixRouter = void 0;
|
|
8
|
+
class RadixRouter {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.roots = new Map();
|
|
11
|
+
this.isFrozen = false;
|
|
12
|
+
}
|
|
13
|
+
register(method, path, handler) {
|
|
14
|
+
if (this.isFrozen) {
|
|
15
|
+
console.warn(`Radix router is frozen. Late route registration (${method} ${path}) may not be optimized.`);
|
|
16
|
+
}
|
|
17
|
+
if (!this.roots.has(method)) {
|
|
18
|
+
this.roots.set(method, {});
|
|
19
|
+
}
|
|
20
|
+
const root = this.roots.get(method);
|
|
21
|
+
const segments = this.normalize(path);
|
|
22
|
+
let node = root;
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
if (!node.children) {
|
|
25
|
+
node.children = new Map();
|
|
26
|
+
}
|
|
27
|
+
const isParam = segment.startsWith(':');
|
|
28
|
+
const key = isParam ? segment.slice(1) : segment;
|
|
29
|
+
if (!node.children.has(key)) {
|
|
30
|
+
node.children.set(key, {
|
|
31
|
+
paramKey: isParam ? key : undefined,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
node = node.children.get(key);
|
|
35
|
+
}
|
|
36
|
+
node.handler = handler;
|
|
37
|
+
}
|
|
38
|
+
match(method, path) {
|
|
39
|
+
const root = this.roots.get(method);
|
|
40
|
+
if (!root) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const segments = this.normalize(path);
|
|
44
|
+
const params = {};
|
|
45
|
+
let node = root;
|
|
46
|
+
for (const segment of segments) {
|
|
47
|
+
if (!node.children) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
// Try exact match first
|
|
51
|
+
let child = node.children.get(segment);
|
|
52
|
+
// If no exact match, try param match
|
|
53
|
+
if (!child) {
|
|
54
|
+
for (const [key, candidate] of node.children.entries()) {
|
|
55
|
+
if (candidate.paramKey && typeof key === 'string') {
|
|
56
|
+
params[key] = segment;
|
|
57
|
+
child = candidate;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!child) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
node = child;
|
|
66
|
+
}
|
|
67
|
+
if (!node.handler) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return { handler: node.handler, params };
|
|
71
|
+
}
|
|
72
|
+
freeze() {
|
|
73
|
+
if (this.isFrozen) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.isFrozen = true;
|
|
77
|
+
// Future: compile to flat array structure here
|
|
78
|
+
}
|
|
79
|
+
isFrozenRouter() {
|
|
80
|
+
return this.isFrozen;
|
|
81
|
+
}
|
|
82
|
+
normalize(path) {
|
|
83
|
+
if (!path || path === '/') {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return path.split('/').filter((segment) => segment.length > 0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.RadixRouter = RadixRouter;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { QHTTPXHandler, RoutePriority } from '../core/types';
|
|
2
|
+
export type RouteData = {
|
|
3
|
+
handler: QHTTPXHandler;
|
|
4
|
+
priority: RoutePriority;
|
|
5
|
+
};
|
|
6
|
+
export type MatchResult = {
|
|
7
|
+
handler: QHTTPXHandler;
|
|
8
|
+
priority: RoutePriority;
|
|
9
|
+
params: Record<string, string>;
|
|
10
|
+
};
|
|
11
|
+
export declare class RadixTree {
|
|
12
|
+
private root;
|
|
13
|
+
insert(segments: string[], handler: QHTTPXHandler, priority: RoutePriority): void;
|
|
14
|
+
lookup(segments: string[]): MatchResult | null;
|
|
15
|
+
lookupPath(path: string): MatchResult | null;
|
|
16
|
+
private findPath;
|
|
17
|
+
private find;
|
|
18
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RadixTree = void 0;
|
|
4
|
+
class Node {
|
|
5
|
+
constructor() {
|
|
6
|
+
// Map segment -> Node
|
|
7
|
+
this.children = new Map();
|
|
8
|
+
// Parameter child node (only one allowed per level for simplicity)
|
|
9
|
+
this.paramChild = null;
|
|
10
|
+
this.paramName = null;
|
|
11
|
+
// Data if this node is a route end
|
|
12
|
+
this.data = null;
|
|
13
|
+
// Optimization: Pre-allocated match result for static matches
|
|
14
|
+
this.staticMatch = null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class RadixTree {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.root = new Node();
|
|
20
|
+
}
|
|
21
|
+
insert(segments, handler, priority) {
|
|
22
|
+
let node = this.root;
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
if (segment.startsWith(':')) {
|
|
25
|
+
const paramName = segment.slice(1);
|
|
26
|
+
if (!node.paramChild) {
|
|
27
|
+
node.paramChild = new Node();
|
|
28
|
+
node.paramChild.paramName = paramName;
|
|
29
|
+
}
|
|
30
|
+
else if (node.paramChild.paramName !== paramName) {
|
|
31
|
+
throw new Error(`Cannot have two different parameter names at the same level: "${node.paramChild.paramName}" and "${paramName}"`);
|
|
32
|
+
}
|
|
33
|
+
node = node.paramChild;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
if (!node.children.has(segment)) {
|
|
37
|
+
node.children.set(segment, new Node());
|
|
38
|
+
}
|
|
39
|
+
node = node.children.get(segment);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
node.data = { handler, priority };
|
|
43
|
+
node.staticMatch = { handler, priority, params: {} };
|
|
44
|
+
}
|
|
45
|
+
lookup(segments) {
|
|
46
|
+
return this.find(this.root, segments, 0);
|
|
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
|
+
}
|
|
99
|
+
find(node, segments, index) {
|
|
100
|
+
if (index === segments.length) {
|
|
101
|
+
if (node.data) {
|
|
102
|
+
return {
|
|
103
|
+
handler: node.data.handler,
|
|
104
|
+
priority: node.data.priority,
|
|
105
|
+
params: {},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const segment = segments[index];
|
|
111
|
+
// 1. Try exact match
|
|
112
|
+
const child = node.children.get(segment);
|
|
113
|
+
if (child) {
|
|
114
|
+
const result = this.find(child, segments, index + 1);
|
|
115
|
+
if (result) {
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 2. Try param match
|
|
120
|
+
if (node.paramChild) {
|
|
121
|
+
const result = this.find(node.paramChild, segments, index + 1);
|
|
122
|
+
if (result) {
|
|
123
|
+
// If match found down this path, add current param to it
|
|
124
|
+
result.params[node.paramChild.paramName] = segment;
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.RadixTree = RadixTree;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { HTTPMethod, QHTTPXHandler, RouteOptions, RoutePriority } from '../core/types';
|
|
2
|
+
import { RouteSchema } from '../validation/types';
|
|
3
|
+
type RouteDefinition = {
|
|
4
|
+
path: string;
|
|
5
|
+
segments: string[];
|
|
6
|
+
handler: QHTTPXHandler;
|
|
7
|
+
priority: RoutePriority;
|
|
8
|
+
schema?: RouteSchema | Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
export type RouteMatch = {
|
|
11
|
+
handler: QHTTPXHandler;
|
|
12
|
+
params: Record<string, string>;
|
|
13
|
+
priority: RoutePriority;
|
|
14
|
+
};
|
|
15
|
+
export declare class Router {
|
|
16
|
+
private readonly methodBuckets;
|
|
17
|
+
private readonly radixTrees;
|
|
18
|
+
private readonly staticRoutes;
|
|
19
|
+
private isFrozen;
|
|
20
|
+
register(method: HTTPMethod, path: string, handler: QHTTPXHandler, options?: RouteOptions & {
|
|
21
|
+
schema?: RouteSchema | Record<string, unknown>;
|
|
22
|
+
}): void;
|
|
23
|
+
getRoutes(): Map<HTTPMethod, RouteDefinition[]>;
|
|
24
|
+
match(method: HTTPMethod, path: string): RouteMatch | undefined;
|
|
25
|
+
getAllowedMethods(path: string): HTTPMethod[];
|
|
26
|
+
/**
|
|
27
|
+
* Freeze the router after server starts.
|
|
28
|
+
* Prevents further route registration and builds derived structures for optimized matching.
|
|
29
|
+
*/
|
|
30
|
+
freeze(): void;
|
|
31
|
+
isFrozenRouter(): boolean;
|
|
32
|
+
private normalize;
|
|
33
|
+
}
|
|
34
|
+
export {};
|