@glion/mllp-client 0.16.0 → 0.20.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/README.md +466 -193
- package/dist/assertions.d.ts +17 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/chunks/mllp-invalid-option-error.js +32 -0
- package/dist/chunks/mllp-invalid-option-error.js.map +1 -0
- package/dist/client.d.ts +127 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/constants.d.ts +23 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/errors/base.d.ts +30 -0
- package/dist/errors/base.d.ts.map +1 -0
- package/dist/errors/index.d.ts +14 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/mllp-client-closed-error.d.ts +12 -0
- package/dist/errors/mllp-client-closed-error.d.ts.map +1 -0
- package/dist/errors/mllp-connection-error.d.ts +9 -0
- package/dist/errors/mllp-connection-error.d.ts.map +1 -0
- package/dist/errors/mllp-connection-failed-error.d.ts +12 -0
- package/dist/errors/mllp-connection-failed-error.d.ts.map +1 -0
- package/dist/errors/mllp-connection-lost-error.d.ts +13 -0
- package/dist/errors/mllp-connection-lost-error.d.ts.map +1 -0
- package/dist/errors/mllp-connection-timeout-error.d.ts +13 -0
- package/dist/errors/mllp-connection-timeout-error.d.ts.map +1 -0
- package/dist/errors/mllp-invalid-message-error.d.ts +13 -0
- package/dist/errors/mllp-invalid-message-error.d.ts.map +1 -0
- package/dist/errors/mllp-invalid-option-error.d.ts +8 -0
- package/dist/errors/mllp-invalid-option-error.d.ts.map +1 -0
- package/dist/errors/mllp-invalid-response-error.d.ts +16 -0
- package/dist/errors/mllp-invalid-response-error.d.ts.map +1 -0
- package/dist/errors/mllp-send-aborted-error.d.ts +12 -0
- package/dist/errors/mllp-send-aborted-error.d.ts.map +1 -0
- package/dist/errors/mllp-send-timeout-error.d.ts +13 -0
- package/dist/errors/mllp-send-timeout-error.d.ts.map +1 -0
- package/dist/errors/nak-exception.d.ts +17 -0
- package/dist/errors/nak-exception.d.ts.map +1 -0
- package/dist/events.d.ts +35 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/index.d.ts +13 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +843 -3
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +37 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/queue.d.ts +25 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/reconnect.d.ts +18 -0
- package/dist/reconnect.d.ts.map +1 -0
- package/dist/runtime/node.d.ts +61 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +121 -0
- package/dist/runtime/node.js.map +1 -0
- package/dist/runtime/workers.d.ts +33 -0
- package/dist/runtime/workers.d.ts.map +1 -0
- package/dist/runtime/workers.js +70 -0
- package/dist/runtime/workers.js.map +1 -0
- package/dist/session.d.ts +63 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/state.d.ts +42 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +37 -46
- package/dist/client.js +0 -549
- package/dist/client.js.map +0 -1
- package/dist/core/acknowledgment.d.ts +0 -103
- package/dist/core/acknowledgment.d.ts.map +0 -1
- package/dist/core/client.d.ts +0 -256
- package/dist/core/client.d.ts.map +0 -1
- package/dist/core/connect.d.ts +0 -101
- package/dist/core/connect.d.ts.map +0 -1
- package/dist/core/errors.d.ts +0 -81
- package/dist/core/errors.d.ts.map +0 -1
- package/dist/core/index.d.ts +0 -23
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -2
- package/dist/core/internal/ack-parser-stream.d.ts +0 -22
- package/dist/core/internal/ack-parser-stream.d.ts.map +0 -1
- package/dist/core/internal/subscribe-abort.d.ts +0 -30
- package/dist/core/internal/subscribe-abort.d.ts.map +0 -1
- package/dist/node.js +0 -214
- package/dist/node.js.map +0 -1
- package/dist/runtimes/internal/node-error-mapping.d.ts +0 -68
- package/dist/runtimes/internal/node-error-mapping.d.ts.map +0 -1
- package/dist/runtimes/node.d.ts +0 -47
- package/dist/runtimes/node.d.ts.map +0 -1
- package/dist/runtimes/node.js +0 -3
- package/dist/runtimes/workers.d.ts +0 -64
- package/dist/runtimes/workers.d.ts.map +0 -1
- package/dist/runtimes/workers.js +0 -143
- package/dist/runtimes/workers.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,322 +1,595 @@
|
|
|
1
1
|
# @glion/mllp-client
|
|
2
2
|
|
|
3
|
-
MLLP client for
|
|
3
|
+
An HL7v2 MLLP client for Node.js, Bun, Deno, and Cloudflare Workers.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
- 📦 **MLLP built in.** Framing, message boundaries and acknowledgment matching are handled. You send a parsed message and get one back.
|
|
6
|
+
- 🔄 **Predictable connection lifecycle.** Timeouts on connecting and on waiting for a reply, connection attempts retried with backoff, TCP keepalive by default, and explicit states you can read.
|
|
7
|
+
- ⚡ **Thin over TCP.** One socket, one message on the wire at a time. Further sends queue in call order. No worker threads, no polling.
|
|
8
|
+
- 🧯 **Errors you can act on.** Every failure carries a stable code and says whether the connection is still usable.
|
|
9
|
+
- 🧩 **Any transport.** TCP and TLS on Node.js, Bun, Deno, and Cloudflare Workers included. An in-memory or custom socket plugs in behind the same two-method interface.
|
|
10
|
+
- 🔤 **Typed end to end.** TypeScript throughout, with parsed HL7v2 going in and coming out.
|
|
8
11
|
|
|
9
12
|
## Install
|
|
10
13
|
|
|
11
14
|
```bash
|
|
12
|
-
npm install @glion/mllp-client
|
|
15
|
+
npm install @glion/mllp-client
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
|
|
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.
|
|
18
|
+
Node.js 22 or later. ESM only.
|
|
37
19
|
|
|
38
20
|
## Use
|
|
39
21
|
|
|
22
|
+
### Send a message
|
|
23
|
+
|
|
40
24
|
```ts
|
|
25
|
+
import { parseHL7v2 } from "@glion/parser";
|
|
41
26
|
import { MllpClient } from "@glion/mllp-client";
|
|
27
|
+
import { nodeSocket } from "@glion/mllp-client/node";
|
|
42
28
|
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
await using client = new MllpClient({
|
|
30
|
+
socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
|
|
31
|
+
});
|
|
45
32
|
|
|
46
|
-
const
|
|
33
|
+
const adt = parseHL7v2(
|
|
47
34
|
[
|
|
48
|
-
"MSH|^~\\&|
|
|
49
|
-
"EVN|A01|
|
|
50
|
-
"PID|1||12345^^^MRN||
|
|
35
|
+
"MSH|^~\\&|SENDER|FAC|RECV|FAC|20260101120000||ADT^A01|MSG00001|P|2.5",
|
|
36
|
+
"EVN|A01|20260101120000",
|
|
37
|
+
"PID|1||12345^^^MRN||DOE^JOHN||19800101|M",
|
|
51
38
|
].join("\r")
|
|
52
39
|
);
|
|
53
40
|
|
|
54
|
-
|
|
55
|
-
|
|
41
|
+
const ack = await client.send(adt);
|
|
42
|
+
ack.code; //=> "AA" or "CA"
|
|
43
|
+
ack.raw; //=> the acknowledgment as text
|
|
44
|
+
ack.tree; //=> the acknowledgment as a tree
|
|
56
45
|
```
|
|
57
46
|
|
|
58
|
-
|
|
47
|
+
`send()` connects on first use, so `connect()` is optional. `await using` closes the client at the end of the block; without it, call `close()`.
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
import { AckApplicationError, AckApplicationReject } from "@glion/ack";
|
|
62
|
-
import { MllpClient, MllpClientError } from "@glion/mllp-client";
|
|
49
|
+
### Handle a rejection
|
|
63
50
|
|
|
64
|
-
|
|
51
|
+
A NAK means the receiver read your message and refused it — a bad field, a patient it does not know, a message type it does not handle. `send()` throws, and the connection stays open for the next message.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { AckException } from "@glion/ack";
|
|
55
|
+
import { MllpClientError } from "@glion/mllp-client";
|
|
65
56
|
|
|
66
57
|
try {
|
|
67
|
-
await client.send(
|
|
58
|
+
await client.send(adt);
|
|
68
59
|
} catch (error) {
|
|
69
|
-
if (error instanceof
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// error.raw holds the wire-format ACK message.
|
|
73
|
-
} else if (error instanceof AckApplicationReject) {
|
|
74
|
-
// MSA-1 = AR — receiver rejected the message.
|
|
60
|
+
if (error instanceof AckException) {
|
|
61
|
+
error.code; //=> "AE", "AR", "CE", or "CR"
|
|
62
|
+
error.text; //=> the receiver's reason, when it gave one
|
|
75
63
|
} else if (error instanceof MllpClientError) {
|
|
76
|
-
|
|
77
|
-
// TIMEOUT, CONNECTION_CLOSED, MALFORMED_FRAME, or MALFORMED_ACK.
|
|
64
|
+
error.code; //=> e.g. "SEND_TIMEOUT" — see Errors
|
|
78
65
|
}
|
|
79
66
|
}
|
|
80
67
|
```
|
|
81
68
|
|
|
82
|
-
|
|
69
|
+
### Connect at startup
|
|
70
|
+
|
|
71
|
+
`connect()` opens the connection without sending anything. Call it at startup to find out immediately that a host, port, or firewall is wrong, instead of on your first real message.
|
|
83
72
|
|
|
84
73
|
```ts
|
|
85
74
|
const client = new MllpClient({
|
|
86
|
-
host: "
|
|
87
|
-
port: 2575,
|
|
88
|
-
tls: false,
|
|
75
|
+
socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
|
|
89
76
|
});
|
|
77
|
+
await client.connect();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Calling it twice is safe. On a connected client it returns immediately; while a connection is still opening, it waits for that attempt and shares its result.
|
|
90
81
|
|
|
91
|
-
|
|
82
|
+
### Reconnect
|
|
83
|
+
|
|
84
|
+
A connection that cannot be opened is dialed again. By default the client makes five further attempts, waiting a random time before each: up to 1 s, then 2 s, 4 s, 8 s, and 16 s, so it gives up about 30 seconds after the first failure and closes with the last attempt's error. `attempts: Infinity` keeps dialing; `close()` stops it. A `send()` that arrives while the client is connecting waits for the outcome.
|
|
85
|
+
|
|
86
|
+
A connection that is lost is not restored. The client closes with the failure, and the message in flight is **not sent again**: its `send()` rejects, and whether the receiver got it is the caller's to decide. A client is as disposable as a socket: construct a new one to send again.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const client = new MllpClient({
|
|
90
|
+
socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
|
|
91
|
+
reconnect: {
|
|
92
|
+
attempts: 20,
|
|
93
|
+
delay: (attempt) => {
|
|
94
|
+
logger.warn({ attempt }, "mllp reconnecting");
|
|
95
|
+
return Math.min(30_000, 500 * 2 ** attempt);
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
92
99
|
```
|
|
93
100
|
|
|
94
|
-
|
|
101
|
+
`reconnect: false` turns it off: the client dials once.
|
|
102
|
+
|
|
103
|
+
## Options
|
|
104
|
+
|
|
105
|
+
| Option | Type | Default | Description |
|
|
106
|
+
| ------------------ | ------------------------------- | --------- | ------------------------------------------------------------------------------------------------------ |
|
|
107
|
+
| `socket` | `MllpSocket` | required | Runtime adapter, such as `nodeSocket({ host, port })`. |
|
|
108
|
+
| `connectTimeoutMs` | `number` | `10000` | Time allowed to open the connection. Exceeded: `MllpConnectionTimeoutError`. |
|
|
109
|
+
| `sendTimeoutMs` | `number` | `30000` | Time allowed from writing a message to receiving its acknowledgment. Exceeded: `MllpSendTimeoutError`. |
|
|
110
|
+
| `maxBufferedBytes` | `number` | 16 MiB | Largest reply the client buffers. Exceeded: `MllpInvalidResponseError`. |
|
|
111
|
+
| `reconnect` | `MllpReconnectOptions \| false` | see below | How the client dials again after a failed attempt. `false` dials once. |
|
|
112
|
+
|
|
113
|
+
`send(message, { timeoutMs })` overrides `sendTimeoutMs` for one message.
|
|
114
|
+
|
|
115
|
+
`MllpReconnectOptions`:
|
|
116
|
+
|
|
117
|
+
| Option | Type | Default | Description |
|
|
118
|
+
| ---------- | ----------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
119
|
+
| `attempts` | `number` | `5` | Attempts after a failed one before the client closes. `Infinity` for no limit. |
|
|
120
|
+
| `delay` | `(attempt: number) => number` | full-jitter backoff from 1 s, capped at 30 s | Milliseconds to wait before `attempt` (from 1). Called only while attempts remain. |
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
### `new MllpClient(options)`
|
|
95
125
|
|
|
96
126
|
```ts
|
|
97
|
-
|
|
127
|
+
new MllpClient(options: MllpClientOptions): MllpClient
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Creates a client for one remote system. Nothing is opened until the first `connect()` or `send()`.
|
|
131
|
+
|
|
132
|
+
**Parameters** — see [Options](#options).
|
|
133
|
+
|
|
134
|
+
**Throws** `MllpInvalidOptionError` when a timeout is not a positive number of milliseconds, `maxBufferedBytes` is not a positive integer, or `reconnect.attempts` is not a non-negative integer.
|
|
98
135
|
|
|
136
|
+
```ts
|
|
99
137
|
const client = new MllpClient({
|
|
100
|
-
host: "
|
|
101
|
-
|
|
102
|
-
tls: {
|
|
103
|
-
ca: readFileSync("ca.pem"),
|
|
104
|
-
cert: readFileSync("client-cert.pem"),
|
|
105
|
-
key: readFileSync("client-key.pem"),
|
|
106
|
-
},
|
|
138
|
+
socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
|
|
139
|
+
sendTimeoutMs: 10_000,
|
|
107
140
|
});
|
|
141
|
+
```
|
|
108
142
|
|
|
109
|
-
|
|
143
|
+
### `client.send(message, options?)`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
send(message: Root, options?: { timeoutMs?: number }): Promise<MllpClientResponse>
|
|
110
147
|
```
|
|
111
148
|
|
|
112
|
-
|
|
149
|
+
Sends one message and resolves with the acknowledgment that answers it. Connects first if the client is not connected.
|
|
150
|
+
|
|
151
|
+
| Parameter | Type | Description |
|
|
152
|
+
| ------------------- | -------- | -------------------------------------------------------- |
|
|
153
|
+
| `message` | `Root` | A parsed HL7v2 message. Must carry an MSH-10 control ID. |
|
|
154
|
+
| `options.timeoutMs` | `number` | Overrides `sendTimeoutMs` for this message only. |
|
|
155
|
+
|
|
156
|
+
**Returns** an `MllpClientResponse`:
|
|
157
|
+
|
|
158
|
+
| Field | Type | Description |
|
|
159
|
+
| ----------- | ---------------- | --------------------------------------------------- |
|
|
160
|
+
| `code` | `AckSuccessCode` | MSA-1, `AA` or `CA`. |
|
|
161
|
+
| `controlId` | `string` | MSA-2: the control ID of the message this answers. |
|
|
162
|
+
| `id` | `string` | The acknowledgment's own MSH-10. |
|
|
163
|
+
| `text` | `string?` | MSA-3, the receiver's diagnostic, when it gave one. |
|
|
164
|
+
| `tree` | `Root` | The acknowledgment, parsed. |
|
|
165
|
+
| `raw` | `string` | The acknowledgment as text. |
|
|
166
|
+
|
|
167
|
+
**Throws**
|
|
168
|
+
|
|
169
|
+
- `AckException` — the receiver refused the message. The connection stays open.
|
|
170
|
+
- `MllpInvalidMessageError` — no MSH-10, or the message could not be serialized. Nothing was sent.
|
|
171
|
+
- `MllpInvalidOptionError` — `timeoutMs` is out of range. Nothing was sent.
|
|
172
|
+
- `MllpClientClosedError` — the client is closed, or closed while this send was waiting its turn.
|
|
173
|
+
- `MllpConnectionFailedError`, `MllpConnectionTimeoutError` — the connection could not be opened.
|
|
174
|
+
- `MllpSendTimeoutError`, `MllpConnectionLostError`, `MllpInvalidResponseError` — the exchange failed. These close the connection; see [Errors](#errors).
|
|
175
|
+
- `MllpSendAbortedError` — `destroy()` cut the send off. Delivery is unknown.
|
|
176
|
+
|
|
177
|
+
Every error carries `delivery`, `not-sent` or `unknown`, the one fact a retry needs; see [Errors](#errors).
|
|
178
|
+
|
|
179
|
+
One message is on the wire at a time. A `send()` arriving while another is in flight waits its turn, and sends go out in the order they were called, so a batch may be fired at once:
|
|
113
180
|
|
|
114
181
|
```ts
|
|
115
|
-
|
|
182
|
+
const outcomes = await Promise.allSettled(
|
|
183
|
+
batch.map((message) => client.send(message))
|
|
184
|
+
);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The queue is in memory and has no bound. `timeoutMs` runs from the moment the write starts, not from the call. A send still waiting when `close()` or `destroy()` is called, or when a failure closes the client, rejects at once with `delivery: "not-sent"`: with `MllpClientClosedError`, or with the failure itself when the failure is one of not being sent, such as the dial's `MllpConnectionFailedError` or `MllpConnectionTimeoutError`, as `connect()` gets. Nothing behind a failed message goes out. See [Does `send()` queue?](#does-send-queue).
|
|
188
|
+
|
|
189
|
+
### `client.connect()`
|
|
116
190
|
|
|
117
|
-
|
|
118
|
-
|
|
191
|
+
```ts
|
|
192
|
+
connect(): Promise<void>
|
|
119
193
|
```
|
|
120
194
|
|
|
121
|
-
|
|
195
|
+
Opens the connection without sending anything, dialing again under the [reconnect](#reconnect) policy when an attempt fails. Optional — `send()` connects on first use — but calling it at startup surfaces a wrong host, port, or firewall rule as soon as the policy gives up.
|
|
196
|
+
|
|
197
|
+
Idempotent. On a connected client it resolves at once; while an attempt is in flight it waits for the outcome and shares it.
|
|
198
|
+
|
|
199
|
+
**Throws** the last attempt's `MllpConnectionFailedError` or `MllpConnectionTimeoutError`, or `MllpClientClosedError` when the client is already closed or `close()` or `destroy()` cancelled the attempt.
|
|
200
|
+
|
|
201
|
+
### `client.close()`
|
|
122
202
|
|
|
123
203
|
```ts
|
|
124
|
-
|
|
204
|
+
close(): Promise<void>
|
|
205
|
+
```
|
|
125
206
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await client.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// error.message names the rejected fields and points at the docs.
|
|
136
|
-
}
|
|
137
|
-
}
|
|
207
|
+
Closes the connection once the message in flight has been acknowledged. New sends, and sends still waiting their turn, are refused from the moment it is called with `MllpClientClosedError`; the message in flight is never cut off. Await the sends you want delivered before calling it. An attempt to connect stops at once.
|
|
208
|
+
|
|
209
|
+
Resolves when the connection is down, from any phase. Never throws. Idempotent. The wait is bounded by the in-flight send's own deadline. If the message it waits for fails, the client closes with that failure; otherwise with `null`, the owner's decision.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
process.on("SIGTERM", async () => {
|
|
213
|
+
await client.close();
|
|
214
|
+
process.exit(0);
|
|
215
|
+
});
|
|
138
216
|
```
|
|
139
217
|
|
|
140
|
-
|
|
218
|
+
### `client.destroy(reason?)`
|
|
141
219
|
|
|
142
|
-
|
|
220
|
+
```ts
|
|
221
|
+
destroy(reason?: MllpClientError | null): Promise<void>
|
|
222
|
+
```
|
|
143
223
|
|
|
144
|
-
|
|
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. |
|
|
224
|
+
Closes the connection now, without waiting for anything in flight. A message in flight rejects with `MllpSendAbortedError`. An attempt to connect stops at once. The client closes with `reason`: the `close` event carries it, and so does `cause` on every later call's `MllpClientClosedError`. Omitted or `null`, the client closed by its owner's decision, as `net.Socket.destroy(error)` does. Sends waiting their turn are rejected with `reason` itself when its delivery is `not-sent`, and with `MllpClientClosedError` carrying `reason` otherwise. Arriving while the client is already closing, it cuts the message in flight and joins the ending under way; `reason` is recorded when no failure is known yet.
|
|
151
225
|
|
|
152
|
-
|
|
226
|
+
Resolves when the connection is down, from any phase. Never throws. Idempotent.
|
|
153
227
|
|
|
154
|
-
|
|
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. |
|
|
228
|
+
### `client[Symbol.asyncDispose]()`
|
|
162
229
|
|
|
163
|
-
|
|
230
|
+
Calls `close()`. Lets a client be scoped with `await using`:
|
|
164
231
|
|
|
165
|
-
|
|
232
|
+
```ts
|
|
233
|
+
{
|
|
234
|
+
await using client = new MllpClient({ socket });
|
|
235
|
+
await client.send(message);
|
|
236
|
+
} // closed here, even if send() threw
|
|
237
|
+
```
|
|
166
238
|
|
|
167
|
-
|
|
239
|
+
### `client.state`
|
|
168
240
|
|
|
169
241
|
```ts
|
|
170
|
-
|
|
242
|
+
readonly state: MllpClientState
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
| Value | Meaning |
|
|
246
|
+
| ------------ | -------------------------------------------------------------------------------- |
|
|
247
|
+
| `idle` | Nothing opened yet. The first `send()` or `connect()` opens the connection. |
|
|
248
|
+
| `connecting` | The connection is being opened, further attempts included. |
|
|
249
|
+
| `connected` | Open, with no message in flight. |
|
|
250
|
+
| `sending` | A message is on the wire, waiting for its acknowledgment. Further sends wait. |
|
|
251
|
+
| `closing` | The client is ending: a message being waited out, or the connection coming down. |
|
|
252
|
+
| `closed` | The connection is down. |
|
|
253
|
+
|
|
254
|
+
A client closes once. After `close()`, `destroy()`, a connection the reconnect policy could not open, or a connection that was lost, every call throws `MllpClientClosedError`; construct a new client to send again.
|
|
171
255
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
256
|
+
### `client.pending`
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
readonly pending: number
|
|
175
260
|
```
|
|
176
261
|
|
|
177
|
-
|
|
262
|
+
How many sends wait their turn, the one in flight excluded. The queue's backlog, for a metric or a backpressure decision; `0` whenever nothing waits.
|
|
178
263
|
|
|
179
|
-
|
|
264
|
+
### `client.connected`
|
|
180
265
|
|
|
181
|
-
|
|
266
|
+
```ts
|
|
267
|
+
readonly connected: boolean
|
|
268
|
+
```
|
|
182
269
|
|
|
183
|
-
|
|
270
|
+
`true` in `connected` and `sending`, `false` everywhere else.
|
|
271
|
+
|
|
272
|
+
### `client.on(event, listener)` / `client.off(event, listener)`
|
|
184
273
|
|
|
185
274
|
```ts
|
|
186
|
-
|
|
275
|
+
on<E>(event: E, listener: MllpClientListener<E>): this
|
|
276
|
+
off<E>(event: E, listener: MllpClientListener<E>): this
|
|
187
277
|
```
|
|
188
278
|
|
|
189
|
-
|
|
279
|
+
Adds or removes a listener. Both return the client, so calls chain.
|
|
280
|
+
|
|
281
|
+
| Event | Listener | Fires when |
|
|
282
|
+
| --------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
283
|
+
| `connect` | `() => void` | The connection opened. |
|
|
284
|
+
| `close` | `(error: MllpClientError \| null) => void` | The connection is down. Fires once, from any phase, even if it never connected. `error` is the failure that closed the client, or `null` when you closed it. |
|
|
285
|
+
|
|
286
|
+
A lost connection is not an event of its own: it closes the client, so `close` fires with the failure. Listeners are synchronous, and one that throws propagates to whatever triggered the event.
|
|
190
287
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
288
|
+
```ts
|
|
289
|
+
client
|
|
290
|
+
.on("connect", () => metrics.increment("mllp.connected"))
|
|
291
|
+
.on("close", (error) => {
|
|
292
|
+
logger.warn({ code: error?.code ?? "closed_by_owner" }, "mllp closed");
|
|
293
|
+
});
|
|
294
|
+
```
|
|
195
295
|
|
|
196
|
-
|
|
296
|
+
## Runtimes
|
|
197
297
|
|
|
198
|
-
|
|
298
|
+
The client speaks MLLP over a pair of byte streams and knows nothing else about the transport. That whole dependency is [`MllpSocket`](#custom-socket) — two methods — so supporting a runtime means an adapter, not a fork of the client.
|
|
199
299
|
|
|
200
|
-
|
|
300
|
+
| Runtime | Adapter | Import | TLS | Proven by |
|
|
301
|
+
| ------------------ | -------------------------------------- | ---------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
302
|
+
| Node.js ≥ 22 | [`nodeSocket`](#nodejs) | `@glion/mllp-client/node` | [Trust store, CA, client certificate](#tls) | Conformance suite in CI, Node 22 and 24 |
|
|
303
|
+
| Bun | [`nodeSocket`](#nodejs) | `@glion/mllp-client/node` | [Trust store, CA, client certificate](#tls) | Conformance suite in CI, Bun 1.4 |
|
|
304
|
+
| Deno | [`nodeSocket`](#nodejs) | `@glion/mllp-client/node` | [Trust store, CA, client certificate](#tls) | Conformance suite in CI, Deno 2.9 |
|
|
305
|
+
| Cloudflare Workers | [`workersSocket`](#cloudflare-workers) | `@glion/mllp-client/workers` | Public trust store only | Integration suite in CI, inside `workerd`; TCP only, TLS handshake untested ([#626](https://github.com/rethinkhealth/glion/issues/626)) |
|
|
306
|
+
|
|
307
|
+
Every adapter runs the same conformance suite: the [`MllpSocket` contract](#custom-socket) case by case, and the client's behaviour over a real socket scenario by scenario. On Node.js, Bun, and Deno it runs twice, over TCP and over TLS. A custom socket can run it too; see `tests/integration/conformance/` in the package source.
|
|
308
|
+
|
|
309
|
+
### Node.js
|
|
201
310
|
|
|
202
311
|
```ts
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
312
|
+
import { nodeSocket } from "@glion/mllp-client/node";
|
|
313
|
+
|
|
314
|
+
nodeSocket(options: NodeSocketOptions): MllpSocket
|
|
206
315
|
```
|
|
207
316
|
|
|
208
|
-
|
|
317
|
+
TCP over `net.Socket`, or TLS over `tls.TLSSocket` when `tls` is set. The same adapter runs on Bun and Deno through their `node:net` and `node:tls` compatibility; no separate import is needed there.
|
|
318
|
+
|
|
319
|
+
| Option | Type | Default | Description |
|
|
320
|
+
| ----------------- | --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
321
|
+
| `host` | `string` | required | Host name or address of the receiver. |
|
|
322
|
+
| `port` | `number` | required | TCP port of the receiver. |
|
|
323
|
+
| `tls` | `boolean \| NodeTlsOptions` | `false` | TLS for the connection. See [TLS](#tls). |
|
|
324
|
+
| `gracefulCloseMs` | `number` | `1000` | How long a socket gets to end cleanly before it is destroyed. |
|
|
325
|
+
| `keepAliveIdleMs` | `number` | `30000` | Idle time before the first keepalive probe. Once the probes fail, the OS ends the socket, and the next send fails at once with `CONNECTION_LOST` instead of waiting out `sendTimeoutMs`. |
|
|
209
326
|
|
|
210
|
-
|
|
327
|
+
`TCP_NODELAY` is set, so a message goes out immediately rather than waiting on Nagle's algorithm. On Bun and Deno the keepalive delay is passed through to the runtime; whether the runtime honours it is not verified.
|
|
211
328
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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. |
|
|
329
|
+
```ts
|
|
330
|
+
const client = new MllpClient({
|
|
331
|
+
socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
|
|
332
|
+
});
|
|
333
|
+
```
|
|
221
334
|
|
|
222
|
-
|
|
335
|
+
Deno needs `--allow-net` for the receiver's host and port.
|
|
223
336
|
|
|
224
|
-
|
|
337
|
+
#### TLS
|
|
225
338
|
|
|
226
|
-
|
|
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. |
|
|
339
|
+
`tls: true` verifies the receiver's certificate against the runtime's trusted CAs, for `host`, and needs no certificate files. `host` is sent as the server name (SNI) unless it is an IP address.
|
|
235
340
|
|
|
236
|
-
|
|
341
|
+
```ts
|
|
342
|
+
nodeSocket({ host: "hl7.example.org", port: 2575, tls: true });
|
|
343
|
+
```
|
|
237
344
|
|
|
238
|
-
|
|
345
|
+
An object passes Node's `tls.connect()` options:
|
|
239
346
|
|
|
240
|
-
|
|
347
|
+
| Option | Type | Default | Description |
|
|
348
|
+
| -------------------- | --------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
349
|
+
| `ca` | `string \| Buffer \| Array` | Runtime CAs | CA certificates that issued the receiver's certificate, PEM. Replaces the runtime's trusted CAs. |
|
|
350
|
+
| `cert` | `string \| Buffer \| Array` | none | Client certificate chain for mutual TLS, PEM. |
|
|
351
|
+
| `key` | `string \| Buffer \| Array` | none | Private key for `cert`, PEM. |
|
|
352
|
+
| `passphrase` | `string` | none | Passphrase for an encrypted `key` or `pfx`. |
|
|
353
|
+
| `pfx` | `string \| Buffer \| Array` | none | Client certificate and key as PKCS#12. |
|
|
354
|
+
| `servername` | `string` | `host`, unless an IP address | Name the receiver's certificate is verified against, and the SNI name. |
|
|
355
|
+
| `rejectUnauthorized` | `boolean` | `true` | `false` accepts any certificate, including one presented by an attacker. |
|
|
241
356
|
|
|
242
357
|
```ts
|
|
243
|
-
import {
|
|
358
|
+
import { readFileSync } from "node:fs";
|
|
359
|
+
|
|
360
|
+
nodeSocket({
|
|
361
|
+
host: "10.20.0.15",
|
|
362
|
+
port: 2575,
|
|
363
|
+
tls: {
|
|
364
|
+
ca: readFileSync("hospital-ca.pem"),
|
|
365
|
+
cert: readFileSync("client.pem"),
|
|
366
|
+
key: readFileSync("client-key.pem"),
|
|
367
|
+
servername: "hl7.hospital.internal",
|
|
368
|
+
},
|
|
369
|
+
});
|
|
244
370
|
```
|
|
245
371
|
|
|
246
|
-
|
|
372
|
+
A receiver dialed by IP address must present a certificate that lists that address, or `servername` must name one the certificate lists.
|
|
373
|
+
|
|
374
|
+
A handshake that fails rejects with [`CONNECTION_FAILED`](#connection_failed), with Node's TLS error on `cause`: for example `UNABLE_TO_VERIFY_LEAF_SIGNATURE` or `ERR_TLS_CERT_ALTNAME_INVALID`.
|
|
247
375
|
|
|
248
|
-
|
|
376
|
+
A receiver that refuses the client certificate over TLS 1.2 fails the handshake: [`CONNECTION_FAILED`](#connection_failed), nothing sent. Over TLS 1.3 it refuses after the handshake, so the connection opens and the first `send()` rejects with `CONNECTION_LOST`, delivery `unknown`.
|
|
249
377
|
|
|
250
|
-
|
|
378
|
+
On Deno, a wrong `passphrase` does not fail the handshake; the connection opens without the client certificate and fails as a refused client certificate.
|
|
379
|
+
|
|
380
|
+
### Cloudflare Workers
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { workersSocket } from "@glion/mllp-client/workers";
|
|
251
384
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
| `
|
|
385
|
+
workersSocket(options: WorkersSocketOptions): MllpSocket
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
TCP or TLS over `cloudflare:sockets`. The module resolves only inside the Workers runtime; import it from the `./workers` subpath, never from `.`.
|
|
389
|
+
|
|
390
|
+
| Option | Type | Default | Description |
|
|
391
|
+
| ------ | --------- | -------- | --------------------------------------------------------------------------------------------- |
|
|
392
|
+
| `host` | `string` | required | Host name or address of the receiver. |
|
|
393
|
+
| `port` | `number` | required | TCP port of the receiver. |
|
|
394
|
+
| `tls` | `boolean` | `false` | `true` verifies the receiver's certificate against the public CAs Workers trusts, for `host`. |
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { MllpClient } from "@glion/mllp-client";
|
|
398
|
+
import { workersSocket } from "@glion/mllp-client/workers";
|
|
399
|
+
|
|
400
|
+
export default {
|
|
401
|
+
async fetch(request: Request): Promise<Response> {
|
|
402
|
+
await using client = new MllpClient({
|
|
403
|
+
socket: workersSocket({ host: "hl7.example.org", port: 2575 }),
|
|
404
|
+
});
|
|
405
|
+
const ack = await client.send(await messageFrom(request));
|
|
406
|
+
return Response.json({ code: ack.code, controlId: ack.controlId });
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
```
|
|
260
410
|
|
|
261
|
-
|
|
411
|
+
What differs from Node:
|
|
262
412
|
|
|
263
|
-
|
|
413
|
+
- A Worker reaches only endpoints routable from Cloudflare's network. A receiver on a private network needs a publicly reachable endpoint in front of it.
|
|
414
|
+
- `close()` resolves as soon as the runtime has ended the socket; there is no grace window.
|
|
415
|
+
- Locally, under Miniflare (`wrangler dev` or the Vitest integration), connections go through a proxy, so `MllpConnectionFailedError.cause` carries the proxy's message rather than a socket error code. `code` is the same in both.
|
|
416
|
+
- Locally, under Miniflare, a `tls: true` connection fails with the proxy's message, whatever certificate the receiver presents. TLS to a receiver works only when the Worker runs on Cloudflare.
|
|
417
|
+
- Cloudflare blocks some destination ports.
|
|
418
|
+
- `tls` takes no CA, client certificate, or server name. The receiver must present a publicly trusted certificate for `host`, so dial a host name rather than an address. Any value other than a boolean throws `MllpInvalidOptionError`.
|
|
264
419
|
|
|
265
|
-
|
|
420
|
+
### Custom Socket
|
|
266
421
|
|
|
267
|
-
|
|
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.
|
|
422
|
+
You can expand `MllpSocket` to build a custom socket to one remote system, which the client opens, uses, and ends.
|
|
269
423
|
|
|
270
424
|
```ts
|
|
271
|
-
|
|
272
|
-
|
|
425
|
+
interface MllpSocket {
|
|
426
|
+
connect(signal: AbortSignal): Promise<MllpStreams>;
|
|
427
|
+
close(): Promise<void>;
|
|
428
|
+
}
|
|
273
429
|
|
|
274
|
-
|
|
275
|
-
|
|
430
|
+
interface MllpStreams {
|
|
431
|
+
readonly readable: ReadableStream<Uint8Array>;
|
|
432
|
+
readonly writable: WritableStream<Uint8Array>;
|
|
433
|
+
}
|
|
276
434
|
```
|
|
277
435
|
|
|
278
|
-
|
|
436
|
+
An implementation must satisfy four rules:
|
|
437
|
+
|
|
438
|
+
1. `connect()` rejects with `signal.reason` when the signal aborts, and a rejection leaves nothing open.
|
|
439
|
+
2. `close()` never rejects, may be called more than once, and always finishes within a bounded time, even when the receiver never answers.
|
|
440
|
+
3. When the socket ends, a pending read on `readable` ends or errors. Bytes sent before a clean close arrive first.
|
|
441
|
+
4. The client owns the streams while connected, and releases them before calling `close()`.
|
|
442
|
+
|
|
443
|
+
An implementation never sees an MLLP frame — framing belongs to the layer above.
|
|
444
|
+
|
|
445
|
+
## Errors
|
|
446
|
+
|
|
447
|
+
Every failure the client raises extends `MllpClientError` and carries three fixed things: a `code`, one word per class for a `switch` or a log field; a `delivery`, what became of the message; and a message that never changes. Errors from the layers below arrive on `cause`, never as the thrown type.
|
|
448
|
+
|
|
449
|
+
A rejection from the receiver is **not** an `MllpClientError` — see [`AckException`](#ackexception).
|
|
450
|
+
|
|
451
|
+
`delivery` is the one fact a retry decision needs:
|
|
279
452
|
|
|
280
|
-
|
|
453
|
+
| `delivery` | Meaning | Codes |
|
|
454
|
+
| ---------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
455
|
+
| `not-sent` | Nothing reached the wire. The message may be sent again as it is. | `INVALID_OPTION`, `INVALID_MESSAGE`, `CLOSED`, `CONNECTION_FAILED`, `CONNECTION_TIMEOUT` |
|
|
456
|
+
| `unknown` | The message may have reached the receiver. Sending it again may deliver it twice. | `SEND_TIMEOUT`, `CONNECTION_LOST`, `SEND_ABORTED`, `INVALID_RESPONSE` |
|
|
281
457
|
|
|
282
458
|
```ts
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
459
|
+
import { MllpClientError, MllpErrorCode } from "@glion/mllp-client";
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
await client.send(message);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (error instanceof MllpClientError) {
|
|
465
|
+
if (error.delivery === "not-sent") {
|
|
466
|
+
return requeue(message); // safe to send again
|
|
467
|
+
}
|
|
468
|
+
switch (error.code) {
|
|
469
|
+
case MllpErrorCode.INVALID_RESPONSE:
|
|
470
|
+
return quarantine(message); // the receiver answered, unreadably
|
|
471
|
+
default:
|
|
472
|
+
return holdForReview(message); // it may already have been processed
|
|
473
|
+
}
|
|
474
|
+
}
|
|
286
475
|
}
|
|
287
476
|
```
|
|
288
477
|
|
|
289
|
-
|
|
478
|
+
The four failures of the wire — `CONNECTION_FAILED`, `CONNECTION_TIMEOUT`, `SEND_TIMEOUT`, `CONNECTION_LOST` — also extend `MllpConnectionError`, one `instanceof` for "the link, not the message".
|
|
479
|
+
|
|
480
|
+
### `INVALID_OPTION`
|
|
481
|
+
|
|
482
|
+
`MllpInvalidOptionError` · delivery `not-sent`
|
|
483
|
+
|
|
484
|
+
A constructor option or a per-send `timeoutMs` is out of range — a timeout that is not a positive number of milliseconds, a `maxBufferedBytes` that is not a positive integer, or a `reconnect.attempts` that is not a non-negative integer or `Infinity`. `workersSocket()` also throws it for a `tls` that is not a boolean. The message names which one.
|
|
485
|
+
|
|
486
|
+
Thrown before anything is opened or sent. A configuration bug, not a runtime condition.
|
|
487
|
+
|
|
488
|
+
### `INVALID_MESSAGE`
|
|
489
|
+
|
|
490
|
+
`MllpInvalidMessageError` · delivery `not-sent` · field: `cause`
|
|
491
|
+
|
|
492
|
+
The message cannot be sent as it stands: no MSH-10 control ID, or it could not be serialized, or its content contains a byte MLLP reserves as a frame marker.
|
|
493
|
+
|
|
494
|
+
**Nothing reached the wire and the connection is still in step**, so the next message can go out on it. Fix or quarantine the message; do not reconnect.
|
|
495
|
+
|
|
496
|
+
### `CLOSED`
|
|
497
|
+
|
|
498
|
+
`MllpClientClosedError` · delivery `not-sent` · field: `cause`
|
|
499
|
+
|
|
500
|
+
The client is closed or closing, so the call cannot be served. Also the error a `connect()` gets when `close()` or `destroy()` cancelled the attempt it was waiting for, and the error a `send()` gets when the client closed while it was waiting its turn; a send waiting behind a dial that failed gets the dial's error instead.
|
|
501
|
+
|
|
502
|
+
A client closes once. Construct a new one to send again. When the client closed on a failure, `cause` is that failure: the last attempt's `CONNECTION_FAILED` or `CONNECTION_TIMEOUT` when the reconnect policy gave up, the `CONNECTION_LOST`, `SEND_TIMEOUT`, or `INVALID_RESPONSE` that ended an earlier message, including one `close()` was waiting out, or the `reason` given to `destroy()`.
|
|
503
|
+
|
|
504
|
+
### `CONNECTION_FAILED`
|
|
505
|
+
|
|
506
|
+
`MllpConnectionFailedError` · delivery `not-sent` · field: `cause`
|
|
507
|
+
|
|
508
|
+
The socket could not be opened. `cause` carries the underlying error — `ECONNREFUSED`, `ENOTFOUND`, `EHOSTUNREACH`, or a TLS error such as `UNABLE_TO_VERIFY_LEAF_SIGNATURE`.
|
|
509
|
+
|
|
510
|
+
Check host, port, whether a firewall allows the route, and, over TLS, the certificate the receiver presents. Nothing was opened, so there is nothing to close.
|
|
511
|
+
|
|
512
|
+
### `CONNECTION_TIMEOUT`
|
|
513
|
+
|
|
514
|
+
`MllpConnectionTimeoutError` · delivery `not-sent` · field: `timeoutMs`
|
|
515
|
+
|
|
516
|
+
The receiver did not accept the connection within `connectTimeoutMs`.
|
|
517
|
+
|
|
518
|
+
Typically a packet-dropping firewall rather than a refused connection — a refusal arrives fast and surfaces as `CONNECTION_FAILED`. With `tls` set, also a receiver that speaks plain TCP and never answers the TLS handshake.
|
|
519
|
+
|
|
520
|
+
### `SEND_TIMEOUT`
|
|
521
|
+
|
|
522
|
+
`MllpSendTimeoutError` · delivery `unknown` · field: `timeoutMs` — **closes the connection**
|
|
523
|
+
|
|
524
|
+
No acknowledgment arrived within the send timeout.
|
|
525
|
+
|
|
526
|
+
Whether the receiver got the message is unknown: it may be slow, or it may have processed the message and failed to reply. The connection closes because a late acknowledgment can no longer be told apart from the next message's — see [Why does a failed send close the connection?](#why-does-a-failed-send-close-the-connection). The client closes with this error; the message is not sent again.
|
|
527
|
+
|
|
528
|
+
### `CONNECTION_LOST`
|
|
529
|
+
|
|
530
|
+
`MllpConnectionLostError` · delivery `unknown` · field: `cause` — **closes the connection**
|
|
531
|
+
|
|
532
|
+
The link went away mid-send: the receiver hung up, or the network broke. `cause` carries the stream error when there was one.
|
|
533
|
+
|
|
534
|
+
Whether the message was received is unknown. The client closes with this error; the message is not sent again.
|
|
535
|
+
|
|
536
|
+
### `SEND_ABORTED`
|
|
537
|
+
|
|
538
|
+
`MllpSendAbortedError` · delivery `unknown`
|
|
539
|
+
|
|
540
|
+
`destroy()` cut off the message in flight. `close()` never raises this: it waits the message out.
|
|
541
|
+
|
|
542
|
+
### `INVALID_RESPONSE`
|
|
543
|
+
|
|
544
|
+
`MllpInvalidResponseError` · delivery `unknown` · fields: `controlId?`, `cause` — **closes the connection**
|
|
545
|
+
|
|
546
|
+
The reply was not a usable acknowledgment of the message that was waiting. Causes, in the order they are checked:
|
|
547
|
+
|
|
548
|
+
- the bytes were not valid UTF-8 or not parseable HL7v2;
|
|
549
|
+
- MSA-2 names a different message — usually a late acknowledgment from an earlier timed-out send;
|
|
550
|
+
- MSA-1 is empty or is not one of the six codes of Table 0008;
|
|
551
|
+
- the reply passed `maxBufferedBytes` before it was complete;
|
|
552
|
+
- the connection closed partway through the reply.
|
|
553
|
+
|
|
554
|
+
The connection closes because the client can no longer tell which reply answers which message. The client closes with this error.
|
|
555
|
+
|
|
556
|
+
### `AckException`
|
|
557
|
+
|
|
558
|
+
From `@glion/ack` · fields: `code`, `controlId`, `text`, `errorCode`, `severity`
|
|
559
|
+
|
|
560
|
+
Not an `MllpClientError`. The receiver read the message and refused it, which is an answer rather than a fault — **the connection stays open** and the next message can go out on it.
|
|
561
|
+
|
|
562
|
+
| Field | Source | Description |
|
|
563
|
+
| ----------- | ------ | --------------------------------------------- |
|
|
564
|
+
| `code` | MSA-1 | `AE`, `AR`, `CE`, or `CR`. |
|
|
565
|
+
| `controlId` | MSA-2 | The message being refused. |
|
|
566
|
+
| `text` | MSA-3 | The receiver's reason, when it gave one. |
|
|
567
|
+
| `errorCode` | ERR-3 | HL7v2 Table 0357 error condition, when given. |
|
|
568
|
+
| `severity` | ERR-4 | `E`, `W`, or `I`, when given. |
|
|
290
569
|
|
|
291
|
-
|
|
570
|
+
The class is the MSA-1 code: `AckApplicationError` (`AE`), `AckApplicationReject` (`AR`), `AckCommitError` (`CE`), `AckCommitReject` (`CR`). An MLLP server built on Glion raises the same types, so both ends of an integration catch the same thing.
|
|
292
571
|
|
|
293
|
-
|
|
572
|
+
## FAQs
|
|
294
573
|
|
|
295
|
-
|
|
574
|
+
### Why does a failed send close the connection?
|
|
296
575
|
|
|
297
|
-
|
|
576
|
+
MLLP has no correlation of its own. The only thing tying a reply to a message is the receiver echoing your MSH-10 back in MSA-2 — and the only reason that can be trusted is that one message is outstanding at a time.
|
|
298
577
|
|
|
299
|
-
|
|
578
|
+
Once a send times out, that no longer holds. A wrong guess here reports one message's outcome under another message's identity, which in a clinical feed is worse than an error.
|
|
300
579
|
|
|
301
|
-
|
|
580
|
+
Clients that pipeline can survive a timeout, because they keep a table of outstanding control IDs and a background reader to match against it. A lockstep client has no table to fall back on.
|
|
302
581
|
|
|
303
|
-
|
|
582
|
+
The client closes with the failure, and never sends the failed message again — only you know whether the receiver already has it. Construct a new client to go on, as you would open a new socket.
|
|
304
583
|
|
|
305
|
-
|
|
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.
|
|
584
|
+
### Does `send()` queue?
|
|
310
585
|
|
|
311
|
-
|
|
586
|
+
Yes, in memory and without a bound. A `send()` arriving while a message is on the wire waits for that message's acknowledgment, then goes out; sends leave in the order they were called, one at a time, so an A01 fired before its A03 reaches the receiver first. Nothing goes out behind a message whose delivery is unknown: a failure closes the client, and every send still waiting rejects with `CLOSED` and `delivery: "not-sent"`. A send waiting behind a dial that fails rejects with the dial's error, as `connect()` does.
|
|
312
587
|
|
|
313
|
-
|
|
588
|
+
The queue exists only in this process. A message waiting in it is not on disk, so a crash loses it without a trace, and a producer faster than the receiver grows it without limit. An interface that must not lose events keeps its own persistent outbound queue and hands the client one message at a time; the client's line is for a batch whose outcomes the caller is awaiting.
|
|
314
589
|
|
|
315
|
-
|
|
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.
|
|
590
|
+
### Why a socket rather than a host and port?
|
|
318
591
|
|
|
319
|
-
|
|
592
|
+
The client handles MLLP and deliberately handles no transport. Putting the whole runtime dependency behind two methods means the same client runs over TCP, TLS, a Unix socket, or a pair of in-memory streams — and the test suite covers every lifecycle path without opening a port.
|
|
320
593
|
|
|
321
594
|
## Part of Glion
|
|
322
595
|
|