@glion/mllp-client 0.16.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +370 -195
  2. package/dist/assertions.d.ts +10 -0
  3. package/dist/assertions.d.ts.map +1 -0
  4. package/dist/client.d.ts +108 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/constants.d.ts +23 -0
  7. package/dist/constants.d.ts.map +1 -0
  8. package/dist/errors/base.d.ts +31 -0
  9. package/dist/errors/base.d.ts.map +1 -0
  10. package/dist/errors/index.d.ts +15 -0
  11. package/dist/errors/index.d.ts.map +1 -0
  12. package/dist/errors/mllp-already-sending-error.d.ts +14 -0
  13. package/dist/errors/mllp-already-sending-error.d.ts.map +1 -0
  14. package/dist/errors/mllp-client-closed-error.d.ts +13 -0
  15. package/dist/errors/mllp-client-closed-error.d.ts.map +1 -0
  16. package/dist/errors/mllp-connection-error.d.ts +9 -0
  17. package/dist/errors/mllp-connection-error.d.ts.map +1 -0
  18. package/dist/errors/mllp-connection-failed-error.d.ts +12 -0
  19. package/dist/errors/mllp-connection-failed-error.d.ts.map +1 -0
  20. package/dist/errors/mllp-connection-lost-error.d.ts +13 -0
  21. package/dist/errors/mllp-connection-lost-error.d.ts.map +1 -0
  22. package/dist/errors/mllp-connection-timeout-error.d.ts +13 -0
  23. package/dist/errors/mllp-connection-timeout-error.d.ts.map +1 -0
  24. package/dist/errors/mllp-invalid-message-error.d.ts +13 -0
  25. package/dist/errors/mllp-invalid-message-error.d.ts.map +1 -0
  26. package/dist/errors/mllp-invalid-option-error.d.ts +8 -0
  27. package/dist/errors/mllp-invalid-option-error.d.ts.map +1 -0
  28. package/dist/errors/mllp-invalid-response-error.d.ts +16 -0
  29. package/dist/errors/mllp-invalid-response-error.d.ts.map +1 -0
  30. package/dist/errors/mllp-send-aborted-error.d.ts +12 -0
  31. package/dist/errors/mllp-send-aborted-error.d.ts.map +1 -0
  32. package/dist/errors/mllp-send-timeout-error.d.ts +13 -0
  33. package/dist/errors/mllp-send-timeout-error.d.ts.map +1 -0
  34. package/dist/errors/nak-exception.d.ts +17 -0
  35. package/dist/errors/nak-exception.d.ts.map +1 -0
  36. package/dist/events.d.ts +35 -0
  37. package/dist/events.d.ts.map +1 -0
  38. package/dist/index.d.ts +13 -20
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +805 -3
  41. package/dist/index.js.map +1 -0
  42. package/dist/messages.d.ts +55 -0
  43. package/dist/messages.d.ts.map +1 -0
  44. package/dist/reconnect.d.ts +18 -0
  45. package/dist/reconnect.d.ts.map +1 -0
  46. package/dist/runtime/node.d.ts +39 -0
  47. package/dist/runtime/node.d.ts.map +1 -0
  48. package/dist/runtime/node.js +100 -0
  49. package/dist/runtime/node.js.map +1 -0
  50. package/dist/session.d.ts +63 -0
  51. package/dist/session.d.ts.map +1 -0
  52. package/dist/state.d.ts +39 -0
  53. package/dist/state.d.ts.map +1 -0
  54. package/dist/types.d.ts +109 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/utils.d.ts +9 -0
  57. package/dist/utils.d.ts.map +1 -0
  58. package/package.json +22 -48
  59. package/dist/client.js +0 -549
  60. package/dist/client.js.map +0 -1
  61. package/dist/core/acknowledgment.d.ts +0 -103
  62. package/dist/core/acknowledgment.d.ts.map +0 -1
  63. package/dist/core/client.d.ts +0 -256
  64. package/dist/core/client.d.ts.map +0 -1
  65. package/dist/core/connect.d.ts +0 -101
  66. package/dist/core/connect.d.ts.map +0 -1
  67. package/dist/core/errors.d.ts +0 -81
  68. package/dist/core/errors.d.ts.map +0 -1
  69. package/dist/core/index.d.ts +0 -23
  70. package/dist/core/index.d.ts.map +0 -1
  71. package/dist/core/index.js +0 -2
  72. package/dist/core/internal/ack-parser-stream.d.ts +0 -22
  73. package/dist/core/internal/ack-parser-stream.d.ts.map +0 -1
  74. package/dist/core/internal/subscribe-abort.d.ts +0 -30
  75. package/dist/core/internal/subscribe-abort.d.ts.map +0 -1
  76. package/dist/node.js +0 -214
  77. package/dist/node.js.map +0 -1
  78. package/dist/runtimes/internal/node-error-mapping.d.ts +0 -68
  79. package/dist/runtimes/internal/node-error-mapping.d.ts.map +0 -1
  80. package/dist/runtimes/node.d.ts +0 -47
  81. package/dist/runtimes/node.d.ts.map +0 -1
  82. package/dist/runtimes/node.js +0 -3
  83. package/dist/runtimes/workers.d.ts +0 -64
  84. package/dist/runtimes/workers.d.ts.map +0 -1
  85. package/dist/runtimes/workers.js +0 -143
  86. package/dist/runtimes/workers.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,3 +1,805 @@
1
- import { i as MllpClientErrorCode, r as MllpClientError } from "./client.js";
2
- import { n as nodeConnect, t as MllpClient } from "./node.js";
3
- export { MllpClient, MllpClientError, MllpClientErrorCode, nodeConnect };
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