qhttpx 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/.github/workflows/npm-publish.yml +37 -0
  4. package/.github/workflows/release.yml +21 -0
  5. package/.prettierrc +7 -0
  6. package/CHANGELOG.md +145 -0
  7. package/LICENSE +21 -0
  8. package/README.md +343 -0
  9. package/dist/package.json +61 -0
  10. package/dist/src/benchmarks/compare-frameworks.js +119 -0
  11. package/dist/src/benchmarks/quantam-users.js +56 -0
  12. package/dist/src/benchmarks/simple-json.js +58 -0
  13. package/dist/src/benchmarks/ultra-mode.js +122 -0
  14. package/dist/src/cli/index.js +200 -0
  15. package/dist/src/client/index.js +72 -0
  16. package/dist/src/core/batch.js +97 -0
  17. package/dist/src/core/body-parser.js +121 -0
  18. package/dist/src/core/buffer-pool.js +70 -0
  19. package/dist/src/core/config.js +50 -0
  20. package/dist/src/core/fusion.js +183 -0
  21. package/dist/src/core/logger.js +49 -0
  22. package/dist/src/core/metrics.js +111 -0
  23. package/dist/src/core/resources.js +25 -0
  24. package/dist/src/core/scheduler.js +85 -0
  25. package/dist/src/core/scope.js +68 -0
  26. package/dist/src/core/serializer.js +44 -0
  27. package/dist/src/core/server.js +905 -0
  28. package/dist/src/core/stream.js +71 -0
  29. package/dist/src/core/tasks.js +87 -0
  30. package/dist/src/core/types.js +19 -0
  31. package/dist/src/core/websocket.js +86 -0
  32. package/dist/src/core/worker-queue.js +73 -0
  33. package/dist/src/database/adapters/memory.js +90 -0
  34. package/dist/src/database/adapters/mongo.js +141 -0
  35. package/dist/src/database/adapters/postgres.js +111 -0
  36. package/dist/src/database/adapters/sqlite.js +42 -0
  37. package/dist/src/database/coalescer.js +134 -0
  38. package/dist/src/database/manager.js +87 -0
  39. package/dist/src/database/types.js +2 -0
  40. package/dist/src/index.js +61 -0
  41. package/dist/src/middleware/compression.js +133 -0
  42. package/dist/src/middleware/cors.js +66 -0
  43. package/dist/src/middleware/presets.js +33 -0
  44. package/dist/src/middleware/rate-limit.js +77 -0
  45. package/dist/src/middleware/security.js +69 -0
  46. package/dist/src/middleware/static.js +191 -0
  47. package/dist/src/openapi/generator.js +149 -0
  48. package/dist/src/router/radix-router.js +89 -0
  49. package/dist/src/router/radix-tree.js +81 -0
  50. package/dist/src/router/router.js +146 -0
  51. package/dist/src/testing/index.js +84 -0
  52. package/dist/src/utils/cookies.js +59 -0
  53. package/dist/src/utils/logger.js +45 -0
  54. package/dist/src/utils/signals.js +31 -0
  55. package/dist/src/utils/sse.js +32 -0
  56. package/dist/src/validation/index.js +19 -0
  57. package/dist/src/validation/simple.js +102 -0
  58. package/dist/src/validation/types.js +12 -0
  59. package/dist/src/validation/zod.js +18 -0
  60. package/dist/src/views/index.js +17 -0
  61. package/dist/src/views/types.js +2 -0
  62. package/dist/tests/adapters.test.js +106 -0
  63. package/dist/tests/batch.test.js +117 -0
  64. package/dist/tests/body-parser.test.js +52 -0
  65. package/dist/tests/compression-sse.test.js +87 -0
  66. package/dist/tests/cookies.test.js +63 -0
  67. package/dist/tests/cors.test.js +55 -0
  68. package/dist/tests/database.test.js +80 -0
  69. package/dist/tests/dx.test.js +64 -0
  70. package/dist/tests/ecosystem.test.js +133 -0
  71. package/dist/tests/features.test.js +47 -0
  72. package/dist/tests/fusion.test.js +92 -0
  73. package/dist/tests/http-basic.test.js +124 -0
  74. package/dist/tests/logger.test.js +33 -0
  75. package/dist/tests/middleware.test.js +109 -0
  76. package/dist/tests/observability.test.js +59 -0
  77. package/dist/tests/openapi.test.js +64 -0
  78. package/dist/tests/plugin.test.js +65 -0
  79. package/dist/tests/plugins.test.js +71 -0
  80. package/dist/tests/rate-limit.test.js +77 -0
  81. package/dist/tests/resources.test.js +44 -0
  82. package/dist/tests/scheduler.test.js +46 -0
  83. package/dist/tests/schema-routes.test.js +77 -0
  84. package/dist/tests/security.test.js +83 -0
  85. package/dist/tests/server-db.test.js +72 -0
  86. package/dist/tests/smoke.test.js +10 -0
  87. package/dist/tests/sqlite-fusion.test.js +92 -0
  88. package/dist/tests/static.test.js +102 -0
  89. package/dist/tests/stream.test.js +44 -0
  90. package/dist/tests/task-metrics.test.js +53 -0
  91. package/dist/tests/tasks.test.js +62 -0
  92. package/dist/tests/testing.test.js +47 -0
  93. package/dist/tests/validation.test.js +107 -0
  94. package/dist/tests/websocket.test.js +146 -0
  95. package/dist/vitest.config.js +9 -0
  96. package/docs/AEGIS.md +76 -0
  97. package/docs/BENCHMARKS.md +36 -0
  98. package/docs/CAPABILITIES.md +70 -0
  99. package/docs/CLI.md +43 -0
  100. package/docs/DATABASE.md +142 -0
  101. package/docs/ECOSYSTEM.md +146 -0
  102. package/docs/NEXT_STEPS.md +99 -0
  103. package/docs/OPENAPI.md +99 -0
  104. package/docs/PLUGINS.md +59 -0
  105. package/docs/REAL_WORLD_EXAMPLES.md +109 -0
  106. package/docs/ROADMAP.md +366 -0
  107. package/docs/VALIDATION.md +136 -0
  108. package/eslint.config.cjs +26 -0
  109. package/examples/api-server.ts +254 -0
  110. package/package.json +61 -0
  111. package/src/benchmarks/compare-frameworks.ts +149 -0
  112. package/src/benchmarks/quantam-users.ts +70 -0
  113. package/src/benchmarks/simple-json.ts +71 -0
  114. package/src/benchmarks/ultra-mode.ts +159 -0
  115. package/src/cli/index.ts +214 -0
  116. package/src/client/index.ts +93 -0
  117. package/src/core/batch.ts +110 -0
  118. package/src/core/body-parser.ts +151 -0
  119. package/src/core/buffer-pool.ts +96 -0
  120. package/src/core/config.ts +60 -0
  121. package/src/core/fusion.ts +210 -0
  122. package/src/core/logger.ts +70 -0
  123. package/src/core/metrics.ts +166 -0
  124. package/src/core/resources.ts +38 -0
  125. package/src/core/scheduler.ts +126 -0
  126. package/src/core/scope.ts +87 -0
  127. package/src/core/serializer.ts +41 -0
  128. package/src/core/server.ts +1113 -0
  129. package/src/core/stream.ts +111 -0
  130. package/src/core/tasks.ts +138 -0
  131. package/src/core/types.ts +178 -0
  132. package/src/core/websocket.ts +112 -0
  133. package/src/core/worker-queue.ts +90 -0
  134. package/src/database/adapters/memory.ts +99 -0
  135. package/src/database/adapters/mongo.ts +116 -0
  136. package/src/database/adapters/postgres.ts +86 -0
  137. package/src/database/adapters/sqlite.ts +44 -0
  138. package/src/database/coalescer.ts +153 -0
  139. package/src/database/manager.ts +97 -0
  140. package/src/database/types.ts +24 -0
  141. package/src/index.ts +42 -0
  142. package/src/middleware/compression.ts +147 -0
  143. package/src/middleware/cors.ts +98 -0
  144. package/src/middleware/presets.ts +50 -0
  145. package/src/middleware/rate-limit.ts +106 -0
  146. package/src/middleware/security.ts +109 -0
  147. package/src/middleware/static.ts +216 -0
  148. package/src/openapi/generator.ts +167 -0
  149. package/src/router/radix-router.ts +119 -0
  150. package/src/router/radix-tree.ts +106 -0
  151. package/src/router/router.ts +190 -0
  152. package/src/testing/index.ts +104 -0
  153. package/src/utils/cookies.ts +67 -0
  154. package/src/utils/logger.ts +59 -0
  155. package/src/utils/signals.ts +45 -0
  156. package/src/utils/sse.ts +41 -0
  157. package/src/validation/index.ts +3 -0
  158. package/src/validation/simple.ts +93 -0
  159. package/src/validation/types.ts +38 -0
  160. package/src/validation/zod.ts +14 -0
  161. package/src/views/index.ts +1 -0
  162. package/src/views/types.ts +4 -0
  163. package/tests/adapters.test.ts +120 -0
  164. package/tests/batch.test.ts +139 -0
  165. package/tests/body-parser.test.ts +83 -0
  166. package/tests/compression-sse.test.ts +98 -0
  167. package/tests/cookies.test.ts +74 -0
  168. package/tests/cors.test.ts +79 -0
  169. package/tests/database.test.ts +90 -0
  170. package/tests/dx.test.ts +78 -0
  171. package/tests/ecosystem.test.ts +156 -0
  172. package/tests/features.test.ts +51 -0
  173. package/tests/fusion.test.ts +121 -0
  174. package/tests/http-basic.test.ts +161 -0
  175. package/tests/logger.test.ts +48 -0
  176. package/tests/middleware.test.ts +137 -0
  177. package/tests/observability.test.ts +91 -0
  178. package/tests/openapi.test.ts +74 -0
  179. package/tests/plugin.test.ts +85 -0
  180. package/tests/plugins.test.ts +93 -0
  181. package/tests/rate-limit.test.ts +97 -0
  182. package/tests/resources.test.ts +64 -0
  183. package/tests/scheduler.test.ts +71 -0
  184. package/tests/schema-routes.test.ts +89 -0
  185. package/tests/security.test.ts +128 -0
  186. package/tests/server-db.test.ts +72 -0
  187. package/tests/smoke.test.ts +9 -0
  188. package/tests/sqlite-fusion.test.ts +106 -0
  189. package/tests/static.test.ts +111 -0
  190. package/tests/stream.test.ts +58 -0
  191. package/tests/task-metrics.test.ts +78 -0
  192. package/tests/tasks.test.ts +90 -0
  193. package/tests/testing.test.ts +53 -0
  194. package/tests/validation.test.ts +126 -0
  195. package/tests/websocket.test.ts +132 -0
  196. package/tsconfig.json +16 -0
  197. package/vitest.config.ts +9 -0
