@morojs/moro 1.8.15 → 1.8.17

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.
@@ -190,6 +190,14 @@ export declare class MoroEngineServer {
190
190
  /** @internal */ _getBody: (reqId: number) => ArrayBuffer | null;
191
191
  /** @internal */ _getRemoteAddress: (reqId: number) => string | undefined;
192
192
  /** @internal */ _getMethod: (reqId: number) => string | undefined;
193
+ /** @internal */ _wsSend: (wsId: number, data: any, isBinary: boolean) => boolean;
194
+ /** @internal */ _wsClose: (wsId: number, code?: number, reason?: string) => void;
195
+ /** @internal */ _upgradeToWebSocket: (reqId: number) => number;
196
+ private _setWsAutoUpgrade?;
197
+ private _wsGetHeaders?;
198
+ private _wsGetQuery?;
199
+ private _wsGetRemoteAddress?;
200
+ private _wsAutoOn;
193
201
  /** @internal */ _prepareResponse?: (serverId: number, status: number, headers: string[] | null) => number;
194
202
  /** @internal */ _respondPrepared?: (reqId: number, tplId: number, body: any) => void;
195
203
  /** @internal */ _respondPreparedEmpty?: (reqId: number, tplId: number) => void;
@@ -305,6 +313,17 @@ export declare class MoroEngineServer {
305
313
  onMessage(wsId: number, data: any, isBinary: boolean): void;
306
314
  onClose(wsId: number, code: number): void;
307
315
  }): void;
316
+ /**
317
+ * The handshake of a WebSocket the engine upgraded itself: the peer's
318
+ * address, the request headers as the engine's flat list and the raw query
319
+ * string. undefined without the capability, or once the socket is gone
320
+ * (the engine keeps it for the socket's lifetime, through onWsClose).
321
+ */
322
+ wsHandshake(wsId: number): {
323
+ ip: string;
324
+ flat: string[] | undefined;
325
+ query: string;
326
+ } | undefined;
308
327
  /** Send a frame on a WebSocket. Returns false on backpressure. */
309
328
  wsSend(wsId: number, data: string | ArrayBuffer | Buffer, isBinary: boolean): boolean;
310
329
  /** Close a WebSocket (RFC 6455 close frame). */
