@glion/mllp-client 0.19.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/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { n as MllpClientError, r as MllpErrorCode, t as MllpInvalidOptionError } from "./chunks/mllp-invalid-option-error.js";
1
2
  import { MllpCodecError, frame, unframe } from "@glion/mllp-codec";
2
3
  import { AckApplicationError, AckApplicationReject, AckCode, AckCommitError, AckCommitReject, isAckNakCode, isAckSuccessCode } from "@glion/ack";
3
4
  import { value } from "@glion/util-query";
@@ -11,57 +12,17 @@ import { decodeBytes, encodeBytes } from "@glion/util-charset";
11
12
  */
12
13
  const MAX_TIMEOUT_MS = 2 ** 31 - 1;
13
14
  //#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
15
  //#region src/errors/mllp-client-closed-error.ts
54
16
  /**
55
17
  * 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`.
18
+ * `destroy()`, or by a failure. The failure is on `cause`.
58
19
  */
59
20
  var MllpClientClosedError = class extends MllpClientError {
60
21
  name = "MllpClientClosedError";
61
22
  code = MllpErrorCode.CLOSED;
62
23
  delivery = "not-sent";
63
24
  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 });
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 });
65
26
  }
66
27
  };
67
28
  //#endregion
@@ -133,14 +94,6 @@ var MllpInvalidMessageError = class extends MllpClientError {
133
94
  }
134
95
  };
135
96
  //#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
97
  //#region src/errors/mllp-invalid-response-error.ts
145
98
  /**
146
99
  * The reply was not a usable acknowledgment of the message that was waiting:
@@ -238,8 +191,10 @@ var MllpSendTimeoutError = class extends MllpConnectionError {
238
191
  //#endregion
239
192
  //#region src/assertions.ts
240
193
  /**
241
- * Option validation. Each `assert*` throws {@link MllpInvalidOptionError} for
242
- * an option a caller passed out of range, before anything happens.
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.
243
198
  *
244
199
  * @module
245
200
  */
@@ -249,6 +204,10 @@ function assertTimeoutMs(option, ms) {
249
204
  function assertByteCap(bytes) {
250
205
  if (!Number.isInteger(bytes) || bytes <= 0) throw new MllpInvalidOptionError("maxBufferedBytes must be a positive integer.");
251
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
+ }
252
211
  function assertReconnectAttempts(attempts) {
253
212
  if (!(attempts === Number.POSITIVE_INFINITY) && (!Number.isInteger(attempts) || attempts < 0)) throw new MllpInvalidOptionError("reconnect.attempts must be a non-negative integer, or Infinity.");
254
213
  }
@@ -306,13 +265,17 @@ function encode(tree) {
306
265
  }
307
266
  }
308
267
  /**
309
- * `bytes` as the reply to the message `controlId` identifies.
268
+ * `bytes` as the acknowledgment of the message `controlId` identifies.
310
269
  *
311
270
  * MSA-2 is checked before MSA-1 is read: an accept or a NAK is reported only
312
271
  * for the message it answers. MSH-9 and the HL7 version are not checked
313
272
  * (#668).
314
273
  *
315
- * Never throws.
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.
316
279
  */
317
280
  function decode(bytes, controlId) {
318
281
  let raw;
@@ -321,32 +284,59 @@ function decode(bytes, controlId) {
321
284
  raw = decodeBytes(bytes);
322
285
  tree = parseHL7v2(raw);
323
286
  } catch (error) {
324
- return invalid(error, controlId);
287
+ throw new MllpInvalidResponseError(error, controlId);
325
288
  }
326
289
  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);
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);
328
291
  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);
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);
334
294
  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"
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
344
301
  };
345
302
  }
346
- function invalid(reason, controlId) {
303
+ //#endregion
304
+ //#region src/queue.ts
305
+ function createQueue() {
306
+ const entries = [];
307
+ let empty = null;
347
308
  return {
348
- error: new MllpInvalidResponseError(reason, controlId),
349
- type: "invalid"
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
+ }
350
340
  };
351
341
  }
352
342
  //#endregion
