@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/dist/index.js
CHANGED
|
@@ -1,3 +1,805 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { MllpCodecError, frame, unframe } from "@glion/mllp-codec";
|
|
2
|
+
import { AckApplicationError, AckApplicationReject, AckCode, AckCommitError, AckCommitReject, isAckNakCode, isAckSuccessCode } from "@glion/ack";
|
|
3
|
+
import { value } from "@glion/util-query";
|
|
4
|
+
import { parseHL7v2 } from "@glion/parser";
|
|
5
|
+
import { toHl7v2 } from "@glion/to-hl7v2";
|
|
6
|
+
import { decodeBytes, encodeBytes } from "@glion/util-charset";
|
|
7
|
+
/**
|
|
8
|
+
* Maximum timeout `setTimeout` supports, in milliseconds (2^31 − 1). Larger
|
|
9
|
+
* values, including `Infinity`, are rejected: the platform would clamp them
|
|
10
|
+
* to about 1 ms.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/errors/base.ts
|
|
15
|
+
/**
|
|
16
|
+
* The base every client error extends, and what each one carries: a fixed
|
|
17
|
+
* `code`, a fixed message, what became of the message, and the lower layer's
|
|
18
|
+
* failure on `cause`.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
const MllpErrorCode = {
|
|
23
|
+
ALREADY_SENDING: "ALREADY_SENDING",
|
|
24
|
+
CLOSED: "CLOSED",
|
|
25
|
+
CONNECTION_FAILED: "CONNECTION_FAILED",
|
|
26
|
+
CONNECTION_LOST: "CONNECTION_LOST",
|
|
27
|
+
CONNECTION_TIMEOUT: "CONNECTION_TIMEOUT",
|
|
28
|
+
INVALID_MESSAGE: "INVALID_MESSAGE",
|
|
29
|
+
INVALID_OPTION: "INVALID_OPTION",
|
|
30
|
+
INVALID_RESPONSE: "INVALID_RESPONSE",
|
|
31
|
+
SEND_ABORTED: "SEND_ABORTED",
|
|
32
|
+
SEND_TIMEOUT: "SEND_TIMEOUT"
|
|
33
|
+
};
|
|
34
|
+
var MllpClientError = class extends Error {};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/errors/mllp-already-sending-error.ts
|
|
37
|
+
/**
|
|
38
|
+
* `send()` was called while another message was still waiting for its
|
|
39
|
+
* acknowledgment. The client sends one message at a time.
|
|
40
|
+
*/
|
|
41
|
+
var MllpAlreadySendingError = class extends MllpClientError {
|
|
42
|
+
name = "MllpAlreadySendingError";
|
|
43
|
+
code = MllpErrorCode.ALREADY_SENDING;
|
|
44
|
+
delivery = "not-sent";
|
|
45
|
+
/** MSH-10 of the message that is still waiting. */
|
|
46
|
+
controlId;
|
|
47
|
+
constructor(controlId) {
|
|
48
|
+
super("Cannot send: another message is still waiting for its acknowledgment — await the in-flight send() first.");
|
|
49
|
+
this.controlId = controlId;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/errors/mllp-client-closed-error.ts
|
|
54
|
+
/**
|
|
55
|
+
* The call cannot be served: the client is closed, by `close()`, by
|
|
56
|
+
* `destroy()`, or by a lost connection the reconnect policy did not restore.
|
|
57
|
+
* That last failure is on `cause`.
|
|
58
|
+
*/
|
|
59
|
+
var MllpClientClosedError = class extends MllpClientError {
|
|
60
|
+
name = "MllpClientClosedError";
|
|
61
|
+
code = MllpErrorCode.CLOSED;
|
|
62
|
+
delivery = "not-sent";
|
|
63
|
+
constructor(cause = null) {
|
|
64
|
+
super(cause === null ? "The client is closed — construct a new MllpClient to send again." : "The client closed after its connection could not be restored — construct a new MllpClient to send again.", { cause: cause ?? void 0 });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/errors/mllp-connection-error.ts
|
|
69
|
+
/**
|
|
70
|
+
* A failure of the wire: the connection could not be opened, gave no reply in
|
|
71
|
+
* time, or was lost. Raised by the layer that holds the socket; the client
|
|
72
|
+
* relays it unchanged.
|
|
73
|
+
*/
|
|
74
|
+
var MllpConnectionError = class extends MllpClientError {};
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/errors/mllp-connection-failed-error.ts
|
|
77
|
+
/**
|
|
78
|
+
* The connection could not be opened: the host was unreachable, refused the
|
|
79
|
+
* connection, or failed DNS or TLS. The socket's error is on `cause`.
|
|
80
|
+
*/
|
|
81
|
+
var MllpConnectionFailedError = class extends MllpConnectionError {
|
|
82
|
+
name = "MllpConnectionFailedError";
|
|
83
|
+
code = MllpErrorCode.CONNECTION_FAILED;
|
|
84
|
+
delivery = "not-sent";
|
|
85
|
+
constructor(cause) {
|
|
86
|
+
super("Connecting failed — check that the host is reachable and the port is listening.", { cause });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/errors/mllp-connection-timeout-error.ts
|
|
91
|
+
/**
|
|
92
|
+
* The remote system did not accept the connection within `connectTimeoutMs`.
|
|
93
|
+
* The host may be down, overloaded, or silently dropping packets.
|
|
94
|
+
*/
|
|
95
|
+
var MllpConnectionTimeoutError = class extends MllpConnectionError {
|
|
96
|
+
name = "MllpConnectionTimeoutError";
|
|
97
|
+
code = MllpErrorCode.CONNECTION_TIMEOUT;
|
|
98
|
+
delivery = "not-sent";
|
|
99
|
+
timeoutMs;
|
|
100
|
+
constructor(timeoutMs) {
|
|
101
|
+
super("Connecting timed out — check that the host is reachable and the port is listening.");
|
|
102
|
+
this.timeoutMs = timeoutMs;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/errors/mllp-connection-lost-error.ts
|
|
107
|
+
/**
|
|
108
|
+
* The connection was lost during a send: the remote system hung up, or the
|
|
109
|
+
* network broke. The remote system may or may not have received the message.
|
|
110
|
+
* A stream error, when there was one, is on `cause`.
|
|
111
|
+
*/
|
|
112
|
+
var MllpConnectionLostError = class extends MllpConnectionError {
|
|
113
|
+
name = "MllpConnectionLostError";
|
|
114
|
+
code = MllpErrorCode.CONNECTION_LOST;
|
|
115
|
+
delivery = "unknown";
|
|
116
|
+
constructor(cause) {
|
|
117
|
+
super("The connection was lost while a message was waiting for its acknowledgment — the remote system may or may not have received it; resend only if the message is safe to repeat.", { cause });
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/errors/mllp-invalid-message-error.ts
|
|
122
|
+
/**
|
|
123
|
+
* The message cannot be sent as-is: it has no MSH-10 control ID, it could not
|
|
124
|
+
* be serialized, or it contains a byte MLLP reserves. Nothing was written; the
|
|
125
|
+
* reason is on `cause`. Fix the message and send again.
|
|
126
|
+
*/
|
|
127
|
+
var MllpInvalidMessageError = class extends MllpClientError {
|
|
128
|
+
name = "MllpInvalidMessageError";
|
|
129
|
+
code = MllpErrorCode.INVALID_MESSAGE;
|
|
130
|
+
delivery = "not-sent";
|
|
131
|
+
constructor(cause) {
|
|
132
|
+
super("The message could not be prepared for sending; nothing was written.", { cause });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/errors/mllp-invalid-option-error.ts
|
|
137
|
+
/** An option is out of range. Raised before anything happens. */
|
|
138
|
+
var MllpInvalidOptionError = class extends MllpClientError {
|
|
139
|
+
name = "MllpInvalidOptionError";
|
|
140
|
+
code = MllpErrorCode.INVALID_OPTION;
|
|
141
|
+
delivery = "not-sent";
|
|
142
|
+
};
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/errors/mllp-invalid-response-error.ts
|
|
145
|
+
/**
|
|
146
|
+
* The reply was not a usable acknowledgment of the message that was waiting:
|
|
147
|
+
* it could not be decoded, its acknowledgment code was missing or unknown, or
|
|
148
|
+
* it answered a different message. The connection is ended. The reason is on
|
|
149
|
+
* `cause`.
|
|
150
|
+
*/
|
|
151
|
+
var MllpInvalidResponseError = class extends MllpClientError {
|
|
152
|
+
name = "MllpInvalidResponseError";
|
|
153
|
+
code = MllpErrorCode.INVALID_RESPONSE;
|
|
154
|
+
delivery = "unknown";
|
|
155
|
+
/** MSH-10 of the message that was waiting, when the thrower knew it. */
|
|
156
|
+
controlId;
|
|
157
|
+
constructor(cause, controlId) {
|
|
158
|
+
super("The reply is not a usable acknowledgment of the message that was waiting — the connection has been closed.", { cause });
|
|
159
|
+
this.controlId = controlId;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/utils.ts
|
|
164
|
+
/** The value at `path`, or `""` when the field is absent. */
|
|
165
|
+
function read(tree, path) {
|
|
166
|
+
return value(tree, path)?.value ?? "";
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/errors/nak-exception.ts
|
|
170
|
+
/**
|
|
171
|
+
* The exception a NAK carries. Not an {@link MllpClientError}: the remote
|
|
172
|
+
* system understood the message and refused it, and the class is the one
|
|
173
|
+
* `@glion/ack` defines for that MSA-1 code.
|
|
174
|
+
*
|
|
175
|
+
* @module
|
|
176
|
+
*/
|
|
177
|
+
/** The `@glion/ack` exception class for each NAK code. */
|
|
178
|
+
const NAK_EXCEPTIONS = {
|
|
179
|
+
[AckCode.ApplicationError]: AckApplicationError,
|
|
180
|
+
[AckCode.ApplicationReject]: AckApplicationReject,
|
|
181
|
+
[AckCode.CommitError]: AckCommitError,
|
|
182
|
+
[AckCode.CommitReject]: AckCommitReject
|
|
183
|
+
};
|
|
184
|
+
/** MSA-3, ERR-3, and ERR-4 as one sentence, or a note that none were given. */
|
|
185
|
+
function nakMessage(code, nak) {
|
|
186
|
+
const said = [];
|
|
187
|
+
if (nak.text !== void 0) said.push(nak.text);
|
|
188
|
+
if (nak.errorCode !== void 0) said.push(`ERR-3 ${nak.errorCode}`);
|
|
189
|
+
if (nak.severity !== void 0) said.push(`ERR-4 ${nak.severity}`);
|
|
190
|
+
const reason = said.length === 0 ? "It gave no reason." : `${said.join("; ")}.`;
|
|
191
|
+
return `The remote system did not accept message ${nak.controlId} (MSA-1 ${code}). ${reason}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The exception a NAK of `code` carries, read from `tree`.
|
|
195
|
+
*
|
|
196
|
+
* `code` picks the class; MSA-2 gives `controlId`, ERR-3 `errorCode`, ERR-4
|
|
197
|
+
* `severity`, and MSA-3 (or ERR-8) `text`.
|
|
198
|
+
*/
|
|
199
|
+
function nakException(tree, code) {
|
|
200
|
+
const nak = {
|
|
201
|
+
controlId: read(tree, "MSA-2[1].1.1"),
|
|
202
|
+
errorCode: read(tree, "ERR-3[1].1.1") || void 0,
|
|
203
|
+
severity: read(tree, "ERR-4[1].1.1") || void 0,
|
|
204
|
+
text: read(tree, "MSA-3[1].1.1") || read(tree, "ERR-8[1].1.1") || void 0
|
|
205
|
+
};
|
|
206
|
+
return new NAK_EXCEPTIONS[code](nakMessage(code, nak), nak);
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/errors/mllp-send-aborted-error.ts
|
|
210
|
+
/**
|
|
211
|
+
* `destroy()` cut off the message in flight. The remote system may or may not
|
|
212
|
+
* have received it.
|
|
213
|
+
*/
|
|
214
|
+
var MllpSendAbortedError = class extends MllpClientError {
|
|
215
|
+
name = "MllpSendAbortedError";
|
|
216
|
+
code = MllpErrorCode.SEND_ABORTED;
|
|
217
|
+
delivery = "unknown";
|
|
218
|
+
constructor() {
|
|
219
|
+
super("The send was cut off by destroy() — the remote system may or may not have received the message; resend only if the message is safe to repeat.");
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/errors/mllp-send-timeout-error.ts
|
|
224
|
+
/**
|
|
225
|
+
* No acknowledgment arrived within the send timeout. The connection is ended.
|
|
226
|
+
* Whether the remote system received the message is unknown.
|
|
227
|
+
*/
|
|
228
|
+
var MllpSendTimeoutError = class extends MllpConnectionError {
|
|
229
|
+
name = "MllpSendTimeoutError";
|
|
230
|
+
code = MllpErrorCode.SEND_TIMEOUT;
|
|
231
|
+
delivery = "unknown";
|
|
232
|
+
timeoutMs;
|
|
233
|
+
constructor(timeoutMs) {
|
|
234
|
+
super("No acknowledgment arrived within the send timeout — the connection has been closed, because a late acknowledgment could not be matched safely.");
|
|
235
|
+
this.timeoutMs = timeoutMs;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/assertions.ts
|
|
240
|
+
/**
|
|
241
|
+
* Option validation. Each `assert*` throws {@link MllpInvalidOptionError} for
|
|
242
|
+
* an option a caller passed out of range, before anything happens.
|
|
243
|
+
*
|
|
244
|
+
* @module
|
|
245
|
+
*/
|
|
246
|
+
function assertTimeoutMs(option, ms) {
|
|
247
|
+
if (!Number.isFinite(ms) || ms <= 0 || ms > MAX_TIMEOUT_MS) throw new MllpInvalidOptionError(`${option} must be a number of milliseconds between 1 and ${MAX_TIMEOUT_MS}.`);
|
|
248
|
+
}
|
|
249
|
+
function assertByteCap(bytes) {
|
|
250
|
+
if (!Number.isInteger(bytes) || bytes <= 0) throw new MllpInvalidOptionError("maxBufferedBytes must be a positive integer.");
|
|
251
|
+
}
|
|
252
|
+
function assertReconnectAttempts(attempts) {
|
|
253
|
+
if (!(attempts === Number.POSITIVE_INFINITY) && (!Number.isInteger(attempts) || attempts < 0)) throw new MllpInvalidOptionError("reconnect.attempts must be a non-negative integer, or Infinity.");
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/events.ts
|
|
257
|
+
/**
|
|
258
|
+
* The listeners of a client.
|
|
259
|
+
*
|
|
260
|
+
* A listener that throws propagates to whoever triggered the event, the same
|
|
261
|
+
* as Node's `EventEmitter`.
|
|
262
|
+
*/
|
|
263
|
+
var MllpClientEmitter = class {
|
|
264
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
265
|
+
/** Adds a listener for `event`, and returns the client so calls chain. */
|
|
266
|
+
on(event, listener) {
|
|
267
|
+
const listeners = this.#listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
268
|
+
listeners.add(listener);
|
|
269
|
+
this.#listeners.set(event, listeners);
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
/** Removes a listener added with {@link on}. */
|
|
273
|
+
off(event, listener) {
|
|
274
|
+
this.#listeners.get(event)?.delete(listener);
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
/** Calls every listener for `event`, in the order they were added. */
|
|
278
|
+
emit(event, ...args) {
|
|
279
|
+
for (const listener of this.#listeners.get(event) ?? []) listener(...args);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/messages.ts
|
|
284
|
+
/**
|
|
285
|
+
* Encoding a message to send, and decoding the acknowledgment that answers it.
|
|
286
|
+
* Pure.
|
|
287
|
+
*
|
|
288
|
+
* @module
|
|
289
|
+
*/
|
|
290
|
+
/**
|
|
291
|
+
* `tree` as the message to send, in canonical HL7v2.
|
|
292
|
+
*
|
|
293
|
+
* @throws {MllpInvalidMessageError} The message has no MSH-10, or it could not
|
|
294
|
+
* be serialized or encoded.
|
|
295
|
+
*/
|
|
296
|
+
function encode(tree) {
|
|
297
|
+
const controlId = read(tree, "MSH-10[1].1.1");
|
|
298
|
+
if (controlId === "") throw new MllpInvalidMessageError("the message has no MSH-10 control ID, so its acknowledgment could not be matched");
|
|
299
|
+
try {
|
|
300
|
+
return {
|
|
301
|
+
bytes: encodeBytes(toHl7v2(tree)),
|
|
302
|
+
controlId
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
throw new MllpInvalidMessageError(error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* `bytes` as the reply to the message `controlId` identifies.
|
|
310
|
+
*
|
|
311
|
+
* MSA-2 is checked before MSA-1 is read: an accept or a NAK is reported only
|
|
312
|
+
* for the message it answers. MSH-9 and the HL7 version are not checked
|
|
313
|
+
* (#668).
|
|
314
|
+
*
|
|
315
|
+
* Never throws.
|
|
316
|
+
*/
|
|
317
|
+
function decode(bytes, controlId) {
|
|
318
|
+
let raw;
|
|
319
|
+
let tree;
|
|
320
|
+
try {
|
|
321
|
+
raw = decodeBytes(bytes);
|
|
322
|
+
tree = parseHL7v2(raw);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return invalid(error, controlId);
|
|
325
|
+
}
|
|
326
|
+
const answers = read(tree, "MSA-2[1].1.1");
|
|
327
|
+
if (answers !== controlId) return invalid(`MSA-2 is "${answers}", so it answers a different message — usually a late acknowledgment from an earlier timed-out send.`, controlId);
|
|
328
|
+
const code = read(tree, "MSA-1[1].1.1");
|
|
329
|
+
if (isAckNakCode(code)) return {
|
|
330
|
+
exception: nakException(tree, code),
|
|
331
|
+
type: "nak"
|
|
332
|
+
};
|
|
333
|
+
if (!isAckSuccessCode(code)) return invalid(code === "" ? "MSA-1 is empty, so accept or reject cannot be determined." : `MSA-1 is "${code}", which is not one of AA, AE, AR, CA, CE, or CR.`, controlId);
|
|
334
|
+
return {
|
|
335
|
+
response: {
|
|
336
|
+
code,
|
|
337
|
+
controlId: answers,
|
|
338
|
+
id: read(tree, "MSH-10[1].1.1"),
|
|
339
|
+
raw,
|
|
340
|
+
text: read(tree, "MSA-3[1].1.1") || void 0,
|
|
341
|
+
tree
|
|
342
|
+
},
|
|
343
|
+
type: "accept"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function invalid(reason, controlId) {
|
|
347
|
+
return {
|
|
348
|
+
error: new MllpInvalidResponseError(reason, controlId),
|
|
349
|
+
type: "invalid"
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/reconnect.ts
|
|
354
|
+
/** Ceiling of the first default wait, in milliseconds. Doubles per attempt. */
|
|
355
|
+
const BACKOFF_BASE_MS = 1e3;
|
|
356
|
+
/** Ceiling of every default wait, in milliseconds. */
|
|
357
|
+
const BACKOFF_CAP_MS = 3e4;
|
|
358
|
+
/**
|
|
359
|
+
* Full-jitter exponential backoff: a wait drawn uniformly from zero to
|
|
360
|
+
* `min(30 s, 1 s × 2^(attempt − 1))`.
|
|
361
|
+
*/
|
|
362
|
+
function defaultReconnectDelay(attempt) {
|
|
363
|
+
const ceiling = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** (attempt - 1));
|
|
364
|
+
return Math.floor(Math.random() * ceiling);
|
|
365
|
+
}
|
|
366
|
+
/** Resolves after `ms`, or at once when `signal` aborts first. Never rejects. */
|
|
367
|
+
function sleep(ms, signal) {
|
|
368
|
+
if (signal.aborted) return Promise.resolve();
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
const done = () => {
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
signal.removeEventListener("abort", done);
|
|
373
|
+
resolve();
|
|
374
|
+
};
|
|
375
|
+
const timer = setTimeout(done, ms);
|
|
376
|
+
signal.addEventListener("abort", done, { once: true });
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/session.ts
|
|
381
|
+
/**
|
|
382
|
+
* One session over a socket: the message layer over its bytes. A message
|
|
383
|
+
* written here is framed, and bytes read here are unframed.
|
|
384
|
+
*
|
|
385
|
+
* @module
|
|
386
|
+
*/
|
|
387
|
+
/**
|
|
388
|
+
* Opens `socket` and takes ownership of its streams.
|
|
389
|
+
*
|
|
390
|
+
* @throws {MllpConnectionTimeoutError} No socket within `timeoutMs`.
|
|
391
|
+
* @throws {MllpConnectionFailedError} The socket failed to open, or `signal`
|
|
392
|
+
* cancelled the attempt; the adapter's rejection is on `cause`.
|
|
393
|
+
*/
|
|
394
|
+
async function openSocketStreams(socket, signal, opts) {
|
|
395
|
+
const timeout = AbortSignal.timeout(opts.timeoutMs);
|
|
396
|
+
let streams;
|
|
397
|
+
try {
|
|
398
|
+
streams = await socket.connect(AbortSignal.any([signal, timeout]));
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (timeout.aborted) throw new MllpConnectionTimeoutError(opts.timeoutMs);
|
|
401
|
+
throw new MllpConnectionFailedError(error);
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
return {
|
|
405
|
+
reader: streams.readable.pipeThrough(unframe({ maxBufferedBytes: opts.maxBufferedBytes })).getReader(),
|
|
406
|
+
writer: streams.writable.getWriter()
|
|
407
|
+
};
|
|
408
|
+
} catch (error) {
|
|
409
|
+
await socket.close();
|
|
410
|
+
throw new MllpConnectionFailedError(error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Starts opening a session over `socket`, and hands it back at once. The
|
|
415
|
+
* session ends itself when `signal` aborts.
|
|
416
|
+
*
|
|
417
|
+
* Never throws: every failure, an adapter's synchronous throw included,
|
|
418
|
+
* reaches `ready`. Nothing is left open on any path: an attempt that fails or
|
|
419
|
+
* is cancelled closes the socket before `ready` rejects.
|
|
420
|
+
*/
|
|
421
|
+
function createSession(socket, opts, signal) {
|
|
422
|
+
const abort = new AbortController();
|
|
423
|
+
const streams = openSocketStreams(socket, abort.signal, opts);
|
|
424
|
+
let closing = null;
|
|
425
|
+
const cancel = () => void destroy();
|
|
426
|
+
const ready = (async () => {
|
|
427
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
428
|
+
try {
|
|
429
|
+
await streams;
|
|
430
|
+
} catch (error) {
|
|
431
|
+
signal?.removeEventListener("abort", cancel);
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
if (closing !== null) {
|
|
435
|
+
await closing;
|
|
436
|
+
throw new MllpConnectionFailedError(abort.signal.reason);
|
|
437
|
+
}
|
|
438
|
+
})();
|
|
439
|
+
const destroy = () => {
|
|
440
|
+
closing ??= (async () => {
|
|
441
|
+
abort.abort();
|
|
442
|
+
signal?.removeEventListener("abort", cancel);
|
|
443
|
+
const [opened] = await Promise.allSettled([streams]);
|
|
444
|
+
if (opened.status === "fulfilled") {
|
|
445
|
+
opened.value.reader.releaseLock();
|
|
446
|
+
opened.value.writer.releaseLock();
|
|
447
|
+
await socket.close();
|
|
448
|
+
}
|
|
449
|
+
})();
|
|
450
|
+
return closing;
|
|
451
|
+
};
|
|
452
|
+
const exchange = async (message, timeoutMs) => {
|
|
453
|
+
let framed;
|
|
454
|
+
try {
|
|
455
|
+
framed = frame(message);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
throw new MllpInvalidMessageError(error);
|
|
458
|
+
}
|
|
459
|
+
await ready;
|
|
460
|
+
let timedOut = false;
|
|
461
|
+
const deadline = setTimeout(() => {
|
|
462
|
+
timedOut = true;
|
|
463
|
+
destroy();
|
|
464
|
+
}, timeoutMs);
|
|
465
|
+
try {
|
|
466
|
+
const { reader, writer } = await streams;
|
|
467
|
+
await writer.write(framed);
|
|
468
|
+
const reply = await reader.read();
|
|
469
|
+
if (reply.done) throw new MllpConnectionLostError();
|
|
470
|
+
return reply.value;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if (timedOut) throw new MllpSendTimeoutError(timeoutMs);
|
|
473
|
+
if (abort.signal.aborted) throw new MllpSendAbortedError();
|
|
474
|
+
throw error instanceof MllpCodecError || error instanceof MllpConnectionLostError ? error : new MllpConnectionLostError(error);
|
|
475
|
+
} finally {
|
|
476
|
+
clearTimeout(deadline);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
return {
|
|
480
|
+
destroy,
|
|
481
|
+
exchange,
|
|
482
|
+
ready
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/client.ts
|
|
487
|
+
/**
|
|
488
|
+
* Sends HL7v2 messages to one remote system over MLLP and returns each
|
|
489
|
+
* message's acknowledgment.
|
|
490
|
+
*
|
|
491
|
+
* One client owns one socket and sends one message at a time. `send()` writes
|
|
492
|
+
* the message, waits for the acknowledgment, checks that it answers this
|
|
493
|
+
* message, and resolves with it.
|
|
494
|
+
*
|
|
495
|
+
* ```ts
|
|
496
|
+
* import { MllpClient } from "@glion/mllp-client";
|
|
497
|
+
* import { nodeSocket } from "@glion/mllp-client/node";
|
|
498
|
+
*
|
|
499
|
+
* await using client = new MllpClient({
|
|
500
|
+
* socket: nodeSocket({ host: "hl7.example.org", port: 2575 }),
|
|
501
|
+
* });
|
|
502
|
+
*
|
|
503
|
+
* const ack = await client.send(adtMessage);
|
|
504
|
+
* ack.code; // "AA" or "CA"
|
|
505
|
+
* ```
|
|
506
|
+
*
|
|
507
|
+
* ## Lifecycle
|
|
508
|
+
*
|
|
509
|
+
* `send()` connects on first use; `connect()` opens the connection ahead of
|
|
510
|
+
* time. `state` reports the phase. A connection that fails to open is dialed
|
|
511
|
+
* again under the `reconnect` policy. A connection that is lost closes the
|
|
512
|
+
* client: the message in flight rejects and is not sent again. After
|
|
513
|
+
* `close()`, or once the client has closed on a failure, every call rejects
|
|
514
|
+
* with {@link MllpClientClosedError}.
|
|
515
|
+
*
|
|
516
|
+
* ## Errors
|
|
517
|
+
*
|
|
518
|
+
* When the remote system answers `AE`, `AR`, `CE`, or `CR`, `send()` rejects
|
|
519
|
+
* with the matching `@glion/ack` exception and the connection stays open.
|
|
520
|
+
*/
|
|
521
|
+
var MllpClient = class extends MllpClientEmitter {
|
|
522
|
+
#socket;
|
|
523
|
+
#connect;
|
|
524
|
+
#policy;
|
|
525
|
+
#sendTimeoutMs;
|
|
526
|
+
#state = { phase: "idle" };
|
|
527
|
+
/**
|
|
528
|
+
* @throws {MllpInvalidOptionError} A timeout, byte cap, or attempt count is
|
|
529
|
+
* out of range.
|
|
530
|
+
*/
|
|
531
|
+
constructor(opts) {
|
|
532
|
+
super();
|
|
533
|
+
const connectTimeoutMs = opts.connectTimeoutMs ?? 1e4;
|
|
534
|
+
const maxBufferedBytes = opts.maxBufferedBytes ?? 16777216;
|
|
535
|
+
const attempts = opts.reconnect === false ? 0 : opts.reconnect?.attempts ?? 5;
|
|
536
|
+
const delay = opts.reconnect === false ? defaultReconnectDelay : opts.reconnect?.delay ?? defaultReconnectDelay;
|
|
537
|
+
this.#sendTimeoutMs = opts.sendTimeoutMs ?? 3e4;
|
|
538
|
+
assertTimeoutMs("connectTimeoutMs", connectTimeoutMs);
|
|
539
|
+
assertTimeoutMs("sendTimeoutMs", this.#sendTimeoutMs);
|
|
540
|
+
assertByteCap(maxBufferedBytes);
|
|
541
|
+
assertReconnectAttempts(attempts);
|
|
542
|
+
this.#socket = opts.socket;
|
|
543
|
+
this.#connect = {
|
|
544
|
+
maxBufferedBytes,
|
|
545
|
+
timeoutMs: connectTimeoutMs
|
|
546
|
+
};
|
|
547
|
+
this.#policy = {
|
|
548
|
+
attempts,
|
|
549
|
+
delay
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* The current phase: `idle`, `connecting`, `connected`, `sending`,
|
|
554
|
+
* `closing`, or `closed`.
|
|
555
|
+
*/
|
|
556
|
+
get state() {
|
|
557
|
+
return this.#state.phase;
|
|
558
|
+
}
|
|
559
|
+
/** Whether the connection is open. True in both `connected` and `sending`. */
|
|
560
|
+
get connected() {
|
|
561
|
+
const { phase } = this.#state;
|
|
562
|
+
return phase === "connected" || phase === "sending";
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Opens the connection ahead of the first `send()`, dialing again under the
|
|
566
|
+
* `reconnect` policy when an attempt fails.
|
|
567
|
+
*
|
|
568
|
+
* Idempotent: a connected client resolves at once, and a call arriving while
|
|
569
|
+
* an attempt is in flight waits for that attempt and shares its outcome.
|
|
570
|
+
*
|
|
571
|
+
* @throws {MllpConnectionFailedError} The last attempt could not open the
|
|
572
|
+
* socket.
|
|
573
|
+
* @throws {MllpConnectionTimeoutError} The last attempt did not open in time.
|
|
574
|
+
* @throws {MllpClientClosedError} The client is closed, or `close()` arrived
|
|
575
|
+
* while the connection was still being established.
|
|
576
|
+
*/
|
|
577
|
+
async connect() {
|
|
578
|
+
const state = this.#state;
|
|
579
|
+
switch (state.phase) {
|
|
580
|
+
case "connected":
|
|
581
|
+
case "sending": return;
|
|
582
|
+
case "connecting":
|
|
583
|
+
await state.opening;
|
|
584
|
+
return;
|
|
585
|
+
case "closing": throw new MllpClientClosedError();
|
|
586
|
+
case "closed": throw new MllpClientClosedError(state.reason);
|
|
587
|
+
case "idle": {
|
|
588
|
+
const abort = new AbortController();
|
|
589
|
+
const opening = this.#dial(abort.signal);
|
|
590
|
+
this.#state = {
|
|
591
|
+
abort,
|
|
592
|
+
opening,
|
|
593
|
+
phase: "connecting"
|
|
594
|
+
};
|
|
595
|
+
await opening;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Sends one message and returns its acknowledgment. Connects first when the
|
|
601
|
+
* client is not connected yet.
|
|
602
|
+
*
|
|
603
|
+
* Nothing is written when an option, phase, or message error is thrown.
|
|
604
|
+
* Every other failure also closes the client.
|
|
605
|
+
*
|
|
606
|
+
* @throws {AckException} The remote system answered with a NAK. The
|
|
607
|
+
* connection stays open.
|
|
608
|
+
* @throws {MllpInvalidOptionError} `timeoutMs` is out of range.
|
|
609
|
+
* @throws {MllpInvalidMessageError} The message cannot be sent as-is.
|
|
610
|
+
* @throws {MllpClientClosedError} The client is closed.
|
|
611
|
+
* @throws {MllpAlreadySendingError} Another send is in flight.
|
|
612
|
+
* @throws {MllpConnectionFailedError} The connection could not be opened.
|
|
613
|
+
* @throws {MllpConnectionTimeoutError} The connection did not open in time.
|
|
614
|
+
* @throws {MllpSendTimeoutError} No acknowledgment arrived in time.
|
|
615
|
+
* @throws {MllpConnectionLostError} The connection was lost.
|
|
616
|
+
* @throws {MllpSendAbortedError} `destroy()` cut the send off.
|
|
617
|
+
* @throws {MllpInvalidResponseError} The reply is not a usable
|
|
618
|
+
* acknowledgment of this message.
|
|
619
|
+
*/
|
|
620
|
+
async send(message, opts = {}) {
|
|
621
|
+
const timeoutMs = opts.timeoutMs ?? this.#sendTimeoutMs;
|
|
622
|
+
assertTimeoutMs("timeoutMs", timeoutMs);
|
|
623
|
+
const { bytes, controlId } = encode(message);
|
|
624
|
+
if (!this.connected) await this.connect();
|
|
625
|
+
const state = this.#state;
|
|
626
|
+
switch (state.phase) {
|
|
627
|
+
case "sending": throw new MllpAlreadySendingError(state.controlId);
|
|
628
|
+
case "closing": throw new MllpClientClosedError();
|
|
629
|
+
case "closed": throw new MllpClientClosedError(state.reason);
|
|
630
|
+
case "idle":
|
|
631
|
+
case "connecting": throw new Error(`send() found the client ${state.phase} after it connected. This is a bug in @glion/mllp-client; please report it.`);
|
|
632
|
+
case "connected": break;
|
|
633
|
+
}
|
|
634
|
+
const done = this.#send(state.session, controlId, bytes, timeoutMs);
|
|
635
|
+
this.#state = {
|
|
636
|
+
controlId,
|
|
637
|
+
done,
|
|
638
|
+
phase: "sending",
|
|
639
|
+
session: state.session
|
|
640
|
+
};
|
|
641
|
+
return await done;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Ends the connection once the message in flight is acknowledged, and
|
|
645
|
+
* resolves when it is down. New sends are refused from the moment this is
|
|
646
|
+
* called. An attempt to connect stops at once.
|
|
647
|
+
*
|
|
648
|
+
* Resolves from any phase. Never rejects. Idempotent. The wait is bounded by
|
|
649
|
+
* the in-flight send's own deadline; {@link destroy} does not wait at all.
|
|
650
|
+
*/
|
|
651
|
+
async close() {
|
|
652
|
+
const from = this.#state;
|
|
653
|
+
if (from.phase === "sending") {
|
|
654
|
+
this.#state = {
|
|
655
|
+
done: from.done,
|
|
656
|
+
phase: "closing",
|
|
657
|
+
session: from.session
|
|
658
|
+
};
|
|
659
|
+
await Promise.allSettled([from.done]);
|
|
660
|
+
}
|
|
661
|
+
await this.destroy();
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Ends the connection now, and resolves when it is down. A message in flight
|
|
665
|
+
* rejects with {@link MllpSendAbortedError}. An attempt to connect stops at
|
|
666
|
+
* once.
|
|
667
|
+
*
|
|
668
|
+
* Resolves from any phase. Never rejects. Idempotent.
|
|
669
|
+
*/
|
|
670
|
+
async destroy() {
|
|
671
|
+
const from = this.#state;
|
|
672
|
+
this.#closed(null);
|
|
673
|
+
switch (from.phase) {
|
|
674
|
+
case "idle":
|
|
675
|
+
case "closed": return;
|
|
676
|
+
case "connecting":
|
|
677
|
+
from.abort.abort();
|
|
678
|
+
await Promise.allSettled([from.opening]);
|
|
679
|
+
return;
|
|
680
|
+
case "connected":
|
|
681
|
+
case "sending":
|
|
682
|
+
case "closing": await from.session.destroy();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/** Calls {@link close}. Enables `await using`. */
|
|
686
|
+
async [Symbol.asyncDispose]() {
|
|
687
|
+
await this.close();
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Dials under the policy until a session opens, then moves to `connected`
|
|
691
|
+
* and reports `connect`. Each attempt after the first waits
|
|
692
|
+
* `delay(attempt)` first. Once the policy gives up, closes the client with
|
|
693
|
+
* the last attempt's failure.
|
|
694
|
+
*
|
|
695
|
+
* @throws {MllpClientClosedError} `signal` aborted: the owner closed the
|
|
696
|
+
* client. Nothing is left open.
|
|
697
|
+
* @throws {MllpConnectionFailedError} The last attempt could not open the
|
|
698
|
+
* socket.
|
|
699
|
+
* @throws {MllpConnectionTimeoutError} The last attempt did not open in
|
|
700
|
+
* time.
|
|
701
|
+
*/
|
|
702
|
+
async #dial(signal) {
|
|
703
|
+
for (let attempt = 0;; attempt += 1) {
|
|
704
|
+
if (attempt > 0) await sleep(this.#policy.delay(attempt), signal);
|
|
705
|
+
if (signal.aborted) throw new MllpClientClosedError();
|
|
706
|
+
const session = createSession(this.#socket, this.#connect, signal);
|
|
707
|
+
try {
|
|
708
|
+
await session.ready;
|
|
709
|
+
} catch (error) {
|
|
710
|
+
this.#failed(attempt, error, signal);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (signal.aborted) {
|
|
714
|
+
await session.destroy();
|
|
715
|
+
throw new MllpClientClosedError();
|
|
716
|
+
}
|
|
717
|
+
this.#state = {
|
|
718
|
+
phase: "connected",
|
|
719
|
+
session
|
|
720
|
+
};
|
|
721
|
+
this.emit("connect");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Attempt `attempt` failed with `error`: returns when the policy allows
|
|
727
|
+
* another. Throws `MllpClientClosedError` once `signal` has aborted, and
|
|
728
|
+
* `error` once the policy is out of attempts, the client closed with it.
|
|
729
|
+
*/
|
|
730
|
+
#failed(attempt, error, signal) {
|
|
731
|
+
if (signal.aborted) throw new MllpClientClosedError();
|
|
732
|
+
if (attempt < this.#policy.attempts) return;
|
|
733
|
+
if (error instanceof MllpConnectionError) this.#closed(error);
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* The terminal move, from any phase: `reason` is the failure the client
|
|
738
|
+
* closes with, or `null` when the owner closes it. Once.
|
|
739
|
+
*/
|
|
740
|
+
#closed(reason) {
|
|
741
|
+
if (this.#state.phase === "closed") return;
|
|
742
|
+
this.#state = {
|
|
743
|
+
phase: "closed",
|
|
744
|
+
reason
|
|
745
|
+
};
|
|
746
|
+
this.emit("close", reason);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* One message on the wire, and the acknowledgment it comes back with.
|
|
750
|
+
*
|
|
751
|
+
* Settles when the send is over, however it ended.
|
|
752
|
+
*/
|
|
753
|
+
async #send(session, controlId, bytes, timeoutMs) {
|
|
754
|
+
try {
|
|
755
|
+
const reply = await this.#exchange(session, controlId, bytes, timeoutMs);
|
|
756
|
+
return await this.#readAcknowledgment(session, controlId, reply);
|
|
757
|
+
} finally {
|
|
758
|
+
if (this.#state.phase === "sending") this.#state = {
|
|
759
|
+
phase: "connected",
|
|
760
|
+
session
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* The message on the wire, and the reply it must produce to be a send.
|
|
766
|
+
* Anything else closes the client.
|
|
767
|
+
*/
|
|
768
|
+
async #exchange(session, controlId, bytes, timeoutMs) {
|
|
769
|
+
try {
|
|
770
|
+
return await session.exchange(bytes, timeoutMs);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
if (error instanceof MllpInvalidMessageError) throw error;
|
|
773
|
+
if (error instanceof MllpCodecError) await this.#fail(session, new MllpInvalidResponseError(error, controlId));
|
|
774
|
+
if (error instanceof MllpClientError) await this.#fail(session, error);
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* `reply` as this message's acknowledgment.
|
|
780
|
+
*
|
|
781
|
+
* A reply that cannot be read, or that answers another message, closes the
|
|
782
|
+
* client. A NAK does not.
|
|
783
|
+
*/
|
|
784
|
+
async #readAcknowledgment(session, controlId, reply) {
|
|
785
|
+
const answer = decode(reply, controlId);
|
|
786
|
+
switch (answer.type) {
|
|
787
|
+
case "accept": return answer.response;
|
|
788
|
+
case "nak": throw answer.exception;
|
|
789
|
+
case "invalid": return await this.#fail(session, answer.error);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Closes the client with `reason` and ends `session`. Throws `reason` once
|
|
794
|
+
* the socket is down.
|
|
795
|
+
*/
|
|
796
|
+
async #fail(session, reason) {
|
|
797
|
+
this.#closed(reason);
|
|
798
|
+
await session.destroy();
|
|
799
|
+
throw reason;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
//#endregion
|
|
803
|
+
export { MllpAlreadySendingError, MllpClient, MllpClientClosedError, MllpClientEmitter, MllpClientError, MllpConnectionError, MllpConnectionFailedError, MllpConnectionLostError, MllpConnectionTimeoutError, MllpErrorCode, MllpInvalidMessageError, MllpInvalidOptionError, MllpInvalidResponseError, MllpSendAbortedError, MllpSendTimeoutError };
|
|
804
|
+
|
|
805
|
+
//# sourceMappingURL=index.js.map
|