@morojs/moro 1.8.9 → 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.
Files changed (33) hide show
  1. package/dist/core/cluster/thread-cluster.d.ts +178 -0
  2. package/dist/core/cluster/thread-cluster.js +389 -0
  3. package/dist/core/cluster/thread-cluster.js.map +1 -0
  4. package/dist/core/framework.js.map +1 -1
  5. package/dist/core/http/moro-engine-server.d.ts +28 -1
  6. package/dist/core/http/moro-engine-server.js +274 -36
  7. package/dist/core/http/moro-engine-server.js.map +1 -1
  8. package/dist/core/http/utils/compression.js +12 -6
  9. package/dist/core/http/utils/compression.js.map +1 -1
  10. package/dist/core/http/utils/uws-worker-clustering.js +10 -6
  11. package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
  12. package/dist/core/http/uws-http-server.js +3 -2
  13. package/dist/core/http/uws-http-server.js.map +1 -1
  14. package/dist/core/jobs/leader-election.js +16 -5
  15. package/dist/core/jobs/leader-election.js.map +1 -1
  16. package/dist/core/networking/websocket-manager.js +4 -2
  17. package/dist/core/networking/websocket-manager.js.map +1 -1
  18. package/dist/core/runtime/node-adapter.d.ts +1 -1
  19. package/dist/core/runtime/node-adapter.js.map +1 -1
  20. package/dist/core/utilities/builtin.d.ts +1 -0
  21. package/dist/core/utilities/builtin.js +25 -0
  22. package/dist/core/utilities/builtin.js.map +1 -0
  23. package/dist/core/utilities/package-utils.d.ts +28 -3
  24. package/dist/core/utilities/package-utils.js +24 -12
  25. package/dist/core/utilities/package-utils.js.map +1 -1
  26. package/dist/core/workers/worker-manager.js +4 -3
  27. package/dist/core/workers/worker-manager.js.map +1 -1
  28. package/dist/moro.d.ts +5 -0
  29. package/dist/moro.js +364 -204
  30. package/dist/moro.js.map +1 -1
  31. package/dist/types/http.d.ts +1 -1
  32. package/dist/types/http.js.map +1 -1
  33. package/package.json +1 -1
@@ -80,6 +80,30 @@ 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;
83
107
  // Content-Type sniff for send(): does the body start with '{' or '[' after
84
108
  // optional leading whitespace? Regex `\s` matches exactly the set trimStart()
85
109
  // strips, and the match stops at the first non-space character, so this costs
@@ -266,7 +290,7 @@ export class EngineRequest extends LazyEventEmitter {
266
290
  this._ip = value;
267
291
  }
268
292
  // Minimal net.Socket-shaped shim. `encrypted` reflects whether the engine
269
- // is terminating TLS for this server (engine >= 1.2.0); when it isn't and
293
+ // is terminating TLS for this server (engine >= 1.1.6); when it isn't and
270
294
  // TLS is proxy-terminated, middleware should read `x-forwarded-proto`. This
271
295
  // exists so code written against Node's IncomingMessage (req.socket.encrypted,
272
296
  // the CSRF middleware; req.connection.remoteAddress, rate-limiters) doesn't
