@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/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@glion/mllp-client",
3
- "version": "0.16.0",
4
- "description": "MLLP client for sending HL7v2 messages and receiving acknowledgments",
3
+ "version": "0.19.0",
4
+ "description": "Persistent, single-flight MLLP client for HL7v2",
5
5
  "keywords": [
6
- "client",
7
6
  "health",
8
7
  "healthcare",
9
8
  "hl7",
@@ -29,67 +28,42 @@
29
28
  "type": "module",
30
29
  "types": "./dist/index.d.ts",
31
30
  "exports": {
32
- ".": {
33
- "workerd": {
34
- "types": "./dist/runtimes/workers.d.ts",
35
- "import": "./dist/runtimes/workers.js"
36
- },
37
- "default": {
38
- "types": "./dist/index.d.ts",
39
- "import": "./dist/index.js"
40
- }
41
- },
42
- "./core": {
43
- "types": "./dist/core/index.d.ts",
44
- "import": "./dist/core/index.js"
45
- },
46
- "./node": {
47
- "types": "./dist/runtimes/node.d.ts",
48
- "import": "./dist/runtimes/node.js"
49
- },
50
- "./workers": {
51
- "types": "./dist/runtimes/workers.d.ts",
52
- "import": "./dist/runtimes/workers.js"
53
- }
31
+ ".": "./dist/index.js",
32
+ "./node": "./dist/runtime/node.js"
54
33
  },
55
34
  "publishConfig": {
56
35
  "access": "public"
57
36
  },
58
37
  "dependencies": {
59
- "@glion/ack": "0.16.0",
60
- "@glion/ast": "0.16.0",
61
- "@glion/mllp-transport": "0.16.0",
62
- "@glion/util-query": "0.16.0",
63
- "@glion/parser": "0.16.0"
38
+ "@glion/ack": "0.19.0",
39
+ "@glion/ast": "0.19.0",
40
+ "@glion/mllp-codec": "0.19.0",
41
+ "@glion/to-hl7v2": "0.19.0",
42
+ "@glion/util-charset": "0.19.0",
43
+ "@glion/parser": "0.19.0",
44
+ "@glion/util-query": "0.19.0"
64
45
  },
65
46
  "devDependencies": {
66
47
  "@types/node": "25.6.0",
67
- "@vitest/coverage-v8": "4.1.5",
48
+ "@vitest/coverage-v8": "4.1.11",
68
49
  "publint": "0.3.18",
69
50
  "tsdown": "0.21.10",
70
51
  "typescript": "6.0.3",
71
- "vitest": "4.1.5",
72
- "wrangler": "^4.86.0",
73
- "@glion/hl7v2": "0.16.0",
74
- "@glion/mllp": "0.16.0",
75
- "@glion/mllp-ack": "0.16.0",
76
- "@glion/testing": "0.16.0",
77
- "@glion/tsconfig": "0.16.0",
78
- "@glion/check-readme": "0.16.0"
52
+ "vitest": "4.1.11",
53
+ "@glion/check-readme": "0.18.0",
54
+ "@glion/testing": "0.18.0",
55
+ "@glion/tsconfig": "0.18.0"
79
56
  },
80
57
  "engines": {
81
- "node": ">=20"
58
+ "node": ">=22"
82
59
  },
83
60
  "scripts": {
84
61
  "build": "tsdown && tsc --emitDeclarationOnly",
85
- "check-types": "tsc --noEmit",
62
+ "check-types": "tsc --noEmit && tsc --noEmit -p tests/tsconfig.json",
86
63
  "check:readme": "glion-check-readme",
87
- "publint": "publint --strict",
88
- "test": "vitest run --project 'hl7v2-mllp-client (node)'",
89
- "test:bun": "bun --bun test test/core.test.ts",
90
- "test:cf": "vitest run --project 'hl7v2-mllp-client (workerd)'",
91
- "test:coverage": "vitest run --coverage --project 'hl7v2-mllp-client (node)'",
92
- "test:node": "vitest run --project 'hl7v2-mllp-client (node)'",
93
- "test:watch": "vitest"
64
+ "test": "vitest run",
65
+ "test:coverage": "vitest run --coverage",
66
+ "test:watch": "vitest",
67
+ "publint": "publint --strict"
94
68
  }
95
69
  }
