@morojs/moro 1.8.12 → 1.8.14

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 (90) hide show
  1. package/README.md +4 -2
  2. package/dist/core/cluster/thread-cluster.d.ts +25 -0
  3. package/dist/core/cluster/thread-cluster.js +95 -3
  4. package/dist/core/cluster/thread-cluster.js.map +1 -1
  5. package/dist/core/config/config-validator.js +20 -0
  6. package/dist/core/config/config-validator.js.map +1 -1
  7. package/dist/core/config/index.js +19 -2
  8. package/dist/core/config/index.js.map +1 -1
  9. package/dist/core/config/schema.js +1 -0
  10. package/dist/core/config/schema.js.map +1 -1
  11. package/dist/core/framework.d.ts +26 -0
  12. package/dist/core/framework.js +195 -6
  13. package/dist/core/framework.js.map +1 -1
  14. package/dist/core/http/http-server.d.ts +19 -0
  15. package/dist/core/http/http-server.js +75 -1
  16. package/dist/core/http/http-server.js.map +1 -1
  17. package/dist/core/http/http2-server.d.ts +15 -0
  18. package/dist/core/http/http2-server.js +36 -3
  19. package/dist/core/http/http2-server.js.map +1 -1
  20. package/dist/core/http/moro-engine-server.d.ts +28 -0
  21. package/dist/core/http/moro-engine-server.js +138 -12
  22. package/dist/core/http/moro-engine-server.js.map +1 -1
  23. package/dist/core/http/utils/body-type.d.ts +12 -0
  24. package/dist/core/http/utils/body-type.js +70 -0
  25. package/dist/core/http/utils/body-type.js.map +1 -0
  26. package/dist/core/http/utils/compression.d.ts +21 -0
  27. package/dist/core/http/utils/compression.js +53 -7
  28. package/dist/core/http/utils/compression.js.map +1 -1
  29. package/dist/core/http/utils/ssl-config.d.ts +11 -0
  30. package/dist/core/http/utils/ssl-config.js +3 -0
  31. package/dist/core/http/utils/ssl-config.js.map +1 -1
  32. package/dist/core/http/utils/uws-worker-clustering.js +5 -4
  33. package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
  34. package/dist/core/http/uws-http-server.d.ts +3 -0
  35. package/dist/core/http/uws-http-server.js +47 -2
  36. package/dist/core/http/uws-http-server.js.map +1 -1
  37. package/dist/core/middleware/built-in/body-parsers/index.d.ts +18 -0
  38. package/dist/core/middleware/built-in/body-parsers/index.js +41 -0
  39. package/dist/core/middleware/built-in/body-parsers/index.js.map +1 -1
  40. package/dist/core/middleware/built-in/cache/adapters/cache/redis.d.ts +32 -8
  41. package/dist/core/middleware/built-in/cache/adapters/cache/redis.js +117 -25
  42. package/dist/core/middleware/built-in/cache/adapters/cache/redis.js.map +1 -1
  43. package/dist/core/middleware/built-in/compression/core.d.ts +7 -0
  44. package/dist/core/middleware/built-in/compression/core.js +32 -1
  45. package/dist/core/middleware/built-in/compression/core.js.map +1 -1
  46. package/dist/core/middleware/built-in/session/core.d.ts +1 -0
  47. package/dist/core/middleware/built-in/session/core.js.map +1 -1
  48. package/dist/core/middleware/built-in/static/core.d.ts +18 -0
  49. package/dist/core/middleware/built-in/static/core.js +128 -7
  50. package/dist/core/middleware/built-in/static/core.js.map +1 -1
  51. package/dist/core/networking/adapters/engine-adapter.d.ts +2 -2
  52. package/dist/core/networking/adapters/engine-adapter.js +40 -1
  53. package/dist/core/networking/adapters/engine-adapter.js.map +1 -1
  54. package/dist/core/networking/adapters/socketio-adapter.d.ts +2 -2
  55. package/dist/core/networking/adapters/socketio-adapter.js +10 -1
  56. package/dist/core/networking/adapters/socketio-adapter.js.map +1 -1
  57. package/dist/core/networking/adapters/uws-adapter.d.ts +2 -2
  58. package/dist/core/networking/adapters/uws-adapter.js +54 -3
  59. package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
  60. package/dist/core/networking/adapters/ws-adapter.d.ts +9 -2
  61. package/dist/core/networking/adapters/ws-adapter.js +97 -12
  62. package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
  63. package/dist/core/networking/websocket-adapter.d.ts +19 -1
  64. package/dist/core/networking/websocket-adapter.js.map +1 -1
  65. package/dist/core/routing/index.d.ts +12 -8
  66. package/dist/core/routing/index.js +7 -2
  67. package/dist/core/routing/index.js.map +1 -1
  68. package/dist/core/routing/unified-router.d.ts +46 -8
  69. package/dist/core/routing/unified-router.js +108 -32
  70. package/dist/core/routing/unified-router.js.map +1 -1
  71. package/dist/core/utilities/package-utils.d.ts +8 -1
  72. package/dist/core/utilities/package-utils.js +2 -0
  73. package/dist/core/utilities/package-utils.js.map +1 -1
  74. package/dist/core/workers/worker-manager.js +2 -2
  75. package/dist/core/workers/worker-manager.js.map +1 -1
  76. package/dist/index.d.ts +4 -3
  77. package/dist/index.js +2 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/moro.d.ts +12 -1
  80. package/dist/moro.js +63 -7
  81. package/dist/moro.js.map +1 -1
  82. package/dist/types/cache.d.ts +2 -0
  83. package/dist/types/cache.js.map +1 -1
  84. package/dist/types/config.d.ts +16 -0
  85. package/dist/types/config.js.map +1 -1
  86. package/dist/types/http.d.ts +6 -0
  87. package/dist/types/http.js.map +1 -1
  88. package/dist/types/session.d.ts +1 -0
  89. package/dist/types/session.js.map +1 -1
  90. package/package.json +1 -1