@@ -429,6 +453,11 @@ export class EngineResponse extends LazyEventEmitter {
429
453
  // materialize a header object (see _headersFlat / the json fast path)
430
454
  _responseHeaders = undefined;
431
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;
432
461
  // Terminal-write latch: headersSent means "head flushed" (true mid-stream
433
462
  // after writeHead/write), _ended means the body is complete
434
463
  _ended = false;
@@ -443,6 +472,11 @@ export class EngineResponse extends LazyEventEmitter {
443
472
  let h = this._responseHeaders;
444
473
  if (h === undefined) {
445
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;
446
480
  this._responseHeaders = h;
447
481
  }
448
482
  return h;
@@ -549,7 +583,11 @@ export class EngineResponse extends LazyEventEmitter {
549
583
  }
550
584
  getHeader(name) {
551
585
  const headers = this._responseHeaders;
552
- return headers === undefined ? undefined : headers[name.toLowerCase()];
586
+ const lower = name.toLowerCase();
587
+ if (headers === undefined) {
588
+ return lower === 'content-type' ? this._impliedContentType : undefined;
589
+ }
590
+ return headers[lower];
553
591
  }
554
592
  removeHeader(name) {
555
593
  const headers = this._responseHeaders;
@@ -617,9 +655,16 @@ export class EngineResponse extends LazyEventEmitter {
617
655
  try {
618
656
  const rh = this._responseHeaders;
619
657
  if (rh === undefined && !this._server._compression.enabled) {
620
- // Hot path: no user-set headers, no compression - one native call
621
- // with the shared content-type pair; zero per-response header work
622
- this._server._respond(this._reqId, this.statusCode, JSON_HEADER_PAIR, body);
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);
623
668
  this.headersSent = true;
624
669
  this._emitDone();
625
670
  return;
@@ -703,24 +748,72 @@ export class EngineResponse extends LazyEventEmitter {
703
748
  this._emitDone();
704
749
  }
705
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
+ }
706
793
  send(data) {
707
794
  if (this.headersSent || this._ended)
708
795
  return;
709
796
  // Default a Content-Type to match the Node server (parity for the now-
710
797
  // default engine path): JSON for JSON-looking strings, octet-stream for
711
798
  // Buffers, text/plain otherwise.
712
- if (!('content-type' in this.responseHeaders)) {
713
- if (typeof data === 'string') {
714
- this.responseHeaders['content-type'] = JSON_BODY_START.test(data)
715
- ? 'application/json; charset=utf-8'
716
- : 'text/plain; charset=utf-8';
717
- }
718
- else {
719
- this.responseHeaders['content-type'] = 'application/octet-stream';
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;
720
813
  }
814
+ this.responseHeaders['content-type'] = TPL_CONTENT_TYPE[kind];
721
815
  }
722
816
  try {
723
- // Buffers pass through binary-safe (the engine accepts Uint8Array).
724
817
  // Compression applies only to compressible content types (isCompressible
725
818
  // excludes octet-stream), so binary sends stay on the sync fast path.
726
819
  this._respondMaybeCompressed(data);
@@ -793,13 +886,36 @@ export class EngineResponse extends LazyEventEmitter {
793
886
  return this;
794
887
  }
795
888
  try {
889
+ const hasBody = data !== undefined && data !== null;
796
890
  if (!this.headersSent) {
797
- // Terminal single-shot: status + headers + body in one native call
798
- this._server._respond(this._reqId, this.statusCode, this._headersFlat(), data !== undefined && data !== null ? data : null);
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
+ }
799
911
  this.headersSent = true;
800
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
+ }
801
917
  else {
802
- this._server._end(this._reqId, data !== undefined && data !== null ? data : undefined);
918
+ this._server._end(this._reqId, hasBody ? data : undefined);
803
919
  }
804
920
  this._emitDone();
805
921
  if (typeof callback === 'function')
@@ -1225,6 +1341,11 @@ export class EngineResponse extends LazyEventEmitter {
1225
1341
  // (Node behavior) - never a fabricated "OK" for a non-2xx status.
1226
1342
  const statusString = STATUS_STRINGS.get(code);
1227
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
+ }
1228
1349
  this.setHeader('Content-Type', 'text/plain; charset=utf-8');
1229
1350
  this.end(body);
1230
1351
  }
@@ -1305,8 +1426,28 @@ export class MoroEngineServer {
1305
1426
  /** @internal */ _getBody;
1306
1427
  /** @internal */ _getRemoteAddress;
1307
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());
1308
1449
  /** @internal read by EngineRequest#protocol/socket.encrypted - true when the
1309
- * engine is terminating TLS for this server (engine >= 1.2.0 + ssl passed) */
1450
+ * engine is terminating TLS for this server (engine >= 1.1.6 + ssl passed) */
1310
1451
  isSsl = false;
1311
1452
  serverId;
1312
1453
  globalMiddleware = [];
@@ -1401,6 +1542,24 @@ export class MoroEngineServer {
1401
1542
  this._getBody = surface.getBody;
1402
1543
  this._getRemoteAddress = surface.getRemoteAddress;
1403
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
+ }
1404
1563
  // TLS: pass it through when the engine supports it; otherwise keep the
1405
1564
  // (now capability-gated) warning so a proxy/node fallback is clear.
1406
1565
  if (this._ssl && this._capabilities?.tls) {
@@ -1436,6 +1595,37 @@ export class MoroEngineServer {
1436
1595
  _complete(reqId) {
1437
1596
  this.inflight.delete(reqId);
1438
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
+ }
1439
1629
  // Register callbacks + limits with the native engine, returning the new
1440
1630
  // serverId. Called from the constructor and again by listen() after a
1441
1631
  // close() (the old native server is gone; a restart needs a fresh one).
@@ -1492,14 +1682,70 @@ export class MoroEngineServer {
1492
1682
  ...(this._http2Settings.settings ?? {}),
1493
1683
  };
1494
1684
  }
1495
- return this._engine.serve({
1496
- onRequest: (reqId, methodIdx, path) => this.onRequest(reqId, methodIdx, path),
1685
+ const callbacks = {
1686
+ onRequest: (reqId, methodIdx, path) => this.dispatchOne(reqId, methodIdx, path),
1497
1687
  onAborted: (reqId) => this.onAborted(reqId),
1498
1688
  onWritable: (reqId) => this.onWritable(reqId),
1499
1689
  onWsOpen: (wsId, path) => this._wsBridge?.onOpen(wsId, path, this._pendingWsInfo),
1500
1690
  onWsMessage: (wsId, data, isBinary) => this._wsBridge?.onMessage(wsId, data, isBinary),
1501
1691
  onWsClose: (wsId, code) => this._wsBridge?.onClose(wsId, code),
1502
- }, serveOptions);
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
+ }
1503
1749
  }
1504
1750
  // ---- WebSocket bridge (used by EngineWebSocketAdapter) ----
1505
1751
  /** Register the engine WebSocket bridge; upgrade requests then route natively. */
@@ -1783,9 +2029,7 @@ export class MoroEngineServer {
1783
2029
  if (await this.dispatchDirectRoute(httpReq, httpRes))
1784
2030
  return;
1785
2031
  if (!httpRes.headersSent && !httpRes.writableEnded) {
1786
- httpRes.statusCode = 404;
1787
- httpRes.setHeader('Content-Type', 'application/json');
1788
- httpRes.end('{"success":false,"error":"Not found"}');
2032
+ httpRes._sendConstantJson(404, '{"success":false,"error":"Not found"}');
1789
2033
  }
1790
2034
  }
1791
2035
  catch (error) {
@@ -1854,9 +2098,7 @@ export class MoroEngineServer {
1854
2098
  return;
1855
2099
  // No route matched
1856
2100
  if (!httpRes.headersSent && !httpRes.writableEnded) {
1857
- httpRes.statusCode = 404;
1858
- httpRes.setHeader('Content-Type', 'application/json');
1859
- httpRes.end('{"success":false,"error":"Not found"}');
2101
+ httpRes._sendConstantJson(404, '{"success":false,"error":"Not found"}');
1860
2102
  }
1861
2103
  }
1862
2104
  catch (error) {
@@ -1869,16 +2111,12 @@ export class MoroEngineServer {
1869
2111
  {
1870
2112
  // Payload-too-large: respond 413 rather than a generic 500
1871
2113
  if (error?.statusCode === 413 && !httpRes.writableEnded && !httpRes.headersSent) {
1872
- httpRes.statusCode = 413;
1873
- httpRes.setHeader('Content-Type', 'application/json');
1874
- httpRes.end('{"success":false,"error":"Request entity too large"}');
2114
+ httpRes._sendConstantJson(413, '{"success":false,"error":"Request entity too large"}');
1875
2115
  return;
1876
2116
  }
1877
2117
  // Malformed body (parseBody): a client error, not a server error
1878
2118
  if (error?.statusCode === 400 && !httpRes.writableEnded && !httpRes.headersSent) {
1879
- httpRes.statusCode = 400;
1880
- httpRes.setHeader('Content-Type', 'application/json');
1881
- httpRes.end('{"success":false,"error":"Invalid request body"}');
2119
+ httpRes._sendConstantJson(400, '{"success":false,"error":"Invalid request body"}');
1882
2120
  return;
1883
2121
  }
1884
2122
  this.logger.error(`Request handling error: ${error instanceof Error ? error.message : String(error)}`, 'RequestError');
@@ -1903,9 +2141,7 @@ export class MoroEngineServer {
1903
2141
  // Send error response if not already sent
1904
2142
  if (!httpRes.writableEnded && !httpRes.headersSent) {
1905
2143
  try {
1906
- httpRes.statusCode = 500;
1907
- httpRes.setHeader('Content-Type', 'application/json');
1908
- httpRes.end('{"success":false,"error":"Internal server error"}');
2144
+ httpRes._sendConstantJson(500, '{"success":false,"error":"Internal server error"}');
1909
2145
  }
1910
2146
  catch {
1911
2147
  this.logger.error('Failed to send error response', 'ResponseError');
@@ -2107,6 +2343,7 @@ export class MoroEngineServer {
2107
2343
  // http.Server supports close() -> listen() cycles.
2108
2344
  if (this.closed) {
2109
2345
  this.serverId = this.registerWithEngine();
2346
+ this._resetTemplates();
2110
2347
  this.closed = false;
2111
2348
  }
2112
2349
  // Synchronous bind; throws on bind errors, returns the actual port
@@ -2157,6 +2394,7 @@ export class MoroEngineServer {
2157
2394
  return;
2158
2395
  }
2159
2396
  this.inflight.clear();
2397
+ this._resetTemplates();
2160
2398
  this.logger.info('Moro engine HTTP server closed', 'Close');
2161
2399
  if (callback) {
2162
2400
  // Give the event loop time to process the engine's socket teardown