@morojs/moro 1.8.8 → 1.8.9

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.
@@ -268,6 +268,7 @@ export declare class MoroEngineServer {
268
268
  private handleDispatchError;
269
269
  private parseBody;
270
270
  private executeMiddleware;
271
+ private dispatchMiddleware;
271
272
  use(middleware: Middleware): void;
272
273
  setRequestTracking(enabled: boolean): void;
273
274
  setHookManager(hookManager: any): void;
@@ -80,6 +80,12 @@ const SENDFILE_BUFFER_LIMIT = 1024 * 1024;
80
80
  // the response has no user-set headers, so no header object or flat array is
81
81
  // allocated. The engine only reads it; never mutate.
82
82
  const JSON_HEADER_PAIR = ['content-type', 'application/json'];
83
+ // Content-Type sniff for send(): does the body start with '{' or '[' after
84
+ // optional leading whitespace? Regex `\s` matches exactly the set trimStart()
85
+ // strips, and the match stops at the first non-space character, so this costs
86
+ // O(leading whitespace) and allocates nothing - trimStart() copied the entire
87
+ // body just to read one character.
88
+ const JSON_BODY_START = /^\s*[{[]/;
83
89
  /**
84
90
  * Prototype-based request object for the engine adapter.
85
91
  *
@@ -705,11 +711,9 @@ export class EngineResponse extends LazyEventEmitter {
705
711
  // Buffers, text/plain otherwise.
706
712
  if (!('content-type' in this.responseHeaders)) {
707
713
  if (typeof data === 'string') {
708
- const t = data.trimStart();
709
- this.responseHeaders['content-type'] =
710
- t.startsWith('{') || t.startsWith('[')
711
- ? 'application/json; charset=utf-8'
712
- : 'text/plain; charset=utf-8';
714
+ this.responseHeaders['content-type'] = JSON_BODY_START.test(data)
715
+ ? 'application/json; charset=utf-8'
716
+ : 'text/plain; charset=utf-8';
713
717
  }
714
718
  else {
715
719
  this.responseHeaders['content-type'] = 'application/octet-stream';
@@ -1816,9 +1820,13 @@ export class MoroEngineServer {
1816
1820
  // rejection). Registration of the response hooks above still stands.
1817
1821
  if (httpRes.headersSent)
1818
1822
  return;
1819
- // Execute global middleware chain
1823
+ // Execute global middleware chain. executeMiddleware returns undefined
1824
+ // when the whole chain completed synchronously, so a sync chain costs
1825
+ // zero promise allocations.
1820
1826
  if (this.globalMiddleware.length > 0) {
1821
- await this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
1827
+ const mwResult = this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
1828
+ if (mwResult)
1829
+ await mwResult;
1822
1830
  if (httpRes.headersSent)
1823
1831
  return;
1824
1832
  }
@@ -1957,42 +1965,103 @@ export class MoroEngineServer {
1957
1965
  throw error;
1958
1966
  }
1959
1967
  }
1960
- async executeMiddleware(middleware, req, res) {
1961
- // Express error semantics: a middleware that calls next(err) (or throws)
1962
- // routes to the NEXT 4-arg error handler (err, req, res, next), skipping
1963
- // the normal 3-arg middleware in between; normal middleware is skipped
1964
- // while an error is pending, and error handlers are skipped when there is
1965
- // no error. An uncaught error propagates to handleRequest's 500 path.
1966
- let error = null;
1967
- for (const mw of middleware) {
1968
+ // Middleware execution with Express-compatible error propagation.
1969
+ // Supports: 3-arg (req, res, next), 4-arg (err, req, res, next) error middlewares,
1970
+ // and next(err) to skip forward to the next error middleware.
1971
+ //
1972
+ // Sync-aware dispatch (mirrors MoroHttpServer.executeMiddleware): middleware
1973
+ // that calls next() synchronously (or throws synchronously) advances the
1974
+ // chain in a plain loop with ZERO promise allocations. A promise is only
1975
+ // created when a middleware actually completes asynchronously. Returns
1976
+ // undefined when the whole chain ran synchronously, so the caller can skip
1977
+ // its await - one sync global middleware used to cost a promise, a closure
1978
+ // and an async-context suspension on every single request.
1979
+ executeMiddleware(middleware, req, res) {
1980
+ return this.dispatchMiddleware(middleware, req, res, 0, undefined);
1981
+ }
1982
+ dispatchMiddleware(middleware, req, res, startIndex, initialError) {
1983
+ const len = middleware.length;
1984
+ let activeError = initialError;
1985
+ let i = startIndex;
1986
+ while (i < len) {
1968
1987
  if (res.headersSent)
1969
- break;
1988
+ return;
1989
+ const mw = middleware[i];
1990
+ i++;
1970
1991
  const isErrorHandler = mw.length >= 4;
1971
- if (error && !isErrorHandler)
1992
+ // Non-error middleware is skipped while an error is active; error middleware
1993
+ // is skipped while no error is active. Matches Express semantics.
1994
+ if (activeError !== undefined && !isErrorHandler)
1972
1995
  continue;
1973
- if (!error && isErrorHandler)
1996
+ if (activeError === undefined && isErrorHandler)
1974
1997
  continue;
1998
+ let settled = false;
1999
+ let settledError = undefined;
2000
+ let asyncResolve;
2001
+ const next = (err) => {
2002
+ if (settled)
2003
+ return;
2004
+ settled = true;
2005
+ settledError = err;
2006
+ if (asyncResolve)
2007
+ asyncResolve(err);
2008
+ };
2009
+ let result;
1975
2010
  try {
1976
- await new Promise((resolve, reject) => {
1977
- const next = (err) => (err ? reject(err) : resolve());
1978
- try {
1979
- const result = error ? mw(error, req, res, next) : mw(req, res, next);
1980
- if (result && typeof result.then === 'function') {
1981
- result.then(() => resolve(), reject);
1982
- }
1983
- }
1984
- catch (e) {
1985
- reject(e);
1986
- }
1987
- });
1988
- error = null; // handler (error or normal) completed cleanly
2011
+ result = isErrorHandler ? mw(activeError, req, res, next) : mw(req, res, next);
1989
2012
  }
1990
- catch (e) {
1991
- error = e; // route to the next error handler
2013
+ catch (err) {
2014
+ if (!settled) {
2015
+ settled = true;
2016
+ settledError = err;
2017
+ }
1992
2018
  }
2019
+ const isThenable = result && typeof result.then === 'function';
2020
+ if (settled) {
2021
+ // Completed synchronously (next() called sync, or threw sync). A still
2022
+ // pending returned promise no longer gates the chain once next() has
2023
+ // been called; swallow late rejections so they never go unhandled.
2024
+ if (isThenable) {
2025
+ result.then(undefined, () => { });
2026
+ }
2027
+ activeError = settledError;
2028
+ continue;
2029
+ }
2030
+ if (!isThenable && res.headersSent) {
2031
+ // Terminal middleware (Express style): it answered the request and
2032
+ // deliberately never calls next(). The chain is finished - skip
2033
+ // allocating a promise that nothing would ever settle (the response was
2034
+ // already sent, so this only ever produced immediate garbage).
2035
+ return;
2036
+ }
2037
+ if (!isThenable) {
2038
+ // Sync-looking middleware that neither called next() nor returned a
2039
+ // promise: it will call next() later (e.g. from an event callback).
2040
+ // Fall back to a promise for the remainder of the chain.
2041
+ const resumeIndex = i;
2042
+ return new Promise(resolve => {
2043
+ asyncResolve = resolve;
2044
+ }).then(err => this.dispatchMiddleware(middleware, req, res, resumeIndex, err));
2045
+ }
2046
+ // Async middleware: completion is next() OR promise settle, whichever
2047
+ // happens first (same semantics as the previous implementation).
2048
+ const resumeIndex = i;
2049
+ return new Promise(resolve => {
2050
+ asyncResolve = resolve;
2051
+ result.then(() => {
2052
+ if (!settled)
2053
+ next();
2054
+ }, (err) => {
2055
+ if (!settled)
2056
+ next(err);
2057
+ });
2058
+ }).then(err => this.dispatchMiddleware(middleware, req, res, resumeIndex, err));
2059
+ }
2060
+ // If an error is still unhandled after the chain, re-throw so the top-level
2061
+ // catch in handleRequest can invoke the registered errorHandler / default 500.
2062
+ if (activeError !== undefined) {
2063
+ throw activeError;
1993
2064
  }
1994
- if (error)
1995
- throw error;
1996
2065
  }
1997
2066
  // Public API - matches MoroHttpServer/UWebSocketsHttpServer interface
1998
2067
  use(middleware) {