qhttpx 1.9.3 → 2.0.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.
- package/CHANGELOG.md +27 -0
- package/README.md +17 -12
- package/dist/examples/api-server.js +38 -35
- package/dist/examples/basic.js +3 -4
- package/dist/examples/compression.js +6 -8
- package/dist/examples/cors.js +5 -6
- package/dist/examples/errors.js +12 -11
- package/dist/examples/file-upload.js +4 -6
- package/dist/examples/fusion.js +6 -6
- package/dist/examples/rate-limiting.js +10 -10
- package/dist/examples/validation.js +5 -6
- package/dist/examples/websockets.js +3 -4
- package/dist/package.json +3 -8
- package/dist/src/benchmarks/quantam-users.js +2 -2
- package/dist/src/benchmarks/quick-bench.js +57 -0
- package/dist/src/benchmarks/simple-json.js +133 -22
- package/dist/src/benchmarks/ultra-mode.js +8 -38
- package/dist/src/core/context-pool.d.ts +12 -0
- package/dist/src/core/context-pool.js +34 -0
- package/dist/src/core/fusion.js +0 -2
- package/dist/src/core/metrics.d.ts +1 -0
- package/dist/src/core/metrics.js +3 -0
- package/dist/src/core/scheduler.d.ts +4 -0
- package/dist/src/core/scheduler.js +75 -34
- package/dist/src/core/scope.d.ts +23 -8
- package/dist/src/core/scope.js +53 -14
- package/dist/src/core/serializer.d.ts +1 -1
- package/dist/src/core/serializer.js +45 -7
- package/dist/src/core/server.d.ts +51 -10
- package/dist/src/core/server.js +695 -259
- package/dist/src/core/timer.d.ts +11 -0
- package/dist/src/core/timer.js +29 -0
- package/dist/src/core/types.d.ts +64 -12
- package/dist/src/core/types.js +6 -6
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.js +19 -18
- package/dist/src/middleware/presets.d.ts +1 -2
- package/dist/src/middleware/presets.js +1 -1
- package/dist/src/middleware/security.d.ts +2 -13
- package/dist/src/middleware/security.js +6 -1
- package/dist/src/router/radix-tree.d.ts +5 -2
- package/dist/src/router/radix-tree.js +58 -14
- package/dist/src/router/router.d.ts +5 -2
- package/dist/src/router/router.js +80 -63
- package/dist/src/utils/logger.d.ts +1 -11
- package/dist/tests/fusion.test.js +4 -4
- package/dist/tests/rate-limit.test.js +2 -2
- package/dist/tests/schema-routes.test.js +3 -1
- package/docs/AEGIS.md +18 -28
- package/docs/BENCHMARKS.md +8 -6
- package/docs/DATABASE.md +4 -4
- package/docs/MIDDLEWARE.md +3 -3
- package/docs/ROUTING.md +21 -13
- package/docs/VALIDATION.md +9 -31
- package/package.json +3 -8
- package/binding.gyp +0 -18
- package/dist/src/benchmarks/compare-frameworks.js +0 -119
- package/dist/src/benchmarks/compare.js +0 -288
- package/dist/src/buffer-pool.js +0 -70
- package/dist/src/config.js +0 -50
- package/dist/src/cookies.js +0 -59
- package/dist/src/core/native-adapter.d.ts +0 -11
- package/dist/src/core/native-adapter.js +0 -211
- package/dist/src/cors.js +0 -66
- package/dist/src/logger.js +0 -45
- package/dist/src/metrics.js +0 -111
- package/dist/src/native/index.d.ts +0 -32
- package/dist/src/native/index.js +0 -141
- package/dist/src/presets.js +0 -33
- package/dist/src/radix-router.js +0 -89
- package/dist/src/radix-tree.js +0 -81
- package/dist/src/resources.js +0 -25
- package/dist/src/router.js +0 -138
- package/dist/src/scheduler.js +0 -85
- package/dist/src/security.js +0 -69
- package/dist/src/server.js +0 -685
- package/dist/src/signals.js +0 -31
- package/dist/src/static.js +0 -107
- package/dist/src/stream.js +0 -71
- package/dist/src/tasks.js +0 -87
- package/dist/src/testing.js +0 -40
- package/dist/src/types.js +0 -19
- package/dist/src/utils/testing.js +0 -40
- package/dist/src/worker-queue.js +0 -73
- package/dist/tests/native-adapter.test.d.ts +0 -1
- package/dist/tests/native-adapter.test.js +0 -71
- 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 +0 -26
- package/src/native/README.md +0 -31
- package/src/native/addon.cc +0 -8
- package/src/native/index.ts +0 -158
- package/src/native/picohttpparser.c +0 -608
- package/src/native/picohttpparser.h +0 -76
- package/src/native/server.cc +0 -264
- package/src/native/server.h +0 -30
- /package/dist/src/benchmarks/{compare.d.ts → quick-bench.d.ts} +0 -0
package/dist/src/core/server.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -21,15 +54,43 @@ const serializer_1 = require("./serializer");
|
|
|
21
54
|
const batch_1 = require("./batch");
|
|
22
55
|
const fusion_1 = require("./fusion");
|
|
23
56
|
const simple_1 = require("../validation/simple");
|
|
57
|
+
const zod_1 = require("../validation/zod");
|
|
24
58
|
const generator_1 = require("../openapi/generator");
|
|
59
|
+
const cors_1 = require("../middleware/cors");
|
|
60
|
+
const rate_limit_1 = require("../middleware/rate-limit");
|
|
61
|
+
const logger_1 = require("../utils/logger");
|
|
62
|
+
const security_1 = require("../middleware/security");
|
|
63
|
+
const compression_1 = require("../middleware/compression");
|
|
25
64
|
const types_1 = require("./types");
|
|
26
65
|
const cookies_1 = require("../utils/cookies");
|
|
66
|
+
const context_pool_1 = require("./context-pool");
|
|
27
67
|
const scope_1 = require("./scope");
|
|
28
|
-
const
|
|
68
|
+
const logger_2 = require("./logger");
|
|
69
|
+
const timer_1 = require("./timer");
|
|
70
|
+
const serializer_2 = require("./serializer");
|
|
29
71
|
const EMPTY_QUERY = Object.freeze({});
|
|
72
|
+
const CONTENT_TYPE = {
|
|
73
|
+
JSON: 'application/json; charset=utf-8',
|
|
74
|
+
HTML: 'text/html; charset=utf-8',
|
|
75
|
+
PLAIN: 'text/plain; charset=utf-8',
|
|
76
|
+
};
|
|
30
77
|
class QHTTPXContextImpl {
|
|
78
|
+
get params() { return this._params; }
|
|
79
|
+
set params(v) { this._params = v; this._dirty |= 0b0010; }
|
|
80
|
+
get body() { return this._body; }
|
|
81
|
+
set body(v) { this._body = v; this._dirty |= 0b0001; }
|
|
82
|
+
get files() { return this._files; }
|
|
83
|
+
set files(v) { this._files = v; this._dirty |= 0b10000; }
|
|
84
|
+
// Ultra-Simple API
|
|
85
|
+
httpError(status, message, details) {
|
|
86
|
+
return new types_1.HttpError(status, message, { details });
|
|
87
|
+
}
|
|
31
88
|
constructor(app) {
|
|
32
89
|
this._url = null;
|
|
90
|
+
// Dirty tracking for optimization
|
|
91
|
+
this._generation = 0;
|
|
92
|
+
this._dirty = 0;
|
|
93
|
+
this._customHeaders = null;
|
|
33
94
|
this._app = app;
|
|
34
95
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
96
|
this._appJsonSerializer = app.options.jsonSerializer;
|
|
@@ -40,6 +101,136 @@ class QHTTPXContextImpl {
|
|
|
40
101
|
this.setCookie = this.setCookie.bind(this);
|
|
41
102
|
this.render = this.render.bind(this);
|
|
42
103
|
this.validate = this.validate.bind(this);
|
|
104
|
+
this.httpError = this.httpError.bind(this);
|
|
105
|
+
}
|
|
106
|
+
get cookies() {
|
|
107
|
+
if (this._dirty & 0b1000)
|
|
108
|
+
return this._cookies;
|
|
109
|
+
if (!this._cookies)
|
|
110
|
+
this._cookies = Object.create(null);
|
|
111
|
+
const header = this.req.headers.cookie;
|
|
112
|
+
if (header) {
|
|
113
|
+
this._parseCookies(header);
|
|
114
|
+
}
|
|
115
|
+
this._dirty |= 0b1000;
|
|
116
|
+
return this._cookies;
|
|
117
|
+
}
|
|
118
|
+
set cookies(v) {
|
|
119
|
+
this._cookies = v;
|
|
120
|
+
this._dirty |= 0b1000;
|
|
121
|
+
}
|
|
122
|
+
get query() {
|
|
123
|
+
if (this._dirty & 0b0100)
|
|
124
|
+
return this._query;
|
|
125
|
+
const url = this.req.url || '/';
|
|
126
|
+
const queryIndex = url.indexOf('?');
|
|
127
|
+
if (queryIndex !== -1) {
|
|
128
|
+
const queryString = url.slice(queryIndex + 1);
|
|
129
|
+
if (queryString.length > 0) {
|
|
130
|
+
this._parseQuery(queryString);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
if (!this._query)
|
|
134
|
+
this._query = Object.create(null);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
if (!this._query)
|
|
139
|
+
this._query = Object.create(null);
|
|
140
|
+
}
|
|
141
|
+
this._dirty |= 0b0100;
|
|
142
|
+
return this._query;
|
|
143
|
+
}
|
|
144
|
+
set query(v) {
|
|
145
|
+
this._query = v;
|
|
146
|
+
this._dirty |= 0b0100;
|
|
147
|
+
}
|
|
148
|
+
_parseQuery(queryString) {
|
|
149
|
+
if (!this._query)
|
|
150
|
+
this._query = Object.create(null);
|
|
151
|
+
const query = this._query;
|
|
152
|
+
let i = 0;
|
|
153
|
+
const len = queryString.length;
|
|
154
|
+
while (i < len) {
|
|
155
|
+
let keyStart = i;
|
|
156
|
+
let keyEnd = -1;
|
|
157
|
+
let valStart = -1;
|
|
158
|
+
let valEnd = -1;
|
|
159
|
+
// Find key
|
|
160
|
+
while (i < len) {
|
|
161
|
+
const c = queryString.charCodeAt(i);
|
|
162
|
+
if (c === 61) { // =
|
|
163
|
+
keyEnd = i;
|
|
164
|
+
valStart = i + 1;
|
|
165
|
+
i++;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (c === 38) { // &
|
|
169
|
+
keyEnd = i;
|
|
170
|
+
valEnd = i; // empty value
|
|
171
|
+
i++;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
i++;
|
|
175
|
+
}
|
|
176
|
+
if (keyEnd === -1) {
|
|
177
|
+
keyEnd = i;
|
|
178
|
+
valEnd = i; // empty value
|
|
179
|
+
}
|
|
180
|
+
// Find value
|
|
181
|
+
if (valStart !== -1) {
|
|
182
|
+
while (i < len) {
|
|
183
|
+
if (queryString.charCodeAt(i) === 38) { // &
|
|
184
|
+
valEnd = i;
|
|
185
|
+
i++;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
if (valEnd === -1)
|
|
191
|
+
valEnd = i;
|
|
192
|
+
}
|
|
193
|
+
const key = decodeURIComponent(queryString.slice(keyStart, keyEnd));
|
|
194
|
+
const val = valStart !== -1 ? decodeURIComponent(queryString.slice(valStart, valEnd)) : '';
|
|
195
|
+
if (query[key] === undefined) {
|
|
196
|
+
query[key] = val;
|
|
197
|
+
}
|
|
198
|
+
else if (Array.isArray(query[key])) {
|
|
199
|
+
query[key].push(val);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
query[key] = [query[key], val];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
_parseCookies(header) {
|
|
207
|
+
const list = this._cookies;
|
|
208
|
+
let start = 0;
|
|
209
|
+
let end = header.length;
|
|
210
|
+
// Inline cookie parsing for speed
|
|
211
|
+
while (start < end) {
|
|
212
|
+
let eqIdx = header.indexOf('=', start);
|
|
213
|
+
if (eqIdx === -1)
|
|
214
|
+
break;
|
|
215
|
+
let semiIdx = header.indexOf(';', start);
|
|
216
|
+
if (semiIdx === -1)
|
|
217
|
+
semiIdx = end;
|
|
218
|
+
if (eqIdx < semiIdx) {
|
|
219
|
+
const key = header.slice(start, eqIdx).trim();
|
|
220
|
+
let val = header.slice(eqIdx + 1, semiIdx).trim();
|
|
221
|
+
// Decode if needed, simple check first
|
|
222
|
+
if (val.indexOf('%') !== -1) {
|
|
223
|
+
try {
|
|
224
|
+
val = decodeURIComponent(val);
|
|
225
|
+
}
|
|
226
|
+
catch { /* ignore */ }
|
|
227
|
+
}
|
|
228
|
+
if (!list[key]) {
|
|
229
|
+
list[key] = val;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
start = semiIdx + 1;
|
|
233
|
+
}
|
|
43
234
|
}
|
|
44
235
|
get bufferPool() {
|
|
45
236
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -52,11 +243,13 @@ class QHTTPXContextImpl {
|
|
|
52
243
|
get state() {
|
|
53
244
|
if (!this._state) {
|
|
54
245
|
this._state = {};
|
|
246
|
+
this._dirty |= 0b100000;
|
|
55
247
|
}
|
|
56
248
|
return this._state;
|
|
57
249
|
}
|
|
58
250
|
set state(v) {
|
|
59
251
|
this._state = v;
|
|
252
|
+
this._dirty |= 0b100000;
|
|
60
253
|
}
|
|
61
254
|
get ip() {
|
|
62
255
|
if (this._ip)
|
|
@@ -88,12 +281,12 @@ class QHTTPXContextImpl {
|
|
|
88
281
|
body = this._appJsonSerializer(payload);
|
|
89
282
|
}
|
|
90
283
|
else {
|
|
91
|
-
//
|
|
92
|
-
body =
|
|
284
|
+
// Use optimized fast path serializer
|
|
285
|
+
body = (0, serializer_2.fastJsonStringify)(payload);
|
|
93
286
|
}
|
|
94
287
|
if (!res.headersSent) {
|
|
95
288
|
res.statusCode = status;
|
|
96
|
-
res.setHeader('content-type',
|
|
289
|
+
res.setHeader('content-type', CONTENT_TYPE.JSON);
|
|
97
290
|
}
|
|
98
291
|
res.end(body);
|
|
99
292
|
}
|
|
@@ -108,7 +301,7 @@ class QHTTPXContextImpl {
|
|
|
108
301
|
const res = this.res;
|
|
109
302
|
if (!res.headersSent) {
|
|
110
303
|
res.statusCode = status;
|
|
111
|
-
res.setHeader('content-type',
|
|
304
|
+
res.setHeader('content-type', CONTENT_TYPE.HTML);
|
|
112
305
|
}
|
|
113
306
|
res.end(payload);
|
|
114
307
|
}
|
|
@@ -148,7 +341,7 @@ class QHTTPXContextImpl {
|
|
|
148
341
|
const res = this.res;
|
|
149
342
|
if (!res.headersSent) {
|
|
150
343
|
res.statusCode = 200;
|
|
151
|
-
res.setHeader('content-type',
|
|
344
|
+
res.setHeader('content-type', CONTENT_TYPE.HTML);
|
|
152
345
|
}
|
|
153
346
|
res.end(html);
|
|
154
347
|
}
|
|
@@ -164,47 +357,66 @@ class QHTTPXContextImpl {
|
|
|
164
357
|
});
|
|
165
358
|
}
|
|
166
359
|
reset() {
|
|
360
|
+
// Tiered reset based on what was actually used
|
|
361
|
+
if (this._dirty & 0b0001)
|
|
362
|
+
this._body = undefined;
|
|
363
|
+
if (this._dirty & 0b0010)
|
|
364
|
+
this._params = null;
|
|
365
|
+
// Recycle query object
|
|
366
|
+
if (this._dirty & 0b0100) {
|
|
367
|
+
if (this._query) {
|
|
368
|
+
for (const key in this._query)
|
|
369
|
+
delete this._query[key];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Recycle cookies object
|
|
373
|
+
if (this._dirty & 0b1000) {
|
|
374
|
+
if (this._cookies) {
|
|
375
|
+
for (const key in this._cookies)
|
|
376
|
+
delete this._cookies[key];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (this._dirty & 0b10000)
|
|
380
|
+
this._files = undefined;
|
|
381
|
+
if (this._dirty & 0b100000)
|
|
382
|
+
this._state = undefined;
|
|
383
|
+
this._dirty = 0;
|
|
167
384
|
this.req = null;
|
|
168
385
|
this.res = null;
|
|
169
386
|
this.headers = null;
|
|
170
387
|
this._url = null;
|
|
171
388
|
this.method = null;
|
|
172
389
|
this._ip = undefined;
|
|
173
|
-
this.params = null;
|
|
174
|
-
this.query = null;
|
|
175
|
-
this.body = undefined;
|
|
176
|
-
this.files = undefined;
|
|
177
|
-
this.cookies = null;
|
|
178
|
-
this._state = undefined;
|
|
179
390
|
this.requestId = undefined;
|
|
180
391
|
this.requestStart = undefined;
|
|
181
392
|
this.serializer = undefined;
|
|
182
393
|
this.path = '';
|
|
183
394
|
this.disableAutoEnd = false;
|
|
184
395
|
this.error = undefined;
|
|
396
|
+
this.next = undefined;
|
|
185
397
|
}
|
|
186
398
|
}
|
|
187
399
|
exports.QHTTPXContextImpl = QHTTPXContextImpl;
|
|
188
400
|
class QHTTPX {
|
|
189
401
|
constructor(options = {}) {
|
|
190
402
|
this.middlewares = [];
|
|
191
|
-
this.contextPool = [];
|
|
192
403
|
this.pipelineRunner = null;
|
|
193
404
|
this.onStartHooks = [];
|
|
194
405
|
this.onBeforeShutdownHooks = [];
|
|
195
406
|
this.onShutdownHooks = [];
|
|
196
407
|
this.nextRequestId = 1;
|
|
408
|
+
this.lastDateNow = Date.now();
|
|
409
|
+
this.lastDateString = this.lastDateNow.toString(36);
|
|
197
410
|
this.requestCounter = 0;
|
|
198
411
|
this.options = options;
|
|
199
|
-
this.logger = new
|
|
412
|
+
this.logger = new logger_2.Logger({
|
|
200
413
|
name: options.name,
|
|
201
|
-
level:
|
|
414
|
+
level: 'info'
|
|
202
415
|
});
|
|
203
|
-
this.
|
|
204
|
-
this.
|
|
205
|
-
this.
|
|
206
|
-
this.
|
|
207
|
-
this.tracer = this.ultraMode ? undefined : options.tracer;
|
|
416
|
+
this.errorHandler = options.errorHandler;
|
|
417
|
+
this.notFoundHandler = options.notFoundHandler;
|
|
418
|
+
this.methodNotAllowedHandler = options.methodNotAllowedHandler;
|
|
419
|
+
this.tracer = options.tracer;
|
|
208
420
|
this.workerCount = (0, resources_1.calculateWorkerCount)(options.workers ?? 'auto');
|
|
209
421
|
this.router = new router_1.Router();
|
|
210
422
|
this.wsManager = new websocket_1.WebSocketManager(this.generateRequestId.bind(this));
|
|
@@ -219,35 +431,23 @@ class QHTTPX {
|
|
|
219
431
|
this.batchExecutor = new batch_1.BatchExecutor(options.database);
|
|
220
432
|
}
|
|
221
433
|
if (options.enableRequestFusion) {
|
|
222
|
-
this.
|
|
434
|
+
this._fusion = new fusion_1.RequestFusion(options.enableRequestFusion);
|
|
223
435
|
}
|
|
224
436
|
this.validator = options.validator ?? new simple_1.SimpleValidator();
|
|
225
|
-
this.
|
|
226
|
-
enabled:
|
|
227
|
-
}, this.tasks, this.
|
|
228
|
-
|
|
229
|
-
this.contextPool.push(this.createContext());
|
|
230
|
-
}
|
|
437
|
+
this._metrics = new metrics_1.Metrics(this.scheduler, {
|
|
438
|
+
enabled: this.options.metricsEnabled ?? true,
|
|
439
|
+
}, this.tasks, this._fusion);
|
|
440
|
+
this.contextPool = new context_pool_1.ContextPool(() => this.createContext(), (ctx) => ctx.reset(), maxConcurrency, this.poolLimit);
|
|
231
441
|
this.registerInternalRoutes();
|
|
232
442
|
this.compileMiddlewarePipeline();
|
|
233
443
|
this.server = http_1.default.createServer(this.handleRequest.bind(this));
|
|
234
444
|
if (this.options.keepAliveTimeoutMs !== undefined) {
|
|
235
445
|
this.server.keepAliveTimeout = this.options.keepAliveTimeoutMs;
|
|
236
446
|
}
|
|
237
|
-
else if (this.ultraMode) {
|
|
238
|
-
this.server.keepAliveTimeout = 0;
|
|
239
|
-
}
|
|
240
447
|
if (this.options.headersTimeoutMs !== undefined) {
|
|
241
448
|
this.server.headersTimeout = this.options.headersTimeoutMs;
|
|
242
449
|
}
|
|
243
|
-
|
|
244
|
-
this.server.headersTimeout = 0;
|
|
245
|
-
}
|
|
246
|
-
if (this.ultraMode) {
|
|
247
|
-
this.server.requestTimeout = 0;
|
|
248
|
-
this.server.maxHeadersCount = 0;
|
|
249
|
-
this.server.timeout = 0;
|
|
250
|
-
}
|
|
450
|
+
// Keep-alive and timeout settings handled by options above
|
|
251
451
|
this.server.on('upgrade', (req, socket, head) => {
|
|
252
452
|
void this.handleUpgrade(req, socket, head);
|
|
253
453
|
});
|
|
@@ -304,44 +504,94 @@ class QHTTPX {
|
|
|
304
504
|
}
|
|
305
505
|
compileMiddlewarePipeline() {
|
|
306
506
|
const middlewares = this.middlewares;
|
|
507
|
+
let runner;
|
|
307
508
|
if (middlewares.length === 0) {
|
|
308
|
-
|
|
509
|
+
runner = async (ctx, handler) => {
|
|
309
510
|
const result = handler(ctx);
|
|
310
|
-
|
|
311
|
-
|
|
511
|
+
// Handle return values
|
|
512
|
+
if (result instanceof Promise) {
|
|
513
|
+
const val = await result;
|
|
514
|
+
if (val !== undefined && !ctx.res.headersSent) {
|
|
515
|
+
if (ctx.method === 'POST' && ctx.res.statusCode === 200)
|
|
516
|
+
ctx.res.statusCode = 201;
|
|
517
|
+
if (typeof val === 'object')
|
|
518
|
+
ctx.json(val);
|
|
519
|
+
else
|
|
520
|
+
ctx.send(String(val));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else if (result !== undefined && !ctx.res.headersSent) {
|
|
524
|
+
if (ctx.method === 'POST' && ctx.res.statusCode === 200)
|
|
525
|
+
ctx.res.statusCode = 201;
|
|
526
|
+
if (typeof result === 'object')
|
|
527
|
+
ctx.json(result);
|
|
528
|
+
else
|
|
529
|
+
ctx.send(String(result));
|
|
312
530
|
}
|
|
313
531
|
};
|
|
314
|
-
return;
|
|
315
532
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
533
|
+
else {
|
|
534
|
+
// Flatten middleware pipeline into a single function chain
|
|
535
|
+
// This eliminates Promise nesting, recursive dispatch overhead, and microtask backlogs
|
|
536
|
+
// Each middleware executes directly without closure allocation overhead
|
|
537
|
+
runner = async (ctx, handler) => {
|
|
538
|
+
let index = 0;
|
|
539
|
+
const executeNext = async () => {
|
|
540
|
+
if (index >= middlewares.length) {
|
|
541
|
+
// All middlewares done, execute handler
|
|
542
|
+
const result = handler(ctx);
|
|
543
|
+
// Handle return values (Auto-Response)
|
|
544
|
+
if (result instanceof Promise) {
|
|
545
|
+
const val = await result;
|
|
546
|
+
if (val !== undefined && !ctx.res.headersSent) {
|
|
547
|
+
if (typeof val === 'object')
|
|
548
|
+
ctx.json(val);
|
|
549
|
+
else
|
|
550
|
+
ctx.send(String(val));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else if (result !== undefined && !ctx.res.headersSent) {
|
|
554
|
+
if (typeof result === 'object')
|
|
555
|
+
ctx.json(result);
|
|
556
|
+
else
|
|
557
|
+
ctx.send(String(result));
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const currentIndex = index;
|
|
562
|
+
index += 1;
|
|
563
|
+
ctx.next = executeNext;
|
|
564
|
+
const result = middlewares[currentIndex](ctx, executeNext);
|
|
325
565
|
if (result && typeof result.then === 'function') {
|
|
326
566
|
await result;
|
|
327
567
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const currentIndex = index;
|
|
331
|
-
index += 1;
|
|
332
|
-
ctx.next = executeNext;
|
|
333
|
-
const result = middlewares[currentIndex](ctx, executeNext);
|
|
334
|
-
if (result && typeof result.then === 'function') {
|
|
335
|
-
await result;
|
|
336
|
-
}
|
|
568
|
+
};
|
|
569
|
+
await executeNext();
|
|
337
570
|
};
|
|
338
|
-
|
|
339
|
-
|
|
571
|
+
}
|
|
572
|
+
if (this._fusion) {
|
|
573
|
+
// Deprecated: Global fusion wrapping moved to per-route compilation
|
|
574
|
+
// to ensure body parsing and middleware execution happens before key generation.
|
|
575
|
+
// this.pipelineRunner = async (ctx, handler) => {
|
|
576
|
+
// await this._fusion!.coalesce(ctx, async (c) => {
|
|
577
|
+
// await runner(c, handler);
|
|
578
|
+
// });
|
|
579
|
+
// };
|
|
580
|
+
this.pipelineRunner = runner;
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
this.pipelineRunner = runner;
|
|
584
|
+
}
|
|
340
585
|
}
|
|
341
586
|
compileRoutePipeline(handler,
|
|
342
587
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
343
588
|
schema) {
|
|
344
589
|
const middlewares = this.middlewares;
|
|
590
|
+
// Optimization: If no middleware and no schema and no fusion, return handler directly
|
|
591
|
+
// This avoids wrapping for simple routes
|
|
592
|
+
if (middlewares.length === 0 && !schema && !this._fusion) {
|
|
593
|
+
return handler;
|
|
594
|
+
}
|
|
345
595
|
// Heuristic: Is it a structured RouteSchema or a legacy ResponseSchema?
|
|
346
596
|
let responseSchema;
|
|
347
597
|
let requestSchema;
|
|
@@ -404,26 +654,35 @@ class QHTTPX {
|
|
|
404
654
|
};
|
|
405
655
|
}
|
|
406
656
|
// Wrap with Request Fusion if enabled
|
|
407
|
-
if (this.
|
|
657
|
+
if (this._fusion) {
|
|
408
658
|
const inner = pipeline;
|
|
409
|
-
pipeline = (ctx) => this.
|
|
410
|
-
}
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
pipeline = (ctx) => {
|
|
417
|
-
|
|
418
|
-
|
|
659
|
+
pipeline = (ctx) => this._fusion.coalesce(ctx, inner);
|
|
660
|
+
}
|
|
661
|
+
// Optimization: Pre-compile middleware chain for this specific route
|
|
662
|
+
// This avoids closure allocation per-request and recursive dispatch overhead
|
|
663
|
+
// "Route-Specific Middleware Compilation"
|
|
664
|
+
if (middlewares.length > 0) {
|
|
665
|
+
const innerHandler = pipeline;
|
|
666
|
+
pipeline = async (ctx) => {
|
|
667
|
+
let index = 0;
|
|
668
|
+
const executeNext = async () => {
|
|
669
|
+
if (index >= middlewares.length) {
|
|
670
|
+
// All middlewares done, execute handler
|
|
671
|
+
const result = innerHandler(ctx);
|
|
672
|
+
if (result && typeof result.then === 'function') {
|
|
673
|
+
await result;
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const currentIndex = index;
|
|
678
|
+
index += 1;
|
|
679
|
+
ctx.next = executeNext;
|
|
680
|
+
const result = middlewares[currentIndex](ctx, executeNext);
|
|
419
681
|
if (result && typeof result.then === 'function') {
|
|
420
682
|
await result;
|
|
421
683
|
}
|
|
422
684
|
};
|
|
423
|
-
|
|
424
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
425
|
-
ctx.next = nextFn;
|
|
426
|
-
return middleware(ctx, nextFn);
|
|
685
|
+
await executeNext();
|
|
427
686
|
};
|
|
428
687
|
}
|
|
429
688
|
if (stringifier) {
|
|
@@ -436,8 +695,8 @@ class QHTTPX {
|
|
|
436
695
|
return pipeline;
|
|
437
696
|
}
|
|
438
697
|
// Internal registration to be accessed by Scopes
|
|
439
|
-
_registerRoute(method, path, handlerOrOptions) {
|
|
440
|
-
this.registerRoute(method, path, handlerOrOptions);
|
|
698
|
+
_registerRoute(method, path, handlerOrOptions, handlerIfOptions) {
|
|
699
|
+
this.registerRoute(method, path, handlerOrOptions, handlerIfOptions);
|
|
441
700
|
}
|
|
442
701
|
async register(plugin, options) {
|
|
443
702
|
const scope = new scope_1.QHTTPXScope(this, options?.prefix);
|
|
@@ -446,36 +705,101 @@ class QHTTPX {
|
|
|
446
705
|
registerRoute(method, path, handlerOrOptions, handlerIfOptions) {
|
|
447
706
|
let handler;
|
|
448
707
|
let schema;
|
|
708
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
709
|
+
let staticResponse;
|
|
449
710
|
const options = {};
|
|
450
711
|
if (typeof handlerOrOptions === 'function') {
|
|
451
712
|
handler = handlerOrOptions;
|
|
452
713
|
}
|
|
453
714
|
else if (handlerIfOptions) {
|
|
454
715
|
handler = handlerIfOptions;
|
|
455
|
-
|
|
456
|
-
options
|
|
716
|
+
const config = handlerOrOptions;
|
|
717
|
+
// Extract schema from flattened options or use explicit schema
|
|
718
|
+
if (config.schema) {
|
|
719
|
+
schema = config.schema;
|
|
720
|
+
}
|
|
721
|
+
else if (config.body || config.params || config.query || config.headers || config.response) {
|
|
722
|
+
schema = {
|
|
723
|
+
body: config.body,
|
|
724
|
+
params: config.params,
|
|
725
|
+
query: config.query,
|
|
726
|
+
headers: config.headers,
|
|
727
|
+
response: config.response
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
options.priority = config.priority;
|
|
731
|
+
staticResponse = config.staticResponse;
|
|
457
732
|
}
|
|
458
733
|
else {
|
|
459
734
|
const opts = handlerOrOptions;
|
|
460
|
-
|
|
461
|
-
|
|
735
|
+
if (opts.handler) {
|
|
736
|
+
handler = opts.handler;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
throw new Error(`Handler is required for route ${method} ${path}`);
|
|
740
|
+
}
|
|
741
|
+
// Extract schema from flattened options or use explicit schema
|
|
742
|
+
if (opts.schema) {
|
|
743
|
+
schema = opts.schema;
|
|
744
|
+
}
|
|
745
|
+
else if (opts.body || opts.params || opts.query || opts.headers || opts.response) {
|
|
746
|
+
schema = {
|
|
747
|
+
body: opts.body,
|
|
748
|
+
params: opts.params,
|
|
749
|
+
query: opts.query,
|
|
750
|
+
headers: opts.headers,
|
|
751
|
+
response: opts.response
|
|
752
|
+
};
|
|
753
|
+
}
|
|
462
754
|
options.priority = opts.priority;
|
|
755
|
+
staticResponse = opts.staticResponse;
|
|
756
|
+
}
|
|
757
|
+
// Nuclear Optimization: Static Response Pre-building
|
|
758
|
+
if (staticResponse !== undefined) {
|
|
759
|
+
const jsonStr = JSON.stringify(staticResponse);
|
|
760
|
+
const buffer = Buffer.from(jsonStr);
|
|
761
|
+
const length = buffer.length;
|
|
762
|
+
// Replace handler with optimized one that bypasses serialization
|
|
763
|
+
handler = (ctx) => {
|
|
764
|
+
ctx.res.writeHead(200, {
|
|
765
|
+
'Content-Type': 'application/json',
|
|
766
|
+
'Content-Length': String(length),
|
|
767
|
+
});
|
|
768
|
+
ctx.res.end(buffer);
|
|
769
|
+
};
|
|
463
770
|
}
|
|
464
771
|
const compiled = this.compileRoutePipeline(handler, schema);
|
|
772
|
+
// Pass the compiled pipeline as metadata to the router
|
|
773
|
+
// This allows match results to contain the full execution chain directly
|
|
774
|
+
// The router will still perform matching based on path, but the resulting
|
|
775
|
+
// RouteMatch will have this pipeline readily available
|
|
776
|
+
options.metadata = {
|
|
777
|
+
needsQuery: false, // Calculated by Router.detectMetadata if not provided
|
|
778
|
+
needsCookies: false,
|
|
779
|
+
needsBody: false,
|
|
780
|
+
pipeline: compiled
|
|
781
|
+
};
|
|
465
782
|
this.router.register(method, path, compiled, { ...options, schema });
|
|
466
783
|
}
|
|
467
784
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
468
785
|
get(path, arg1, arg2) {
|
|
469
786
|
this.registerRoute('GET', path, arg1, arg2);
|
|
470
787
|
}
|
|
471
|
-
post(path, handlerOrOptions,
|
|
472
|
-
|
|
788
|
+
post(path, handlerOrOptions, handlerOrConfig) {
|
|
789
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
790
|
+
this.registerRoute('POST', path, handlerOrOptions, handlerOrConfig);
|
|
473
791
|
}
|
|
474
|
-
put(path, handlerOrOptions,
|
|
475
|
-
|
|
792
|
+
put(path, handlerOrOptions, handlerOrConfig) {
|
|
793
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
794
|
+
this.registerRoute('PUT', path, handlerOrOptions, handlerOrConfig);
|
|
476
795
|
}
|
|
477
|
-
delete(path, handlerOrOptions,
|
|
478
|
-
|
|
796
|
+
delete(path, handlerOrOptions, handlerOrConfig) {
|
|
797
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
798
|
+
this.registerRoute('DELETE', path, handlerOrOptions, handlerOrConfig);
|
|
799
|
+
}
|
|
800
|
+
patch(path, handlerOrOptions, handlerOrConfig) {
|
|
801
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
802
|
+
this.registerRoute('PATCH', path, handlerOrOptions, handlerOrConfig);
|
|
479
803
|
}
|
|
480
804
|
route(path) {
|
|
481
805
|
const register = (method, handler) => {
|
|
@@ -519,26 +843,26 @@ class QHTTPX {
|
|
|
519
843
|
this.batchExecutor.register(name, handler);
|
|
520
844
|
}
|
|
521
845
|
registerInternalRoutes() {
|
|
522
|
-
this.router.register('GET', '/__qhttpx/health', (
|
|
846
|
+
this.router.register('GET', '/__qhttpx/health', ({ json }) => {
|
|
523
847
|
const version = typeof package_json_1.default.version === 'string' ? package_json_1.default.version : '';
|
|
524
848
|
const name = typeof package_json_1.default.name === 'string' ? package_json_1.default.name : '';
|
|
525
|
-
|
|
849
|
+
json({
|
|
526
850
|
status: 'ok',
|
|
527
851
|
name,
|
|
528
852
|
version,
|
|
529
853
|
workers: this.workerCount,
|
|
530
854
|
});
|
|
531
855
|
});
|
|
532
|
-
this.router.register('GET', '/__qhttpx/metrics', (
|
|
533
|
-
const snapshot = this.
|
|
534
|
-
|
|
856
|
+
this.router.register('GET', '/__qhttpx/metrics', ({ json }) => {
|
|
857
|
+
const snapshot = this._metrics.snapshot();
|
|
858
|
+
json({
|
|
535
859
|
...snapshot,
|
|
536
860
|
workers: this.workerCount,
|
|
537
861
|
});
|
|
538
862
|
});
|
|
539
|
-
this.router.register('GET', '/__qhttpx/runtime', (
|
|
863
|
+
this.router.register('GET', '/__qhttpx/runtime', ({ json }) => {
|
|
540
864
|
const schedulerStats = this.scheduler.getStats();
|
|
541
|
-
|
|
865
|
+
json({
|
|
542
866
|
workers: this.workerCount,
|
|
543
867
|
router: {
|
|
544
868
|
frozen: this.router.isFrozenRouter(),
|
|
@@ -551,6 +875,7 @@ class QHTTPX {
|
|
|
551
875
|
? this.options.enableBatching.endpoint
|
|
552
876
|
: '/qhttpx';
|
|
553
877
|
this.router.register('POST', endpoint, async (ctx) => {
|
|
878
|
+
// Keep full ctx here as we pass it to handleBatch
|
|
554
879
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
555
880
|
const body = ctx.body;
|
|
556
881
|
if (!body || !Array.isArray(body.batch)) {
|
|
@@ -568,11 +893,187 @@ class QHTTPX {
|
|
|
568
893
|
});
|
|
569
894
|
}
|
|
570
895
|
}
|
|
896
|
+
// Sugar Methods for Fluent API
|
|
897
|
+
database(manager) {
|
|
898
|
+
this.options.database = manager;
|
|
899
|
+
// Auto-connect?
|
|
900
|
+
// manager.connect() is async.
|
|
901
|
+
// We can't await here.
|
|
902
|
+
// Maybe we queue it?
|
|
903
|
+
// But QHTTPX doesn't have a queue for DB connection.
|
|
904
|
+
// The user might need to await app.start().
|
|
905
|
+
// We can hook into onStart.
|
|
906
|
+
this.onStart(async () => {
|
|
907
|
+
await manager.connect();
|
|
908
|
+
});
|
|
909
|
+
return this;
|
|
910
|
+
}
|
|
911
|
+
security(options) {
|
|
912
|
+
// 1. CORS
|
|
913
|
+
let corsOpts = options?.cors;
|
|
914
|
+
if (corsOpts === undefined)
|
|
915
|
+
corsOpts = true;
|
|
916
|
+
if (corsOpts !== false) {
|
|
917
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
918
|
+
const opts = corsOpts === true ? {} : corsOpts;
|
|
919
|
+
this.use((0, cors_1.createCorsMiddleware)(opts));
|
|
920
|
+
}
|
|
921
|
+
// 2. Security Headers
|
|
922
|
+
this.use((0, security_1.createSecurityHeadersMiddleware)(options?.securityHeaders));
|
|
923
|
+
// 3. Rate Limit (Default 100 req/15min)
|
|
924
|
+
const rateLimitOpts = options?.rateLimit || {};
|
|
925
|
+
this.use((0, rate_limit_1.rateLimit)({
|
|
926
|
+
windowMs: 15 * 60 * 1000,
|
|
927
|
+
max: 100,
|
|
928
|
+
...rateLimitOpts
|
|
929
|
+
}));
|
|
930
|
+
return this;
|
|
931
|
+
}
|
|
932
|
+
log(options) {
|
|
933
|
+
const loggerOptions = typeof options === 'object' ? options : {};
|
|
934
|
+
this.use((0, logger_1.createLoggerMiddleware)(loggerOptions));
|
|
935
|
+
this.options.logging = options || true;
|
|
936
|
+
return this;
|
|
937
|
+
}
|
|
938
|
+
validate(validator) {
|
|
939
|
+
if (validator) {
|
|
940
|
+
this.options.validator = validator;
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
// Default to ZodValidator
|
|
944
|
+
this.options.validator = new zod_1.ZodValidator();
|
|
945
|
+
}
|
|
946
|
+
return this;
|
|
947
|
+
}
|
|
948
|
+
production() {
|
|
949
|
+
this.options.performanceMode = 'default';
|
|
950
|
+
this.use((0, compression_1.createCompressionMiddleware)());
|
|
951
|
+
// Auto-Cluster
|
|
952
|
+
// Note: Clustering usually needs to be at the process entry point.
|
|
953
|
+
// But we can check if we are primary and fork.
|
|
954
|
+
// For now, let's just enable optimizations.
|
|
955
|
+
// We can add a helper or documentation that 'app.start()' handles clustering if production() was called?
|
|
956
|
+
// Or better, app.start() can check options.performanceMode.
|
|
957
|
+
return this;
|
|
958
|
+
}
|
|
959
|
+
routes(prefix, plugin) {
|
|
960
|
+
this.register(plugin, { prefix });
|
|
961
|
+
return this;
|
|
962
|
+
}
|
|
963
|
+
// Ultra-Simple API: Fluent Rate Limit
|
|
964
|
+
rateLimit(option, interval) {
|
|
965
|
+
if (typeof option === 'string') {
|
|
966
|
+
// Preset
|
|
967
|
+
const presets = {
|
|
968
|
+
'strict': { windowMs: 15 * 60 * 1000, max: 10 },
|
|
969
|
+
'standard': { windowMs: 15 * 60 * 1000, max: 100 },
|
|
970
|
+
'relaxed': { windowMs: 60 * 60 * 1000, max: 1000 }
|
|
971
|
+
};
|
|
972
|
+
const config = presets[option] || presets['standard'];
|
|
973
|
+
this.use((0, rate_limit_1.rateLimit)(config));
|
|
974
|
+
}
|
|
975
|
+
else if (typeof option === 'number') {
|
|
976
|
+
// app.rateLimit(100, "1m");
|
|
977
|
+
let ms = 60 * 1000;
|
|
978
|
+
if (interval) {
|
|
979
|
+
if (interval === 'minute' || interval === '1m')
|
|
980
|
+
ms = 60 * 1000;
|
|
981
|
+
if (interval === 'hour' || interval === '1h')
|
|
982
|
+
ms = 60 * 60 * 1000;
|
|
983
|
+
if (interval === 'second' || interval === '1s')
|
|
984
|
+
ms = 1000;
|
|
985
|
+
}
|
|
986
|
+
this.use((0, rate_limit_1.rateLimit)({
|
|
987
|
+
max: option,
|
|
988
|
+
windowMs: ms
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
991
|
+
return this;
|
|
992
|
+
}
|
|
993
|
+
allow(count) {
|
|
994
|
+
return {
|
|
995
|
+
per: (interval) => {
|
|
996
|
+
let ms = 60 * 1000;
|
|
997
|
+
if (interval === 'minute' || interval === '1m')
|
|
998
|
+
ms = 60 * 1000;
|
|
999
|
+
if (interval === 'hour' || interval === '1h')
|
|
1000
|
+
ms = 60 * 60 * 1000;
|
|
1001
|
+
if (interval === 'second' || interval === '1s')
|
|
1002
|
+
ms = 1000;
|
|
1003
|
+
this.use((0, rate_limit_1.rateLimit)({
|
|
1004
|
+
max: count,
|
|
1005
|
+
windowMs: ms
|
|
1006
|
+
}));
|
|
1007
|
+
return this;
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
fusion(enable = true) {
|
|
1012
|
+
if (enable === false) {
|
|
1013
|
+
// We can't easily remove it if it's there, but we can disable it in logic if we had a flag.
|
|
1014
|
+
// But for now, let's assume this is for enabling.
|
|
1015
|
+
return this;
|
|
1016
|
+
}
|
|
1017
|
+
if (!this._fusion) {
|
|
1018
|
+
// Lazy load the class to avoid circular dep issues if any (though imported at top)
|
|
1019
|
+
// But we already import RequestFusion.
|
|
1020
|
+
// We need to set it to this.fusion (which is readonly in TS, but we can cast or ignore)
|
|
1021
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1022
|
+
this._fusion = new fusion_1.RequestFusion(enable);
|
|
1023
|
+
// Re-compile pipeline to include fusion wrapper if needed
|
|
1024
|
+
// Actually, fusion is applied per-route in compileRoutePipeline.
|
|
1025
|
+
// But we might need to re-compile existing routes?
|
|
1026
|
+
// Usually configuration happens before routes.
|
|
1027
|
+
}
|
|
1028
|
+
return this;
|
|
1029
|
+
}
|
|
1030
|
+
bodyLimit(bytes) {
|
|
1031
|
+
this.options.maxBodyBytes = bytes;
|
|
1032
|
+
return this;
|
|
1033
|
+
}
|
|
1034
|
+
metrics(enable = true) {
|
|
1035
|
+
this.options.metricsEnabled = enable;
|
|
1036
|
+
// Metrics instance checks this.options.metricsEnabled in some places?
|
|
1037
|
+
// The Metrics class is initialized in constructor.
|
|
1038
|
+
// We might need to update the metrics instance.
|
|
1039
|
+
// But looking at Metrics class, it takes options in constructor.
|
|
1040
|
+
// However, we can probably update the internal state if needed.
|
|
1041
|
+
// For now, let's assume setting options is enough or we might need to expose a method on Metrics.
|
|
1042
|
+
// Re-initializing metrics might be complex.
|
|
1043
|
+
// Let's assume the user calls this before start.
|
|
1044
|
+
return this;
|
|
1045
|
+
}
|
|
1046
|
+
error(status, message, details) {
|
|
1047
|
+
return new types_1.HttpError(status, message, { details });
|
|
1048
|
+
}
|
|
1049
|
+
start(port) {
|
|
1050
|
+
return this.listen(port);
|
|
1051
|
+
}
|
|
571
1052
|
getOpenAPI(options) {
|
|
572
1053
|
const generator = new generator_1.OpenAPIGenerator(this.router, options);
|
|
573
1054
|
return generator.generate();
|
|
574
1055
|
}
|
|
575
1056
|
async listen(port, hostnameOrCallback, callback) {
|
|
1057
|
+
// Ultra-Simple API: Clustering support in Production
|
|
1058
|
+
if (this.options.performanceMode === 'default' && !process.env.QHTTPX_NO_CLUSTER) {
|
|
1059
|
+
const cluster = await Promise.resolve().then(() => __importStar(require('cluster')));
|
|
1060
|
+
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
|
1061
|
+
if (cluster.default.isPrimary) {
|
|
1062
|
+
const numCPUs = os.cpus().length;
|
|
1063
|
+
console.log(`Primary ${process.pid} is running. Forking ${numCPUs} workers...`);
|
|
1064
|
+
for (let i = 0; i < numCPUs; i++) {
|
|
1065
|
+
cluster.default.fork();
|
|
1066
|
+
}
|
|
1067
|
+
cluster.default.on('exit', (worker) => {
|
|
1068
|
+
console.log(`worker ${worker.process.pid} died`);
|
|
1069
|
+
});
|
|
1070
|
+
// Return a dummy promise for primary, it keeps running
|
|
1071
|
+
return new Promise(() => { });
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
// Worker process falls through to normal listen
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
576
1077
|
let hostname;
|
|
577
1078
|
let cb;
|
|
578
1079
|
if (typeof hostnameOrCallback === 'function') {
|
|
@@ -636,8 +1137,8 @@ class QHTTPX {
|
|
|
636
1137
|
}
|
|
637
1138
|
acquireContext(req, res, urlOrPath, params, query, requestId, body,
|
|
638
1139
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
639
|
-
files) {
|
|
640
|
-
const ctx = this.contextPool.
|
|
1140
|
+
files, cookies) {
|
|
1141
|
+
const ctx = this.contextPool.acquire();
|
|
641
1142
|
// Reset and populate properties
|
|
642
1143
|
// We use type assertions to write to readonly/managed properties for performance
|
|
643
1144
|
const mutableCtx = ctx;
|
|
@@ -655,24 +1156,23 @@ class QHTTPX {
|
|
|
655
1156
|
}
|
|
656
1157
|
mutableCtx.method = req.method;
|
|
657
1158
|
mutableCtx.params = params;
|
|
658
|
-
|
|
1159
|
+
if (query) {
|
|
1160
|
+
mutableCtx.query = query;
|
|
1161
|
+
}
|
|
659
1162
|
mutableCtx.body = body;
|
|
660
1163
|
mutableCtx.files = files;
|
|
1164
|
+
if (cookies) {
|
|
1165
|
+
mutableCtx.cookies = cookies;
|
|
1166
|
+
}
|
|
661
1167
|
mutableCtx.requestId = requestId;
|
|
662
1168
|
mutableCtx.serializer = undefined;
|
|
663
1169
|
// In ultra mode, skip cookie parsing to save overhead
|
|
664
|
-
|
|
665
|
-
mutableCtx.cookies = (0, cookies_1.parseCookies)(req.headers.cookie);
|
|
666
|
-
}
|
|
1170
|
+
// Cookies are now parsed lazily via getter in QHTTPXContextImpl
|
|
667
1171
|
mutableCtx.disableAutoEnd = false;
|
|
668
1172
|
return mutableCtx;
|
|
669
1173
|
}
|
|
670
1174
|
releaseContext(ctx) {
|
|
671
|
-
|
|
672
|
-
mutableCtx.reset();
|
|
673
|
-
if (this.contextPool.length < this.poolLimit) {
|
|
674
|
-
this.contextPool.push(ctx);
|
|
675
|
-
}
|
|
1175
|
+
this.contextPool.release(ctx);
|
|
676
1176
|
}
|
|
677
1177
|
async handleNoMatch(ctx, allowedMethods) {
|
|
678
1178
|
const res = ctx.res;
|
|
@@ -684,7 +1184,7 @@ class QHTTPX {
|
|
|
684
1184
|
}
|
|
685
1185
|
if (!res.writableEnded && !res.headersSent) {
|
|
686
1186
|
res.statusCode = 405;
|
|
687
|
-
res.setHeader('content-type',
|
|
1187
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
688
1188
|
res.end('Method Not Allowed');
|
|
689
1189
|
}
|
|
690
1190
|
}
|
|
@@ -695,144 +1195,71 @@ class QHTTPX {
|
|
|
695
1195
|
}
|
|
696
1196
|
if (!res.writableEnded && !res.headersSent) {
|
|
697
1197
|
res.statusCode = 404;
|
|
698
|
-
res.setHeader('content-type',
|
|
1198
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
699
1199
|
res.end('Not Found');
|
|
700
1200
|
}
|
|
701
1201
|
}
|
|
702
1202
|
else if (hasAnyMethod) {
|
|
703
1203
|
if (!res.headersSent) {
|
|
704
1204
|
res.statusCode = 405;
|
|
705
|
-
res.setHeader('content-type',
|
|
1205
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
706
1206
|
}
|
|
707
1207
|
res.end('Method Not Allowed');
|
|
708
1208
|
}
|
|
709
1209
|
else {
|
|
710
1210
|
if (!res.headersSent) {
|
|
711
1211
|
res.statusCode = 404;
|
|
712
|
-
res.setHeader('content-type',
|
|
1212
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
713
1213
|
}
|
|
714
1214
|
res.end('Not Found');
|
|
715
1215
|
}
|
|
716
1216
|
}
|
|
717
1217
|
handleRequest(req, res) {
|
|
718
|
-
// ⚡
|
|
719
|
-
if (this.ultraMode) {
|
|
720
|
-
const rawUrl = req.url || '/';
|
|
721
|
-
const method = (req.method || 'GET');
|
|
722
|
-
// Single-pass pathname extraction
|
|
723
|
-
let pathname;
|
|
724
|
-
const qIndex = rawUrl.indexOf('?');
|
|
725
|
-
pathname = qIndex === -1 ? rawUrl : rawUrl.substring(0, qIndex);
|
|
726
|
-
const match = this.router.match(method, pathname);
|
|
727
|
-
if (match) {
|
|
728
|
-
// Acquire Context - reuse pooled object (no allocations)
|
|
729
|
-
const ctx = this.contextPool.pop() ?? this.createContext();
|
|
730
|
-
const mutableCtx = ctx;
|
|
731
|
-
mutableCtx.req = req;
|
|
732
|
-
mutableCtx.res = res;
|
|
733
|
-
mutableCtx.path = pathname;
|
|
734
|
-
mutableCtx.method = method;
|
|
735
|
-
mutableCtx.params = match.params || QHTTPX.EMPTY_PARAMS;
|
|
736
|
-
mutableCtx.query = EMPTY_QUERY;
|
|
737
|
-
mutableCtx.requestId = undefined;
|
|
738
|
-
mutableCtx.body = undefined;
|
|
739
|
-
mutableCtx.files = undefined;
|
|
740
|
-
mutableCtx.disableAutoEnd = false;
|
|
741
|
-
// Don't allocate state - leave as undefined
|
|
742
|
-
const releaseCtx = () => {
|
|
743
|
-
mutableCtx.reset();
|
|
744
|
-
if (this.contextPool.length < this.poolLimit) {
|
|
745
|
-
this.contextPool.push(ctx);
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
try {
|
|
749
|
-
const result = match.handler(ctx);
|
|
750
|
-
// Optimized promise detection
|
|
751
|
-
if (result?.then) {
|
|
752
|
-
result.then(() => {
|
|
753
|
-
if (!res.writableEnded && !ctx.disableAutoEnd) {
|
|
754
|
-
res.end();
|
|
755
|
-
}
|
|
756
|
-
releaseCtx();
|
|
757
|
-
}).catch((err) => {
|
|
758
|
-
console.error(err);
|
|
759
|
-
if (!res.headersSent) {
|
|
760
|
-
res.statusCode = 500;
|
|
761
|
-
res.end();
|
|
762
|
-
}
|
|
763
|
-
releaseCtx();
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
else {
|
|
767
|
-
if (!res.writableEnded && !ctx.disableAutoEnd) {
|
|
768
|
-
res.end();
|
|
769
|
-
}
|
|
770
|
-
releaseCtx();
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
catch (err) {
|
|
774
|
-
console.error(err);
|
|
775
|
-
if (!res.headersSent) {
|
|
776
|
-
res.statusCode = 500;
|
|
777
|
-
res.end();
|
|
778
|
-
}
|
|
779
|
-
releaseCtx();
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
// No match - Ultra Mode Fast 404
|
|
784
|
-
if (!res.headersSent) {
|
|
785
|
-
res.statusCode = 404;
|
|
786
|
-
res.end('Not Found');
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
// === Balanced Mode Logic (Legacy) ===
|
|
792
|
-
const rawMethod = (req.method || 'GET').toUpperCase();
|
|
793
|
-
const method = rawMethod;
|
|
1218
|
+
// ⚡ Optimized request handling with fast routing
|
|
794
1219
|
const rawUrl = req.url || '/';
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
let
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
1220
|
+
const method = (req.method || 'GET');
|
|
1221
|
+
// Single-pass pathname extraction
|
|
1222
|
+
let pathname;
|
|
1223
|
+
const qIndex = rawUrl.indexOf('?');
|
|
1224
|
+
pathname = qIndex === -1 ? rawUrl : rawUrl.substring(0, qIndex);
|
|
1225
|
+
// Fast route matching with O(1) static and optimized dynamic routes
|
|
1226
|
+
let match = this.router.match(method, pathname);
|
|
1227
|
+
// Request ID generation (from x-request-id header or generated)
|
|
1228
|
+
let requestId;
|
|
1229
|
+
const incomingRequestIdHeader = req.headers['x-request-id'];
|
|
1230
|
+
if (typeof incomingRequestIdHeader === 'string') {
|
|
1231
|
+
requestId = incomingRequestIdHeader;
|
|
808
1232
|
}
|
|
809
|
-
else {
|
|
810
|
-
|
|
811
|
-
pathname = rawUrl;
|
|
1233
|
+
else if (Array.isArray(incomingRequestIdHeader)) {
|
|
1234
|
+
requestId = incomingRequestIdHeader[0];
|
|
812
1235
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
requestId = incomingRequestIdHeader;
|
|
819
|
-
}
|
|
820
|
-
else if (Array.isArray(incomingRequestIdHeader)) {
|
|
821
|
-
requestId = incomingRequestIdHeader[0];
|
|
822
|
-
}
|
|
823
|
-
if (!requestId) {
|
|
824
|
-
requestId = this.generateRequestId();
|
|
825
|
-
}
|
|
826
|
-
if (!res.headersSent && requestId) {
|
|
827
|
-
res.setHeader('x-request-id', requestId);
|
|
828
|
-
}
|
|
1236
|
+
if (!requestId) {
|
|
1237
|
+
requestId = this.generateRequestId();
|
|
1238
|
+
}
|
|
1239
|
+
if (!res.headersSent && requestId) {
|
|
1240
|
+
res.setHeader('x-request-id', requestId);
|
|
829
1241
|
}
|
|
830
|
-
let match = this.router.match(method, pathname);
|
|
831
1242
|
const hasRoute = !!match;
|
|
832
1243
|
if (!match) {
|
|
833
1244
|
match = QHTTPX.EMPTY_MATCH;
|
|
834
1245
|
}
|
|
835
|
-
|
|
1246
|
+
// Optimization: Predictive Query Parsing
|
|
1247
|
+
// 1. If no query string exists, set empty query to avoid getter scan
|
|
1248
|
+
// 2. If query string exists AND route needs it, parse eagerly
|
|
1249
|
+
let query;
|
|
1250
|
+
if (qIndex === -1) {
|
|
1251
|
+
query = EMPTY_QUERY;
|
|
1252
|
+
}
|
|
1253
|
+
else if (match.metadata?.needsQuery) {
|
|
1254
|
+
query = (0, querystring_1.parse)(rawUrl.slice(qIndex + 1));
|
|
1255
|
+
}
|
|
1256
|
+
// Optimization: Predictive Cookie Parsing
|
|
1257
|
+
let cookies;
|
|
1258
|
+
if (match.metadata?.needsCookies) {
|
|
1259
|
+
cookies = (0, cookies_1.parseCookies)(req.headers.cookie);
|
|
1260
|
+
}
|
|
1261
|
+
// Optimization: Only calculate allowed methods if no route match found
|
|
1262
|
+
const allowedMethods = hasRoute ? [] : this.router.getAllowedMethods(pathname);
|
|
836
1263
|
if (!hasRoute && !res.headersSent) {
|
|
837
1264
|
const hasAnyMethod = allowedMethods.length > 0;
|
|
838
1265
|
res.statusCode = hasAnyMethod ? 405 : 404;
|
|
@@ -848,7 +1275,7 @@ class QHTTPX {
|
|
|
848
1275
|
}
|
|
849
1276
|
if (!res.headersSent) {
|
|
850
1277
|
res.statusCode = 503;
|
|
851
|
-
res.setHeader('content-type',
|
|
1278
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
852
1279
|
}
|
|
853
1280
|
res.end('Server overloaded');
|
|
854
1281
|
};
|
|
@@ -863,12 +1290,12 @@ class QHTTPX {
|
|
|
863
1290
|
body_parser_1.BodyParser.parse(req, {
|
|
864
1291
|
maxBodyBytes: this.options.maxBodyBytes,
|
|
865
1292
|
}).then((parsed) => {
|
|
866
|
-
this.dispatch(req, res, method, pathname, query, requestId, match, hasRoute, allowedMethods, parsed.body, parsed.files);
|
|
1293
|
+
this.dispatch(req, res, method, pathname, query, requestId, match, hasRoute, allowedMethods, parsed.body, parsed.files, cookies);
|
|
867
1294
|
}).catch((err) => {
|
|
868
1295
|
if (err instanceof Error && err.message === 'QHTTPX_INVALID_JSON') {
|
|
869
1296
|
if (!res.headersSent) {
|
|
870
1297
|
res.statusCode = 400;
|
|
871
|
-
res.setHeader('content-type',
|
|
1298
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
872
1299
|
}
|
|
873
1300
|
res.end('Invalid JSON');
|
|
874
1301
|
return;
|
|
@@ -876,23 +1303,23 @@ class QHTTPX {
|
|
|
876
1303
|
if (err instanceof Error && err.message === 'QHTTPX_BODY_TOO_LARGE') {
|
|
877
1304
|
if (!res.headersSent) {
|
|
878
1305
|
res.statusCode = 413;
|
|
879
|
-
res.setHeader('content-type',
|
|
1306
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
880
1307
|
}
|
|
881
1308
|
res.end('Payload Too Large');
|
|
882
1309
|
return;
|
|
883
1310
|
}
|
|
884
1311
|
if (!res.headersSent) {
|
|
885
1312
|
res.statusCode = 500;
|
|
886
|
-
res.setHeader('content-type',
|
|
1313
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
887
1314
|
}
|
|
888
1315
|
res.end('Internal Server Error');
|
|
889
1316
|
});
|
|
890
1317
|
return;
|
|
891
1318
|
}
|
|
892
|
-
this.dispatch(req, res, method, pathname, query, requestId, match, hasRoute, allowedMethods, undefined, undefined);
|
|
1319
|
+
this.dispatch(req, res, method, pathname, query, requestId, match, hasRoute, allowedMethods, undefined, undefined, cookies);
|
|
893
1320
|
}
|
|
894
|
-
dispatch(req, res, method, pathname, query, requestId, match, hasRoute, allowedMethods, body, files) {
|
|
895
|
-
const ctx = this.acquireContext(req, res, pathname, match.params, query, requestId, body, files);
|
|
1321
|
+
async dispatch(req, res, method, pathname, query, requestId, match, hasRoute, allowedMethods, body, files, cookies) {
|
|
1322
|
+
const ctx = this.acquireContext(req, res, pathname, match.params, query, requestId, body, files, cookies);
|
|
896
1323
|
// Balanced Mode Logic
|
|
897
1324
|
const overloaded = () => {
|
|
898
1325
|
if (res.writableEnded) {
|
|
@@ -900,7 +1327,7 @@ class QHTTPX {
|
|
|
900
1327
|
}
|
|
901
1328
|
if (!res.headersSent) {
|
|
902
1329
|
res.statusCode = 503;
|
|
903
|
-
res.setHeader('content-type',
|
|
1330
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
904
1331
|
}
|
|
905
1332
|
res.end('Server overloaded');
|
|
906
1333
|
};
|
|
@@ -912,13 +1339,19 @@ class QHTTPX {
|
|
|
912
1339
|
}
|
|
913
1340
|
if (!res.headersSent) {
|
|
914
1341
|
res.statusCode = 504;
|
|
915
|
-
res.setHeader('content-type',
|
|
1342
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
916
1343
|
}
|
|
917
1344
|
res.end('Request timed out');
|
|
918
1345
|
};
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
this.
|
|
1346
|
+
let start = 0;
|
|
1347
|
+
const metricsEnabled = this._metrics.isEnabled;
|
|
1348
|
+
if (metricsEnabled || this.tracer) {
|
|
1349
|
+
start = timer_1.globalTimer.preciseNow();
|
|
1350
|
+
ctx.requestStart = start;
|
|
1351
|
+
if (metricsEnabled) {
|
|
1352
|
+
this._metrics.onRequestStart();
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
922
1355
|
if (this.tracer) {
|
|
923
1356
|
const event = {
|
|
924
1357
|
type: 'request_start',
|
|
@@ -932,10 +1365,15 @@ class QHTTPX {
|
|
|
932
1365
|
}
|
|
933
1366
|
}
|
|
934
1367
|
const finish = () => {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1368
|
+
let duration = 0;
|
|
1369
|
+
if (metricsEnabled || this.tracer) {
|
|
1370
|
+
duration = timer_1.globalTimer.preciseNow() - start;
|
|
1371
|
+
if (metricsEnabled) {
|
|
1372
|
+
this._metrics.onRequestEnd(duration, res.statusCode);
|
|
1373
|
+
if (timedOut) {
|
|
1374
|
+
this._metrics.onTimeout();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
939
1377
|
}
|
|
940
1378
|
if (this.tracer) {
|
|
941
1379
|
const event = {
|
|
@@ -978,24 +1416,16 @@ class QHTTPX {
|
|
|
978
1416
|
await this.handleError(err, ctx);
|
|
979
1417
|
}
|
|
980
1418
|
};
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
onOverloaded: overloaded,
|
|
992
|
-
timeoutMs: this.options.requestTimeoutMs,
|
|
993
|
-
onTimeout,
|
|
994
|
-
}).then(finish).catch((err) => {
|
|
995
|
-
console.error('Scheduler error:', err);
|
|
996
|
-
finish();
|
|
997
|
-
});
|
|
998
|
-
}
|
|
1419
|
+
// Use scheduler for request handling
|
|
1420
|
+
this.scheduler.run(handle, {
|
|
1421
|
+
priority: match.priority,
|
|
1422
|
+
onOverloaded: overloaded,
|
|
1423
|
+
timeoutMs: this.options.requestTimeoutMs,
|
|
1424
|
+
onTimeout,
|
|
1425
|
+
}).then(finish).catch((err) => {
|
|
1426
|
+
console.error('Scheduler error:', err);
|
|
1427
|
+
finish();
|
|
1428
|
+
});
|
|
999
1429
|
}
|
|
1000
1430
|
async handleUpgrade(req, socket, head) {
|
|
1001
1431
|
await this.wsManager.handleUpgrade(req, socket, head);
|
|
@@ -1047,7 +1477,7 @@ class QHTTPX {
|
|
|
1047
1477
|
if (err instanceof types_1.HttpError) {
|
|
1048
1478
|
if (!res.headersSent) {
|
|
1049
1479
|
res.statusCode = err.status;
|
|
1050
|
-
res.setHeader('content-type',
|
|
1480
|
+
res.setHeader('content-type', CONTENT_TYPE.JSON);
|
|
1051
1481
|
}
|
|
1052
1482
|
const payload = {
|
|
1053
1483
|
error: {
|
|
@@ -1063,14 +1493,19 @@ class QHTTPX {
|
|
|
1063
1493
|
}
|
|
1064
1494
|
if (!res.headersSent) {
|
|
1065
1495
|
res.statusCode = 500;
|
|
1066
|
-
res.setHeader('content-type',
|
|
1496
|
+
res.setHeader('content-type', CONTENT_TYPE.PLAIN);
|
|
1067
1497
|
}
|
|
1068
1498
|
res.end('Internal Server Error');
|
|
1069
1499
|
}
|
|
1070
1500
|
generateRequestId() {
|
|
1501
|
+
const now = timer_1.globalTimer.now();
|
|
1502
|
+
if (now !== this.lastDateNow) {
|
|
1503
|
+
this.lastDateNow = now;
|
|
1504
|
+
this.lastDateString = now.toString(36);
|
|
1505
|
+
}
|
|
1071
1506
|
const id = this.nextRequestId;
|
|
1072
1507
|
this.nextRequestId += 1;
|
|
1073
|
-
return `${
|
|
1508
|
+
return `${this.lastDateString}-${id.toString(36)}`;
|
|
1074
1509
|
}
|
|
1075
1510
|
}
|
|
1076
1511
|
exports.QHTTPX = QHTTPX;
|
|
@@ -1079,4 +1514,5 @@ QHTTPX.EMPTY_MATCH = Object.freeze({
|
|
|
1079
1514
|
handler: () => { },
|
|
1080
1515
|
params: QHTTPX.EMPTY_PARAMS,
|
|
1081
1516
|
priority: types_1.RoutePriority.STANDARD,
|
|
1517
|
+
metadata: { needsQuery: false, needsCookies: false, needsBody: false, pipeline: undefined },
|
|
1082
1518
|
});
|