@glion/mllp-client 0.16.0 → 0.19.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.
Files changed (86) hide show
  1. package/README.md +370 -195
  2. package/dist/assertions.d.ts +10 -0
  3. package/dist/assertions.d.ts.map +1 -0
  4. package/dist/client.d.ts +108 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/constants.d.ts +23 -0
  7. package/dist/constants.d.ts.map +1 -0
  8. package/dist/errors/base.d.ts +31 -0
  9. package/dist/errors/base.d.ts.map +1 -0
  10. package/dist/errors/index.d.ts +15 -0
  11. package/dist/errors/index.d.ts.map +1 -0
  12. package/dist/errors/mllp-already-sending-error.d.ts +14 -0
  13. package/dist/errors/mllp-already-sending-error.d.ts.map +1 -0
  14. package/dist/errors/mllp-client-closed-error.d.ts +13 -0
  15. package/dist/errors/mllp-client-closed-error.d.ts.map +1 -0
  16. package/dist/errors/mllp-connection-error.d.ts +9 -0
  17. package/dist/errors/mllp-connection-error.d.ts.map +1 -0
  18. package/dist/errors/mllp-connection-failed-error.d.ts +12 -0
  19. package/dist/errors/mllp-connection-failed-error.d.ts.map +1 -0
  20. package/dist/errors/mllp-connection-lost-error.d.ts +13 -0
  21. package/dist/errors/mllp-connection-lost-error.d.ts.map +1 -0
  22. package/dist/errors/mllp-connection-timeout-error.d.ts +13 -0
  23. package/dist/errors/mllp-connection-timeout-error.d.ts.map +1 -0
  24. package/dist/errors/mllp-invalid-message-error.d.ts +13 -0
  25. package/dist/errors/mllp-invalid-message-error.d.ts.map +1 -0
  26. package/dist/errors/mllp-invalid-option-error.d.ts +8 -0
  27. package/dist/errors/mllp-invalid-option-error.d.ts.map +1 -0
  28. package/dist/errors/mllp-invalid-response-error.d.ts +16 -0
  29. package/dist/errors/mllp-invalid-response-error.d.ts.map +1 -0
  30. package/dist/errors/mllp-send-aborted-error.d.ts +12 -0
  31. package/dist/errors/mllp-send-aborted-error.d.ts.map +1 -0
  32. package/dist/errors/mllp-send-timeout-error.d.ts +13 -0
  33. package/dist/errors/mllp-send-timeout-error.d.ts.map +1 -0
  34. package/dist/errors/nak-exception.d.ts +17 -0
  35. package/dist/errors/nak-exception.d.ts.map +1 -0
  36. package/dist/events.d.ts +35 -0
  37. package/dist/events.d.ts.map +1 -0
  38. package/dist/index.d.ts +13 -20
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +805 -3
  41. package/dist/index.js.map +1 -0
  42. package/dist/messages.d.ts +55 -0
  43. package/dist/messages.d.ts.map +1 -0
  44. package/dist/reconnect.d.ts +18 -0
  45. package/dist/reconnect.d.ts.map +1 -0
  46. package/dist/runtime/node.d.ts +39 -0
  47. package/dist/runtime/node.d.ts.map +1 -0
  48. package/dist/runtime/node.js +100 -0
  49. package/dist/runtime/node.js.map +1 -0
  50. package/dist/session.d.ts +63 -0
  51. package/dist/session.d.ts.map +1 -0
  52. package/dist/state.d.ts +39 -0
  53. package/dist/state.d.ts.map +1 -0
  54. package/dist/types.d.ts +109 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/utils.d.ts +9 -0
  57. package/dist/utils.d.ts.map +1 -0
  58. package/package.json +22 -48
  59. package/dist/client.js +0 -549
  60. package/dist/client.js.map +0 -1
  61. package/dist/core/acknowledgment.d.ts +0 -103
  62. package/dist/core/acknowledgment.d.ts.map +0 -1
  63. package/dist/core/client.d.ts +0 -256
  64. package/dist/core/client.d.ts.map +0 -1
  65. package/dist/core/connect.d.ts +0 -101
  66. package/dist/core/connect.d.ts.map +0 -1
  67. package/dist/core/errors.d.ts +0 -81
  68. package/dist/core/errors.d.ts.map +0 -1
  69. package/dist/core/index.d.ts +0 -23
  70. package/dist/core/index.d.ts.map +0 -1
  71. package/dist/core/index.js +0 -2
  72. package/dist/core/internal/ack-parser-stream.d.ts +0 -22
  73. package/dist/core/internal/ack-parser-stream.d.ts.map +0 -1
  74. package/dist/core/internal/subscribe-abort.d.ts +0 -30
  75. package/dist/core/internal/subscribe-abort.d.ts.map +0 -1
  76. package/dist/node.js +0 -214
  77. package/dist/node.js.map +0 -1
  78. package/dist/runtimes/internal/node-error-mapping.d.ts +0 -68
  79. package/dist/runtimes/internal/node-error-mapping.d.ts.map +0 -1
  80. package/dist/runtimes/node.d.ts +0 -47
  81. package/dist/runtimes/node.d.ts.map +0 -1
  82. package/dist/runtimes/node.js +0 -3
  83. package/dist/runtimes/workers.d.ts +0 -64
  84. package/dist/runtimes/workers.d.ts.map +0 -1
  85. package/dist/runtimes/workers.js +0 -143
  86. package/dist/runtimes/workers.js.map +0 -1