@@ -0,0 +1,905 @@
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.QHTTPX = void 0;
7
+ const http_1 = __importDefault(require("http"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const url_1 = require("url");
10
+ const package_json_1 = __importDefault(require("../../package.json"));
11
+ const router_1 = require("../router/router");
12
+ const scheduler_1 = require("./scheduler");
13
+ const tasks_1 = require("./tasks");
14
+ const resources_1 = require("./resources");
15
+ const metrics_1 = require("./metrics");
16
+ const body_parser_1 = require("./body-parser");
17
+ const buffer_pool_1 = require("./buffer-pool");
18
+ const websocket_1 = require("./websocket");
19
+ const serializer_1 = require("./serializer");
20
+ const batch_1 = require("./batch");
21
+ const fusion_1 = require("./fusion");
22
+ const simple_1 = require("../validation/simple");
23
+ const generator_1 = require("../openapi/generator");
24
+ const types_1 = require("./types");
25
+ const cookies_1 = require("../utils/cookies");
26
+ const scope_1 = require("./scope");
27
+ const logger_1 = require("./logger");
28
+ class QHTTPX {
29
+ constructor(options = {}) {
30
+ this.middlewares = [];
31
+ this.contextPool = [];
32
+ this.pipelineRunner = null;
33
+ this.onStartHooks = [];
34
+ this.onBeforeShutdownHooks = [];
35
+ this.onShutdownHooks = [];
36
+ this.nextRequestId = 1;
37
+ this.options = options;
38
+ this.logger = new logger_1.Logger({
39
+ name: options.name,
40
+ level: options.performanceMode === 'ultra' ? 'error' : 'info'
41
+ });
42
+ this.ultraMode = options.performanceMode === 'ultra';
43
+ this.errorHandler = this.ultraMode ? undefined : options.errorHandler;
44
+ this.notFoundHandler = this.ultraMode ? undefined : options.notFoundHandler;
45
+ this.methodNotAllowedHandler = this.ultraMode ? undefined : options.methodNotAllowedHandler;
46
+ this.tracer = this.ultraMode ? undefined : options.tracer;
47
+ this.workerCount = (0, resources_1.calculateWorkerCount)(options.workers ?? 'auto');
48
+ this.router = new router_1.Router();
49
+ this.wsManager = new websocket_1.WebSocketManager(this.generateRequestId.bind(this));
50
+ this.bufferPool = new buffer_pool_1.BufferPool(options.bufferPoolConfig);
51
+ const maxConcurrency = options.maxConcurrency ?? 1024;
52
+ this.scheduler = new scheduler_1.Scheduler({
53
+ maxConcurrency,
54
+ });
55
+ this.tasks = new tasks_1.TaskEngine(this.scheduler);
56
+ if (options.enableBatching) {
57
+ this.batchExecutor = new batch_1.BatchExecutor(options.database);
58
+ }
59
+ if (options.enableRequestFusion) {
60
+ this.fusion = new fusion_1.RequestFusion(options.enableRequestFusion);
61
+ }
62
+ this.validator = options.validator ?? new simple_1.SimpleValidator();
63
+ this.metrics = new metrics_1.Metrics(this.scheduler, {
64
+ enabled: !this.ultraMode && (this.options.metricsEnabled ?? true),
65
+ }, this.tasks);
66
+ for (let i = 0; i < maxConcurrency; i += 1) {
67
+ this.contextPool.push(this.createContext());
68
+ }
69
+ this.registerInternalRoutes();
70
+ this.compileMiddlewarePipeline();
71
+ this.server = http_1.default.createServer(this.handleRequest.bind(this));
72
+ if (this.options.keepAliveTimeoutMs !== undefined) {
73
+ this.server.keepAliveTimeout = this.options.keepAliveTimeoutMs;
74
+ }
75
+ else if (this.ultraMode) {
76
+ this.server.keepAliveTimeout = 0;
77
+ }
78
+ if (this.options.headersTimeoutMs !== undefined) {
79
+ this.server.headersTimeout = this.options.headersTimeoutMs;
80
+ }
81
+ else if (this.ultraMode) {
82
+ this.server.headersTimeout = 0;
83
+ }
84
+ if (this.ultraMode) {
85
+ this.server.requestTimeout = 0;
86
+ this.server.maxHeadersCount = 0;
87
+ this.server.timeout = 0;
88
+ }
89
+ this.server.on('upgrade', (req, socket, head) => {
90
+ void this.handleUpgrade(req, socket, head);
91
+ });
92
+ }
93
+ get serverInstance() {
94
+ return this.server;
95
+ }
96
+ get websocket() {
97
+ return this.wsManager;
98
+ }
99
+ setErrorHandler(handler) {
100
+ this.errorHandler = handler;
101
+ }
102
+ setNotFoundHandler(handler) {
103
+ this.notFoundHandler = handler;
104
+ }
105
+ setMethodNotAllowedHandler(handler) {
106
+ this.methodNotAllowedHandler = handler;
107
+ }
108
+ set404Handler(handler) {
109
+ this.setNotFoundHandler(handler);
110
+ }
111
+ set405Handler(handler) {
112
+ this.setMethodNotAllowedHandler(handler);
113
+ }
114
+ onStart(hook) {
115
+ this.onStartHooks.push(hook);
116
+ }
117
+ onBeforeShutdown(hook) {
118
+ this.onBeforeShutdownHooks.push(hook);
119
+ }
120
+ onShutdown(hook) {
121
+ this.onShutdownHooks.push(hook);
122
+ }
123
+ upgrade(path, handler) {
124
+ this.wsManager.register(path, handler);
125
+ }
126
+ use(middleware) {
127
+ this.middlewares.push(middleware);
128
+ // Recompile pipeline after middleware is added
129
+ this.compileMiddlewarePipeline();
130
+ }
131
+ compileMiddlewarePipeline() {
132
+ const middlewares = this.middlewares;
133
+ if (middlewares.length === 0) {
134
+ this.pipelineRunner = async (ctx, handler) => {
135
+ const result = handler(ctx);
136
+ if (result && typeof result.then === 'function') {
137
+ await result;
138
+ }
139
+ };
140
+ return;
141
+ }
142
+ // Flatten middleware pipeline into a single function chain
143
+ // This eliminates Promise nesting, recursive dispatch overhead, and microtask backlogs
144
+ // Each middleware executes directly without closure allocation overhead
145
+ this.pipelineRunner = async (ctx, handler) => {
146
+ let index = 0;
147
+ const executeNext = async () => {
148
+ if (index >= middlewares.length) {
149
+ // All middlewares done, execute handler
150
+ const result = handler(ctx);
151
+ if (result && typeof result.then === 'function') {
152
+ await result;
153
+ }
154
+ return;
155
+ }
156
+ const currentIndex = index;
157
+ index += 1;
158
+ const result = middlewares[currentIndex](ctx, executeNext);
159
+ if (result && typeof result.then === 'function') {
160
+ await result;
161
+ }
162
+ };
163
+ await executeNext();
164
+ };
165
+ }
166
+ compileRoutePipeline(handler,
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ schema) {
169
+ const middlewares = this.middlewares;
170
+ // Heuristic: Is it a structured RouteSchema or a legacy ResponseSchema?
171
+ let responseSchema;
172
+ let requestSchema;
173
+ if (schema) {
174
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
+ const s = schema;
176
+ if (s.body || s.query || s.params || s.headers || s.response) {
177
+ // It is a RouteSchema
178
+ requestSchema = s;
179
+ responseSchema = s.response;
180
+ }
181
+ else {
182
+ // It is a legacy ResponseSchema
183
+ responseSchema = s;
184
+ }
185
+ }
186
+ const stringifier = responseSchema ? (0, serializer_1.getStringifier)(responseSchema) : undefined;
187
+ // Build middleware chain
188
+ let pipeline = handler;
189
+ // Wrap with validation if needed (before handler, after global middleware)
190
+ if (requestSchema) {
191
+ const inner = pipeline;
192
+ pipeline = async (ctx) => {
193
+ // Validate Body
194
+ if (requestSchema.body) {
195
+ const result = await this.validator.validate(requestSchema.body, ctx.body);
196
+ if (!result.success) {
197
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
198
+ const err = result.error;
199
+ ctx.json({ error: 'Validation Error', details: err.details || err.message || err }, 400);
200
+ return;
201
+ }
202
+ ctx.body = result.data;
203
+ }
204
+ // Validate Query
205
+ if (requestSchema.query) {
206
+ const result = await this.validator.validate(requestSchema.query, ctx.query);
207
+ if (!result.success) {
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ const err = result.error;
210
+ ctx.json({ error: 'Validation Error', details: err.details || err.message || err }, 400);
211
+ return;
212
+ }
213
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
214
+ ctx.query = result.data;
215
+ }
216
+ // Validate Params
217
+ if (requestSchema.params) {
218
+ const result = await this.validator.validate(requestSchema.params, ctx.params);
219
+ if (!result.success) {
220
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
221
+ const err = result.error;
222
+ ctx.json({ error: 'Validation Error', details: err.details || err.message || err }, 400);
223
+ return;
224
+ }
225
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
226
+ ctx.params = result.data;
227
+ }
228
+ return inner(ctx);
229
+ };
230
+ }
231
+ // Wrap with Request Fusion if enabled
232
+ if (this.fusion) {
233
+ const inner = pipeline;
234
+ pipeline = (ctx) => this.fusion.coalesce(ctx, inner);
235
+ }
236
+ // We iterate backwards to wrap the handler
237
+ // pipeline = m[last](ctx, () => pipeline(ctx))
238
+ for (let i = middlewares.length - 1; i >= 0; i -= 1) {
239
+ const middleware = middlewares[i];
240
+ const next = pipeline;
241
+ pipeline = (ctx) => {
242
+ return middleware(ctx, async () => {
243
+ const result = next(ctx);
244
+ if (result && typeof result.then === 'function') {
245
+ await result;
246
+ }
247
+ });
248
+ };
249
+ }
250
+ if (stringifier) {
251
+ const inner = pipeline;
252
+ return (ctx) => {
253
+ ctx.serializer = stringifier;
254
+ return inner(ctx);
255
+ };
256
+ }
257
+ return pipeline;
258
+ }
259
+ // Internal registration to be accessed by Scopes
260
+ _registerRoute(method, path, handlerOrOptions) {
261
+ this.registerRoute(method, path, handlerOrOptions);
262
+ }
263
+ async register(plugin, options) {
264
+ const scope = new scope_1.QHTTPXScope(this, options?.prefix);
265
+ await plugin(scope, options);
266
+ }
267
+ registerRoute(method, path, handlerOrOptions) {
268
+ let handler;
269
+ let schema;
270
+ const options = {};
271
+ if (typeof handlerOrOptions === 'function') {
272
+ handler = handlerOrOptions;
273
+ }
274
+ else {
275
+ handler = handlerOrOptions.handler;
276
+ schema = handlerOrOptions.schema;
277
+ options.priority = handlerOrOptions.priority;
278
+ }
279
+ const compiled = this.compileRoutePipeline(handler, schema);
280
+ this.router.register(method, path, compiled, { ...options, schema });
281
+ }
282
+ get(path, handler) {
283
+ this.registerRoute('GET', path, handler);
284
+ }
285
+ post(path, handler) {
286
+ this.registerRoute('POST', path, handler);
287
+ }
288
+ put(path, handler) {
289
+ this.registerRoute('PUT', path, handler);
290
+ }
291
+ delete(path, handler) {
292
+ this.registerRoute('DELETE', path, handler);
293
+ }
294
+ route(path) {
295
+ const register = (method, handler) => {
296
+ this.registerRoute(method, path, handler);
297
+ };
298
+ const builder = {
299
+ get(handler) {
300
+ register('GET', handler);
301
+ return this;
302
+ },
303
+ post(handler) {
304
+ register('POST', handler);
305
+ return this;
306
+ },
307
+ put(handler) {
308
+ register('PUT', handler);
309
+ return this;
310
+ },
311
+ delete(handler) {
312
+ register('DELETE', handler);
313
+ return this;
314
+ },
315
+ };
316
+ return builder;
317
+ }
318
+ task(name, handler, options) {
319
+ this.tasks.register(name, handler, options);
320
+ }
321
+ enqueue(name, payload) {
322
+ return this.tasks.enqueue(name, payload);
323
+ }
324
+ op(name, handler) {
325
+ if (!this.batchExecutor) {
326
+ // Auto-enable batch executor if op is called?
327
+ // Or throw?
328
+ // Better to throw or warn if not enabled.
329
+ // But for ease of use, let's create it if missing (but we miss DB context if done this way without options)
330
+ // The constructor handles options. If enableBatching is false, we shouldn't be here.
331
+ throw new Error('Batching is not enabled. Pass enableBatching: true to QHTTPX options.');
332
+ }
333
+ this.batchExecutor.register(name, handler);
334
+ }
335
+ registerInternalRoutes() {
336
+ this.router.register('GET', '/__qhttpx/health', (ctx) => {
337
+ const version = typeof package_json_1.default.version === 'string' ? package_json_1.default.version : '';
338
+ const name = typeof package_json_1.default.name === 'string' ? package_json_1.default.name : '';
339
+ ctx.json({
340
+ status: 'ok',
341
+ name,
342
+ version,
343
+ workers: this.workerCount,
344
+ });
345
+ });
346
+ this.router.register('GET', '/__qhttpx/metrics', (ctx) => {
347
+ const snapshot = this.metrics.snapshot();
348
+ ctx.json({
349
+ ...snapshot,
350
+ workers: this.workerCount,
351
+ });
352
+ });
353
+ this.router.register('GET', '/__qhttpx/runtime', (ctx) => {
354
+ const schedulerStats = this.scheduler.getStats();
355
+ ctx.json({
356
+ workers: this.workerCount,
357
+ router: {
358
+ frozen: this.router.isFrozenRouter(),
359
+ },
360
+ scheduler: schedulerStats,
361
+ });
362
+ });
363
+ if (this.options.enableBatching && this.batchExecutor) {
364
+ const endpoint = typeof this.options.enableBatching === 'object'
365
+ ? this.options.enableBatching.endpoint
366
+ : '/qhttpx';
367
+ this.router.register('POST', endpoint, async (ctx) => {
368
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
369
+ const body = ctx.body;
370
+ if (!body || !Array.isArray(body.batch)) {
371
+ ctx.json({ error: 'Invalid batch format' }, 400);
372
+ return;
373
+ }
374
+ try {
375
+ const result = await this.batchExecutor.handleBatch(ctx, body.batch);
376
+ ctx.json(result);
377
+ }
378
+ catch (err) {
379
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
380
+ ctx.json({ error: err.message }, 500);
381
+ }
382
+ });
383
+ }
384
+ }
385
+ getOpenAPI(options) {
386
+ const generator = new generator_1.OpenAPIGenerator(this.router, options);
387
+ return generator.generate();
388
+ }
389
+ async listen(port, hostname) {
390
+ if (this.options.database) {
391
+ await this.options.database.connect();
392
+ }
393
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
394
+ if (this.options.plugins) {
395
+ // Plugins are registered synchronously in constructor, but their async init happens now?
396
+ // Actually, plugins are registered via app.register() which is async.
397
+ // So they should be ready.
398
+ }
399
+ return new Promise((resolve, reject) => {
400
+ const onError = (error) => {
401
+ this.server.off('error', onError);
402
+ reject(error);
403
+ };
404
+ this.server.once('error', onError);
405
+ this.server.listen(port, hostname, () => {
406
+ this.server.off('error', onError);
407
+ // Freeze router after server starts to prevent further registrations
408
+ this.router.freeze();
409
+ void this.runLifecycleHooks(this.onStartHooks);
410
+ const address = this.server.address();
411
+ if (address && typeof address === 'object') {
412
+ resolve({ port: address.port });
413
+ }
414
+ else {
415
+ resolve({ port });
416
+ }
417
+ });
418
+ });
419
+ }
420
+ close() {
421
+ return new Promise((resolve, reject) => {
422
+ this.server.close((err) => {
423
+ if (err) {
424
+ reject(err);
425
+ return;
426
+ }
427
+ resolve();
428
+ });
429
+ });
430
+ }
431
+ async shutdown() {
432
+ await this.runLifecycleHooks(this.onBeforeShutdownHooks);
433
+ await this.close();
434
+ await this.runLifecycleHooks(this.onShutdownHooks);
435
+ }
436
+ createContext() {
437
+ const jsonSerializer = this.options.jsonSerializer;
438
+ const useFastStringify = jsonSerializer === undefined;
439
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
440
+ const ctx = {
441
+ req: null,
442
+ res: null,
443
+ url: null,
444
+ params: null,
445
+ query: null,
446
+ body: undefined,
447
+ cookies: null,
448
+ state: null,
449
+ bufferPool: this.bufferPool,
450
+ requestId: '',
451
+ requestStart: 0,
452
+ serializer: null,
453
+ json(payload, status = 200) {
454
+ const res = this.res;
455
+ if (!res.headersSent) {
456
+ res.statusCode = status;
457
+ res.setHeader('content-type', 'application/json; charset=utf-8');
458
+ }
459
+ let body;
460
+ if (this.serializer) {
461
+ body = this.serializer(payload);
462
+ }
463
+ else if (useFastStringify) {
464
+ body = (0, serializer_1.fastJsonStringify)(payload);
465
+ }
466
+ else if (jsonSerializer) {
467
+ body = jsonSerializer(payload);
468
+ }
469
+ else {
470
+ body = JSON.stringify(payload);
471
+ }
472
+ res.end(body);
473
+ },
474
+ send(payload, status = 200) {
475
+ const res = this.res;
476
+ if (!res.headersSent) {
477
+ res.statusCode = status;
478
+ }
479
+ res.end(payload);
480
+ },
481
+ html(payload, status = 200) {
482
+ const res = this.res;
483
+ if (!res.headersSent) {
484
+ res.statusCode = status;
485
+ res.setHeader('content-type', 'text/html; charset=utf-8');
486
+ }
487
+ res.end(payload);
488
+ },
489
+ redirect(url, status = 302) {
490
+ const res = this.res;
491
+ if (!res.headersSent) {
492
+ res.statusCode = status;
493
+ res.setHeader('Location', url);
494
+ }
495
+ res.end();
496
+ },
497
+ setCookie(name, value, options) {
498
+ const res = this.res;
499
+ const serialized = (0, cookies_1.serializeCookie)(name, value, options);
500
+ let existing = res.getHeader('Set-Cookie');
501
+ if (Array.isArray(existing)) {
502
+ existing.push(serialized);
503
+ res.setHeader('Set-Cookie', existing);
504
+ }
505
+ else if (existing) {
506
+ res.setHeader('Set-Cookie', [existing, serialized]);
507
+ }
508
+ else {
509
+ res.setHeader('Set-Cookie', serialized);
510
+ }
511
+ },
512
+ db: this.options.database,
513
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
514
+ render: async (view, locals) => {
515
+ const engine = this.options.viewEngine;
516
+ if (!engine) {
517
+ throw new Error('No view engine registered');
518
+ }
519
+ const viewsPath = this.options.viewsPath || process.cwd();
520
+ const fullPath = path_1.default.resolve(viewsPath, view);
521
+ const html = await engine.render(fullPath, locals || {});
522
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
523
+ const res = ctx.res;
524
+ if (!res.headersSent) {
525
+ res.statusCode = 200;
526
+ res.setHeader('content-type', 'text/html; charset=utf-8');
527
+ }
528
+ res.end(html);
529
+ },
530
+ validate: async (schema, data) => {
531
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
532
+ const target = data ?? ctx.body;
533
+ const result = await this.validator.validate(schema, target);
534
+ if (result.success) {
535
+ return result.data;
536
+ }
537
+ throw new types_1.HttpError(400, 'Validation Error', {
538
+ code: 'VALIDATION_ERROR',
539
+ details: result.error,
540
+ });
541
+ },
542
+ };
543
+ return ctx;
544
+ }
545
+ acquireContext(req, res, url, params, query, requestId, body,
546
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
547
+ files) {
548
+ const ctx = this.contextPool.pop() ?? this.createContext();
549
+ // Reset and populate properties
550
+ // We use type assertions to write to readonly/managed properties for performance
551
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
552
+ const mutableCtx = ctx;
553
+ mutableCtx.req = req;
554
+ mutableCtx.res = res;
555
+ mutableCtx.url = url;
556
+ mutableCtx.params = params;
557
+ mutableCtx.query = query;
558
+ mutableCtx.body = body;
559
+ mutableCtx.files = files;
560
+ mutableCtx.requestId = requestId;
561
+ mutableCtx.serializer = undefined;
562
+ // In ultra mode, skip cookie parsing to save overhead
563
+ if (!this.ultraMode) {
564
+ mutableCtx.cookies = (0, cookies_1.parseCookies)(req.headers.cookie);
565
+ }
566
+ mutableCtx.state = {};
567
+ mutableCtx.disableAutoEnd = false;
568
+ return ctx;
569
+ }
570
+ releaseContext(ctx) {
571
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
572
+ const mutableCtx = ctx;
573
+ mutableCtx.req = null;
574
+ mutableCtx.res = null;
575
+ mutableCtx.url = null;
576
+ mutableCtx.params = null;
577
+ mutableCtx.query = null;
578
+ mutableCtx.body = undefined;
579
+ mutableCtx.files = undefined;
580
+ mutableCtx.requestId = '';
581
+ mutableCtx.requestStart = 0;
582
+ mutableCtx.serializer = null;
583
+ mutableCtx.cookies = null;
584
+ mutableCtx.state = null;
585
+ // render method is static per context instance creation (closure over options),
586
+ // but good to keep it consistent.
587
+ // Wait, 'render' is defined in 'createContext' and depends on 'this.options'.
588
+ // It doesn't hold request-specific state other than 'res' which is updated in 'acquireContext'.
589
+ // So we don't need to null it out.
590
+ if (this.contextPool.length < (this.options.maxConcurrency ?? 1024)) {
591
+ this.contextPool.push(ctx);
592
+ }
593
+ }
594
+ async handleRequest(req, res) {
595
+ const rawMethod = (req.method || 'GET').toUpperCase();
596
+ const method = rawMethod;
597
+ const rawUrl = req.url || '/';
598
+ const host = req.headers.host || 'localhost';
599
+ const url = new url_1.URL(rawUrl, `http://${host}`);
600
+ const incomingRequestIdHeader = req.headers['x-request-id'];
601
+ let requestId;
602
+ if (typeof incomingRequestIdHeader === 'string') {
603
+ requestId = incomingRequestIdHeader;
604
+ }
605
+ else if (Array.isArray(incomingRequestIdHeader)) {
606
+ requestId = incomingRequestIdHeader[0];
607
+ }
608
+ if (!requestId) {
609
+ requestId = this.generateRequestId();
610
+ }
611
+ if (!res.headersSent && requestId) {
612
+ res.setHeader('x-request-id', requestId);
613
+ }
614
+ let match = this.router.match(method, url.pathname);
615
+ const hasRoute = !!match;
616
+ if (!match) {
617
+ match = {
618
+ handler: () => { },
619
+ params: {},
620
+ priority: types_1.RoutePriority.STANDARD,
621
+ };
622
+ }
623
+ const allowedMethods = this.router.getAllowedMethods(url.pathname);
624
+ if (!hasRoute && !res.headersSent) {
625
+ const hasAnyMethod = allowedMethods.length > 0;
626
+ res.statusCode = hasAnyMethod ? 405 : 404;
627
+ }
628
+ const query = {};
629
+ url.searchParams.forEach((value, key) => {
630
+ if (Object.prototype.hasOwnProperty.call(query, key)) {
631
+ const existing = query[key];
632
+ if (Array.isArray(existing)) {
633
+ existing.push(value);
634
+ }
635
+ else {
636
+ query[key] = [existing, value];
637
+ }
638
+ }
639
+ else {
640
+ query[key] = value;
641
+ }
642
+ });
643
+ if (this.options.maxMemoryBytes !== undefined) {
644
+ const sampleRss = process.memoryUsage().rss;
645
+ const overloadedByMemory = (0, resources_1.isResourceOverloaded)({ rssBytes: sampleRss }, { maxRssBytes: this.options.maxMemoryBytes });
646
+ if (overloadedByMemory) {
647
+ const overloaded = () => {
648
+ if (res.writableEnded) {
649
+ return;
650
+ }
651
+ if (!res.headersSent) {
652
+ res.statusCode = 503;
653
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
654
+ }
655
+ res.end('Server overloaded');
656
+ };
657
+ overloaded();
658
+ return;
659
+ }
660
+ }
661
+ let body;
662
+ let files;
663
+ if (method !== 'GET' && method !== 'HEAD') {
664
+ try {
665
+ const parsed = await body_parser_1.BodyParser.parse(req, {
666
+ maxBodyBytes: this.options.maxBodyBytes,
667
+ });
668
+ body = parsed.body;
669
+ files = parsed.files;
670
+ }
671
+ catch (err) {
672
+ if (err instanceof Error && err.message === 'QHTTPX_INVALID_JSON') {
673
+ if (!res.headersSent) {
674
+ res.statusCode = 400;
675
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
676
+ }
677
+ res.end('Invalid JSON');
678
+ return;
679
+ }
680
+ if (err instanceof Error && err.message === 'QHTTPX_BODY_TOO_LARGE') {
681
+ if (!res.headersSent) {
682
+ res.statusCode = 413;
683
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
684
+ }
685
+ res.end('Payload Too Large');
686
+ return;
687
+ }
688
+ if (!res.headersSent) {
689
+ res.statusCode = 500;
690
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
691
+ }
692
+ res.end('Internal Server Error');
693
+ return;
694
+ }
695
+ }
696
+ const ctx = this.acquireContext(req, res, url, match.params, query, requestId, body, files);
697
+ const overloaded = () => {
698
+ if (res.writableEnded) {
699
+ return;
700
+ }
701
+ if (!res.headersSent) {
702
+ res.statusCode = 503;
703
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
704
+ }
705
+ res.end('Server overloaded');
706
+ };
707
+ let timedOut = false;
708
+ const onTimeout = () => {
709
+ timedOut = true;
710
+ if (res.writableEnded) {
711
+ return;
712
+ }
713
+ if (!res.headersSent) {
714
+ res.statusCode = 504;
715
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
716
+ }
717
+ res.end('Request timed out');
718
+ };
719
+ const start = Date.now();
720
+ ctx.requestStart = start;
721
+ if (!this.ultraMode) {
722
+ this.metrics.onRequestStart();
723
+ }
724
+ if (this.tracer) {
725
+ const event = {
726
+ type: 'request_start',
727
+ method,
728
+ path: url.pathname,
729
+ requestId,
730
+ };
731
+ const result = this.tracer(event);
732
+ if (result && typeof result.then === 'function') {
733
+ void result;
734
+ }
735
+ }
736
+ const handle = async () => {
737
+ const handler = match.handler;
738
+ try {
739
+ if (hasRoute) {
740
+ // Optimized path: Route handler is already compiled with middlewares
741
+ const result = handler(ctx);
742
+ if (result && typeof result.then === 'function') {
743
+ await result;
744
+ }
745
+ }
746
+ else if (this.pipelineRunner) {
747
+ // Slow path: Run global middleware pipeline for 404/405
748
+ await this.pipelineRunner(ctx, handler);
749
+ }
750
+ if (!res.writableEnded) {
751
+ if (!hasRoute) {
752
+ const hasAnyMethod = allowedMethods.length > 0;
753
+ if (hasAnyMethod && this.methodNotAllowedHandler) {
754
+ const result = this.methodNotAllowedHandler(ctx, allowedMethods);
755
+ if (result &&
756
+ typeof result.then === 'function') {
757
+ await result;
758
+ }
759
+ if (!res.writableEnded && !res.headersSent) {
760
+ res.statusCode = 405;
761
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
762
+ res.end('Method Not Allowed');
763
+ }
764
+ }
765
+ else if (!hasAnyMethod && this.notFoundHandler) {
766
+ const result = this.notFoundHandler(ctx);
767
+ if (result &&
768
+ typeof result.then === 'function') {
769
+ await result;
770
+ }
771
+ if (!res.writableEnded && !res.headersSent) {
772
+ res.statusCode = 404;
773
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
774
+ res.end('Not Found');
775
+ }
776
+ }
777
+ else if (hasAnyMethod) {
778
+ if (!res.headersSent) {
779
+ res.statusCode = 405;
780
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
781
+ }
782
+ res.end('Method Not Allowed');
783
+ }
784
+ else {
785
+ if (!res.headersSent) {
786
+ res.statusCode = 404;
787
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
788
+ }
789
+ res.end('Not Found');
790
+ }
791
+ }
792
+ else if (!ctx.disableAutoEnd) {
793
+ res.end();
794
+ }
795
+ }
796
+ }
797
+ catch (err) {
798
+ await this.handleError(err, ctx);
799
+ }
800
+ };
801
+ if (this.ultraMode) {
802
+ await handle();
803
+ }
804
+ else {
805
+ await this.scheduler.run(handle, {
806
+ priority: match.priority,
807
+ onOverloaded: overloaded,
808
+ timeoutMs: this.options.requestTimeoutMs,
809
+ onTimeout,
810
+ });
811
+ }
812
+ const duration = Date.now() - start;
813
+ if (!this.ultraMode) {
814
+ this.metrics.onRequestEnd(duration, res.statusCode);
815
+ if (timedOut) {
816
+ this.metrics.onTimeout();
817
+ }
818
+ }
819
+ if (this.tracer) {
820
+ const event = {
821
+ type: 'request_end',
822
+ method,
823
+ path: url.pathname,
824
+ statusCode: res.statusCode,
825
+ durationMs: duration,
826
+ requestId,
827
+ };
828
+ const result = this.tracer(event);
829
+ if (result && typeof result.then === 'function') {
830
+ void result;
831
+ }
832
+ }
833
+ this.releaseContext(ctx);
834
+ }
835
+ async handleUpgrade(req, socket, head) {
836
+ await this.wsManager.handleUpgrade(req, socket, head);
837
+ }
838
+ async runLifecycleHooks(hooks) {
839
+ if (hooks.length === 0) {
840
+ return;
841
+ }
842
+ for (const hook of hooks) {
843
+ try {
844
+ const result = hook();
845
+ if (result && typeof result.then === 'function') {
846
+ await result;
847
+ }
848
+ }
849
+ catch {
850
+ // Ignore hook errors to avoid impacting core server flow
851
+ }
852
+ }
853
+ }
854
+ async handleError(err, ctx) {
855
+ const res = ctx.res;
856
+ if (res.writableEnded) {
857
+ return;
858
+ }
859
+ if (this.errorHandler) {
860
+ try {
861
+ const result = this.errorHandler(err, ctx);
862
+ if (result && typeof result.then === 'function') {
863
+ await result;
864
+ }
865
+ if (res.writableEnded) {
866
+ return;
867
+ }
868
+ }
869
+ catch {
870
+ // Fall through to default error handling below
871
+ }
872
+ }
873
+ if (res.writableEnded) {
874
+ return;
875
+ }
876
+ if (err instanceof types_1.HttpError) {
877
+ if (!res.headersSent) {
878
+ res.statusCode = err.status;
879
+ res.setHeader('content-type', 'application/json; charset=utf-8');
880
+ }
881
+ const payload = {
882
+ error: {
883
+ message: err.message,
884
+ code: err.code ?? 'HTTP_ERROR',
885
+ details: err.details,
886
+ },
887
+ };
888
+ const serializer = this.options.jsonSerializer;
889
+ const body = serializer !== undefined ? serializer(payload) : JSON.stringify(payload);
890
+ res.end(body);
891
+ return;
892
+ }
893
+ if (!res.headersSent) {
894
+ res.statusCode = 500;
895
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
896
+ }
897
+ res.end('Internal Server Error');
898
+ }
899
+ generateRequestId() {
900
+ const id = this.nextRequestId;
901
+ this.nextRequestId += 1;
902
+ return `${Date.now().toString(36)}-${id.toString(36)}`;
903
+ }
904
+ }
905
+ exports.QHTTPX = QHTTPX;