package/dist/client.js DELETED
@@ -1,549 +0,0 @@
1
- import { AckApplicationError, AckApplicationReject, AckCode, AckCommitError, AckCommitReject, AckException, Hl7ErrorCode, Severity } from "@glion/ack";
2
- import { MllpError, createDecoderStream, encode } from "@glion/mllp-transport";
3
- import { parseHL7v2 } from "@glion/parser";
4
- import { value } from "@glion/util-query";
5
- //#region src/core/errors.ts
6
- /**
7
- * Machine-readable codes for `MllpClient` transport failures.
8
- *
9
- * These cover everything that can go wrong between the moment
10
- * `MllpClient.send()` is called and the moment a complete ACK frame
11
- * has been decoded — i.e. everything that is not an HL7v2
12
- * application-level NAK. NAK responses (MSA-1 ∈ {AE, AR, CE, CR})
13
- * surface as `AckException` subclasses thrown directly from
14
- * `@glion/ack`, not as transport errors.
15
- */
16
- const MllpClientErrorCode = {
17
- /** No ACK arrived within the configured `timeout`. */
18
- /** The peer closed the connection before a complete ACK arrived. */
19
- CONNECTION_CLOSED: "CONNECTION_CLOSED",
20
- /** TCP connection could not be established (refused, DNS, routing). */
21
- CONNECTION_REFUSED: "CONNECTION_REFUSED",
22
- /**
23
- * The caller passed a payload that could not be MLLP-encoded —
24
- * typically because it was not a `string` or `Uint8Array`. Also
25
- * surfaced when constructor options fail validation (bad
26
- * host/port/timeout/etc). Thrown before any socket is opened, so
27
- * no connection is made and no resources need cleanup.
28
- */
29
- INVALID_INPUT: "INVALID_INPUT",
30
- MALFORMED_ACK: "MALFORMED_ACK",
31
- /** The remote sent bytes that did not form a valid MLLP frame. */
32
- MALFORMED_FRAME: "MALFORMED_FRAME",
33
- /** A frame was received but could not be parsed as a valid HL7v2 ACK. */
34
- TIMEOUT: "TIMEOUT",
35
- /**
36
- * TLS handshake failed — typically because of certificate
37
- * verification, hostname mismatch, expired cert, untrusted CA, or
38
- * a TLS protocol error. Distinct from `CONNECTION_REFUSED` (the
39
- * TCP connection succeeded but the TLS upgrade failed) so
40
- * operators can identify TLS misconfiguration quickly in
41
- * healthcare deployments where it is a recurring incident class.
42
- */
43
- TLS_HANDSHAKE_FAILED: "TLS_HANDSHAKE_FAILED"
44
- };
45
- /**
46
- * Error thrown by `MllpClient.send()` for transport-level failures.
47
- *
48
- * Extends {@link MllpError} so callers can catch any MLLP error —
49
- * server- or client-side — with a single `instanceof MllpError`
50
- * check. The {@link MllpClientErrorCode | code} field discriminates
51
- * the failure mode for typed handling.
52
- *
53
- * Application-level NAK responses are **not** thrown as
54
- * `MllpClientError`; they are thrown as `AckException` subclasses
55
- * from `@glion/ack`. A typical `catch` block handles both:
56
- *
57
- * ```ts
58
- * import { AckException } from "@glion/ack";
59
- * import { MllpClientError } from "@glion/mllp-client";
60
- *
61
- * try {
62
- * await client.send(message);
63
- * } catch (error) {
64
- * if (error instanceof AckException) {
65
- * // Application-level NAK.
66
- * } else if (error instanceof MllpClientError) {
67
- * // Transport-level failure.
68
- * }
69
- * }
70
- * ```
71
- */
72
- var MllpClientError = class extends MllpError {
73
- constructor(code, message, options) {
74
- super(code, message, options);
75
- this.name = "MllpClientError";
76
- }
77
- };
78
- //#endregion
79
- //#region src/core/acknowledgment.ts
80
- /**
81
- * Acknowledgment parsing for the MLLP client.
82
- *
83
- * This module owns three things:
84
- *
85
- * 1. The {@link Acknowledgment} interface — a structured view of an ACK message
86
- * exposing the fields callers most often need (MSA-1/2/3, ERR-3/4) without
87
- * forcing them to walk the AST themselves.
88
- * 2. {@link parseAck} — turns a raw wire-format ACK string into an
89
- * {@link Acknowledgment}, or throws {@link MllpClientError} when the payload
90
- * is not a recognisable ACK.
91
- * 3. {@link throwOnNak} — inspects an {@link Acknowledgment} and throws the
92
- * matching `AckException` subclass when MSA-1 indicates a NAK.
93
- *
94
- * Both helpers are internal to the package — only the {@link Acknowledgment}
95
- * type is re-exported from `@glion/mllp-client`. Application code interacts
96
- * with parsed acknowledgments through the return value (and thrown errors)
97
- * of `MllpClient.send()`.
98
- *
99
- * The HL7v2 ACK structure used here (Section 2.9.2 of the HL7v2 standard):
100
- *
101
- * MSH | header (sender, receiver, timestamp, message control id) MSA |
102
- * acknowledgment code (MSA-1), original control id (MSA-2), text (MSA-3) ERR |
103
- * optional error segment with HL7 condition code (ERR-3) and severity (ERR-4)
104
- *
105
- * @module
106
- */
107
- /**
108
- * MSA-1 codes that mean "the receiver accepted the message". Anything
109
- * outside this set is a NAK and produces a thrown `AckException`.
110
- *
111
- * See HL7v2 Table 0008 — Acknowledgment Codes.
112
- */
113
- const SUCCESS_CODES = new Set([AckCode.ApplicationAccept, AckCode.CommitAccept]);
114
- /**
115
- * MSA-1 codes that are **intermediate** in HL7v2 enhanced
116
- * acknowledgment mode — the receiver will follow up with a separate
117
- * final acknowledgment later in the exchange. The MLLP client's read
118
- * loop continues past these by default and only resolves on a final
119
- * code.
120
- *
121
- * Per HL7v2 Section 2.9.2, only `CA` (Commit Accept) is intermediate;
122
- * `CE` and `CR` are final at the commit level. `AA`, `AE`, `AR` are
123
- * final at the application level.
124
- */
125
- const INTERMEDIATE_ACK_CODES = new Set([AckCode.CommitAccept]);
126
- /**
127
- * Returns `true` for any acknowledgment code that should terminate the
128
- * client's read loop. Intermediate codes (CA) return `false`; every
129
- * other code — including vendor-specific values — is treated as final
130
- * to avoid hanging on responses we do not recognise.
131
- */
132
- function isFinalAckCode(code) {
133
- return !INTERMEDIATE_ACK_CODES.has(code);
134
- }
135
- /**
136
- * Parse a raw ACK frame string into an {@link Acknowledgment}.
137
- *
138
- * Field extraction is delegated to `@glion/util-query`'s `value()` helper —
139
- * this is the same path used elsewhere in the Glion stack and avoids
140
- * hand-rolling AST traversal here.
141
- *
142
- * Throws `MllpClientError(MALFORMED_ACK)` when:
143
- *
144
- * - The parser fails to produce a tree (malformed HL7v2), or
145
- * - MSA-1 is missing (the message is not a valid acknowledgment).
146
- *
147
- * Other missing fields (MSA-3, ERR-3, ERR-4) are not fatal — they are
148
- * exposed as `undefined` because real-world ACKs frequently omit them.
149
- */
150
- function parseAck(raw) {
151
- const tree = parseTreeOrThrow(raw);
152
- const code = value(tree, "MSA-1")?.value;
153
- if (!code) throw new MllpClientError(MllpClientErrorCode.MALFORMED_ACK, "ACK response is missing MSA-1 acknowledgment code");
154
- return {
155
- code,
156
- controlId: value(tree, "MSA-2")?.value ?? "",
157
- errorCode: value(tree, "ERR-3")?.value ?? void 0,
158
- raw,
159
- severity: value(tree, "ERR-4")?.value ?? void 0,
160
- textMessage: value(tree, "MSA-3")?.value ?? void 0,
161
- tree
162
- };
163
- }
164
- /**
165
- * Run the HL7v2 parser, wrapping any thrown error in
166
- * `MllpClientError(MALFORMED_ACK)` so callers see one error shape
167
- * regardless of which lower-level component failed.
168
- */
169
- function parseTreeOrThrow(raw) {
170
- try {
171
- return parseHL7v2(raw);
172
- } catch (error) {
173
- throw new MllpClientError(MllpClientErrorCode.MALFORMED_ACK, `Failed to parse ACK response: ${formatCause(error)}`, { cause: error });
174
- }
175
- }
176
- /**
177
- * Inspect an {@link Acknowledgment} and throw the matching `AckException`
178
- * subclass when MSA-1 indicates a NAK. Returns the acknowledgment unchanged
179
- * on AA/CA.
180
- *
181
- * The thrown exception mirrors the receiver-side exception types — a
182
- * caller that already handles `AckApplicationError` from a server's
183
- * middleware can reuse the same `catch` blocks here.
184
- */
185
- function throwOnNak(ack) {
186
- if (SUCCESS_CODES.has(ack.code)) return ack;
187
- throw buildAckException(ack);
188
- }
189
- /**
190
- * Convert a NAK acknowledgment into an `AckException` subclass.
191
- *
192
- * The fallbacks below preserve a useful exception even when the receiver
193
- * sent a sparse NAK: a missing MSA-3 yields a synthesised message; a
194
- * missing ERR segment yields the catch-all `ApplicationInternalError`
195
- * code (`207`) and severity Error (`E`). This matches the receiver-side
196
- * conventions in `@glion/ack`.
197
- */
198
- function buildAckException(ack) {
199
- const message = ack.textMessage ?? `Acknowledgment ${ack.code} from receiver`;
200
- const options = {
201
- errorCode: coerceErrorCode(ack.errorCode),
202
- raw: ack.raw,
203
- severity: coerceSeverity(ack.severity)
204
- };
205
- switch (ack.code) {
206
- case AckCode.ApplicationError: return new AckApplicationError(message, options);
207
- case AckCode.ApplicationReject: return new AckApplicationReject(message, options);
208
- case AckCode.CommitError: return new AckCommitError(message, options);
209
- case AckCode.CommitReject: return new AckCommitReject(message, options);
210
- default: return new AckApplicationError(message, options);
211
- }
212
- }
213
- /**
214
- * Set of HL7 condition codes (Table 0357) we recognise. Built once from
215
- * the `Hl7ErrorCode` enum so the lookup stays cheap and the membership
216
- * stays in sync with the source of truth in `@glion/ack`.
217
- */
218
- const KNOWN_ERROR_CODES = new Set(Object.values(Hl7ErrorCode));
219
- /**
220
- * Set of severity codes (Table 0516) we recognise.
221
- */
222
- const KNOWN_SEVERITIES = new Set(Object.values(Severity));
223
- /**
224
- * Map an ERR-3 string to a typed {@link Hl7ErrorCodeValue}. Unknown or
225
- * missing values fall back to `ApplicationInternalError` (`207`) so the
226
- * exception always carries a well-defined code, mirroring receiver-side
227
- * conventions in `@glion/ack`.
228
- *
229
- * The widening cast is intentional and confined here — receivers
230
- * occasionally send vendor-specific codes outside Table 0357, and the
231
- * client must still produce a usable exception rather than reject the
232
- * NAK over an unknown code.
233
- */
234
- function coerceErrorCode(input) {
235
- if (input !== void 0 && KNOWN_ERROR_CODES.has(input)) return input;
236
- return Hl7ErrorCode.ApplicationInternalError;
237
- }
238
- /**
239
- * Map an ERR-4 string to a typed {@link SeverityValue}. Unknown or
240
- * missing values fall back to `Severity.Error` (`E`).
241
- */
242
- function coerceSeverity(input) {
243
- if (input !== void 0 && KNOWN_SEVERITIES.has(input)) return input;
244
- return Severity.Error;
245
- }
246
- /** Render an unknown thrown value as a human-readable string for error messages. */
247
- function formatCause(error) {
248
- return error instanceof Error ? error.message : String(error);
249
- }
250
- //#endregion
251
- //#region src/core/internal/ack-parser-stream.ts
252
- /**
253
- * Build a transform that consumes decoded MLLP frames and produces
254
- * parsed `Acknowledgment` objects. A parse failure errors the
255
- * readable side rather than enqueuing — the consumer's `for await`
256
- * loop rejects with the typed error.
257
- */
258
- function createAckParserStream() {
259
- return new TransformStream({ transform(frame, controller) {
260
- try {
261
- controller.enqueue(parseAck(frame.text));
262
- } catch (error) {
263
- controller.error(error);
264
- }
265
- } });
266
- }
267
- //#endregion
268
- //#region src/core/internal/subscribe-abort.ts
269
- /**
270
- * Subscribe to an `AbortSignal` and run a callback once if it
271
- * aborts. Returns a disposer that unsubscribes — call it from a
272
- * `finally` block so we never leak listeners.
273
- *
274
- * If the signal is already aborted at subscription time, the
275
- * callback fires synchronously and the returned disposer is a
276
- * no-op. This collapses the "aborted vs aborting" branch every
277
- * caller would otherwise need to write by hand.
278
- *
279
- * Internal to `@glion/mllp-client`.
280
- *
281
- * @module
282
- */
283
- /**
284
- * Subscribe `onAbort` to fire once when `signal` aborts. Returns a
285
- * disposer that removes the listener.
286
- *
287
- * @example
288
- * ```ts
289
- * const dispose = subscribeAbort(signal, () => socket.destroy());
290
- * try {
291
- * await waitForReady(socket);
292
- * } finally {
293
- * dispose();
294
- * }
295
- * ```;
296
- */
297
- function subscribeAbort(signal, onAbort) {
298
- if (signal.aborted) {
299
- onAbort();
300
- return noop;
301
- }
302
- signal.addEventListener("abort", onAbort, { once: true });
303
- return () => signal.removeEventListener("abort", onAbort);
304
- }
305
- function noop() {}
306
- //#endregion
307
- //#region src/core/client.ts
308
- /**
309
- * Runtime-free core of `@glion/mllp-client`.
310
- *
311
- * This module owns {@link MllpClient}, the class application code
312
- * instantiates to send HL7v2 messages and receive acknowledgments.
313
- * The class is **transport-agnostic**: it composes a Web-Streams
314
- * pipeline (`socket → decoder → ack-parser`) and consumes parsed
315
- * acknowledgments from the readable side. It does **not** know how
316
- * to open a TCP/TLS connection. That concern is supplied by a
317
- * {@link MllpConnect} function passed in via
318
- * {@link MllpClientOptions.connect}. Per-runtime adapters
319
- * (`@glion/mllp-client/node`, `/deno`, `/workers`) ship pre-wired
320
- * `MllpClient` subclasses so the common case stays a single import.
321
- *
322
- * Each call to `client.send()` performs one short-lived round trip:
323
- *
324
- * ┌─────────────┐ ① connect ┌──────────┐
325
- * │ client │ ───────────────▶ │ duplex │
326
- * │ │ └────┬─────┘
327
- * │ │ ② write MLLP frame │
328
- * │ │ ─────────────────────▶│ <VT> ... <FS><CR>
329
- * │ │ │
330
- * │ │ ③ read frames via │
331
- * │ │ decoder + parser │
332
- * │ │ ◀─────────────────────│ <VT> ACK <FS><CR> ...
333
- * │ │ ┌────┴─────┐
334
- * │ │ ④ close + parse │ duplex │ closed
335
- * └─────────────┘ └──────────┘
336
- *
337
- * The four phases share a single deadline ({@link MllpClientOptions.timeout}).
338
- * If any phase exceeds the budget, the duplex is closed and a typed
339
- * `MllpClientError(TIMEOUT)` is thrown. Connections are deliberately
340
- * **not** pooled or reused — each `send()` opens its own duplex.
341
- *
342
- * @module
343
- */
344
- /** Default `timeout` when {@link MllpClientOptions.timeout} is omitted. */
345
- const DEFAULT_TIMEOUT_MS = 3e4;
346
- /**
347
- * Runtime-free MLLP client. Instantiate via a runtime adapter
348
- * (`@glion/mllp-client/node`, `/deno`, `/workers`) for the common
349
- * case.
350
- *
351
- * **TLS-on by default.** `MllpClientOptions.tls` defaults to `true`;
352
- * pass `tls: false` for plain TCP. See {@link MllpClientOptions.tls}.
353
- *
354
- * @example
355
- * Send a message and read the ACK on Node:
356
- *
357
- * ```ts
358
- * import { MllpClient } from "@glion/mllp-client/node";
359
- *
360
- * const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
361
- * const ack = await client.send(rawHl7Message);
362
- * console.log(ack.code, ack.controlId); // "AA" "MSG001"
363
- * ```
364
- */
365
- var MllpClient = class {
366
- #host;
367
- #port;
368
- #timeout;
369
- #maxAckSize;
370
- #tls;
371
- #connect;
372
- constructor(options) {
373
- validateOptions(options);
374
- this.#host = options.host;
375
- this.#port = options.port;
376
- this.#connect = options.connect;
377
- this.#timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
378
- this.#maxAckSize = options.maxAckSize;
379
- if (options.tls === false) this.#tls = void 0;
380
- else if (options.tls === true || options.tls === void 0) this.#tls = {};
381
- else this.#tls = options.tls;
382
- }
383
- /** The host this client connects to. */
384
- get host() {
385
- return this.#host;
386
- }
387
- /** The port this client connects to. */
388
- get port() {
389
- return this.#port;
390
- }
391
- /**
392
- * Send a single HL7v2 message and resolve with the resolving accept ACK.
393
- *
394
- * Throws:
395
- *
396
- * - {@link MllpClientError} for transport failures. `code` identifies the mode:
397
- * `INVALID_INPUT`, `CONNECTION_REFUSED`, `TIMEOUT`, `CONNECTION_CLOSED`,
398
- * `MALFORMED_FRAME`, or `MALFORMED_ACK`.
399
- * - An `AckException` subclass from `@glion/ack` (`AckApplicationError`,
400
- * `AckApplicationReject`, `AckCommitError`, `AckCommitReject`) when MSA-1 ∈
401
- * {AE, AR, CE, CR}. The thrown exception's `raw` attribute holds the
402
- * wire-format ACK.
403
- *
404
- * To observe each accept ACK as it arrives (e.g. seeing the
405
- * intermediate `CA` frame in HL7v2 enhanced mode), use
406
- * {@link MllpClient.stream} instead.
407
- */
408
- async send(message, options = {}) {
409
- let last;
410
- for await (const ack of this.#exchange(message, options)) last = ack;
411
- if (!last) throw new MllpClientError(MllpClientErrorCode.CONNECTION_CLOSED, "Connection closed before a complete ACK was received");
412
- return last;
413
- }
414
- /**
415
- * Send a single HL7v2 message and yield each accept ACK as it
416
- * arrives. The iterable completes after the resolving frame.
417
- *
418
- * In HL7v2 enhanced mode the iterable yields `CA` then the
419
- * application-level ACK; in basic mode it yields a single
420
- * application-level ACK. NAK codes (`AE`/`AR`/`CE`/`CR`) throw the
421
- * matching `AckException` from the iterator with the wire-format
422
- * ACK on `error.raw`.
423
- *
424
- * Use {@link MllpClient.send} for the simple "give me the resolving
425
- * answer" case.
426
- */
427
- stream(message, options = {}) {
428
- return this.#exchange(message, options);
429
- }
430
- /**
431
- * Internal exchange generator shared by {@link send} and {@link stream}.
432
- *
433
- * Owns the connect → write → read loop. NAK codes throw
434
- * `AckException` instead of yielding so both consumption shapes see
435
- * the same exception. Cleanup (socket teardown, abort
436
- * unsubscription) runs in the generator's `finally`, so a consumer
437
- * that breaks out of iteration still releases the socket.
438
- *
439
- * @yields Each accept ACK in arrival order. `return`s after the
440
- * resolving frame.
441
- */
442
- async *#exchange(message, options) {
443
- const frame = encodeOrThrow(message);
444
- const mode = options.mode ?? "OnApplication";
445
- const sources = [AbortSignal.timeout(this.#timeout)];
446
- if (options.signal) sources.push(options.signal);
447
- const signal = AbortSignal.any(sources);
448
- const duplex = await this.#connect({
449
- host: this.#host,
450
- port: this.#port,
451
- signal,
452
- tls: this.#tls
453
- });
454
- const reader = duplex.readable.pipeThrough(createDecoderStream({
455
- maxMessageSize: this.#maxAckSize,
456
- onError: (frameError) => {
457
- throw new MllpClientError(MllpClientErrorCode.MALFORMED_FRAME, `Invalid ACK frame: ${frameError.message}`, { cause: frameError });
458
- }
459
- })).pipeThrough(createAckParserStream()).getReader();
460
- const writer = duplex.writable.getWriter();
461
- const unsubscribe = subscribeAbort(signal, () => {
462
- duplex.close();
463
- });
464
- try {
465
- await writer.write(frame);
466
- while (true) {
467
- const { done, value: ack } = await reader.read();
468
- if (done) throw new MllpClientError(MllpClientErrorCode.CONNECTION_CLOSED, "Connection closed before a complete ACK was received");
469
- throwOnNak(ack);
470
- yield ack;
471
- if (mode === "OnCommit" || isFinalAckCode(ack.code)) return;
472
- }
473
- } catch (error) {
474
- if (error instanceof AckException) throw error;
475
- throw normaliseSendError(error, signal, this.#timeout);
476
- } finally {
477
- unsubscribe();
478
- await duplex.close();
479
- }
480
- }
481
- };
482
- /**
483
- * MLLP-encode a payload, mapping any failure to a typed
484
- * {@link MllpClientError} with code `INVALID_INPUT`.
485
- */
486
- function encodeOrThrow(message) {
487
- try {
488
- return encode(message);
489
- } catch (error) {
490
- throw new MllpClientError(MllpClientErrorCode.INVALID_INPUT, `Invalid message payload: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : void 0 });
491
- }
492
- }
493
- /**
494
- * Translate a thrown value from any phase of {@link readAck} into a
495
- * typed client error.
496
- *
497
- * Precedence:
498
- *
499
- * 1. If the signal aborted with a typed {@link MllpClientError} reason, surface it
500
- * verbatim. (Connector or pipeline-internal failures keep their original
501
- * code.)
502
- * 2. Else if the signal aborted with the standard timeout `DOMException` produced
503
- * by `AbortSignal.timeout(ms)`, wrap as `TIMEOUT` with the original budget
504
- * in the message.
505
- * 3. Else if the signal aborted with any other reason (a caller's own
506
- * `AbortController.abort(reason)`), wrap as `TIMEOUT` with the caller's
507
- * reason chained as `cause` — caller cancellation conceptually IS a timeout
508
- * from the protocol's perspective.
509
- * 4. Else if the underlying stream rejection is already typed, return it unchanged
510
- * so adapter-specific failures are preserved.
511
- * 5. Otherwise wrap as `CONNECTION_CLOSED`.
512
- */
513
- function normaliseSendError(error, signal, timeoutMs) {
514
- if (signal.aborted) {
515
- if (signal.reason instanceof MllpClientError) return signal.reason;
516
- if (isTimeoutAbort(signal.reason)) return new MllpClientError(MllpClientErrorCode.TIMEOUT, `MLLP round trip exceeded ${timeoutMs}ms`);
517
- return new MllpClientError(MllpClientErrorCode.TIMEOUT, "Send aborted by caller", { cause: signal.reason instanceof Error ? signal.reason : void 0 });
518
- }
519
- if (error instanceof MllpClientError) return error;
520
- const wrapped = error instanceof Error ? error : new Error(String(error));
521
- return new MllpClientError(MllpClientErrorCode.CONNECTION_CLOSED, `Connection failed during MLLP exchange: ${wrapped.message}`, { cause: wrapped });
522
- }
523
- /**
524
- * Detect the standard `DOMException` produced by
525
- * `AbortSignal.timeout(ms)` when the timer fires. The runtime
526
- * convention is `name: "TimeoutError"` — duck-typed because
527
- * `DOMException` exists in Node, Bun, Deno, and Workers but tests
528
- * run in environments where the constructor identity may differ.
529
- */
530
- function isTimeoutAbort(reason) {
531
- return reason !== null && typeof reason === "object" && reason.name === "TimeoutError";
532
- }
533
- /**
534
- * Validate {@link MllpClientOptions} at construction time. Catches
535
- * the sharp edges that would otherwise surface as confusing runtime
536
- * symptoms.
537
- */
538
- function validateOptions(options) {
539
- if (typeof options.host !== "string" || options.host.length === 0) throw new MllpClientError(MllpClientErrorCode.INVALID_INPUT, "MllpClientOptions.host must be a non-empty string");
540
- if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) throw new MllpClientError(MllpClientErrorCode.INVALID_INPUT, `MllpClientOptions.port must be an integer in 1..65535, got ${options.port}`);
541
- if (typeof options.connect !== "function") throw new MllpClientError(MllpClientErrorCode.INVALID_INPUT, "MllpClientOptions.connect must be a function — import the adapter for your runtime (`@glion/mllp-client/node`, `/deno`, `/workers`)");
542
- if (options.timeout !== void 0 && (!Number.isFinite(options.timeout) || options.timeout <= 0)) throw new MllpClientError(MllpClientErrorCode.INVALID_INPUT, `MllpClientOptions.timeout must be a positive finite number of milliseconds, got ${options.timeout}`);
543
- if (options.maxAckSize !== void 0 && (!Number.isInteger(options.maxAckSize) || options.maxAckSize <= 0)) throw new MllpClientError(MllpClientErrorCode.INVALID_INPUT, `MllpClientOptions.maxAckSize must be a positive integer, got ${options.maxAckSize}`);
544
- if (options.tls !== void 0 && typeof options.tls !== "boolean" && (typeof options.tls !== "object" || options.tls === null)) throw new MllpClientError(MllpClientErrorCode.INVALID_INPUT, "MllpClientOptions.tls must be `true`, `false`, or an MllpClientTlsOptions object");
545
- }
546
- //#endregion
547
- export { MllpClientErrorCode as i, subscribeAbort as n, MllpClientError as r, MllpClient as t };
548
-
549
- //# sourceMappingURL=client.js.map