@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/README.md +156 -58
- package/dist/assertions.d.ts +9 -2
- package/dist/assertions.d.ts.map +1 -1
- package/dist/chunks/mllp-invalid-option-error.js +32 -0
- package/dist/chunks/mllp-invalid-option-error.js.map +1 -0
- package/dist/client.d.ts +33 -14
- package/dist/client.d.ts.map +1 -1
- package/dist/errors/base.d.ts +0 -1
- package/dist/errors/base.d.ts.map +1 -1
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/mllp-client-closed-error.d.ts +1 -2
- package/dist/errors/mllp-client-closed-error.d.ts.map +1 -1
- package/dist/events.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +227 -189
- package/dist/index.js.map +1 -1
- package/dist/messages.d.ts +7 -25
- package/dist/messages.d.ts.map +1 -1
- package/dist/queue.d.ts +25 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/runtime/node.d.ts +29 -7
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +44 -23
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/workers.d.ts +33 -0
- package/dist/runtime/workers.d.ts.map +1 -0
- package/dist/runtime/workers.js +70 -0
- package/dist/runtime/workers.js.map +1 -0
- package/dist/state.d.ts +10 -7
- package/dist/state.d.ts.map +1 -1
- package/package.json +29 -12
- package/dist/errors/mllp-already-sending-error.d.ts +0 -14
- package/dist/errors/mllp-already-sending-error.d.ts.map +0 -1
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
|
|
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
|
|
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*`
|
|
242
|
-
* an option a caller passed out of
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
287
|
+
throw new MllpInvalidResponseError(error, controlId);
|
|
325
288
|
}
|
|
326
289
|
const answers = read(tree, "MSA-2[1].1.1");
|
|
327
|
-
if (answers !== 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))
|
|
330
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/queue.ts
|
|
305
|
+
function createQueue() {
|
|
306
|
+
const entries = [];
|
|
307
|
+
let empty = null;
|
|
347
308
|
return {
|
|
348
|
-
|
|
349
|
-
|
|
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
|
|
492
|
-
* the message, waits for the acknowledgment, checks that it
|
|
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()`
|
|
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 "
|
|
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
|
-
|
|
586
|
+
opening = this.#dial(abort.signal);
|
|
590
587
|
this.#state = {
|
|
591
588
|
abort,
|
|
592
589
|
opening,
|
|
593
590
|
phase: "connecting"
|
|
594
591
|
};
|
|
595
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
646
|
-
* called. An attempt to connect stops at
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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 "
|
|
677
|
-
from.
|
|
678
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
698
|
-
* socket.
|
|
699
|
-
* @throws {MllpConnectionTimeoutError} The
|
|
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
|
-
|
|
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
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
#
|
|
741
|
-
|
|
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
|
-
*
|
|
797
|
+
* The `sending` phase: the message on the wire and its acknowledgment,
|
|
798
|
+
* from `connected` and back to it.
|
|
750
799
|
*
|
|
751
|
-
*
|
|
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(
|
|
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
|
-
|
|
756
|
-
|
|
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
|
|
766
|
-
*
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
*
|
|
782
|
-
*
|
|
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 #
|
|
797
|
-
|
|
798
|
-
|
|
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 {
|
|
841
|
+
export { MllpClient, MllpClientClosedError, MllpClientEmitter, MllpClientError, MllpConnectionError, MllpConnectionFailedError, MllpConnectionLostError, MllpConnectionTimeoutError, MllpErrorCode, MllpInvalidMessageError, MllpInvalidOptionError, MllpInvalidResponseError, MllpSendAbortedError, MllpSendTimeoutError };
|
|
804
842
|
|
|
805
843
|
//# sourceMappingURL=index.js.map
|