@morojs/moro 1.8.6 → 1.8.8

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 (68) hide show
  1. package/dist/core/http/http-server.d.ts +5 -3
  2. package/dist/core/http/http-server.js +42 -11
  3. package/dist/core/http/http-server.js.map +1 -1
  4. package/dist/core/http/http2-server.d.ts +1 -1
  5. package/dist/core/http/http2-server.js +27 -9
  6. package/dist/core/http/http2-server.js.map +1 -1
  7. package/dist/core/http/moro-engine-server.d.ts +24 -4
  8. package/dist/core/http/moro-engine-server.js +198 -42
  9. package/dist/core/http/moro-engine-server.js.map +1 -1
  10. package/dist/core/http/uws-http-server.js +4 -0
  11. package/dist/core/http/uws-http-server.js.map +1 -1
  12. package/dist/core/middleware/built-in/cookie/core.d.ts +26 -1
  13. package/dist/core/middleware/built-in/cookie/core.js +70 -2
  14. package/dist/core/middleware/built-in/cookie/core.js.map +1 -1
  15. package/dist/core/middleware/built-in/cookie/hook.d.ts +3 -1
  16. package/dist/core/middleware/built-in/cookie/hook.js +17 -5
  17. package/dist/core/middleware/built-in/cookie/hook.js.map +1 -1
  18. package/dist/core/middleware/built-in/cookie/index.d.ts +1 -1
  19. package/dist/core/middleware/built-in/cookie/index.js +1 -1
  20. package/dist/core/middleware/built-in/cookie/index.js.map +1 -1
  21. package/dist/core/middleware/built-in/cookie/middleware.d.ts +1 -1
  22. package/dist/core/middleware/built-in/cookie/middleware.js +6 -4
  23. package/dist/core/middleware/built-in/cookie/middleware.js.map +1 -1
  24. package/dist/core/middleware/built-in/index.d.ts +4 -12
  25. package/dist/core/middleware/built-in/rate-limit/core.d.ts +24 -0
  26. package/dist/core/middleware/built-in/rate-limit/core.js +47 -0
  27. package/dist/core/middleware/built-in/rate-limit/core.js.map +1 -1
  28. package/dist/core/middleware/built-in/rate-limit/hook.d.ts +24 -9
  29. package/dist/core/middleware/built-in/rate-limit/hook.js +34 -12
  30. package/dist/core/middleware/built-in/rate-limit/hook.js.map +1 -1
  31. package/dist/core/middleware/built-in/rate-limit/middleware.js +9 -1
  32. package/dist/core/middleware/built-in/rate-limit/middleware.js.map +1 -1
  33. package/dist/core/middleware/built-in/session/core.d.ts +8 -0
  34. package/dist/core/middleware/built-in/session/core.js +29 -2
  35. package/dist/core/middleware/built-in/session/core.js.map +1 -1
  36. package/dist/core/middleware/built-in/session/hook.js +2 -3
  37. package/dist/core/middleware/built-in/session/hook.js.map +1 -1
  38. package/dist/core/middleware/built-in/session/middleware.js +2 -3
  39. package/dist/core/middleware/built-in/session/middleware.js.map +1 -1
  40. package/dist/core/middleware/built-in/static/core.d.ts +56 -0
  41. package/dist/core/middleware/built-in/static/core.js +214 -13
  42. package/dist/core/middleware/built-in/static/core.js.map +1 -1
  43. package/dist/core/middleware/built-in/static/middleware.d.ts +5 -0
  44. package/dist/core/middleware/built-in/static/middleware.js +5 -0
  45. package/dist/core/middleware/built-in/static/middleware.js.map +1 -1
  46. package/dist/core/middleware/built-in/upload/core.d.ts +17 -0
  47. package/dist/core/middleware/built-in/upload/core.js +30 -1
  48. package/dist/core/middleware/built-in/upload/core.js.map +1 -1
  49. package/dist/core/middleware/built-in/upload/middleware.js +12 -1
  50. package/dist/core/middleware/built-in/upload/middleware.js.map +1 -1
  51. package/dist/core/pooling/object-pool-manager.js +14 -20
  52. package/dist/core/pooling/object-pool-manager.js.map +1 -1
  53. package/dist/core/routing/index.d.ts +1 -0
  54. package/dist/core/routing/index.js.map +1 -1
  55. package/dist/core/routing/radix-tree.d.ts +1 -0
  56. package/dist/core/routing/radix-tree.js +6 -1
  57. package/dist/core/routing/radix-tree.js.map +1 -1
  58. package/dist/core/routing/unified-router.js +28 -25
  59. package/dist/core/routing/unified-router.js.map +1 -1
  60. package/dist/core/utilities/hooks.d.ts +2 -0
  61. package/dist/core/utilities/hooks.js +28 -2
  62. package/dist/core/utilities/hooks.js.map +1 -1
  63. package/dist/index.d.ts +2 -6
  64. package/dist/moro.js +12 -0
  65. package/dist/moro.js.map +1 -1
  66. package/dist/types/http.d.ts +3 -0
  67. package/dist/types/http.js.map +1 -1
  68. package/package.json +1 -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._engine.getQuery(this._reqId) ?? '';
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._engine.getHeaders(this._reqId);
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._engine.getRemoteAddress(this._reqId) ?? '';
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
- this._server._complete(this._reqId);
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.responseHeaders;
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 lowerName = name.toLowerCase();
496
- return this.responseHeaders[lowerName];
545
+ const headers = this._responseHeaders;
546
+ return headers === undefined ? undefined : headers[name.toLowerCase()];
497
547
  }
