Skip to main content
Version: v0.0.3

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

ServerOptions

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:

  1. net.Server.close(), stops accepting new connections immediately
  2. If _connections is empty: resolves immediately (no drain needed)
  3. 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

ServerStats

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 });