@morojs/moro 1.8.15 → 1.8.16

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,9 @@ 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;
193
196
  /** @internal */ _prepareResponse?: (serverId: number, status: number, headers: string[] | null) => number;
194
197
  /** @internal */ _respondPrepared?: (reqId: number, tplId: number, body: any) => void;
195
198
  /** @internal */ _respondPreparedEmpty?: (reqId: number, tplId: number) => void;
@@ -328,6 +331,7 @@ export declare class MoroEngineServer {
328
331
  private dispatchDirectRoute;
329
332
  setNativeRouteProvider(_provider: () => Array<any>, _onError?: (...args: any[]) => any): void;
330
333
  private onRequest;
334
+ private dispatchRouter;
331
335
  private _armMicrotaskDrain;
332
336
  private onAborted;
333
337
  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,11 @@ 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;
1483
1488
  // Prepared response templates (engine >= 1.1.6): assigned only when the
1484
1489
  // engine advertises capabilities.responseTemplates AND the functions exist,
1485
1490
  // so a flag/binary mismatch can never put `undefined` into a hot call site.
@@ -1614,6 +1619,9 @@ export class MoroEngineServer {
1614
1619
  this._getBody = surface.getBody;
1615
1620
  this._getRemoteAddress = surface.getRemoteAddress;
1616
1621
  this._getMethod = surface.getMethod;
1622
+ this._wsSend = surface.wsSend;
1623
+ this._wsClose = surface.wsClose;
1624
+ this._upgradeToWebSocket = surface.upgradeToWebSocket;
1617
1625
  if (this._capabilities?.responseTemplates === true &&
1618
1626
  typeof surface.prepareResponse === 'function' &&
1619
1627
  typeof surface.respondPrepared === 'function' &&
@@ -1935,11 +1943,11 @@ export class MoroEngineServer {
1935
1943
  }
1936
1944
  /** Send a frame on a WebSocket. Returns false on backpressure. */
1937
1945
  wsSend(wsId, data, isBinary) {
1938
- return this._engine.wsSend(wsId, data, isBinary) !== false;
1946
+ return this._wsSend(wsId, data, isBinary) !== false;
1939
1947
  }
1940
1948
  /** Close a WebSocket (RFC 6455 close frame). */
1941
1949
  wsClose(wsId, code, reason) {
1942
- this._engine.wsClose(wsId, code, reason);
1950
+ this._wsClose(wsId, code, reason);
1943
1951
  }
1944
1952
  // Direct router dispatch - called after global middleware without the
1945
1953
  // per-middleware promise machinery (mirrors MoroHttpServer.setRouterHandler)
@@ -2093,29 +2101,18 @@ export class MoroEngineServer {
2093
2101
  const upgrade = this._getHeader(reqId, 'upgrade');
2094
2102
  if (upgrade && upgrade.toLowerCase() === 'websocket') {
2095
2103
  // Snapshot handshake data now - the upgrade invalidates the reqId,
2096
- // after which these fetchers return empty values.
2104
+ // after which these fetchers return empty values. The headers stay
2105
+ // the engine's flat [name, value, ...] list: the adapter builds the
2106
+ // object (joining duplicates like EngineRequest.headers) the first
2107
+ // time a handler reads socket.headers, so an upgrade whose handlers
2108
+ // never look pays for no object at all.
2097
2109
  const ip = this._getRemoteAddress(reqId) || '';
2098
- const headers = {};
2099
2110
  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
2111
  // Capture the query string too (invalidated by the upgrade like the
2115
2112
  // rest), so handlers can read handshake query params (e.g. orgId).
2116
2113
  const query = this._getQuery(reqId) || '';
2117
- this._pendingWsInfo = { ip, headers, query };
2118
- const wsId = this._engine.upgradeToWebSocket(reqId);
2114
+ this._pendingWsInfo = { ip, flat, query };
2115
+ const wsId = this._upgradeToWebSocket(reqId);
2119
2116
  this._pendingWsInfo = undefined;
2120
2117
  if (wsId !== -1)
2121
2118
  return; // WS took over this connection
@@ -2141,40 +2138,58 @@ export class MoroEngineServer {
2141
2138
  }
2142
2139
  this.requestCounter++;
2143
2140
  // 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.
2141
+ // no request/response hooks, the body parse, the global middleware chain
2142
+ // and the router dispatch all run synchronously in this frame whenever
2143
+ // they can - the async handleRequest below would allocate a promise and
2144
+ // an async context per request only to reach zero awaits. The engine
2145
+ // buffered the body before onRequest fired, so parsing it is synchronous;
2146
+ // a middleware chain whose members call next() synchronously costs no
2147
+ // promise either (executeMiddleware returns undefined then), and only a
2148
+ // chain that genuinely goes async hands the rest of the request to a
2149
+ // continuation. Only the framework's own frame is de-asynced: an async
2150
+ // route handler still returns its promise through the router and is
2151
+ // driven to completion here.
2150
2152
  const hookManager = this.hookManager;
2151
2153
  const hooksActive = hookManager &&
2152
2154
  (hookManager.hasHooks === undefined ||
2153
2155
  hookManager.hasHooks('request') ||
2154
2156
  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);
2157
+ if (!hooksActive && this.routerHandler) {
2158
+ let proceed = true;
2159
+ if (method.charCodeAt(0) === 80 && // 'P' char code
2160
+ (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
2161
+ try {
2162
+ this.parseBody(httpReq);
2163
+ }
2164
+ catch (error) {
2165
+ proceed = false;
2166
+ void this.handleDispatchError(error, httpReq, httpRes);
2167
+ }
2161
2168
  }
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);
2169
+ if (proceed && this.globalMiddleware.length > 0) {
2170
+ let chain;
2171
+ try {
2172
+ chain = this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
2173
+ }
2174
+ catch (error) {
2175
+ proceed = false;
2176
+ chain = undefined;
2177
+ void this.handleDispatchError(error, httpReq, httpRes);
2178
+ }
2179
+ if (chain) {
2180
+ // The chain went async: route once it settles, unless it answered.
2181
+ proceed = false;
2182
+ chain.then(() => {
2183
+ if (!httpRes.headersSent)
2184
+ this.dispatchRouter(httpReq, httpRes);
2171
2185
  }, error => void this.handleDispatchError(error, httpReq, httpRes));
2172
2186
  }
2173
- else {
2174
- // Sync miss from the router - direct-route fallback, then 404
2175
- void this.finishUnrouted(httpReq, httpRes);
2187
+ else if (httpRes.headersSent) {
2188
+ proceed = false; // a middleware answered the request itself
2176
2189
  }
2177
2190
  }
2191
+ if (proceed)
2192
+ this.dispatchRouter(httpReq, httpRes);
2178
2193
  }
2179
2194
  else {
2180
2195
  void this.handleRequest(httpReq, httpRes);
@@ -2192,6 +2207,36 @@ export class MoroEngineServer {
2192
2207
  this._armMicrotaskDrain();
2193
2208
  }
2194
2209
  }
2210
+ // The router dispatch of the sync fast path, and of a global middleware
2211
+ // chain's async continuation: the unified router, then the direct route
2212
+ // table and the 404 behind it, with the same error shaping as handleRequest.
2213
+ dispatchRouter(httpReq, httpRes) {
2214
+ const routerHandler = this.routerHandler;
2215
+ if (!routerHandler) {
2216
+ void this.finishUnrouted(httpReq, httpRes);
2217
+ return;
2218
+ }
2219
+ let handled = true;
2220
+ try {
2221
+ handled = routerHandler(httpReq, httpRes);
2222
+ }
2223
+ catch (error) {
2224
+ // Error path owns the response from here (handled stays true)
2225
+ void this.handleDispatchError(error, httpReq, httpRes);
2226
+ }
2227
+ if (handled !== true) {
2228
+ if (handled && typeof handled.then === 'function') {
2229
+ handled.then(ok => {
2230
+ if (!ok)
2231
+ void this.finishUnrouted(httpReq, httpRes);
2232
+ }, error => void this.handleDispatchError(error, httpReq, httpRes));
2233
+ }
2234
+ else {
2235
+ // Sync miss from the router - direct-route fallback, then 404
2236
+ void this.finishUnrouted(httpReq, httpRes);
2237
+ }
2238
+ }
2239
+ }
2195
2240
  // The engine invokes onRequest with a plain V8 call from its own uv
2196
2241
  // callback, outside a Node callback scope, so microtasks queued during
2197
2242
  // dispatch - an await on an already-settled promise, a .then chain, the
@@ -2357,7 +2402,11 @@ export class MoroEngineServer {
2357
2402
  const raw = this._getBody(httpReq._reqId);
2358
2403
  if (!raw || raw.byteLength === 0)
2359
2404
  return;
2360
- const contentType = httpReq.headers['content-type'] || '';
2405
+ // One header through one crossing: materialising the header object here
2406
+ // would cost every request with a body a string per header line.
2407
+ const contentType = (httpReq._headers !== undefined
2408
+ ? httpReq._headers['content-type']
2409
+ : this._getHeader(httpReq._reqId, 'content-type')) || '';
2361
2410
  const isMultipart = contentType.includes('multipart/form-data');
2362
2411
  const maxSize = isMultipart ? this.maxUploadSize : this.maxBodySize;
2363
2412
  // The engine natively enforces the larger of the two limits; the tighter