Send and receive HL7 v2 over a production-grade MLLP connection in a few lines, with framing, ACK correlation, auto-reconnect, and backpressure handled for you.
A developer-focused MLLP (Minimal Lower Layer Protocol) client and server for Node.js and TypeScript. Transport-only sibling to @cosyte/hl7 (the parser): @cosyte/mllp moves the bytes, @cosyte/hl7 reads them.
Status: under active development. API not yet stable (0.0.x).
# pnpm (recommended), also works with: npm install @cosyte/mllp | yarn add @cosyte/mllp
pnpm add @cosyte/mllpimport { createStarterServer } from "@cosyte/mllp";
// Auto-ACK is on by default: the server awaits your handler (the durable-commit
// step) before it answers, so a positive ACK can never precede the commit.
// A throw answers a negative ACK instead.
const server = await createStarterServer({
port: 2575,
onMessage: async (payload) => {
await db.commit(payload);
},
});import { createStarterClient } from "@cosyte/mllp";
const client = await createStarterClient({ host: "localhost", port: 2575 });
const ack = await client.send(Buffer.from("MSH|^~\\&|..."));The payload API is Buffer-first everywhere. HL7 v2 messages are raw bytes with caller-managed charset decoding.
TLS is built on node:tls: no bundled TLS, no extra dependency. Certificate verification is on
by default; the server binds 127.0.0.1 by default and requires an explicit opt-in
(allowWildcardBind: true) to bind all interfaces.
// Server: plain TLS
const server = createServer({ tls: { cert: certPem, key: keyPem } });
await server.listen(2575, "127.0.0.1");
// Server: mutual TLS (ATNA ITI-19)
const mutualTlsServer = createServer({
tls: { cert: certPem, key: keyPem, ca: clientCaPem, clientAuth: "MUST" },
});// Client
const client = createClient({ host: "mllp.example.com", port: 2575, tls: { ca: caPem } });
// Client: mutual TLS
const mutualTlsClient = createClient({
host: "mllp.example.com",
port: 2575,
tls: { ca: caPem, cert: clientCertPem, key: clientKeyPem },
});See the MLLPS / TLS doc for the ClientAuth table, the TLS 1.2 floor (IHE ATNA ITI-19), typed
failure modes (tls-verify vs tls-handshake), and bind-safety details.
A positive acknowledgement (AA) tells the sender "you may forget this message. I have it." So a
receiver must never send one before the message is durably handled, or the message is silently lost.
@cosyte/mllp makes that structural: pair autoAck: 'AA' with an onMessage handler and the server
awaits your handler (the durable-commit step) and only then ACKs.
const server = createServer({
autoAck: "AA",
onMessage: async (payload) => {
await db.commit(payload); // throw here ⇒ AE (resend may succeed), never AA
},
});Handler resolves ⇒ AA, unless the inbound could not carry a correlatable positive ACK (no
readable MSH, an empty MSH-10, a batch or concatenated payload, or trailing bytes the framer
discarded). In that case the commit still happened, but the ACK is downgraded to AE and a nack
event names the reason, because a positive ACK the sender cannot match is one it will resend.
Handler throws ⇒ AE (or AR via MllpAckError). A positive ACK cannot
precede a successful commit. autoAck: 'AA' without a handler is documented as a
transport-accept: "received and framed", not "processed".
- Client + server with strict MLLP framing (
VT + payload + FS + CR), ACK correlation, auto-reconnect with backoff, and backpressure. - The commit contract: a positive ACK can never precede a durable commit; an unparseable inbound can never yield a positive ACK.
- Explicit 6-state connection machine (
CONNECTING | CONNECTED | DRAINING | RECONNECTING | DISCONNECTED | CLOSED) withstateChangeevents, never socket flags. - Lenient decoder, strict encoder (Postel's Law) with 11 stable warning codes carrying byte offsets. Tolerance is opt-in per flag; the server ships tolerant defaults.
- TLS (MLLPS): verification on by default, mutual TLS (
clientAuth: 'NONE' | 'WANT' | 'MUST'), a TLS 1.2 floor per IHE ATNA ITI-19, and bind-safety guardrails (127.0.0.1default, wildcard binds require opt-in).AbortSignalon every awaitable andSymbol.asyncDisposeon every closeable. - PHI-safe diagnostics: no error, warning, event payload, or stats object ever echoes a run of message content; a framing error carries at most the single byte at the structural violation.
- In-memory transport (
@cosyte/mllp/testing): a deterministic, socket-free test double for fast, reliable tests. @cosyte/hl7as an optional peer: the@cosyte/mllp/ack-from-hl7subpath builds ACKs from parsed messages when it's installed.- Zero runtime dependencies. Node stdlib only.
MLLP + ACK is at-least-once at best. Your application owns idempotency and de-duplication
(MSH-10 + MSH-7). This package does not parse HL7 (use @cosyte/hl7), does not queue or replay
unacked messages, does not decide clinical acceptance, does not speak MLLP Release 2, and ships
no PKI. The full list is in the Known limitations & non-goals doc. Read it before you depend on
this.
Epic, Cerner, Mirth Connect, NextGen, and Google Cloud Healthcare are trademarks of their respective owners. cosyte is not affiliated with, endorsed by, or sponsored by any of them. The names identify the engines this package is tested against, and those it is not. See TRADEMARKS.md.
MIT