package/README.md CHANGED
@@ -1,322 +1,497 @@
1
1
  # @glion/mllp-client
2
2
 
3
- MLLP client for sending HL7v2 messages and receiving acknowledgments.
3
+ A simple HL7v2 MLLP client for Node.js and Cloudflare Workers.
4
4
 
5
- ## What it does
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 at a time. No queue, 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 included. TLS, Cloudflare Workers or an in-memory socket plug in behind it.
10
+ - 🔤 **Typed end to end.** TypeScript throughout, with parsed HL7v2 going in and coming out.
6
11
 
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`.
12
+ > **Coming soon** — TLS and Cloudflare Workers adapters. Today the only bundled adapter is Node.js over TCP.
8
13
 
9
14
  ## Install
10
15
 
11
16
  ```bash
12
- npm install @glion/mllp-client @glion/ack
17
+ npm install @glion/mllp-client
13
18
  ```
14
19
 
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.
20
+ Node.js 22 or later. ESM only.
37
21
 
38
22
  ## Use
39
23
 
24
+ ### Send a message
25
+
40
26
  ```ts
27
+ import { parseHL7v2 } from "@glion/parser";
41
28
  import { MllpClient } from "@glion/mllp-client";
29
+ import { nodeSocket } from "@glion/mllp-client/node";
42
30
 
43
- // TLS-on by default — see "Plain TCP" below for opt-out
44
- const client = new MllpClient({ host: "mllp.example.com", port: 6661 });
31
+ await using client = new MllpClient({
32
+ socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
33
+ });
45
34
 
46
- const ack = await client.send(
35
+ const adt = parseHL7v2(
47
36
  [
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",
37
+ "MSH|^~\\&|SENDER|FAC|RECV|FAC|20260101120000||ADT^A01|MSG00001|P|2.5",
38
+ "EVN|A01|20260101120000",
39
+ "PID|1||12345^^^MRN||DOE^JOHN||19800101|M",
51
40
  ].join("\r")
52
41
  );
53
42
 
54
- console.log(ack.code); // "AA"
55
- console.log(ack.controlId); // "MSG001"
43
+ const ack = await client.send(adt);
44
+ ack.code; //=> "AA" or "CA"
45
+ ack.raw; //=> the acknowledgment as text
46
+ ack.tree; //=> the acknowledgment as a tree
56
47
  ```
57
48
 
58
- Catch a NAK from the receiver as a typed exception. The original raw ACK is available on `error.raw`:
49
+ `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
50
 
60
- ```ts
61
- import { AckApplicationError, AckApplicationReject } from "@glion/ack";
62
- import { MllpClient, MllpClientError } from "@glion/mllp-client";
51
+ ### Handle a rejection
63
52
 
64
- const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
53
+ 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.
54
+
55
+ ```ts
56
+ import { AckException } from "@glion/ack";
57
+ import { MllpClientError } from "@glion/mllp-client";
65
58
 
66
59
  try {
67
- await client.send(rawMessage);
60
+ await client.send(adt);
68
61
  } 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.
62
+ if (error instanceof AckException) {
63
+ error.code; //=> "AE", "AR", "CE", or "CR"
64
+ error.text; //=> the receiver's reason, when it gave one
75
65
  } 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.
66
+ error.code; //=> e.g. "SEND_TIMEOUT" — see Errors
78
67
  }
