@cosyte/mllp 0.0.3 → 0.0.5

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.
package/dist/index.d.cts CHANGED
@@ -46,7 +46,21 @@ type WarningCode = "MLLP_MISSING_LEADING_VT" | "MLLP_FS_WITHOUT_CR" | "MLLP_LF_A
46
46
  */
47
47
  interface MllpWarning {
48
48
  readonly code: WarningCode;
49
- /** Stable human-readable description. Never contains payload bytes or secrets. */
49
+ /**
50
+ * Stable human-readable description. It carries structural facts (a byte
51
+ * offset, an accumulated size) and **never a run of payload content**.
52
+ *
53
+ * Two codes carry exactly one input byte, and it is worth knowing which:
54
+ * `MLLP_MISSING_LEADING_VT` and `MLLP_FS_WITHOUT_CR` render the hex of the
55
+ * single byte found where a framing byte was expected, and on a stream that
56
+ * omits its leading `VT` that byte is the first byte of the unframed content.
57
+ * One byte, never a run, the same bound `MllpFramingError.snippet` carries. If
58
+ * that is more than your threat model allows, log `code` and `byteOffset`
59
+ * rather than `message`.
60
+ *
61
+ * The ACK-correlation codes are stricter: their text comes from a frozen
62
+ * registry and does not vary with the wire at all.
63
+ */
50
64
  readonly message: string;
51
65
  /** Absolute stream byte offset where the anomaly was detected. */
52
66
  readonly byteOffset: number;
@@ -1704,7 +1718,9 @@ interface ServerOptions {
1704
1718
  *
1705
1719
  * const server = await createStarterServer({
1706
1720
  * port: 2575,
1707
- * onMessage: (buf) => buildAckBuffer(buf),
1721
+ * onMessage: async (payload) => {
1722
+ * await db.commit(payload); // the durable-commit step; a throw answers a negative ACK
1723
+ * },
1708
1724
  * });
1709
1725
  * // server is listening, auto-ACK enabled, Symbol.asyncDispose wired
1710
1726
  * await using _ = server; // closes on scope exit
@@ -2029,12 +2045,77 @@ declare function createServer(opts?: ServerOptions): MllpServer;
2029
2045
  *
2030
2046
  * const server = await createStarterServer({
2031
2047
  * port: 2575,
2032
- * onMessage: (payload) => buildAck(payload),
2048
+ * onMessage: async (payload) => {
2049
+ * await db.commit(payload); // the durable-commit step; a throw answers a negative ACK
2050
+ * },
2033
2051
  * });
2034
2052
  * ```
2035
2053
  */
2036
2054
  declare function createStarterServer(opts: StarterServerOptions): Promise<MllpServer>;
2037
2055
 
2056
+ /**
2057
+ * The frozen diagnostic registry for ACK correlation.
2058
+ *
2059
+ * Every message an ACK-correlation diagnostic can carry lives here as a
2060
+ * literal, and the lookup takes **only a code**. There is no value parameter
2061
+ * anywhere in this module, which is the whole point: a factory that cannot be
2062
+ * handed a value cannot interpolate one, regardless of what arrives on the
2063
+ * wire.
2064
+ *
2065
+ * ## What this replaces, and why the shape matters more than the wording
2066
+ *
2067
+ * The `correlateByControlId` path used to build its diagnostics by
2068
+ * interpolation, and both ends of it were consumer-controlled and unbounded.
2069
+ * The unmatched-ACK error read the **peer's** MSA-2 straight out of an inbound
2070
+ * frame and put it on an `Error.message`; measured against a peer sending a
2071
+ * one-megabyte MSA-2, that produced a 1,000,026-byte `message` and an
2072
+ * equally large field on the frozen error payload. The late-ACK warning did
2073
+ * the same with the control id of the send being reported on. Both go to a
2074
+ * log, and in this domain a log is a place a patient identifier must not
2075
+ * reach. That is not a hypothetical about control ids either: this package has
2076
+ * already shipped one scanner that ran past a segment terminator and returned
2077
+ * PID-3, a patient's medical record number, as the "control ID" of a truncated
2078
+ * MSH. Withholding the bytes is the defence that survives the next such bug.
2079
+ *
2080
+ * ## What a correlation diagnostic says instead
2081
+ *
2082
+ * That a control id did not match, plus **numbers**: a byte length, a stream
2083
+ * offset, an elapsed time. A number is a shape, not content. A truncated id is
2084
+ * not an acceptable middle ground, and neither is a hex or base64 rendering:
2085
+ * both still grow the diagnostic with the input and both still disclose the
2086
+ * bytes. Nothing is lost by withholding them, because the caller already holds
2087
+ * them: the outbound payload is the caller's own argument to `send()`, and the
2088
+ * inbound frame is on the `'message'` event.
2089
+ *
2090
+ * Mirrors the same answer this package already reached for the ACK adapter's
2091
+ * verbatim-echo warning, which reports byte lengths and withholds MSH-10 and
2092
+ * MSA-2 themselves.
2093
+ *
2094
+ * @packageDocumentation
2095
+ */
2096
+ /**
2097
+ * The two warning codes emitted by ACK correlation rather than by framing.
2098
+ *
2099
+ * A subset of the framing `WarningCode` union; named separately so the
2100
+ * registry below is exhaustive over exactly the codes it owns and a new
2101
+ * correlation code cannot be added without a message for it.
2102
+ */
2103
+ type AckCorrelationCode = "MLLP_ACK_UNMATCHED_CONTROL_ID" | "MLLP_ACK_AFTER_TIMEOUT";
2104
+ /**
2105
+ * Look up the frozen diagnostic text for an ACK-correlation code.
2106
+ *
2107
+ * A pure table read. It takes no value, so no caller can widen it into an
2108
+ * interpolation site.
2109
+ *
2110
+ * @param code - The correlation code being reported.
2111
+ * @returns The registered message for that code, byte-for-byte.
2112
+ * @example
2113
+ * ```typescript
2114
+ * const message = ackDiagnosticMessage('MLLP_ACK_AFTER_TIMEOUT');
2115
+ * ```
2116
+ */
2117
+ declare function ackDiagnosticMessage(code: AckCorrelationCode): string;
2118
+
2038
2119
  /**
2039
2120
  * MLLP Client, `createClient()` factory and `MllpClient` class.
2040
2121
  *
@@ -2059,6 +2140,37 @@ declare function createStarterServer(opts: StarterServerOptions): Promise<MllpSe
2059
2140
  * @packageDocumentation
2060
2141
  */
2061
2142
 
2143
+ /**
2144
+ * The `'warning'` payload the client emits for an ACK-correlation deviation
2145
+ * (`MLLP_ACK_UNMATCHED_CONTROL_ID`, `MLLP_ACK_AFTER_TIMEOUT`).
2146
+ *
2147
+ * A `MllpWarning` with two extra numeric fields. `message` is a **frozen
2148
+ * registry entry**, byte-for-byte identical for a given code no matter what
2149
+ * arrived on the wire, and everything input-derived is a number: the control
2150
+ * ID's byte length and the elapsed time. A warning is a log line, so it carries
2151
+ * no field content.
2152
+ *
2153
+ * @example
2154
+ * ```typescript
2155
+ * client.on('warning', (w: AckCorrelationWarning) => {
2156
+ * logger.warn({ code: w.code, idBytes: w.controlIdBytes, elapsedMs: w.elapsedSinceSendMs });
2157
+ * });
2158
+ * ```
2159
+ */
2160
+ interface AckCorrelationWarning extends MllpWarning {
2161
+ /** The correlation code this warning reports. */
2162
+ readonly code: AckCorrelationCode;
2163
+ /**
2164
+ * Byte length of the control ID involved, or `null` when there was none to
2165
+ * read. Control IDs are decoded `latin1`, so a code-unit count is a byte count.
2166
+ */
2167
+ readonly controlIdBytes: number | null;
2168
+ /**
2169
+ * Milliseconds between the send's write-flush (or its timeout, for a late
2170
+ * ACK) and this warning.
2171
+ */
2172
+ readonly elapsedSinceSendMs: number;
2173
+ }
2062
2174
  /**
2063
2175
  * Context passed to a custom `retryStrategy` hook on each reconnect attempt.
2064
2176
  *
@@ -2311,7 +2423,10 @@ interface ClientStats {
2311
2423
  * - `'reconnecting'`, `{ connectionId, attempt?, delayMs? }`
2312
2424
  * - `'close'`, `{ connectionId }` once the FSM enters terminal `CLOSED`
2313
2425
  * - `'message'`, `{ payload, connectionId, byteOffset, warnings }` for every inbound frame
2314
- * - `'warning'`, `MllpWarning` enriched with `connectionId` from the Connection layer
2426
+ * - `'warning'`, `MllpWarning | AckCorrelationWarning`. A framing deviation arrives as a
2427
+ * `MllpWarning` enriched with `connectionId` from the Connection layer; the two
2428
+ * ACK-correlation codes arrive as {@link AckCorrelationWarning}, which adds
2429
+ * `controlIdBytes` and `elapsedSinceSendMs`. Narrow on `code` before reading either.
2315
2430
  * - `'securityWarning'`, `SecurityWarning`. Emitted on every successful
2316
2431
  * `secureConnect` (initial + every reconnect) when `tls.allowUnverified` is `true`
2317
2432
  * with code `MLLP_TLS_VERIFY_DISABLED`. Also mirrored to `process.emitWarning`.
@@ -2866,15 +2981,27 @@ declare function createStarterClient(opts: StarterClientOptions): Promise<MllpCl
2866
2981
  * await client.send(payload);
2867
2982
  * } catch (err) {
2868
2983
  * if (err instanceof MllpTimeoutError) {
2869
- * logger.warn({ elapsedMs: err.elapsedMs, controlId: err.messageControlId });
2984
+ * logger.warn({ elapsedMs: err.elapsedMs, idBytes: err.messageControlIdBytes });
2870
2985
  * }
2871
2986
  * }
2872
2987
  * ```
2873
2988
  */
2874
2989
  declare class MllpTimeoutError extends Error {
2875
2990
  readonly name: "MllpTimeoutError";
2876
- /** MSH-10 control ID of the timed-out send (FIFO mode: `undefined`). */
2877
- readonly messageControlId: string | undefined;
2991
+ /**
2992
+ * Byte length of the timed-out send's MSH-10 control ID, or `undefined` when
2993
+ * there was none to read (FIFO mode, or a payload with no MSH-10).
2994
+ *
2995
+ * **The control ID itself is deliberately not here.** An `Error` is a
2996
+ * diagnostic surface: it is logged, and its `stack` is what an error reporter
2997
+ * ships off the box. MSH-10 is payload content, and a scanner that returns
2998
+ * the wrong field returns payload content of some other kind, which is how a
2999
+ * patient identifier reaches a log line. Nothing is lost by withholding it,
3000
+ * because this error rejects the very `send()` whose payload the caller
3001
+ * passed in, so the caller already holds the bytes. Control IDs are decoded
3002
+ * `latin1`, a 1:1 byte to code-unit map, so this count is a byte count.
3003
+ */
3004
+ readonly messageControlIdBytes: number | undefined;
2878
3005
  /** Milliseconds elapsed between write-flush and timeout fire. */
2879
3006
  readonly elapsedMs: number;
2880
3007
  /** Epoch ms timestamp recorded at write-flush callback. */
@@ -2882,11 +3009,11 @@ declare class MllpTimeoutError extends Error {
2882
3009
  /**
2883
3010
  * Construct an MLLP timeout error.
2884
3011
  *
2885
- * @param message - Human-readable error message.
2886
- * @param opts - Timeout context (originating message control id, elapsed time, flush timestamp).
3012
+ * @param message - Human-readable error message. Structural facts only, never field content.
3013
+ * @param opts - Timeout context (control-id byte length, elapsed time, flush timestamp).
2887
3014
  */
2888
3015
  constructor(message: string, opts: {
2889
- messageControlId: string | undefined;
3016
+ messageControlIdBytes: number | undefined;
2890
3017
  elapsedMs: number;
2891
3018
  sentAt: number;
2892
3019
  });
@@ -3087,4 +3214,4 @@ declare class MllpBackpressureError extends Error {
3087
3214
  */
3088
3215
  declare const VERSION: string;
3089
3216
 
3090
- export { type AckCode, type ClientAuth, type ClientOptions, type ClientStats, Connection, type ConnectionErrorCause, type ConnectionErrorPhase, type ConnectionOptions, type ConnectionState, type ConnectionStats, type EncoderOptions, FrameReader, type FrameReaderOptions, MLLP_BIND_ALL_INTERFACES, MLLP_TLS_VERIFY_DISABLED, type MessageMeta, MllpAckError, MllpBackpressureError, MllpClient, MllpConnectionError, MllpFramingError, MllpServer, MllpTimeoutError, type MllpWarning, type NackEvent, type NackReason, type NegativeAckCode, NetTransport, type PemInput, type ReconnectingEvent, type RetryContext, type RetryStrategy, type SecurityWarning, type SecurityWarningCode, type ServerOptions, type ServerStats, type ServerTlsOptions, type StarterClientOptions, type StarterServerOptions, type StateChangeEvent, type TlsOptions, TlsTransport, Transport, VERSION, type WarningCode, buildRawAck, createClient, createServer, createStarterClient, createStarterServer, createWarning, encodeFrame, isTlsProtocolError, isTlsVerificationErrorCode, isTransientConnectionError, rawAckUncorrelatable, resolveNackCode };
3217
+ export { type AckCode, type AckCorrelationCode, type AckCorrelationWarning, type ClientAuth, type ClientOptions, type ClientStats, Connection, type ConnectionErrorCause, type ConnectionErrorPhase, type ConnectionOptions, type ConnectionState, type ConnectionStats, type EncoderOptions, FrameReader, type FrameReaderOptions, MLLP_BIND_ALL_INTERFACES, MLLP_TLS_VERIFY_DISABLED, type MessageMeta, MllpAckError, MllpBackpressureError, MllpClient, MllpConnectionError, MllpFramingError, MllpServer, MllpTimeoutError, type MllpWarning, type NackEvent, type NackReason, type NegativeAckCode, NetTransport, type PemInput, type ReconnectingEvent, type RetryContext, type RetryStrategy, type SecurityWarning, type SecurityWarningCode, type ServerOptions, type ServerStats, type ServerTlsOptions, type StarterClientOptions, type StarterServerOptions, type StateChangeEvent, type TlsOptions, TlsTransport, Transport, VERSION, type WarningCode, ackDiagnosticMessage, buildRawAck, createClient, createServer, createStarterClient, createStarterServer, createWarning, encodeFrame, isTlsProtocolError, isTlsVerificationErrorCode, isTransientConnectionError, rawAckUncorrelatable, resolveNackCode };
package/dist/index.d.ts CHANGED
@@ -46,7 +46,21 @@ type WarningCode = "MLLP_MISSING_LEADING_VT" | "MLLP_FS_WITHOUT_CR" | "MLLP_LF_A
46
46
  */
47
47
  interface MllpWarning {
48
48
  readonly code: WarningCode;
49
- /** Stable human-readable description. Never contains payload bytes or secrets. */
49
+ /**
50
+ * Stable human-readable description. It carries structural facts (a byte
51
+ * offset, an accumulated size) and **never a run of payload content**.
52
+ *
53
+ * Two codes carry exactly one input byte, and it is worth knowing which:
54
+ * `MLLP_MISSING_LEADING_VT` and `MLLP_FS_WITHOUT_CR` render the hex of the
55
+ * single byte found where a framing byte was expected, and on a stream that
56
+ * omits its leading `VT` that byte is the first byte of the unframed content.
57
+ * One byte, never a run, the same bound `MllpFramingError.snippet` carries. If
58
+ * that is more than your threat model allows, log `code` and `byteOffset`
59
+ * rather than `message`.
60
+ *
61
+ * The ACK-correlation codes are stricter: their text comes from a frozen
62
+ * registry and does not vary with the wire at all.
63
+ */
50
64
  readonly message: string;
51
65
  /** Absolute stream byte offset where the anomaly was detected. */
52
66
  readonly byteOffset: number;
@@ -1704,7 +1718,9 @@ interface ServerOptions {
1704
1718
  *
1705
1719
  * const server = await createStarterServer({
1706
1720
  * port: 2575,
1707
- * onMessage: (buf) => buildAckBuffer(buf),
1721
+ * onMessage: async (payload) => {
1722
+ * await db.commit(payload); // the durable-commit step; a throw answers a negative ACK
1723
+ * },
1708
1724
  * });
1709
1725
  * // server is listening, auto-ACK enabled, Symbol.asyncDispose wired
1710
1726
  * await using _ = server; // closes on scope exit
@@ -2029,12 +2045,77 @@ declare function createServer(opts?: ServerOptions): MllpServer;
2029
2045
  *
2030
2046
  * const server = await createStarterServer({
2031
2047
  * port: 2575,
2032
- * onMessage: (payload) => buildAck(payload),
2048
+ * onMessage: async (payload) => {
2049
+ * await db.commit(payload); // the durable-commit step; a throw answers a negative ACK
2050
+ * },
2033
2051
  * });
2034
2052
  * ```
2035
2053
  */
2036
2054
  declare function createStarterServer(opts: StarterServerOptions): Promise<MllpServer>;
2037
2055
 
2056
+ /**
2057
+ * The frozen diagnostic registry for ACK correlation.
2058
+ *
2059
+ * Every message an ACK-correlation diagnostic can carry lives here as a
2060
+ * literal, and the lookup takes **only a code**. There is no value parameter
2061
+ * anywhere in this module, which is the whole point: a factory that cannot be
2062
+ * handed a value cannot interpolate one, regardless of what arrives on the
2063
+ * wire.
2064
+ *
2065
+ * ## What this replaces, and why the shape matters more than the wording
2066
+ *
2067
+ * The `correlateByControlId` path used to build its diagnostics by
2068
+ * interpolation, and both ends of it were consumer-controlled and unbounded.
2069
+ * The unmatched-ACK error read the **peer's** MSA-2 straight out of an inbound
2070
+ * frame and put it on an `Error.message`; measured against a peer sending a
2071
+ * one-megabyte MSA-2, that produced a 1,000,026-byte `message` and an
2072
+ * equally large field on the frozen error payload. The late-ACK warning did
2073
+ * the same with the control id of the send being reported on. Both go to a
2074
+ * log, and in this domain a log is a place a patient identifier must not
2075
+ * reach. That is not a hypothetical about control ids either: this package has
2076
+ * already shipped one scanner that ran past a segment terminator and returned
2077
+ * PID-3, a patient's medical record number, as the "control ID" of a truncated
2078
+ * MSH. Withholding the bytes is the defence that survives the next such bug.
2079
+ *
2080
+ * ## What a correlation diagnostic says instead
2081
+ *
2082
+ * That a control id did not match, plus **numbers**: a byte length, a stream
2083
+ * offset, an elapsed time. A number is a shape, not content. A truncated id is
2084
+ * not an acceptable middle ground, and neither is a hex or base64 rendering:
2085
+ * both still grow the diagnostic with the input and both still disclose the
2086
+ * bytes. Nothing is lost by withholding them, because the caller already holds
2087
+ * them: the outbound payload is the caller's own argument to `send()`, and the
2088
+ * inbound frame is on the `'message'` event.
2089
+ *
2090
+ * Mirrors the same answer this package already reached for the ACK adapter's
2091
+ * verbatim-echo warning, which reports byte lengths and withholds MSH-10 and
2092
+ * MSA-2 themselves.
2093
+ *
2094
+ * @packageDocumentation
2095
+ */
2096
+ /**
2097
+ * The two warning codes emitted by ACK correlation rather than by framing.
2098
+ *
2099
+ * A subset of the framing `WarningCode` union; named separately so the
2100
+ * registry below is exhaustive over exactly the codes it owns and a new
2101
+ * correlation code cannot be added without a message for it.
2102
+ */
2103
+ type AckCorrelationCode = "MLLP_ACK_UNMATCHED_CONTROL_ID" | "MLLP_ACK_AFTER_TIMEOUT";
2104
+ /**
2105
+ * Look up the frozen diagnostic text for an ACK-correlation code.
2106
+ *
2107
+ * A pure table read. It takes no value, so no caller can widen it into an
2108
+ * interpolation site.
2109
+ *
2110
+ * @param code - The correlation code being reported.
2111
+ * @returns The registered message for that code, byte-for-byte.
2112
+ * @example
2113
+ * ```typescript
2114
+ * const message = ackDiagnosticMessage('MLLP_ACK_AFTER_TIMEOUT');
2115
+ * ```
2116
+ */
2117
+ declare function ackDiagnosticMessage(code: AckCorrelationCode): string;
2118
+
2038
2119
  /**
2039
2120
  * MLLP Client, `createClient()` factory and `MllpClient` class.
2040
2121
  *
@@ -2059,6 +2140,37 @@ declare function createStarterServer(opts: StarterServerOptions): Promise<MllpSe
2059
2140
  * @packageDocumentation
2060
2141
  */
2061
2142
 
2143
+ /**
2144
+ * The `'warning'` payload the client emits for an ACK-correlation deviation
2145
+ * (`MLLP_ACK_UNMATCHED_CONTROL_ID`, `MLLP_ACK_AFTER_TIMEOUT`).
2146
+ *
2147
+ * A `MllpWarning` with two extra numeric fields. `message` is a **frozen
2148
+ * registry entry**, byte-for-byte identical for a given code no matter what
2149
+ * arrived on the wire, and everything input-derived is a number: the control
2150
+ * ID's byte length and the elapsed time. A warning is a log line, so it carries
2151
+ * no field content.
2152
+ *
2153
+ * @example
2154
+ * ```typescript
2155
+ * client.on('warning', (w: AckCorrelationWarning) => {
2156
+ * logger.warn({ code: w.code, idBytes: w.controlIdBytes, elapsedMs: w.elapsedSinceSendMs });
2157
+ * });
2158
+ * ```
2159
+ */
2160
+ interface AckCorrelationWarning extends MllpWarning {
2161
+ /** The correlation code this warning reports. */
2162
+ readonly code: AckCorrelationCode;
2163
+ /**
2164
+ * Byte length of the control ID involved, or `null` when there was none to
2165
+ * read. Control IDs are decoded `latin1`, so a code-unit count is a byte count.
2166
+ */
2167
+ readonly controlIdBytes: number | null;
2168
+ /**
2169
+ * Milliseconds between the send's write-flush (or its timeout, for a late
2170
+ * ACK) and this warning.
2171
+ */
2172
+ readonly elapsedSinceSendMs: number;
2173
+ }
2062
2174
  /**
2063
2175
  * Context passed to a custom `retryStrategy` hook on each reconnect attempt.
2064
2176
  *
@@ -2311,7 +2423,10 @@ interface ClientStats {
2311
2423
  * - `'reconnecting'`, `{ connectionId, attempt?, delayMs? }`
2312
2424
  * - `'close'`, `{ connectionId }` once the FSM enters terminal `CLOSED`
2313
2425
  * - `'message'`, `{ payload, connectionId, byteOffset, warnings }` for every inbound frame
2314
- * - `'warning'`, `MllpWarning` enriched with `connectionId` from the Connection layer
2426
+ * - `'warning'`, `MllpWarning | AckCorrelationWarning`. A framing deviation arrives as a
2427
+ * `MllpWarning` enriched with `connectionId` from the Connection layer; the two
2428
+ * ACK-correlation codes arrive as {@link AckCorrelationWarning}, which adds
2429
+ * `controlIdBytes` and `elapsedSinceSendMs`. Narrow on `code` before reading either.
2315
2430
  * - `'securityWarning'`, `SecurityWarning`. Emitted on every successful
2316
2431
  * `secureConnect` (initial + every reconnect) when `tls.allowUnverified` is `true`
2317
2432
  * with code `MLLP_TLS_VERIFY_DISABLED`. Also mirrored to `process.emitWarning`.
@@ -2866,15 +2981,27 @@ declare function createStarterClient(opts: StarterClientOptions): Promise<MllpCl
2866
2981
  * await client.send(payload);
2867
2982
  * } catch (err) {
2868
2983
  * if (err instanceof MllpTimeoutError) {
2869
- * logger.warn({ elapsedMs: err.elapsedMs, controlId: err.messageControlId });
2984
+ * logger.warn({ elapsedMs: err.elapsedMs, idBytes: err.messageControlIdBytes });
2870
2985
  * }
2871
2986
  * }
2872
2987
  * ```
2873
2988
  */
2874
2989
  declare class MllpTimeoutError extends Error {
2875
2990
  readonly name: "MllpTimeoutError";
2876
- /** MSH-10 control ID of the timed-out send (FIFO mode: `undefined`). */
2877
- readonly messageControlId: string | undefined;
2991
+ /**
2992
+ * Byte length of the timed-out send's MSH-10 control ID, or `undefined` when
2993
+ * there was none to read (FIFO mode, or a payload with no MSH-10).
2994
+ *
2995
+ * **The control ID itself is deliberately not here.** An `Error` is a
2996
+ * diagnostic surface: it is logged, and its `stack` is what an error reporter
2997
+ * ships off the box. MSH-10 is payload content, and a scanner that returns
2998
+ * the wrong field returns payload content of some other kind, which is how a
2999
+ * patient identifier reaches a log line. Nothing is lost by withholding it,
3000
+ * because this error rejects the very `send()` whose payload the caller
3001
+ * passed in, so the caller already holds the bytes. Control IDs are decoded
3002
+ * `latin1`, a 1:1 byte to code-unit map, so this count is a byte count.
3003
+ */
3004
+ readonly messageControlIdBytes: number | undefined;
2878
3005
  /** Milliseconds elapsed between write-flush and timeout fire. */
2879
3006
  readonly elapsedMs: number;
2880
3007
  /** Epoch ms timestamp recorded at write-flush callback. */
@@ -2882,11 +3009,11 @@ declare class MllpTimeoutError extends Error {
2882
3009
  /**
2883
3010
  * Construct an MLLP timeout error.
2884
3011
  *
2885
- * @param message - Human-readable error message.
2886
- * @param opts - Timeout context (originating message control id, elapsed time, flush timestamp).
3012
+ * @param message - Human-readable error message. Structural facts only, never field content.
3013
+ * @param opts - Timeout context (control-id byte length, elapsed time, flush timestamp).
2887
3014
  */
2888
3015
  constructor(message: string, opts: {
2889
- messageControlId: string | undefined;
3016
+ messageControlIdBytes: number | undefined;
2890
3017
  elapsedMs: number;
2891
3018
  sentAt: number;
2892
3019
  });
@@ -3087,4 +3214,4 @@ declare class MllpBackpressureError extends Error {
3087
3214
  */
3088
3215
  declare const VERSION: string;
3089
3216
 
3090
- export { type AckCode, type ClientAuth, type ClientOptions, type ClientStats, Connection, type ConnectionErrorCause, type ConnectionErrorPhase, type ConnectionOptions, type ConnectionState, type ConnectionStats, type EncoderOptions, FrameReader, type FrameReaderOptions, MLLP_BIND_ALL_INTERFACES, MLLP_TLS_VERIFY_DISABLED, type MessageMeta, MllpAckError, MllpBackpressureError, MllpClient, MllpConnectionError, MllpFramingError, MllpServer, MllpTimeoutError, type MllpWarning, type NackEvent, type NackReason, type NegativeAckCode, NetTransport, type PemInput, type ReconnectingEvent, type RetryContext, type RetryStrategy, type SecurityWarning, type SecurityWarningCode, type ServerOptions, type ServerStats, type ServerTlsOptions, type StarterClientOptions, type StarterServerOptions, type StateChangeEvent, type TlsOptions, TlsTransport, Transport, VERSION, type WarningCode, buildRawAck, createClient, createServer, createStarterClient, createStarterServer, createWarning, encodeFrame, isTlsProtocolError, isTlsVerificationErrorCode, isTransientConnectionError, rawAckUncorrelatable, resolveNackCode };
3217
+ export { type AckCode, type AckCorrelationCode, type AckCorrelationWarning, type ClientAuth, type ClientOptions, type ClientStats, Connection, type ConnectionErrorCause, type ConnectionErrorPhase, type ConnectionOptions, type ConnectionState, type ConnectionStats, type EncoderOptions, FrameReader, type FrameReaderOptions, MLLP_BIND_ALL_INTERFACES, MLLP_TLS_VERIFY_DISABLED, type MessageMeta, MllpAckError, MllpBackpressureError, MllpClient, MllpConnectionError, MllpFramingError, MllpServer, MllpTimeoutError, type MllpWarning, type NackEvent, type NackReason, type NegativeAckCode, NetTransport, type PemInput, type ReconnectingEvent, type RetryContext, type RetryStrategy, type SecurityWarning, type SecurityWarningCode, type ServerOptions, type ServerStats, type ServerTlsOptions, type StarterClientOptions, type StarterServerOptions, type StateChangeEvent, type TlsOptions, TlsTransport, Transport, VERSION, type WarningCode, ackDiagnosticMessage, buildRawAck, createClient, createServer, createStarterClient, createStarterServer, createWarning, encodeFrame, isTlsProtocolError, isTlsVerificationErrorCode, isTransientConnectionError, rawAckUncorrelatable, resolveNackCode };
package/dist/index.mjs CHANGED
@@ -2005,8 +2005,8 @@ var Correlator = class {
2005
2005
  * removed from live store. Caller calls `entry.resolve(ackPayload)`.
2006
2006
  * - controlId: keyed lookup by `controlIdFromAck`. Live-store hit returns
2007
2007
  * the entry; graveyard hit fires `MLLP_ACK_AFTER_TIMEOUT` warning
2008
- * and returns `null`; otherwise fires
2009
- * `onUnmatchedAck(controlIdFromAck)` and returns `null`.
2008
+ * and returns `null`; otherwise fires `onUnmatchedAck` with the id's
2009
+ * **byte length** and returns `null`.
2010
2010
  *
2011
2011
  * Triggers lazy graveyard eviction.
2012
2012
  *
@@ -2029,7 +2029,7 @@ var Correlator = class {
2029
2029
  }
2030
2030
  if (controlIdFromAck === null) {
2031
2031
  if (this._opts.onUnmatchedAck !== void 0) {
2032
- this._opts.onUnmatchedAck("");
2032
+ this._opts.onUnmatchedAck(null);
2033
2033
  }
2034
2034
  return null;
2035
2035
  }
@@ -2044,7 +2044,9 @@ var Correlator = class {
2044
2044
  if (grave !== void 0) {
2045
2045
  const elapsedSinceSendMs = this._opts.now() - grave.timedOutAt;
2046
2046
  this._opts.onWarning("MLLP_ACK_AFTER_TIMEOUT", {
2047
- controlId: grave.controlId,
2047
+ // The LENGTH, never the id. Control IDs are decoded `latin1`, a 1:1
2048
+ // byte to code-unit map, so `.length` is a byte count.
2049
+ controlIdBytes: grave.controlId === null ? null : grave.controlId.length,
2048
2050
  elapsedSinceSendMs,
2049
2051
  byteOffset: byteOffsetFromAck
2050
2052
  });
@@ -2052,7 +2054,7 @@ var Correlator = class {
2052
2054
  return null;
2053
2055
  }
2054
2056
  if (this._opts.onUnmatchedAck !== void 0) {
2055
- this._opts.onUnmatchedAck(controlIdFromAck);
2057
+ this._opts.onUnmatchedAck(controlIdFromAck.length);
2056
2058
  }
2057
2059
  return null;
2058
2060
  }
@@ -2127,11 +2129,32 @@ var Correlator = class {
2127
2129
  }
2128
2130
  };
2129
2131
 
2132
+ // src/client/ack-diagnostics.ts
2133
+ var ACK_DIAGNOSTIC_MESSAGES = Object.freeze({
2134
+ MLLP_ACK_UNMATCHED_CONTROL_ID: "Inbound ACK control ID matched no in-flight send and no timed-out send; the ACK was dropped. Field values are withheld: MSA-2 is inbound payload content and this diagnostic goes to a log. The inbound bytes are on the 'message' event; controlIdBytes gives the length.",
2135
+ MLLP_ACK_AFTER_TIMEOUT: "ACK arrived after its send had already timed out; the ACK was dropped. Field values are withheld: a control ID is payload content and this diagnostic goes to a log. The outbound bytes are the payload you passed to send(); controlIdBytes gives the length."
2136
+ });
2137
+ function ackDiagnosticMessage(code) {
2138
+ return ACK_DIAGNOSTIC_MESSAGES[code];
2139
+ }
2140
+
2130
2141
  // src/client/error.ts
2131
2142
  var MllpTimeoutError = class _MllpTimeoutError extends Error {
2132
2143
  name = "MllpTimeoutError";
2133
- /** MSH-10 control ID of the timed-out send (FIFO mode: `undefined`). */
2134
- messageControlId;
2144
+ /**
2145
+ * Byte length of the timed-out send's MSH-10 control ID, or `undefined` when
2146
+ * there was none to read (FIFO mode, or a payload with no MSH-10).
2147
+ *
2148
+ * **The control ID itself is deliberately not here.** An `Error` is a
2149
+ * diagnostic surface: it is logged, and its `stack` is what an error reporter
2150
+ * ships off the box. MSH-10 is payload content, and a scanner that returns
2151
+ * the wrong field returns payload content of some other kind, which is how a
2152
+ * patient identifier reaches a log line. Nothing is lost by withholding it,
2153
+ * because this error rejects the very `send()` whose payload the caller
2154
+ * passed in, so the caller already holds the bytes. Control IDs are decoded
2155
+ * `latin1`, a 1:1 byte to code-unit map, so this count is a byte count.
2156
+ */
2157
+ messageControlIdBytes;
2135
2158
  /** Milliseconds elapsed between write-flush and timeout fire. */
2136
2159
  elapsedMs;
2137
2160
  /** Epoch ms timestamp recorded at write-flush callback. */
@@ -2139,12 +2162,12 @@ var MllpTimeoutError = class _MllpTimeoutError extends Error {
2139
2162
  /**
2140
2163
  * Construct an MLLP timeout error.
2141
2164
  *
2142
- * @param message - Human-readable error message.
2143
- * @param opts - Timeout context (originating message control id, elapsed time, flush timestamp).
2165
+ * @param message - Human-readable error message. Structural facts only, never field content.
2166
+ * @param opts - Timeout context (control-id byte length, elapsed time, flush timestamp).
2144
2167
  */
2145
2168
  constructor(message, opts) {
2146
2169
  super(message);
2147
- this.messageControlId = opts.messageControlId;
2170
+ this.messageControlIdBytes = opts.messageControlIdBytes;
2148
2171
  this.elapsedMs = opts.elapsedMs;
2149
2172
  this.sentAt = opts.sentAt;
2150
2173
  if (Error.captureStackTrace) {
@@ -2587,31 +2610,34 @@ var MllpClient = class extends EventEmitter {
2587
2610
  maxInFlight: this._pipeline ? Number.POSITIVE_INFINITY : 1,
2588
2611
  onWarning: (code, ctx) => {
2589
2612
  this._aggregatedWarningsByCode[code] = (this._aggregatedWarningsByCode[code] ?? 0) + 1;
2590
- this._emitContained(
2591
- "warning",
2592
- Object.freeze({
2593
- code,
2594
- byteOffset: ctx.byteOffset,
2595
- message: `${code}: controlId=${ctx.controlId} elapsed=${ctx.elapsedSinceSendMs}ms`,
2596
- connectionId: this._connection?.connectionId ?? conn.connectionId,
2597
- timestamp: /* @__PURE__ */ new Date()
2598
- })
2599
- );
2613
+ const warning = Object.freeze({
2614
+ code,
2615
+ byteOffset: ctx.byteOffset,
2616
+ message: ackDiagnosticMessage(code),
2617
+ controlIdBytes: ctx.controlIdBytes,
2618
+ elapsedSinceSendMs: ctx.elapsedSinceSendMs,
2619
+ connectionId: this._connection?.connectionId ?? conn.connectionId,
2620
+ timestamp: /* @__PURE__ */ new Date()
2621
+ });
2622
+ this._emitContained("warning", warning);
2600
2623
  },
2601
- onUnmatchedAck: (controlId) => {
2624
+ onUnmatchedAck: (controlIdBytes) => {
2602
2625
  if (this.listenerCount("error") === 0) return;
2603
2626
  const err = new MllpFramingError(
2604
2627
  "MLLP_ACK_UNMATCHED_CONTROL_ID",
2605
2628
  0,
2606
2629
  Buffer.alloc(0),
2607
- `Unmatched ACK control ID${controlId === "" ? "" : `: ${controlId}`}`
2630
+ ackDiagnosticMessage("MLLP_ACK_UNMATCHED_CONTROL_ID")
2608
2631
  );
2609
2632
  safeEmitError(
2610
2633
  this,
2611
2634
  Object.freeze({
2612
2635
  connectionId: this._connection?.connectionId ?? conn.connectionId,
2613
2636
  error: err,
2614
- controlId
2637
+ // The peer's MSA-2 byte LENGTH. The bytes themselves used to be
2638
+ // here and on the message above; a peer sending a one-megabyte
2639
+ // MSA-2 produced a one-megabyte Error.message, straight into a log.
2640
+ controlIdBytes
2615
2641
  })
2616
2642
  );
2617
2643
  },
@@ -2619,7 +2645,7 @@ var MllpClient = class extends EventEmitter {
2619
2645
  this._timedOutTotal += 1;
2620
2646
  entry.reject(
2621
2647
  new MllpTimeoutError(`ACK timeout after ${elapsedMs}ms`, {
2622
- messageControlId: entry.controlId ?? void 0,
2648
+ messageControlIdBytes: entry.controlId?.length,
2623
2649
  elapsedMs,
2624
2650
  sentAt: entry.sentAt ?? 0
2625
2651
  })
@@ -3172,7 +3198,7 @@ var MllpClient = class extends EventEmitter {
3172
3198
  cleanup();
3173
3199
  reject(
3174
3200
  new MllpTimeoutError(`waiting for drain timed out after ${ackTimeoutMs}ms`, {
3175
- messageControlId: void 0,
3201
+ messageControlIdBytes: void 0,
3176
3202
  elapsedMs: ackTimeoutMs,
3177
3203
  sentAt: Date.now()
3178
3204
  })
@@ -3410,8 +3436,8 @@ async function createStarterClient(opts) {
3410
3436
  }
3411
3437
 
3412
3438
  // src/index.ts
3413
- var VERSION = "0.0.3";
3439
+ var VERSION = "0.0.5";
3414
3440
 
3415
- export { Connection, FrameReader, MLLP_BIND_ALL_INTERFACES, MLLP_TLS_VERIFY_DISABLED, MllpAckError, MllpBackpressureError, MllpClient, MllpConnectionError, MllpFramingError, MllpServer, MllpTimeoutError, NetTransport, TlsTransport, VERSION, buildRawAck, createClient, createServer, createStarterClient, createStarterServer, createWarning, encodeFrame, isTlsProtocolError, isTlsVerificationErrorCode, isTransientConnectionError, rawAckUncorrelatable, resolveNackCode };
3441
+ export { Connection, FrameReader, MLLP_BIND_ALL_INTERFACES, MLLP_TLS_VERIFY_DISABLED, MllpAckError, MllpBackpressureError, MllpClient, MllpConnectionError, MllpFramingError, MllpServer, MllpTimeoutError, NetTransport, TlsTransport, VERSION, ackDiagnosticMessage, buildRawAck, createClient, createServer, createStarterClient, createStarterServer, createWarning, encodeFrame, isTlsProtocolError, isTlsVerificationErrorCode, isTransientConnectionError, rawAckUncorrelatable, resolveNackCode };
3416
3442
  //# sourceMappingURL=index.mjs.map
3417
3443
  //# sourceMappingURL=index.mjs.map