Class: MllpServer
MLLP TCP server, wraps net.Server without extending it.
Each accepted socket is wrapped in a NetTransport, connected to a Connection
with server-level framing options, and added to _connections. Messages are
surfaced via the 'message' event and the onMessage callback.
Public events: 'listening', 'connection', 'message', 'nack', 'error', 'close'.
The 'nack' event (NackEvent) fires whenever the server returns a negative ACK
instead of AA on the auto-ACK path, a commit-gated autoAck: 'AA' handler failing, or
a positive auto-ACK downgraded because the inbound could not be correlated (an
unreadable/uncorrelatable message, or one whose bytes the decoder discarded). The
NackReason distinguishes the causes.
'error' contract: underlying net.Server/tls.Server errors are forwarded to the
'error' event whenever a listener is attached. With no listener, the outcome depends on
server state: during a listen() (the bind window) the error rejects the listen() promise
the primary error surface, and the process never crashes on a bind error; with no listen()
in flight and the server not serving (e.g. a stale async error after close()) the error is
dropped; but while serving, an unlistened runtime error (e.g. accept-loop EMFILE) keeps
Node's fail-loud crash-on-unlistened-'error' convention, a silent accept outage is
impossible. Caveat for 'error' listeners: the forwarder runs before the internal
listen() rejection handler, so an 'error' listener that synchronously calls close()
during the bind window changes the listen() rejection from the bind error to the typed
close-during-listen MllpConnectionError, match on the 'error' event payload, not the
rejection, if you do that.
TLS (MLLPS) adds two more: 'tlsClientError' (a failed TLS handshake, the
server logs it and keeps serving other connections) and 'securityWarning' (loud,
one-time notice when a wildcard host is bound via allowWildcardBind: true).
All event payloads are Object.freeze()'d before emission.
Example
import { createServer } from '@cosyte/mllp';
const server = createServer({
onMessage: (payload, meta, conn) => {
console.log('received from', meta.connectionId);
},
});
await server.listen(2575);
// await using server = createServer({ ... }); // Symbol.asyncDispose
Extends
EventEmitter
Constructors
Constructor
new MllpServer(
opts):MllpServer
Construct an MLLP server. Created idle; call listen() (or use
createServer/createStarterServer) to begin accepting connections.
Parameters
opts
Server options (bind host/port, auto-ACK policy, message handler, framing, …).
Returns
MllpServer
Overrides
EventEmitter.constructor
Methods
[asyncDispose]()
[asyncDispose]():
Promise<void>
Async disposal, delegates to close() for await using support.
Returns
Promise<void>
Example
await using server = createServer({ onMessage: handler });
await server.listen(2575);
// server.close() is called automatically at end of block
close()
close(
opts?):Promise<void>
Stop accepting new connections and gracefully close all active connections.
Sequence:
net.Server.close(), stops accepting new connections immediately- If
_connectionsis empty: resolves immediately (no drain needed) - Calls
_drainAll(drainTimeoutMs), Promise.all + side-effect setTimeout that force-destroys stragglers after the drain window
Calling close() while a listen() is still in flight proactively
settles that listen() with a typed MllpConnectionError rejection
and clears the single-flight guard, the server is immediately
re-listenable. Symbol.asyncDispose (which delegates here) is
therefore safe before listen() settles. Qualification: a
close({ signal }) whose signal is already aborted rejects
immediately with AbortError and performs no work, it does NOT close
the server and does NOT settle the in-flight listen(), which simply
continues and settles on its own bind outcome.
Parameters
opts?
drainTimeoutMs?
number
Override drain timeout (default: opts.drainTimeoutMs ?? 30_000).
signal?
AbortSignal
AbortSignal to cancel the close operation. On abort, all active connections are destroyed and the promise rejects with AbortError.
Returns
Promise<void>
Example
await server.close({ drainTimeoutMs: 5_000 });
getStats()
getStats():
ServerStats
Return a JSON-serializable observability snapshot.
totalBytesIn and totalBytesOut aggregate from live connections at call time.
Returns
Example
const stats = server.getStats();
logger.info(JSON.stringify(stats));
listen()
listen(
port,hostOrOpts?):Promise<void>
Start listening on the given port.
Resolves once the TCP socket is bound and emits 'listening' with
Object.freeze({ port: actualPort, host: actualHost }).
Single-flight: one listen() per server lifecycle at a time. A call
while the server is already listening, or while another listen() is
still in flight, rejects with a typed MllpConnectionError (concurrent
binds raced each other's post-bind safety checks). Call close() before
re-listening; sequential listen() → close() → listen() is fine.
close() during an in-flight listen() rejects that listen() with
a typed MllpConnectionError (never a hang) and clears the single-flight
guard, a subsequent listen() on the same server works. This makes
Symbol.asyncDispose (which delegates to close()) safe even before a
listen() has settled. (Exception: close({ signal }) with an
already-aborted signal is a no-op AbortError rejection, the in-flight
listen() is left to settle on its own bind outcome; see close().)
Parameters
port
number
TCP port to bind. Use 0 to let the OS assign an ephemeral port.
hostOrOpts?
string | { host?: string; signal?: AbortSignal; }
Host string or object with host and optional signal.
Default host: '127.0.0.1' (bind-safety hardening). Wildcard hosts
are rejected unless ServerOptions.allowWildcardBind: true, literal
spellings pre-bind, resolver-only shorthands post-bind via the
OS-normalized bound address.
Returns
Promise<void>
Example
await server.listen(2575); // binds 127.0.0.1
await server.listen(0, '127.0.0.1');
await server.listen(0, { host: '127.0.0.1', signal: ac.signal });