79
68
  }
80
69
  ```
81
70
 
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):
71
+ ### Connect at startup
72
+
73
+ `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
74
 
84
75
  ```ts
85
76
  const client = new MllpClient({
86
- host: "127.0.0.1",
87
- port: 2575,
88
- tls: false,
77
+ socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
89
78
  });
90
-
91
- const ack = await client.send(rawMessage);
79
+ await client.connect();
92
80
  ```
93
81
 
94
- Send over TLS with explicit configuration (mutual TLS, custom CA, etc.):
82
+ 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.
95
83
 
96
- ```ts
97
- import { readFileSync } from "node:fs";
84
+ ### Reconnect
85
+
86
+ 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.
98
87
 
88
+ 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.
89
+
90
+ ```ts
99
91
  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"),
92
+ socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
93
+ reconnect: {
94
+ attempts: 20,
95
+ delay: (attempt) => {
96
+ logger.warn({ attempt }, "mllp reconnecting");
97
+ return Math.min(30_000, 500 * 2 ** attempt);
98
+ },
106
99
  },
107
100
  });
108
-
109
- const ack = await client.send(rawMessage);
110
101
  ```
111
102
 
112
- The same code on **Cloudflare Workers**:
103
+ `reconnect: false` turns it off: the client dials once.
113
104
 
114
- ```ts
115
- import { MllpClient } from "@glion/mllp-client/workers";
105
+ ## Options
106
+
107
+ | Option | Type | Default | Description |
108
+ | ------------------ | ------------------------------- | --------- | ------------------------------------------------------------------------------------------------------ |
109
+ | `socket` | `MllpSocket` | required | Runtime adapter, such as `nodeSocket({ host, port })`. |
110
+ | `connectTimeoutMs` | `number` | `10000` | Time allowed to open the connection. Exceeded: `MllpConnectionTimeoutError`. |
111
+ | `sendTimeoutMs` | `number` | `30000` | Time allowed from writing a message to receiving its acknowledgment. Exceeded: `MllpSendTimeoutError`. |
112
+ | `maxBufferedBytes` | `number` | 16 MiB | Largest reply the client buffers. Exceeded: `MllpInvalidResponseError`. |
113
+ | `reconnect` | `MllpReconnectOptions \| false` | see below | How the client dials again after a failed attempt. `false` dials once. |
114
+
115
+ `send(message, { timeoutMs })` overrides `sendTimeoutMs` for one message.
116
+
117
+ `MllpReconnectOptions`:
118
+
119
+ | Option | Type | Default | Description |
120
+ | ---------- | ----------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------- |
121
+ | `attempts` | `number` | `5` | Attempts after a failed one before the client closes. `Infinity` for no limit. |
122
+ | `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. |
116
123
 
117
- const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
118
- const ack = await client.send(rawMessage);
124
+ ## API
125
+
126
+ ### `new MllpClient(options)`
127
+
128
+ ```ts
129
+ new MllpClient(options: MllpClientOptions): MllpClient
119
130
  ```
120
131
 
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:
132
+ Creates a client for one remote system. Nothing is opened until the first `connect()` or `send()`.
133
+
134
+ **Parameters** — see [Options](#options).
135
+
136
+ **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.
122
137
 
123
138
  ```ts
