@cosyte/mllp 0.0.1

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.
@@ -0,0 +1,425 @@
1
+ import { AckCode, AckErrorDetail, Hl7Message, AckMode, BuildAckOptions, Hl7ParseError, FATAL_CODES, ACK_CODES } from '@cosyte/hl7';
2
+ export { AckCode, AckErrorDetail, AckMode } from '@cosyte/hl7';
3
+
4
+ /**
5
+ * `buildMllpAck` — a THIN transport adapter over `@cosyte/hl7`'s `buildAck`.
6
+ *
7
+ * `@cosyte/hl7` owns ACK **content** (MSH/MSA/ERR construction, the fail-safe
8
+ * no-correlation downgrade); this module owns **transport policy**: accepting
9
+ * raw inbound bytes (or an already-parsed message), tolerating an unparseable
10
+ * inbound without ever fabricating a positive disposition, and framing the
11
+ * result into a ready-to-write MLLP `Buffer` via `encodeFrame`.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';
16
+ * const { frame } = buildAckAA(inboundBuffer);
17
+ * socket.write(frame);
18
+ * ```
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+
23
+ /**
24
+ * A single ACK-generation warning, surfaced by {@link buildMllpAck}.
25
+ *
26
+ * `code` is either an upstream `@cosyte/hl7` warning code passed through
27
+ * verbatim (e.g. `"ACK_NO_CORRELATION_ID"`) or the one `mllp`-owned code,
28
+ * {@link MLLP_ACK_INBOUND_UNPARSEABLE}.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';
33
+ * const { warnings } = buildAckAA(inbound);
34
+ * for (const w of warnings) console.warn(w.code, w.message);
35
+ * ```
36
+ */
37
+ interface MllpAckWarning {
38
+ readonly code: string;
39
+ readonly message: string;
40
+ }
41
+ /**
42
+ * Warning code emitted when the inbound message could not be parsed at all
43
+ * (a fatal `Hl7ParseError`) and {@link buildMllpAck} fell back to a minimal,
44
+ * uncorrelated ACK rather than fabricating a positive disposition.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * import { MLLP_ACK_INBOUND_UNPARSEABLE, buildAckAA } from '@cosyte/mllp/ack-from-hl7';
49
+ * const { warnings } = buildAckAA("not hl7 at all");
50
+ * warnings.some((w) => w.code === MLLP_ACK_INBOUND_UNPARSEABLE); // true
51
+ * ```
52
+ */
53
+ declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
54
+ /**
55
+ * Warning code emitted when the built ACK's MSA-2 bytes are **not** byte-identical
56
+ * to the inbound MSH-10 bytes — i.e. the ACK does not echo the message control ID
57
+ * verbatim, as HL7 v2.5.1 §2.9.2.2 requires.
58
+ *
59
+ * This is a **correlation failure**, and the reason it is a loud warning rather
60
+ * than a silent success: the sender keys its in-flight store on the MSH-10 bytes
61
+ * it put on the wire. An ACK whose MSA-2 differs by even one byte will not match
62
+ * that key, so the send is never settled → ACK timeout → resend → **duplicate
63
+ * clinical message**. Emitting the ACK anyway (rather than throwing) is the
64
+ * fail-safe choice — a mismatched ACK still tells the peer *something* — but the
65
+ * mismatch must never pass unremarked.
66
+ *
67
+ * `buildMllpAck` verifies this on every build, against the very same byte-level
68
+ * scanners the `@cosyte/mllp` client uses to correlate (`src/internal/control-id.ts`).
69
+ *
70
+ * **The verification is meaningful for a `Buffer` inbound, and only for a `Buffer`.**
71
+ * A `Buffer` *is* the wire bytes, so comparing the ACK to it is a real byte-level check
72
+ * and a clean result means a `@cosyte/mllp` sender will match this ACK. A
73
+ * `string`/`Hl7Message` inbound has **already been decoded** before this module sees it:
74
+ * the only thing left to compare against is that same text, encoded with the same codec,
75
+ * so the codec cancels on both sides and a codec-induced mismatch is **structurally
76
+ * invisible** — it warns about nothing. See {@link BuildMllpAckOptions.encoding}. Pass a
77
+ * `Buffer` if you want this guarantee to mean anything.
78
+ *
79
+ * Two known ways to provoke it, both documented in the package limitations:
80
+ *
81
+ * 1. Overriding {@link BuildMllpAckOptions.encoding} to a codec that cannot
82
+ * round-trip the inbound bytes (`"utf8"`/`"ascii"` against a high-bit,
83
+ * 8859/1-charset control ID). The default never does this.
84
+ * 2. An inbound that declares **non-default encoding characters** (MSH-1/MSH-2).
85
+ * `@cosyte/hl7`'s `buildMessage` always emits the HL7 default `|^~\&`, so an
86
+ * inbound MSH-10 of `ID#X` under a `#` component separator is re-delimited to
87
+ * `ID^X` in MSA-2 — different bytes, unmatchable key. Use `buildRawAck` (root
88
+ * export), which echoes the inbound's own MSH-1/MSH-2, if you must serve such
89
+ * a peer.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * import { MLLP_ACK_CONTROL_ID_NOT_VERBATIM, buildAckAA } from '@cosyte/mllp/ack-from-hl7';
94
+ * const { warnings } = buildAckAA(inbound, { encoding: "ascii" });
95
+ * if (warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_NOT_VERBATIM)) {
96
+ * // this ACK will NOT correlate at the sender — investigate before shipping it
97
+ * }
98
+ * ```
99
+ */
100
+ declare const MLLP_ACK_CONTROL_ID_NOT_VERBATIM = "MLLP_ACK_CONTROL_ID_NOT_VERBATIM";
101
+ /**
102
+ * Options for {@link buildMllpAck} and its six convenience wrappers.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * import type { BuildMllpAckOptions } from '@cosyte/mllp/ack-from-hl7';
107
+ * const opts: BuildMllpAckOptions = { code: "AE", error: { conditionCode: "101" } };
108
+ * ```
109
+ */
110
+ interface BuildMllpAckOptions {
111
+ /** Required acknowledgment disposition to emit in MSA-1 (HL7 Table 0008). */
112
+ readonly code: AckCode;
113
+ /** Optional error detail; one entry emits one ERR segment. */
114
+ readonly error?: AckErrorDetail | readonly AckErrorDetail[];
115
+ /**
116
+ * The codec used to move the ACK between bytes and text — **both** directions,
117
+ * as a single symmetric choice. It decodes a raw-`Buffer` `inbound` on the way in
118
+ * and encodes the serialized ACK on the way out, so the ACK is emitted in the same
119
+ * codec the inbound was read in.
120
+ *
121
+ * That symmetry is the whole point, and it is what makes MSA-2 echo MSH-10
122
+ * **verbatim** (HL7 v2.5.1 §2.9.2.2). Decoding in one codec and encoding in another
123
+ * corrupts every non-ASCII byte it round-trips: a `latin1`-decoded `0x8B` re-encoded
124
+ * as `utf8` goes out as the two bytes `0xC2 0x8B`, which is a *different control ID*
125
+ * — and the sender, which keyed its in-flight store on `0x8B`, cannot match the ACK.
126
+ *
127
+ * **Defaults, by inbound type:**
128
+ *
129
+ * - `Buffer` → **`"latin1"`**. We own the decode, and `latin1` is the only codec
130
+ * that round-trips arbitrary bytes losslessly, so MSA-2 is byte-verbatim for any
131
+ * charset — including an MSH-18 of `8859/1` with high-bit bytes in the control ID.
132
+ * - `string` / `Hl7Message` → **`"utf8"`**. The caller already decoded; we re-encode
133
+ * with the JS-native codec.
134
+ *
135
+ * **Override with care, and only on a `Buffer`.** Any value other than `"latin1"` on a
136
+ * raw-`Buffer` inbound gives up the verbatim guarantee for non-ASCII bytes (`"ascii"`
137
+ * masks the high bit; `"utf8"` folds invalid sequences onto `U+FFFD`). On a `Buffer`,
138
+ * `buildMllpAck` **checks** the result and emits {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}
139
+ * if the echo did not survive, so a bad override there is loud rather than silent — but
140
+ * it is still a broken ACK. Set this only when the receiving system genuinely demands a
141
+ * specific codec.
142
+ *
143
+ * **On a `string` / `Hl7Message` inbound the check cannot see a codec problem at all.**
144
+ * The wire bytes were decoded to text *before* this function saw them, so there is
145
+ * nothing left to compare the ACK against except that same text — the codec cancels on
146
+ * both sides and the comparison is a tautology. Concretely,
147
+ * `buildAckAA(payload.toString("latin1"))` on a high-bit control ID (`0x8B`, legal under
148
+ * an `MSH-18` of `8859/1`) re-encodes it as the two `utf8` bytes `0xC2 0x8B`, emits a
149
+ * **different** control ID, and warns about **nothing** — the sender cannot correlate it.
150
+ * This is not a hole the guard can be grown to cover; the bytes are gone by then.
151
+ *
152
+ * **`Buffer` is the byte-safe path.** Pass the raw payload. It is what the server hands
153
+ * you, it is what the `Buffer`-first API rule exists for, and it is the only input for
154
+ * which the verbatim guarantee — or a warning that it broke — actually means anything.
155
+ */
156
+ readonly encoding?: BufferEncoding;
157
+ /**
158
+ * Passthrough to `encodeFrame`'s `allowDelimiterBytesInPayload`. Defaults
159
+ * to `false` (strict — throws if the serialized ACK somehow contains a
160
+ * VT/FS byte, which a spec-clean `@cosyte/hl7` emit never does).
161
+ */
162
+ readonly allowDelimiterBytesInPayload?: boolean;
163
+ }
164
+ /**
165
+ * Result of {@link buildMllpAck} — a ready-to-write MLLP frame plus the
166
+ * metadata needed to log or assert on what was actually emitted. Frozen
167
+ * (including the `warnings` array) so subscribers cannot mutate shared state.
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';
172
+ * const ack = buildAckAA(inbound);
173
+ * socket.write(ack.frame);
174
+ * console.log(ack.code, ack.correlationId);
175
+ * ```
176
+ */
177
+ interface MllpAck {
178
+ /** `VT + payload + FS + CR`, ready to write to a socket. */
179
+ readonly frame: Buffer;
180
+ /** The serialized ACK, unframed. */
181
+ readonly payload: Buffer;
182
+ /** The built ACK `Hl7Message`. */
183
+ readonly ack: Hl7Message;
184
+ /** The disposition the caller asked for. */
185
+ readonly requestedCode: AckCode;
186
+ /** The disposition MSA-1 actually carries (post fail-safe downgrade). */
187
+ readonly code: AckCode;
188
+ /** Inbound MSH-10, echoed in MSA-2. `undefined` when unfindable. */
189
+ readonly correlationId: string | undefined;
190
+ /** Detected from inbound MSH-15/16. `undefined` when inbound unparseable. */
191
+ readonly mode: AckMode | undefined;
192
+ /** Warnings collected while building the ACK. */
193
+ readonly warnings: readonly MllpAckWarning[];
194
+ }
195
+ /**
196
+ * Build a framed MLLP ACK from an inbound HL7 v2 message — the core
197
+ * `ack-from-hl7` adapter. `inbound` may be an already-parsed `Hl7Message`, or
198
+ * raw bytes/text to parse first.
199
+ *
200
+ * Behavior:
201
+ * - **Fatally unparseable inbound** (`Hl7ParseError`) — never emits a
202
+ * positive code: `AA`→`AE`, `CA`→`CE`; other codes pass through. MSA-2 is
203
+ * empty, `correlationId` and `mode` are `undefined`, and `warnings`
204
+ * contains one {@link MLLP_ACK_INBOUND_UNPARSEABLE} entry naming the fatal
205
+ * code — never any inbound payload bytes.
206
+ * - **Normal path** — detects the ack mode from inbound MSH-15/16, delegates
207
+ * to `@cosyte/hl7`'s `buildAck` (which applies its own no-correlation
208
+ * fail-safe), and forwards `buildAck`'s own warnings (mapped to
209
+ * `{ code, message }`). Inbound parse warnings are NOT forwarded — they are
210
+ * the caller's concern if they parsed the inbound themselves.
211
+ *
212
+ * @throws {MllpPeerMissingError} when `@cosyte/hl7` is not installed.
213
+ * @throws {TypeError} when `options.code` is not a known HL7 Table 0008 code.
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * import { buildMllpAck } from '@cosyte/mllp/ack-from-hl7';
218
+ * const ack = buildMllpAck(inboundBuffer, { code: "AA" });
219
+ * socket.write(ack.frame);
220
+ * ```
221
+ */
222
+ declare function buildMllpAck(inbound: Hl7Message | Buffer | string, options: BuildMllpAckOptions): MllpAck;
223
+ /**
224
+ * Build an `AA` (Application Accept) MLLP ACK.
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';
229
+ * socket.write(buildAckAA(inboundBuffer).frame);
230
+ * ```
231
+ */
232
+ declare function buildAckAA(inbound: Hl7Message | Buffer | string, options?: Omit<BuildMllpAckOptions, "code">): MllpAck;
233
+ /**
234
+ * Build an `AE` (Application Error) MLLP ACK.
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * import { buildAckAE } from '@cosyte/mllp/ack-from-hl7';
239
+ * socket.write(buildAckAE(inboundBuffer, { error: { conditionCode: "101" } }).frame);
240
+ * ```
241
+ */
242
+ declare function buildAckAE(inbound: Hl7Message | Buffer | string, options?: Omit<BuildMllpAckOptions, "code">): MllpAck;
243
+ /**
244
+ * Build an `AR` (Application Reject) MLLP ACK.
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * import { buildAckAR } from '@cosyte/mllp/ack-from-hl7';
249
+ * socket.write(buildAckAR(inboundBuffer, { error: { conditionCode: "200" } }).frame);
250
+ * ```
251
+ */
252
+ declare function buildAckAR(inbound: Hl7Message | Buffer | string, options?: Omit<BuildMllpAckOptions, "code">): MllpAck;
253
+ /**
254
+ * Build a `CA` (Commit Accept) MLLP ACK (enhanced mode).
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * import { buildAckCA } from '@cosyte/mllp/ack-from-hl7';
259
+ * socket.write(buildAckCA(inboundBuffer).frame);
260
+ * ```
261
+ */
262
+ declare function buildAckCA(inbound: Hl7Message | Buffer | string, options?: Omit<BuildMllpAckOptions, "code">): MllpAck;
263
+ /**
264
+ * Build a `CE` (Commit Error) MLLP ACK (enhanced mode).
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * import { buildAckCE } from '@cosyte/mllp/ack-from-hl7';
269
+ * socket.write(buildAckCE(inboundBuffer, { error: { conditionCode: "101" } }).frame);
270
+ * ```
271
+ */
272
+ declare function buildAckCE(inbound: Hl7Message | Buffer | string, options?: Omit<BuildMllpAckOptions, "code">): MllpAck;
273
+ /**
274
+ * Build a `CR` (Commit Reject) MLLP ACK (enhanced mode).
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * import { buildAckCR } from '@cosyte/mllp/ack-from-hl7';
279
+ * socket.write(buildAckCR(inboundBuffer, { error: { conditionCode: "200" } }).frame);
280
+ * ```
281
+ */
282
+ declare function buildAckCR(inbound: Hl7Message | Buffer | string, options?: Omit<BuildMllpAckOptions, "code">): MllpAck;
283
+ /**
284
+ * Detect the HL7 acknowledgment mode (`"original"` vs `"enhanced"`) of an
285
+ * inbound message, from MSH-15/16. Named `detectMode` (not `detectAckMode`)
286
+ * to avoid colliding with the peer's own `detectAckMode` export for callers
287
+ * who import both from `@cosyte/hl7` and `@cosyte/mllp/ack-from-hl7`.
288
+ *
289
+ * `inbound` may be an already-parsed `Hl7Message`, or raw bytes/text to parse
290
+ * first. A fatal parse failure rethrows the `Hl7ParseError` as-is — unlike
291
+ * {@link buildMllpAck}, there is no ACK to build here, so there is no
292
+ * fallback to construct.
293
+ *
294
+ * **Module-realm note:** this adapter loads `@cosyte/hl7` lazily via
295
+ * `createRequire` (see `peer.ts`). If your code ALSO does
296
+ * `import { Hl7ParseError } from "@cosyte/hl7"` directly, Node's module
297
+ * resolution can — depending on your bundler/runtime — hand you a distinct
298
+ * module instance than the one this adapter loaded, in which case
299
+ * `caught instanceof Hl7ParseError` (your import) may be `false` even though
300
+ * `caught.name === "Hl7ParseError"` and `caught.code` is a valid HL7 fatal
301
+ * code (`NO_MSH_SEGMENT` / `MSH_TOO_SHORT` / `INVALID_ENCODING_CHARACTERS` /
302
+ * `EMPTY_INPUT`). Prefer narrowing on `.code`/`.name`, or obtain the exact
303
+ * class via this subpath's own `loadHl7Peer().Hl7ParseError`.
304
+ *
305
+ * @throws {MllpPeerMissingError} when `@cosyte/hl7` is not installed.
306
+ * @throws {Hl7ParseError} when `inbound` is raw bytes/text that fails to parse.
307
+ *
308
+ * @example
309
+ * ```typescript
310
+ * import { detectMode } from '@cosyte/mllp/ack-from-hl7';
311
+ * detectMode(inboundBuffer); // "original" | "enhanced"
312
+ * ```
313
+ */
314
+ declare function detectMode(inbound: Hl7Message | Buffer | string): AckMode;
315
+
316
+ /**
317
+ * Lazy loader for the optional `@cosyte/hl7` peer dependency.
318
+ *
319
+ * `@cosyte/mllp` ships **zero runtime dependencies**; `@cosyte/hl7` is an
320
+ * optional peer dep referenced only from this `/ack-from-hl7` subpath. To keep
321
+ * the rest of the package fully dependency-free, the peer is required lazily —
322
+ * on first call into `ack-from-hl7` — via `createRequire`, not at module load
323
+ * time. The public API of this subpath stays synchronous throughout.
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * import { loadHl7Peer } from '@cosyte/mllp/ack-from-hl7';
328
+ * const hl7 = loadHl7Peer(); // throws MllpPeerMissingError if not installed
329
+ * ```
330
+ *
331
+ * @packageDocumentation
332
+ */
333
+
334
+ /**
335
+ * The subset of the `@cosyte/hl7` runtime surface the `ack-from-hl7` adapter
336
+ * consumes. Kept narrow and typed via `import type` (erased at compile time)
337
+ * so the runtime dependency stays fully lazy — only the shape is known
338
+ * statically, the values are resolved via `require()` at first call.
339
+ *
340
+ * @example
341
+ * ```typescript
342
+ * import type { Hl7Peer } from '@cosyte/mllp/ack-from-hl7';
343
+ * function useAck(peer: Hl7Peer) {
344
+ * return peer.buildAck;
345
+ * }
346
+ * ```
347
+ */
348
+ interface Hl7Peer {
349
+ readonly parseHL7: (raw: string | Buffer) => Hl7Message;
350
+ readonly buildAck: (inbound: Hl7Message, options: BuildAckOptions) => Hl7Message;
351
+ readonly detectAckMode: (inbound: Hl7Message) => "original" | "enhanced";
352
+ readonly buildMessage: (init: {
353
+ readonly type: string;
354
+ }) => Hl7Message;
355
+ /**
356
+ * The peer's own `Hl7Message` class. Exposed (not just used as a type) so
357
+ * callers can detect a cross-realm instance — see the dual-package-hazard
358
+ * note on `resolveInbound` in `build.ts`. Typed as `abstract new` (rather
359
+ * than a concrete constructor signature) purely so TypeScript accepts it as
360
+ * an `instanceof` right-hand side without also implying callers can `new`
361
+ * it directly with arbitrary args.
362
+ */
363
+ readonly Hl7Message: abstract new (...args: never[]) => Hl7Message;
364
+ readonly Hl7ParseError: typeof Hl7ParseError;
365
+ readonly FATAL_CODES: typeof FATAL_CODES;
366
+ readonly ACK_CODES: typeof ACK_CODES;
367
+ /**
368
+ * The peer's fail-safe downgrade primitive (`AA`→`AE`, `CA`→`CE`, everything
369
+ * else unchanged) — the single upstream source of truth for the pair; this
370
+ * adapter never carries its own copy of the mapping.
371
+ */
372
+ readonly downgradePositiveAck: (code: AckCode) => AckCode;
373
+ }
374
+ /**
375
+ * Thrown when `@cosyte/hl7` — the optional peer dependency required by
376
+ * `@cosyte/mllp/ack-from-hl7` — cannot be resolved at runtime.
377
+ *
378
+ * `@cosyte/hl7` is declared as an optional peer dependency, so a consumer of
379
+ * the root `@cosyte/mllp` entry point is never forced to install it. Only
380
+ * code that imports from the `/ack-from-hl7` subpath needs it, and only once
381
+ * that code actually runs (the loader is lazy).
382
+ *
383
+ * @example
384
+ * ```typescript
385
+ * import { buildAckAA, MllpPeerMissingError } from '@cosyte/mllp/ack-from-hl7';
386
+ * try {
387
+ * buildAckAA(inboundBuffer);
388
+ * } catch (err) {
389
+ * if (err instanceof MllpPeerMissingError) {
390
+ * console.error(err.message); // explains how to install @cosyte/hl7
391
+ * }
392
+ * }
393
+ * ```
394
+ */
395
+ declare class MllpPeerMissingError extends Error {
396
+ readonly name: "MllpPeerMissingError";
397
+ /** Stable machine-readable code identifying this error class. */
398
+ readonly code: "MLLP_PEER_MISSING";
399
+ /**
400
+ * Construct an `MllpPeerMissingError`.
401
+ *
402
+ * @param cause - The original module-resolution error, preserved for debugging.
403
+ */
404
+ constructor(cause: unknown);
405
+ }
406
+ /**
407
+ * Load the `@cosyte/hl7` peer module, caching the result for subsequent
408
+ * calls. The public adapter functions in `build.ts` call this internally —
409
+ * most consumers never call it directly.
410
+ *
411
+ * @param requireFn - Injectable `require`-like function, used by tests to
412
+ * simulate a missing peer without actually uninstalling it. Defaults to a
413
+ * real `createRequire(import.meta.url)`.
414
+ * @internal
415
+ *
416
+ * @example
417
+ * ```typescript
418
+ * import { loadHl7Peer } from '@cosyte/mllp/ack-from-hl7';
419
+ * const hl7 = loadHl7Peer();
420
+ * const ack = hl7.buildAck(hl7.parseHL7(raw), { code: "AA" });
421
+ * ```
422
+ */
423
+ declare function loadHl7Peer(requireFn?: (id: string) => unknown): Hl7Peer;
424
+
425
+ export { type BuildMllpAckOptions, type Hl7Peer, MLLP_ACK_CONTROL_ID_NOT_VERBATIM, MLLP_ACK_INBOUND_UNPARSEABLE, type MllpAck, type MllpAckWarning, MllpPeerMissingError, buildAckAA, buildAckAE, buildAckAR, buildAckCA, buildAckCE, buildAckCR, buildMllpAck, detectMode, loadHl7Peer };