@glion/mllp-client 0.16.0
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/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/client.js +549 -0
- package/dist/client.js.map +1 -0
- package/dist/core/acknowledgment.d.ts +103 -0
- package/dist/core/acknowledgment.d.ts.map +1 -0
- package/dist/core/client.d.ts +256 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/connect.d.ts +101 -0
- package/dist/core/connect.d.ts.map +1 -0
- package/dist/core/errors.d.ts +81 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/index.d.ts +23 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +2 -0
- package/dist/core/internal/ack-parser-stream.d.ts +22 -0
- package/dist/core/internal/ack-parser-stream.d.ts.map +1 -0
- package/dist/core/internal/subscribe-abort.d.ts +30 -0
- package/dist/core/internal/subscribe-abort.d.ts.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/node.js +214 -0
- package/dist/node.js.map +1 -0
- package/dist/runtimes/internal/node-error-mapping.d.ts +68 -0
- package/dist/runtimes/internal/node-error-mapping.d.ts.map +1 -0
- package/dist/runtimes/node.d.ts +47 -0
- package/dist/runtimes/node.d.ts.map +1 -0
- package/dist/runtimes/node.js +3 -0
- package/dist/runtimes/workers.d.ts +64 -0
- package/dist/runtimes/workers.d.ts.map +1 -0
- package/dist/runtimes/workers.js +143 -0
- package/dist/runtimes/workers.js.map +1 -0
- package/package.json +95 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Rethink Health
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# @glion/mllp-client
|
|
2
|
+
|
|
3
|
+
MLLP client for sending HL7v2 messages and receiving acknowledgments.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`@glion/mllp-client` exports an `MllpClient` class that sends HL7v2 messages over MLLP/TCP and returns the parsed acknowledgment. Each `send()` opens a one-shot TCP (or TLS) connection, MLLP-frames the message, awaits a single ACK frame, parses it, and tears the connection down — modeled after a single HTTP request/response. NAK responses (MSA-1 ∈ {AE, AR, CE, CR}) are thrown as the matching `AckException` subclass from `@glion/ack`, so callers catch the same exception types that `@glion/mllp-ack` produces on the receiver side. Each thrown exception carries the original raw ACK on its `raw` attribute, so callers can re-parse, log, or persist the wire payload without intercepting the response separately. Transport-level failures (connection refused, timeout, malformed frame) are thrown as `MllpClientError`, a subclass of `MllpError` from `@glion/mllp-transport`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @glion/mllp-client @glion/ack
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`@glion/ack` is required because `client.send()` throws its `AckException` subclasses on NAK responses; install it alongside the client so application code can `import { AckApplicationError } from "@glion/ack"` directly.
|
|
16
|
+
|
|
17
|
+
## Runtime support
|
|
18
|
+
|
|
19
|
+
`@glion/mllp-client` runs on every JavaScript runtime that can open a raw TCP socket. Pick the import path that matches your runtime — the client API is identical in every case.
|
|
20
|
+
|
|
21
|
+
| Runtime | Import path | Connector | Status |
|
|
22
|
+
| ---------------------- | ---------------------------- | ---------------------------------- | ----------------------- |
|
|
23
|
+
| **Node.js** | `@glion/mllp-client/node` | `node:net` / `node:tls` | shipped |
|
|
24
|
+
| **Bun** | `@glion/mllp-client/node` | `node:net` / `node:tls` (compat) | core verified[^2] |
|
|
25
|
+
| **Deno** | — | `Deno.connect` / `Deno.connectTls` | in progress (PR [#615]) |
|
|
26
|
+
| **Cloudflare Workers** | `@glion/mllp-client/workers` | `cloudflare:sockets` | shipped |
|
|
27
|
+
|
|
28
|
+
[^2]: The runtime-free core (`@glion/mllp-client/core`) is exercised under Bun in CI via `pnpm test:bun`. The Node adapter is not currently part of the Bun test matrix; Bun's `node:net`/`node:tls` compatibility means it works in practice, but regressions are caught by Node CI rather than Bun CI.
|
|
29
|
+
|
|
30
|
+
Always import from the runtime-specific subpath (`@glion/mllp-client/workers`, `@glion/mllp-client/node`). Some bundlers honour the `workerd` key in this package's `exports` map and will resolve the bare specifier `@glion/mllp-client` to the Workers entry on a Workers target — but bundler support for `exports` conditions varies, so the subpath is the only path the package guarantees.
|
|
31
|
+
|
|
32
|
+
> **Heads-up.** The Deno adapter is being reviewed in a separate pull request and is not yet part of a release. The runtime-agnostic core is stable; you can wire your own `MllpConnect` against any transport (or a custom test harness) by importing from `@glion/mllp-client/core` until that adapter lands.
|
|
33
|
+
|
|
34
|
+
[#615]: https://github.com/rethinkhealth/glion/pull/615
|
|
35
|
+
|
|
36
|
+
**Browsers cannot run this client directly** — they have no API for raw TCP sockets.
|
|
37
|
+
|
|
38
|
+
## Use
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { MllpClient } from "@glion/mllp-client";
|
|
42
|
+
|
|
43
|
+
// TLS-on by default — see "Plain TCP" below for opt-out
|
|
44
|
+
const client = new MllpClient({ host: "mllp.example.com", port: 6661 });
|
|
45
|
+
|
|
46
|
+
const ack = await client.send(
|
|
47
|
+
[
|
|
48
|
+
"MSH|^~\\&|SendApp|SendFac|RecvApp|RecvFac|20240101120000||ADT^A01^ADT_A01|MSG001|P|2.5.1",
|
|
49
|
+
"EVN|A01|20240101120000",
|
|
50
|
+
"PID|1||12345^^^MRN||Doe^John",
|
|
51
|
+
].join("\r")
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
console.log(ack.code); // "AA"
|
|
55
|
+
console.log(ack.controlId); // "MSG001"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Catch a NAK from the receiver as a typed exception. The original raw ACK is available on `error.raw`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { AckApplicationError, AckApplicationReject } from "@glion/ack";
|
|
62
|
+
import { MllpClient, MllpClientError } from "@glion/mllp-client";
|
|
63
|
+
|
|
64
|
+
const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await client.send(rawMessage);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof AckApplicationError) {
|
|
70
|
+
// MSA-1 = AE — receiver returned an application-level error.
|
|
71
|
+
// error.message holds MSA-3; error.errorCode holds ERR-3; error.severity holds ERR-4.
|
|
72
|
+
// error.raw holds the wire-format ACK message.
|
|
73
|
+
} else if (error instanceof AckApplicationReject) {
|
|
74
|
+
// MSA-1 = AR — receiver rejected the message.
|
|
75
|
+
} else if (error instanceof MllpClientError) {
|
|
76
|
+
// Transport-level failure: error.code is one of CONNECTION_REFUSED,
|
|
77
|
+
// TIMEOUT, CONNECTION_CLOSED, MALFORMED_FRAME, or MALFORMED_ACK.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Send over plain TCP — opt out of the secure default. Use only when the wire is already protected (trusted hospital intranet, VPN tunnel, or a TLS terminator hop in front of the receiver):
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const client = new MllpClient({
|
|
86
|
+
host: "127.0.0.1",
|
|
87
|
+
port: 2575,
|
|
88
|
+
tls: false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const ack = await client.send(rawMessage);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Send over TLS with explicit configuration (mutual TLS, custom CA, etc.):
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { readFileSync } from "node:fs";
|
|
98
|
+
|
|
99
|
+
const client = new MllpClient({
|
|
100
|
+
host: "mllp.example.com",
|
|
101
|
+
port: 6661,
|
|
102
|
+
tls: {
|
|
103
|
+
ca: readFileSync("ca.pem"),
|
|
104
|
+
cert: readFileSync("client-cert.pem"),
|
|
105
|
+
key: readFileSync("client-key.pem"),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const ack = await client.send(rawMessage);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The same code on **Cloudflare Workers**:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { MllpClient } from "@glion/mllp-client/workers";
|
|
116
|
+
|
|
117
|
+
const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
|
|
118
|
+
const ack = await client.send(rawMessage);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The Workers runtime cannot honour programmatic TLS material. Passing any of `tls.ca`, `tls.cert`, `tls.key`, `tls.passphrase`, `tls.servername`, or `tls.insecure: true` to the Workers adapter throws `MllpClientError` with `code: "INVALID_INPUT"` **before any socket is opened**. Configure TLS material via the Cloudflare platform — Hyperdrive, Worker bindings, or terminate TLS upstream of the receiver. The error path:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { MllpClient, MllpClientError } from "@glion/mllp-client/workers";
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const client = new MllpClient({
|
|
128
|
+
host: "mllp.example.com",
|
|
129
|
+
port: 6661,
|
|
130
|
+
tls: { ca: caPem }, // rejected on Workers
|
|
131
|
+
});
|
|
132
|
+
await client.send(rawMessage);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof MllpClientError && error.code === "INVALID_INPUT") {
|
|
135
|
+
// error.message names the rejected fields and points at the docs.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Options
|
|
141
|
+
|
|
142
|
+
`MllpClientOptions`:
|
|
143
|
+
|
|
144
|
+
| Option | Type | Default | Description |
|
|
145
|
+
| ------------ | --------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| `host` | `string` | — | Hostname or IP of the MLLP server. |
|
|
147
|
+
| `port` | `number` | — | TCP port of the MLLP server. |
|
|
148
|
+
| `timeout` | `number` | `30000` | Maximum total milliseconds for connect → send → ACK. |
|
|
149
|
+
| `maxAckSize` | `number` | — | Maximum bytes accepted for an inbound ACK frame. No limit when omitted. |
|
|
150
|
+
| `tls` | `boolean \| MllpClientTlsOptions` | `true` | TLS configuration. Defaults to `true` (TLS-on, system trust store, hostname verification). Pass an options object for custom config, or `false` to opt out into plain TCP. |
|
|
151
|
+
|
|
152
|
+
`MllpClientTlsOptions`:
|
|
153
|
+
|
|
154
|
+
| Option | Type | Description |
|
|
155
|
+
| ------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
156
|
+
| `ca` | `string \| Buffer` | Trusted CA certificate(s) for verifying the server. |
|
|
157
|
+
| `cert` | `string \| Buffer` | Client certificate for mutual TLS. |
|
|
158
|
+
| `key` | `string \| Buffer` | Client private key for mutual TLS. |
|
|
159
|
+
| `passphrase` | `string` | Passphrase for the private key. |
|
|
160
|
+
| `servername` | `string` | Server name for SNI and certificate verification. Defaults to `host`. Setting this to anything other than the real hostname disables strict hostname verification. |
|
|
161
|
+
| `insecure` | `true` | **Test/dev only.** Disable server-certificate verification. Only the literal value `true` opts out — there is no `false` form because the secure default is non-negotiable. |
|
|
162
|
+
|
|
163
|
+
## API
|
|
164
|
+
|
|
165
|
+
### `new MllpClient(options)`
|
|
166
|
+
|
|
167
|
+
Construct a client bound to a host/port. The constructor does not open a connection — every `send()` call opens its own.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { MllpClient } from "@glion/mllp-client";
|
|
171
|
+
|
|
172
|
+
const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
|
|
173
|
+
client.host; // "127.0.0.1"
|
|
174
|
+
client.port; // 2575
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `client.send(message, options?)`
|
|
178
|
+
|
|
179
|
+
Send a single HL7v2 message and resolve with the resolving accept ACK. Returns a `Promise<Acknowledgment>`. Accepts a `string` or `Uint8Array` payload. Opens a TCP/TLS connection, writes the MLLP-framed bytes, reads acknowledgment frames according to `options.mode`, and closes the connection when the resolving frame arrives.
|
|
180
|
+
|
|
181
|
+
On NAK (MSA-1 ∈ {AE, AR, CE, CR}) throws the matching `AckException` subclass with the original wire-format ACK on `error.raw`.
|
|
182
|
+
|
|
183
|
+
To observe each accept ACK as it arrives (e.g. seeing the intermediate `CA` frame in HL7v2 enhanced mode), use [`client.stream()`](#clientstreammessage-options) instead.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const ack = await client.send(rawHl7Message);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`options` is optional:
|
|
190
|
+
|
|
191
|
+
| Field | Type | Default | Description |
|
|
192
|
+
| -------- | ------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
193
|
+
| `signal` | `AbortSignal` | — | Cancel the in-flight send. Composed with the client's `timeout` via `AbortSignal.any` so either source aborts. |
|
|
194
|
+
| `mode` | `"OnApplication" \| "OnCommit"` | `"OnApplication"` | Which HL7v2 acknowledgment level resolves the send. See [Acknowledgment modes](#acknowledgment-modes). |
|
|
195
|
+
|
|
196
|
+
### `client.stream(message, options?)`
|
|
197
|
+
|
|
198
|
+
Send a single HL7v2 message and yield each accept ACK as it arrives. Returns an `AsyncIterable<Acknowledgment>`. Accepts the same `message` and `options` as `client.send()`.
|
|
199
|
+
|
|
200
|
+
The iterable completes after the resolving accept frame: in HL7v2 enhanced mode it yields `CA` then the application-level ACK; in basic mode it yields a single application-level ACK. NAK codes (`AE`/`AR`/`CE`/`CR`) throw the matching `AckException` from the iterator, exactly as they would from `await client.send(...)`. Breaking out of the loop closes the underlying socket.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
for await (const ack of client.stream(rawHl7Message)) {
|
|
204
|
+
log.info({ code: ack.code }, "ack received");
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `Acknowledgment`
|
|
209
|
+
|
|
210
|
+
The HL7v2 acknowledgment returned by `client.send()`.
|
|
211
|
+
|
|
212
|
+
| Field | Type | Description |
|
|
213
|
+
| ------------- | --------------------- | --------------------------------------------------------------------- |
|
|
214
|
+
| `raw` | `string` | The raw HL7v2 ACK message as received from the wire. |
|
|
215
|
+
| `tree` | `Root` | The ACK message AST. |
|
|
216
|
+
| `code` | `string` | MSA-1 acknowledgment code (`AA`, `AE`, `AR`, `CA`, `CE`, or `CR`). |
|
|
217
|
+
| `controlId` | `string` | MSA-2 message control ID — references the original message's MSH-10. |
|
|
218
|
+
| `textMessage` | `string \| undefined` | MSA-3 text message. Present on NAK; optional on ACK. |
|
|
219
|
+
| `errorCode` | `string \| undefined` | ERR-3 HL7 error condition code (Table 0357). Present when ERR is set. |
|
|
220
|
+
| `severity` | `string \| undefined` | ERR-4 severity (Table 0516). Present when ERR is set. |
|
|
221
|
+
|
|
222
|
+
### `MllpClientError`
|
|
223
|
+
|
|
224
|
+
A subclass of `MllpError` from `@glion/mllp-transport`, thrown for transport-level failures. The `code` property identifies the failure mode:
|
|
225
|
+
|
|
226
|
+
| `code` | Meaning |
|
|
227
|
+
| ---------------------- | --------------------------------------------------------------------------------------------- |
|
|
228
|
+
| `INVALID_INPUT` | A constructor option or send payload was rejected before any socket was opened. |
|
|
229
|
+
| `CONNECTION_REFUSED` | The TCP connection could not be established (refused, DNS, routing). |
|
|
230
|
+
| `TLS_HANDSHAKE_FAILED` | TLS handshake failed (certificate, protocol, or trust-store mismatch). Node adapter only.[^1] |
|
|
231
|
+
| `CONNECTION_CLOSED` | The peer closed the connection before a complete ACK arrived. |
|
|
232
|
+
| `TIMEOUT` | No ACK arrived within `timeout`. |
|
|
233
|
+
| `MALFORMED_FRAME` | Bytes received did not form a valid MLLP frame. |
|
|
234
|
+
| `MALFORMED_ACK` | The ACK frame was received but could not be parsed as HL7v2. |
|
|
235
|
+
|
|
236
|
+
[^1]: The Workers adapter cannot distinguish a TLS handshake failure from a TCP connect failure; both surface as `CONNECTION_REFUSED`. Cross-runtime callers switching on `error.code` should treat `CONNECTION_REFUSED` as the supertype.
|
|
237
|
+
|
|
238
|
+
### Application-level NAK exceptions
|
|
239
|
+
|
|
240
|
+
`MllpClient.send()` throws the same `AckException` subclasses that `@glion/mllp-ack` produces on the receiver side: `AckApplicationError` (AE), `AckApplicationReject` (AR), `AckCommitError` (CE), `AckCommitReject` (CR), and the abstract base `AckException`. They are not re-exported from this package — install `@glion/ack` and import them directly:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { AckApplicationError, AckException } from "@glion/ack";
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Keeping a single import path keeps `@glion/ack` as the authoritative source for the exception hierarchy across both client and server code paths.
|
|
247
|
+
|
|
248
|
+
## Behavior
|
|
249
|
+
|
|
250
|
+
### NAK to exception mapping
|
|
251
|
+
|
|
252
|
+
| MSA-1 | Thrown class | Source fields |
|
|
253
|
+
| ----- | ---------------------- | ---------------------------------------------------------------------------- |
|
|
254
|
+
| `AA` | — (resolves) | — |
|
|
255
|
+
| `CA` | — (resolves) | — |
|
|
256
|
+
| `AE` | `AckApplicationError` | `message` ← MSA-3, `errorCode` ← ERR-3, `severity` ← ERR-4, `raw` ← wire ACK |
|
|
257
|
+
| `AR` | `AckApplicationReject` | same |
|
|
258
|
+
| `CE` | `AckCommitError` | same |
|
|
259
|
+
| `CR` | `AckCommitReject` | same |
|
|
260
|
+
|
|
261
|
+
Every thrown `AckException` carries the original raw ACK message on its `raw` attribute. When MSA-3 is empty, the thrown error's `message` defaults to `Acknowledgment <code> from receiver`. When ERR-3 is missing, `errorCode` defaults to `Hl7ErrorCode.ApplicationInternalError` (`207`). When ERR-4 is missing, `severity` defaults to `Severity.Error` (`E`).
|
|
262
|
+
|
|
263
|
+
### Acknowledgment modes
|
|
264
|
+
|
|
265
|
+
HL7v2 §2.9.2 defines two acknowledgment levels — the same wire protocol carries both. Each `send()` picks which one to wait for via the `mode` option:
|
|
266
|
+
|
|
267
|
+
- `mode: "OnApplication"` (default) — return when the application-level ACK arrives (`AA`/`AE`/`AR`). Intermediate `CA` (Commit Accept) frames sent by enhanced-mode receivers are surfaced via iteration but do not resolve the send. Use this when you need to know that the receiver successfully processed the message.
|
|
268
|
+
- `mode: "OnCommit"` — return on the first frame regardless of code, typically a commit-level ACK (`CA`). The send resolves as soon as the receiver confirms receipt; later application-level frames are not observed because the connection is closed. Use this for receivers that only send commit-level ACKs (basic mode) or when the commit confirmation is what you need.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
// Default — wait for application-level processing to complete.
|
|
272
|
+
const ack = await client.send(message); // resolves with AA, throws AckException on AE/AR
|
|
273
|
+
|
|
274
|
+
// Resolve as soon as the receiver acknowledges receipt.
|
|
275
|
+
const commit = await client.send(message, { mode: "OnCommit" }); // resolves with CA
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Streaming acknowledgments
|
|
279
|
+
|
|
280
|
+
`client.send()` resolves with the resolving accept ACK — the simple "give me the answer" path. `client.stream()` is the sibling method for real-time observation: it yields each accept ACK as it arrives and completes after the resolving frame. The intermediate `CA` is visible before the application-level frame in enhanced mode.
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
for await (const ack of client.stream(message)) {
|
|
284
|
+
log.info({ code: ack.code }, "ack received");
|
|
285
|
+
// stream completes itself after the resolving frame
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
NAK codes (`AE`/`AR`/`CE`/`CR`) throw the matching `AckException` from both `send()` and `stream()` — same exception, same `error.raw`. Streams that want to inspect a NAK can wrap the loop in `try/catch`.
|
|
290
|
+
|
|
291
|
+
Each call (whether `send` or `stream`) opens its own connection. There's no shared state between methods; pick the one that matches the consumption pattern you need at the call site.
|
|
292
|
+
|
|
293
|
+
### Connection lifecycle
|
|
294
|
+
|
|
295
|
+
`MllpClient` does not pool or reuse connections. Each `send()` opens a fresh TCP/TLS connection and closes it after the ACK frame is read. This trades TCP and TLS handshake overhead for a simpler API and isolates each request from the failure modes of the others. Receivers that require long-lived connections may need a different client.
|
|
296
|
+
|
|
297
|
+
### Concurrency
|
|
298
|
+
|
|
299
|
+
Each `send()` is independent. Concurrent calls open independent connections; there is no shared state between in-flight requests.
|
|
300
|
+
|
|
301
|
+
## Limitations
|
|
302
|
+
|
|
303
|
+
Things `@glion/mllp-client` deliberately does **not** do — call them out explicitly so callers can decide whether the client fits their integration:
|
|
304
|
+
|
|
305
|
+
- **No connection pooling or reuse.** Every `send()` opens a fresh socket. High-volume integrations that need to amortise the TCP/TLS handshake should layer their own pool above `send()` (or wait for a future opt-in `Connection` handle).
|
|
306
|
+
- **No retry or backoff.** A failed `send()` rejects once. Retry policy is the caller's responsibility — semantics vary too much across HL7v2 deployments to bake one in.
|
|
307
|
+
- **No outbound queueing.** The client sends what it is given, immediately. Callers that need rate-limiting or queueing should compose those above `send()`.
|
|
308
|
+
- **No streaming beyond the resolving frame.** Both `send()` and `stream()` read frames until the one selected by `mode` arrives (a `CA` frame followed by an application-level ACK is the most you will ever see); any further bytes from the receiver are discarded when the socket is closed. `stream()` surfaces every accept frame in real time, but the connection still closes after the resolving frame.
|
|
309
|
+
- **No outbound message-size limit.** The encoder will frame whatever you pass in. Use `maxAckSize` to cap inbound frames; cap outbound size in your application code if that matters.
|
|
310
|
+
|
|
311
|
+
## Errors and PHI
|
|
312
|
+
|
|
313
|
+
Thrown errors carry the full diagnostic context so callers can debug without reaching for additional state. **That context can include HL7v2 message content**:
|
|
314
|
+
|
|
315
|
+
- `AckException.raw` (from `@glion/ack`) holds the full wire-format ACK that triggered the exception. ACKs typically do not contain protected health information (PHI), but a NAK whose MSA-3 echoes input data can.
|
|
316
|
+
- `error.message` on a NAK exception is MSA-3 verbatim, which a misbehaving receiver may populate with patient identifiers, dates of birth, or free-text notes.
|
|
317
|
+
- `error.cause` on `MllpClientError(MALFORMED_ACK)` chains the underlying parser error, whose message may include a slice of the unparseable input.
|
|
318
|
+
|
|
319
|
+
If you log thrown errors to a destination that is not approved for PHI (general-purpose log aggregators, third-party error trackers, terminals shared with non-clinical staff), redact `error.raw`, `error.message`, and `error.cause` first. The package never logs these fields itself — propagation is entirely the caller's choice.
|
|
320
|
+
|
|
321
|
+
## Part of Glion
|
|
322
|
+
|
|
323
|
+
`@glion/mllp-client` is part of **[Glion]**, the application framework for HL7v2. See the [Glion README] for the full package catalog and architecture.
|
|
324
|
+
|
|
325
|
+
[Glion]: https://github.com/rethinkhealth/glion#readme
|
|
326
|
+
[Glion README]: https://github.com/rethinkhealth/glion#readme
|