@morojs/moro 1.8.5 → 1.8.7
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/README.md +7 -6
- package/dist/core/http/http-server.d.ts +5 -3
- package/dist/core/http/http-server.js +38 -11
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/http/http2-server.d.ts +1 -1
- package/dist/core/http/http2-server.js +23 -9
- package/dist/core/http/http2-server.js.map +1 -1
- package/dist/core/http/moro-engine-server.d.ts +24 -4
- package/dist/core/http/moro-engine-server.js +194 -42
- package/dist/core/http/moro-engine-server.js.map +1 -1
- package/dist/core/jobs/job-executor.d.ts +1 -0
- package/dist/core/jobs/job-executor.js +23 -1
- package/dist/core/jobs/job-executor.js.map +1 -1
- package/dist/core/middleware/built-in/auth/helpers.js +11 -1
- package/dist/core/middleware/built-in/auth/helpers.js.map +1 -1
- package/dist/core/pooling/object-pool-manager.js +14 -20
- package/dist/core/pooling/object-pool-manager.js.map +1 -1
- package/dist/core/routing/radix-tree.d.ts +1 -0
- package/dist/core/routing/radix-tree.js +6 -1
- package/dist/core/routing/radix-tree.js.map +1 -1
- package/dist/core/routing/unified-router.js +33 -25
- package/dist/core/routing/unified-router.js.map +1 -1
- package/dist/core/utilities/hooks.d.ts +2 -0
- package/dist/core/utilities/hooks.js +28 -2
- package/dist/core/utilities/hooks.js.map +1 -1
- package/dist/moro.js +12 -0
- package/dist/moro.js.map +1 -1
- package/package.json +3 -1
|
@@ -76,6 +76,10 @@ function addCharsetIfNeeded(mimeType) {
|
|
|
76
76
|
// Files up to this size are sent with a single respond(); larger ones stream
|
|
77
77
|
// through writeHead/write/end with backpressure respected via 'drain'
|
|
78
78
|
const SENDFILE_BUFFER_LIMIT = 1024 * 1024;
|
|
79
|
+
// Shared flat header pair for the json() fast path - passed to respond() when
|
|
80
|
+
// the response has no user-set headers, so no header object or flat array is
|
|
81
|
+
// allocated. The engine only reads it; never mutate.
|
|
82
|
+
const JSON_HEADER_PAIR = ['content-type', 'application/json'];
|
|
79
83
|
/**
|
|
80
84
|
* Prototype-based request object for the engine adapter.
|
|
81
85
|
*
|
|
@@ -87,10 +91,10 @@ const SENDFILE_BUFFER_LIMIT = 1024 * 1024;
|
|
|
87
91
|
export class EngineRequest extends LazyEventEmitter {
|
|
88
92
|
method;
|
|
89
93
|
path;
|
|
90
|
-
params = {};
|
|
91
94
|
body = null;
|
|
92
95
|
_server;
|
|
93
96
|
_reqId;
|
|
97
|
+
_params = undefined;
|
|
94
98
|
_url = undefined;
|
|
95
99
|
_queryString = undefined;
|
|
96
100
|
_query = undefined;
|
|
@@ -110,11 +114,25 @@ export class EngineRequest extends LazyEventEmitter {
|
|
|
110
114
|
/** Engine snapshots are stable - nothing to detach. No-op kept for
|
|
111
115
|
* interface parity with UwsRequest so shared dispatch code can call it. */
|
|
112
116
|
materialize() { }
|
|
117
|
+
// Lazy like context: dispatch writes params through the setter only for
|
|
118
|
+
// parametric routes, so a handler that never reads req.params on a static
|
|
119
|
+
// route pays no allocation.
|
|
120
|
+
get params() {
|
|
121
|
+
let p = this._params;
|
|
122
|
+
if (p === undefined) {
|
|
123
|
+
p = {};
|
|
124
|
+
this._params = p;
|
|
125
|
+
}
|
|
126
|
+
return p;
|
|
127
|
+
}
|
|
128
|
+
set params(value) {
|
|
129
|
+
this._params = value;
|
|
130
|
+
}
|
|
113
131
|
_rawQueryString() {
|
|
114
132
|
let qs = this._queryString;
|
|
115
133
|
if (qs === undefined) {
|
|
116
134
|
// Safe no-op (undefined) after the response terminates - coerce to ''
|
|
117
|
-
qs = this._server.
|
|
135
|
+
qs = this._server._getQuery(this._reqId) ?? '';
|
|
118
136
|
this._queryString = qs;
|
|
119
137
|
}
|
|
120
138
|
return qs;
|
|
@@ -151,7 +169,7 @@ export class EngineRequest extends LazyEventEmitter {
|
|
|
151
169
|
// Duplicate header lines are joined like Node's IncomingMessage ('; '
|
|
152
170
|
// for cookie, ', ' otherwise) - last-wins would let a client hide
|
|
153
171
|
// earlier X-Forwarded-For entries from IP-based middleware.
|
|
154
|
-
const flat = this._server.
|
|
172
|
+
const flat = this._server._getHeaders(this._reqId);
|
|
155
173
|
if (flat) {
|
|
156
174
|
for (let i = 0; i + 1 < flat.length; i += 2) {
|
|
157
175
|
const key = flat[i];
|
|
@@ -233,7 +251,7 @@ export class EngineRequest extends LazyEventEmitter {
|
|
|
233
251
|
get ip() {
|
|
234
252
|
let ip = this._ip;
|
|
235
253
|
if (ip === undefined) {
|
|
236
|
-
ip = this._server.
|
|
254
|
+
ip = this._server._getRemoteAddress(this._reqId) ?? '';
|
|
237
255
|
this._ip = ip;
|
|
238
256
|
}
|
|
239
257
|
return ip;
|
|
@@ -393,15 +411,18 @@ export class EngineRequest extends LazyEventEmitter {
|
|
|
393
411
|
export class EngineResponse extends LazyEventEmitter {
|
|
394
412
|
headersSent = false;
|
|
395
413
|
statusCode = 200;
|
|
396
|
-
responseHeaders = {};
|
|
397
|
-
locals = {};
|
|
398
414
|
// The EngineRequest wrapper, linked by the server so lifecycle events reach
|
|
399
415
|
// req.on('close') listeners (SSE cleanup, monitors)
|
|
400
416
|
_moroReq = null;
|
|
417
|
+
/** @internal true while this response is in the server's inflight map */
|
|
418
|
+
_registered = false;
|
|
401
419
|
_server;
|
|
402
|
-
_engine;
|
|
403
420
|
_reqId;
|
|
404
421
|
_logger;
|
|
422
|
+
// Lazy: json/send/end on a response with no user-set headers never
|
|
423
|
+
// materialize a header object (see _headersFlat / the json fast path)
|
|
424
|
+
_responseHeaders = undefined;
|
|
425
|
+
_locals = undefined;
|
|
405
426
|
// Terminal-write latch: headersSent means "head flushed" (true mid-stream
|
|
406
427
|
// after writeHead/write), _ended means the body is complete
|
|
407
428
|
_ended = false;
|
|
@@ -409,10 +430,31 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
409
430
|
constructor(server, reqId, logger) {
|
|
410
431
|
super();
|
|
411
432
|
this._server = server;
|
|
412
|
-
this._engine = server._engine;
|
|
413
433
|
this._reqId = reqId;
|
|
414
434
|
this._logger = logger;
|
|
415
435
|
}
|
|
436
|
+
get responseHeaders() {
|
|
437
|
+
let h = this._responseHeaders;
|
|
438
|
+
if (h === undefined) {
|
|
439
|
+
h = {};
|
|
440
|
+
this._responseHeaders = h;
|
|
441
|
+
}
|
|
442
|
+
return h;
|
|
443
|
+
}
|
|
444
|
+
set responseHeaders(value) {
|
|
445
|
+
this._responseHeaders = value;
|
|
446
|
+
}
|
|
447
|
+
get locals() {
|
|
448
|
+
let l = this._locals;
|
|
449
|
+
if (l === undefined) {
|
|
450
|
+
l = {};
|
|
451
|
+
this._locals = l;
|
|
452
|
+
}
|
|
453
|
+
return l;
|
|
454
|
+
}
|
|
455
|
+
set locals(value) {
|
|
456
|
+
this._locals = value;
|
|
457
|
+
}
|
|
416
458
|
get writableEnded() {
|
|
417
459
|
return this._ended;
|
|
418
460
|
}
|
|
@@ -430,7 +472,12 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
430
472
|
// Also releases the reqId from the server's in-flight map.
|
|
431
473
|
_emitDone() {
|
|
432
474
|
this._ended = true;
|
|
433
|
-
|
|
475
|
+
// Sync responses end before onRequest's registration window, so they were
|
|
476
|
+
// never in the inflight map - skip the wasted delete on a missing key
|
|
477
|
+
if (this._registered) {
|
|
478
|
+
this._registered = false;
|
|
479
|
+
this._server._complete(this._reqId);
|
|
480
|
+
}
|
|
434
481
|
if (this._events) {
|
|
435
482
|
this.emit('finish');
|
|
436
483
|
this.emit('close');
|
|
@@ -447,6 +494,7 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
447
494
|
return;
|
|
448
495
|
this._ended = true;
|
|
449
496
|
this._aborted = true;
|
|
497
|
+
this._registered = false; // onAborted already dropped the map entry
|
|
450
498
|
if (this._events)
|
|
451
499
|
this.emit('close');
|
|
452
500
|
const req = this._moroReq;
|
|
@@ -464,7 +512,9 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
464
512
|
// actually set. Multi-value headers (set-cookie) become separate pairs so
|
|
465
513
|
// each goes out as its own header line.
|
|
466
514
|
_headersFlat() {
|
|
467
|
-
const headers = this.
|
|
515
|
+
const headers = this._responseHeaders;
|
|
516
|
+
if (headers === undefined)
|
|
517
|
+
return null;
|
|
468
518
|
let flat = null;
|
|
469
519
|
// Performance: for...in is the fastest way to iterate response headers
|
|
470
520
|
for (const key in headers) {
|
|
@@ -492,13 +542,15 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
492
542
|
return this;
|
|
493
543
|
}
|
|
494
544
|
getHeader(name) {
|
|
495
|
-
const
|
|
496
|
-
return
|
|
545
|
+
const headers = this._responseHeaders;
|
|
546
|
+
return headers === undefined ? undefined : headers[name.toLowerCase()];
|
|
497
547
|
}
|
|
498
548
|
removeHeader(name) {
|
|
499
|
-
const
|
|
500
|
-
if (
|
|
501
|
-
|
|
549
|
+
const headers = this._responseHeaders;
|
|
550
|
+
if (headers !== undefined) {
|
|
551
|
+
const lowerName = name.toLowerCase();
|
|
552
|
+
if (lowerName in headers)
|
|
553
|
+
delete headers[lowerName];
|
|
502
554
|
}
|
|
503
555
|
return this;
|
|
504
556
|
}
|
|
@@ -556,10 +608,19 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
556
608
|
else {
|
|
557
609
|
body = JSON.stringify(data);
|
|
558
610
|
}
|
|
559
|
-
if (!('content-type' in this.responseHeaders)) {
|
|
560
|
-
this.responseHeaders['content-type'] = 'application/json';
|
|
561
|
-
}
|
|
562
611
|
try {
|
|
612
|
+
const rh = this._responseHeaders;
|
|
613
|
+
if (rh === undefined && !this._server._compression.enabled) {
|
|
614
|
+
// Hot path: no user-set headers, no compression - one native call
|
|
615
|
+
// with the shared content-type pair; zero per-response header work
|
|
616
|
+
this._server._respond(this._reqId, this.statusCode, JSON_HEADER_PAIR, body);
|
|
617
|
+
this.headersSent = true;
|
|
618
|
+
this._emitDone();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (rh === undefined || !('content-type' in rh)) {
|
|
622
|
+
this.responseHeaders['content-type'] = 'application/json';
|
|
623
|
+
}
|
|
563
624
|
this._respondMaybeCompressed(body, 'application/json');
|
|
564
625
|
}
|
|
565
626
|
catch (err) {
|
|
@@ -577,29 +638,30 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
577
638
|
// work (byteLength scans the string; header reads allocate nothing but
|
|
578
639
|
// still cost). The default config must pay zero for this feature.
|
|
579
640
|
if (!s.enabled) {
|
|
580
|
-
this.
|
|
641
|
+
this._server._respond(this._reqId, this.statusCode, this._headersFlat(), body);
|
|
581
642
|
this.headersSent = true;
|
|
582
643
|
this._emitDone();
|
|
583
644
|
return;
|
|
584
645
|
}
|
|
585
646
|
const bytes = typeof body === 'string' ? Buffer.byteLength(body) : body.length;
|
|
586
|
-
const
|
|
587
|
-
const
|
|
647
|
+
const rh = this._responseHeaders;
|
|
648
|
+
const alreadyEncoded = rh !== undefined && 'content-encoding' in rh;
|
|
649
|
+
const ctHeader = rh?.['content-type'];
|
|
588
650
|
const ct = contentType ?? (Array.isArray(ctHeader) ? ctHeader[0] : ctHeader);
|
|
589
651
|
if (alreadyEncoded ||
|
|
590
652
|
bytes < s.threshold ||
|
|
591
653
|
!isCompressible(ct) ||
|
|
592
654
|
this.statusCode === 204 ||
|
|
593
655
|
this.statusCode === 304) {
|
|
594
|
-
this.
|
|
656
|
+
this._server._respond(this._reqId, this.statusCode, this._headersFlat(), body);
|
|
595
657
|
this.headersSent = true;
|
|
596
658
|
this._emitDone();
|
|
597
659
|
return;
|
|
598
660
|
}
|
|
599
|
-
const accept = this.
|
|
661
|
+
const accept = this._server._getHeader(this._reqId, 'accept-encoding');
|
|
600
662
|
const encoding = negotiateEncoding(accept, s.encodings);
|
|
601
663
|
if (!encoding) {
|
|
602
|
-
this.
|
|
664
|
+
this._server._respond(this._reqId, this.statusCode, this._headersFlat(), body);
|
|
603
665
|
this.headersSent = true;
|
|
604
666
|
this._emitDone();
|
|
605
667
|
return;
|
|
@@ -608,12 +670,12 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
608
670
|
// raced the await - the reqId would be invalid).
|
|
609
671
|
void compressBuffer(body, encoding, s.level)
|
|
610
672
|
.then(compressed => {
|
|
611
|
-
if (this._ended || this.
|
|
673
|
+
if (this._ended || this._server._isAborted(this._reqId))
|
|
612
674
|
return;
|
|
613
675
|
this.responseHeaders['content-encoding'] = encoding;
|
|
614
676
|
const vary = this.responseHeaders['vary'];
|
|
615
677
|
this.responseHeaders['vary'] = vary ? `${vary}, Accept-Encoding` : 'Accept-Encoding';
|
|
616
|
-
this.
|
|
678
|
+
this._server._respond(this._reqId, this.statusCode, this._headersFlat(), compressed);
|
|
617
679
|
this.headersSent = true;
|
|
618
680
|
this._emitDone();
|
|
619
681
|
})
|
|
@@ -626,7 +688,7 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
626
688
|
this._logger.error(`${context}: ${err instanceof Error ? err.message : String(err)}`, 'ResponseError');
|
|
627
689
|
if (!this._ended) {
|
|
628
690
|
try {
|
|
629
|
-
this.
|
|
691
|
+
this._server._respond(this._reqId, 500, null, '{"success":false,"error":"Internal server error"}');
|
|
630
692
|
this.headersSent = true;
|
|
631
693
|
}
|
|
632
694
|
catch {
|
|
@@ -680,7 +742,7 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
680
742
|
}
|
|
681
743
|
}
|
|
682
744
|
try {
|
|
683
|
-
this.
|
|
745
|
+
this._server._writeHead(this._reqId, statusCode, this._headersFlat());
|
|
684
746
|
this.headersSent = true;
|
|
685
747
|
}
|
|
686
748
|
catch {
|
|
@@ -701,10 +763,10 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
701
763
|
let ok = true;
|
|
702
764
|
try {
|
|
703
765
|
if (!this.headersSent) {
|
|
704
|
-
this.
|
|
766
|
+
this._server._writeHead(this._reqId, this.statusCode, this._headersFlat());
|
|
705
767
|
this.headersSent = true;
|
|
706
768
|
}
|
|
707
|
-
ok = this.
|
|
769
|
+
ok = this._server._write(this._reqId, chunk) !== false;
|
|
708
770
|
}
|
|
709
771
|
catch {
|
|
710
772
|
this._logger.error('Failed to write response chunk', 'ResponseError');
|
|
@@ -729,11 +791,11 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
729
791
|
try {
|
|
730
792
|
if (!this.headersSent) {
|
|
731
793
|
// Terminal single-shot: status + headers + body in one native call
|
|
732
|
-
this.
|
|
794
|
+
this._server._respond(this._reqId, this.statusCode, this._headersFlat(), data !== undefined && data !== null ? data : null);
|
|
733
795
|
this.headersSent = true;
|
|
734
796
|
}
|
|
735
797
|
else {
|
|
736
|
-
this.
|
|
798
|
+
this._server._end(this._reqId, data !== undefined && data !== null ? data : undefined);
|
|
737
799
|
}
|
|
738
800
|
this._emitDone();
|
|
739
801
|
if (typeof callback === 'function')
|
|
@@ -753,7 +815,7 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
753
815
|
this.statusCode = redirectCode;
|
|
754
816
|
this.setHeader('location', url.replace(/[\r\n]/g, ''));
|
|
755
817
|
try {
|
|
756
|
-
this.
|
|
818
|
+
this._server._respond(this._reqId, redirectCode, this._headersFlat(), null);
|
|
757
819
|
this.headersSent = true;
|
|
758
820
|
this._emitDone();
|
|
759
821
|
}
|
|
@@ -1223,6 +1285,22 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
1223
1285
|
export class MoroEngineServer {
|
|
1224
1286
|
/** @internal read by EngineRequest/EngineResponse for lazy fetchers */
|
|
1225
1287
|
_engine;
|
|
1288
|
+
// Hot-path native functions hoisted off the loader's module Proxy: every
|
|
1289
|
+
// property access on the engine module runs its `get` trap (a Set probe +
|
|
1290
|
+
// binding load) that V8 cannot inline-cache. Hoisted once here, each
|
|
1291
|
+
// per-request native call becomes a plain monomorphic field load. The
|
|
1292
|
+
// functions are plain N-API exports with no `this` dependence.
|
|
1293
|
+
/** @internal */ _respond;
|
|
1294
|
+
/** @internal */ _writeHead;
|
|
1295
|
+
/** @internal */ _write;
|
|
1296
|
+
/** @internal */ _end;
|
|
1297
|
+
/** @internal */ _isAborted;
|
|
1298
|
+
/** @internal */ _getQuery;
|
|
1299
|
+
/** @internal */ _getHeaders;
|
|
1300
|
+
/** @internal */ _getHeader;
|
|
1301
|
+
/** @internal */ _getBody;
|
|
1302
|
+
/** @internal */ _getRemoteAddress;
|
|
1303
|
+
/** @internal */ _getMethod;
|
|
1226
1304
|
/** @internal read by EngineRequest#protocol/socket.encrypted - true when the
|
|
1227
1305
|
* engine is terminating TLS for this server (engine >= 1.2.0 + ssl passed) */
|
|
1228
1306
|
isSsl = false;
|
|
@@ -1308,6 +1386,17 @@ export class MoroEngineServer {
|
|
|
1308
1386
|
'use UWebSocketsHttpServer for uWS-style modules');
|
|
1309
1387
|
}
|
|
1310
1388
|
this._engine = surface;
|
|
1389
|
+
this._respond = surface.respond;
|
|
1390
|
+
this._writeHead = surface.writeHead;
|
|
1391
|
+
this._write = surface.write;
|
|
1392
|
+
this._end = surface.end;
|
|
1393
|
+
this._isAborted = surface.isAborted;
|
|
1394
|
+
this._getQuery = surface.getQuery;
|
|
1395
|
+
this._getHeaders = surface.getHeaders;
|
|
1396
|
+
this._getHeader = surface.getHeader;
|
|
1397
|
+
this._getBody = surface.getBody;
|
|
1398
|
+
this._getRemoteAddress = surface.getRemoteAddress;
|
|
1399
|
+
this._getMethod = surface.getMethod;
|
|
1311
1400
|
// TLS: pass it through when the engine supports it; otherwise keep the
|
|
1312
1401
|
// (now capability-gated) warning so a proxy/node fallback is clear.
|
|
1313
1402
|
if (this._ssl && this._capabilities?.tls) {
|
|
@@ -1570,13 +1659,13 @@ export class MoroEngineServer {
|
|
|
1570
1659
|
// request, hand the connection to the engine's native WS path. The engine
|
|
1571
1660
|
// sends the 101 handshake and drives onWsOpen/Message/Close.
|
|
1572
1661
|
if (this._wsBridge) {
|
|
1573
|
-
const upgrade = this.
|
|
1662
|
+
const upgrade = this._getHeader(reqId, 'upgrade');
|
|
1574
1663
|
if (upgrade && upgrade.toLowerCase() === 'websocket') {
|
|
1575
1664
|
// Snapshot handshake data now - the upgrade invalidates the reqId,
|
|
1576
1665
|
// after which these fetchers return empty values.
|
|
1577
|
-
const ip = this.
|
|
1666
|
+
const ip = this._getRemoteAddress(reqId) || '';
|
|
1578
1667
|
const headers = {};
|
|
1579
|
-
const flat = this.
|
|
1668
|
+
const flat = this._getHeaders(reqId);
|
|
1580
1669
|
if (flat) {
|
|
1581
1670
|
// Join duplicates exactly like the EngineRequest.headers getter -
|
|
1582
1671
|
// last-wins would let a client send two X-Forwarded-For lines and
|
|
@@ -1593,7 +1682,7 @@ export class MoroEngineServer {
|
|
|
1593
1682
|
}
|
|
1594
1683
|
// Capture the query string too (invalidated by the upgrade like the
|
|
1595
1684
|
// rest), so handlers can read handshake query params (e.g. orgId).
|
|
1596
|
-
const query = this.
|
|
1685
|
+
const query = this._getQuery(reqId) || '';
|
|
1597
1686
|
this._pendingWsInfo = { ip, headers, query };
|
|
1598
1687
|
const wsId = this._engine.upgradeToWebSocket(reqId);
|
|
1599
1688
|
this._pendingWsInfo = undefined;
|
|
@@ -1604,7 +1693,7 @@ export class MoroEngineServer {
|
|
|
1604
1693
|
}
|
|
1605
1694
|
// methodIdx 7 (OTHER) needs the extra crossing; the seven common methods
|
|
1606
1695
|
// resolve from the static table
|
|
1607
|
-
const method = methodIdx === 7 ? this.
|
|
1696
|
+
const method = (methodIdx === 7 ? this._getMethod(reqId) || 'OTHER' : METHODS[methodIdx]);
|
|
1608
1697
|
const httpReq = new EngineRequest(this, reqId, method, path);
|
|
1609
1698
|
const httpRes = new EngineResponse(this, reqId, this.logger);
|
|
1610
1699
|
httpRes._moroReq = httpReq;
|
|
@@ -1619,7 +1708,46 @@ export class MoroEngineServer {
|
|
|
1619
1708
|
httpRes[key] = this.responseDecorations[key];
|
|
1620
1709
|
}
|
|
1621
1710
|
}
|
|
1622
|
-
|
|
1711
|
+
this.requestCounter++;
|
|
1712
|
+
// SYNC FAST PATH (parity with MoroHttpServer's handleRequest split): with
|
|
1713
|
+
// no body to parse, no request/response hooks and no global middleware,
|
|
1714
|
+
// the router dispatch runs synchronously in this frame - the async
|
|
1715
|
+
// handleRequest below would allocate a promise + async context per
|
|
1716
|
+
// request only to reach zero awaits. Only the framework's own frame is
|
|
1717
|
+
// de-asynced: an async route handler still returns its promise through
|
|
1718
|
+
// the router and is driven to completion here.
|
|
1719
|
+
const hookManager = this.hookManager;
|
|
1720
|
+
const hooksActive = hookManager &&
|
|
1721
|
+
(hookManager.hasHooks === undefined ||
|
|
1722
|
+
hookManager.hasHooks('request') ||
|
|
1723
|
+
hookManager.hasHooks('response'));
|
|
1724
|
+
const needsBody = method.charCodeAt(0) === 80 && // 'P' char code
|
|
1725
|
+
(method === 'POST' || method === 'PUT' || method === 'PATCH');
|
|
1726
|
+
if (!needsBody && !hooksActive && this.globalMiddleware.length === 0 && this.routerHandler) {
|
|
1727
|
+
let handled = true;
|
|
1728
|
+
try {
|
|
1729
|
+
handled = this.routerHandler(httpReq, httpRes);
|
|
1730
|
+
}
|
|
1731
|
+
catch (error) {
|
|
1732
|
+
// Error path owns the response from here (handled stays true)
|
|
1733
|
+
void this.handleDispatchError(error, httpReq, httpRes);
|
|
1734
|
+
}
|
|
1735
|
+
if (handled !== true) {
|
|
1736
|
+
if (handled && typeof handled.then === 'function') {
|
|
1737
|
+
handled.then(ok => {
|
|
1738
|
+
if (!ok)
|
|
1739
|
+
void this.finishUnrouted(httpReq, httpRes);
|
|
1740
|
+
}, error => void this.handleDispatchError(error, httpReq, httpRes));
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
// Sync miss from the router - direct-route fallback, then 404
|
|
1744
|
+
void this.finishUnrouted(httpReq, httpRes);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
else {
|
|
1749
|
+
void this.handleRequest(httpReq, httpRes);
|
|
1750
|
+
}
|
|
1623
1751
|
// Register for onAborted/onWritable routing only when the response is
|
|
1624
1752
|
// still in flight after the synchronous dispatch window. Sync handlers
|
|
1625
1753
|
// have already ended by here, and the engine never fires those callbacks
|
|
@@ -1627,8 +1755,10 @@ export class MoroEngineServer {
|
|
|
1627
1755
|
// long-lived Map's old-space backing store, and entries alive at a
|
|
1628
1756
|
// scavenge promote the whole request/response graph (at ~570k pipelined
|
|
1629
1757
|
// req/s that meant full GCs every ~400ms and a ~150 MB heap vs ~30 MB).
|
|
1630
|
-
if (!httpRes.writableEnded)
|
|
1758
|
+
if (!httpRes.writableEnded) {
|
|
1759
|
+
httpRes._registered = true;
|
|
1631
1760
|
this.inflight.set(reqId, httpRes);
|
|
1761
|
+
}
|
|
1632
1762
|
}
|
|
1633
1763
|
onAborted(reqId) {
|
|
1634
1764
|
const httpRes = this.inflight.get(reqId);
|
|
@@ -1642,8 +1772,23 @@ export class MoroEngineServer {
|
|
|
1642
1772
|
if (httpRes)
|
|
1643
1773
|
httpRes._handleWritable();
|
|
1644
1774
|
}
|
|
1775
|
+
// Router-miss continuation shared by the sync fast path: direct-route
|
|
1776
|
+
// fallback, then 404 - with the same error shaping as handleRequest.
|
|
1777
|
+
async finishUnrouted(httpReq, httpRes) {
|
|
1778
|
+
try {
|
|
1779
|
+
if (await this.dispatchDirectRoute(httpReq, httpRes))
|
|
1780
|
+
return;
|
|
1781
|
+
if (!httpRes.headersSent && !httpRes.writableEnded) {
|
|
1782
|
+
httpRes.statusCode = 404;
|
|
1783
|
+
httpRes.setHeader('Content-Type', 'application/json');
|
|
1784
|
+
httpRes.end('{"success":false,"error":"Not found"}');
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
catch (error) {
|
|
1788
|
+
await this.handleDispatchError(error, httpReq, httpRes);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1645
1791
|
async handleRequest(httpReq, httpRes) {
|
|
1646
|
-
this.requestCounter++;
|
|
1647
1792
|
try {
|
|
1648
1793
|
const method = httpReq.method;
|
|
1649
1794
|
// Parse body only for body methods (all start with 'P'). The engine
|
|
@@ -1703,6 +1848,13 @@ export class MoroEngineServer {
|
|
|
1703
1848
|
}
|
|
1704
1849
|
}
|
|
1705
1850
|
catch (error) {
|
|
1851
|
+
await this.handleDispatchError(error, httpReq, httpRes);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
// Error shaping shared by the async pipeline and the sync fast path:
|
|
1855
|
+
// 413/400 client responses, error hooks, the app's error handler, then 500.
|
|
1856
|
+
async handleDispatchError(error, httpReq, httpRes) {
|
|
1857
|
+
{
|
|
1706
1858
|
// Payload-too-large: respond 413 rather than a generic 500
|
|
1707
1859
|
if (error?.statusCode === 413 && !httpRes.writableEnded && !httpRes.headersSent) {
|
|
1708
1860
|
httpRes.statusCode = 413;
|
|
@@ -1753,7 +1905,7 @@ export class MoroEngineServer {
|
|
|
1753
1905
|
// semantics to the Node and uWS servers' readBody). getBody returns a STABLE
|
|
1754
1906
|
// ArrayBuffer copy, so Buffer.from() views it with no aliasing hazard.
|
|
1755
1907
|
parseBody(httpReq) {
|
|
1756
|
-
const raw = this.
|
|
1908
|
+
const raw = this._getBody(httpReq._reqId);
|
|
1757
1909
|
if (!raw || raw.byteLength === 0)
|
|
1758
1910
|
return;
|
|
1759
1911
|
const contentType = httpReq.headers['content-type'] || '';
|