@@ -19,7 +19,8 @@ import { randomUUID } from 'crypto';
19
19
  import { createFrameworkLogger } from '../logger/index.js';
20
20
  import { loadNativeEngine, getNativeEngineLoadErrors, } from '../utilities/package-utils.js';
21
21
  import { parseMultipart as parseMultipartBuffer, } from './utils/multipart-parser.js';
22
- import { resolveCompressionSettings, compressBuffer, isCompressible, negotiateEncoding, } from './utils/compression.js';
22
+ import { resolveCompressionSettings, compressBuffer, compressBufferSync, shouldInline, DEFAULT_MAX_INLINE_BYTES, isCompressible, negotiateEncoding, } from './utils/compression.js';
23
+ import { bodyIsText } from './utils/body-type.js';
23
24
  import { LazyEventEmitter } from './utils/lazy-event-emitter.js';
24
25
  import { parseRawQueryString as parseUwsQueryString } from './utils/query-parser.js';
25
26
  import { PathMatcher } from '../routing/path-matcher.js';
@@ -148,6 +149,12 @@ export class EngineRequest extends LazyEventEmitter {
148
149
  method;
149
150
  path;
150
151
  body = null;
152
+ /** Undecoded request body bytes (parseBody keeps the same Buffer it parsed) */
153
+ _rawBody = undefined;
154
+ /** The request body exactly as received, before any parsing; null when empty. */
155
+ get rawBody() {
156
+ return this._rawBody ?? null;
157
+ }
151
158
  _server;
152
159
  _reqId;
153
160
  _params = undefined;
@@ -743,6 +750,27 @@ export class EngineResponse extends LazyEventEmitter {
743
750
  this._emitDone();
744
751
  return;
745
752
  }
753
+ // A small body is compressed on this thread: cheaper than the threadpool
754
+ // round trip, and the reply goes out in the same native call an
755
+ // uncompressed one would. A failure sends the body as it is.
756
+ if (shouldInline(bytes, s)) {
757
+ let compressed;
758
+ try {
759
+ compressed = compressBufferSync(body, encoding, s.level);
760
+ }
761
+ catch {
762
+ compressed = undefined;
763
+ }
764
+ if (compressed !== undefined) {
765
+ this.responseHeaders['content-encoding'] = encoding;
766
+ const vary = this.responseHeaders['vary'];
767
+ this.responseHeaders['vary'] = vary ? `${vary}, Accept-Encoding` : 'Accept-Encoding';
768
+ }
769
+ this._server._respond(this._reqId, this.statusCode, this._headersFlat(), compressed ?? body);
770
+ this.headersSent = true;
771
+ this._emitDone();
772
+ return;
773
+ }
746
774
  // Async path: compress, then respond (guarding against an abort that
747
775
  // raced the await - the reqId would be invalid).
748
776
  void compressBuffer(body, encoding, s.level)
@@ -1468,6 +1496,13 @@ export class MoroEngineServer {
1468
1496
  /** @internal */ _setStaticRoute;
1469
1497
  _staticOn = false;
1470
1498
  staticRoutes = [];
1499
+ // Engine-answered parameter routes (engine >= 1.1.9, capabilities.paramRoutes):
1500
+ // one variable path segment between prefix and suffix, echoed as the body,
1501
+ // sent without calling into JS. Same guard and same re-apply story as the
1502
+ // static routes above.
1503
+ /** @internal */ _setParamRoute;
1504
+ _paramOn = false;
1505
+ paramRoutes = [];
1471
1506
  // Batched pipelined dispatch (engine >= 1.1.6, capabilities.batchDispatch):
1472
1507
  // the engine parses complete pipelined requests ahead and delivers them in
1473
1508
  // one onRequestBatch(count) call; the descriptors/control/paths buffers are
@@ -1520,6 +1555,7 @@ export class MoroEngineServer {
1520
1555
  enabled: false,
1521
1556
  threshold: 1024,
1522
1557
  level: 6,
1558
+ maxInlineBytes: DEFAULT_MAX_INLINE_BYTES,
1523
1559
  encodings: ['br', 'gzip', 'deflate'],
1524
1560
  };
1525
1561
  // Set by close(); listen() then registers a fresh native server, because the
@@ -1593,6 +1629,10 @@ export class MoroEngineServer {
1593
1629
  this._setStaticRoute = surface.setStaticRoute;
1594
1630
  this._staticOn = true;
1595
1631
  }
1632
+ if (this._capabilities?.paramRoutes === true && typeof surface.setParamRoute === 'function') {
1633
+ this._setParamRoute = surface.setParamRoute;
1634
+ this._paramOn = true;
1635
+ }
1596
1636
  if (this._capabilities?.batchDispatch === true &&
1597
1637
  typeof surface.getBatchBuffers === 'function' &&
1598
1638
  typeof surface.getPath === 'function') {
@@ -1619,7 +1659,11 @@ export class MoroEngineServer {
1619
1659
  this.handle = Object.defineProperties({
1620
1660
  engine: '@morojs/engine',
1621
1661
  address: () => this.isListening
1622
- ? { address: this.host || '0.0.0.0', family: 'IPv4', port: this.port || 0 }
1662
+ ? {
1663
+ address: this.host || '::',
1664
+ family: (this.host || '::').includes(':') ? 'IPv6' : 'IPv4',
1665
+ port: this.port || 0,
1666
+ }
1623
1667
  : null,
1624
1668
  close: (cb) => {
1625
1669
  this.close(cb);
@@ -1709,13 +1753,58 @@ export class MoroEngineServer {
1709
1753
  }
1710
1754
  }
1711
1755
  // listen() after close() registers a fresh native server; give it every
1712
- // static route the old one had.
1756
+ // static and parameter route the old one had.
1713
1757
  _reapplyStaticRoutes() {
1714
1758
  const fn = this._setStaticRoute;
1715
- if (fn === undefined)
1716
- return;
1717
- for (const r of this.staticRoutes) {
1718
- fn(this.serverId, r.methodIdx, r.path, r.status, r.headersFlat, r.body);
1759
+ if (fn !== undefined) {
1760
+ for (const r of this.staticRoutes) {
1761
+ fn(this.serverId, r.methodIdx, r.path, r.status, r.headersFlat, r.body);
1762
+ }
1763
+ }
1764
+ const pfn = this._setParamRoute;
1765
+ if (pfn !== undefined) {
1766
+ for (const r of this.paramRoutes) {
1767
+ pfn(this.serverId, r.methodIdx, r.prefix, r.suffix, r.status, r.headersFlat);
1768
+ }
1769
+ }
1770
+ }
1771
+ /** Whether this engine build answers parameter routes itself. */
1772
+ get paramRoutesEnabled() {
1773
+ return this._paramOn;
1774
+ }
1775
+ /**
1776
+ * Register a route with one variable path segment, between `prefix` and
1777
+ * `suffix`, that the engine answers itself with that segment as the body:
1778
+ * "/user/" + "" for `/user/:id`. The segment goes out undecoded, as it is
1779
+ * on the wire. Returns false when this engine cannot take it (no
1780
+ * capability, compression on, or an unindexed method), so the caller keeps
1781
+ * its JS handler. Registering the same (method, prefix, suffix) again
1782
+ * replaces the earlier reply.
1783
+ */
1784
+ setParamRoute(method, prefix, suffix, status = 200, headers) {
1785
+ const fn = this._setParamRoute;
1786
+ if (!this._paramOn || fn === undefined)
1787
+ return false;
1788
+ if (this._compression.enabled)
1789
+ return false;
1790
+ const methodIdx = METHODS.indexOf(method.toUpperCase());
1791
+ if (methodIdx < 0 || methodIdx >= STATIC_ROUTE_METHODS)
1792
+ return false;
1793
+ const headersFlat = flattenHeaders(headers);
1794
+ const entry = { methodIdx, prefix, suffix, status, headersFlat };
1795
+ const at = this.paramRoutes.findIndex(r => r.methodIdx === methodIdx && r.prefix === prefix && r.suffix === suffix);
1796
+ if (at === -1)
1797
+ this.paramRoutes.push(entry);
1798
+ else
1799
+ this.paramRoutes[at] = entry;
1800
+ fn(this.serverId, methodIdx, prefix, suffix, status, headersFlat);
1801
+ return true;
1802
+ }
1803
+ /** Drop every engine-answered parameter route; the JS routes are untouched. */
1804
+ clearParamRoutes() {
1805
+ this.paramRoutes.length = 0;
1806
+ if (this._paramOn && typeof this._engine.clearParamRoutes === 'function') {
1807
+ this._engine.clearParamRoutes(this.serverId);
1719
1808
  }
1720
1809
  }
1721
1810
  // Register callbacks + limits with the native engine, returning the new
@@ -2279,6 +2368,9 @@ export class MoroEngineServer {
2279
2368
  throw error;
2280
2369
  }
2281
2370
  const buffer = Buffer.from(raw);
2371
+ // getBody() handed us a stable copy, so the raw bytes cost nothing to keep
2372
+ // (webhook signature checks need them even for JSON bodies)
2373
+ httpReq._rawBody = buffer;
2282
2374
  try {
2283
2375
  if (contentType.includes('application/json')) {
2284
2376
  httpReq.body = JSON.parse(buffer.toString('utf-8'));
@@ -2297,7 +2389,9 @@ export class MoroEngineServer {
2297
2389
  httpReq.body = parseMultipartBuffer(buffer, contentType, this.multipartLimits);
2298
2390
  }
2299
2391
  else {
2300
- httpReq.body = buffer.toString('utf-8');
2392
+ // Text decodes to a string; binary (octet-stream, images, protobuf,
2393
+ // ...) stays a Buffer - decoding it would corrupt it irreversibly.
2394
+ httpReq.body = bodyIsText(contentType) ? buffer.toString('utf-8') : buffer;
2301
2395
  }
2302
2396
  }
2303
2397
  catch (parseError) {
@@ -2447,7 +2541,10 @@ export class MoroEngineServer {
2447
2541
  this.logger.warn('Server is already listening', 'Listen');
2448
2542
  return;
2449
2543
  }
2450
- const host = typeof hostOrCallback === 'string' ? hostOrCallback : '0.0.0.0';
2544
+ // No host = every interface, dual-stack, exactly like Node's
2545
+ // server.listen(port): a client that resolves `localhost` to ::1 first
2546
+ // must not be refused. Hosts without IPv6 fall back to IPv4-any below.
2547
+ const host = typeof hostOrCallback === 'string' ? hostOrCallback : '::';
2451
2548
  const cb = typeof hostOrCallback === 'function' ? hostOrCallback : callback;
2452
2549
  try {
2453
2550
  // listen() after close(): the old native server's handles are torn
@@ -2461,11 +2558,23 @@ export class MoroEngineServer {
2461
2558
  }
2462
2559
  // Synchronous bind; throws on bind errors, returns the actual port
2463
2560
  // (meaningful when port 0 asks for an ephemeral one)
2464
- const actualPort = this._engine.listen(this.serverId, host, port);
2561
+ let boundHost = host;
2562
+ let actualPort;
2563
+ try {
2564
+ actualPort = this._engine.listen(this.serverId, host, port);
2565
+ }
2566
+ catch (error) {
2567
+ const code = error?.code;
2568
+ if (host !== '::' || (code !== 'EAFNOSUPPORT' && code !== 'EADDRNOTAVAIL'))
2569
+ throw error;
2570
+ // IPv6 not configured on this host - IPv4-any is what Node does too
2571
+ boundHost = '0.0.0.0';
2572
+ actualPort = this._engine.listen(this.serverId, boundHost, port);
2573
+ }
2465
2574
  this.port = typeof actualPort === 'number' ? actualPort : port;
2466
- this.host = host;
2575
+ this.host = boundHost;
2467
2576
  this.isListening = true;
2468
- this.logger.info(`Moro engine HTTP server listening on ${host}:${this.port}`, 'Listen');
2577
+ this.logger.info(`Moro engine HTTP server listening on ${boundHost}:${this.port}`, 'Listen');
2469
2578
  if (cb)
2470
2579
  cb();
2471
2580
  }
@@ -2539,6 +2648,23 @@ export class MoroEngineServer {
2539
2648
  callback(error instanceof Error ? error : new Error(String(error)));
2540
2649
  }
2541
2650
  }
2651
+ /**
2652
+ * Replace the TLS certificate/key for every handshake from now on
2653
+ * (engine capabilities.tlsReload). Established connections keep the context
2654
+ * they handshaked with. Validated by the engine before the swap: a bad
2655
+ * config throws and the current certificate keeps serving.
2656
+ */
2657
+ updateSsl(ssl) {
2658
+ if (!this._ssl || !this.isSsl) {
2659
+ throw new Error('updateSsl: this server was not started with TLS (server.ssl)');
2660
+ }
2661
+ if (!this._capabilities?.tlsReload || typeof this._engine.updateSsl !== 'function') {
2662
+ throw new Error('TLS reload needs @morojs/engine with capabilities.tlsReload (engine >= 1.1.8)');
2663
+ }
2664
+ this._engine.updateSsl(this.serverId, ssl);
2665
+ // A later close() -> listen() cycle re-registers with the new material
2666
+ this._ssl = ssl;
2667
+ }
2542
2668
  getServer() {
2543
2669
  // Stable observability handle (listening/address) - there is no app object
2544
2670
  return this.handle;