Skip to main content
Version: v0.0.3

Function: buildRawAck()

buildRawAck(payload, code): Buffer

Build a minimal original-mode HL7 v2 acknowledgement from raw inbound payload bytes, without a parser (parser-driven ACKs are the @cosyte/mllp/ack-from-hl7 subpath).

Locates the MSH segment (the first CR/LF-delimited segment starting with MSH, so a leading CR or an FHS/BHS batch header does not hide it), reads the field separator from MSH-1 and the encoding characters from MSH-2, then splits that segment on that separator to read fields. The ACK swaps sender/receiver per HL7 ACK rules, echoes the inbound MSH-10 into MSA-2 (§2.9.2.2), sets MSA-1 to code, and, for negative codes, adds a static, PHI-free MSA-3 reason. A fresh control ID fills the ACK's own MSH-10.

Why the delimiters are read, not assumed

MSH-1 is the field separator (HL7 v2.5.1 §2.5.4): the byte at offset 3 of the MSH segment defines it for the whole message. | is overwhelmingly common but it is a convention, not the spec. Assuming it was wrong in two compounding ways:

  1. Reading. Splitting a !-delimited message on | yields ONE field, so every echoed field, including MSH-10, came back empty. The ACK went out as MSA|AA| with no correlation id at all: the sender cannot match it, times out, and resends → a duplicate clinical message. That is the exact failure this package's correlator exists to prevent, manufactured by the ACK builder itself.
  2. Writing. The echoed MSH-3..6 and MSH-10 field content is copied verbatim from the inbound, still escaped against the inbound's encoding characters. Re- emitting that content under a different delimiter set silently reinterprets it: an inbound whose component separator is # and whose MSH-10 is ID#X would be re-emitted under ^~\& as the literal ID#X, which the sender then reads as a single component rather than two. Echoing MSH-1 and MSH-2 keeps the content and the delimiters that define it together.

It reads the MSH through the SHARED scanner

The MSH read is readMshSegment from src/internal/control-id.ts, the same call the client's correlator makes to derive the key it will later match this ACK against. That is deliberate and it is the whole point: this builder used to re-derive the read itself (payload.toString("latin1").split("\r"), hunting for an MSH anywhere in the payload), and the two disagreed on real inputs. On a truncated MSH followed by a PID the correlator keyed on the PID's MRN while this builder echoed an empty MSA-2. On a payload with a leading CR, which the MLLP decoder passes straight through, and which real senders emit, this builder found the MSH and echoed MSH-10 correctly while the correlator, requiring MSH at byte 0, gave up. Every such disagreement is an ACK the sender cannot match → timeout → resend → duplicate clinical message.

The fix is one scan, but note which scan. The first attempt made them agree by requiring MSH at byte 0 everywhere, which "resolved" the leading-CR disagreement by degrading the side that had been right: buildRawAck began emitting a positive AA with an empty MSA-2, silently, for a message whose MSH-10 was plainly present. Agreement is not the goal; agreeing on the correct, tolerant answer is. A lenient reader may never drop data that is there (Postel's Law, CLAUDE.md).

The fail-safe downgrade: never a positive AA/CA it cannot correlate

A positive acknowledgement is a promise the sender may forget the message. If it names a control ID the sender cannot match, or names one message out of several it never read, the sender times out and resends, committing a duplicate clinical message. So a requested positive code is downgraded to its non-positive counterpart (AAAE, CACE) whenever the payload cannot carry a correlatable positive ACK: no readable MSH, an empty MSH-10, or a batch/concatenated-message shape that a single MSA-2 cannot acknowledge. See rawAckUncorrelatable for the exact conditions and why each is a refusal rather than a widened reader. A requested negative code (AE/AR/CE/CR) is never touched. This mirrors the parser-backed buildMllpAck, which downgrades and warns on an unparseable inbound, the two builders' fail-safe semantics now agree.

Never throws and never copies payload content beyond the routing/control metadata above, readMshSegment stops at the MSH's segment terminator, so no field of any later segment (PID and friends) can be reached, let alone echoed. On a missing or unreadable MSH it returns a minimal well-formed ACK carrying the (downgraded) code so the caller can still respond.

Parameters

payload

Buffer

Raw decoded HL7 v2 payload bytes (MLLP framing already stripped).

code

AckCode

Requested MSA-1 acknowledgement code. A positive AA/CA is downgraded to AE/CE when the message cannot be correlated (see above).

Returns

Buffer

ACK payload bytes (no framing, the caller wraps with encodeFrame).

Example

import { buildRawAck } from '@cosyte/mllp';
const ack = buildRawAck(inboundPayload, 'AE'); // MSA|AE|<inbound MSH-10>|message could not be processed