@@ -488,9 +478,10 @@ function createSession(socket, opts, signal) {
488
478
  * Sends HL7v2 messages to one remote system over MLLP and returns each
489
479
  * message's acknowledgment.
490
480
  *
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.
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.
494
485
  *
495
486
  * ```ts
496
487
  * import { MllpClient } from "@glion/mllp-client";
@@ -524,6 +515,7 @@ var MllpClient = class extends MllpClientEmitter {
524
515
  #policy;
525
516
  #sendTimeoutMs;
526
517
  #state = { phase: "idle" };
518
+ #queue = createQueue();
527
519
  /**
528
520
  * @throws {MllpInvalidOptionError} A timeout, byte cap, or attempt count is
529
521
  * out of range.
@@ -556,6 +548,10 @@ var MllpClient = class extends MllpClientEmitter {
556
548
  get state() {
557
549
  return this.#state.phase;
558
550
  }
551
+ /** How many sends wait their turn, the one in flight excluded. */
552
+ get pending() {
553
+ return this.#queue.pending;
554
+ }
559
555
  /** Whether the connection is open. True in both `connected` and `sending`. */
560
556
  get connected() {
561
557
  const { phase } = this.#state;
@@ -571,44 +567,58 @@ var MllpClient = class extends MllpClientEmitter {
571
567
  * @throws {MllpConnectionFailedError} The last attempt could not open the
572
568
  * socket.
573
569
  * @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.
570
+ * @throws {MllpClientClosedError} The client is closed, or `close()` or
571
+ * `destroy()` arrived while the connection was still being established.
576
572
  */
577
573
  async connect() {
578
574
  const state = this.#state;
575
+ let opening;
579
576
  switch (state.phase) {
580
577
  case "connected":
581
578
  case "sending": return;
582
- case "connecting":
583
- await state.opening;
584
- return;
585
- case "closing": throw new MllpClientClosedError();
579
+ case "closing":
586
580
  case "closed": throw new MllpClientClosedError(state.reason);
581
+ case "connecting":
582
+ opening = state.opening;
583
+ break;
587
584
  case "idle": {
588
585
  const abort = new AbortController();
589
- const opening = this.#dial(abort.signal);
586
+ opening = this.#dial(abort.signal);
590
587
  this.#state = {
591
588
  abort,
592
589
  opening,
593
590
  phase: "connecting"
594
591
  };
595
- await opening;
592
+ break;
596
593
  }
597
594
  }
595
+ try {
596
+ await opening;
597
+ } catch (error) {
598
+ if (error instanceof MllpConnectionError) await this.destroy(error);
599
+ throw error;
600
+ }
598
601
  }
599
602
  /**
600
603
  * Sends one message and returns its acknowledgment. Connects first when the
601
- * client is not connected yet.
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.
602
612
  *
603
613
  * Nothing is written when an option, phase, or message error is thrown.
604
- * Every other failure also closes the client.
614
+ * Every wire failure also closes the client; a NAK does not.
605
615
  *
606
616
  * @throws {AckException} The remote system answered with a NAK. The
607
617
  * connection stays open.
608
618
  * @throws {MllpInvalidOptionError} `timeoutMs` is out of range.
609
619
  * @throws {MllpInvalidMessageError} The message cannot be sent as-is.
610
- * @throws {MllpClientClosedError} The client is closed.
611
- * @throws {MllpAlreadySendingError} Another send is in flight.
620
+ * @throws {MllpClientClosedError} The client is closed, or closed while
621
+ * this send was waiting its turn.
612
622
  * @throws {MllpConnectionFailedError} The connection could not be opened.
613
623
  * @throws {MllpConnectionTimeoutError} The connection did not open in time.
614
624
  * @throws {MllpSendTimeoutError} No acknowledgment arrived in time.
@@ -621,65 +631,99 @@ var MllpClient = class extends MllpClientEmitter {
621
631
  const timeoutMs = opts.timeoutMs ?? this.#sendTimeoutMs;
622
632
  assertTimeoutMs("timeoutMs", timeoutMs);
623
633
  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;
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);
633
643
  }
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
644
  }
643
645
  /**
644
646
  * 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
+ * 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.
647
650
  *
648
651
  * Resolves from any phase. Never rejects. Idempotent. The wait is bounded by
649
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`.
650
655
  */
651
656
  async close() {
652
657
  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]);
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();
660
681
  }
661
- await this.destroy();
662
682
  }
663
683
  /**
664
684
  * Ends the connection now, and resolves when it is down. A message in flight
665
685
  * rejects with {@link MllpSendAbortedError}. An attempt to connect stops at
666
- * once.
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.
667
688
  *
668
- * Resolves from any phase. Never rejects. Idempotent.
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.
669
694
  */
