@cosyte/mllp 0.0.1 → 0.0.3
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/CHANGELOG.md +329 -108
- package/README.md +23 -18
- package/TRADEMARKS.md +30 -0
- package/dist/ack-from-hl7/index.cjs +55 -15
- package/dist/ack-from-hl7/index.cjs.map +1 -1
- package/dist/ack-from-hl7/index.d.cts +112 -38
- package/dist/ack-from-hl7/index.d.ts +112 -38
- package/dist/ack-from-hl7/index.mjs +54 -15
- package/dist/ack-from-hl7/index.mjs.map +1 -1
- package/dist/{index-DjGQJtYE.d.cts → index-CD8V2Mtk.d.cts} +5 -5
- package/dist/{index-DjGQJtYE.d.ts → index-CD8V2Mtk.d.ts} +5 -5
- package/dist/index.cjs +157 -168
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +398 -420
- package/dist/index.d.ts +398 -420
- package/dist/index.mjs +157 -168
- package/dist/index.mjs.map +1 -1
- package/dist/testing/index.cjs +3 -3
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +8 -8
- package/dist/testing/index.d.ts +8 -8
- package/dist/testing/index.mjs +3 -3
- package/dist/testing/index.mjs.map +1 -1
- package/package.json +16 -7
package/dist/index.cjs
CHANGED
|
@@ -27,13 +27,13 @@ var MllpFramingError = class _MllpFramingError extends Error {
|
|
|
27
27
|
/**
|
|
28
28
|
* Up to 64 bytes copied from around the anomaly.
|
|
29
29
|
*
|
|
30
|
-
* This is a **copied** Buffer
|
|
30
|
+
* This is a **copied** Buffer, isolated from the source buffer so it remains
|
|
31
31
|
* valid after the underlying buffer is reused or overwritten.
|
|
32
32
|
*
|
|
33
|
-
* **PHI contract
|
|
33
|
+
* **PHI contract:** the decoder only ever populates this with the single
|
|
34
34
|
* framing-boundary byte that violated the frame structure (whose hex value the
|
|
35
|
-
* `message` already discloses)
|
|
36
|
-
* whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE
|
|
35
|
+
* `message` already discloses), never a run of payload content bytes. For anomalies
|
|
36
|
+
* whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE`, the accumulated size),
|
|
37
37
|
* the snippet is **empty**: a payload slice on a public error field would leak a
|
|
38
38
|
* field-body slice of clinical content. Callers constructing this directly are
|
|
39
39
|
* responsible for the same discipline.
|
|
@@ -106,19 +106,19 @@ var FrameReader = class {
|
|
|
106
106
|
_opts;
|
|
107
107
|
_maxFrameSize;
|
|
108
108
|
_state = "SCANNING_FOR_VT";
|
|
109
|
-
/** Reusable accumulator buffer
|
|
109
|
+
/** Reusable accumulator buffer, grown by doubling as needed. */
|
|
110
110
|
_accumulator = Buffer.allocUnsafe(INITIAL_ACCUMULATOR_SIZE);
|
|
111
111
|
/** Number of payload bytes written to accumulator in current frame. */
|
|
112
112
|
_writePos = 0;
|
|
113
113
|
/** Monotonic absolute stream byte offset across the entire connection lifetime. */
|
|
114
114
|
_byteOffset = 0;
|
|
115
|
-
/** Byte offset where current leading-whitespace run started
|
|
115
|
+
/** Byte offset where current leading-whitespace run started. */
|
|
116
116
|
_wsStart = 0;
|
|
117
|
-
/** Count of leading whitespace bytes accumulated in current run
|
|
117
|
+
/** Count of leading whitespace bytes accumulated in current run. */
|
|
118
118
|
_wsCount = 0;
|
|
119
119
|
/** Byte offset of the VT byte that started the current frame. */
|
|
120
120
|
_frameStartOffset = 0;
|
|
121
|
-
/** Per-frame warning accumulator
|
|
121
|
+
/** Per-frame warning accumulator, cleared after each _deliverFrame(). */
|
|
122
122
|
_frameWarnings = [];
|
|
123
123
|
/**
|
|
124
124
|
* Construct a chunked MLLP frame reader.
|
|
@@ -149,7 +149,7 @@ var FrameReader = class {
|
|
|
149
149
|
/**
|
|
150
150
|
* Clear internal accumulator state and reset byte offset to 0.
|
|
151
151
|
*
|
|
152
|
-
* Call on reconnect / connection reuse to start fresh without allocating a new reader
|
|
152
|
+
* Call on reconnect / connection reuse to start fresh without allocating a new reader.
|
|
153
153
|
* Pending partial frame state is discarded silently.
|
|
154
154
|
*
|
|
155
155
|
* @example
|
|
@@ -242,7 +242,7 @@ var FrameReader = class {
|
|
|
242
242
|
this._emitWarning(
|
|
243
243
|
"MLLP_MISSING_LEADING_VT",
|
|
244
244
|
this._byteOffset,
|
|
245
|
-
`Missing leading VT (0x0B)
|
|
245
|
+
`Missing leading VT (0x0B), treating byte 0x${byte.toString(16).padStart(2, "0")} at offset ${this._byteOffset} as payload start`
|
|
246
246
|
);
|
|
247
247
|
this._frameStartOffset = this._byteOffset;
|
|
248
248
|
this._state = "READING_PAYLOAD";
|
|
@@ -390,7 +390,7 @@ var FrameReader = class {
|
|
|
390
390
|
}
|
|
391
391
|
/**
|
|
392
392
|
* Copy accumulated payload bytes into a new Buffer and deliver via `onFrame`.
|
|
393
|
-
* The copy isolates callers from internal accumulator reuse
|
|
393
|
+
* The copy isolates callers from internal accumulator reuse.
|
|
394
394
|
*/
|
|
395
395
|
_deliverFrame() {
|
|
396
396
|
const payload = Buffer.from(this._accumulator.subarray(0, this._writePos));
|
|
@@ -400,7 +400,7 @@ var FrameReader = class {
|
|
|
400
400
|
this._frameWarnings = [];
|
|
401
401
|
this._opts.onFrame(payload, frameStart, frameWarnings);
|
|
402
402
|
}
|
|
403
|
-
/** Emit a warning via the `onWarning` callback, swallowing any handler exceptions
|
|
403
|
+
/** Emit a warning via the `onWarning` callback, swallowing any handler exceptions. */
|
|
404
404
|
_emitWarning(code, byteOffset, message) {
|
|
405
405
|
const warning = createWarning(code, byteOffset, message);
|
|
406
406
|
this._frameWarnings.push(warning);
|
|
@@ -487,8 +487,8 @@ var TlsTransport = class {
|
|
|
487
487
|
this._socket.on("data", fn);
|
|
488
488
|
}
|
|
489
489
|
/**
|
|
490
|
-
* See {@link Transport.onConnect}. Armed on `'secureConnect'
|
|
491
|
-
* handshake-complete event
|
|
490
|
+
* See {@link Transport.onConnect}. Armed on `'secureConnect'`, the TLS
|
|
491
|
+
* handshake-complete event, not the raw TCP `'connect'` event.
|
|
492
492
|
*/
|
|
493
493
|
onConnect(fn) {
|
|
494
494
|
this._socket.removeAllListeners("secureConnect");
|
|
@@ -554,7 +554,7 @@ var LEGAL_TRANSITIONS = /* @__PURE__ */ new Map([
|
|
|
554
554
|
["CLOSED", /* @__PURE__ */ new Set()]
|
|
555
555
|
]);
|
|
556
556
|
var Connection = class extends events.EventEmitter {
|
|
557
|
-
/** Stable UUIDv4 identifier for this connection
|
|
557
|
+
/** Stable UUIDv4 identifier for this connection. */
|
|
558
558
|
connectionId;
|
|
559
559
|
_state = "CONNECTING";
|
|
560
560
|
_transport;
|
|
@@ -567,20 +567,19 @@ var Connection = class extends events.EventEmitter {
|
|
|
567
567
|
_bytesOut = 0;
|
|
568
568
|
_lastByteInAt = null;
|
|
569
569
|
_lastByteOutAt = null;
|
|
570
|
-
/** Ring buffer: last MAX_WARNINGS entries
|
|
570
|
+
/** Ring buffer: last MAX_WARNINGS entries. */
|
|
571
571
|
_warningBuffer = [];
|
|
572
|
-
/** Accurate total counts per code
|
|
572
|
+
/** Accurate total counts per code, unaffected by ring buffer truncation. */
|
|
573
573
|
_warningsByCode = /* @__PURE__ */ new Map();
|
|
574
|
-
/** Set to true once the ring buffer overflows
|
|
574
|
+
/** Set to true once the ring buffer overflows. */
|
|
575
575
|
_warningsTruncated = false;
|
|
576
576
|
_onWarningFn = null;
|
|
577
577
|
_drainPromise = null;
|
|
578
578
|
/**
|
|
579
|
-
* beforeClose hook
|
|
579
|
+
* beforeClose hook, no-op default that resolves immediately.
|
|
580
580
|
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
* Connection calls this during `close()`, racing against `drainTimeoutMs`.
|
|
581
|
+
* An owner may replace this instance property to run work before the socket
|
|
582
|
+
* closes. Connection calls it during `close()`, racing against `drainTimeoutMs`.
|
|
584
583
|
*/
|
|
585
584
|
beforeClose = () => Promise.resolve();
|
|
586
585
|
/**
|
|
@@ -627,7 +626,7 @@ var Connection = class extends events.EventEmitter {
|
|
|
627
626
|
});
|
|
628
627
|
}
|
|
629
628
|
/**
|
|
630
|
-
* Current FSM state. One of the 6 `ConnectionState` values
|
|
629
|
+
* Current FSM state. One of the 6 `ConnectionState` values.
|
|
631
630
|
*
|
|
632
631
|
* Subscribe to `'stateChange'` events for reactive state monitoring.
|
|
633
632
|
*/
|
|
@@ -635,10 +634,10 @@ var Connection = class extends events.EventEmitter {
|
|
|
635
634
|
return this._state;
|
|
636
635
|
}
|
|
637
636
|
/**
|
|
638
|
-
* Register or replace the per-connection warning subscriber
|
|
637
|
+
* Register or replace the per-connection warning subscriber.
|
|
639
638
|
*
|
|
640
639
|
* Subsequent calls replace the previous handler (set-once semantics prevent leaks).
|
|
641
|
-
* The handler is called synchronously and any exception it throws is swallowed
|
|
640
|
+
* The handler is called synchronously and any exception it throws is swallowed.
|
|
642
641
|
*
|
|
643
642
|
* @example
|
|
644
643
|
* ```typescript
|
|
@@ -676,7 +675,7 @@ var Connection = class extends events.EventEmitter {
|
|
|
676
675
|
/**
|
|
677
676
|
* Write raw bytes to the transport (no framing applied).
|
|
678
677
|
*
|
|
679
|
-
*
|
|
678
|
+
* The server and the client wrap this with `encodeFrame()` to add MLLP framing.
|
|
680
679
|
* Returns `false` if the connection is CLOSED or DISCONNECTED (no bytes written).
|
|
681
680
|
*
|
|
682
681
|
* @returns `true` if bytes flushed immediately; `false` if buffered (backpressure) or not writable.
|
|
@@ -698,7 +697,7 @@ var Connection = class extends events.EventEmitter {
|
|
|
698
697
|
* Initiate graceful close of the connection.
|
|
699
698
|
*
|
|
700
699
|
* - If `CONNECTING` or `RECONNECTING`: cancels the pending attempt, transitions
|
|
701
|
-
* directly to `CLOSED`, and calls `transport.destroy()
|
|
700
|
+
* directly to `CLOSED`, and calls `transport.destroy()`.
|
|
702
701
|
* - If `CONNECTED`: transitions to `DRAINING`, calls `beforeClose(drainTimeoutMs)`,
|
|
703
702
|
* then `DRAINING → DISCONNECTED` once the hook resolves, or `DRAINING → CLOSED`
|
|
704
703
|
* if the drain timeout elapses first.
|
|
@@ -733,7 +732,7 @@ var Connection = class extends events.EventEmitter {
|
|
|
733
732
|
* Race `beforeClose()` against the drain timeout.
|
|
734
733
|
*
|
|
735
734
|
* On normal completion: `DRAINING → DISCONNECTED` + `transport.close()`.
|
|
736
|
-
* On timeout: `DRAINING → CLOSED` + `transport.destroy()
|
|
735
|
+
* On timeout: `DRAINING → CLOSED` + `transport.destroy()`.
|
|
737
736
|
*/
|
|
738
737
|
async _drainWithTimeout(timeoutMs) {
|
|
739
738
|
const drainPromise = this.beforeClose(timeoutMs);
|
|
@@ -760,7 +759,7 @@ var Connection = class extends events.EventEmitter {
|
|
|
760
759
|
* Abruptly destroy the connection, discarding any pending writes.
|
|
761
760
|
*
|
|
762
761
|
* Transitions from any non-terminal state directly to `CLOSED` and calls
|
|
763
|
-
* `transport.destroy(reason)`. Emits `'close'` event. Idempotent
|
|
762
|
+
* `transport.destroy(reason)`. Emits `'close'` event. Idempotent, safe to
|
|
764
763
|
* call multiple times.
|
|
765
764
|
*
|
|
766
765
|
* @param reason - Optional error to propagate to the transport.
|
|
@@ -776,7 +775,7 @@ var Connection = class extends events.EventEmitter {
|
|
|
776
775
|
this._transport.destroy(reason);
|
|
777
776
|
}
|
|
778
777
|
/**
|
|
779
|
-
* Return a JSON-serializable observability snapshot
|
|
778
|
+
* Return a JSON-serializable observability snapshot.
|
|
780
779
|
*
|
|
781
780
|
* All timestamp fields are `Date | null` (not epoch milliseconds). `JSON.stringify()`
|
|
782
781
|
* serialises them to ISO 8601 strings with no information loss.
|
|
@@ -860,12 +859,12 @@ var Connection = class extends events.EventEmitter {
|
|
|
860
859
|
* Node throws `ERR_UNHANDLED_ERROR` when `'error'` is emitted on an `EventEmitter` with no
|
|
861
860
|
* listener. Every `'error'` emission in this class is reached from inside a transport callback
|
|
862
861
|
* (the socket's `'data'`/`'error'` listener), so an unguarded emit would become an **uncaught
|
|
863
|
-
* exception that kills the process
|
|
862
|
+
* exception that kills the process**, the exact failure the containment above exists to
|
|
864
863
|
* prevent, merely relocated one frame up the stack.
|
|
865
864
|
*
|
|
866
865
|
* `Connection` is a public export and can legitimately be driven with no `'error'` listener
|
|
867
866
|
* (`MllpServer` and `MllpClient` each attach one; a bare `Connection` need not). The connection
|
|
868
|
-
* is still torn down and `'stateChange'`/`'close'` still fire, so a caller is never blind
|
|
867
|
+
* is still torn down and `'stateChange'`/`'close'` still fire, so a caller is never blind,
|
|
869
868
|
* they simply do not get the typed error object they never asked for. Mirrors
|
|
870
869
|
* `MllpServer._emitErrorIfListened`.
|
|
871
870
|
*/
|
|
@@ -884,7 +883,7 @@ var Connection = class extends events.EventEmitter {
|
|
|
884
883
|
* consumers keep the stable `code` and `byteOffset`), then **destroy this connection only**.
|
|
885
884
|
*
|
|
886
885
|
* Why destroy rather than resynchronize: once `push` has thrown, the reader's position within
|
|
887
|
-
* the byte stream is no longer trustworthy
|
|
886
|
+
* the byte stream is no longer trustworthy, the next bytes could be the middle of a message.
|
|
888
887
|
* Guessing where the next frame starts is how a clinical message gets silently mis-split, so
|
|
889
888
|
* the connection is dropped instead. A server drops that one peer and keeps serving everyone
|
|
890
889
|
* else.
|
|
@@ -896,7 +895,7 @@ var Connection = class extends events.EventEmitter {
|
|
|
896
895
|
* engine that is already unwell. A compatibility failure is not a network blip.
|
|
897
896
|
*
|
|
898
897
|
* If a peer's quirk is *expected* (trailing junk, keepalive bytes, a missing leading VT), the
|
|
899
|
-
* decoder's tolerance opt-ins are the supported answer
|
|
898
|
+
* decoder's tolerance opt-ins are the supported answer, they turn the throw into a warning.
|
|
900
899
|
*/
|
|
901
900
|
_onFatalFramingError(err) {
|
|
902
901
|
if (this._state === "CLOSED") return;
|
|
@@ -910,13 +909,13 @@ var Connection = class extends events.EventEmitter {
|
|
|
910
909
|
this.destroy(err);
|
|
911
910
|
}
|
|
912
911
|
/**
|
|
913
|
-
* Backstop for a NON-framing throw escaping `FrameReader.push
|
|
912
|
+
* Backstop for a NON-framing throw escaping `FrameReader.push`, i.e. our own bug, or a
|
|
914
913
|
* subscriber's, rather than the peer's bytes.
|
|
915
914
|
*
|
|
916
915
|
* `_onFrameDecoded` already contains subscriber throws at the dispatch site, so this should be
|
|
917
916
|
* unreachable. It exists because "unreachable" and "cannot kill the process from inside a
|
|
918
917
|
* socket `'data'` handler" are different claims, and only the second one is safe to bet a
|
|
919
|
-
* clinical interface on. Reported honestly as an internal receive error
|
|
918
|
+
* clinical interface on. Reported honestly as an internal receive error, never dressed up as
|
|
920
919
|
* a peer framing fault.
|
|
921
920
|
*/
|
|
922
921
|
_onInternalReceiveError(err) {
|
|
@@ -992,9 +991,9 @@ var UNSAFE_FIELD_SEPARATORS = /* @__PURE__ */ new Set([
|
|
|
992
991
|
SEGMENT_SEPARATOR_CR,
|
|
993
992
|
SEGMENT_SEPARATOR_LF,
|
|
994
993
|
11,
|
|
995
|
-
// VT
|
|
994
|
+
// VT, MLLP start block
|
|
996
995
|
28
|
|
997
|
-
// FS
|
|
996
|
+
// FS, MLLP end block
|
|
998
997
|
]);
|
|
999
998
|
function isMshSegmentAt(buf, i) {
|
|
1000
999
|
return i + 3 < buf.length && buf[i] === ASCII_M && buf[i + 1] === ASCII_S && buf[i + 2] === ASCII_H && !UNSAFE_FIELD_SEPARATORS.has(buf[i + 3]);
|
|
@@ -1227,13 +1226,13 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1227
1226
|
_netServer;
|
|
1228
1227
|
_connections = /* @__PURE__ */ new Set();
|
|
1229
1228
|
_opts;
|
|
1230
|
-
/** `true` when `ServerOptions.tls` is set
|
|
1229
|
+
/** `true` when `ServerOptions.tls` is set, `_netServer` is actually a `tls.Server`. */
|
|
1231
1230
|
_isTls;
|
|
1232
1231
|
_listening = false;
|
|
1233
|
-
/**
|
|
1232
|
+
/** Single-flight guard: `true` while a `listen()` call is settling. */
|
|
1234
1233
|
_listenInFlight = false;
|
|
1235
1234
|
/**
|
|
1236
|
-
*
|
|
1235
|
+
* Settle hook for an in-flight `listen()`. Non-null only while a
|
|
1237
1236
|
* `listen()` is in flight; invoked by `close()` so a close racing startup
|
|
1238
1237
|
* rejects the pending `listen()` (typed) instead of leaving it hung with
|
|
1239
1238
|
* the single-flight guard stuck. Cleared on every settle path.
|
|
@@ -1243,7 +1242,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1243
1242
|
_host = null;
|
|
1244
1243
|
_acceptedTotal = 0;
|
|
1245
1244
|
_closedTotal = 0;
|
|
1246
|
-
/**
|
|
1245
|
+
/** Count of `'tlsClientError'` events since listen(). */
|
|
1247
1246
|
_tlsClientErrorsTotal = 0;
|
|
1248
1247
|
/**
|
|
1249
1248
|
* Construct an MLLP server. Created idle; call `listen()` (or use
|
|
@@ -1306,23 +1305,23 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1306
1305
|
* `Object.freeze({ port: actualPort, host: actualHost })`.
|
|
1307
1306
|
*
|
|
1308
1307
|
* **Single-flight:** one `listen()` per server lifecycle at a time. A call
|
|
1309
|
-
* while the server is already listening
|
|
1310
|
-
* still in flight
|
|
1308
|
+
* while the server is already listening, or while another `listen()` is
|
|
1309
|
+
* still in flight, rejects with a typed `MllpConnectionError` (concurrent
|
|
1311
1310
|
* binds raced each other's post-bind safety checks). Call `close()` before
|
|
1312
1311
|
* re-listening; sequential `listen()` → `close()` → `listen()` is fine.
|
|
1313
1312
|
*
|
|
1314
1313
|
* **`close()` during an in-flight `listen()`** rejects that `listen()` with
|
|
1315
1314
|
* a typed `MllpConnectionError` (never a hang) and clears the single-flight
|
|
1316
|
-
* guard
|
|
1315
|
+
* guard, a subsequent `listen()` on the same server works. This makes
|
|
1317
1316
|
* `Symbol.asyncDispose` (which delegates to `close()`) safe even before a
|
|
1318
1317
|
* `listen()` has settled. (Exception: `close({ signal })` with an
|
|
1319
|
-
* already-aborted signal is a no-op AbortError rejection
|
|
1318
|
+
* already-aborted signal is a no-op AbortError rejection, the in-flight
|
|
1320
1319
|
* `listen()` is left to settle on its own bind outcome; see `close()`.)
|
|
1321
1320
|
*
|
|
1322
1321
|
* @param port - TCP port to bind. Use `0` to let the OS assign an ephemeral port.
|
|
1323
1322
|
* @param hostOrOpts - Host string or object with `host` and optional `signal`.
|
|
1324
|
-
* Default host: `'127.0.0.1'` (
|
|
1325
|
-
* are rejected unless `ServerOptions.allowWildcardBind: true
|
|
1323
|
+
* Default host: `'127.0.0.1'` (bind-safety hardening). Wildcard hosts
|
|
1324
|
+
* are rejected unless `ServerOptions.allowWildcardBind: true`, literal
|
|
1326
1325
|
* spellings pre-bind, resolver-only shorthands post-bind via the
|
|
1327
1326
|
* OS-normalized bound address.
|
|
1328
1327
|
*
|
|
@@ -1342,7 +1341,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1342
1341
|
if (this._listening || this._listenInFlight) {
|
|
1343
1342
|
return Promise.reject(
|
|
1344
1343
|
new MllpConnectionError(
|
|
1345
|
-
this._listening ? "listen() rejected: server is already listening
|
|
1344
|
+
this._listening ? "listen() rejected: server is already listening, call close() before re-listening" : "listen() rejected: another listen() is already in flight on this server",
|
|
1346
1345
|
{
|
|
1347
1346
|
cause: new Error("listen already listening or in flight"),
|
|
1348
1347
|
phase: "connect"
|
|
@@ -1353,7 +1352,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1353
1352
|
if (isWildcardHost(host) && this._opts.allowWildcardBind !== true) {
|
|
1354
1353
|
return Promise.reject(
|
|
1355
1354
|
new MllpConnectionError(
|
|
1356
|
-
`refusing to bind wildcard host '${host}'
|
|
1355
|
+
`refusing to bind wildcard host '${host}', set ServerOptions.allowWildcardBind: true to bind all interfaces`,
|
|
1357
1356
|
{
|
|
1358
1357
|
cause: new Error("wildcard bind requires allowWildcardBind"),
|
|
1359
1358
|
phase: "connect"
|
|
@@ -1432,7 +1431,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1432
1431
|
{
|
|
1433
1432
|
ok: false,
|
|
1434
1433
|
error: new MllpConnectionError(
|
|
1435
|
-
`refusing to bind wildcard host '${host}' (bound address '${actualHost}')
|
|
1434
|
+
`refusing to bind wildcard host '${host}' (bound address '${actualHost}'), set ServerOptions.allowWildcardBind: true to bind all interfaces`,
|
|
1436
1435
|
{
|
|
1437
1436
|
cause: new Error("wildcard bind requires allowWildcardBind"),
|
|
1438
1437
|
phase: "connect"
|
|
@@ -1459,7 +1458,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1459
1458
|
}
|
|
1460
1459
|
};
|
|
1461
1460
|
const wildcardBound = isWildcardHost(actualHost);
|
|
1462
|
-
const wildcardMessage = `MLLP server is bound to ALL network interfaces (host='${actualHost}')
|
|
1461
|
+
const wildcardMessage = `MLLP server is bound to ALL network interfaces (host='${actualHost}'), set a specific bind address unless this is intentional.`;
|
|
1463
1462
|
if (wildcardBound) {
|
|
1464
1463
|
process.emitWarning(wildcardMessage, { code: MLLP_BIND_ALL_INTERFACES });
|
|
1465
1464
|
}
|
|
@@ -1487,19 +1486,19 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1487
1486
|
/**
|
|
1488
1487
|
* Stop accepting new connections and gracefully close all active connections.
|
|
1489
1488
|
*
|
|
1490
|
-
* Sequence
|
|
1491
|
-
* 1. `net.Server.close()
|
|
1489
|
+
* Sequence:
|
|
1490
|
+
* 1. `net.Server.close()`, stops accepting new connections immediately
|
|
1492
1491
|
* 2. If `_connections` is empty: resolves immediately (no drain needed)
|
|
1493
|
-
* 3. Calls `_drainAll(drainTimeoutMs)
|
|
1492
|
+
* 3. Calls `_drainAll(drainTimeoutMs)`, Promise.all + side-effect setTimeout
|
|
1494
1493
|
* that force-destroys stragglers after the drain window
|
|
1495
1494
|
*
|
|
1496
1495
|
* Calling `close()` while a `listen()` is still in flight proactively
|
|
1497
1496
|
* settles that `listen()` with a typed `MllpConnectionError` rejection
|
|
1498
|
-
* and clears the single-flight guard
|
|
1497
|
+
* and clears the single-flight guard, the server is immediately
|
|
1499
1498
|
* re-listenable. `Symbol.asyncDispose` (which delegates here) is
|
|
1500
1499
|
* therefore safe before `listen()` settles. **Qualification:** a
|
|
1501
1500
|
* `close({ signal })` whose signal is **already aborted** rejects
|
|
1502
|
-
* immediately with `AbortError` and performs no work
|
|
1501
|
+
* immediately with `AbortError` and performs no work, it does NOT close
|
|
1503
1502
|
* the server and does NOT settle the in-flight `listen()`, which simply
|
|
1504
1503
|
* continues and settles on its own bind outcome.
|
|
1505
1504
|
*
|
|
@@ -1551,7 +1550,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1551
1550
|
/**
|
|
1552
1551
|
* Drain all active connections with a shared timeout.
|
|
1553
1552
|
*
|
|
1554
|
-
* Uses Promise.all (not Promise.race) for coordination
|
|
1553
|
+
* Uses Promise.all (not Promise.race) for coordination, the timeout is a
|
|
1555
1554
|
* side effect that calls `conn.destroy()` on stragglers after `drainTimeoutMs`.
|
|
1556
1555
|
* Promise.all resolves when all `conn.close()` promises settle (which happens
|
|
1557
1556
|
* because `conn.destroy()` transitions connections to CLOSED).
|
|
@@ -1574,7 +1573,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1574
1573
|
}
|
|
1575
1574
|
}
|
|
1576
1575
|
/**
|
|
1577
|
-
* Async disposal
|
|
1576
|
+
* Async disposal, delegates to `close()` for `await using` support.
|
|
1578
1577
|
*
|
|
1579
1578
|
* @example
|
|
1580
1579
|
* ```typescript
|
|
@@ -1587,10 +1586,9 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1587
1586
|
return this.close();
|
|
1588
1587
|
}
|
|
1589
1588
|
/**
|
|
1590
|
-
* Return a JSON-serializable observability snapshot
|
|
1589
|
+
* Return a JSON-serializable observability snapshot.
|
|
1591
1590
|
*
|
|
1592
1591
|
* `totalBytesIn` and `totalBytesOut` aggregate from live connections at call time.
|
|
1593
|
-
* Plan 04 adds full aggregation; at Plan 01 scope they return 0.
|
|
1594
1592
|
*
|
|
1595
1593
|
* @example
|
|
1596
1594
|
* ```typescript
|
|
@@ -1621,7 +1619,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1621
1619
|
};
|
|
1622
1620
|
}
|
|
1623
1621
|
/**
|
|
1624
|
-
* Emit on `'error'` only when a listener is attached
|
|
1622
|
+
* Emit on `'error'` only when a listener is attached, an unlistened
|
|
1625
1623
|
* `EventEmitter` `'error'` emission throws (see the class JSDoc error
|
|
1626
1624
|
* contract). The single guard shared by the optional-tap forwarding sites
|
|
1627
1625
|
* in this class; the constructor's net.Server forwarder deliberately does
|
|
@@ -1631,7 +1629,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1631
1629
|
/**
|
|
1632
1630
|
* Emit a server event, containing a throwing subscriber.
|
|
1633
1631
|
*
|
|
1634
|
-
* Every event this server emits is reachable from a callback we do not own
|
|
1632
|
+
* Every event this server emits is reachable from a callback we do not own, the `net.Server`
|
|
1635
1633
|
* `'connection'` listener, the `tls.Server` `'tlsClientError'` listener, a socket's `'data'`
|
|
1636
1634
|
* listener (via `Connection`), or the `catch` block of a `void`-ed async ACK task. A throwing
|
|
1637
1635
|
* subscriber would therefore unwind into that callback and kill the process (or reject a void-ed
|
|
@@ -1736,8 +1734,8 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1736
1734
|
);
|
|
1737
1735
|
}
|
|
1738
1736
|
/**
|
|
1739
|
-
* Extract a minimal, content-free peer-certificate summary (
|
|
1740
|
-
*
|
|
1737
|
+
* Extract a minimal, content-free peer-certificate summary (ATNA ITI-19
|
|
1738
|
+
* mutual authentication), CN strings, expiry, and the verification
|
|
1741
1739
|
* outcome (`authorized`) only; never the full certificate object. Returns
|
|
1742
1740
|
* `null` when no peer certificate was presented (e.g. `clientAuth: 'WANT'`
|
|
1743
1741
|
* with no client cert).
|
|
@@ -1755,7 +1753,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1755
1753
|
subjectCN: asCn(cert.subject?.CN),
|
|
1756
1754
|
issuerCN: asCn(cert.issuer?.CN),
|
|
1757
1755
|
validTo: cert.valid_to ?? null,
|
|
1758
|
-
// Verification outcome
|
|
1756
|
+
// Verification outcome, under 'WANT' a certificate can be present yet
|
|
1759
1757
|
// UNVERIFIED (see PeerCertificateSummary.authorized JSDoc).
|
|
1760
1758
|
authorized: socket.authorized === true
|
|
1761
1759
|
});
|
|
@@ -1767,12 +1765,12 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1767
1765
|
* then dispatches: resolve ⇒ `AA`; throw/reject ⇒ a negative ACK (`AE` by default,
|
|
1768
1766
|
* `AR` via {@link MllpAckError}). A positive ACK can never precede a successful commit.
|
|
1769
1767
|
*
|
|
1770
|
-
* Without an `onMessage` handler, `'AA'` degrades to a **transport-accept
|
|
1768
|
+
* Without an `onMessage` handler, `'AA'` degrades to a **transport-accept**, `AA`
|
|
1771
1769
|
* meaning "bytes received and framed", not "application-processed".
|
|
1772
1770
|
*
|
|
1773
1771
|
* A handler failure is **expected flow**, not a server error: it produces a negative
|
|
1774
1772
|
* ACK and a `'nack'` event. The thrown error's message is never placed on the wire or
|
|
1775
|
-
* in the event (it may carry PHI)
|
|
1773
|
+
* in the event (it may carry PHI), only the static `ackCode` and the inbound control
|
|
1776
1774
|
* metadata reach the peer.
|
|
1777
1775
|
*/
|
|
1778
1776
|
async _sendCommitAck(payload, meta, conn) {
|
|
@@ -1804,18 +1802,18 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1804
1802
|
}
|
|
1805
1803
|
/**
|
|
1806
1804
|
* Resolve the auto-ACK code for a message the server would otherwise acknowledge
|
|
1807
|
-
* **positively** (`AA`)
|
|
1805
|
+
* **positively** (`AA`), the no-handler transport-accept and the post-commit success
|
|
1808
1806
|
* paths. Fail-safe: downgrade to `AE` and emit a PHI-safe `'nack'` whenever the message
|
|
1809
1807
|
* cannot carry a correlatable positive ACK, so `AA` never names a control ID the sender
|
|
1810
1808
|
* cannot match (→ timeout → resend → **duplicate clinical message**).
|
|
1811
1809
|
*
|
|
1812
1810
|
* Two independent disqualifiers, one from the bytes and one from the frame:
|
|
1813
1811
|
*
|
|
1814
|
-
* - **`rawAckUncorrelatable(payload)
|
|
1812
|
+
* - **`rawAckUncorrelatable(payload)`**, no readable `MSH`, an empty MSH-10, or a
|
|
1815
1813
|
* batch/concatenated-message shape. This is the SAME predicate `buildRawAck` enforces
|
|
1816
1814
|
* on the wire; it is re-checked here so the downgrade is **observable** (a `'nack'`
|
|
1817
1815
|
* event) rather than a silent code change inside the builder.
|
|
1818
|
-
* - **`MLLP_TRAILING_BYTES`** in the frame's warnings
|
|
1816
|
+
* - **`MLLP_TRAILING_BYTES`** in the frame's warnings, a mid-payload `VT` made the
|
|
1819
1817
|
* decoder discard accumulated bytes and deliver only the fragment after it. The
|
|
1820
1818
|
* clinical message was destroyed in transit, so even a fragment that happens to carry
|
|
1821
1819
|
* a readable MSH must not be positively acknowledged. This condition is invisible to
|
|
@@ -1824,7 +1822,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1824
1822
|
* accumulating the delivered frame, so it is frame-scoped: it names *this* payload's own
|
|
1825
1823
|
* discard, never a neighbouring frame's trailing junk.
|
|
1826
1824
|
*
|
|
1827
|
-
* `'discarded-bytes'` takes precedence in the reason when both hold
|
|
1825
|
+
* `'discarded-bytes'` takes precedence in the reason when both hold, it is the root cause.
|
|
1828
1826
|
*/
|
|
1829
1827
|
_resolveAutoAckPositive(payload, meta, conn) {
|
|
1830
1828
|
const discarded = meta.warnings.some((w) => w.code === "MLLP_TRAILING_BYTES");
|
|
@@ -1845,7 +1843,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1845
1843
|
* Run `onMessage` for its side effects only (custom-ACK and manual modes), guarding
|
|
1846
1844
|
* against a synchronous throw or a rejected promise. In these modes the handler does
|
|
1847
1845
|
* **not** gate the ACK, so a failure is re-emitted as a PHI-safe `'error'` on the
|
|
1848
|
-
* connection (
|
|
1846
|
+
* connection (a handler error never crashes the server) rather than escaping as
|
|
1849
1847
|
* an unhandled rejection. The error text is never placed on the wire.
|
|
1850
1848
|
*/
|
|
1851
1849
|
_observeMessage(payload, meta, conn) {
|
|
@@ -1859,7 +1857,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1859
1857
|
/**
|
|
1860
1858
|
* The custom-builder path for `autoAck: fn`. Awaits the builder and dispatches its
|
|
1861
1859
|
* Buffer as the ACK; the caller owns MSA-1. A builder error is re-emitted as `'error'`
|
|
1862
|
-
* on the connection
|
|
1860
|
+
* on the connection, the server never crashes.
|
|
1863
1861
|
*/
|
|
1864
1862
|
async _sendCustomAck(fn, payload, meta, conn) {
|
|
1865
1863
|
try {
|
|
@@ -1873,7 +1871,7 @@ var MllpServer = class extends events.EventEmitter {
|
|
|
1873
1871
|
/**
|
|
1874
1872
|
* Frame an ACK payload and write it to the connection. `Connection.send()` returns
|
|
1875
1873
|
* `false` under backpressure (socket write buffer full); that drops the ACK and emits
|
|
1876
|
-
* a `MllpConnectionError({ phase: 'send' })` on the connection
|
|
1874
|
+
* a `MllpConnectionError({ phase: 'send' })` on the connection, the peer will
|
|
1877
1875
|
* time out waiting and may retry. The server never crashes.
|
|
1878
1876
|
*/
|
|
1879
1877
|
_dispatchAck(conn, ackPayload) {
|
|
@@ -1938,7 +1936,7 @@ var Correlator = class {
|
|
|
1938
1936
|
_sendSeq = 0;
|
|
1939
1937
|
_queueBytes = 0;
|
|
1940
1938
|
/**
|
|
1941
|
-
*
|
|
1939
|
+
* Count of pending entries with `sentAt !== null`.
|
|
1942
1940
|
* Maintained at every site that mutates `entry.sentAt` or removes a
|
|
1943
1941
|
* flushed entry: `markFlushed`, `remove`, `matchAck`, `expireDue`, `clear`.
|
|
1944
1942
|
*/
|
|
@@ -1974,7 +1972,7 @@ var Correlator = class {
|
|
|
1974
1972
|
/**
|
|
1975
1973
|
* Enqueue a new send awaiting its ACK. Returns the assigned
|
|
1976
1974
|
* `correlationKey`, or `null` if `maxInFlight` is reached (caller awaits
|
|
1977
|
-
* drain
|
|
1975
|
+
* drain, `pipeline:false`).
|
|
1978
1976
|
*/
|
|
1979
1977
|
enqueue(frame, controlIdOrNull, resolve, reject) {
|
|
1980
1978
|
if (this._pending.size >= this._opts.maxInFlight) return null;
|
|
@@ -1993,7 +1991,7 @@ var Correlator = class {
|
|
|
1993
1991
|
return key;
|
|
1994
1992
|
}
|
|
1995
1993
|
/**
|
|
1996
|
-
* Record write-flush timestamp (
|
|
1994
|
+
* Record write-flush timestamp (the clock starts at flush, NOT
|
|
1997
1995
|
* at `send()` call). No-op if key is unknown (e.g. removed by abort).
|
|
1998
1996
|
*/
|
|
1999
1997
|
markFlushed(key, now) {
|
|
@@ -2009,16 +2007,16 @@ var Correlator = class {
|
|
|
2009
2007
|
* removed from live store. Caller calls `entry.resolve(ackPayload)`.
|
|
2010
2008
|
* - controlId: keyed lookup by `controlIdFromAck`. Live-store hit returns
|
|
2011
2009
|
* the entry; graveyard hit fires `MLLP_ACK_AFTER_TIMEOUT` warning
|
|
2012
|
-
*
|
|
2013
|
-
* `onUnmatchedAck(controlIdFromAck)`
|
|
2010
|
+
* and returns `null`; otherwise fires
|
|
2011
|
+
* `onUnmatchedAck(controlIdFromAck)` and returns `null`.
|
|
2014
2012
|
*
|
|
2015
|
-
* Triggers lazy graveyard eviction
|
|
2013
|
+
* Triggers lazy graveyard eviction.
|
|
2016
2014
|
*
|
|
2017
2015
|
* @param _payload Inbound ACK bytes (framing stripped). MSA-2 extraction
|
|
2018
2016
|
* happens at MllpClient's `_onAckPayload` hook; this method takes the
|
|
2019
2017
|
* already-extracted control ID as a parameter.
|
|
2020
2018
|
* @param controlIdFromAck MSA-2 extracted from ACK (controlId mode only).
|
|
2021
|
-
* @param byteOffsetFromAck Stream offset; forwarded to `onWarning
|
|
2019
|
+
* @param byteOffsetFromAck Stream offset; forwarded to `onWarning`.
|
|
2022
2020
|
*/
|
|
2023
2021
|
matchAck(_payload, controlIdFromAck = null, byteOffsetFromAck = 0) {
|
|
2024
2022
|
this._evictGraveyardDue(this._opts.now());
|
|
@@ -2062,9 +2060,9 @@ var Correlator = class {
|
|
|
2062
2060
|
}
|
|
2063
2061
|
/**
|
|
2064
2062
|
* Sweep live entries; expire those past `sentAt + ackTimeoutMs`.
|
|
2065
|
-
* Fires `onTimeout(entry, elapsedMs)`; entries move to graveyard
|
|
2066
|
-
* Driven externally by `MllpClient`'s periodic sweep tick
|
|
2067
|
-
* itself owns no timers
|
|
2063
|
+
* Fires `onTimeout(entry, elapsedMs)`; entries move to graveyard.
|
|
2064
|
+
* Driven externally by `MllpClient`'s periodic sweep tick, Correlator
|
|
2065
|
+
* itself owns no timers.
|
|
2068
2066
|
*/
|
|
2069
2067
|
expireDue(now) {
|
|
2070
2068
|
const t = now ?? this._opts.now();
|
|
@@ -2086,8 +2084,7 @@ var Correlator = class {
|
|
|
2086
2084
|
/**
|
|
2087
2085
|
* Reject every live entry with `reason` (insertion order) and clear
|
|
2088
2086
|
* the live store. Graveyard is left intact (ages out via lazy eviction).
|
|
2089
|
-
* Used by
|
|
2090
|
-
* and `MllpClient.close()` to cancel pending sends.
|
|
2087
|
+
* Used by `MllpClient`'s correlator teardown to cancel pending sends.
|
|
2091
2088
|
*/
|
|
2092
2089
|
clear(reason) {
|
|
2093
2090
|
for (const entry of this._pending.values()) entry.reject(reason);
|
|
@@ -2095,14 +2092,14 @@ var Correlator = class {
|
|
|
2095
2092
|
this._queueBytes = 0;
|
|
2096
2093
|
this._inFlight = 0;
|
|
2097
2094
|
}
|
|
2098
|
-
/** Iterate live entries in insertion order (
|
|
2095
|
+
/** Iterate live entries in insertion order (reconnect-resend). */
|
|
2099
2096
|
*liveEntries() {
|
|
2100
2097
|
for (const entry of this._pending.values()) yield entry;
|
|
2101
2098
|
}
|
|
2102
2099
|
/**
|
|
2103
2100
|
* Remove a live entry by key WITHOUT resolving/rejecting. Returns the
|
|
2104
2101
|
* removed entry, or `null` if no entry with that key exists.
|
|
2105
|
-
* Used by `MllpClient.send()` for AbortSignal cleanup and
|
|
2102
|
+
* Used by `MllpClient.send()` for AbortSignal cleanup and by the
|
|
2106
2103
|
* reconnect-reject FSM walk.
|
|
2107
2104
|
*/
|
|
2108
2105
|
remove(key) {
|
|
@@ -2123,7 +2120,7 @@ var Correlator = class {
|
|
|
2123
2120
|
inFlight: this._inFlight
|
|
2124
2121
|
};
|
|
2125
2122
|
}
|
|
2126
|
-
/** Lazy graveyard eviction
|
|
2123
|
+
/** Lazy graveyard eviction: drop entries past `timedOutAt + 2 * ackTimeoutMs`. */
|
|
2127
2124
|
_evictGraveyardDue(now) {
|
|
2128
2125
|
const threshold = 2 * this._opts.ackTimeoutMs;
|
|
2129
2126
|
for (const [key, entry] of this._graveyard) {
|
|
@@ -2216,7 +2213,7 @@ var MllpBackpressureError = class _MllpBackpressureError extends Error {
|
|
|
2216
2213
|
queueDepth;
|
|
2217
2214
|
/** Total bytes of in-flight + queued frames at the moment of rejection. */
|
|
2218
2215
|
queueBytes;
|
|
2219
|
-
/** The high-water-mark configuration that was triggered
|
|
2216
|
+
/** The high-water-mark configuration that was triggered. */
|
|
2220
2217
|
highWaterMark;
|
|
2221
2218
|
/**
|
|
2222
2219
|
* Construct an MLLP backpressure error.
|
|
@@ -2246,15 +2243,15 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2246
2243
|
* is attached, `state` mirrors `_connection.state`.
|
|
2247
2244
|
*/
|
|
2248
2245
|
_state = "DISCONNECTED";
|
|
2249
|
-
/** Per-message ACK timeout in ms
|
|
2246
|
+
/** Per-message ACK timeout in ms. Resolved at construction. */
|
|
2250
2247
|
_ackTimeoutMs;
|
|
2251
|
-
/** controlId-mode flag
|
|
2248
|
+
/** controlId-mode flag. `false` → FIFO. */
|
|
2252
2249
|
_correlateByControlId;
|
|
2253
|
-
/** Unified ACK correlator
|
|
2250
|
+
/** Unified ACK correlator. Built during `_attachConnection`. */
|
|
2254
2251
|
_correlator = null;
|
|
2255
2252
|
/**
|
|
2256
2253
|
* Periodic ACK-timeout sweep timer. Drives `_correlator.expireDue()` because
|
|
2257
|
-
* the Correlator is timer-free
|
|
2254
|
+
* the Correlator is timer-free. Cleared on close/destroy.
|
|
2258
2255
|
*/
|
|
2259
2256
|
_ackSweepTimer = null;
|
|
2260
2257
|
// ── Reconnect state (Plan 04, CLIENT-05/06/12/17/18) ─────────────────────
|
|
@@ -2267,12 +2264,12 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2267
2264
|
/** 0-indexed attempt counter for the current reconnect cycle. */
|
|
2268
2265
|
_attempt = 0;
|
|
2269
2266
|
/**
|
|
2270
|
-
*
|
|
2267
|
+
* Total reconnect attempts since construction, surfaced as
|
|
2271
2268
|
* `getStats().reconnectAttempts`. Incremented at the entry of every
|
|
2272
2269
|
* `_handleDisconnect` invocation that proceeds to schedule a backoff.
|
|
2273
2270
|
*/
|
|
2274
2271
|
_reconnectAttempts = 0;
|
|
2275
|
-
/** Epoch ms of the last successful ACK. Drives
|
|
2272
|
+
/** Epoch ms of the last successful ACK. Drives backoff-reset. */
|
|
2276
2273
|
_lastSuccessAt = null;
|
|
2277
2274
|
/** Epoch ms when the current reconnect cycle began. `null` outside a cycle. */
|
|
2278
2275
|
_reconnectCycleStartedAt = null;
|
|
@@ -2289,12 +2286,12 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2289
2286
|
_onBackpressure;
|
|
2290
2287
|
/** Pipeline flag; default `true` (parallel up to highWaterMark). */
|
|
2291
2288
|
_pipeline;
|
|
2292
|
-
/** Dead-peer idle timer
|
|
2289
|
+
/** Dead-peer idle timer. `null` when not armed. */
|
|
2293
2290
|
_deadPeerTimer = null;
|
|
2294
2291
|
/** True once the self-'ack' listener that resets the dead-peer timer
|
|
2295
2292
|
* has been attached. Guards against duplicate listeners on reconnect. */
|
|
2296
2293
|
_ackResetWired = false;
|
|
2297
|
-
/** Most recently bound `connect()` signal. Reread on every RetryContext build
|
|
2294
|
+
/** Most recently bound `connect()` signal. Reread on every RetryContext build. */
|
|
2298
2295
|
_connectSignal;
|
|
2299
2296
|
/** Set when close()/destroy()/abort fires; reconnect handler short-circuits. */
|
|
2300
2297
|
_userClosed = false;
|
|
@@ -2303,13 +2300,13 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2303
2300
|
/** Listener-removal handle for the abort listener bound in connect(). */
|
|
2304
2301
|
_abortListener = null;
|
|
2305
2302
|
/**
|
|
2306
|
-
* Test-only seam
|
|
2303
|
+
* Test-only seam, when set, `_beginReconnectAttempt` builds the new
|
|
2307
2304
|
* Connection through this factory instead of opening a real net.Socket.
|
|
2308
2305
|
*
|
|
2309
2306
|
* @internal
|
|
2310
2307
|
*/
|
|
2311
2308
|
_reconnectFactory = null;
|
|
2312
|
-
// ── PLAN-06
|
|
2309
|
+
// ── PLAN-06, observability counters for getStats (OBS-01, D-26) ─────────
|
|
2313
2310
|
/** Total successful `conn.send()` flushes since construction. */
|
|
2314
2311
|
_sentTotal = 0;
|
|
2315
2312
|
/** Total ACKs resolved since construction. */
|
|
@@ -2323,7 +2320,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2323
2320
|
/**
|
|
2324
2321
|
* Aggregated Correlator-emitted warning counts (MLLP_ACK_*). Connection-level
|
|
2325
2322
|
* warnings are read directly from `_connection.getStats().warningsByCode`
|
|
2326
|
-
* at observation time and merged into the snapshot
|
|
2323
|
+
* at observation time and merged into the snapshot.
|
|
2327
2324
|
*/
|
|
2328
2325
|
_aggregatedWarningsByCode = {};
|
|
2329
2326
|
/**
|
|
@@ -2363,9 +2360,9 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2363
2360
|
return this._connection?.state ?? this._state;
|
|
2364
2361
|
}
|
|
2365
2362
|
/**
|
|
2366
|
-
* Open a TCP (or TLS
|
|
2367
|
-
* and attach a
|
|
2368
|
-
* enters `CONNECTED
|
|
2363
|
+
* Open a TCP (or TLS) connection to the configured `host:port`
|
|
2364
|
+
* and attach a {@link Connection} to it. Resolves once the FSM
|
|
2365
|
+
* enters `CONNECTED`, for TLS, on `'secureConnect'` (handshake complete,
|
|
2369
2366
|
* including certificate verification when it is on).
|
|
2370
2367
|
*
|
|
2371
2368
|
* Rejects with:
|
|
@@ -2385,11 +2382,11 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2385
2382
|
*
|
|
2386
2383
|
* **TLS 1.3 + mutual TLS caveat (RFC 8446 §4.4.2):** `connect()` resolving
|
|
2387
2384
|
* does NOT guarantee that a `clientAuth: 'MUST'` server accepted your
|
|
2388
|
-
* client certificate. Under TLS 1.3 the client's handshake
|
|
2389
|
-
* `'secureConnect'
|
|
2385
|
+
* client certificate. Under TLS 1.3 the client's handshake, and its
|
|
2386
|
+
* `'secureConnect'`, can complete before the server finishes validating
|
|
2390
2387
|
* the certificate; a rejection then surfaces moments later as a typed
|
|
2391
2388
|
* post-connect error (`'error'` event with an `ERR_SSL_*`/alert cause,
|
|
2392
|
-
* classified **permanent
|
|
2389
|
+
* classified **permanent**, no auto-reconnect loop). ACK correlation via
|
|
2393
2390
|
* {@link MllpClient.send} remains the delivery guarantee: no send resolves
|
|
2394
2391
|
* without its ACK, so a rejected session can never silently "deliver".
|
|
2395
2392
|
*
|
|
@@ -2472,8 +2469,8 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2472
2469
|
* Build the raw socket + `Transport` pair for a connect / reconnect attempt.
|
|
2473
2470
|
*
|
|
2474
2471
|
* Plaintext (`ClientOptions.tls` unset): a `net.Socket` wrapped in
|
|
2475
|
-
* `NetTransport`. TLS
|
|
2476
|
-
* `TlsTransport
|
|
2472
|
+
* `NetTransport`. TLS: a `tls.TLSSocket` wrapped in
|
|
2473
|
+
* `TlsTransport`, verification defaults **on**
|
|
2477
2474
|
* (`rejectUnauthorized: !allowUnverified`), floor `minVersion: 'TLSv1.2'`
|
|
2478
2475
|
* (the IHE ATNA ITI-19 BCP195 floor), `servername` defaulting to
|
|
2479
2476
|
* `ClientOptions.host`.
|
|
@@ -2502,14 +2499,14 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2502
2499
|
}
|
|
2503
2500
|
/**
|
|
2504
2501
|
* Wrap a connect-phase socket error as `MllpConnectionError`, classifying
|
|
2505
|
-
* TLS failures
|
|
2502
|
+
* TLS failures into the additive `connectionCause`:
|
|
2506
2503
|
*
|
|
2507
|
-
* - `'tls-verify'
|
|
2504
|
+
* - `'tls-verify'`, certificate-verification failures
|
|
2508
2505
|
* ({@link isTlsVerificationErrorCode}).
|
|
2509
|
-
* - `'tls-handshake'
|
|
2506
|
+
* - `'tls-handshake'`, TLS-**protocol**-shaped failures only
|
|
2510
2507
|
* ({@link isTlsProtocolError}): `ERR_SSL_*`, `EPROTO`, OpenSSL
|
|
2511
2508
|
* alert-bearing errors.
|
|
2512
|
-
* - **No `connectionCause
|
|
2509
|
+
* - **No `connectionCause`**, pure TCP-level failures (`ECONNREFUSED`,
|
|
2513
2510
|
* `ETIMEDOUT`, …) even on a TLS-configured connection; these carry the
|
|
2514
2511
|
* same shape as plaintext connect failures.
|
|
2515
2512
|
*/
|
|
@@ -2537,7 +2534,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2537
2534
|
/**
|
|
2538
2535
|
* Emit a client event, containing a throwing subscriber.
|
|
2539
2536
|
*
|
|
2540
|
-
* Every event this client emits is reached from a callback we do not own
|
|
2537
|
+
* Every event this client emits is reached from a callback we do not own, a socket's
|
|
2541
2538
|
* `'connect'`/`'secureConnect'`/`'data'`/`'error'` listener, or a backoff timer. A throwing
|
|
2542
2539
|
* subscriber would unwind into it and kill the process, and on several paths it would also skip
|
|
2543
2540
|
* the work queued after the emit (the `resolve()` of `connect()`, the backoff scheduling).
|
|
@@ -2550,8 +2547,8 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2550
2547
|
});
|
|
2551
2548
|
}
|
|
2552
2549
|
/**
|
|
2553
|
-
* Emit the per-connection insecure-TLS warning
|
|
2554
|
-
* `tls.allowUnverified === true
|
|
2550
|
+
* Emit the per-connection insecure-TLS warning when
|
|
2551
|
+
* `tls.allowUnverified === true`, fires on EVERY successful
|
|
2555
2552
|
* `secureConnect`, initial connect and every reconnect. Emits both a frozen
|
|
2556
2553
|
* `'securityWarning'` event and `process.emitWarning`. No-op for plaintext
|
|
2557
2554
|
* connections or when verification is on.
|
|
@@ -2560,7 +2557,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2560
2557
|
const tlsOpt = this._opts.tls;
|
|
2561
2558
|
if (tlsOpt === void 0 || tlsOpt === true) return;
|
|
2562
2559
|
if (tlsOpt.allowUnverified !== true) return;
|
|
2563
|
-
const message = "MLLP TLS certificate verification is DISABLED (allowUnverified: true)
|
|
2560
|
+
const message = "MLLP TLS certificate verification is DISABLED (allowUnverified: true), this connection does not authenticate the peer.";
|
|
2564
2561
|
const warning = Object.freeze({
|
|
2565
2562
|
code: MLLP_TLS_VERIFY_DISABLED,
|
|
2566
2563
|
message,
|
|
@@ -2573,11 +2570,11 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2573
2570
|
}
|
|
2574
2571
|
/**
|
|
2575
2572
|
* Wire a Connection's events through to this MllpClient. Every re-emitted
|
|
2576
|
-
* payload is `Object.freeze`'d before emission
|
|
2577
|
-
* Connection layer already freezes
|
|
2573
|
+
* payload is `Object.freeze`'d before emission, even though the
|
|
2574
|
+
* Connection layer already freezes, defense-in-depth, harmless on
|
|
2578
2575
|
* already-frozen objects.
|
|
2579
2576
|
*
|
|
2580
|
-
* Builds the unified `Correlator`
|
|
2577
|
+
* Builds the unified `Correlator` bound to this Connection and
|
|
2581
2578
|
* arms the periodic ACK-timeout sweep. The Correlator is teardown-aware:
|
|
2582
2579
|
* `close()` / `destroy()` clear the sweep timer and reject pending sends.
|
|
2583
2580
|
*
|
|
@@ -2588,7 +2585,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2588
2585
|
this._correlator = new Correlator({
|
|
2589
2586
|
mode: this._correlateByControlId ? "controlId" : "fifo",
|
|
2590
2587
|
ackTimeoutMs: this._ackTimeoutMs,
|
|
2591
|
-
// Plan 05
|
|
2588
|
+
// Plan 05, pipeline:false collapses the in-flight set to ≤1 (D-06).
|
|
2592
2589
|
maxInFlight: this._pipeline ? Number.POSITIVE_INFINITY : 1,
|
|
2593
2590
|
onWarning: (code, ctx) => {
|
|
2594
2591
|
this._aggregatedWarningsByCode[code] = (this._aggregatedWarningsByCode[code] ?? 0) + 1;
|
|
@@ -2687,7 +2684,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2687
2684
|
}
|
|
2688
2685
|
}
|
|
2689
2686
|
/**
|
|
2690
|
-
* Arm (or re-arm) the dead-peer idle timer
|
|
2687
|
+
* Arm (or re-arm) the dead-peer idle timer.
|
|
2691
2688
|
* No-op when `deadPeerTimeoutMs` is unset.
|
|
2692
2689
|
*/
|
|
2693
2690
|
_armDeadPeerTimer() {
|
|
@@ -2701,8 +2698,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2701
2698
|
this._deadPeerTimer.unref();
|
|
2702
2699
|
}
|
|
2703
2700
|
/**
|
|
2704
|
-
* Clear the dead-peer idle timer
|
|
2705
|
-
* every transition out of CONNECTED).
|
|
2701
|
+
* Clear the dead-peer idle timer on every transition out of CONNECTED.
|
|
2706
2702
|
*/
|
|
2707
2703
|
_clearDeadPeerTimer() {
|
|
2708
2704
|
if (this._deadPeerTimer !== null) {
|
|
@@ -2711,22 +2707,22 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2711
2707
|
}
|
|
2712
2708
|
}
|
|
2713
2709
|
/**
|
|
2714
|
-
* Disconnect handler
|
|
2710
|
+
* Disconnect handler, reconnect FSM core.
|
|
2715
2711
|
*
|
|
2716
2712
|
* Implements:
|
|
2717
|
-
* -
|
|
2713
|
+
* - Hybrid in-flight handling: controlId mode preserves
|
|
2718
2714
|
* pending sends for resend; FIFO mode rejects in-flight with
|
|
2719
2715
|
* `connectionCause: 'in-flight-orphan'` and queued with `'fifo-unsafe'`.
|
|
2720
|
-
* -
|
|
2716
|
+
* - Classifier-first: permanent errors
|
|
2721
2717
|
* transition straight to CLOSED without invoking `retryStrategy`.
|
|
2722
|
-
* -
|
|
2718
|
+
* - Backoff-reset on recent success: first disconnect after a
|
|
2723
2719
|
* successful ACK on the prior session resets `_attempt` to 0.
|
|
2724
|
-
* -
|
|
2725
|
-
* -
|
|
2726
|
-
* -
|
|
2720
|
+
* - `_reconnectAttempts` counter increment.
|
|
2721
|
+
* - Frozen RetryContext; a `null` return from the strategy halts reconnection.
|
|
2722
|
+
* - Default exponential strategy.
|
|
2727
2723
|
*
|
|
2728
|
-
* Invoked from the SINGLE `_onStateChange` hook
|
|
2729
|
-
*
|
|
2724
|
+
* Invoked from the SINGLE `_onStateChange` hook, no parallel listener.
|
|
2725
|
+
* Idempotent across same-cycle re-entry: cycle-start flag
|
|
2730
2726
|
* coordinates first-disconnect vs subsequent-within-cycle behavior.
|
|
2731
2727
|
*/
|
|
2732
2728
|
_handleDisconnect(err) {
|
|
@@ -2826,7 +2822,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2826
2822
|
this._backoffTimer.unref();
|
|
2827
2823
|
}
|
|
2828
2824
|
/**
|
|
2829
|
-
* Default retry strategy
|
|
2825
|
+
* Default retry strategy: `min(maxDelay, initialDelay * multiplier^attempt)`
|
|
2830
2826
|
* with ±jitter applied.
|
|
2831
2827
|
*/
|
|
2832
2828
|
_defaultRetryStrategy = (ctx) => {
|
|
@@ -2914,7 +2910,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2914
2910
|
this._reconnectCycleStartedAt = null;
|
|
2915
2911
|
}
|
|
2916
2912
|
/**
|
|
2917
|
-
* Test seam
|
|
2913
|
+
* Test seam, install a factory that produces the next reconnect Connection.
|
|
2918
2914
|
*
|
|
2919
2915
|
* @internal
|
|
2920
2916
|
*/
|
|
@@ -2922,7 +2918,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2922
2918
|
this._reconnectFactory = factory;
|
|
2923
2919
|
}
|
|
2924
2920
|
/**
|
|
2925
|
-
* Test seam
|
|
2921
|
+
* Test seam, capture or rebind the connect-signal mid-flight.
|
|
2926
2922
|
*
|
|
2927
2923
|
* @internal
|
|
2928
2924
|
*/
|
|
@@ -2948,12 +2944,11 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2948
2944
|
*
|
|
2949
2945
|
* - FIFO mode: passes `null` controlId so `matchAck` returns the head of
|
|
2950
2946
|
* the live store.
|
|
2951
|
-
* - controlId mode: extracts MSA-2
|
|
2952
|
-
*
|
|
2947
|
+
* - controlId mode: extracts MSA-2 and passes it to `matchAck` for keyed
|
|
2948
|
+
* lookup.
|
|
2953
2949
|
*
|
|
2954
2950
|
* Called from the SINGLE `'message'` listener registered in `_attachConnection`.
|
|
2955
|
-
* No parallel listener is registered
|
|
2956
|
-
* anchor (B-04).
|
|
2951
|
+
* No parallel listener is registered.
|
|
2957
2952
|
*/
|
|
2958
2953
|
_onAckPayload(ackPayload, byteOffset) {
|
|
2959
2954
|
if (this._correlator === null) return;
|
|
@@ -2966,10 +2961,8 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2966
2961
|
/**
|
|
2967
2962
|
* Single source-of-truth for a successfully matched ACK (live-store hit).
|
|
2968
2963
|
*
|
|
2969
|
-
*
|
|
2970
|
-
*
|
|
2971
|
-
* PLAN-06 extends at HOOK_EXTENSION_POINT: ack-matched to bump _ackedTotal and
|
|
2972
|
-
* set _lastAckAt.
|
|
2964
|
+
* Emits the frozen 'ack' event and resolves the pending send. Updates
|
|
2965
|
+
* `_lastSuccessAt`, `_ackedTotal` and `_lastAckAt`.
|
|
2973
2966
|
*
|
|
2974
2967
|
* Called from _onAckPayload when matchAck() returns a non-null PendingAck.
|
|
2975
2968
|
*/
|
|
@@ -2991,9 +2984,9 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
2991
2984
|
}
|
|
2992
2985
|
/**
|
|
2993
2986
|
* Emit a frozen `'drain'` event when the queue depth and bytes fall below
|
|
2994
|
-
* both configured caps
|
|
2987
|
+
* both configured caps. Called from `_onAckMatched`
|
|
2995
2988
|
* (every successful ACK) and from the Correlator's `onTimeout` callback
|
|
2996
|
-
* (every expired send)
|
|
2989
|
+
* (every expired send), both code paths free a live-store slot.
|
|
2997
2990
|
*/
|
|
2998
2991
|
_maybeEmitDrain() {
|
|
2999
2992
|
const corr = this._correlator;
|
|
@@ -3013,12 +3006,9 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
3013
3006
|
/**
|
|
3014
3007
|
* Single source-of-truth for Connection FSM transitions.
|
|
3015
3008
|
*
|
|
3016
|
-
*
|
|
3017
|
-
*
|
|
3018
|
-
*
|
|
3019
|
-
* CONNECTED → DISCONNECTED|RECONNECTING and trigger _handleDisconnect.
|
|
3020
|
-
* Plan 05 extends at HOOK_EXTENSION_POINT: state-change to clear/arm
|
|
3021
|
-
* dead-peer timer on transitions out of / into CONNECTED.
|
|
3009
|
+
* Re-emits a frozen 'stateChange' event, detects
|
|
3010
|
+
* CONNECTED → DISCONNECTED|RECONNECTING to trigger `_handleDisconnect`, and
|
|
3011
|
+
* clears or arms the dead-peer timer on transitions out of / into CONNECTED.
|
|
3022
3012
|
*
|
|
3023
3013
|
* Called from the SINGLE 'stateChange' listener registered in _attachConnection.
|
|
3024
3014
|
*/
|
|
@@ -3049,17 +3039,16 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
3049
3039
|
}
|
|
3050
3040
|
}
|
|
3051
3041
|
/**
|
|
3052
|
-
* Send an MLLP-framed payload and await the inbound ACK
|
|
3042
|
+
* Send an MLLP-framed payload and await the inbound ACK.
|
|
3053
3043
|
*
|
|
3054
3044
|
* Resolves with the ACK Buffer (framing stripped). Rejects with:
|
|
3055
|
-
* - `DOMException('Aborted', 'AbortError')` if `signal` aborts before ACK
|
|
3056
|
-
*
|
|
3057
|
-
* - `MllpTimeoutError` if no ACK arrives within `ackTimeoutMs` (ERR-02).
|
|
3045
|
+
* - `DOMException('Aborted', 'AbortError')` if `signal` aborts before the ACK.
|
|
3046
|
+
* - `MllpTimeoutError` if no ACK arrives within `ackTimeoutMs`.
|
|
3058
3047
|
* The clock starts at the underlying `write()` flush callback, NOT at
|
|
3059
|
-
* the `send()` call
|
|
3048
|
+
* the `send()` call.
|
|
3060
3049
|
* - `MllpConnectionError({ phase: 'send' })` if the client is not connected.
|
|
3061
3050
|
*
|
|
3062
|
-
* Emits a frozen `'ack'` event on every successful match
|
|
3051
|
+
* Emits a frozen `'ack'` event on every successful match.
|
|
3063
3052
|
*
|
|
3064
3053
|
* @example
|
|
3065
3054
|
* ```typescript
|
|
@@ -3068,7 +3057,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
3068
3057
|
* ```
|
|
3069
3058
|
*
|
|
3070
3059
|
* @param payload Raw bytes; MLLP framing is added internally via `encodeFrame`.
|
|
3071
|
-
* @param opts.signal AbortSignal
|
|
3060
|
+
* @param opts.signal AbortSignal, aborting cancels the ACK wait.
|
|
3072
3061
|
*/
|
|
3073
3062
|
send(payload, opts) {
|
|
3074
3063
|
const signal = opts?.signal;
|
|
@@ -3156,12 +3145,12 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
3156
3145
|
});
|
|
3157
3146
|
}
|
|
3158
3147
|
/**
|
|
3159
|
-
* 'wait'-mode backpressure handler
|
|
3148
|
+
* 'wait'-mode backpressure handler.
|
|
3160
3149
|
*
|
|
3161
3150
|
* Awaits one of three terminating signals, in order:
|
|
3162
3151
|
* - `'drain'` event → re-enter `send()` (the gate will now pass).
|
|
3163
3152
|
* - `ackTimeoutMs` elapses → reject with `MllpTimeoutError`.
|
|
3164
|
-
* - Caller's `signal` aborts → reject with `AbortError
|
|
3153
|
+
* - Caller's `signal` aborts → reject with `AbortError`. Cleanup
|
|
3165
3154
|
* removes the drain listener AND the abort listener AND clears the
|
|
3166
3155
|
* timer to prevent leaks.
|
|
3167
3156
|
*/
|
|
@@ -3218,7 +3207,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
3218
3207
|
this._clearDeadPeerTimer();
|
|
3219
3208
|
}
|
|
3220
3209
|
/**
|
|
3221
|
-
* **Test seam
|
|
3210
|
+
* **Test seam**, attach an externally-built {@link Connection} directly,
|
|
3222
3211
|
* bypassing the `net.createConnection` + `NetTransport` path. Used by
|
|
3223
3212
|
* lifecycle tests driving `InMemoryTransport.pair()` for determinism.
|
|
3224
3213
|
*
|
|
@@ -3297,7 +3286,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
3297
3286
|
}
|
|
3298
3287
|
}
|
|
3299
3288
|
/**
|
|
3300
|
-
* Abruptly destroy the client
|
|
3289
|
+
* Abruptly destroy the client, force-transitions the underlying Connection
|
|
3301
3290
|
* to `CLOSED` immediately. No-op if no Connection is attached. Idempotent.
|
|
3302
3291
|
*
|
|
3303
3292
|
* @example
|
|
@@ -3321,9 +3310,9 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
3321
3310
|
conn.destroy(reason);
|
|
3322
3311
|
}
|
|
3323
3312
|
/**
|
|
3324
|
-
* Returns a JSON-serializable observability snapshot
|
|
3313
|
+
* Returns a JSON-serializable observability snapshot.
|
|
3325
3314
|
*
|
|
3326
|
-
* All fields are plain values
|
|
3315
|
+
* All fields are plain values, no Buffers, no class instances, no Maps,
|
|
3327
3316
|
* no circular refs. Safe to `JSON.stringify` directly.
|
|
3328
3317
|
*
|
|
3329
3318
|
* `inFlight` is the count of correlator entries with `sentAt !== null`
|
|
@@ -3367,7 +3356,7 @@ var MllpClient = class extends events.EventEmitter {
|
|
|
3367
3356
|
};
|
|
3368
3357
|
}
|
|
3369
3358
|
/**
|
|
3370
|
-
* Async disposal
|
|
3359
|
+
* Async disposal, delegates to {@link MllpClient.close} for `await using` support.
|
|
3371
3360
|
*
|
|
3372
3361
|
* @example
|
|
3373
3362
|
* ```typescript
|
|
@@ -3423,7 +3412,7 @@ async function createStarterClient(opts) {
|
|
|
3423
3412
|
}
|
|
3424
3413
|
|
|
3425
3414
|
// src/index.ts
|
|
3426
|
-
var VERSION = "0.0.
|
|
3415
|
+
var VERSION = "0.0.3";
|
|
3427
3416
|
|
|
3428
3417
|
exports.Connection = Connection;
|
|
3429
3418
|
exports.FrameReader = FrameReader;
|