124
- import { MllpClient, MllpClientError } from "@glion/mllp-client/workers";
139
+ const client = new MllpClient({
140
+ socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
141
+ sendTimeoutMs: 10_000,
142
+ });
143
+ ```
125
144
 
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
- }
145
+ ### `client.send(message, options?)`
146
+
147
+ ```ts
148
+ send(message: Root, options?: { timeoutMs?: number }): Promise<MllpClientResponse>
138
149
  ```
139
150
 
140
- ## Options
151
+ Sends one message and resolves with the acknowledgment that answers it. Connects first if the client is not connected.
141
152
 
142
- `MllpClientOptions`:
153
+ | Parameter | Type | Description |
154
+ | ------------------- | -------- | -------------------------------------------------------- |
155
+ | `message` | `Root` | A parsed HL7v2 message. Must carry an MSH-10 control ID. |
156
+ | `options.timeoutMs` | `number` | Overrides `sendTimeoutMs` for this message only. |
143
157
 
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. |
158
+ **Returns** an `MllpClientResponse`:
151
159
 
152
- `MllpClientTlsOptions`:
160
+ | Field | Type | Description |
161
+ | ----------- | ---------------- | --------------------------------------------------- |
162
+ | `code` | `AckSuccessCode` | MSA-1, `AA` or `CA`. |
163
+ | `controlId` | `string` | MSA-2: the control ID of the message this answers. |
164
+ | `id` | `string` | The acknowledgment's own MSH-10. |
165
+ | `text` | `string?` | MSA-3, the receiver's diagnostic, when it gave one. |
166
+ | `tree` | `Root` | The acknowledgment, parsed. |
167
+ | `raw` | `string` | The acknowledgment as text. |
153
168
 
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. |
169
+ **Throws**
162
170
 
163
- ## API
171
+ - `AckException` — the receiver refused the message. The connection stays open.
172
+ - `MllpInvalidMessageError` — no MSH-10, or the message could not be serialized. Nothing was sent.
173
+ - `MllpInvalidOptionError` — `timeoutMs` is out of range. Nothing was sent.
174
+ - `MllpAlreadySendingError` — another send is in flight.
175
+ - `MllpClientClosedError` — the client is closed.
176
+ - `MllpConnectionFailedError`, `MllpConnectionTimeoutError` — the connection could not be opened.
177
+ - `MllpSendTimeoutError`, `MllpConnectionLostError`, `MllpInvalidResponseError` — the exchange failed. These close the connection; see [Errors](#errors).
164
178
 
165
- ### `new MllpClient(options)`
179
+ Every error carries `delivery`, `not-sent` or `unknown`, the one fact a retry needs; see [Errors](#errors).
166
180
 
167
- Construct a client bound to a host/port. The constructor does not open a connection — every `send()` call opens its own.
181
+ One message at a time. To send several, await each in turn:
168
182
 
169
183
  ```ts
170
- import { MllpClient } from "@glion/mllp-client";
184
+ for (const message of batch) {
185
+ const ack = await client.send(message);
186
+ record(ack.controlId, ack.code);
187
+ }
188
+ ```
189
+
190
+ ### `client.connect()`
171
191
 
172
- const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
173
- client.host; // "127.0.0.1"
174
- client.port; // 2575
192
+ ```ts
193
+ connect(): Promise<void>
175
194
  ```
176
195
 
177
- ### `client.send(message, options?)`
196
+ 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.
178
197
 
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.
198
+ Idempotent. On a connected client it resolves at once; while an attempt is in flight it waits for the outcome and shares it.
180
199
 
181
- On NAK (MSA-1 ∈ {AE, AR, CE, CR}) throws the matching `AckException` subclass with the original wire-format ACK on `error.raw`.
200
+ **Throws** the last attempt's `MllpConnectionFailedError` or `MllpConnectionTimeoutError`, or `MllpClientClosedError` when the client is already closed or `close()` cancelled the attempt.
182
201
 
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.
202
+ ### `client.close()`
184
203
 
185
204
  ```ts
186
- const ack = await client.send(rawHl7Message);
205
+ close(): Promise<void>
187
206
  ```
188
207
 
189
- `options` is optional:
208
+ Closes the connection once the message in flight has been acknowledged. New sends are refused from the moment it is called, so an in-flight message is never cut off. An attempt to connect stops at once.
190
209
 
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). |
210
+ Resolves when the connection is down, from any phase. Never throws. Idempotent. The wait is bounded by the in-flight send's own deadline.
195
211
 
196
- ### `client.stream(message, options?)`
212
+ ```ts
213
+ process.on("SIGTERM", async () => {
214
+ await client.close();
215
+ process.exit(0);
216
+ });
217
+ ```
218
+
219
+ ### `client.destroy()`
220
+
221
+ ```ts
222
+ destroy(): Promise<void>
223
+ ```
197
224
 
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()`.
225
+ Closes the connection now, without waiting for anything in flight. A message in flight rejects with `MllpSendAbortedError`. An attempt to connect stops at once.
199
226
 
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.
227
+ Resolves when the connection is down, from any phase. Never throws. Idempotent.
228
+
229
+ ### `client[Symbol.asyncDispose]()`
230
+
231
+ Calls `close()`. Lets a client be scoped with `await using`:
201
232
 
