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