@morojs/moro 1.7.29 → 1.8.0

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 (67) hide show
  1. package/README.md +89 -37
  2. package/dist/core/config/config-sources.js +114 -2
  3. package/dist/core/config/config-sources.js.map +1 -1
  4. package/dist/core/config/config-validator.js +187 -5
  5. package/dist/core/config/config-validator.js.map +1 -1
  6. package/dist/core/config/schema.js +31 -3
  7. package/dist/core/config/schema.js.map +1 -1
  8. package/dist/core/framework.d.ts +41 -2
  9. package/dist/core/framework.js +338 -73
  10. package/dist/core/framework.js.map +1 -1
  11. package/dist/core/http/http-server.d.ts +21 -22
  12. package/dist/core/http/http-server.js +268 -226
  13. package/dist/core/http/http-server.js.map +1 -1
  14. package/dist/core/http/http2-server.d.ts +6 -16
  15. package/dist/core/http/http2-server.js +14 -79
  16. package/dist/core/http/http2-server.js.map +1 -1
  17. package/dist/core/http/index.d.ts +1 -0
  18. package/dist/core/http/index.js +3 -0
  19. package/dist/core/http/index.js.map +1 -1
  20. package/dist/core/http/moro-engine-server.d.ts +263 -0
  21. package/dist/core/http/moro-engine-server.js +1939 -0
  22. package/dist/core/http/moro-engine-server.js.map +1 -0
  23. package/dist/core/http/utils/compression.d.ts +47 -0
  24. package/dist/core/http/utils/compression.js +132 -0
  25. package/dist/core/http/utils/compression.js.map +1 -0
  26. package/dist/core/http/utils/lazy-event-emitter.d.ts +11 -0
  27. package/dist/core/http/utils/lazy-event-emitter.js +69 -0
  28. package/dist/core/http/utils/lazy-event-emitter.js.map +1 -0
  29. package/dist/core/http/utils/multipart-parser.d.ts +32 -0
  30. package/dist/core/http/utils/multipart-parser.js +211 -0
  31. package/dist/core/http/utils/multipart-parser.js.map +1 -0
  32. package/dist/core/http/utils/query-parser.d.ts +1 -0
  33. package/dist/core/http/utils/query-parser.js +44 -0
  34. package/dist/core/http/utils/query-parser.js.map +1 -0
  35. package/dist/core/http/utils/size.d.ts +33 -0
  36. package/dist/core/http/utils/size.js +37 -0
  37. package/dist/core/http/utils/size.js.map +1 -0
  38. package/dist/core/http/utils/ssl-config.d.ts +69 -0
  39. package/dist/core/http/utils/ssl-config.js +116 -0
  40. package/dist/core/http/utils/ssl-config.js.map +1 -0
  41. package/dist/core/http/utils/uws-worker-clustering.js +8 -2
  42. package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
  43. package/dist/core/http/uws-http-server.d.ts +24 -3
  44. package/dist/core/http/uws-http-server.js +386 -65
  45. package/dist/core/http/uws-http-server.js.map +1 -1
  46. package/dist/core/middleware/built-in/compression/core.d.ts +3 -0
  47. package/dist/core/middleware/built-in/compression/core.js +28 -34
  48. package/dist/core/middleware/built-in/compression/core.js.map +1 -1
  49. package/dist/core/networking/adapters/engine-adapter.d.ts +23 -0
  50. package/dist/core/networking/adapters/engine-adapter.js +365 -0
  51. package/dist/core/networking/adapters/engine-adapter.js.map +1 -0
  52. package/dist/core/networking/adapters/index.d.ts +1 -0
  53. package/dist/core/networking/adapters/index.js +1 -0
  54. package/dist/core/networking/adapters/index.js.map +1 -1
  55. package/dist/core/networking/adapters/uws-adapter.js +26 -15
  56. package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
  57. package/dist/core/utilities/package-utils.d.ts +68 -0
  58. package/dist/core/utilities/package-utils.js +198 -1
  59. package/dist/core/utilities/package-utils.js.map +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/index.js.map +1 -1
  63. package/dist/moro.d.ts +2 -0
  64. package/dist/moro.js +63 -21
  65. package/dist/moro.js.map +1 -1
  66. package/dist/types/config.d.ts +90 -5
  67. package/package.json +4 -1
@@ -1,12 +1,28 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { MoroHttpServer } from './http/index.js';
3
3
  import { UWebSocketsHttpServer } from './http/uws-http-server.js';