202
233
  ```ts
203
- for await (const ack of client.stream(rawHl7Message)) {
204
- log.info({ code: ack.code }, "ack received");
205
- }
234
+ {
235
+ await using client = new MllpClient({ socket });
236
+ await client.send(message);
237
+ } // closed here, even if send() threw
238
+ ```
239
+
240
+ ### `client.state`
241
+
242
+ ```ts
243
+ readonly state: MllpClientState
206
244
  ```
207
245
 
208
- ### `Acknowledgment`
246
+ | Value | Meaning |
247
+ | ------------ | --------------------------------------------------------------------------- |
248
+ | `idle` | Nothing opened yet. The first `send()` or `connect()` opens the connection. |
249
+ | `connecting` | The connection is being opened, further attempts included. |
250
+ | `connected` | Open, with no message in flight. |
251
+ | `sending` | A message is on the wire, waiting for its acknowledgment. |
252
+ | `closing` | `close()` is waiting out the message in flight. |
253
+ | `closed` | Done. |
254
+
255
+ 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.
209
256
 
210
- The HL7v2 acknowledgment returned by `client.send()`.
257
+ ### `client.connected`
211
258
 
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. |
259
+ ```ts
260
+ readonly connected: boolean
261
+ ```
221
262
 
222
- ### `MllpClientError`
263
+ `true` in `connected` and `sending`, `false` everywhere else.
223
264
 
224
- A subclass of `MllpError` from `@glion/mllp-transport`, thrown for transport-level failures. The `code` property identifies the failure mode:
265
+ ### `client.on(event, listener)` / `client.off(event, listener)`
225
266
 
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. |
267
+ ```ts
268
+ on<E>(event: E, listener: MllpClientListener<E>): this
269
+ off<E>(event: E, listener: MllpClientListener<E>): this
270
+ ```
235
271
 
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.
272
+ Adds or removes a listener. Both return the client, so calls chain.
237
273
 
238
- ### Application-level NAK exceptions
274
+ | Event | Listener | Fires when |
275
+ | --------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
276
+ | `connect` | `() => void` | The connection opened. |
277
+ | `close` | `(error: MllpClientError \| null) => void` | The client is done. Fires once, from any phase, even if it never connected. `error` is the failure it could not recover from, or `null` when you closed it. |
239
278
 
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:
279
+ 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.
241
280
 
242
281
  ```ts
243
- import { AckApplicationError, AckException } from "@glion/ack";
282
+ client
283
+ .on("connect", () => metrics.increment("mllp.connected"))
284
+ .on("close", (error) => {
285
+ logger.warn({ code: error?.code ?? "closed_by_owner" }, "mllp closed");
286
+ });
244
287
  ```
245
288
 
246
- Keeping a single import path keeps `@glion/ack` as the authoritative source for the exception hierarchy across both client and server code paths.
289
+ ## Runtimes
247
290
 
248
- ## Behavior
291
+ 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 new runtime means writing an adapter, not forking the client.
249
292
 
250
- ### NAK to exception mapping
293
+ Node.js is the only adapter that ships today.
251
294
 
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 |
295
+ ### Node.js
260
296
 
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`).
297
+ ```ts
298
+ import { nodeSocket } from "@glion/mllp-client/node";
299
+
300
+ nodeSocket(options: NodeSocketOptions): MllpSocket
301
+ ```
262
302
 
263
- ### Acknowledgment modes
303
+ Plain TCP over `net.Socket`.
264
304
 
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:
305
+ | Option | Type | Default | Description |
306
+ | ----------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------- |
307
+ | `host` | `string` | required | Host name or address of the receiver. |
308
+ | `port` | `number` | required | TCP port of the receiver. |
309
+ | `gracefulCloseMs` | `number` | `1000` | How long a socket gets to end cleanly before it is destroyed. |
310
+ | `keepAliveIdleMs` | `number` | `30000` | Idle time before the first keepalive probe, so a silent NAT or firewall drop surfaces before the next send. |
266
311
 
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.
312
+ `TCP_NODELAY` is set, so a message goes out immediately rather than waiting on Nagle's algorithm.
269
313
 
270
314
  ```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