@@ -328,6 +347,7 @@ export declare class MoroEngineServer {
328
347
  private dispatchDirectRoute;
329
348
  setNativeRouteProvider(_provider: () => Array<any>, _onError?: (...args: any[]) => any): void;
330
349
  private onRequest;
350
+ private dispatchRouter;
331
351
  private _armMicrotaskDrain;
332
352
  private onAborted;
333
353
  private onWritable;
@@ -756,7 +756,7 @@ export class EngineResponse extends LazyEventEmitter {
756
756
  if (shouldInline(bytes, s)) {
757
757
  let compressed;
758
758
  try {
759
- compressed = compressBufferSync(body, encoding, s.level);
759
+ compressed = compressBufferSync(body, encoding, s.level, s);
760
760
  }
761
761
  catch {
762
762
  compressed = undefined;
@@ -773,7 +773,7 @@ export class EngineResponse extends LazyEventEmitter {
773
773
  }
774
774
  // Async path: compress, then respond (guarding against an abort that
775
775
  // raced the await - the reqId would be invalid).
776
- void compressBuffer(body, encoding, s.level)
776
+ void compressBuffer(body, encoding, s.level, s)
777
777
  .then(compressed => {
778
778
  if (this._ended || this._server._isAborted(this._reqId))
779
779
  return;
@@ -1480,6 +1480,22 @@ export class MoroEngineServer {
1480
1480
  /** @internal */ _getBody;
1481
1481
  /** @internal */ _getRemoteAddress;
1482
1482
  /** @internal */ _getMethod;
1483
+ // The WebSocket natives, hoisted for the same reason: wsSend runs once per
1484
+ // frame, and through the loader's Proxy every call paid the `get` trap.
1485
+ /** @internal */ _wsSend;
1486
+ /** @internal */ _wsClose;
1487
+ /** @internal */ _upgradeToWebSocket;
1488
+ // Engine-side upgrades (engine >= 1.1.15, capabilities.wsAutoUpgrade): a
1489
+ // valid handshake is answered inside the engine and onWsOpen fires without
1490
+ // the request ever reaching onRequest - no header materialisation, no
1491
+ // native round trips, no request/response objects per upgrade. The
1492
+ // handshake is read from the socket on first access (wsHandshake). Assigned
1493
+ // only when the flag AND every function are present, like the templates.
1494
+ _setWsAutoUpgrade;
1495
+ _wsGetHeaders;
1496
+ _wsGetQuery;
1497
+ _wsGetRemoteAddress;
1498
+ _wsAutoOn = false;
1483
1499
  // Prepared response templates (engine >= 1.1.6): assigned only when the
1484
1500
  // engine advertises capabilities.responseTemplates AND the functions exist,
1485
1501
  // so a flag/binary mismatch can never put `undefined` into a hot call site.
@@ -1614,6 +1630,9 @@ export class MoroEngineServer {
1614
1630
  this._getBody = surface.getBody;
1615
1631
  this._getRemoteAddress = surface.getRemoteAddress;
1616
1632
  this._getMethod = surface.getMethod;
1633
+ this._wsSend = surface.wsSend;
1634
+ this._wsClose = surface.wsClose;
1635
+ this._upgradeToWebSocket = surface.upgradeToWebSocket;
1617
1636
  if (this._capabilities?.responseTemplates === true &&
1618
1637
  typeof surface.prepareResponse === 'function' &&
1619
1638
  typeof surface.respondPrepared === 'function' &&
@@ -1633,6 +1652,16 @@ export class MoroEngineServer {
1633
1652
  this._setParamRoute = surface.setParamRoute;
1634
1653
  this._paramOn = true;
1635
1654
  }
1655
+ if (this._capabilities?.wsAutoUpgrade === true &&
1656
+ typeof surface.setWsAutoUpgrade === 'function' &&
1657
+ typeof surface.wsGetHeaders === 'function' &&
1658
+ typeof surface.wsGetQuery === 'function' &&
1659
+ typeof surface.wsGetRemoteAddress === 'function') {
1660
+ this._setWsAutoUpgrade = surface.setWsAutoUpgrade;
1661
+ this._wsGetHeaders = surface.wsGetHeaders;
1662
+ this._wsGetQuery = surface.wsGetQuery;
1663
+ this._wsGetRemoteAddress = surface.wsGetRemoteAddress;
1664
+ }
1636
1665
  if (this._capabilities?.batchDispatch === true &&
1637
1666
  typeof surface.getBatchBuffers === 'function' &&
1638
1667
  typeof surface.getPath === 'function') {
@@ -1932,14 +1961,35 @@ export class MoroEngineServer {
1932
1961
  /** Register the engine WebSocket bridge; upgrade requests then route natively. */
1933
1962
  enableWebSocket(bridge) {
1934
1963
  this._wsBridge = bridge;
1964
+ // With the capability the engine answers valid handshakes itself; the
1965
+ // onRequest branch below stays for an invalid one (surfaced as a plain
1966
+ // request, answered 400 by the router) and for older engines.
1967
+ if (this._setWsAutoUpgrade) {
1968
+ this._setWsAutoUpgrade(this.serverId, true);
1969
+ this._wsAutoOn = true;
1970
+ }
1971
+ }
1972
+ /**
1973
+ * The handshake of a WebSocket the engine upgraded itself: the peer's
1974
+ * address, the request headers as the engine's flat list and the raw query
1975
+ * string. undefined without the capability, or once the socket is gone
1976
+ * (the engine keeps it for the socket's lifetime, through onWsClose).
1977
+ */
1978
+ wsHandshake(wsId) {
1979
+ if (!this._wsGetHeaders || !this._wsGetQuery || !this._wsGetRemoteAddress)
1980
+ return undefined;
1981
+ const flat = this._wsGetHeaders(wsId);
1982
+ if (flat === undefined)
1983
+ return undefined;
1984
+ return { ip: this._wsGetRemoteAddress(wsId) || '', flat, query: this._wsGetQuery(wsId) || '' };
1935
1985
  }
1936
1986
  /** Send a frame on a WebSocket. Returns false on backpressure. */
1937
1987
  wsSend(wsId, data, isBinary) {
1938
- return this._engine.wsSend(wsId, data, isBinary) !== false;
1988
+ return this._wsSend(wsId, data, isBinary) !== false;
1939
1989
  }
1940
1990
  /** Close a WebSocket (RFC 6455 close frame). */
1941
1991
  wsClose(wsId, code, reason) {
1942
- this._engine.wsClose(wsId, code, reason);
1992
+ this._wsClose(wsId, code, reason);
1943
1993
  }
1944
1994
  // Direct router dispatch - called after global middleware without the
1945
1995
  // per-middleware promise machinery (mirrors MoroHttpServer.setRouterHandler)
@@ -2093,29 +2143,18 @@ export class MoroEngineServer {
2093
2143
  const upgrade = this._getHeader(reqId, 'upgrade');
2094
2144
  if (upgrade && upgrade.toLowerCase() === 'websocket') {
2095
2145
  // Snapshot handshake data now - the upgrade invalidates the reqId,
2096
- // after which these fetchers return empty values.
2146
+ // after which these fetchers return empty values. The headers stay
2147
+ // the engine's flat [name, value, ...] list: the adapter builds the
2148
+ // object (joining duplicates like EngineRequest.headers) the first
2149
+ // time a handler reads socket.headers, so an upgrade whose handlers
2150
+ // never look pays for no object at all.
2097
2151
  const ip = this._getRemoteAddress(reqId) || '';
2098
- const headers = {};
2099
2152
  const flat = this._getHeaders(reqId);
2100
- if (flat) {
2101
- // Join duplicates exactly like the EngineRequest.headers getter -
2102
- // last-wins would let a client send two X-Forwarded-For lines and
2103
- // hide the earlier one from IP-based WS auth / rate-limiting.
2104
- for (let i = 0; i + 1 < flat.length; i += 2) {
2105
- const key = flat[i];
2106
- const val = flat[i + 1];
2107
- if (key === undefined || val === undefined)
2108
- continue;
2109
- const existing = headers[key];
2110
- headers[key] =
2111
- existing === undefined ? val : existing + (key === 'cookie' ? '; ' : ', ') + val;
2112
- }
2113
- }
2114
2153
  // Capture the query string too (invalidated by the upgrade like the
2115
2154
  // rest), so handlers can read handshake query params (e.g. orgId).
2116
2155
  const query = this._getQuery(reqId) || '';
2117
- this._pendingWsInfo = { ip, headers, query };
2118
- const wsId = this._engine.upgradeToWebSocket(reqId);
2156
+ this._pendingWsInfo = { ip, flat, query };
2157
+ const wsId = this._upgradeToWebSocket(reqId);
2119
2158
  this._pendingWsInfo = undefined;
2120
2159
  if (wsId !== -1)
2121
2160
  return; // WS took over this connection
@@ -2141,40 +2180,58 @@ export class MoroEngineServer {
2141
2180
  }
2142
2181
  this.requestCounter++;
2143
2182
  // SYNC FAST PATH (parity with MoroHttpServer's handleRequest split): with
2144
- // no body to parse, no request/response hooks and no global middleware,
2145
- // the router dispatch runs synchronously in this frame - the async
2146
- // handleRequest below would allocate a promise + async context per
2147
- // request only to reach zero awaits. Only the framework's own frame is
2148
- // de-asynced: an async route handler still returns its promise through
2149
- // the router and is driven to completion here.
2183
+ // no request/response hooks, the body parse, the global middleware chain
2184
+ // and the router dispatch all run synchronously in this frame whenever
2185
+ // they can - the async handleRequest below would allocate a promise and
2186
+ // an async context per request only to reach zero awaits. The engine
2187
+ // buffered the body before onRequest fired, so parsing it is synchronous;
2188
+ // a middleware chain whose members call next() synchronously costs no
2189
+ // promise either (executeMiddleware returns undefined then), and only a
2190
+ // chain that genuinely goes async hands the rest of the request to a
2191
+ // continuation. Only the framework's own frame is de-asynced: an async
2192
+ // route handler still returns its promise through the router and is
2193
+ // driven to completion here.
2150
2194
  const hookManager = this.hookManager;
2151
2195
  const hooksActive = hookManager &&
2152
2196
  (hookManager.hasHooks === undefined ||
2153
2197
  hookManager.hasHooks('request') ||
2154
2198
  hookManager.hasHooks('response'));
2155
- const needsBody = method.charCodeAt(0) === 80 && // 'P' char code
2156
- (method === 'POST' || method === 'PUT' || method === 'PATCH');
2157
- if (!needsBody && !hooksActive && this.globalMiddleware.length === 0 && this.routerHandler) {
2158
- let handled = true;
2159
- try {
2160
- handled = this.routerHandler(httpReq, httpRes);
2199
+ if (!hooksActive && this.routerHandler) {
2200
+ let proceed = true;
2201
+ if (method.charCodeAt(0) === 80 && // 'P' char code
2202
+ (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
2203
+ try {
2204
+ this.parseBody(httpReq);
2205
+ }
2206
+ catch (error) {
2207
+ proceed = false;
2208
+ void this.handleDispatchError(error, httpReq, httpRes);
2209
+ }
2161
2210
  }
2162
- catch (error) {
2163
- // Error path owns the response from here (handled stays true)
2164
- void this.handleDispatchError(error, httpReq, httpRes);
2165
- }
2166
- if (handled !== true) {
2167
- if (handled && typeof handled.then === 'function') {
2168
- handled.then(ok => {
2169
- if (!ok)
2170
- void this.finishUnrouted(httpReq, httpRes);
2211
+ if (proceed && this.globalMiddleware.length > 0) {
2212
+ let chain;
2213
+ try {
2214
+ chain = this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
2215
+ }
2216
+ catch (error) {
2217
+ proceed = false;
2218
+ chain = undefined;
2219
+ void this.handleDispatchError(error, httpReq, httpRes);
2220
+ }
2221
+ if (chain) {
2222
+ // The chain went async: route once it settles, unless it answered.
2223
+ proceed = false;
2224
+ chain.then(() => {
2225
+ if (!httpRes.headersSent)
2226
+ this.dispatchRouter(httpReq, httpRes);
2171
2227
  }, error => void this.handleDispatchError(error, httpReq, httpRes));
2172
2228
  }
2173
- else {
2174
- // Sync miss from the router - direct-route fallback, then 404
2175
- void this.finishUnrouted(httpReq, httpRes);
2229
+ else if (httpRes.headersSent) {
2230
+ proceed = false; // a middleware answered the request itself
2176
2231
  }
2177
2232
  }
2233
+ if (proceed)
2234
+ this.dispatchRouter(httpReq, httpRes);
2178
2235
  }
2179
2236
  else {
2180
2237
  void this.handleRequest(httpReq, httpRes);
@@ -2192,6 +2249,36 @@ export class MoroEngineServer {
2192
2249
  this._armMicrotaskDrain();
2193
2250
  }
2194
2251
  }
2252
+ // The router dispatch of the sync fast path, and of a global middleware
2253
+ // chain's async continuation: the unified router, then the direct route
2254
+ // table and the 404 behind it, with the same error shaping as handleRequest.
2255
+ dispatchRouter(httpReq, httpRes) {
2256
+ const routerHandler = this.routerHandler;
2257
+ if (!routerHandler) {
2258
+ void this.finishUnrouted(httpReq, httpRes);
2259
+ return;
2260
+ }
2261
+ let handled = true;
2262
+ try {
2263
+ handled = routerHandler(httpReq, httpRes);
2264
+ }
2265
+ catch (error) {
2266
+ // Error path owns the response from here (handled stays true)
2267
+ void this.handleDispatchError(error, httpReq, httpRes);
2268
+ }
2269
+ if (handled !== true) {
2270
+ if (handled && typeof handled.then === 'function') {
2271
+ handled.then(ok => {
2272
+ if (!ok)
2273
+ void this.finishUnrouted(httpReq, httpRes);
2274
+ }, error => void this.handleDispatchError(error, httpReq, httpRes));
2275
+ }
2276
+ else {
2277
+ // Sync miss from the router - direct-route fallback, then 404
2278
+ void this.finishUnrouted(httpReq, httpRes);
2279
+ }
2280
+ }
2281
+ }
2195
2282
  // The engine invokes onRequest with a plain V8 call from its own uv
2196
2283
  // callback, outside a Node callback scope, so microtasks queued during
2197
2284
  // dispatch - an await on an already-settled promise, a .then chain, the
@@ -2357,7 +2444,11 @@ export class MoroEngineServer {
2357
2444
  const raw = this._getBody(httpReq._reqId);
2358
2445
  if (!raw || raw.byteLength === 0)
2359
2446
  return;
2360
- const contentType = httpReq.headers['content-type'] || '';
2447
+ // One header through one crossing: materialising the header object here
2448
+ // would cost every request with a body a string per header line.
2449
+ const contentType = (httpReq._headers !== undefined
2450
+ ? httpReq._headers['content-type']
2451
+ : this._getHeader(httpReq._reqId, 'content-type')) || '';
2361
2452
  const isMultipart = contentType.includes('multipart/form-data');
2362
2453
  const maxSize = isMultipart ? this.maxUploadSize : this.maxBodySize;
2363
2454
  // The engine natively enforces the larger of the two limits; the tighter
@@ -2554,6 +2645,8 @@ export class MoroEngineServer {
2554
2645
  this.serverId = this.registerWithEngine();
2555
2646
  this._resetTemplates();
2556
2647
  this._reapplyStaticRoutes();
2648
+ if (this._wsAutoOn && this._setWsAutoUpgrade)
2649
+ this._setWsAutoUpgrade(this.serverId, true);
2557
2650
  this.closed = false;
2558
2651
  }
2559
2652
  // Synchronous bind; throws on bind errors, returns the actual port