498
548
  removeHeader(name) {
499
- const lowerName = name.toLowerCase();
500
- if (lowerName in this.responseHeaders) {
501
- delete this.responseHeaders[lowerName];
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._engine.respond(this._reqId, this.statusCode, this._headersFlat(), body);
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 alreadyEncoded = 'content-encoding' in this.responseHeaders;
587
- const ctHeader = this.responseHeaders['content-type'];
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._engine.respond(this._reqId, this.statusCode, this._headersFlat(), body);
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._engine.getHeader(this._reqId, 'accept-encoding');
661
+ const accept = this._server._getHeader(this._reqId, 'accept-encoding');
600
662
  const encoding = negotiateEncoding(accept, s.encodings);
601
663
  if (!encoding) {
602
- this._engine.respond(this._reqId, this.statusCode, this._headersFlat(), body);
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._engine.isAborted(this._reqId))
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._engine.respond(this._reqId, this.statusCode, this._headersFlat(), compressed);
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._engine.respond(this._reqId, 500, null, '{"success":false,"error":"Internal server error"}');
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._engine.writeHead(this._reqId, statusCode, this._headersFlat());
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._engine.writeHead(this._reqId, this.statusCode, this._headersFlat());
766
+ this._server._writeHead(this._reqId, this.statusCode, this._headersFlat());
705
767
  this.headersSent = true;
706
768
  }
707
- ok = this._engine.write(this._reqId, chunk) !== false;
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._engine.respond(this._reqId, this.statusCode, this._headersFlat(), data !== undefined && data !== null ? data : null);
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._engine.end(this._reqId, data !== undefined && data !== null ? data : undefined);
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._engine.respond(this._reqId, redirectCode, this._headersFlat(), null);
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._engine.getHeader(reqId, 'upgrade');
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._engine.getRemoteAddress(reqId) || '';
1666
+ const ip = this._getRemoteAddress(reqId) || '';
1578
1667
  const headers = {};
1579
- const flat = this._engine.getHeaders(reqId);
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._engine.getQuery(reqId) || '';
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._engine.getMethod(reqId) || 'OTHER' : METHODS[methodIdx];
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
- void this.handleRequest(httpReq, httpRes);
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
@@ -1667,6 +1812,10 @@ export class MoroEngineServer {
1667
1812
  void Promise.resolve(hookManager.execute('response', { request: httpReq, response: httpRes })).catch((error) => this.logger?.error?.(`Response hook error: ${error?.message || error}`, 'Hooks'));
1668
1813
  });
1669
1814
  }
1815
+ // A request hook may answer the request itself (rate limiting, auth
1816
+ // rejection). Registration of the response hooks above still stands.
1817
+ if (httpRes.headersSent)
1818
+ return;
1670
1819
  // Execute global middleware chain
1671
1820
  if (this.globalMiddleware.length > 0) {
1672
1821
  await this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
@@ -1703,6 +1852,13 @@ export class MoroEngineServer {
1703
1852
  }
1704
1853
  }
1705
1854
  catch (error) {
1855
+ await this.handleDispatchError(error, httpReq, httpRes);
1856
+ }
1857
+ }
1858
+ // Error shaping shared by the async pipeline and the sync fast path:
1859
+ // 413/400 client responses, error hooks, the app's error handler, then 500.
1860
+ async handleDispatchError(error, httpReq, httpRes) {
1861
+ {
1706
1862
  // Payload-too-large: respond 413 rather than a generic 500
1707
1863
  if (error?.statusCode === 413 && !httpRes.writableEnded && !httpRes.headersSent) {
1708
1864
  httpRes.statusCode = 413;
@@ -1753,7 +1909,7 @@ export class MoroEngineServer {
1753
1909
  // semantics to the Node and uWS servers' readBody). getBody returns a STABLE
1754
1910
  // ArrayBuffer copy, so Buffer.from() views it with no aliasing hazard.
1755
1911
  parseBody(httpReq) {
1756
- const raw = this._engine.getBody(httpReq._reqId);
1912
+ const raw = this._getBody(httpReq._reqId);
1757
1913
  if (!raw || raw.byteLength === 0)
1758
1914
  return;
1759
1915
  const contentType = httpReq.headers['content-type'] || '';