670
- async destroy() {
695
+ async destroy(reason = null) {
671
696
  const from = this.#state;
672
- this.#closed(null);
673
697
  switch (from.phase) {
674
- case "idle":
675
698
  case "closed": return;
676
- case "connecting":
677
- from.abort.abort();
678
- await Promise.allSettled([from.opening]);
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;
679
706
  return;
707
+ case "idle":
708
+ this.#state = {
709
+ phase: "closed",
710
+ reason
711
+ };
712
+ this.emit("close", reason);
713
+ return;
714
+ case "connecting":
680
715
  case "connected":
681
- case "sending":
682
- case "closing": await from.session.destroy();
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
+ }
683
727
  }
684
728
  }
685
729
  /** Calls {@link close}. Enables `await using`. */
@@ -689,30 +733,30 @@ var MllpClient = class extends MllpClientEmitter {
689
733
  /**
690
734
  * Dials under the policy until a session opens, then moves to `connected`
691
735
  * 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.
736
+ * `delay(attempt)` first.
694
737
  *
695
738
  * @throws {MllpClientClosedError} `signal` aborted: the owner closed the
696
739
  * 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.
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.
701
744
  */
702
745
  async #dial(signal) {
703
746
  for (let attempt = 0;; attempt += 1) {
704
747
  if (attempt > 0) await sleep(this.#policy.delay(attempt), signal);
705
- if (signal.aborted) throw new MllpClientClosedError();
748
+ if (signal.aborted) throw new MllpClientClosedError(signal.reason);
706
749
  const session = createSession(this.#socket, this.#connect, signal);
707
750
  try {
708
751
  await session.ready;
709
752
  } catch (error) {
710
- this.#failed(attempt, error, signal);
753
+ if (signal.aborted) throw new MllpClientClosedError(signal.reason);
754
+ if (attempt >= this.#policy.attempts) throw error;
711
755
  continue;
712
756
  }
713
757
  if (signal.aborted) {
714
758
  await session.destroy();
715
- throw new MllpClientClosedError();
759
+ throw new MllpClientClosedError(signal.reason);
716
760
  }
717
761
  this.#state = {
718
762
  phase: "connected",
@@ -723,37 +767,51 @@ var MllpClient = class extends MllpClientEmitter {
723
767
  }
724
768
  }
725
769
  /**
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.
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.
739
776
  */
740
- #closed(reason) {
741
- if (this.#state.phase === "closed") return;
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");
742
790
  this.#state = {
743
791
  phase: "closed",
744
- reason
792
+ reason: ending.reason
745
793
  };
746
- this.emit("close", reason);
794
+ this.emit("close", ending.reason);
747
795
  }
748
796
  /**
749
- * One message on the wire, and the acknowledgment it comes back with.
797
+ * The `sending` phase: the message on the wire and its acknowledgment,
798
+ * from `connected` and back to it.
750
799
  *
751
- * Settles when the send is over, however it ended.
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.
752
802
  */
753
- async #send(session, controlId, bytes, timeoutMs) {
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
+ };
754
811
  try {
755
- const reply = await this.#exchange(session, controlId, bytes, timeoutMs);
756
- return await this.#readAcknowledgment(session, controlId, reply);
812
+ return decode(await session.exchange(bytes, timeoutMs), controlId);
813
+ } catch (error) {
814
+ return await this.#sendFailed(error, controlId);
757
815
  } finally {
758
816
  if (this.#state.phase === "sending") this.#state = {
759
817
  phase: "connected",
@@ -762,44 +820,24 @@ var MllpClient = class extends MllpClientEmitter {
762
820
  }
763
821
  }
764
822
  /**
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.
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.
780
827
  *
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.
828
+ * From `closing`, the ending under way is waiting for this send: started,
829
+ * not awaited.
795
830
  */
796
- async #fail(session, reason) {
797
- this.#closed(reason);
798
- await session.destroy();
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);
799
837
  throw reason;
800
838
  }
801
839
  };
802
840
  //#endregion
803
- export { MllpAlreadySendingError, MllpClient, MllpClientClosedError, MllpClientEmitter, MllpClientError, MllpConnectionError, MllpConnectionFailedError, MllpConnectionLostError, MllpConnectionTimeoutError, MllpErrorCode, MllpInvalidMessageError, MllpInvalidOptionError, MllpInvalidResponseError, MllpSendAbortedError, MllpSendTimeoutError };
841
+ export { MllpClient, MllpClientClosedError, MllpClientEmitter, MllpClientError, MllpConnectionError, MllpConnectionFailedError, MllpConnectionLostError, MllpConnectionTimeoutError, MllpErrorCode, MllpInvalidMessageError, MllpInvalidOptionError, MllpInvalidResponseError, MllpSendAbortedError, MllpSendTimeoutError };
804
842
 
805
843
  //# sourceMappingURL=index.js.map