@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.
Files changed (92) hide show
  1. package/README.md +466 -193
  2. package/dist/assertions.d.ts +17 -0
  3. package/dist/assertions.d.ts.map +1 -0
  4. package/dist/chunks/mllp-invalid-option-error.js +32 -0
  5. package/dist/chunks/mllp-invalid-option-error.js.map +1 -0
  6. package/dist/client.d.ts +127 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/constants.d.ts +23 -0
  9. package/dist/constants.d.ts.map +1 -0
  10. package/dist/errors/base.d.ts +30 -0
  11. package/dist/errors/base.d.ts.map +1 -0
  12. package/dist/errors/index.d.ts +14 -0
  13. package/dist/errors/index.d.ts.map +1 -0
  14. package/dist/errors/mllp-client-closed-error.d.ts +12 -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 +843 -3
  41. package/dist/index.js.map +1 -0
  42. package/dist/messages.d.ts +37 -0
  43. package/dist/messages.d.ts.map +1 -0
  44. package/dist/queue.d.ts +25 -0
  45. package/dist/queue.d.ts.map +1 -0
  46. package/dist/reconnect.d.ts +18 -0
  47. package/dist/reconnect.d.ts.map +1 -0
  48. package/dist/runtime/node.d.ts +61 -0
  49. package/dist/runtime/node.d.ts.map +1 -0
  50. package/dist/runtime/node.js +121 -0
  51. package/dist/runtime/node.js.map +1 -0
  52. package/dist/runtime/workers.d.ts +33 -0
  53. package/dist/runtime/workers.d.ts.map +1 -0
  54. package/dist/runtime/workers.js +70 -0
  55. package/dist/runtime/workers.js.map +1 -0
  56. package/dist/session.d.ts +63 -0
  57. package/dist/session.d.ts.map +1 -0
  58. package/dist/state.d.ts +42 -0
  59. package/dist/state.d.ts.map +1 -0
  60. package/dist/types.d.ts +109 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/utils.d.ts +9 -0
  63. package/dist/utils.d.ts.map +1 -0
  64. package/package.json +37 -46
  65. package/dist/client.js +0 -549
  66. package/dist/client.js.map +0 -1
  67. package/dist/core/acknowledgment.d.ts +0 -103
  68. package/dist/core/acknowledgment.d.ts.map +0 -1
  69. package/dist/core/client.d.ts +0 -256
  70. package/dist/core/client.d.ts.map +0 -1
  71. package/dist/core/connect.d.ts +0 -101
  72. package/dist/core/connect.d.ts.map +0 -1
  73. package/dist/core/errors.d.ts +0 -81
  74. package/dist/core/errors.d.ts.map +0 -1
  75. package/dist/core/index.d.ts +0 -23
  76. package/dist/core/index.d.ts.map +0 -1
  77. package/dist/core/index.js +0 -2
  78. package/dist/core/internal/ack-parser-stream.d.ts +0 -22
  79. package/dist/core/internal/ack-parser-stream.d.ts.map +0 -1
  80. package/dist/core/internal/subscribe-abort.d.ts +0 -30
  81. package/dist/core/internal/subscribe-abort.d.ts.map +0 -1
  82. package/dist/node.js +0 -214
  83. package/dist/node.js.map +0 -1
  84. package/dist/runtimes/internal/node-error-mapping.d.ts +0 -68
  85. package/dist/runtimes/internal/node-error-mapping.d.ts.map +0 -1
  86. package/dist/runtimes/node.d.ts +0 -47
  87. package/dist/runtimes/node.d.ts.map +0 -1
  88. package/dist/runtimes/node.js +0 -3
  89. package/dist/runtimes/workers.d.ts +0 -64
  90. package/dist/runtimes/workers.d.ts.map +0 -1
  91. package/dist/runtimes/workers.js +0 -143
  92. package/dist/runtimes/workers.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,3 +1,843 @@
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 { 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