315
+ const client = new MllpClient({
316
+ socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
317
+ });
318
+ ```
319
+
320
+ ### Custom Socket
321
+
322
+ You can expand `MllpSocket` to build a custom socket to one remote system, which the client opens, uses, and ends.
273
323
 
274
- // Resolve as soon as the receiver acknowledges receipt.
275
- const commit = await client.send(message, { mode: "OnCommit" }); // resolves with CA
324
+ ```ts
325
+ interface MllpSocket {
326
+ connect(signal: AbortSignal): Promise<MllpStreams>;
327
+ close(): Promise<void>;
328
+ }
329
+
330
+ interface MllpStreams {
331
+ readonly readable: ReadableStream<Uint8Array>;
332
+ readonly writable: WritableStream<Uint8Array>;
333
+ }
276
334
  ```
277
335
 
278
- ### Streaming acknowledgments
336
+ An implementation must satisfy four rules:
279
337
 
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.
338
+ 1. `connect()` rejects with `signal.reason` when the signal aborts, and a rejection leaves nothing open.
339
+ 2. `close()` never rejects, may be called more than once, and always finishes within a bounded time, even when the receiver never answers.
340
+ 3. When the socket ends, a pending read on `readable` ends or errors. Bytes sent before a clean close arrive first.
341
+ 4. The client owns the streams while connected, and releases them before calling `close()`.
342
+
343
+ An implementation never sees an MLLP frame — framing belongs to the layer above.
344
+
345
+ ## Errors
346
+
347
+ 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.
348
+
349
+ A rejection from the receiver is **not** an `MllpClientError` — see [`AckException`](#ackexception).
350
+
351
+ `delivery` is the one fact a retry decision needs:
352
+
353
+ | `delivery` | Meaning | Codes |
354
+ | ---------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
355
+ | `not-sent` | Nothing reached the wire. The message may be sent again as it is. | `INVALID_OPTION`, `INVALID_MESSAGE`, `ALREADY_SENDING`, `CLOSED`, `CONNECTION_FAILED`, `CONNECTION_TIMEOUT` |
356
+ | `unknown` | The message may have reached the receiver. Sending it again may deliver it twice. | `SEND_TIMEOUT`, `CONNECTION_LOST`, `SEND_ABORTED`, `INVALID_RESPONSE` |
281
357
 
282
358
  ```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
359
+ import { MllpClientError, MllpErrorCode } from "@glion/mllp-client";
360
+
361
+ try {
362
+ await client.send(message);
363
+ } catch (error) {
364
+ if (error instanceof MllpClientError) {
365
+ if (error.delivery === "not-sent") {
366
+ return requeue(message); // safe to send again
367
+ }
368
+ switch (error.code) {
369
+ case MllpErrorCode.INVALID_RESPONSE:
370
+ return quarantine(message); // the receiver answered, unreadably
371
+ default:
372
+ return holdForReview(message); // it may already have been processed
373
+ }
374
+ }
286
375
  }