4
+ import { MoroEngineServer } from './http/moro-engine-server.js';
4
5
  import { MoroHttp2Server, Http2ServerOptions } from './http/http2-server.js';
5
6
  import { Container } from './utilities/index.js';
6
7
  import { WebSocketManager } from './networking/websocket-manager.js';
7
8
  import { ModuleConfig } from '../types/module.js';
8
9
  import { MoroOptions as CoreMoroOptions } from '../types/core.js';
9
10
  import { WebSocketAdapter, WebSocketAdapterOptions } from './networking/websocket-adapter.js';
11
+ /**
12
+ * Which HTTP server actually booted, and why, when the native engine was
13
+ * bypassed. Exposed via getServerKind() / app.engine for logs, tests and
14
+ * benchmarks that need to assert the transport in use.
15
+ */
16
+ export interface ServerKind {
17
+ server: 'engine' | 'node' | 'http2';
18
+ /** Package backing the native engine ('@morojs/engine' or 'uWebSockets.js') */
19
+ enginePackage?: string;
20
+ engineVersion?: string;
21
+ /** Present when the native engine was requested but the Node server booted */
22
+ fallbackReason?: string;
23
+ /** Protocols the booted server can speak (observability for app.engine). */
24
+ protocols?: Array<'http/1.1' | 'h2'>;
25
+ }
10
26
  export interface MoroOptions extends CoreMoroOptions {
11
27
  http2?: boolean | Http2ServerOptions;
12
28
  https?: {
@@ -37,7 +53,9 @@ export declare class Moro extends EventEmitter {
37
53
  private options;
38
54
  private config;
39
55
  private usingUWebSockets;
56
+ private usingEngine;
40
57
  private usingHttp2;
58
+ private engineInfo;
41
59
  private websocketSetupPromise;
42
60
  constructor(options?: MoroOptions);
43
61
  use(middleware: any): this;
@@ -55,7 +73,7 @@ export declare class Moro extends EventEmitter {
55
73
  addMiddleware(middleware: any): this;
56
74
  registerDatabase(adapter: any): this;
57
75
  getContainer(): Container;
58
- getHttpServer(): MoroHttpServer | UWebSocketsHttpServer | MoroHttp2Server;
76
+ getHttpServer(): MoroHttpServer | UWebSocketsHttpServer | MoroHttp2Server | MoroEngineServer;
59
77
  /**
60
78
  * Get WebSocket adapter (for backward compatibility)
61
79
  * @deprecated Use getWebSocketAdapter() instead
@@ -88,9 +106,30 @@ export declare class Moro extends EventEmitter {
88
106
  private setupWebSocketHandlers;
89
107
  private checkRateLimit;
90
108
  /**
91
- * Parse size string (e.g., "10mb", "5gb") to bytes
109
+ * Which HTTP server backs this app, which package provides it, and the
110
+ * fallback reason when the native engine was requested but unavailable.
111
+ */
112
+ getServerKind(): ServerKind;
113
+ private resolveEngineMode;
114
+ private nativeEngineGate;
115
+ /**
116
+ * Parse size string (e.g., "10mb", "5gb") to bytes.
117
+ * Delegates to the shared utility so every server + config path agrees.
92
118
  */
93
119
  private parseSizeToBytes;
120
+ /**
121
+ * Merge HTTP/2 settings from config.server.http2 and the top-level
122
+ * options.http2 (options win — the highest-precedence, back-compat path)
123
+ * into one Http2ServerOptions used by both the engine (ALPN h2) and the
124
+ * MoroHttp2Server fallback.
125
+ */
126
+ private resolveH2Settings;
127
+ /**
128
+ * Resolve every size string in server.limits/timeouts/backlog into one flat
129
+ * object of bytes/ms, built once and passed to whichever server boots. A
130
+ * value left undefined means "use the server's own documented default".
131
+ */
132
+ private buildRuntimeLimits;
94
133
  private getCircuitBreaker;
95
134
  listen(port: number, callback?: () => void): void;
96
135
  listen(port: number, host: string, callback?: () => void): void;
@@ -2,13 +2,16 @@ import crypto from 'crypto';
2
2
  import { EventEmitter } from 'events';
3
3
  import { MoroHttpServer } from './http/index.js';
4
4
  import { UWebSocketsHttpServer } from './http/uws-http-server.js';
5
+ import { MoroEngineServer } from './http/moro-engine-server.js';
5
6
  import { MoroHttp2Server } from './http/http2-server.js';
6
7
  import { Router } from './routing/router.js';
7
8
  import { Container, buildModuleBasePath } from './utilities/index.js';
8
9
  import { ModuleLoader } from './modules/index.js';
9
10
  import { WebSocketManager } from './networking/websocket-manager.js';
10
11
  import { CircuitBreaker } from './utilities/circuit-breaker.js';
11
- import { isPackageAvailable } from './utilities/package-utils.js';
12
+ import { isPackageAvailable, loadNativeEngine, getNativeEngineLoadErrors, } from './utilities/package-utils.js';
13
+ import { parseSizeToBytes } from './http/utils/size.js';
14
+ import { normalizeSSLConfig, sslForNode, sslForUws, sslIsComplete, } from './http/utils/ssl-config.js';
12
15
  import { MoroEventBus } from './events/index.js';
13
16
  import { createFrameworkLogger, logger as globalLogger } from './logger/index.js';
14
17
  import { mergeWebSocketConfig, } from './networking/websocket-adapter.js';
@@ -29,7 +32,12 @@ export class Moro extends EventEmitter {
29
32
  options;
30
33
  config;
31
34
  usingUWebSockets = false;
35
+ // True when the Moro-shaped @morojs/engine backs this app (usingUWebSockets
36
+ // stays reserved for the legacy uWS-API-shaped engine)
37
+ usingEngine = false;
32
38
  usingHttp2 = false;
39
+ // Which server actually booted (engine = native uWS-style engine)
40
+ engineInfo = { server: 'node' };
33
41
  // WebSocket initialization promise to handle async adapter detection
34
42
  websocketSetupPromise = null;
35
43
  constructor(options = {}) {
@@ -53,65 +61,129 @@ export class Moro extends EventEmitter {
53
61
  }
54
62
  // Initialize framework logger after global configuration
55
63
  this.logger = createFrameworkLogger('Core');
56
- // Check if uWebSockets should be used for HTTP and WebSocket
57
- const useUWebSockets = this.config.server?.useUWebSockets || false;
58
- if (useUWebSockets) {
64
+ // Resolve which HTTP server backs this app: the native engine
65
+ // (@morojs/engine, or a legacy user-installed uWebSockets.js), Node's
66
+ // http server, or the http2 server. The engine module is loaded
67
+ // SYNCHRONOUSLY here - UWebSocketsHttpServer finishes initialization
68
+ // asynchronously, so a load failure (package missing, or no prebuilt
69
+ // binary for this Node ABI) surfacing there would never hit the fallback
70
+ // and the app would hang at listen() bound to nothing.
71
+ const engineChoice = this.resolveEngineMode(options);
72
+ // One normalized SSL config + one flattened limits object, built here and
73
+ // projected per-runtime, so a single server.ssl / server.limits flows
74
+ // everywhere regardless of which server boots.
75
+ const ssl = normalizeSSLConfig(this.config.server?.ssl, options.https, this.logger);
76
+ const rt = this.buildRuntimeLimits();
77
+ if (engineChoice.kind === 'engine') {
59
78
  try {
60
- // Try to use uWebSockets for high performance HTTP and WebSocket
61
- const sslOptions = this.config.server?.ssl || options.https;
62
- this.httpServer = new UWebSocketsHttpServer({
63
- ssl: sslOptions,
64
- maxBodySize: this.parseSizeToBytes(this.config.server.bodySizeLimit),
65
- maxUploadSize: this.parseSizeToBytes(this.config.server.maxUploadSize),
66
- });
67
- this.server = this.httpServer.getApp();
68
- this.usingUWebSockets = true;
69
- this.logger.info('uWebSockets HTTP+WebSocket server created', 'ServerInit');
79
+ // The two native engines expose different APIs: @morojs/engine is
80
+ // Moro-shaped (serve/respond, one JS crossing each way), the legacy
81
+ // uWS peer is App()-shaped. Pick the adapter by capability.
82
+ const engineSurface = engineChoice.engine.module?.default || engineChoice.engine.module;
83
+ const caps = engineChoice.engine.capabilities;
84
+ if (typeof engineSurface?.serve === 'function' &&
85
+ typeof engineSurface?.respond === 'function') {
86
+ this.httpServer = new MoroEngineServer({
87
+ ssl,
88
+ capabilities: caps,
89
+ limits: rt,
90
+ http2Settings: engineChoice.h2 ? this.resolveH2Settings(options) : undefined,
91
+ maxBodySize: rt.maxBodySize,
92
+ maxUploadSize: rt.maxUploadSize,
93
+ engineModule: engineChoice.engine.module,
94
+ // Cluster workers each bind the port; SO_REUSEPORT lets the kernel
95
+ // balance accepts across them (Windows is gated to Node earlier).
96
+ reusePort: this.config.performance?.clustering?.enabled === true,
97
+ });
98
+ this.server = this.httpServer.getServer();
99
+ this.usingEngine = true;
100
+ }
101
+ else {
102
+ this.httpServer = new UWebSocketsHttpServer({
103
+ ssl: ssl ? sslForUws(ssl) : undefined,
104
+ sslInlineOnly: ssl ? !sslForUws(ssl) : false,
105
+ limits: rt,
106
+ maxBodySize: rt.maxBodySize,
107
+ maxUploadSize: rt.maxUploadSize,
108
+ engineModule: engineChoice.engine.module,
109
+ });
110
+ this.server = this.httpServer.getApp();
111
+ this.usingUWebSockets = true;
112
+ }
113
+ this.engineInfo = {
114
+ server: 'engine',
115
+ enginePackage: engineChoice.engine.source,
116
+ engineVersion: engineChoice.engine.version,
117
+ protocols: engineChoice.h2 && caps?.http2 ? ['h2', 'http/1.1'] : ['http/1.1'],
118
+ };
119
+ this.logger.info(`HTTP engine: ${engineChoice.engine.source}` +
120
+ (engineChoice.engine.version ? ` v${engineChoice.engine.version}` : '') +
121
+ ` (native, Node ABI ${process.versions.modules})`, 'ServerInit');
70
122
  }
71
123
  catch (error) {
72
- // Fallback to standard HTTP/1.1 if uWebSockets fails to load
73
- this.logger.warn('uWebSockets failed to initialize, falling back to Node.js http.Server. ' +
74
- 'Error: ' +
75
- (error instanceof Error ? error.message : String(error)), 'ServerInit');
124
+ // Construction failed after a successful module load - unexpected, but
125
+ // never boot nothing: fall back to the Node.js http server.
126
+ const reason = error instanceof Error ? error.message : String(error);
127
+ this.logger.warn(`Native engine (${engineChoice.requested}) failed to initialize, falling back to Node.js http.Server. Error: ${reason}`, 'ServerInit');
76
128
  this.usingUWebSockets = false;
129
+ this.usingEngine = false;
130
+ this.engineInfo = { server: 'node', fallbackReason: reason };
77
131
  this.httpServer = new MoroHttpServer({
78
- maxBodySize: this.parseSizeToBytes(this.config.server.bodySizeLimit),
79
- maxUploadSize: this.parseSizeToBytes(this.config.server.maxUploadSize),
132
+ ssl: ssl && sslIsComplete(ssl) ? sslForNode(ssl) : undefined,
133
+ limits: rt,
134
+ maxBodySize: rt.maxBodySize,
135
+ maxUploadSize: rt.maxUploadSize,
80
136
  });
81
137
  this.server = this.httpServer.getServer();
82
138
  }
83
139
  }
84
- else if (options.http2) {
140
+ else if (engineChoice.kind === 'http2') {
85
141
  // Use HTTP/2 with proper adapter
86
- const http2Options = {};
87
- if (typeof options.http2 === 'object') {
88
- // Advanced HTTP/2 configuration
89
- Object.assign(http2Options, options.http2);
90
- }
91
- // Add SSL configuration if provided
92
- if (options.https) {
93
- http2Options.key = options.https.key;
94
- http2Options.cert = options.https.cert;
95
- if (options.https.ca) {
96
- http2Options.ca = options.https.ca;
97
- }
142
+ const http2Options = this.resolveH2Settings(options);
143
+ // SSL now flows from the unified config (either shape) as well as the
144
+ // legacy options.https, via the normalizer.
145
+ if (ssl) {
146
+ const nodeSsl = sslForNode(ssl);
147
+ http2Options.key = nodeSsl.key;
148
+ http2Options.cert = nodeSsl.cert;
149
+ if (nodeSsl.ca)
150
+ http2Options.ca = nodeSsl.ca;
98
151
  }
99
152
  this.httpServer = new MoroHttp2Server({
100
153
  ...http2Options,
101
- maxBodySize: this.parseSizeToBytes(this.config.server.bodySizeLimit),
102
- maxUploadSize: this.parseSizeToBytes(this.config.server.maxUploadSize),
154
+ limits: rt,
155
+ maxBodySize: rt.maxBodySize,
156
+ maxUploadSize: rt.maxUploadSize,
103
157
  });
104
158
  this.server = this.httpServer.getServer();
105
159
  this.usingHttp2 = true;
160
+ this.engineInfo = { server: 'http2', protocols: ['h2', 'http/1.1'] };
106
161
  this.logger.info('HTTP/2 server created with native adapter', 'ServerInit');
107
162
  }
108
163
  else {
109
- // Use standard HTTP/1.1
164
+ // Use standard HTTP/1.1 (Node), now with optional in-process HTTPS.
110
165
  this.httpServer = new MoroHttpServer({
111
- maxBodySize: this.parseSizeToBytes(this.config.server.bodySizeLimit),
112
- maxUploadSize: this.parseSizeToBytes(this.config.server.maxUploadSize),
166
+ ssl: ssl && require('./http/utils/ssl-config.js').sslIsComplete(ssl)
167
+ ? sslForNode(ssl)
168
+ : undefined,
169
+ limits: rt,
170
+ maxBodySize: rt.maxBodySize,
171
+ maxUploadSize: rt.maxUploadSize,
113
172
  });
114
173
  this.server = this.httpServer.getServer();
174
+ this.engineInfo = { server: 'node', fallbackReason: engineChoice.fallbackReason };
175
+ if (engineChoice.fallbackReason) {
176
+ // The default engine simply not being installed is expected (it is an
177
+ // optional dependency) - only an EXPLICIT engine choice that cannot be
178
+ // honored deserves warn-level noise on every boot.
179
+ const message = `Native engine unavailable, using Node.js http.Server - ${engineChoice.fallbackReason}`;
180
+ if (engineChoice.explicitEngine) {
181
+ this.logger.warn(message, 'ServerInit');
182
+ }
183
+ else {
184
+ this.logger.info(message, 'ServerInit');
185
+ }
186
+ }
115
187
  }
116
188
  this.container = new Container();
117
189
  this.moduleLoader = new ModuleLoader(this.container);
@@ -182,6 +254,21 @@ export class Moro extends EventEmitter {
182
254
  */
183
255
  async setupWebSockets(wsConfig) {
184
256
  try {
257
+ // The Moro-shaped @morojs/engine has native RFC 6455 WebSocket support:
258
+ // upgrades share the HTTP listen socket, driven by the engine's C++ WS
259
+ // path via the EngineWebSocketAdapter.
260
+ if (this.usingEngine) {
261
+ const { EngineWebSocketAdapter } = await import('./networking/adapters/engine-adapter.js');
262
+ this.websocketAdapter = new EngineWebSocketAdapter();
263
+ await this.websocketAdapter.initialize(this.httpServer, wsConfig.options);
264
+ this.websocketManager = new WebSocketManager(this.websocketAdapter, this.container);
265
+ if (wsConfig.compression)
266
+ this.websocketAdapter.setCompression(true);
267
+ if (wsConfig.customIdGenerator)
268
+ this.websocketAdapter.setCustomIdGenerator(wsConfig.customIdGenerator);
269
+ this.logger.info('Engine WebSocket adapter initialized (integrated with @morojs/engine)', 'WebSocketSetup');
270
+ return;
271
+ }
185
272
  // If using uWebSockets HTTP server, automatically use uWebSockets for WebSocket too
186
273
  if (this.usingUWebSockets) {
187
274
  const { UWebSocketsAdapter } = await import('./networking/adapters/index.js');
@@ -227,14 +314,13 @@ export class Moro extends EventEmitter {
227
314
  // Check if adapter is specified in config
228
315
  if (this.config.websocket?.adapter) {
229
316
  const adapterType = this.config.websocket.adapter;
230
- if (adapterType === 'uws' && isPackageAvailable('uWebSockets.js')) {
231
- try {
232
- const { UWebSocketsAdapter } = await import('./networking/adapters/index.js');
233
- return new UWebSocketsAdapter();
234
- }
235
- catch {
236
- this.logger.warn('uWebSockets.js specified but failed to load', 'AdapterDetection');
237
- }
317
+ if (adapterType === 'uws') {
318
+ // The uws adapter only works integrated with the native engine's own
319
+ // app (setupWebSockets handles that path before detection runs).
320
+ // Standalone it would register routes on a second, never-listening
321
+ // uWS app - fall through to the Node-compatible adapters instead.
322
+ this.logger.warn("websocket adapter 'uws' requires a native engine HTTP server " +
323
+ "(engine: 'moro' or 'uws') - falling back to socket.io/ws detection", 'AdapterDetection');
238
324
  }
239
325
  else if (adapterType === 'socket.io' && isPackageAvailable('socket.io')) {
240
326
  try {
@@ -255,18 +341,11 @@ export class Moro extends EventEmitter {
255
341
  }
256
342
  }
257
343
  }
258
- // Auto-detect: Try uWebSockets.js first (highest performance)
259
- if (isPackageAvailable('uWebSockets.js')) {
260
- try {
261
- const { UWebSocketsAdapter } = await import('./networking/adapters/index.js');
262
- this.logger.debug('uWebSockets.js detected and loaded', 'AdapterDetection');
263
- return new UWebSocketsAdapter();
264
- }
265
- catch {
266
- // Failed to load adapter
267
- }
268
- }
269
- // Try socket.io second
344
+ // Auto-detect. The uws adapter is deliberately NOT probed here: it only
345
+ // works integrated with the native engine's app (the usingUWebSockets
346
+ // branch of setupWebSockets), and with the engine installed by default a
347
+ // resolvable package no longer implies the engine is serving HTTP.
348
+ // Try socket.io first
270
349
  if (isPackageAvailable('socket.io')) {
271
350
  try {
272
351
  const { SocketIOAdapter } = await import('./networking/adapters/index.js');
@@ -840,24 +919,210 @@ export class Moro extends EventEmitter {
840
919
  return true;
841
920
  }
842
921
  /**
843
- * Parse size string (e.g., "10mb", "5gb") to bytes
922
+ * Which HTTP server backs this app, which package provides it, and the
923
+ * fallback reason when the native engine was requested but unavailable.
924
+ */
925
+ getServerKind() {
926
+ return { ...this.engineInfo };
927
+ }
928
+ // Decide which HTTP server to construct. Engine values:
929
+ // 'moro' (default) - Moro's own native engine (@morojs/engine)
930
+ // 'uws' - opt in to uWebSockets.js
931
+ // 'node' - the Node.js http server (no native engine)
932
+ // A chosen native engine that cannot load on this platform/Node ABI (e.g.
933
+ // no prebuilt binary) silently degrades to the Node.js http server and logs
934
+ // why - the app never fails to boot. The engine load happens here,
935
+ // synchronously; the CJS module cache shares the instance with the server.
936
+ resolveEngineMode(options) {
937
+ const server = this.config.server || {};
938
+ // Explicit engine wins. The deprecated useUWebSockets:true counts as an
939
+ // explicit 'uws' choice - it always beat http2 before the engine option
940
+ // existed, and silently changing that transport on upgrade would break
941
+ // existing apps.
942
+ let engineExplicit = server.engine !== undefined || server.useUWebSockets === true;
943
+ let mode = server.engine ?? (server.useUWebSockets ? 'uws' : 'moro');
944
+ // A user-provided uWS adapter INSTANCE implies the uWS engine when the
945
+ // engine wasn't chosen explicitly - otherwise the instance would be
946
+ // silently discarded in favor of the default engine's WS bridge.
947
+ const wsAdapterOption = options.websocket && typeof options.websocket === 'object'
948
+ ? options.websocket.adapter
949
+ : undefined;
950
+ const isUwsAdapterInstance = wsAdapterOption &&
951
+ typeof wsAdapterOption.getAdapterName === 'function' &&
952
+ wsAdapterOption.getAdapterName() === 'uWebSockets.js';
953
+ if (isUwsAdapterInstance) {
954
+ if (!engineExplicit && mode === 'moro') {
955
+ mode = 'uws';
956
+ engineExplicit = true;
957
+ }
958
+ else if (mode === 'moro') {
959
+ this.logger.warn("A uWebSockets.js adapter instance was provided but engine: 'moro' is set - " +
960
+ 'the instance is superseded by the Moro engine WebSocket bridge', 'ServerInit');
961
+ }
962
+ }
963
+ // HTTP/2 is wanted via either the top-level option or server.http2 config.
964
+ const wantH2 = Boolean(options.http2 ?? this.config.server?.http2);
965
+ if (mode === 'node') {
966
+ // The Node runtime serves HTTP/2 via the separate MoroHttp2Server.
967
+ return wantH2 ? { kind: 'http2' } : { kind: 'node' };
968
+ }
969
+ // Clustering forks workers that each bind the port themselves; the native
970
+ // engines rely on SO_REUSEPORT for that, which Windows lacks. The Node
971
+ // server shares the listener over cluster IPC instead, so fall back there
972
+ // rather than letting workers 2..N crash with EADDRINUSE.
973
+ if (this.config.performance?.clustering?.enabled === true && process.platform === 'win32') {
974
+ return {
975
+ kind: 'node',
976
+ fallbackReason: 'clustering on Windows requires the Node.js http server (SO_REUSEPORT is unavailable)',
977
+ explicitEngine: engineExplicit,
978
+ };
979
+ }
980
+ // Environments where a native engine cannot apply (edge runtime, an
981
+ // explicit ws/socket.io adapter that needs a Node http.Server)
982
+ const gate = this.nativeEngineGate(options);
983
+ if (gate) {
984
+ // The native engine can't apply here - but http2 still can, and works
985
+ // fine with the ws/socket.io adapters that trigger the gate, so honor an
986
+ // explicit http2 opt-in rather than silently dropping it.
987
+ return wantH2 ? { kind: 'http2' } : { kind: 'node', fallbackReason: gate };
988
+ }
989
+ // Load the specific package for the chosen engine - 'moro' never silently
990
+ // uses uWS and vice-versa; each falls back only to Node. The load happens
991
+ // BEFORE the http2 decision so we can feature-detect native ALPN h2.
992
+ const pkg = mode === 'uws' ? 'uWebSockets.js' : '@morojs/engine';
993
+ const loadErrors = [];
994
+ const engine = loadNativeEngine({ candidates: [pkg], collectErrors: loadErrors });
995
+ if (!engine) {
996
+ // Prefer this load's own failure detail (missing prebuilt binary, ABI
997
+ // mismatch, ...); only degrade to "not installed" when there is none.
998
+ const detail = (loadErrors.length ? loadErrors : getNativeEngineLoadErrors())
999
+ .map(e => e.split('\n')[0])
1000
+ .join('; ') || `${pkg} is not installed`;
1001
+ // An explicit http2 opt-in survives the engine's absence - falling all
1002
+ // the way to plain HTTP/1.1 would silently drop the user's choice.
1003
+ if (wantH2) {
1004
+ this.logger.warn(`Native engine (${mode}) unavailable (${detail}) - using the configured http2 server`, 'ServerInit');
1005
+ return { kind: 'http2' };
1006
+ }
1007
+ return { kind: 'node', fallbackReason: detail, explicitEngine: engineExplicit };
1008
+ }
1009
+ // HTTP/2 decision, now that the engine is loaded and its capabilities known.
1010
+ if (wantH2) {
1011
+ const canH2 = engine.capabilities?.http2 === true;
1012
+ if (mode === 'uws') {
1013
+ // uWebSockets.js does not serve HTTP/2 through this integration.
1014
+ this.logger.warn("engine: 'uws' does not serve HTTP/2 - serving HTTP/1.1 (use engine: " +
1015
+ "'moro' with an h2-capable build, or engine: 'node' for the http2 server)", 'ServerInit');
1016
+ return { kind: 'engine', engine, requested: mode };
1017
+ }
1018
+ if (canH2) {
1019
+ // The Moro engine speaks ALPN h2 + http/1.1 on one TLS port.
1020
+ return { kind: 'engine', engine, requested: mode, h2: true };
1021
+ }
1022
+ // Engine can't do h2. An explicit engine choice keeps the engine (h1);
1023
+ // the default engine yields to the dedicated MoroHttp2Server.
1024
+ if (engineExplicit) {
1025
+ this.logger.warn(`engine: '${mode}' (build ${engine.version ?? 'unknown'}) does not support ` +
1026
+ 'HTTP/2 - serving HTTP/1.1. Upgrade @morojs/engine or use engine: ' +
1027
+ "'node' for the http2 server.", 'ServerInit');
1028
+ return { kind: 'engine', engine, requested: mode };
1029
+ }
1030
+ return { kind: 'http2' };
1031
+ }
1032
+ return { kind: 'engine', engine, requested: mode };
1033
+ }
1034
+ // Reasons the native engine should not back this app even when loadable:
1035
+ // edge/serverless runtimes never use a listening server, and the ws /
1036
+ // socket.io adapters attach to a real Node http.Server for upgrades.
1037
+ nativeEngineGate(options) {
1038
+ const runtimeType = options.runtime?.type;
1039
+ if (runtimeType && runtimeType !== 'node') {
1040
+ return `runtime '${runtimeType}' does not use a Node listening server`;
1041
+ }
1042
+ const optionsAdapter = options.websocket && typeof options.websocket === 'object'
1043
+ ? options.websocket.adapter
1044
+ : undefined;
1045
+ if (optionsAdapter && typeof optionsAdapter.getAdapterName === 'function') {
1046
+ const name = optionsAdapter.getAdapterName();
1047
+ if (name && name !== 'uWebSockets.js') {
1048
+ return `websocket adapter '${name}' requires the Node.js http server`;
1049
+ }
1050
+ }
1051
+ const configuredAdapter = this.config.websocket?.adapter;
1052
+ if (typeof configuredAdapter === 'string' && configuredAdapter !== 'uws') {
1053
+ return `websocket adapter '${configuredAdapter}' requires the Node.js http server`;
1054
+ }
1055
+ return null;
1056
+ }
1057
+ /**
1058
+ * Parse size string (e.g., "10mb", "5gb") to bytes.
1059
+ * Delegates to the shared utility so every server + config path agrees.
844
1060
  */
845
1061
  parseSizeToBytes(size) {
846
- if (typeof size === 'number') {
847
- return size;
848
- }
849
- const units = {
850
- b: 1,
851
- kb: 1024,
852
- mb: 1024 * 1024,
853
- gb: 1024 * 1024 * 1024,
1062
+ return parseSizeToBytes(size);
1063
+ }
1064
+ /**
1065
+ * Merge HTTP/2 settings from config.server.http2 and the top-level
1066
+ * options.http2 (options win — the highest-precedence, back-compat path)
1067
+ * into one Http2ServerOptions used by both the engine (ALPN h2) and the
1068
+ * MoroHttp2Server fallback.
1069
+ */
1070
+ resolveH2Settings(options) {
1071
+ const out = {};
1072
+ const cfg = this.config.server?.http2;
1073
+ if (cfg && typeof cfg === 'object') {
1074
+ if (cfg.allowHTTP1 !== undefined)
1075
+ out.allowHTTP1 = cfg.allowHTTP1;
1076
+ if (cfg.maxSessionMemory !== undefined)
1077
+ out.maxSessionMemory = cfg.maxSessionMemory;
1078
+ if (cfg.settings)
1079
+ out.settings = { ...cfg.settings };
1080
+ }
1081
+ if (typeof options.http2 === 'object') {
1082
+ Object.assign(out, options.http2);
1083
+ if (options.http2.settings) {
1084
+ out.settings = { ...(out.settings ?? {}), ...options.http2.settings };
1085
+ }
1086
+ }
1087
+ if (out.allowHTTP1 === undefined)
1088
+ out.allowHTTP1 = true;
1089
+ return out;
1090
+ }
1091
+ /**
1092
+ * Resolve every size string in server.limits/timeouts/backlog into one flat
1093
+ * object of bytes/ms, built once and passed to whichever server boots. A
1094
+ * value left undefined means "use the server's own documented default".
1095
+ */
1096
+ buildRuntimeLimits() {
1097
+ const s = this.config.server ?? {};
1098
+ const limits = s.limits ?? {};
1099
+ const timeouts = s.timeouts ?? {};
1100
+ const size = (v) => v === undefined ? undefined : parseSizeToBytes(v);
1101
+ const multipart = limits.multipart ?? {};
1102
+ return {
1103
+ maxBodySize: parseSizeToBytes(s.bodySizeLimit ?? '10mb'),
1104
+ maxUploadSize: parseSizeToBytes(s.maxUploadSize ?? '100mb'),
1105
+ maxConnections: typeof s.maxConnections === 'number' ? s.maxConnections : 0,
1106
+ backlog: s.backlog,
1107
+ timeouts: {
1108
+ request: timeouts.request,
1109
+ idle: timeouts.idle,
1110
+ keepAlive: timeouts.keepAlive,
1111
+ headers: timeouts.headers,
1112
+ },
1113
+ maxHeaderSize: size(limits.maxHeaderSize),
1114
+ maxHeaders: limits.maxHeaders,
1115
+ wsMaxMessageSize: size(limits.wsMaxMessageSize ?? this.config.websocket?.options?.maxPayloadLength),
1116
+ wsBackpressureLimit: size(limits.wsBackpressureLimit),
1117
+ writeHighWaterMark: size(limits.writeHighWaterMark),
1118
+ maxPendingBytes: size(limits.maxPendingBytes),
1119
+ multipart: {
1120
+ maxParts: multipart.maxParts,
1121
+ maxPartHeaderBytes: size(multipart.maxPartHeaderBytes),
1122
+ maxFiles: multipart.maxFiles,
1123
+ maxFileSize: size(multipart.maxFileSize),
1124
+ },
854
1125
  };
855
- const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
856
- if (!match)
857
- return 10 * 1024 * 1024; // Default 10MB
858
- const value = parseFloat(match[1]);
859
- const unit = match[2] || 'b';
860
- return Math.round(value * units[unit]);
861
1126
  }
862
1127
  getCircuitBreaker(key) {
863
1128
  if (!this.circuitBreakers.has(key)) {