@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.
- package/README.md +370 -195
- package/dist/assertions.d.ts +10 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/client.d.ts +108 -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 +31 -0
- package/dist/errors/base.d.ts.map +1 -0
- package/dist/errors/index.d.ts +15 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/mllp-already-sending-error.d.ts +14 -0
- package/dist/errors/mllp-already-sending-error.d.ts.map +1 -0
- package/dist/errors/mllp-client-closed-error.d.ts +13 -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 +805 -3
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +55 -0
- package/dist/messages.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 +39 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +100 -0
- package/dist/runtime/node.js.map +1 -0
- package/dist/session.d.ts +63 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/state.d.ts +39 -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 +22 -48
- 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/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glion/mllp-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MLLP client for
|
|
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
|
-
|
|
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.
|
|
60
|
-
"@glion/ast": "0.
|
|
61
|
-
"@glion/mllp-
|
|
62
|
-
"@glion/
|
|
63
|
-
"@glion/
|
|
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.
|
|
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.
|
|
72
|
-
"
|
|
73
|
-
"@glion/
|
|
74
|
-
"@glion/
|
|
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": ">=
|
|
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
|
-
"
|
|
88
|
-
"test": "vitest run --
|
|
89
|
-
"test:
|
|
90
|
-
"
|
|
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
|