287
376
  ```
288
377
 
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`.
378
+ 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".
379
+
380
+ ### `INVALID_OPTION`
381
+
382
+ `MllpInvalidOptionError` · delivery `not-sent`
383
+
384
+ 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`. The message names which one.
385
+
386
+ Thrown before anything is opened or sent. A configuration bug, not a runtime condition.
387
+
388
+ ### `INVALID_MESSAGE`
389
+
390
+ `MllpInvalidMessageError` · delivery `not-sent` · field: `cause`
391
+
392
+ 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.
393
+
394
+ **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.
395
+
396
+ ### `ALREADY_SENDING`
397
+
398
+ `MllpAlreadySendingError` · delivery `not-sent` · field: `controlId`
399
+
400
+ `send()` was called while another send was in flight. `controlId` identifies the message already on the wire.
401
+
402
+ The client is lockstep by design. Await each send before starting the next, or give each concurrent stream its own client.
403
+
404
+ ### `CLOSED`
405
+
406
+ `MllpClientClosedError` · delivery `not-sent` · field: `cause`
407
+
408
+ The client is closed, so the call cannot be served. Also the error a `connect()` gets when `close()` cancelled the attempt it was waiting for.
409
+
410
+ A client closes once. Construct a new one to send again. When the client closed because the reconnect policy gave up, `cause` is the last attempt's failure.
411
+
412
+ ### `CONNECTION_FAILED`
413
+
414
+ `MllpConnectionFailedError` · delivery `not-sent` · field: `cause`
415
+
416
+ The socket could not be opened. `cause` carries the underlying error — `ECONNREFUSED`, `ENOTFOUND`, `EHOSTUNREACH` and the like.
417
+
418
+ Check host, port, and whether a firewall allows the route. Nothing was opened, so there is nothing to close.
419
+
420
+ ### `CONNECTION_TIMEOUT`
421
+
422
+ `MllpConnectionTimeoutError` · delivery `not-sent` · field: `timeoutMs`
423
+
424
+ The receiver did not accept the connection within `connectTimeoutMs`.
425
+
426
+ Typically a packet-dropping firewall rather than a refused connection — a refusal arrives fast and surfaces as `CONNECTION_FAILED`.
427
+
428
+ ### `SEND_TIMEOUT`
429
+
430
+ `MllpSendTimeoutError` · delivery `unknown` · field: `timeoutMs` — **closes the connection**
431
+
432
+ No acknowledgment arrived within the send timeout.
433
+
434
+ 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.
435
+
436
+ ### `CONNECTION_LOST`
437
+
438
+ `MllpConnectionLostError` · delivery `unknown` · field: `cause` — **closes the connection**
439
+
440
+ The link went away mid-send: the receiver hung up, or the network broke. `cause` carries the stream error when there was one.
441
+
442
+ Whether the message was received is unknown. The client closes with this error; the message is not sent again.
443
+
444
+ ### `SEND_ABORTED`
445
+
446
+ `MllpSendAbortedError` · delivery `unknown`
447
+
448
+ `destroy()` cut off the message in flight. `close()` never raises this: it waits the message out.
449
+
450
+ ### `INVALID_RESPONSE`
451
+
452
+ `MllpInvalidResponseError` · delivery `unknown` · fields: `controlId?`, `cause` — **closes the connection**
453
+
454
+ The reply was not a usable acknowledgment of the message that was waiting. Causes, in the order they are checked:
455
+
456
+ - the bytes were not valid UTF-8 or not parseable HL7v2;
457
+ - MSA-2 names a different message — usually a late acknowledgment from an earlier timed-out send;
458
+ - MSA-1 is empty or is not one of the six codes of Table 0008;
459
+ - the reply passed `maxBufferedBytes` before it was complete;
460
+ - the connection closed partway through the reply.
461
+
462
+ The connection closes because the client can no longer tell which reply answers which message. The client closes with this error.
463
+
464
+ ### `AckException`
290
465
 
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.
466
+ From `@glion/ack` · fields: `code`, `controlId`, `text`, `errorCode`, `severity`
292
467
 
293
- ### Connection lifecycle
468
+ 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.
294
469
 
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.
470
+ | Field | Source | Description |
471
+ | ----------- | ------ | --------------------------------------------- |
472
+ | `code` | MSA-1 | `AE`, `AR`, `CE`, or `CR`. |
473
+ | `controlId` | MSA-2 | The message being refused. |
474
+ | `text` | MSA-3 | The receiver's reason, when it gave one. |
475
+ | `errorCode` | ERR-3 | HL7v2 Table 0357 error condition, when given. |
476
+ | `severity` | ERR-4 | `E`, `W`, or `I`, when given. |
296
477
 
297
- ### Concurrency
478
+ 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.
298
479
 
299
- Each `send()` is independent. Concurrent calls open independent connections; there is no shared state between in-flight requests.
480
+ ## FAQs
300
481
 
301
- ## Limitations
482
+ ### Why does a failed send close the connection?
302
483
 
303
- Things `@glion/mllp-client` deliberately does **not** do — call them out explicitly so callers can decide whether the client fits their integration:
484
+ 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.
304
485
 
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.
486
+ 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.
310
487
 
311
- ## Errors and PHI
488
+ 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.
312
489
 
313
- Thrown errors carry the full diagnostic context so callers can debug without reaching for additional state. **That context can include HL7v2 message content**:
490
+ 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.
314
491
 
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.
492
+ ### Why a socket rather than a host and port?
318
493
 
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.
494
+ 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
495
 
321
496
  ## Part of Glion
322
497