@morojs/moro 1.8.8 → 1.8.10
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/dist/core/cluster/thread-cluster.d.ts +178 -0
- package/dist/core/cluster/thread-cluster.js +389 -0
- package/dist/core/cluster/thread-cluster.js.map +1 -0
- package/dist/core/framework.js.map +1 -1
- package/dist/core/http/moro-engine-server.d.ts +29 -1
- package/dist/core/http/moro-engine-server.js +375 -68
- package/dist/core/http/moro-engine-server.js.map +1 -1
- package/dist/core/http/utils/compression.js +12 -6
- package/dist/core/http/utils/compression.js.map +1 -1
- package/dist/core/http/utils/uws-worker-clustering.js +10 -6
- package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
- package/dist/core/http/uws-http-server.d.ts +1 -0
- package/dist/core/http/uws-http-server.js +105 -22
- package/dist/core/http/uws-http-server.js.map +1 -1
- package/dist/core/jobs/leader-election.js +16 -5
- package/dist/core/jobs/leader-election.js.map +1 -1
- package/dist/core/networking/websocket-manager.js +4 -2
- package/dist/core/networking/websocket-manager.js.map +1 -1
- package/dist/core/routing/unified-router.js +60 -29
- package/dist/core/routing/unified-router.js.map +1 -1
- package/dist/core/runtime/node-adapter.d.ts +1 -1
- package/dist/core/runtime/node-adapter.js.map +1 -1
- package/dist/core/utilities/builtin.d.ts +1 -0
- package/dist/core/utilities/builtin.js +25 -0
- package/dist/core/utilities/builtin.js.map +1 -0
- package/dist/core/utilities/package-utils.d.ts +28 -3
- package/dist/core/utilities/package-utils.js +24 -12
- package/dist/core/utilities/package-utils.js.map +1 -1
- package/dist/core/workers/worker-manager.js +4 -3
- package/dist/core/workers/worker-manager.js.map +1 -1
- package/dist/moro.d.ts +5 -0
- package/dist/moro.js +364 -204
- package/dist/moro.js.map +1 -1
- package/dist/types/http.d.ts +1 -1
- package/dist/types/http.js.map +1 -1
- package/package.json +1 -1
|
@@ -80,6 +80,36 @@ const SENDFILE_BUFFER_LIMIT = 1024 * 1024;
|
|
|
80
80
|
// the response has no user-set headers, so no header object or flat array is
|
|
81
81
|
// allocated. The engine only reads it; never mutate.
|
|
82
82
|
const JSON_HEADER_PAIR = ['content-type', 'application/json'];
|
|
83
|
+
// Prepared response templates (engine >= 1.1.6, capabilities.responseTemplates).
|
|
84
|
+
// The header sets the framework ITSELF emits - the JSON/text/octet-stream
|
|
85
|
+
// defaults and "no headers at all" - are materialised once per (kind, status)
|
|
86
|
+
// inside the engine (prepareResponse) and replayed per response with
|
|
87
|
+
// respondPrepared: no header object, no flat array, no per-response header
|
|
88
|
+
// walk in the engine, and the call is fast-call eligible. Used only when the
|
|
89
|
+
// response carries NO user-set headers (then the wire bytes are identical to
|
|
90
|
+
// respond() with the same pair - the engine's wire-parity suite proves it)
|
|
91
|
+
// and compression is off. TPL_HEADERS doubles as the respond() fallback for
|
|
92
|
+
// engines without templates, so both paths send exactly these headers.
|
|
93
|
+
const TPL_JSON = 0; // json(): 'application/json'
|
|
94
|
+
const TPL_JSON_UTF8 = 1; // send() of a JSON-looking string
|
|
95
|
+
const TPL_TEXT_UTF8 = 2; // send() of text, sendStatus()
|
|
96
|
+
const TPL_OCTET = 3; // send() of a Buffer
|
|
97
|
+
const TPL_NONE = 4; // end() with no headers set
|
|
98
|
+
const TPL_HEADERS = [
|
|
99
|
+
JSON_HEADER_PAIR,
|
|
100
|
+
['content-type', 'application/json; charset=utf-8'],
|
|
101
|
+
['content-type', 'text/plain; charset=utf-8'],
|
|
102
|
+
['content-type', 'application/octet-stream'],
|
|
103
|
+
null,
|
|
104
|
+
];
|
|
105
|
+
const TPL_CONTENT_TYPE = TPL_HEADERS.map(h => h?.[1]);
|
|
106
|
+
const TPL_KINDS = TPL_HEADERS.length;
|
|
107
|
+
// Content-Type sniff for send(): does the body start with '{' or '[' after
|
|
108
|
+
// optional leading whitespace? Regex `\s` matches exactly the set trimStart()
|
|
109
|
+
// strips, and the match stops at the first non-space character, so this costs
|
|
110
|
+
// O(leading whitespace) and allocates nothing - trimStart() copied the entire
|
|
111
|
+
// body just to read one character.
|
|
112
|
+
const JSON_BODY_START = /^\s*[{[]/;
|
|
83
113
|
/**
|
|
84
114
|
* Prototype-based request object for the engine adapter.
|
|
85
115
|
*
|
|
@@ -260,7 +290,7 @@ export class EngineRequest extends LazyEventEmitter {
|
|
|
260
290
|
this._ip = value;
|
|
261
291
|
}
|
|
262
292
|
// Minimal net.Socket-shaped shim. `encrypted` reflects whether the engine
|
|
263
|
-
// is terminating TLS for this server (engine >= 1.
|
|
293
|
+
// is terminating TLS for this server (engine >= 1.1.6); when it isn't and
|
|
264
294
|
// TLS is proxy-terminated, middleware should read `x-forwarded-proto`. This
|
|
265
295
|
// exists so code written against Node's IncomingMessage (req.socket.encrypted,
|
|
266
296
|
// the CSRF middleware; req.connection.remoteAddress, rate-limiters) doesn't
|
|
@@ -423,6 +453,11 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
423
453
|
// materialize a header object (see _headersFlat / the json fast path)
|
|
424
454
|
_responseHeaders = undefined;
|
|
425
455
|
_locals = undefined;
|
|
456
|
+
// The default content-type a template reply carried when the app set no
|
|
457
|
+
// headers: send()/sendStatus()/the framework's own error replies no longer
|
|
458
|
+
// materialise a header object for it, so getHeader('content-type') and a
|
|
459
|
+
// late responseHeaders read answer from here (parity with the old path).
|
|
460
|
+
_impliedContentType = undefined;
|
|
426
461
|
// Terminal-write latch: headersSent means "head flushed" (true mid-stream
|
|
427
462
|
// after writeHead/write), _ended means the body is complete
|
|
428
463
|
_ended = false;
|
|
@@ -437,6 +472,11 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
437
472
|
let h = this._responseHeaders;
|
|
438
473
|
if (h === undefined) {
|
|
439
474
|
h = {};
|
|
475
|
+
// A reply already sent through a template carried this content-type;
|
|
476
|
+
// a reader materialising the object afterwards (a 'finish' listener,
|
|
477
|
+
// a logger) must still see it.
|
|
478
|
+
if (this._impliedContentType !== undefined)
|
|
479
|
+
h['content-type'] = this._impliedContentType;
|
|
440
480
|
this._responseHeaders = h;
|
|
441
481
|
}
|
|
442
482
|
return h;
|
|
@@ -543,7 +583,11 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
543
583
|
}
|
|
544
584
|
getHeader(name) {
|
|
545
585
|
const headers = this._responseHeaders;
|
|
546
|
-
|
|
586
|
+
const lower = name.toLowerCase();
|
|
587
|
+
if (headers === undefined) {
|
|
588
|
+
return lower === 'content-type' ? this._impliedContentType : undefined;
|
|
589
|
+
}
|
|
590
|
+
return headers[lower];
|
|
547
591
|
}
|
|
548
592
|
removeHeader(name) {
|
|
549
593
|
const headers = this._responseHeaders;
|
|
@@ -611,9 +655,16 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
611
655
|
try {
|
|
612
656
|
const rh = this._responseHeaders;
|
|
613
657
|
if (rh === undefined && !this._server._compression.enabled) {
|
|
614
|
-
// Hot path: no user-set headers, no compression - one native call
|
|
615
|
-
//
|
|
616
|
-
|
|
658
|
+
// Hot path: no user-set headers, no compression - one native call.
|
|
659
|
+
// With templates (engine >= 1.1.6) the content-type pair was
|
|
660
|
+
// materialised once in the engine; otherwise the shared pair goes
|
|
661
|
+
// with the call. Zero per-response header work either way.
|
|
662
|
+
const tpl = this._server._template(TPL_JSON, this.statusCode);
|
|
663
|
+
const rp = this._server._respondPrepared;
|
|
664
|
+
if (tpl > 0 && rp !== undefined)
|
|
665
|
+
rp(this._reqId, tpl, body);
|
|
666
|
+
else
|
|
667
|
+
this._server._respond(this._reqId, this.statusCode, JSON_HEADER_PAIR, body);
|
|
617
668
|
this.headersSent = true;
|
|
618
669
|
this._emitDone();
|
|
619
670
|
return;
|
|
@@ -697,26 +748,72 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
697
748
|
this._emitDone();
|
|
698
749
|
}
|
|
699
750
|
}
|
|
751
|
+
// Terminal reply carrying one of the framework's fixed header sets (see
|
|
752
|
+
// TPL_*): through a prepared template when the engine has them, else
|
|
753
|
+
// respond() with the same pair - identical bytes, no header object either
|
|
754
|
+
// way. Callers guarantee the app set no headers of its own.
|
|
755
|
+
_sendTemplated(kind, body) {
|
|
756
|
+
try {
|
|
757
|
+
const tpl = this._server._template(kind, this.statusCode);
|
|
758
|
+
const hasBody = body !== undefined && body !== null && body !== '';
|
|
759
|
+
const rp = this._server._respondPrepared;
|
|
760
|
+
const rpe = this._server._respondPreparedEmpty;
|
|
761
|
+
if (tpl > 0 && rp !== undefined && rpe !== undefined) {
|
|
762
|
+
if (hasBody)
|
|
763
|
+
rp(this._reqId, tpl, body);
|
|
764
|
+
else
|
|
765
|
+
rpe(this._reqId, tpl);
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
this._server._respond(this._reqId, this.statusCode, TPL_HEADERS[kind] ?? null, hasBody ? body : null);
|
|
769
|
+
}
|
|
770
|
+
this.headersSent = true;
|
|
771
|
+
this._emitDone();
|
|
772
|
+
}
|
|
773
|
+
catch (err) {
|
|
774
|
+
this._failSafe('Failed to send response', err);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/** @internal The framework's own constant JSON replies (404/413/400/500):
|
|
778
|
+
* the same bytes as setHeader('Content-Type','application/json') + end(body)
|
|
779
|
+
* (setHeader lowercases the name), without allocating a header object when
|
|
780
|
+
* the app set none. */
|
|
781
|
+
_sendConstantJson(status, body) {
|
|
782
|
+
if (this.headersSent || this._ended)
|
|
783
|
+
return;
|
|
784
|
+
this.statusCode = status;
|
|
785
|
+
if (this._responseHeaders === undefined) {
|
|
786
|
+
this._impliedContentType = TPL_CONTENT_TYPE[TPL_JSON];
|
|
787
|
+
this._sendTemplated(TPL_JSON, body);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
this._responseHeaders['content-type'] = 'application/json';
|
|
791
|
+
this.end(body);
|
|
792
|
+
}
|
|
700
793
|
send(data) {
|
|
701
794
|
if (this.headersSent || this._ended)
|
|
702
795
|
return;
|
|
703
796
|
// Default a Content-Type to match the Node server (parity for the now-
|
|
704
797
|
// default engine path): JSON for JSON-looking strings, octet-stream for
|
|
705
798
|
// Buffers, text/plain otherwise.
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
799
|
+
const rh = this._responseHeaders;
|
|
800
|
+
if (rh === undefined || !('content-type' in rh)) {
|
|
801
|
+
const kind = typeof data === 'string'
|
|
802
|
+
? JSON_BODY_START.test(data)
|
|
803
|
+
? TPL_JSON_UTF8
|
|
804
|
+
: TPL_TEXT_UTF8
|
|
805
|
+
: TPL_OCTET;
|
|
806
|
+
if (rh === undefined && !this._server._compression.enabled) {
|
|
807
|
+
// No app headers, no compression: the implied content-type rides a
|
|
808
|
+
// prepared template (or the shared pair) - no header object at all.
|
|
809
|
+
// Buffers pass through binary-safe (the engine accepts Uint8Array).
|
|
810
|
+
this._impliedContentType = TPL_CONTENT_TYPE[kind];
|
|
811
|
+
this._sendTemplated(kind, data);
|
|
812
|
+
return;
|
|
716
813
|
}
|
|
814
|
+
this.responseHeaders['content-type'] = TPL_CONTENT_TYPE[kind];
|
|
717
815
|
}
|
|
718
816
|
try {
|
|
719
|
-
// Buffers pass through binary-safe (the engine accepts Uint8Array).
|
|
720
817
|
// Compression applies only to compressible content types (isCompressible
|
|
721
818
|
// excludes octet-stream), so binary sends stay on the sync fast path.
|
|
722
819
|
this._respondMaybeCompressed(data);
|
|
@@ -789,13 +886,36 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
789
886
|
return this;
|
|
790
887
|
}
|
|
791
888
|
try {
|
|
889
|
+
const hasBody = data !== undefined && data !== null;
|
|
792
890
|
if (!this.headersSent) {
|
|
793
|
-
// Terminal single-shot: status + headers + body in one native call
|
|
794
|
-
|
|
891
|
+
// Terminal single-shot: status + headers + body in one native call.
|
|
892
|
+
// No app headers: the "no headers" template (engine >= 1.1.6) or a
|
|
893
|
+
// null header list - identical bytes.
|
|
894
|
+
if (this._responseHeaders === undefined) {
|
|
895
|
+
const tpl = this._server._template(TPL_NONE, this.statusCode);
|
|
896
|
+
const rp = this._server._respondPrepared;
|
|
897
|
+
const rpe = this._server._respondPreparedEmpty;
|
|
898
|
+
if (tpl > 0 && rp !== undefined && rpe !== undefined) {
|
|
899
|
+
if (hasBody)
|
|
900
|
+
rp(this._reqId, tpl, data);
|
|
901
|
+
else
|
|
902
|
+
rpe(this._reqId, tpl);
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
this._server._respond(this._reqId, this.statusCode, null, hasBody ? data : null);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
this._server._respond(this._reqId, this.statusCode, this._headersFlat(), hasBody ? data : null);
|
|
910
|
+
}
|
|
795
911
|
this.headersSent = true;
|
|
796
912
|
}
|
|
913
|
+
else if (hasBody && this._server._endWith !== undefined) {
|
|
914
|
+
// Streaming terminal with a chunk: the fixed-arity twin of end()
|
|
915
|
+
this._server._endWith(this._reqId, data);
|
|
916
|
+
}
|
|
797
917
|
else {
|
|
798
|
-
this._server._end(this._reqId,
|
|
918
|
+
this._server._end(this._reqId, hasBody ? data : undefined);
|
|
799
919
|
}
|
|
800
920
|
this._emitDone();
|
|
801
921
|
if (typeof callback === 'function')
|
|
@@ -1221,6 +1341,11 @@ export class EngineResponse extends LazyEventEmitter {
|
|
|
1221
1341
|
// (Node behavior) - never a fabricated "OK" for a non-2xx status.
|
|
1222
1342
|
const statusString = STATUS_STRINGS.get(code);
|
|
1223
1343
|
const body = statusString ? statusString.slice(String(code).length + 1) : String(code);
|
|
1344
|
+
if (this._responseHeaders === undefined && !this._ended) {
|
|
1345
|
+
this._impliedContentType = TPL_CONTENT_TYPE[TPL_TEXT_UTF8];
|
|
1346
|
+
this._sendTemplated(TPL_TEXT_UTF8, body);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1224
1349
|
this.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
1225
1350
|
this.end(body);
|
|
1226
1351
|
}
|
|
@@ -1301,8 +1426,28 @@ export class MoroEngineServer {
|
|
|
1301
1426
|
/** @internal */ _getBody;
|
|
1302
1427
|
/** @internal */ _getRemoteAddress;
|
|
1303
1428
|
/** @internal */ _getMethod;
|
|
1429
|
+
// Prepared response templates (engine >= 1.1.6): assigned only when the
|
|
1430
|
+
// engine advertises capabilities.responseTemplates AND the functions exist,
|
|
1431
|
+
// so a flag/binary mismatch can never put `undefined` into a hot call site.
|
|
1432
|
+
/** @internal */ _prepareResponse;
|
|
1433
|
+
/** @internal */ _respondPrepared;
|
|
1434
|
+
/** @internal */ _respondPreparedEmpty;
|
|
1435
|
+
/** @internal */ _endWith;
|
|
1436
|
+
_templatesOn = false;
|
|
1437
|
+
// Batched pipelined dispatch (engine >= 1.1.6, capabilities.batchDispatch):
|
|
1438
|
+
// the engine parses complete pipelined requests ahead and delivers them in
|
|
1439
|
+
// one onRequestBatch(count) call; the descriptors/control/paths buffers are
|
|
1440
|
+
// the engine's (getBatchBuffers), fetched once per native server.
|
|
1441
|
+
_batchDesc = undefined;
|
|
1442
|
+
_batchCtl = undefined;
|
|
1443
|
+
_batchPaths = undefined;
|
|
1444
|
+
_batchOn = false;
|
|
1445
|
+
_getBatchBuffers;
|
|
1446
|
+
_getPath;
|
|
1447
|
+
// (kind -> status -> template id), prepared on first use, per engine server
|
|
1448
|
+
_tpl = Array.from({ length: TPL_KINDS }, () => new Map());
|
|
1304
1449
|
/** @internal read by EngineRequest#protocol/socket.encrypted - true when the
|
|
1305
|
-
* engine is terminating TLS for this server (engine >= 1.
|
|
1450
|
+
* engine is terminating TLS for this server (engine >= 1.1.6 + ssl passed) */
|
|
1306
1451
|
isSsl = false;
|
|
1307
1452
|
serverId;
|
|
1308
1453
|
globalMiddleware = [];
|
|
@@ -1397,6 +1542,24 @@ export class MoroEngineServer {
|
|
|
1397
1542
|
this._getBody = surface.getBody;
|
|
1398
1543
|
this._getRemoteAddress = surface.getRemoteAddress;
|
|
1399
1544
|
this._getMethod = surface.getMethod;
|
|
1545
|
+
if (this._capabilities?.responseTemplates === true &&
|
|
1546
|
+
typeof surface.prepareResponse === 'function' &&
|
|
1547
|
+
typeof surface.respondPrepared === 'function' &&
|
|
1548
|
+
typeof surface.respondPreparedEmpty === 'function' &&
|
|
1549
|
+
typeof surface.endWith === 'function') {
|
|
1550
|
+
this._prepareResponse = surface.prepareResponse;
|
|
1551
|
+
this._respondPrepared = surface.respondPrepared;
|
|
1552
|
+
this._respondPreparedEmpty = surface.respondPreparedEmpty;
|
|
1553
|
+
this._endWith = surface.endWith;
|
|
1554
|
+
this._templatesOn = true;
|
|
1555
|
+
}
|
|
1556
|
+
if (this._capabilities?.batchDispatch === true &&
|
|
1557
|
+
typeof surface.getBatchBuffers === 'function' &&
|
|
1558
|
+
typeof surface.getPath === 'function') {
|
|
1559
|
+
this._getBatchBuffers = surface.getBatchBuffers;
|
|
1560
|
+
this._getPath = surface.getPath;
|
|
1561
|
+
this._batchOn = true;
|
|
1562
|
+
}
|
|
1400
1563
|
// TLS: pass it through when the engine supports it; otherwise keep the
|
|
1401
1564
|
// (now capability-gated) warning so a proxy/node fallback is clear.
|
|
1402
1565
|
if (this._ssl && this._capabilities?.tls) {
|
|
@@ -1432,6 +1595,37 @@ export class MoroEngineServer {
|
|
|
1432
1595
|
_complete(reqId) {
|
|
1433
1596
|
this.inflight.delete(reqId);
|
|
1434
1597
|
}
|
|
1598
|
+
/** @internal Template id for one of the framework's fixed header sets
|
|
1599
|
+
* (TPL_*) at a status, materialised in the engine on first use; -1 when
|
|
1600
|
+
* the engine has no templates (callers then respond() with the same
|
|
1601
|
+
* headers) or the engine's per-server store is full. */
|
|
1602
|
+
_template(kind, status) {
|
|
1603
|
+
const prep = this._prepareResponse;
|
|
1604
|
+
if (!this._templatesOn || prep === undefined)
|
|
1605
|
+
return -1;
|
|
1606
|
+
let m = this._tpl[kind];
|
|
1607
|
+
if (m === undefined) {
|
|
1608
|
+
m = new Map();
|
|
1609
|
+
this._tpl[kind] = m;
|
|
1610
|
+
}
|
|
1611
|
+
let id = m.get(status);
|
|
1612
|
+
if (id === undefined) {
|
|
1613
|
+
try {
|
|
1614
|
+
id = prep(this.serverId, status, TPL_HEADERS[kind]);
|
|
1615
|
+
}
|
|
1616
|
+
catch {
|
|
1617
|
+
id = -1; // store full (4096/server) - a caller bug elsewhere; respond() still works
|
|
1618
|
+
}
|
|
1619
|
+
m.set(status, id);
|
|
1620
|
+
}
|
|
1621
|
+
return id;
|
|
1622
|
+
}
|
|
1623
|
+
// Template ids belong to one native server: forget them whenever that
|
|
1624
|
+
// server is replaced (close() then listen() registers a fresh one).
|
|
1625
|
+
_resetTemplates() {
|
|
1626
|
+
for (const m of this._tpl)
|
|
1627
|
+
m.clear();
|
|
1628
|
+
}
|
|
1435
1629
|
// Register callbacks + limits with the native engine, returning the new
|
|
1436
1630
|
// serverId. Called from the constructor and again by listen() after a
|
|
1437
1631
|
// close() (the old native server is gone; a restart needs a fresh one).
|
|
@@ -1488,14 +1682,70 @@ export class MoroEngineServer {
|
|
|
1488
1682
|
...(this._http2Settings.settings ?? {}),
|
|
1489
1683
|
};
|
|
1490
1684
|
}
|
|
1491
|
-
|
|
1492
|
-
onRequest: (reqId, methodIdx, path) => this.
|
|
1685
|
+
const callbacks = {
|
|
1686
|
+
onRequest: (reqId, methodIdx, path) => this.dispatchOne(reqId, methodIdx, path),
|
|
1493
1687
|
onAborted: (reqId) => this.onAborted(reqId),
|
|
1494
1688
|
onWritable: (reqId) => this.onWritable(reqId),
|
|
1495
1689
|
onWsOpen: (wsId, path) => this._wsBridge?.onOpen(wsId, path, this._pendingWsInfo),
|
|
1496
1690
|
onWsMessage: (wsId, data, isBinary) => this._wsBridge?.onMessage(wsId, data, isBinary),
|
|
1497
1691
|
onWsClose: (wsId, code) => this._wsBridge?.onClose(wsId, code),
|
|
1498
|
-
}
|
|
1692
|
+
};
|
|
1693
|
+
if (this._batchOn)
|
|
1694
|
+
callbacks.onRequestBatch = (count) => this.onRequestBatch(count);
|
|
1695
|
+
const serverId = this._engine.serve(callbacks, serveOptions);
|
|
1696
|
+
if (this._batchOn && this._getBatchBuffers) {
|
|
1697
|
+
const b = this._getBatchBuffers(serverId);
|
|
1698
|
+
this._batchDesc = b.descriptors;
|
|
1699
|
+
this._batchCtl = b.control;
|
|
1700
|
+
this._batchPaths = b.paths;
|
|
1701
|
+
}
|
|
1702
|
+
return serverId;
|
|
1703
|
+
}
|
|
1704
|
+
// Batched pipelined dispatch: `count` requests are ready, described three
|
|
1705
|
+
// Uint32 per slot (reqId, methodIdx, pathIdx). Dispatch in order; after
|
|
1706
|
+
// each, the control cell says which slot the engine has activated (the next
|
|
1707
|
+
// one only if this handler completed synchronously) - continue there, or
|
|
1708
|
+
// stop when it still points at the slot just dispatched (async handler;
|
|
1709
|
+
// the engine re-delivers the rest once that response completes).
|
|
1710
|
+
onRequestBatch(count) {
|
|
1711
|
+
const d = this._batchDesc;
|
|
1712
|
+
const ctl = this._batchCtl;
|
|
1713
|
+
const paths = this._batchPaths;
|
|
1714
|
+
const getPath = this._getPath;
|
|
1715
|
+
if (d === undefined || ctl === undefined || paths === undefined || getPath === undefined)
|
|
1716
|
+
return 0;
|
|
1717
|
+
let i = 0;
|
|
1718
|
+
for (;;) {
|
|
1719
|
+
const reqId = d[3 * i];
|
|
1720
|
+
const methodIdx = d[3 * i + 1];
|
|
1721
|
+
const pi = d[3 * i + 2];
|
|
1722
|
+
const path = pi === 0xffffffff ? getPath(reqId) : paths[pi];
|
|
1723
|
+
this.dispatchOne(reqId, methodIdx, path);
|
|
1724
|
+
const next = ctl[0];
|
|
1725
|
+
if (next === i)
|
|
1726
|
+
return i + 1;
|
|
1727
|
+
if (next >= count)
|
|
1728
|
+
return count;
|
|
1729
|
+
i = next;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
// One request from the engine (single dispatch or a batch slot).
|
|
1733
|
+
dispatchOne(reqId, methodIdx, path) {
|
|
1734
|
+
try {
|
|
1735
|
+
this.onRequest(reqId, methodIdx, path);
|
|
1736
|
+
}
|
|
1737
|
+
catch (err) {
|
|
1738
|
+
// A synchronous throw out of dispatch must not stall the pipelined
|
|
1739
|
+
// requests behind this one: answer 500 here (the async paths have
|
|
1740
|
+
// their own error boundary inside handleRequest).
|
|
1741
|
+
this.logger.error(`Unhandled error dispatching ${path}: ${err?.message ?? err}`, 'Request');
|
|
1742
|
+
try {
|
|
1743
|
+
this._respond(reqId, 500, ['content-type', 'application/json'], '{"error":"Internal Server Error"}');
|
|
1744
|
+
}
|
|
1745
|
+
catch {
|
|
1746
|
+
// already answered or gone
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1499
1749
|
}
|
|
1500
1750
|
// ---- WebSocket bridge (used by EngineWebSocketAdapter) ----
|
|
1501
1751
|
/** Register the engine WebSocket bridge; upgrade requests then route natively. */
|
|
@@ -1779,9 +2029,7 @@ export class MoroEngineServer {
|
|
|
1779
2029
|
if (await this.dispatchDirectRoute(httpReq, httpRes))
|
|
1780
2030
|
return;
|
|
1781
2031
|
if (!httpRes.headersSent && !httpRes.writableEnded) {
|
|
1782
|
-
httpRes.
|
|
1783
|
-
httpRes.setHeader('Content-Type', 'application/json');
|
|
1784
|
-
httpRes.end('{"success":false,"error":"Not found"}');
|
|
2032
|
+
httpRes._sendConstantJson(404, '{"success":false,"error":"Not found"}');
|
|
1785
2033
|
}
|
|
1786
2034
|
}
|
|
1787
2035
|
catch (error) {
|
|
@@ -1816,9 +2064,13 @@ export class MoroEngineServer {
|
|
|
1816
2064
|
// rejection). Registration of the response hooks above still stands.
|
|
1817
2065
|
if (httpRes.headersSent)
|
|
1818
2066
|
return;
|
|
1819
|
-
// Execute global middleware chain
|
|
2067
|
+
// Execute global middleware chain. executeMiddleware returns undefined
|
|
2068
|
+
// when the whole chain completed synchronously, so a sync chain costs
|
|
2069
|
+
// zero promise allocations.
|
|
1820
2070
|
if (this.globalMiddleware.length > 0) {
|
|
1821
|
-
|
|
2071
|
+
const mwResult = this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
|
|
2072
|
+
if (mwResult)
|
|
2073
|
+
await mwResult;
|
|
1822
2074
|
if (httpRes.headersSent)
|
|
1823
2075
|
return;
|
|
1824
2076
|
}
|
|
@@ -1846,9 +2098,7 @@ export class MoroEngineServer {
|
|
|
1846
2098
|
return;
|
|
1847
2099
|
// No route matched
|
|
1848
2100
|
if (!httpRes.headersSent && !httpRes.writableEnded) {
|
|
1849
|
-
httpRes.
|
|
1850
|
-
httpRes.setHeader('Content-Type', 'application/json');
|
|
1851
|
-
httpRes.end('{"success":false,"error":"Not found"}');
|
|
2101
|
+
httpRes._sendConstantJson(404, '{"success":false,"error":"Not found"}');
|
|
1852
2102
|
}
|
|
1853
2103
|
}
|
|
1854
2104
|
catch (error) {
|
|
@@ -1861,16 +2111,12 @@ export class MoroEngineServer {
|
|
|
1861
2111
|
{
|
|
1862
2112
|
// Payload-too-large: respond 413 rather than a generic 500
|
|
1863
2113
|
if (error?.statusCode === 413 && !httpRes.writableEnded && !httpRes.headersSent) {
|
|
1864
|
-
httpRes.
|
|
1865
|
-
httpRes.setHeader('Content-Type', 'application/json');
|
|
1866
|
-
httpRes.end('{"success":false,"error":"Request entity too large"}');
|
|
2114
|
+
httpRes._sendConstantJson(413, '{"success":false,"error":"Request entity too large"}');
|
|
1867
2115
|
return;
|
|
1868
2116
|
}
|
|
1869
2117
|
// Malformed body (parseBody): a client error, not a server error
|
|
1870
2118
|
if (error?.statusCode === 400 && !httpRes.writableEnded && !httpRes.headersSent) {
|
|
1871
|
-
httpRes.
|
|
1872
|
-
httpRes.setHeader('Content-Type', 'application/json');
|
|
1873
|
-
httpRes.end('{"success":false,"error":"Invalid request body"}');
|
|
2119
|
+
httpRes._sendConstantJson(400, '{"success":false,"error":"Invalid request body"}');
|
|
1874
2120
|
return;
|
|
1875
2121
|
}
|
|
1876
2122
|
this.logger.error(`Request handling error: ${error instanceof Error ? error.message : String(error)}`, 'RequestError');
|
|
@@ -1895,9 +2141,7 @@ export class MoroEngineServer {
|
|
|
1895
2141
|
// Send error response if not already sent
|
|
1896
2142
|
if (!httpRes.writableEnded && !httpRes.headersSent) {
|
|
1897
2143
|
try {
|
|
1898
|
-
httpRes.
|
|
1899
|
-
httpRes.setHeader('Content-Type', 'application/json');
|
|
1900
|
-
httpRes.end('{"success":false,"error":"Internal server error"}');
|
|
2144
|
+
httpRes._sendConstantJson(500, '{"success":false,"error":"Internal server error"}');
|
|
1901
2145
|
}
|
|
1902
2146
|
catch {
|
|
1903
2147
|
this.logger.error('Failed to send error response', 'ResponseError');
|
|
@@ -1957,42 +2201,103 @@ export class MoroEngineServer {
|
|
|
1957
2201
|
throw error;
|
|
1958
2202
|
}
|
|
1959
2203
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2204
|
+
// Middleware execution with Express-compatible error propagation.
|
|
2205
|
+
// Supports: 3-arg (req, res, next), 4-arg (err, req, res, next) error middlewares,
|
|
2206
|
+
// and next(err) to skip forward to the next error middleware.
|
|
2207
|
+
//
|
|
2208
|
+
// Sync-aware dispatch (mirrors MoroHttpServer.executeMiddleware): middleware
|
|
2209
|
+
// that calls next() synchronously (or throws synchronously) advances the
|
|
2210
|
+
// chain in a plain loop with ZERO promise allocations. A promise is only
|
|
2211
|
+
// created when a middleware actually completes asynchronously. Returns
|
|
2212
|
+
// undefined when the whole chain ran synchronously, so the caller can skip
|
|
2213
|
+
// its await - one sync global middleware used to cost a promise, a closure
|
|
2214
|
+
// and an async-context suspension on every single request.
|
|
2215
|
+
executeMiddleware(middleware, req, res) {
|
|
2216
|
+
return this.dispatchMiddleware(middleware, req, res, 0, undefined);
|
|
2217
|
+
}
|
|
2218
|
+
dispatchMiddleware(middleware, req, res, startIndex, initialError) {
|
|
2219
|
+
const len = middleware.length;
|
|
2220
|
+
let activeError = initialError;
|
|
2221
|
+
let i = startIndex;
|
|
2222
|
+
while (i < len) {
|
|
1968
2223
|
if (res.headersSent)
|
|
1969
|
-
|
|
2224
|
+
return;
|
|
2225
|
+
const mw = middleware[i];
|
|
2226
|
+
i++;
|
|
1970
2227
|
const isErrorHandler = mw.length >= 4;
|
|
1971
|
-
|
|
2228
|
+
// Non-error middleware is skipped while an error is active; error middleware
|
|
2229
|
+
// is skipped while no error is active. Matches Express semantics.
|
|
2230
|
+
if (activeError !== undefined && !isErrorHandler)
|
|
1972
2231
|
continue;
|
|
1973
|
-
if (
|
|
2232
|
+
if (activeError === undefined && isErrorHandler)
|
|
1974
2233
|
continue;
|
|
2234
|
+
let settled = false;
|
|
2235
|
+
let settledError = undefined;
|
|
2236
|
+
let asyncResolve;
|
|
2237
|
+
const next = (err) => {
|
|
2238
|
+
if (settled)
|
|
2239
|
+
return;
|
|
2240
|
+
settled = true;
|
|
2241
|
+
settledError = err;
|
|
2242
|
+
if (asyncResolve)
|
|
2243
|
+
asyncResolve(err);
|
|
2244
|
+
};
|
|
2245
|
+
let result;
|
|
1975
2246
|
try {
|
|
1976
|
-
|
|
1977
|
-
const next = (err) => (err ? reject(err) : resolve());
|
|
1978
|
-
try {
|
|
1979
|
-
const result = error ? mw(error, req, res, next) : mw(req, res, next);
|
|
1980
|
-
if (result && typeof result.then === 'function') {
|
|
1981
|
-
result.then(() => resolve(), reject);
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
catch (e) {
|
|
1985
|
-
reject(e);
|
|
1986
|
-
}
|
|
1987
|
-
});
|
|
1988
|
-
error = null; // handler (error or normal) completed cleanly
|
|
2247
|
+
result = isErrorHandler ? mw(activeError, req, res, next) : mw(req, res, next);
|
|
1989
2248
|
}
|
|
1990
|
-
catch (
|
|
1991
|
-
|
|
2249
|
+
catch (err) {
|
|
2250
|
+
if (!settled) {
|
|
2251
|
+
settled = true;
|
|
2252
|
+
settledError = err;
|
|
2253
|
+
}
|
|
1992
2254
|
}
|
|
2255
|
+
const isThenable = result && typeof result.then === 'function';
|
|
2256
|
+
if (settled) {
|
|
2257
|
+
// Completed synchronously (next() called sync, or threw sync). A still
|
|
2258
|
+
// pending returned promise no longer gates the chain once next() has
|
|
2259
|
+
// been called; swallow late rejections so they never go unhandled.
|
|
2260
|
+
if (isThenable) {
|
|
2261
|
+
result.then(undefined, () => { });
|
|
2262
|
+
}
|
|
2263
|
+
activeError = settledError;
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
if (!isThenable && res.headersSent) {
|
|
2267
|
+
// Terminal middleware (Express style): it answered the request and
|
|
2268
|
+
// deliberately never calls next(). The chain is finished - skip
|
|
2269
|
+
// allocating a promise that nothing would ever settle (the response was
|
|
2270
|
+
// already sent, so this only ever produced immediate garbage).
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
if (!isThenable) {
|
|
2274
|
+
// Sync-looking middleware that neither called next() nor returned a
|
|
2275
|
+
// promise: it will call next() later (e.g. from an event callback).
|
|
2276
|
+
// Fall back to a promise for the remainder of the chain.
|
|
2277
|
+
const resumeIndex = i;
|
|
2278
|
+
return new Promise(resolve => {
|
|
2279
|
+
asyncResolve = resolve;
|
|
2280
|
+
}).then(err => this.dispatchMiddleware(middleware, req, res, resumeIndex, err));
|
|
2281
|
+
}
|
|
2282
|
+
// Async middleware: completion is next() OR promise settle, whichever
|
|
2283
|
+
// happens first (same semantics as the previous implementation).
|
|
2284
|
+
const resumeIndex = i;
|
|
2285
|
+
return new Promise(resolve => {
|
|
2286
|
+
asyncResolve = resolve;
|
|
2287
|
+
result.then(() => {
|
|
2288
|
+
if (!settled)
|
|
2289
|
+
next();
|
|
2290
|
+
}, (err) => {
|
|
2291
|
+
if (!settled)
|
|
2292
|
+
next(err);
|
|
2293
|
+
});
|
|
2294
|
+
}).then(err => this.dispatchMiddleware(middleware, req, res, resumeIndex, err));
|
|
2295
|
+
}
|
|
2296
|
+
// If an error is still unhandled after the chain, re-throw so the top-level
|
|
2297
|
+
// catch in handleRequest can invoke the registered errorHandler / default 500.
|
|
2298
|
+
if (activeError !== undefined) {
|
|
2299
|
+
throw activeError;
|
|
1993
2300
|
}
|
|
1994
|
-
if (error)
|
|
1995
|
-
throw error;
|
|
1996
2301
|
}
|
|
1997
2302
|
// Public API - matches MoroHttpServer/UWebSocketsHttpServer interface
|
|
1998
2303
|
use(middleware) {
|
|
@@ -2038,6 +2343,7 @@ export class MoroEngineServer {
|
|
|
2038
2343
|
// http.Server supports close() -> listen() cycles.
|
|
2039
2344
|
if (this.closed) {
|
|
2040
2345
|
this.serverId = this.registerWithEngine();
|
|
2346
|
+
this._resetTemplates();
|
|
2041
2347
|
this.closed = false;
|
|
2042
2348
|
}
|
|
2043
2349
|
// Synchronous bind; throws on bind errors, returns the actual port
|
|
@@ -2088,6 +2394,7 @@ export class MoroEngineServer {
|
|
|
2088
2394
|
return;
|
|
2089
2395
|
}
|
|
2090
2396
|
this.inflight.clear();
|
|
2397
|
+
this._resetTemplates();
|
|
2091
2398
|
this.logger.info('Moro engine HTTP server closed', 'Close');
|
|
2092
2399
|
if (callback) {
|
|
2093
2400
|
// Give the event loop time to process the engine's socket teardown
|