@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.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { T as Transport } from './index-
|
|
1
|
+
import { T as Transport } from './index-CD8V2Mtk.cjs';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import { Socket } from 'node:net';
|
|
4
4
|
import { TLSSocket } from 'node:tls';
|
|
@@ -20,7 +20,7 @@ import { TLSSocket } from 'node:tls';
|
|
|
20
20
|
/**
|
|
21
21
|
* Union of all stable MLLP warning codes.
|
|
22
22
|
*
|
|
23
|
-
* These codes are a **public API
|
|
23
|
+
* These codes are a **public API**, they appear in `onWarning` handlers, log pipelines,
|
|
24
24
|
* monitoring dashboards, and error messages. Renaming is a breaking change.
|
|
25
25
|
*
|
|
26
26
|
* @example
|
|
@@ -32,8 +32,8 @@ type WarningCode = "MLLP_MISSING_LEADING_VT" | "MLLP_FS_WITHOUT_CR" | "MLLP_LF_A
|
|
|
32
32
|
/**
|
|
33
33
|
* A frozen warning object emitted when the decoder tolerates a framing deviation.
|
|
34
34
|
*
|
|
35
|
-
* `connectionId` is `undefined` when emitted by a standalone `FrameReader
|
|
36
|
-
*
|
|
35
|
+
* `connectionId` is `undefined` when emitted by a standalone `FrameReader`.
|
|
36
|
+
* `Connection` enriches it to the real UUIDv4 before forwarding upstream.
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
39
39
|
* ```typescript
|
|
@@ -52,7 +52,7 @@ interface MllpWarning {
|
|
|
52
52
|
readonly byteOffset: number;
|
|
53
53
|
/**
|
|
54
54
|
* Connection identifier. `undefined` at the framing layer;
|
|
55
|
-
* enriched by
|
|
55
|
+
* enriched by `Connection` before emitting upstream.
|
|
56
56
|
*/
|
|
57
57
|
readonly connectionId: string | undefined;
|
|
58
58
|
/** Wall-clock time at point of emission. */
|
|
@@ -62,8 +62,8 @@ interface MllpWarning {
|
|
|
62
62
|
* Create a frozen `MllpWarning` object. The returned object is `Object.freeze()`'d
|
|
63
63
|
* so subscribers cannot mutate shared warning state.
|
|
64
64
|
*
|
|
65
|
-
* `connectionId` is always `undefined` here
|
|
66
|
-
* `{ ...w, connectionId: this.connectionId }` then re-freezes
|
|
65
|
+
* `connectionId` is always `undefined` here; `Connection` enriches it via
|
|
66
|
+
* `{ ...w, connectionId: this.connectionId }` then re-freezes.
|
|
67
67
|
*
|
|
68
68
|
* @example
|
|
69
69
|
* ```typescript
|
|
@@ -94,15 +94,15 @@ declare function createWarning(code: WarningCode, byteOffset: number, message: s
|
|
|
94
94
|
/**
|
|
95
95
|
* Thrown for unrecoverable MLLP wire-format violations.
|
|
96
96
|
*
|
|
97
|
-
* - `code
|
|
98
|
-
* - `byteOffset
|
|
99
|
-
* - `snippet
|
|
97
|
+
* - `code`, stable `WarningCode` identifying the violation
|
|
98
|
+
* - `byteOffset`, absolute stream position where the violation was detected
|
|
99
|
+
* - `snippet`, the single framing-boundary byte that broke the structure, or empty when the
|
|
100
100
|
* anomaly is not a specific byte. **Never a run of payload content** (see the `snippet` PHI
|
|
101
101
|
* contract below); the constructor caps whatever it is given to 64 bytes as a backstop.
|
|
102
102
|
*
|
|
103
103
|
* @example
|
|
104
104
|
* ```typescript
|
|
105
|
-
* // Pass only the offending framing-boundary byte
|
|
105
|
+
* // Pass only the offending framing-boundary byte, never a slice of payload content.
|
|
106
106
|
* throw new MllpFramingError('MLLP_MISSING_LEADING_VT', byteOffset, Buffer.from([byte]));
|
|
107
107
|
* ```
|
|
108
108
|
*/
|
|
@@ -115,13 +115,13 @@ declare class MllpFramingError extends Error {
|
|
|
115
115
|
/**
|
|
116
116
|
* Up to 64 bytes copied from around the anomaly.
|
|
117
117
|
*
|
|
118
|
-
* This is a **copied** Buffer
|
|
118
|
+
* This is a **copied** Buffer, isolated from the source buffer so it remains
|
|
119
119
|
* valid after the underlying buffer is reused or overwritten.
|
|
120
120
|
*
|
|
121
|
-
* **PHI contract
|
|
121
|
+
* **PHI contract:** the decoder only ever populates this with the single
|
|
122
122
|
* framing-boundary byte that violated the frame structure (whose hex value the
|
|
123
|
-
* `message` already discloses)
|
|
124
|
-
* whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE
|
|
123
|
+
* `message` already discloses), never a run of payload content bytes. For anomalies
|
|
124
|
+
* whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE`, the accumulated size),
|
|
125
125
|
* the snippet is **empty**: a payload slice on a public error field would leak a
|
|
126
126
|
* field-body slice of clinical content. Callers constructing this directly are
|
|
127
127
|
* responsible for the same discipline.
|
|
@@ -141,10 +141,10 @@ declare class MllpFramingError extends Error {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
* MLLP frame encoder
|
|
144
|
+
* MLLP frame encoder, canonical `VT + payload + FS + CR` wrapping.
|
|
145
145
|
*
|
|
146
146
|
* The encoder is a **strict emitter** (Postel's Law: conservative on write).
|
|
147
|
-
* There is no option to loosen the output format
|
|
147
|
+
* There is no option to loosen the output format, it always emits canonical framing.
|
|
148
148
|
* Tolerance for delimiter bytes in the payload is available via `allowDelimiterBytesInPayload`,
|
|
149
149
|
* which passes bytes through verbatim and emits an `MllpWarning` for each offending byte.
|
|
150
150
|
*
|
|
@@ -175,12 +175,12 @@ interface EncoderOptions {
|
|
|
175
175
|
* verbatim in the output frame instead of throwing `MllpFramingError`.
|
|
176
176
|
* An `MllpWarning` is emitted per offending byte if `onWarning` is provided.
|
|
177
177
|
*
|
|
178
|
-
* Default: `false` (strict
|
|
178
|
+
* Default: `false` (strict, throws on delimiter bytes).
|
|
179
179
|
*/
|
|
180
180
|
allowDelimiterBytesInPayload?: boolean;
|
|
181
181
|
/**
|
|
182
182
|
* Called for each offending delimiter byte when `allowDelimiterBytesInPayload` is `true`.
|
|
183
|
-
* Invocation is wrapped in try/catch
|
|
183
|
+
* Invocation is wrapped in try/catch, a throwing handler does not interrupt encoding.
|
|
184
184
|
*
|
|
185
185
|
* @example
|
|
186
186
|
* ```typescript
|
|
@@ -211,11 +211,11 @@ interface EncoderOptions {
|
|
|
211
211
|
* ```typescript
|
|
212
212
|
* import { encodeFrame } from '@cosyte/mllp';
|
|
213
213
|
*
|
|
214
|
-
* // Strict (default)
|
|
214
|
+
* // Strict (default), throws on delimiter bytes in payload
|
|
215
215
|
* const frame = encodeFrame(Buffer.from('MSH|^~\\&|SEND|FAC|RECV|FAC|...'));
|
|
216
216
|
* socket.write(frame);
|
|
217
217
|
*
|
|
218
|
-
* // Tolerant
|
|
218
|
+
* // Tolerant, passes delimiter bytes through with a warning
|
|
219
219
|
* const frame2 = encodeFrame(dirtyPayload, {
|
|
220
220
|
* allowDelimiterBytesInPayload: true,
|
|
221
221
|
* onWarning: (w) => logger.warn(w),
|
|
@@ -225,13 +225,13 @@ interface EncoderOptions {
|
|
|
225
225
|
declare function encodeFrame(payload: Buffer, opts?: EncoderOptions): Buffer;
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
|
-
* MLLP frame decoder
|
|
228
|
+
* MLLP frame decoder, stateful 3-state FSM for chunked byte-stream parsing.
|
|
229
229
|
*
|
|
230
230
|
* Feed raw TCP chunks via `push(chunk)`. Complete MLLP frames fire synchronously
|
|
231
231
|
* via the `onFrame` callback during each `push()` call.
|
|
232
232
|
*
|
|
233
233
|
* The decoder is **strict by default**: any framing deviation throws `MllpFramingError`
|
|
234
|
-
* unless the matching tolerance option is explicitly enabled (Postel's Law
|
|
234
|
+
* unless the matching tolerance option is explicitly enabled (Postel's Law, liberal receiver
|
|
235
235
|
* means explicit tolerance, not silent acceptance).
|
|
236
236
|
*
|
|
237
237
|
* @example
|
|
@@ -252,9 +252,9 @@ declare function encodeFrame(payload: Buffer, opts?: EncoderOptions): Buffer;
|
|
|
252
252
|
/**
|
|
253
253
|
* Options for `FrameReader`.
|
|
254
254
|
*
|
|
255
|
-
* All tolerance opts default to `false
|
|
255
|
+
* All tolerance opts default to `false`, every framing deviation throws unless
|
|
256
256
|
* explicitly enabled. Server-level defaults (allowFsOnly, allowLfAfterFs, allowLeadingWhitespace)
|
|
257
|
-
* are applied by
|
|
257
|
+
* are applied by the server when constructing readers.
|
|
258
258
|
*
|
|
259
259
|
* @example
|
|
260
260
|
* ```typescript
|
|
@@ -279,27 +279,27 @@ interface FrameReaderOptions {
|
|
|
279
279
|
*/
|
|
280
280
|
onFrame: (payload: Buffer, byteOffset: number, warnings: readonly MllpWarning[]) => void;
|
|
281
281
|
/**
|
|
282
|
-
* Called for each tolerated framing deviation. Wrapped in try/catch
|
|
282
|
+
* Called for each tolerated framing deviation. Wrapped in try/catch.
|
|
283
283
|
* A throwing handler will not corrupt FSM state.
|
|
284
284
|
*/
|
|
285
285
|
onWarning?: (w: MllpWarning) => void;
|
|
286
286
|
/**
|
|
287
287
|
* Maximum accumulated payload bytes before `MllpFramingError('MLLP_FRAME_TOO_LARGE')`.
|
|
288
|
-
* Default: 16 MiB (
|
|
288
|
+
* Default: 16 MiB (DoS prevention).
|
|
289
289
|
*/
|
|
290
290
|
maxFrameSizeBytes?: number;
|
|
291
|
-
/**
|
|
291
|
+
/** Tolerate FS without trailing CR; emits `MLLP_FS_WITHOUT_CR`. */
|
|
292
292
|
allowFsOnly?: boolean;
|
|
293
|
-
/**
|
|
293
|
+
/** Tolerate FS+LF instead of FS+CR; emits `MLLP_LF_AFTER_FS`. */
|
|
294
294
|
allowLfAfterFs?: boolean;
|
|
295
|
-
/**
|
|
295
|
+
/** Tolerate missing leading VT; emits `MLLP_MISSING_LEADING_VT`. */
|
|
296
296
|
allowMissingLeadingVt?: boolean;
|
|
297
|
-
/**
|
|
297
|
+
/** Tolerate SP/TAB/LF/CR before VT; emits `MLLP_LEADING_WHITESPACE`. */
|
|
298
298
|
allowLeadingWhitespace?: boolean;
|
|
299
299
|
/**
|
|
300
300
|
* When `true`, escalates the following tolerances to thrown `MllpFramingError`
|
|
301
301
|
* even if individual opt-ins (`allowFsOnly`, `allowLfAfterFs`, `allowMissingLeadingVt`,
|
|
302
|
-
* `allowLeadingWhitespace`) are enabled
|
|
302
|
+
* `allowLeadingWhitespace`) are enabled:
|
|
303
303
|
* - `MLLP_MISSING_LEADING_VT`
|
|
304
304
|
* - `MLLP_FS_WITHOUT_CR`
|
|
305
305
|
* - `MLLP_LF_AFTER_FS`
|
|
@@ -309,7 +309,7 @@ interface FrameReaderOptions {
|
|
|
309
309
|
*
|
|
310
310
|
* @example
|
|
311
311
|
* ```typescript
|
|
312
|
-
* // Hardened enforcement
|
|
312
|
+
* // Hardened enforcement, no tolerance, all violations throw
|
|
313
313
|
* const reader = new FrameReader({ onFrame: fn, strict: true });
|
|
314
314
|
* ```
|
|
315
315
|
*/
|
|
@@ -320,9 +320,9 @@ interface FrameReaderOptions {
|
|
|
320
320
|
* Complete frames fire synchronously via `onFrame` callback during `push()`.
|
|
321
321
|
*
|
|
322
322
|
* The decoder operates as a 3-state FSM:
|
|
323
|
-
* - `SCANNING_FOR_VT
|
|
324
|
-
* - `READING_PAYLOAD
|
|
325
|
-
* - `EXPECTING_CR
|
|
323
|
+
* - `SCANNING_FOR_VT`, waiting for the 0x0B frame-start byte
|
|
324
|
+
* - `READING_PAYLOAD`, accumulating payload bytes until FS (0x1C)
|
|
325
|
+
* - `EXPECTING_CR`, received FS, waiting for CR (0x0D) to complete the frame
|
|
326
326
|
*
|
|
327
327
|
* @example
|
|
328
328
|
* ```typescript
|
|
@@ -337,19 +337,19 @@ declare class FrameReader {
|
|
|
337
337
|
private readonly _opts;
|
|
338
338
|
private readonly _maxFrameSize;
|
|
339
339
|
private _state;
|
|
340
|
-
/** Reusable accumulator buffer
|
|
340
|
+
/** Reusable accumulator buffer, grown by doubling as needed. */
|
|
341
341
|
private _accumulator;
|
|
342
342
|
/** Number of payload bytes written to accumulator in current frame. */
|
|
343
343
|
private _writePos;
|
|
344
344
|
/** Monotonic absolute stream byte offset across the entire connection lifetime. */
|
|
345
345
|
private _byteOffset;
|
|
346
|
-
/** Byte offset where current leading-whitespace run started
|
|
346
|
+
/** Byte offset where current leading-whitespace run started. */
|
|
347
347
|
private _wsStart;
|
|
348
|
-
/** Count of leading whitespace bytes accumulated in current run
|
|
348
|
+
/** Count of leading whitespace bytes accumulated in current run. */
|
|
349
349
|
private _wsCount;
|
|
350
350
|
/** Byte offset of the VT byte that started the current frame. */
|
|
351
351
|
private _frameStartOffset;
|
|
352
|
-
/** Per-frame warning accumulator
|
|
352
|
+
/** Per-frame warning accumulator, cleared after each _deliverFrame(). */
|
|
353
353
|
private _frameWarnings;
|
|
354
354
|
/**
|
|
355
355
|
* Construct a chunked MLLP frame reader.
|
|
@@ -371,7 +371,7 @@ declare class FrameReader {
|
|
|
371
371
|
/**
|
|
372
372
|
* Clear internal accumulator state and reset byte offset to 0.
|
|
373
373
|
*
|
|
374
|
-
* Call on reconnect / connection reuse to start fresh without allocating a new reader
|
|
374
|
+
* Call on reconnect / connection reuse to start fresh without allocating a new reader.
|
|
375
375
|
* Pending partial frame state is discarded silently.
|
|
376
376
|
*
|
|
377
377
|
* @example
|
|
@@ -388,15 +388,15 @@ declare class FrameReader {
|
|
|
388
388
|
private _appendByte;
|
|
389
389
|
/**
|
|
390
390
|
* Copy accumulated payload bytes into a new Buffer and deliver via `onFrame`.
|
|
391
|
-
* The copy isolates callers from internal accumulator reuse
|
|
391
|
+
* The copy isolates callers from internal accumulator reuse.
|
|
392
392
|
*/
|
|
393
393
|
private _deliverFrame;
|
|
394
|
-
/** Emit a warning via the `onWarning` callback, swallowing any handler exceptions
|
|
394
|
+
/** Emit a warning via the `onWarning` callback, swallowing any handler exceptions. */
|
|
395
395
|
private _emitWarning;
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
/**
|
|
399
|
-
* NetTransport
|
|
399
|
+
* NetTransport, `Transport` implementation wrapping a `net.Socket`.
|
|
400
400
|
*
|
|
401
401
|
* This is the **only** place in the codebase that consumes `net.Socket`'s
|
|
402
402
|
* EventEmitter surface. All higher layers (Connection, Server, Client) work
|
|
@@ -455,13 +455,13 @@ declare class NetTransport implements Transport {
|
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
/**
|
|
458
|
-
* TlsTransport
|
|
458
|
+
* TlsTransport, `Transport` implementation wrapping a `tls.TLSSocket`.
|
|
459
459
|
*
|
|
460
460
|
* Identical wiring to `NetTransport` with one deliberate difference: `onConnect`
|
|
461
461
|
* maps to the socket's `'secureConnect'` event (fires once the TLS handshake
|
|
462
462
|
* completes) rather than `'connect'` (fires once the raw TCP handshake
|
|
463
463
|
* completes). Consumers of `Transport` (Connection, Client, Server) never see
|
|
464
|
-
* the difference
|
|
464
|
+
* the difference, they only ever observe "the transport is ready."
|
|
465
465
|
*
|
|
466
466
|
* @example
|
|
467
467
|
* ```typescript
|
|
@@ -484,7 +484,7 @@ declare class NetTransport implements Transport {
|
|
|
484
484
|
* `'connect'` event that `NetTransport` uses.
|
|
485
485
|
*
|
|
486
486
|
* Each `onXxx(fn)` call replaces the prior handler and re-registers on the
|
|
487
|
-
* socket (`socket.removeAllListeners(event)` + `socket.on(event, fn)`)
|
|
487
|
+
* socket (`socket.removeAllListeners(event)` + `socket.on(event, fn)`),
|
|
488
488
|
* the same set-once semantics as `NetTransport`.
|
|
489
489
|
*
|
|
490
490
|
* @example
|
|
@@ -511,8 +511,8 @@ declare class TlsTransport implements Transport {
|
|
|
511
511
|
/** See {@link Transport.onData}. */
|
|
512
512
|
onData(fn: (chunk: Buffer) => void): void;
|
|
513
513
|
/**
|
|
514
|
-
* See {@link Transport.onConnect}. Armed on `'secureConnect'
|
|
515
|
-
* handshake-complete event
|
|
514
|
+
* See {@link Transport.onConnect}. Armed on `'secureConnect'`, the TLS
|
|
515
|
+
* handshake-complete event, not the raw TCP `'connect'` event.
|
|
516
516
|
*/
|
|
517
517
|
onConnect(fn: () => void): void;
|
|
518
518
|
/** See {@link Transport.onClose}. */
|
|
@@ -522,9 +522,9 @@ declare class TlsTransport implements Transport {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
/**
|
|
525
|
-
* Security-warning codes and payload shape shared by the client and server
|
|
525
|
+
* Security-warning codes and payload shape shared by the client and server.
|
|
526
526
|
*
|
|
527
|
-
* These codes are a **public API
|
|
527
|
+
* These codes are a **public API**, they appear in `'securityWarning'` event
|
|
528
528
|
* handlers, log pipelines, and monitoring dashboards. Renaming or removing a
|
|
529
529
|
* code is a breaking change (mirrors the `WarningCode` guardrail in
|
|
530
530
|
* `src/framing/registry.ts`).
|
|
@@ -540,8 +540,8 @@ declare class TlsTransport implements Transport {
|
|
|
540
540
|
* @packageDocumentation
|
|
541
541
|
*/
|
|
542
542
|
/**
|
|
543
|
-
* Emitted (client-side) on every successful `secureConnect
|
|
544
|
-
* AND every reconnect
|
|
543
|
+
* Emitted (client-side) on every successful `secureConnect`, initial connect
|
|
544
|
+
* AND every reconnect, when {@link TlsOptions.allowUnverified} is `true`.
|
|
545
545
|
* Certificate verification is disabled for the connection; this is the loud,
|
|
546
546
|
* per-connection reminder that the channel is not authenticated per IHE ATNA
|
|
547
547
|
* ITI-19 (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html).
|
|
@@ -584,7 +584,7 @@ type SecurityWarningCode = typeof MLLP_TLS_VERIFY_DISABLED | typeof MLLP_BIND_AL
|
|
|
584
584
|
* {@link MllpClient} (`MLLP_TLS_VERIFY_DISABLED`) and {@link MllpServer}
|
|
585
585
|
* (`MLLP_BIND_ALL_INTERFACES`).
|
|
586
586
|
*
|
|
587
|
-
* Never carries payload bytes or PHI
|
|
587
|
+
* Never carries payload bytes or PHI, only routing metadata (host/port) and
|
|
588
588
|
* a fixed, static message string.
|
|
589
589
|
*
|
|
590
590
|
* @example
|
|
@@ -608,18 +608,18 @@ interface SecurityWarning {
|
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
/**
|
|
611
|
-
* TLS (MLLPS) option types for the client and server
|
|
611
|
+
* TLS (MLLPS) option types for the client and server.
|
|
612
612
|
*
|
|
613
613
|
* Spec anchor: IHE ATNA, ITI-19 Authenticate Node
|
|
614
614
|
* (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html), the "STX: TLS 1.2
|
|
615
|
-
* Floor using BCP195" option, ITI TF-2 §3.19.6.2.3
|
|
615
|
+
* Floor using BCP195" option, ITI TF-2 §3.19.6.2.3, TLS >= 1.2 required;
|
|
616
616
|
* mutual node authentication; certificate validation via chain-of-trust or
|
|
617
617
|
* direct comparison. ITI TF-2 §3.19.6.2.3 mandates four TLS 1.2 cipher
|
|
618
618
|
* suites: `TLS_DHE_RSA_WITH_AES_128_GCM_SHA256`,
|
|
619
619
|
* `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`, `TLS_DHE_RSA_WITH_AES_256_GCM_SHA384`,
|
|
620
620
|
* `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384`. Node's default TLS 1.2 cipher list
|
|
621
621
|
* already includes both mandated ECDHE suites, so this package pins to Node
|
|
622
|
-
* defaults rather than bundling a cipher list
|
|
622
|
+
* defaults rather than bundling a cipher list, see `docs-content/tls.md`.
|
|
623
623
|
*
|
|
624
624
|
* @example
|
|
625
625
|
* ```typescript
|
|
@@ -631,7 +631,7 @@ interface SecurityWarning {
|
|
|
631
631
|
*/
|
|
632
632
|
/**
|
|
633
633
|
* A PEM-encoded credential (certificate, key, or CA), matching Node's
|
|
634
|
-
* `tls.connect`/`tls.createServer` input shape
|
|
634
|
+
* `tls.connect`/`tls.createServer` input shape, a single PEM string/Buffer,
|
|
635
635
|
* or an array of them (chain / multiple trust anchors).
|
|
636
636
|
*
|
|
637
637
|
* @example
|
|
@@ -642,9 +642,9 @@ interface SecurityWarning {
|
|
|
642
642
|
*/
|
|
643
643
|
type PemInput = string | Buffer | Array<string | Buffer>;
|
|
644
644
|
/**
|
|
645
|
-
* Client-side TLS options (
|
|
645
|
+
* Client-side TLS options (`ClientOptions.tls`).
|
|
646
646
|
*
|
|
647
|
-
* Passing `true` for `ClientOptions.tls` is equivalent to `{}
|
|
647
|
+
* Passing `true` for `ClientOptions.tls` is equivalent to `{}`, TLS enabled
|
|
648
648
|
* with all defaults, including certificate verification **on**.
|
|
649
649
|
*
|
|
650
650
|
* @example
|
|
@@ -674,9 +674,9 @@ interface TlsOptions {
|
|
|
674
674
|
/**
|
|
675
675
|
* Minimum negotiated TLS protocol version.
|
|
676
676
|
*
|
|
677
|
-
* Default `'TLSv1.2'
|
|
677
|
+
* Default `'TLSv1.2'`, the IHE ATNA ITI-19 "TLS 1.2 Floor" (BCP195) floor
|
|
678
678
|
* (ITI TF-2 §3.19.6.2.3). `'TLSv1.0'`/`'TLSv1.1'` are intentionally **not**
|
|
679
|
-
* expressible by this type
|
|
679
|
+
* expressible by this type, the floor cannot be lowered through this API.
|
|
680
680
|
*
|
|
681
681
|
* @default 'TLSv1.2'
|
|
682
682
|
*/
|
|
@@ -695,7 +695,7 @@ interface TlsOptions {
|
|
|
695
695
|
/**
|
|
696
696
|
* Loud, explicit dev opt-out from certificate verification (maps to
|
|
697
697
|
* `tls.connect`'s `rejectUnauthorized: false`). There is deliberately no
|
|
698
|
-
* raw `rejectUnauthorized` surface on this type
|
|
698
|
+
* raw `rejectUnauthorized` surface on this type, this is the only door,
|
|
699
699
|
* and it is loud: every successful connection (initial + every reconnect)
|
|
700
700
|
* emits a `'securityWarning'` (`MLLP_TLS_VERIFY_DISABLED`) event and calls
|
|
701
701
|
* `process.emitWarning`.
|
|
@@ -712,11 +712,11 @@ interface TlsOptions {
|
|
|
712
712
|
* Mirrors the IHE ATNA "Authenticate Node" mutual-auth requirement
|
|
713
713
|
* (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html):
|
|
714
714
|
*
|
|
715
|
-
* - `'NONE'
|
|
716
|
-
* - `'WANT'
|
|
715
|
+
* - `'NONE'`, no client certificate requested (default).
|
|
716
|
+
* - `'WANT'`, client certificate requested but NOT required; an untrusted or
|
|
717
717
|
* absent client certificate does not reject the connection. The peer
|
|
718
718
|
* certificate (if any) is surfaced on the `'connection'` event.
|
|
719
|
-
* - `'MUST'
|
|
719
|
+
* - `'MUST'`, client certificate required AND verified against
|
|
720
720
|
* {@link ServerTlsOptions.ca}; the ATNA mutual-authentication mode. A
|
|
721
721
|
* missing or untrusted client certificate rejects the handshake.
|
|
722
722
|
*
|
|
@@ -728,7 +728,7 @@ interface TlsOptions {
|
|
|
728
728
|
*/
|
|
729
729
|
type ClientAuth = "NONE" | "WANT" | "MUST";
|
|
730
730
|
/**
|
|
731
|
-
* Server-side TLS options (
|
|
731
|
+
* Server-side TLS options (`ServerOptions.tls`).
|
|
732
732
|
*
|
|
733
733
|
* @example
|
|
734
734
|
* ```typescript
|
|
@@ -757,7 +757,7 @@ interface ServerTlsOptions {
|
|
|
757
757
|
*/
|
|
758
758
|
readonly clientAuth?: ClientAuth;
|
|
759
759
|
/**
|
|
760
|
-
* Minimum negotiated TLS protocol version
|
|
760
|
+
* Minimum negotiated TLS protocol version, the IHE ATNA ITI-19 "TLS 1.2
|
|
761
761
|
* Floor" (BCP195) floor (ITI TF-2 §3.19.6.2.3).
|
|
762
762
|
*
|
|
763
763
|
* @default 'TLSv1.2'
|
|
@@ -775,7 +775,7 @@ interface ServerTlsOptions {
|
|
|
775
775
|
}
|
|
776
776
|
|
|
777
777
|
/**
|
|
778
|
-
* Connection
|
|
778
|
+
* Connection, 6-state FSM over a Transport with lifecycle events, warning streams,
|
|
779
779
|
* and getStats() observability.
|
|
780
780
|
*
|
|
781
781
|
* @example
|
|
@@ -795,9 +795,9 @@ interface ServerTlsOptions {
|
|
|
795
795
|
*/
|
|
796
796
|
|
|
797
797
|
/**
|
|
798
|
-
* The 6 connection states
|
|
798
|
+
* The 6 connection states.
|
|
799
799
|
*
|
|
800
|
-
* Transitions are validated against the
|
|
800
|
+
* Transitions are validated against the legal-transition edge graph. Illegal transitions
|
|
801
801
|
* are silently ignored to preserve FSM integrity.
|
|
802
802
|
*/
|
|
803
803
|
type ConnectionState = "CONNECTING" | "CONNECTED" | "DRAINING" | "RECONNECTING" | "DISCONNECTED" | "CLOSED";
|
|
@@ -819,8 +819,8 @@ interface StateChangeEvent {
|
|
|
819
819
|
/**
|
|
820
820
|
* Payload for the 'reconnecting' event. Always `Object.freeze`'d.
|
|
821
821
|
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
822
|
+
* `attempt` and `delayMs` are populated by `MllpClient` when it schedules a
|
|
823
|
+
* reconnect. A `Connection` emitting on its own supplies `connectionId` only.
|
|
824
824
|
*
|
|
825
825
|
* @example
|
|
826
826
|
* ```typescript
|
|
@@ -835,7 +835,7 @@ interface ReconnectingEvent {
|
|
|
835
835
|
readonly delayMs?: number;
|
|
836
836
|
}
|
|
837
837
|
/**
|
|
838
|
-
* Return type of `connection.getStats()
|
|
838
|
+
* Return type of `connection.getStats()`, JSON-serializable.
|
|
839
839
|
*
|
|
840
840
|
* All timestamps are `Date | null` (not epoch milliseconds). `JSON.stringify()`
|
|
841
841
|
* serialises them to ISO 8601 strings by default with no information loss.
|
|
@@ -877,9 +877,9 @@ interface ConnectionOptions {
|
|
|
877
877
|
transport: Transport;
|
|
878
878
|
/** Called for each decoded MLLP frame (raw payload bytes, framing stripped). */
|
|
879
879
|
onMessage?: (payload: Buffer) => void;
|
|
880
|
-
/** Per-connection warning subscriber
|
|
880
|
+
/** Per-connection warning subscriber. Replaces previous subscription. */
|
|
881
881
|
onWarning?: (w: MllpWarning) => void;
|
|
882
|
-
/** Drain timeout used by close()
|
|
882
|
+
/** Drain timeout used by close() (default: 30_000 ms). */
|
|
883
883
|
drainTimeoutMs?: number;
|
|
884
884
|
/** FrameReader options (tolerance, maxFrameSizeBytes). onFrame/onWarning are managed internally. */
|
|
885
885
|
framing?: Omit<FrameReaderOptions, "onFrame" | "onWarning">;
|
|
@@ -906,7 +906,7 @@ interface ConnectionOptions {
|
|
|
906
906
|
* ```
|
|
907
907
|
*/
|
|
908
908
|
declare class Connection extends EventEmitter {
|
|
909
|
-
/** Stable UUIDv4 identifier for this connection
|
|
909
|
+
/** Stable UUIDv4 identifier for this connection. */
|
|
910
910
|
readonly connectionId: string;
|
|
911
911
|
private _state;
|
|
912
912
|
private readonly _transport;
|
|
@@ -919,20 +919,19 @@ declare class Connection extends EventEmitter {
|
|
|
919
919
|
private _bytesOut;
|
|
920
920
|
private _lastByteInAt;
|
|
921
921
|
private _lastByteOutAt;
|
|
922
|
-
/** Ring buffer: last MAX_WARNINGS entries
|
|
922
|
+
/** Ring buffer: last MAX_WARNINGS entries. */
|
|
923
923
|
private _warningBuffer;
|
|
924
|
-
/** Accurate total counts per code
|
|
924
|
+
/** Accurate total counts per code, unaffected by ring buffer truncation. */
|
|
925
925
|
private readonly _warningsByCode;
|
|
926
|
-
/** Set to true once the ring buffer overflows
|
|
926
|
+
/** Set to true once the ring buffer overflows. */
|
|
927
927
|
private _warningsTruncated;
|
|
928
928
|
private _onWarningFn;
|
|
929
929
|
private _drainPromise;
|
|
930
930
|
/**
|
|
931
|
-
* beforeClose hook
|
|
931
|
+
* beforeClose hook, no-op default that resolves immediately.
|
|
932
932
|
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
* Connection calls this during `close()`, racing against `drainTimeoutMs`.
|
|
933
|
+
* An owner may replace this instance property to run work before the socket
|
|
934
|
+
* closes. Connection calls it during `close()`, racing against `drainTimeoutMs`.
|
|
936
935
|
*/
|
|
937
936
|
beforeClose: (drainTimeoutMs: number) => Promise<void>;
|
|
938
937
|
/**
|
|
@@ -943,16 +942,16 @@ declare class Connection extends EventEmitter {
|
|
|
943
942
|
*/
|
|
944
943
|
constructor(opts: ConnectionOptions);
|
|
945
944
|
/**
|
|
946
|
-
* Current FSM state. One of the 6 `ConnectionState` values
|
|
945
|
+
* Current FSM state. One of the 6 `ConnectionState` values.
|
|
947
946
|
*
|
|
948
947
|
* Subscribe to `'stateChange'` events for reactive state monitoring.
|
|
949
948
|
*/
|
|
950
949
|
get state(): ConnectionState;
|
|
951
950
|
/**
|
|
952
|
-
* Register or replace the per-connection warning subscriber
|
|
951
|
+
* Register or replace the per-connection warning subscriber.
|
|
953
952
|
*
|
|
954
953
|
* Subsequent calls replace the previous handler (set-once semantics prevent leaks).
|
|
955
|
-
* The handler is called synchronously and any exception it throws is swallowed
|
|
954
|
+
* The handler is called synchronously and any exception it throws is swallowed.
|
|
956
955
|
*
|
|
957
956
|
* @example
|
|
958
957
|
* ```typescript
|
|
@@ -980,7 +979,7 @@ declare class Connection extends EventEmitter {
|
|
|
980
979
|
/**
|
|
981
980
|
* Write raw bytes to the transport (no framing applied).
|
|
982
981
|
*
|
|
983
|
-
*
|
|
982
|
+
* The server and the client wrap this with `encodeFrame()` to add MLLP framing.
|
|
984
983
|
* Returns `false` if the connection is CLOSED or DISCONNECTED (no bytes written).
|
|
985
984
|
*
|
|
986
985
|
* @returns `true` if bytes flushed immediately; `false` if buffered (backpressure) or not writable.
|
|
@@ -996,7 +995,7 @@ declare class Connection extends EventEmitter {
|
|
|
996
995
|
* Initiate graceful close of the connection.
|
|
997
996
|
*
|
|
998
997
|
* - If `CONNECTING` or `RECONNECTING`: cancels the pending attempt, transitions
|
|
999
|
-
* directly to `CLOSED`, and calls `transport.destroy()
|
|
998
|
+
* directly to `CLOSED`, and calls `transport.destroy()`.
|
|
1000
999
|
* - If `CONNECTED`: transitions to `DRAINING`, calls `beforeClose(drainTimeoutMs)`,
|
|
1001
1000
|
* then `DRAINING → DISCONNECTED` once the hook resolves, or `DRAINING → CLOSED`
|
|
1002
1001
|
* if the drain timeout elapses first.
|
|
@@ -1017,14 +1016,14 @@ declare class Connection extends EventEmitter {
|
|
|
1017
1016
|
* Race `beforeClose()` against the drain timeout.
|
|
1018
1017
|
*
|
|
1019
1018
|
* On normal completion: `DRAINING → DISCONNECTED` + `transport.close()`.
|
|
1020
|
-
* On timeout: `DRAINING → CLOSED` + `transport.destroy()
|
|
1019
|
+
* On timeout: `DRAINING → CLOSED` + `transport.destroy()`.
|
|
1021
1020
|
*/
|
|
1022
1021
|
private _drainWithTimeout;
|
|
1023
1022
|
/**
|
|
1024
1023
|
* Abruptly destroy the connection, discarding any pending writes.
|
|
1025
1024
|
*
|
|
1026
1025
|
* Transitions from any non-terminal state directly to `CLOSED` and calls
|
|
1027
|
-
* `transport.destroy(reason)`. Emits `'close'` event. Idempotent
|
|
1026
|
+
* `transport.destroy(reason)`. Emits `'close'` event. Idempotent, safe to
|
|
1028
1027
|
* call multiple times.
|
|
1029
1028
|
*
|
|
1030
1029
|
* @param reason - Optional error to propagate to the transport.
|
|
@@ -1036,7 +1035,7 @@ declare class Connection extends EventEmitter {
|
|
|
1036
1035
|
*/
|
|
1037
1036
|
destroy(reason?: Error): void;
|
|
1038
1037
|
/**
|
|
1039
|
-
* Return a JSON-serializable observability snapshot
|
|
1038
|
+
* Return a JSON-serializable observability snapshot.
|
|
1040
1039
|
*
|
|
1041
1040
|
* All timestamp fields are `Date | null` (not epoch milliseconds). `JSON.stringify()`
|
|
1042
1041
|
* serialises them to ISO 8601 strings with no information loss.
|
|
@@ -1059,12 +1058,12 @@ declare class Connection extends EventEmitter {
|
|
|
1059
1058
|
* Node throws `ERR_UNHANDLED_ERROR` when `'error'` is emitted on an `EventEmitter` with no
|
|
1060
1059
|
* listener. Every `'error'` emission in this class is reached from inside a transport callback
|
|
1061
1060
|
* (the socket's `'data'`/`'error'` listener), so an unguarded emit would become an **uncaught
|
|
1062
|
-
* exception that kills the process
|
|
1061
|
+
* exception that kills the process**, the exact failure the containment above exists to
|
|
1063
1062
|
* prevent, merely relocated one frame up the stack.
|
|
1064
1063
|
*
|
|
1065
1064
|
* `Connection` is a public export and can legitimately be driven with no `'error'` listener
|
|
1066
1065
|
* (`MllpServer` and `MllpClient` each attach one; a bare `Connection` need not). The connection
|
|
1067
|
-
* is still torn down and `'stateChange'`/`'close'` still fire, so a caller is never blind
|
|
1066
|
+
* is still torn down and `'stateChange'`/`'close'` still fire, so a caller is never blind,
|
|
1068
1067
|
* they simply do not get the typed error object they never asked for. Mirrors
|
|
1069
1068
|
* `MllpServer._emitErrorIfListened`.
|
|
1070
1069
|
*/
|
|
@@ -1077,7 +1076,7 @@ declare class Connection extends EventEmitter {
|
|
|
1077
1076
|
* consumers keep the stable `code` and `byteOffset`), then **destroy this connection only**.
|
|
1078
1077
|
*
|
|
1079
1078
|
* Why destroy rather than resynchronize: once `push` has thrown, the reader's position within
|
|
1080
|
-
* the byte stream is no longer trustworthy
|
|
1079
|
+
* the byte stream is no longer trustworthy, the next bytes could be the middle of a message.
|
|
1081
1080
|
* Guessing where the next frame starts is how a clinical message gets silently mis-split, so
|
|
1082
1081
|
* the connection is dropped instead. A server drops that one peer and keeps serving everyone
|
|
1083
1082
|
* else.
|
|
@@ -1089,17 +1088,17 @@ declare class Connection extends EventEmitter {
|
|
|
1089
1088
|
* engine that is already unwell. A compatibility failure is not a network blip.
|
|
1090
1089
|
*
|
|
1091
1090
|
* If a peer's quirk is *expected* (trailing junk, keepalive bytes, a missing leading VT), the
|
|
1092
|
-
* decoder's tolerance opt-ins are the supported answer
|
|
1091
|
+
* decoder's tolerance opt-ins are the supported answer, they turn the throw into a warning.
|
|
1093
1092
|
*/
|
|
1094
1093
|
private _onFatalFramingError;
|
|
1095
1094
|
/**
|
|
1096
|
-
* Backstop for a NON-framing throw escaping `FrameReader.push
|
|
1095
|
+
* Backstop for a NON-framing throw escaping `FrameReader.push`, i.e. our own bug, or a
|
|
1097
1096
|
* subscriber's, rather than the peer's bytes.
|
|
1098
1097
|
*
|
|
1099
1098
|
* `_onFrameDecoded` already contains subscriber throws at the dispatch site, so this should be
|
|
1100
1099
|
* unreachable. It exists because "unreachable" and "cannot kill the process from inside a
|
|
1101
1100
|
* socket `'data'` handler" are different claims, and only the second one is safe to bet a
|
|
1102
|
-
* clinical interface on. Reported honestly as an internal receive error
|
|
1101
|
+
* clinical interface on. Reported honestly as an internal receive error, never dressed up as
|
|
1103
1102
|
* a peer framing fault.
|
|
1104
1103
|
*/
|
|
1105
1104
|
private _onInternalReceiveError;
|
|
@@ -1133,41 +1132,41 @@ declare class Connection extends EventEmitter {
|
|
|
1133
1132
|
/**
|
|
1134
1133
|
* Connection lifecycle phase where the error occurred.
|
|
1135
1134
|
*
|
|
1136
|
-
* All 5 phases are defined
|
|
1137
|
-
* exercised
|
|
1138
|
-
* breaking type change later
|
|
1135
|
+
* All 5 phases are defined even though `'reconnect'` is only
|
|
1136
|
+
* exercised by the client. Locking the full union now prevents a
|
|
1137
|
+
* breaking type change later.
|
|
1139
1138
|
*/
|
|
1140
1139
|
type ConnectionErrorPhase = "connect" | "send" | "receive" | "close" | "reconnect";
|
|
1141
1140
|
/**
|
|
1142
1141
|
* Stable cause codes for `MllpConnectionError`.
|
|
1143
1142
|
*
|
|
1144
|
-
* These codes are a **public API
|
|
1143
|
+
* These codes are a **public API**, they appear in error inspection
|
|
1145
1144
|
* by callers, log pipelines, and monitoring dashboards. Renaming or
|
|
1146
1145
|
* removing a member is a breaking change.
|
|
1147
1146
|
*
|
|
1148
|
-
* - `'fifo-unsafe'
|
|
1149
|
-
* FIFO ordering cannot be safely resumed across sessions
|
|
1150
|
-
* - `'in-flight-orphan'
|
|
1147
|
+
* - `'fifo-unsafe'`, A queued send was rejected during reconnect because
|
|
1148
|
+
* FIFO ordering cannot be safely resumed across sessions.
|
|
1149
|
+
* - `'in-flight-orphan'`, An in-flight send (already write-flushed,
|
|
1151
1150
|
* ACK timer started) was rejected during reconnect in FIFO mode because
|
|
1152
1151
|
* the at-most-once delivery contract cannot be preserved across a
|
|
1153
|
-
* socket drop (
|
|
1154
|
-
* - `'tls-verify'
|
|
1152
|
+
* socket drop (healthcare medication/orders semantics).
|
|
1153
|
+
* - `'tls-verify'`, The TLS handshake failed certificate
|
|
1155
1154
|
* verification (untrusted chain, expired/not-yet-valid cert, hostname
|
|
1156
1155
|
* mismatch, revocation, …). See {@link isTlsVerificationErrorCode} for the
|
|
1157
1156
|
* exact underlying error codes. Classified **permanent** by
|
|
1158
|
-
* `isTransientConnectionError
|
|
1157
|
+
* `isTransientConnectionError`, never auto-reconnect-looped into a
|
|
1159
1158
|
* misconfigured or MITM'd endpoint.
|
|
1160
|
-
* - `'tls-handshake'
|
|
1159
|
+
* - `'tls-handshake'`, A TLS-**protocol**-shaped handshake
|
|
1161
1160
|
* failure observed before `'secureConnect'`: `ERR_SSL_*` codes, `EPROTO`,
|
|
1162
1161
|
* or an OpenSSL alert-bearing error (protocol version mismatch, no shared
|
|
1163
1162
|
* cipher, a required mutual-TLS client certificate rejected by the
|
|
1164
1163
|
* server, …). See `isTlsProtocolError` for the boundary. Pure TCP-level
|
|
1165
1164
|
* failures (`ECONNREFUSED`, `ETIMEDOUT`, …) on a TLS-configured
|
|
1166
|
-
* connection carry **no** `connectionCause
|
|
1165
|
+
* connection carry **no** `connectionCause`, the same shape as plaintext.
|
|
1167
1166
|
*
|
|
1168
1167
|
* Scope note: the `'tls-verify'`/`'tls-handshake'` values are attached on
|
|
1169
1168
|
* the client's **initial `connect()` path**. Failures on the auto-reconnect
|
|
1170
|
-
* path surface as raw socket errors
|
|
1169
|
+
* path surface as raw socket errors, their transient/permanent
|
|
1171
1170
|
* classification still applies, but they do not (yet) carry a
|
|
1172
1171
|
* `connectionCause`.
|
|
1173
1172
|
*
|
|
@@ -1184,9 +1183,9 @@ type ConnectionErrorCause = "fifo-unsafe" | "in-flight-orphan" | "tls-verify" |
|
|
|
1184
1183
|
* Thrown (or emitted via `onError`) for socket-layer problems such as
|
|
1185
1184
|
* connection refused, ECONNRESET, ETIMEDOUT, or DNS failure.
|
|
1186
1185
|
*
|
|
1187
|
-
* - `cause
|
|
1188
|
-
* - `phase
|
|
1189
|
-
* - `connectionCause
|
|
1186
|
+
* - `cause`, the original OS or TLS error
|
|
1187
|
+
* - `phase`, which connection lifecycle phase the failure occurred in
|
|
1188
|
+
* - `connectionCause`, optional stable cause-code (e.g. for FIFO reconnect rejections)
|
|
1190
1189
|
*
|
|
1191
1190
|
* @example
|
|
1192
1191
|
* ```typescript
|
|
@@ -1231,14 +1230,14 @@ declare class MllpConnectionError extends Error {
|
|
|
1231
1230
|
*
|
|
1232
1231
|
* ## Why this is safety-critical
|
|
1233
1232
|
*
|
|
1234
|
-
* A positive acknowledgement (`AA`) tells the sender "you may forget this message
|
|
1233
|
+
* A positive acknowledgement (`AA`) tells the sender "you may forget this message, I
|
|
1235
1234
|
* have it." If a server emits `AA` *before* the message is durably handled and then the
|
|
1236
1235
|
* process crashes, the message is **silently lost** with no record on either side. For
|
|
1237
1236
|
* clinical messages (an admit, an order, a result) that is a patient-safety failure.
|
|
1238
1237
|
*
|
|
1239
1238
|
* The contract this module enforces: a positive ACK (`AA`) is only built once the
|
|
1240
1239
|
* application handler has resolved success. A handler that throws or rejects yields a
|
|
1241
|
-
* **negative** ACK (`AE` by default, `AR` when the handler asks for it)
|
|
1240
|
+
* **negative** ACK (`AE` by default, `AR` when the handler asks for it), never `AA`.
|
|
1242
1241
|
*
|
|
1243
1242
|
* Spec: HL7 v2.5.1 ch. 2 §2.9.2 (original acknowledgement mode), §2.9.3 (enhanced
|
|
1244
1243
|
* accept/application acknowledgement), Table 0008 (Acknowledgment Code), §2.9.2.2
|
|
@@ -1247,14 +1246,14 @@ declare class MllpConnectionError extends Error {
|
|
|
1247
1246
|
* @packageDocumentation
|
|
1248
1247
|
*/
|
|
1249
1248
|
/**
|
|
1250
|
-
* HL7 Table 0008
|
|
1249
|
+
* HL7 Table 0008, Acknowledgment Code. A **stable public API**.
|
|
1251
1250
|
*
|
|
1252
1251
|
* Two families, by mode (HL7 v2.5.1 §2.9):
|
|
1253
1252
|
*
|
|
1254
1253
|
* - **Original mode (§2.9.2):** `AA` accept, `AE` application error, `AR` application
|
|
1255
1254
|
* reject. The single ACK reports application-level outcome.
|
|
1256
1255
|
* - **Enhanced mode (§2.9.3):** `CA` commit accept, `CE` commit error, `CR` commit
|
|
1257
|
-
* reject
|
|
1256
|
+
* reject, the *accept* acknowledgement, distinct from a later application ACK.
|
|
1258
1257
|
*
|
|
1259
1258
|
* `AE` vs `AR`: `AE` is a processing **error** (the sender may resend later, e.g. a
|
|
1260
1259
|
* transient downstream outage); `AR` is a **reject** (the sender should not resend the
|
|
@@ -1284,7 +1283,7 @@ type NegativeAckCode = "AE" | "AR";
|
|
|
1284
1283
|
* returns. Throwing **any** error from a commit-gated handler yields `AE` by default;
|
|
1285
1284
|
* throw an `MllpAckError` to choose `AR` (application reject) instead.
|
|
1286
1285
|
*
|
|
1287
|
-
* The `message` is **never** copied into the ACK bytes or any emitted event
|
|
1286
|
+
* The `message` is **never** copied into the ACK bytes or any emitted event, it may
|
|
1288
1287
|
* carry PHI. Only the static, non-PHI `ackCode` influences the wire response.
|
|
1289
1288
|
*
|
|
1290
1289
|
* @example
|
|
@@ -1320,7 +1319,7 @@ declare class MllpAckError extends Error {
|
|
|
1320
1319
|
* Resolve the negative acknowledgement code for a handler failure.
|
|
1321
1320
|
*
|
|
1322
1321
|
* An {@link MllpAckError} carries an explicit `ackCode`; any other thrown value maps to
|
|
1323
|
-
* `AE` (application error
|
|
1322
|
+
* `AE` (application error, the default, since most handler failures are transient and
|
|
1324
1323
|
* a resend may succeed).
|
|
1325
1324
|
*
|
|
1326
1325
|
* @example
|
|
@@ -1338,27 +1337,27 @@ declare function resolveNackCode(err: unknown): NegativeAckCode;
|
|
|
1338
1337
|
*
|
|
1339
1338
|
* The rule this enforces: **never answer `AA` for a message you could not correlate.** A
|
|
1340
1339
|
* positive ACK is a promise the sender may forget the message; if it names a control ID
|
|
1341
|
-
* the sender cannot match
|
|
1340
|
+
* the sender cannot match, or names one of several messages it never read, the sender
|
|
1342
1341
|
* times out and resends, committing a **duplicate clinical message** (or worse, believes a
|
|
1343
1342
|
* destroyed message was delivered). Three payload-shaped reasons make a positive ACK
|
|
1344
1343
|
* uncorrelatable, all peer-reachable off the wire:
|
|
1345
1344
|
*
|
|
1346
|
-
* 1. **No readable `MSH`.** `readMshSegment` returns `null
|
|
1345
|
+
* 1. **No readable `MSH`.** `readMshSegment` returns `null`, e.g. a `BOM`/`SP`/`TAB`
|
|
1347
1346
|
* before `MSH` (which shares the `MSH`'s segment line, so `MSH` heads no segment), or
|
|
1348
1347
|
* a bare fragment delivered after a mid-payload `VT` discard (`MLLP_TRAILING_BYTES`).
|
|
1349
1348
|
* MSA-2 would be empty: an ACK that correlates to nothing.
|
|
1350
1349
|
* 2. **Empty MSH-10.** The `MSH` is readable but carries no message control ID, so there
|
|
1351
|
-
* is nothing to echo
|
|
1350
|
+
* is nothing to echo, again `MSA|AA|` with an empty MSA-2.
|
|
1352
1351
|
* 3. **A batch or concatenated messages.** An `FHS`/`BHS`/`BTS`/`FTS` envelope (§2.10.3)
|
|
1353
1352
|
* or a second `MSH` in the same frame: a single MSA-2 can echo only ONE control ID, so
|
|
1354
1353
|
* a positive ACK naming the first silently drops the rest (see
|
|
1355
|
-
* {@link containsBatchOrExtraMessage}). Batch ACK is its own feature
|
|
1356
|
-
*
|
|
1354
|
+
* {@link containsBatchOrExtraMessage}). Batch ACK is its own feature; until it is
|
|
1355
|
+
* designed, a batch must stay a **loud non-positive** answer.
|
|
1357
1356
|
*
|
|
1358
1357
|
* This is a **refusal**, not a tolerance widening: it never makes an unreadable message
|
|
1359
1358
|
* readable, never re-bases on a located `MSH`, never parses a batch. It only recognizes
|
|
1360
1359
|
* the shapes for which a positive disposition would be a lie, so the builder can fall back
|
|
1361
|
-
* to `AE`. A **negative** requested code (`AE`/`AR`/`CE`/`CR`) is unaffected
|
|
1360
|
+
* to `AE`. A **negative** requested code (`AE`/`AR`/`CE`/`CR`) is unaffected, it is
|
|
1362
1361
|
* already non-positive, and echoing whatever control ID it can find is still useful.
|
|
1363
1362
|
*
|
|
1364
1363
|
* Pure, byte-level, never throws.
|
|
@@ -1375,12 +1374,12 @@ declare function rawAckUncorrelatable(payload: Buffer): boolean;
|
|
|
1375
1374
|
* Build a minimal original-mode HL7 v2 acknowledgement from raw inbound payload bytes,
|
|
1376
1375
|
* **without a parser** (parser-driven ACKs are the `@cosyte/mllp/ack-from-hl7` subpath).
|
|
1377
1376
|
*
|
|
1378
|
-
* Locates the `MSH` segment (the first `CR`/`LF`-delimited segment starting with `MSH
|
|
1377
|
+
* Locates the `MSH` segment (the first `CR`/`LF`-delimited segment starting with `MSH`,
|
|
1379
1378
|
* so a leading `CR` or an `FHS`/`BHS` batch header does not hide it), reads the field
|
|
1380
1379
|
* separator from **MSH-1** and the encoding characters from **MSH-2**, then splits that
|
|
1381
1380
|
* segment on that separator to read fields. The ACK swaps sender/receiver per HL7 ACK
|
|
1382
|
-
* rules, echoes the inbound MSH-10 into MSA-2 (§2.9.2.2), sets MSA-1 to `code`, and
|
|
1383
|
-
* negative codes
|
|
1381
|
+
* rules, echoes the inbound MSH-10 into MSA-2 (§2.9.2.2), sets MSA-1 to `code`, and, for
|
|
1382
|
+
* negative codes, adds a **static, PHI-free** MSA-3 reason. A fresh control ID fills the
|
|
1384
1383
|
* ACK's own MSH-10.
|
|
1385
1384
|
*
|
|
1386
1385
|
* ## Why the delimiters are read, not assumed
|
|
@@ -1390,7 +1389,7 @@ declare function rawAckUncorrelatable(payload: Buffer): boolean;
|
|
|
1390
1389
|
* convention, not the spec. Assuming it was wrong in two compounding ways:
|
|
1391
1390
|
*
|
|
1392
1391
|
* 1. **Reading.** Splitting a `!`-delimited message on `|` yields ONE field, so every
|
|
1393
|
-
* echoed field
|
|
1392
|
+
* echoed field, including MSH-10, came back empty. The ACK went out as
|
|
1394
1393
|
* `MSA|AA|` with **no correlation id at all**: the sender cannot match it, times
|
|
1395
1394
|
* out, and resends → a duplicate clinical message. That is the exact failure this
|
|
1396
1395
|
* package's correlator exists to prevent, manufactured by the ACK builder itself.
|
|
@@ -1404,28 +1403,28 @@ declare function rawAckUncorrelatable(payload: Buffer): boolean;
|
|
|
1404
1403
|
*
|
|
1405
1404
|
* ## It reads the MSH through the SHARED scanner
|
|
1406
1405
|
*
|
|
1407
|
-
* The MSH read is `readMshSegment` from `src/internal/control-id.ts
|
|
1406
|
+
* The MSH read is `readMshSegment` from `src/internal/control-id.ts`, the same call the
|
|
1408
1407
|
* client's correlator makes to derive the key it will later match this ACK against. That
|
|
1409
1408
|
* is deliberate and it is the whole point: this builder used to re-derive the read itself
|
|
1410
1409
|
* (`payload.toString("latin1").split("\r")`, hunting for an `MSH` anywhere in the
|
|
1411
1410
|
* payload), and the two disagreed on real inputs. On a **truncated MSH** followed by a
|
|
1412
1411
|
* `PID` the correlator keyed on the PID's MRN while this builder echoed an empty MSA-2. On
|
|
1413
|
-
* a payload with a **leading `CR
|
|
1414
|
-
* which real senders emit
|
|
1412
|
+
* a payload with a **leading `CR`**, which the MLLP decoder passes straight through, and
|
|
1413
|
+
* which real senders emit, this builder found the `MSH` and echoed MSH-10 correctly while
|
|
1415
1414
|
* the correlator, requiring `MSH` at byte 0, gave up. Every such disagreement is an ACK the
|
|
1416
1415
|
* sender cannot match → timeout → resend → **duplicate clinical message**.
|
|
1417
1416
|
*
|
|
1418
|
-
* The fix is one scan
|
|
1417
|
+
* The fix is one scan, but note *which* scan. The first attempt made them agree by
|
|
1419
1418
|
* requiring `MSH` at byte 0 everywhere, which "resolved" the leading-`CR` disagreement by
|
|
1420
1419
|
* degrading the side that had been **right**: `buildRawAck` began emitting a positive `AA`
|
|
1421
1420
|
* with an empty MSA-2, silently, for a message whose MSH-10 was plainly present. Agreement
|
|
1422
1421
|
* is not the goal; agreeing on the *correct*, *tolerant* answer is. A lenient reader may
|
|
1423
|
-
* never drop data that is there (Postel's Law
|
|
1422
|
+
* never drop data that is there (Postel's Law, CLAUDE.md).
|
|
1424
1423
|
*
|
|
1425
1424
|
* ## The fail-safe downgrade: never a positive `AA`/`CA` it cannot correlate
|
|
1426
1425
|
*
|
|
1427
1426
|
* A positive acknowledgement is a promise the sender may forget the message. If it names a
|
|
1428
|
-
* control ID the sender cannot match
|
|
1427
|
+
* control ID the sender cannot match, or names one message out of several it never read,
|
|
1429
1428
|
* the sender times out and resends, committing a **duplicate clinical message**. So a
|
|
1430
1429
|
* requested positive code is **downgraded** to its non-positive counterpart (`AA`→`AE`,
|
|
1431
1430
|
* `CA`→`CE`) whenever the payload cannot carry a correlatable positive ACK: no readable
|
|
@@ -1433,10 +1432,10 @@ declare function rawAckUncorrelatable(payload: Buffer): boolean;
|
|
|
1433
1432
|
* acknowledge. See {@link rawAckUncorrelatable} for the exact conditions and why each is a
|
|
1434
1433
|
* refusal rather than a widened reader. A requested **negative** code (`AE`/`AR`/`CE`/`CR`)
|
|
1435
1434
|
* is never touched. This mirrors the parser-backed `buildMllpAck`, which downgrades and
|
|
1436
|
-
* warns on an unparseable inbound
|
|
1435
|
+
* warns on an unparseable inbound, the two builders' fail-safe semantics now agree.
|
|
1437
1436
|
*
|
|
1438
1437
|
* **Never throws** and **never copies payload content** beyond the routing/control
|
|
1439
|
-
* metadata above
|
|
1438
|
+
* metadata above, `readMshSegment` stops at the MSH's segment terminator, so no field of
|
|
1440
1439
|
* any later segment (PID and friends) can be reached, let alone echoed. On a missing or
|
|
1441
1440
|
* unreadable `MSH` it returns a minimal well-formed ACK carrying the (downgraded) `code` so
|
|
1442
1441
|
* the caller can still respond.
|
|
@@ -1444,7 +1443,7 @@ declare function rawAckUncorrelatable(payload: Buffer): boolean;
|
|
|
1444
1443
|
* @param payload - Raw decoded HL7 v2 payload bytes (MLLP framing already stripped).
|
|
1445
1444
|
* @param code - Requested MSA-1 acknowledgement code. A positive `AA`/`CA` is downgraded to
|
|
1446
1445
|
* `AE`/`CE` when the message cannot be correlated (see above).
|
|
1447
|
-
* @returns ACK payload bytes (no framing
|
|
1446
|
+
* @returns ACK payload bytes (no framing, the caller wraps with `encodeFrame`).
|
|
1448
1447
|
*
|
|
1449
1448
|
* @example
|
|
1450
1449
|
* ```typescript
|
|
@@ -1455,7 +1454,7 @@ declare function rawAckUncorrelatable(payload: Buffer): boolean;
|
|
|
1455
1454
|
declare function buildRawAck(payload: Buffer, code: AckCode): Buffer;
|
|
1456
1455
|
|
|
1457
1456
|
/**
|
|
1458
|
-
* MLLP Server
|
|
1457
|
+
* MLLP Server, createServer(), createStarterServer(), and MllpServer class.
|
|
1459
1458
|
*
|
|
1460
1459
|
* Provides the server-side MLLP transport: listen for inbound TCP connections,
|
|
1461
1460
|
* decode MLLP-framed messages, surface them as Buffer payloads, and support
|
|
@@ -1477,9 +1476,9 @@ declare function buildRawAck(payload: Buffer, code: AckCode): Buffer;
|
|
|
1477
1476
|
*/
|
|
1478
1477
|
|
|
1479
1478
|
/**
|
|
1480
|
-
* Metadata attached to each decoded MLLP message
|
|
1479
|
+
* Metadata attached to each decoded MLLP message.
|
|
1481
1480
|
*
|
|
1482
|
-
* All fields are `readonly
|
|
1481
|
+
* All fields are `readonly`, the object is `Object.freeze()`'d before emission.
|
|
1483
1482
|
*
|
|
1484
1483
|
* @example
|
|
1485
1484
|
* ```typescript
|
|
@@ -1498,30 +1497,30 @@ interface MessageMeta {
|
|
|
1498
1497
|
}
|
|
1499
1498
|
/**
|
|
1500
1499
|
* Why the server sent a **negative** acknowledgement instead of `AA` on the auto-ACK path.
|
|
1501
|
-
* A stable, **PHI-free** enum (no payload bytes, no control ID)
|
|
1500
|
+
* A stable, **PHI-free** enum (no payload bytes, no control ID), safe to log and to key
|
|
1502
1501
|
* metrics on.
|
|
1503
1502
|
*
|
|
1504
|
-
* - `'handler-rejected'
|
|
1503
|
+
* - `'handler-rejected'`, a commit-gated `onMessage` handler threw/rejected (the commit
|
|
1505
1504
|
* contract: a positive ACK cannot precede a successful commit).
|
|
1506
|
-
* - `'uncorrelatable-inbound'
|
|
1505
|
+
* - `'uncorrelatable-inbound'`, the inbound could not carry a correlatable positive ACK
|
|
1507
1506
|
* (no readable `MSH`, an empty MSH-10, or a batch/concatenated-message shape). A positive
|
|
1508
1507
|
* `AA` here names a control ID the sender cannot match → timeout → resend → **duplicate
|
|
1509
1508
|
* clinical message**. See {@link rawAckUncorrelatable}.
|
|
1510
|
-
* - `'discarded-bytes'
|
|
1509
|
+
* - `'discarded-bytes'`, the decoder flagged `MLLP_TRAILING_BYTES` for this frame: a
|
|
1511
1510
|
* mid-payload `VT` made it **discard accumulated bytes** and deliver only the fragment
|
|
1512
1511
|
* after it. The clinical message was destroyed in transit; a positive `AA` would tell the
|
|
1513
1512
|
* sender a message we never received was delivered.
|
|
1514
1513
|
*/
|
|
1515
1514
|
type NackReason = "handler-rejected" | "uncorrelatable-inbound" | "discarded-bytes";
|
|
1516
1515
|
/**
|
|
1517
|
-
* Payload of the server `'nack'` event
|
|
1516
|
+
* Payload of the server `'nack'` event, emitted whenever the server responds with a
|
|
1518
1517
|
* **negative** acknowledgement instead of `AA` on the auto-ACK path (the fail-safe commit
|
|
1519
1518
|
* contract). This fires both when a commit-gated `autoAck: 'AA'` handler throws/rejects and
|
|
1520
1519
|
* when the server **downgrades** a positive auto-ACK because the inbound could not be
|
|
1521
1520
|
* correlated ({@link NackReason}).
|
|
1522
1521
|
*
|
|
1523
1522
|
* **PHI-safe by construction:** carries only the connection ID, the resolved
|
|
1524
|
-
* acknowledgement code, and a static `reason
|
|
1523
|
+
* acknowledgement code, and a static `reason`, never the payload, the inbound control ID,
|
|
1525
1524
|
* or the thrown error's message (which may carry PHI). The object is `Object.freeze()`'d
|
|
1526
1525
|
* before emission.
|
|
1527
1526
|
*
|
|
@@ -1541,10 +1540,10 @@ interface NackEvent {
|
|
|
1541
1540
|
readonly reason: NackReason;
|
|
1542
1541
|
}
|
|
1543
1542
|
/**
|
|
1544
|
-
* Observability snapshot returned by `server.getStats()
|
|
1543
|
+
* Observability snapshot returned by `server.getStats()`.
|
|
1545
1544
|
*
|
|
1546
1545
|
* All fields are JSON-serializable. `connections` and `activeConnections` both
|
|
1547
|
-
* reflect the current live connection count
|
|
1546
|
+
* reflect the current live connection count.
|
|
1548
1547
|
*
|
|
1549
1548
|
* @example
|
|
1550
1549
|
* ```typescript
|
|
@@ -1560,7 +1559,7 @@ interface ServerStats {
|
|
|
1560
1559
|
/** Bound host, or `null` before listen(). */
|
|
1561
1560
|
readonly host: string | null;
|
|
1562
1561
|
/**
|
|
1563
|
-
* Current live connection count
|
|
1562
|
+
* Current live connection count.
|
|
1564
1563
|
* Same value as `activeConnections`.
|
|
1565
1564
|
*/
|
|
1566
1565
|
readonly connections: number;
|
|
@@ -1574,9 +1573,9 @@ interface ServerStats {
|
|
|
1574
1573
|
readonly acceptedTotal: number;
|
|
1575
1574
|
/** Total connections closed since listen() (monotonically increasing). */
|
|
1576
1575
|
readonly closedTotal: number;
|
|
1577
|
-
/** Whether this server is configured for TLS
|
|
1576
|
+
/** Whether this server is configured for TLS. Mirrors `ServerOptions.tls` being set. */
|
|
1578
1577
|
readonly tls: boolean;
|
|
1579
|
-
/** Total `'tlsClientError'` events (failed TLS handshakes, incl. rejected client certs) since listen()
|
|
1578
|
+
/** Total `'tlsClientError'` events (failed TLS handshakes, incl. rejected client certs) since listen(). */
|
|
1580
1579
|
readonly tlsClientErrorsTotal: number;
|
|
1581
1580
|
}
|
|
1582
1581
|
/**
|
|
@@ -1600,11 +1599,11 @@ interface ServerOptions {
|
|
|
1600
1599
|
/**
|
|
1601
1600
|
* Called for each decoded MLLP message.
|
|
1602
1601
|
*
|
|
1603
|
-
* Its role depends on `autoAck
|
|
1602
|
+
* Its role depends on `autoAck`, this is the **commit contract** (HL7 v2.5.1 §2.9.2):
|
|
1604
1603
|
*
|
|
1605
1604
|
* - **`autoAck: 'AA'` + this handler ⇒ commit-gated (the safe default).** The handler
|
|
1606
1605
|
* is the durable-commit step. The server **awaits** it and only then sends the ACK:
|
|
1607
|
-
* resolve ⇒ `AA`; **throw/reject ⇒ `AE`** (or `AR` via {@link MllpAckError})
|
|
1606
|
+
* resolve ⇒ `AA`; **throw/reject ⇒ `AE`** (or `AR` via {@link MllpAckError}), a
|
|
1608
1607
|
* positive ACK can never precede a successful commit. Do **not** call `conn.send()`
|
|
1609
1608
|
* here in this mode.
|
|
1610
1609
|
* - **`autoAck` unset ⇒ manual mode.** The handler owns the response; build and send the
|
|
@@ -1616,8 +1615,8 @@ interface ServerOptions {
|
|
|
1616
1615
|
*/
|
|
1617
1616
|
onMessage?: (payload: Buffer, meta: MessageMeta, conn: Connection) => void | Promise<void>;
|
|
1618
1617
|
/**
|
|
1619
|
-
* FrameReader tolerance options applied to every accepted connection
|
|
1620
|
-
* Merged with SERVER_DEFAULT_FRAMING
|
|
1618
|
+
* FrameReader tolerance options applied to every accepted connection.
|
|
1619
|
+
* Merged with SERVER_DEFAULT_FRAMING, caller-supplied values override defaults.
|
|
1621
1620
|
* `onFrame` and `onWarning` are managed internally and must not be supplied here.
|
|
1622
1621
|
*/
|
|
1623
1622
|
framing?: Omit<FrameReaderOptions, "onFrame" | "onWarning">;
|
|
@@ -1628,44 +1627,44 @@ interface ServerOptions {
|
|
|
1628
1627
|
/**
|
|
1629
1628
|
* Auto-ACK mode. When set, the server builds and sends the ACK for each message.
|
|
1630
1629
|
*
|
|
1631
|
-
* - **`'AA'
|
|
1630
|
+
* - **`'AA'`**, auto-acknowledge with the fail-safe **commit contract**:
|
|
1632
1631
|
* - **With an `onMessage` handler ⇒ commit-gated (recommended).** The server awaits
|
|
1633
1632
|
* `onMessage` (the durable-commit step), then sends `AA` on success or a **negative**
|
|
1634
1633
|
* ACK on failure (`AE` by default; `AR` via {@link MllpAckError}). The positive ACK
|
|
1635
|
-
* **cannot precede a successful commit
|
|
1634
|
+
* **cannot precede a successful commit**, a handler throw can never yield `AA`.
|
|
1636
1635
|
* - **Without an `onMessage` handler ⇒ transport-accept.** `AA` is sent on frame
|
|
1637
|
-
* receipt. This `AA` means only **"bytes received and framed"
|
|
1636
|
+
* receipt. This `AA` means only **"bytes received and framed"**, *not*
|
|
1638
1637
|
* "application-processed". ⚠️ For clinical messages this is unsafe on its own:
|
|
1639
1638
|
* pair `'AA'` with an `onMessage` handler that durably commits, so the ACK reflects
|
|
1640
1639
|
* real processing.
|
|
1641
|
-
* - **`fn
|
|
1640
|
+
* - **`fn`**, `fn(payload, meta, conn)` builds the ACK bytes the server sends; the
|
|
1642
1641
|
* caller fully owns MSA-1 (e.g. to emit enhanced-mode `CA`/`CE`/`CR`).
|
|
1643
1642
|
*
|
|
1644
|
-
* The `'message'` event always fires BEFORE the ACK is sent
|
|
1645
|
-
* `conn.send()` in `onMessage` when `autoAck` is set
|
|
1643
|
+
* The `'message'` event always fires BEFORE the ACK is sent. Do NOT call
|
|
1644
|
+
* `conn.send()` in `onMessage` when `autoAck` is set, this results in two ACKs.
|
|
1646
1645
|
*/
|
|
1647
1646
|
autoAck?: "AA" | ((payload: Buffer, meta: MessageMeta, conn: Connection) => Buffer | Promise<Buffer>);
|
|
1648
1647
|
/**
|
|
1649
|
-
* TCP keepalive probe interval in ms
|
|
1648
|
+
* TCP keepalive probe interval in ms.
|
|
1650
1649
|
* Calls `socket.setKeepAlive(true, ms)` on each accepted socket.
|
|
1651
1650
|
* Uses OS TCP stack to detect dead peers (half-open, network partitions).
|
|
1652
1651
|
* Distinct from `deadPeerTimeoutMs` (application-level idle close).
|
|
1653
1652
|
*/
|
|
1654
1653
|
keepaliveIntervalMs?: number;
|
|
1655
1654
|
/**
|
|
1656
|
-
* Application-level idle close timeout in ms
|
|
1655
|
+
* Application-level idle close timeout in ms.
|
|
1657
1656
|
* If no HL7 messages are received on a connection for this interval, the connection
|
|
1658
1657
|
* is destroyed via `conn.destroy(new Error('idle timeout'))`.
|
|
1659
1658
|
* Resets on every `'message'` event. Distinct from `keepaliveIntervalMs` (OS TCP probe).
|
|
1660
1659
|
*/
|
|
1661
1660
|
deadPeerTimeoutMs?: number;
|
|
1662
1661
|
/**
|
|
1663
|
-
* Graceful drain timeout passed to `conn.close()` during `server.close()
|
|
1662
|
+
* Graceful drain timeout passed to `conn.close()` during `server.close()`.
|
|
1664
1663
|
* Default: 30 000 ms.
|
|
1665
1664
|
*/
|
|
1666
1665
|
drainTimeoutMs?: number;
|
|
1667
1666
|
/**
|
|
1668
|
-
* Enable TLS (MLLPS) for this server
|
|
1667
|
+
* Enable TLS (MLLPS) for this server. When set, the server binds a
|
|
1669
1668
|
* `tls.Server` instead of a plain `net.Server`, consumes `'secureConnection'`
|
|
1670
1669
|
* (post-handshake sockets) instead of `'connection'`, and surfaces failed
|
|
1671
1670
|
* handshakes via the `'tlsClientError'` event rather than crashing.
|
|
@@ -1676,14 +1675,14 @@ interface ServerOptions {
|
|
|
1676
1675
|
*/
|
|
1677
1676
|
tls?: ServerTlsOptions;
|
|
1678
1677
|
/**
|
|
1679
|
-
* Opt-in required to bind a wildcard host
|
|
1680
|
-
*
|
|
1678
|
+
* Opt-in required to bind a wildcard host, a bind-safety guardrail.
|
|
1679
|
+
* Without this flag, `listen()` rejects a wildcard host in
|
|
1681
1680
|
* two tiers: **literal spellings** (`'0.0.0.0'`, `'::'`, `''`, `'::0'`,
|
|
1682
1681
|
* `'0:0:0:0:0:0:0:0'`, `'::ffff:0.0.0.0'`, …) reject **pre-bind** (fast
|
|
1683
1682
|
* path, nothing is ever bound); **resolver-only shorthands** (`'0'`,
|
|
1684
1683
|
* `'0.0'`, `'0x0.0.0.0'`, hostnames resolving to the unspecified
|
|
1685
1684
|
* address, …) are caught **post-bind** against the OS-normalized bound
|
|
1686
|
-
* address
|
|
1685
|
+
* address, the just-bound server closes before any connection can be
|
|
1687
1686
|
* accepted and `listen()` rejects, with no listening state and no
|
|
1688
1687
|
* `'listening'` event either way. When `true`, binding a wildcard host
|
|
1689
1688
|
* emits a one-time `'securityWarning'` (`MLLP_BIND_ALL_INTERFACES`) at
|
|
@@ -1694,7 +1693,7 @@ interface ServerOptions {
|
|
|
1694
1693
|
allowWildcardBind?: boolean;
|
|
1695
1694
|
}
|
|
1696
1695
|
/**
|
|
1697
|
-
* Options for `createStarterServer()
|
|
1696
|
+
* Options for `createStarterServer()`, the "three lines of code" factory.
|
|
1698
1697
|
*
|
|
1699
1698
|
* Extends `ServerOptions` with `port`, `host`, and `handleSignals`. Defaults:
|
|
1700
1699
|
* `autoAck: 'AA'`, `drainTimeoutMs: 30_000`, `Symbol.asyncDispose` wired.
|
|
@@ -1715,23 +1714,23 @@ interface StarterServerOptions extends ServerOptions {
|
|
|
1715
1714
|
/** Port to listen on. */
|
|
1716
1715
|
port: number;
|
|
1717
1716
|
/**
|
|
1718
|
-
* Host to bind to. Default `'127.0.0.1'` (
|
|
1717
|
+
* Host to bind to. Default `'127.0.0.1'` (bind-safety hardening,
|
|
1719
1718
|
* was `'0.0.0.0'`; binding all interfaces now requires
|
|
1720
1719
|
* `ServerOptions.allowWildcardBind: true`).
|
|
1721
1720
|
*/
|
|
1722
1721
|
host?: string;
|
|
1723
1722
|
/**
|
|
1724
1723
|
* Register `process.once('SIGTERM')` and `process.once('SIGINT')` handlers that
|
|
1725
|
-
* call `server.close()` then `process.exit(0)
|
|
1724
|
+
* call `server.close()` then `process.exit(0)`. Default: `false`.
|
|
1726
1725
|
*
|
|
1727
1726
|
* Handlers are automatically removed when `server.close()` is called, so
|
|
1728
|
-
* `process.listenerCount('SIGTERM') === 0` after close() completes
|
|
1727
|
+
* `process.listenerCount('SIGTERM') === 0` after close() completes, preventing
|
|
1729
1728
|
* handler accumulation across test instances or multiple server restarts.
|
|
1730
1729
|
*/
|
|
1731
1730
|
handleSignals?: boolean;
|
|
1732
1731
|
}
|
|
1733
1732
|
/**
|
|
1734
|
-
* MLLP TCP server
|
|
1733
|
+
* MLLP TCP server, wraps `net.Server` without extending it.
|
|
1735
1734
|
*
|
|
1736
1735
|
* Each accepted socket is wrapped in a `NetTransport`, connected to a `Connection`
|
|
1737
1736
|
* with server-level framing options, and added to `_connections`. Messages are
|
|
@@ -1739,7 +1738,7 @@ interface StarterServerOptions extends ServerOptions {
|
|
|
1739
1738
|
*
|
|
1740
1739
|
* Public events: `'listening'`, `'connection'`, `'message'`, `'nack'`, `'error'`, `'close'`.
|
|
1741
1740
|
* The `'nack'` event ({@link NackEvent}) fires whenever the server returns a negative ACK
|
|
1742
|
-
* instead of `AA` on the auto-ACK path
|
|
1741
|
+
* instead of `AA` on the auto-ACK path, a commit-gated `autoAck: 'AA'` handler failing, or
|
|
1743
1742
|
* a positive auto-ACK **downgraded** because the inbound could not be correlated (an
|
|
1744
1743
|
* unreadable/uncorrelatable message, or one whose bytes the decoder discarded). The
|
|
1745
1744
|
* {@link NackReason} distinguishes the causes.
|
|
@@ -1747,19 +1746,19 @@ interface StarterServerOptions extends ServerOptions {
|
|
|
1747
1746
|
* **`'error'` contract:** underlying `net.Server`/`tls.Server` errors are forwarded to the
|
|
1748
1747
|
* `'error'` event whenever a listener is attached. With **no** listener, the outcome depends on
|
|
1749
1748
|
* server state: during a `listen()` (the bind window) the error **rejects the `listen()` promise**
|
|
1750
|
-
*
|
|
1749
|
+
* the primary error surface, and the process never crashes on a bind error; with no `listen()`
|
|
1751
1750
|
* in flight and the server not serving (e.g. a stale async error after `close()`) the error is
|
|
1752
1751
|
* dropped; but **while serving**, an unlistened runtime error (e.g. accept-loop `EMFILE`) keeps
|
|
1753
|
-
* Node's fail-loud crash-on-unlistened-`'error'` convention
|
|
1752
|
+
* Node's fail-loud crash-on-unlistened-`'error'` convention, a silent accept outage is
|
|
1754
1753
|
* impossible. Caveat for `'error'` listeners: the forwarder runs **before** the internal
|
|
1755
1754
|
* `listen()` rejection handler, so an `'error'` listener that synchronously calls `close()`
|
|
1756
1755
|
* during the bind window changes the `listen()` rejection from the bind error to the typed
|
|
1757
|
-
* close-during-listen `MllpConnectionError
|
|
1756
|
+
* close-during-listen `MllpConnectionError`, match on the `'error'` event payload, not the
|
|
1758
1757
|
* rejection, if you do that.
|
|
1759
|
-
*
|
|
1758
|
+
* TLS (MLLPS) adds two more: `'tlsClientError'` (a failed TLS handshake, the
|
|
1760
1759
|
* server logs it and keeps serving other connections) and `'securityWarning'` (loud,
|
|
1761
1760
|
* one-time notice when a wildcard host is bound via `allowWildcardBind: true`).
|
|
1762
|
-
* All event payloads are `Object.freeze()`'d before emission
|
|
1761
|
+
* All event payloads are `Object.freeze()`'d before emission.
|
|
1763
1762
|
*
|
|
1764
1763
|
* @example
|
|
1765
1764
|
* ```typescript
|
|
@@ -1778,13 +1777,13 @@ declare class MllpServer extends EventEmitter {
|
|
|
1778
1777
|
private readonly _netServer;
|
|
1779
1778
|
private readonly _connections;
|
|
1780
1779
|
private readonly _opts;
|
|
1781
|
-
/** `true` when `ServerOptions.tls` is set
|
|
1780
|
+
/** `true` when `ServerOptions.tls` is set, `_netServer` is actually a `tls.Server`. */
|
|
1782
1781
|
private readonly _isTls;
|
|
1783
1782
|
private _listening;
|
|
1784
|
-
/**
|
|
1783
|
+
/** Single-flight guard: `true` while a `listen()` call is settling. */
|
|
1785
1784
|
private _listenInFlight;
|
|
1786
1785
|
/**
|
|
1787
|
-
*
|
|
1786
|
+
* Settle hook for an in-flight `listen()`. Non-null only while a
|
|
1788
1787
|
* `listen()` is in flight; invoked by `close()` so a close racing startup
|
|
1789
1788
|
* rejects the pending `listen()` (typed) instead of leaving it hung with
|
|
1790
1789
|
* the single-flight guard stuck. Cleared on every settle path.
|
|
@@ -1794,7 +1793,7 @@ declare class MllpServer extends EventEmitter {
|
|
|
1794
1793
|
private _host;
|
|
1795
1794
|
private _acceptedTotal;
|
|
1796
1795
|
private _closedTotal;
|
|
1797
|
-
/**
|
|
1796
|
+
/** Count of `'tlsClientError'` events since listen(). */
|
|
1798
1797
|
private _tlsClientErrorsTotal;
|
|
1799
1798
|
/**
|
|
1800
1799
|
* Construct an MLLP server. Created idle; call `listen()` (or use
|
|
@@ -1810,23 +1809,23 @@ declare class MllpServer extends EventEmitter {
|
|
|
1810
1809
|
* `Object.freeze({ port: actualPort, host: actualHost })`.
|
|
1811
1810
|
*
|
|
1812
1811
|
* **Single-flight:** one `listen()` per server lifecycle at a time. A call
|
|
1813
|
-
* while the server is already listening
|
|
1814
|
-
* still in flight
|
|
1812
|
+
* while the server is already listening, or while another `listen()` is
|
|
1813
|
+
* still in flight, rejects with a typed `MllpConnectionError` (concurrent
|
|
1815
1814
|
* binds raced each other's post-bind safety checks). Call `close()` before
|
|
1816
1815
|
* re-listening; sequential `listen()` → `close()` → `listen()` is fine.
|
|
1817
1816
|
*
|
|
1818
1817
|
* **`close()` during an in-flight `listen()`** rejects that `listen()` with
|
|
1819
1818
|
* a typed `MllpConnectionError` (never a hang) and clears the single-flight
|
|
1820
|
-
* guard
|
|
1819
|
+
* guard, a subsequent `listen()` on the same server works. This makes
|
|
1821
1820
|
* `Symbol.asyncDispose` (which delegates to `close()`) safe even before a
|
|
1822
1821
|
* `listen()` has settled. (Exception: `close({ signal })` with an
|
|
1823
|
-
* already-aborted signal is a no-op AbortError rejection
|
|
1822
|
+
* already-aborted signal is a no-op AbortError rejection, the in-flight
|
|
1824
1823
|
* `listen()` is left to settle on its own bind outcome; see `close()`.)
|
|
1825
1824
|
*
|
|
1826
1825
|
* @param port - TCP port to bind. Use `0` to let the OS assign an ephemeral port.
|
|
1827
1826
|
* @param hostOrOpts - Host string or object with `host` and optional `signal`.
|
|
1828
|
-
* Default host: `'127.0.0.1'` (
|
|
1829
|
-
* are rejected unless `ServerOptions.allowWildcardBind: true
|
|
1827
|
+
* Default host: `'127.0.0.1'` (bind-safety hardening). Wildcard hosts
|
|
1828
|
+
* are rejected unless `ServerOptions.allowWildcardBind: true`, literal
|
|
1830
1829
|
* spellings pre-bind, resolver-only shorthands post-bind via the
|
|
1831
1830
|
* OS-normalized bound address.
|
|
1832
1831
|
*
|
|
@@ -1844,19 +1843,19 @@ declare class MllpServer extends EventEmitter {
|
|
|
1844
1843
|
/**
|
|
1845
1844
|
* Stop accepting new connections and gracefully close all active connections.
|
|
1846
1845
|
*
|
|
1847
|
-
* Sequence
|
|
1848
|
-
* 1. `net.Server.close()
|
|
1846
|
+
* Sequence:
|
|
1847
|
+
* 1. `net.Server.close()`, stops accepting new connections immediately
|
|
1849
1848
|
* 2. If `_connections` is empty: resolves immediately (no drain needed)
|
|
1850
|
-
* 3. Calls `_drainAll(drainTimeoutMs)
|
|
1849
|
+
* 3. Calls `_drainAll(drainTimeoutMs)`, Promise.all + side-effect setTimeout
|
|
1851
1850
|
* that force-destroys stragglers after the drain window
|
|
1852
1851
|
*
|
|
1853
1852
|
* Calling `close()` while a `listen()` is still in flight proactively
|
|
1854
1853
|
* settles that `listen()` with a typed `MllpConnectionError` rejection
|
|
1855
|
-
* and clears the single-flight guard
|
|
1854
|
+
* and clears the single-flight guard, the server is immediately
|
|
1856
1855
|
* re-listenable. `Symbol.asyncDispose` (which delegates here) is
|
|
1857
1856
|
* therefore safe before `listen()` settles. **Qualification:** a
|
|
1858
1857
|
* `close({ signal })` whose signal is **already aborted** rejects
|
|
1859
|
-
* immediately with `AbortError` and performs no work
|
|
1858
|
+
* immediately with `AbortError` and performs no work, it does NOT close
|
|
1860
1859
|
* the server and does NOT settle the in-flight `listen()`, which simply
|
|
1861
1860
|
* continues and settles on its own bind outcome.
|
|
1862
1861
|
*
|
|
@@ -1876,7 +1875,7 @@ declare class MllpServer extends EventEmitter {
|
|
|
1876
1875
|
/**
|
|
1877
1876
|
* Drain all active connections with a shared timeout.
|
|
1878
1877
|
*
|
|
1879
|
-
* Uses Promise.all (not Promise.race) for coordination
|
|
1878
|
+
* Uses Promise.all (not Promise.race) for coordination, the timeout is a
|
|
1880
1879
|
* side effect that calls `conn.destroy()` on stragglers after `drainTimeoutMs`.
|
|
1881
1880
|
* Promise.all resolves when all `conn.close()` promises settle (which happens
|
|
1882
1881
|
* because `conn.destroy()` transitions connections to CLOSED).
|
|
@@ -1885,7 +1884,7 @@ declare class MllpServer extends EventEmitter {
|
|
|
1885
1884
|
*/
|
|
1886
1885
|
private _drainAll;
|
|
1887
1886
|
/**
|
|
1888
|
-
* Async disposal
|
|
1887
|
+
* Async disposal, delegates to `close()` for `await using` support.
|
|
1889
1888
|
*
|
|
1890
1889
|
* @example
|
|
1891
1890
|
* ```typescript
|
|
@@ -1896,10 +1895,9 @@ declare class MllpServer extends EventEmitter {
|
|
|
1896
1895
|
*/
|
|
1897
1896
|
[Symbol.asyncDispose](): Promise<void>;
|
|
1898
1897
|
/**
|
|
1899
|
-
* Return a JSON-serializable observability snapshot
|
|
1898
|
+
* Return a JSON-serializable observability snapshot.
|
|
1900
1899
|
*
|
|
1901
1900
|
* `totalBytesIn` and `totalBytesOut` aggregate from live connections at call time.
|
|
1902
|
-
* Plan 04 adds full aggregation; at Plan 01 scope they return 0.
|
|
1903
1901
|
*
|
|
1904
1902
|
* @example
|
|
1905
1903
|
* ```typescript
|
|
@@ -1909,7 +1907,7 @@ declare class MllpServer extends EventEmitter {
|
|
|
1909
1907
|
*/
|
|
1910
1908
|
getStats(): ServerStats;
|
|
1911
1909
|
/**
|
|
1912
|
-
* Emit on `'error'` only when a listener is attached
|
|
1910
|
+
* Emit on `'error'` only when a listener is attached, an unlistened
|
|
1913
1911
|
* `EventEmitter` `'error'` emission throws (see the class JSDoc error
|
|
1914
1912
|
* contract). The single guard shared by the optional-tap forwarding sites
|
|
1915
1913
|
* in this class; the constructor's net.Server forwarder deliberately does
|
|
@@ -1919,7 +1917,7 @@ declare class MllpServer extends EventEmitter {
|
|
|
1919
1917
|
/**
|
|
1920
1918
|
* Emit a server event, containing a throwing subscriber.
|
|
1921
1919
|
*
|
|
1922
|
-
* Every event this server emits is reachable from a callback we do not own
|
|
1920
|
+
* Every event this server emits is reachable from a callback we do not own, the `net.Server`
|
|
1923
1921
|
* `'connection'` listener, the `tls.Server` `'tlsClientError'` listener, a socket's `'data'`
|
|
1924
1922
|
* listener (via `Connection`), or the `catch` block of a `void`-ed async ACK task. A throwing
|
|
1925
1923
|
* subscriber would therefore unwind into that callback and kill the process (or reject a void-ed
|
|
@@ -1931,8 +1929,8 @@ declare class MllpServer extends EventEmitter {
|
|
|
1931
1929
|
private _emitErrorIfListened;
|
|
1932
1930
|
private _onSocketAccepted;
|
|
1933
1931
|
/**
|
|
1934
|
-
* Extract a minimal, content-free peer-certificate summary (
|
|
1935
|
-
*
|
|
1932
|
+
* Extract a minimal, content-free peer-certificate summary (ATNA ITI-19
|
|
1933
|
+
* mutual authentication), CN strings, expiry, and the verification
|
|
1936
1934
|
* outcome (`authorized`) only; never the full certificate object. Returns
|
|
1937
1935
|
* `null` when no peer certificate was presented (e.g. `clientAuth: 'WANT'`
|
|
1938
1936
|
* with no client cert).
|
|
@@ -1945,29 +1943,29 @@ declare class MllpServer extends EventEmitter {
|
|
|
1945
1943
|
* then dispatches: resolve ⇒ `AA`; throw/reject ⇒ a negative ACK (`AE` by default,
|
|
1946
1944
|
* `AR` via {@link MllpAckError}). A positive ACK can never precede a successful commit.
|
|
1947
1945
|
*
|
|
1948
|
-
* Without an `onMessage` handler, `'AA'` degrades to a **transport-accept
|
|
1946
|
+
* Without an `onMessage` handler, `'AA'` degrades to a **transport-accept**, `AA`
|
|
1949
1947
|
* meaning "bytes received and framed", not "application-processed".
|
|
1950
1948
|
*
|
|
1951
1949
|
* A handler failure is **expected flow**, not a server error: it produces a negative
|
|
1952
1950
|
* ACK and a `'nack'` event. The thrown error's message is never placed on the wire or
|
|
1953
|
-
* in the event (it may carry PHI)
|
|
1951
|
+
* in the event (it may carry PHI), only the static `ackCode` and the inbound control
|
|
1954
1952
|
* metadata reach the peer.
|
|
1955
1953
|
*/
|
|
1956
1954
|
private _sendCommitAck;
|
|
1957
1955
|
/**
|
|
1958
1956
|
* Resolve the auto-ACK code for a message the server would otherwise acknowledge
|
|
1959
|
-
* **positively** (`AA`)
|
|
1957
|
+
* **positively** (`AA`), the no-handler transport-accept and the post-commit success
|
|
1960
1958
|
* paths. Fail-safe: downgrade to `AE` and emit a PHI-safe `'nack'` whenever the message
|
|
1961
1959
|
* cannot carry a correlatable positive ACK, so `AA` never names a control ID the sender
|
|
1962
1960
|
* cannot match (→ timeout → resend → **duplicate clinical message**).
|
|
1963
1961
|
*
|
|
1964
1962
|
* Two independent disqualifiers, one from the bytes and one from the frame:
|
|
1965
1963
|
*
|
|
1966
|
-
* - **`rawAckUncorrelatable(payload)
|
|
1964
|
+
* - **`rawAckUncorrelatable(payload)`**, no readable `MSH`, an empty MSH-10, or a
|
|
1967
1965
|
* batch/concatenated-message shape. This is the SAME predicate `buildRawAck` enforces
|
|
1968
1966
|
* on the wire; it is re-checked here so the downgrade is **observable** (a `'nack'`
|
|
1969
1967
|
* event) rather than a silent code change inside the builder.
|
|
1970
|
-
* - **`MLLP_TRAILING_BYTES`** in the frame's warnings
|
|
1968
|
+
* - **`MLLP_TRAILING_BYTES`** in the frame's warnings, a mid-payload `VT` made the
|
|
1971
1969
|
* decoder discard accumulated bytes and deliver only the fragment after it. The
|
|
1972
1970
|
* clinical message was destroyed in transit, so even a fragment that happens to carry
|
|
1973
1971
|
* a readable MSH must not be positively acknowledged. This condition is invisible to
|
|
@@ -1976,33 +1974,33 @@ declare class MllpServer extends EventEmitter {
|
|
|
1976
1974
|
* accumulating the delivered frame, so it is frame-scoped: it names *this* payload's own
|
|
1977
1975
|
* discard, never a neighbouring frame's trailing junk.
|
|
1978
1976
|
*
|
|
1979
|
-
* `'discarded-bytes'` takes precedence in the reason when both hold
|
|
1977
|
+
* `'discarded-bytes'` takes precedence in the reason when both hold, it is the root cause.
|
|
1980
1978
|
*/
|
|
1981
1979
|
private _resolveAutoAckPositive;
|
|
1982
1980
|
/**
|
|
1983
1981
|
* Run `onMessage` for its side effects only (custom-ACK and manual modes), guarding
|
|
1984
1982
|
* against a synchronous throw or a rejected promise. In these modes the handler does
|
|
1985
1983
|
* **not** gate the ACK, so a failure is re-emitted as a PHI-safe `'error'` on the
|
|
1986
|
-
* connection (
|
|
1984
|
+
* connection (a handler error never crashes the server) rather than escaping as
|
|
1987
1985
|
* an unhandled rejection. The error text is never placed on the wire.
|
|
1988
1986
|
*/
|
|
1989
1987
|
private _observeMessage;
|
|
1990
1988
|
/**
|
|
1991
1989
|
* The custom-builder path for `autoAck: fn`. Awaits the builder and dispatches its
|
|
1992
1990
|
* Buffer as the ACK; the caller owns MSA-1. A builder error is re-emitted as `'error'`
|
|
1993
|
-
* on the connection
|
|
1991
|
+
* on the connection, the server never crashes.
|
|
1994
1992
|
*/
|
|
1995
1993
|
private _sendCustomAck;
|
|
1996
1994
|
/**
|
|
1997
1995
|
* Frame an ACK payload and write it to the connection. `Connection.send()` returns
|
|
1998
1996
|
* `false` under backpressure (socket write buffer full); that drops the ACK and emits
|
|
1999
|
-
* a `MllpConnectionError({ phase: 'send' })` on the connection
|
|
1997
|
+
* a `MllpConnectionError({ phase: 'send' })` on the connection, the peer will
|
|
2000
1998
|
* time out waiting and may retry. The server never crashes.
|
|
2001
1999
|
*/
|
|
2002
2000
|
private _dispatchAck;
|
|
2003
2001
|
}
|
|
2004
2002
|
/**
|
|
2005
|
-
* Factory function
|
|
2003
|
+
* Factory function, creates a new `MllpServer` with the supplied options.
|
|
2006
2004
|
*
|
|
2007
2005
|
* Prefer `createServer()` over `new MllpServer()` for forward-compatible construction.
|
|
2008
2006
|
*
|
|
@@ -2020,13 +2018,11 @@ declare class MllpServer extends EventEmitter {
|
|
|
2020
2018
|
*/
|
|
2021
2019
|
declare function createServer(opts?: ServerOptions): MllpServer;
|
|
2022
2020
|
/**
|
|
2023
|
-
* Starter factory
|
|
2021
|
+
* Starter factory, creates, configures, and starts an `MllpServer` in one call.
|
|
2024
2022
|
*
|
|
2025
2023
|
* Provides the "three lines of code" north-star experience with sensible defaults:
|
|
2026
2024
|
* `autoAck: 'AA'`, `drainTimeoutMs: 30_000`, `Symbol.asyncDispose` wired.
|
|
2027
2025
|
*
|
|
2028
|
-
* NOTE: Stub at Plan 01 scope — Plan 04 fills in the full implementation.
|
|
2029
|
-
*
|
|
2030
2026
|
* @example
|
|
2031
2027
|
* ```typescript
|
|
2032
2028
|
* import { createStarterServer } from '@cosyte/mllp';
|
|
@@ -2040,18 +2036,15 @@ declare function createServer(opts?: ServerOptions): MllpServer;
|
|
|
2040
2036
|
declare function createStarterServer(opts: StarterServerOptions): Promise<MllpServer>;
|
|
2041
2037
|
|
|
2042
2038
|
/**
|
|
2043
|
-
* MLLP Client
|
|
2039
|
+
* MLLP Client, `createClient()` factory and `MllpClient` class.
|
|
2044
2040
|
*
|
|
2045
2041
|
* Provides the client-side MLLP transport: connect to an MLLP server over TCP,
|
|
2046
2042
|
* encode and send framed messages, decode inbound ACKs, and surface lifecycle
|
|
2047
2043
|
* events with frozen payloads. Supports `AbortSignal` cancellation on every
|
|
2048
2044
|
* awaitable and `Symbol.asyncDispose` for `await using` ergonomics.
|
|
2049
2045
|
*
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2052
|
-
* The `correlateByControlId` option (MSH-10 → MSA-2 ACK matching) lights up
|
|
2053
|
-
* out-of-order ACK handling. Subsequent plans add auto-reconnect (Plan 04),
|
|
2054
|
-
* backpressure (Plan 05), and `createStarterClient` + `getStats()` (PLAN-06).
|
|
2046
|
+
* The `correlateByControlId` option (MSH-10 → MSA-2 ACK matching) enables
|
|
2047
|
+
* out-of-order ACK handling.
|
|
2055
2048
|
*
|
|
2056
2049
|
* @example
|
|
2057
2050
|
* ```typescript
|
|
@@ -2059,7 +2052,7 @@ declare function createStarterServer(opts: StarterServerOptions): Promise<MllpSe
|
|
|
2059
2052
|
*
|
|
2060
2053
|
* const client = createClient({ host: 'localhost', port: 2575 });
|
|
2061
2054
|
* await client.connect();
|
|
2062
|
-
*
|
|
2055
|
+
* const ack = await client.send(payloadBuffer);
|
|
2063
2056
|
* await client.close();
|
|
2064
2057
|
* ```
|
|
2065
2058
|
*
|
|
@@ -2067,11 +2060,9 @@ declare function createStarterServer(opts: StarterServerOptions): Promise<MllpSe
|
|
|
2067
2060
|
*/
|
|
2068
2061
|
|
|
2069
2062
|
/**
|
|
2070
|
-
* Context passed to a custom `retryStrategy` hook on each reconnect attempt
|
|
2071
|
-
* (CLIENT-12, D-15).
|
|
2063
|
+
* Context passed to a custom `retryStrategy` hook on each reconnect attempt.
|
|
2072
2064
|
*
|
|
2073
|
-
* Frozen via `Object.freeze` before invocation
|
|
2074
|
-
* (T-05-04-04 mitigation).
|
|
2065
|
+
* Frozen via `Object.freeze` before invocation, handlers cannot mutate.
|
|
2075
2066
|
*
|
|
2076
2067
|
* @example
|
|
2077
2068
|
* ```typescript
|
|
@@ -2093,26 +2084,26 @@ interface RetryContext {
|
|
|
2093
2084
|
readonly totalElapsedMs: number;
|
|
2094
2085
|
/** Ms since the last successful ACK. `Infinity` if no success seen. */
|
|
2095
2086
|
readonly sinceLastSuccessMs: number;
|
|
2096
|
-
/**
|
|
2087
|
+
/** How the failure that triggered this reconnect attempt was classified. */
|
|
2097
2088
|
readonly classifiedAs: "transient" | "permanent";
|
|
2098
2089
|
/**
|
|
2099
2090
|
* The same `AbortSignal` passed into `connect()`. If no signal was
|
|
2100
2091
|
* supplied, the module-level `NEVER_ABORTING_SIGNAL` sentinel is provided
|
|
2101
|
-
* so handlers always have a real `AbortSignal` to inspect.
|
|
2092
|
+
* so handlers always have a real `AbortSignal` to inspect.
|
|
2102
2093
|
*/
|
|
2103
2094
|
readonly signal: AbortSignal;
|
|
2104
2095
|
}
|
|
2105
2096
|
/**
|
|
2106
|
-
* Custom reconnect-backoff hook
|
|
2107
|
-
* reconnection
|
|
2097
|
+
* Custom reconnect-backoff hook. Return `null` to halt
|
|
2098
|
+
* reconnection, the FSM transitions to `CLOSED`.
|
|
2108
2099
|
*/
|
|
2109
2100
|
type RetryStrategy = (ctx: RetryContext) => number | null;
|
|
2110
2101
|
/**
|
|
2111
|
-
* Combined count + byte-based queue cap. Stricter-of-two wins
|
|
2102
|
+
* Combined count + byte-based queue cap. Stricter-of-two wins.
|
|
2112
2103
|
*
|
|
2113
|
-
* - `number
|
|
2114
|
-
* - `{ bytes }
|
|
2115
|
-
* - `{ count, bytes }
|
|
2104
|
+
* - `number`, count cap only (default 64).
|
|
2105
|
+
* - `{ bytes }`, byte cap only.
|
|
2106
|
+
* - `{ count, bytes }`, both caps; whichever trips first wins.
|
|
2116
2107
|
*
|
|
2117
2108
|
* @example
|
|
2118
2109
|
* ```typescript
|
|
@@ -2129,8 +2120,6 @@ type HighWaterMark = number | {
|
|
|
2129
2120
|
/**
|
|
2130
2121
|
* Options for {@link createClient} and the {@link MllpClient} constructor.
|
|
2131
2122
|
*
|
|
2132
|
-
* Phase 5 plans extend this incrementally — new fields are additive and optional.
|
|
2133
|
-
*
|
|
2134
2123
|
* @example
|
|
2135
2124
|
* ```typescript
|
|
2136
2125
|
* const opts: ClientOptions = { host: 'localhost', port: 2575, drainTimeoutMs: 10_000 };
|
|
@@ -2146,54 +2135,54 @@ interface ClientOptions {
|
|
|
2146
2135
|
/** Drain timeout for {@link MllpClient.close} (default: `30_000` ms). */
|
|
2147
2136
|
readonly drainTimeoutMs?: number;
|
|
2148
2137
|
/**
|
|
2149
|
-
* Per-message ACK timeout in milliseconds
|
|
2150
|
-
* the underlying `write()` flush callback, NOT at the `send()` call
|
|
2138
|
+
* Per-message ACK timeout in milliseconds. The clock starts at
|
|
2139
|
+
* the underlying `write()` flush callback, NOT at the `send()` call,
|
|
2151
2140
|
* pre-flush queue time is not charged to the peer. Default: `30_000`.
|
|
2152
2141
|
*/
|
|
2153
2142
|
readonly ackTimeoutMs?: number;
|
|
2154
2143
|
/**
|
|
2155
|
-
* If `true`, ACKs are matched against outgoing sends by MSH-10 → MSA-2
|
|
2156
|
-
*
|
|
2144
|
+
* If `true`, ACKs are matched against outgoing sends by MSH-10 → MSA-2.
|
|
2145
|
+
* Default `false` (FIFO mode).
|
|
2157
2146
|
*
|
|
2158
2147
|
* Out-of-order ACKs from the peer are supported in this mode. MSH-10 is
|
|
2159
2148
|
* extracted from the outbound payload before send; MSA-2 is extracted from
|
|
2160
2149
|
* the inbound ACK payload. An ACK whose MSA-2 matches no pending send
|
|
2161
2150
|
* (and is not in the late-ACK graveyard) emits a frozen
|
|
2162
|
-
* `MllpFramingError('MLLP_ACK_UNMATCHED_CONTROL_ID')` to the `'error'` event
|
|
2163
|
-
*
|
|
2164
|
-
* `MLLP_ACK_AFTER_TIMEOUT` warning
|
|
2151
|
+
* `MllpFramingError('MLLP_ACK_UNMATCHED_CONTROL_ID')` to the `'error'` event.
|
|
2152
|
+
* A late ACK whose MSA-2 matches a graveyard entry emits a
|
|
2153
|
+
* `MLLP_ACK_AFTER_TIMEOUT` warning and is dropped.
|
|
2165
2154
|
*
|
|
2166
2155
|
* @default false
|
|
2167
2156
|
*/
|
|
2168
2157
|
readonly correlateByControlId?: boolean;
|
|
2169
2158
|
/**
|
|
2170
|
-
* Auto-reconnect on transient disconnect
|
|
2159
|
+
* Auto-reconnect on transient disconnect. Default `false`.
|
|
2171
2160
|
*
|
|
2172
2161
|
* When `true`, dropped connections caused by transient errors (per
|
|
2173
2162
|
* {@link isTransientConnectionError}) trigger the FSM cycle
|
|
2174
2163
|
* `CONNECTED → DISCONNECTED → RECONNECTING → CONNECTING → CONNECTED`
|
|
2175
|
-
* with exponential backoff
|
|
2164
|
+
* with exponential backoff unless overridden by
|
|
2176
2165
|
* {@link ClientOptions.retryStrategy}. Permanent errors halt and
|
|
2177
|
-
* transition directly to `CLOSED
|
|
2166
|
+
* transition directly to `CLOSED`.
|
|
2178
2167
|
*/
|
|
2179
2168
|
readonly autoReconnect?: boolean;
|
|
2180
2169
|
/**
|
|
2181
|
-
* Custom reconnect-backoff hook
|
|
2182
|
-
* reconnection
|
|
2183
|
-
* to the exponential strategy
|
|
2170
|
+
* Custom reconnect-backoff hook. Return `null` to halt
|
|
2171
|
+
* reconnection. Receives a frozen {@link RetryContext}. Defaults
|
|
2172
|
+
* to the exponential strategy.
|
|
2184
2173
|
*/
|
|
2185
2174
|
readonly retryStrategy?: RetryStrategy;
|
|
2186
|
-
/** First delay (ms) on auto-reconnect; default 100.
|
|
2175
|
+
/** First delay (ms) on auto-reconnect; default 100. */
|
|
2187
2176
|
readonly initialDelayMs?: number;
|
|
2188
|
-
/** Maximum backoff cap (ms); default 30_000.
|
|
2177
|
+
/** Maximum backoff cap (ms); default 30_000. */
|
|
2189
2178
|
readonly maxDelayMs?: number;
|
|
2190
|
-
/** Backoff multiplier; default 2.
|
|
2179
|
+
/** Backoff multiplier; default 2. */
|
|
2191
2180
|
readonly multiplier?: number;
|
|
2192
|
-
/** Jitter fraction, e.g. 0.2 = ±20%; default 0.2.
|
|
2181
|
+
/** Jitter fraction, e.g. 0.2 = ±20%; default 0.2. */
|
|
2193
2182
|
readonly jitter?: number;
|
|
2194
2183
|
/**
|
|
2195
|
-
* Application-level high-water mark on the in-flight + queued send set
|
|
2196
|
-
*
|
|
2184
|
+
* Application-level high-water mark on the in-flight + queued send set.
|
|
2185
|
+
* `number` configures a count cap (default 64);
|
|
2197
2186
|
* `{ bytes }` configures a byte cap; `{ count, bytes }` configures
|
|
2198
2187
|
* both, with the stricter-of-two trigger winning.
|
|
2199
2188
|
*
|
|
@@ -2204,21 +2193,21 @@ interface ClientOptions {
|
|
|
2204
2193
|
*/
|
|
2205
2194
|
readonly highWaterMark?: HighWaterMark;
|
|
2206
2195
|
/**
|
|
2207
|
-
* Behavior when the high-water mark is exceeded
|
|
2196
|
+
* Behavior when the high-water mark is exceeded.
|
|
2208
2197
|
*
|
|
2209
|
-
* - `'reject'` (default)
|
|
2210
|
-
* - `'wait'
|
|
2211
|
-
* `ackTimeoutMs` OR `signal` abort, whichever fires first
|
|
2198
|
+
* - `'reject'` (default), `send()` rejects with `MllpBackpressureError`.
|
|
2199
|
+
* - `'wait'`, `send()` awaits the `'drain'` event OR the per-message
|
|
2200
|
+
* `ackTimeoutMs` OR `signal` abort, whichever fires first.
|
|
2212
2201
|
*
|
|
2213
2202
|
* @default 'reject'
|
|
2214
2203
|
*/
|
|
2215
2204
|
readonly onBackpressure?: "reject" | "wait";
|
|
2216
2205
|
/**
|
|
2217
|
-
* Strict serialization send → await-ACK → send
|
|
2206
|
+
* Strict serialization send → await-ACK → send.
|
|
2218
2207
|
*
|
|
2219
|
-
* - `true` (default)
|
|
2208
|
+
* - `true` (default), concurrent in-flight sends up to
|
|
2220
2209
|
* {@link ClientOptions.highWaterMark}.
|
|
2221
|
-
* - `false
|
|
2210
|
+
* - `false`, collapses the in-flight set to ≤1 (the unified Correlator's
|
|
2222
2211
|
* `maxInFlight=1`); the next `send()` waits for the prior ACK before
|
|
2223
2212
|
* reaching the wire.
|
|
2224
2213
|
*
|
|
@@ -2229,24 +2218,24 @@ interface ClientOptions {
|
|
|
2229
2218
|
* TCP keepalive interval (ms). Sets `socket.setKeepAlive(true, ms)` on
|
|
2230
2219
|
* the underlying `net.Socket` BEFORE wrapping in `NetTransport`. OS-level
|
|
2231
2220
|
* half-open detection (network partitions, NAT-table eviction). Independent
|
|
2232
|
-
* of {@link ClientOptions.deadPeerTimeoutMs}
|
|
2221
|
+
* of {@link ClientOptions.deadPeerTimeoutMs}.
|
|
2233
2222
|
*
|
|
2234
2223
|
* @default undefined (off)
|
|
2235
2224
|
*/
|
|
2236
2225
|
readonly keepaliveIntervalMs?: number;
|
|
2237
2226
|
/**
|
|
2238
|
-
* Application-idle timeout (ms) keyed on last inbound bytes / ACK / warning
|
|
2239
|
-
*
|
|
2227
|
+
* Application-idle timeout (ms) keyed on last inbound bytes / ACK / warning.
|
|
2228
|
+
* On trip, calls `connection.destroy(new Error('dead
|
|
2240
2229
|
* peer timeout'))` which surfaces as `MllpConnectionError({ phase: 'receive' })`.
|
|
2241
|
-
* Trip honors {@link ClientOptions.autoReconnect}
|
|
2242
|
-
* {@link ClientOptions.keepaliveIntervalMs}
|
|
2230
|
+
* Trip honors {@link ClientOptions.autoReconnect}. Independent of
|
|
2231
|
+
* {@link ClientOptions.keepaliveIntervalMs}.
|
|
2243
2232
|
*
|
|
2244
2233
|
* @default undefined (off)
|
|
2245
2234
|
*/
|
|
2246
2235
|
readonly deadPeerTimeoutMs?: number;
|
|
2247
2236
|
/**
|
|
2248
|
-
* Enable TLS (MLLPS) for this connection
|
|
2249
|
-
* all defaults
|
|
2237
|
+
* Enable TLS (MLLPS) for this connection. `true` enables TLS with
|
|
2238
|
+
* all defaults, including certificate verification **on**. Pass a
|
|
2250
2239
|
* {@link TlsOptions} object to customize (`ca`/`cert`/`key`, minimum
|
|
2251
2240
|
* version, ciphers, `allowUnverified`, …).
|
|
2252
2241
|
*
|
|
@@ -2257,16 +2246,16 @@ interface ClientOptions {
|
|
|
2257
2246
|
readonly tls?: TlsOptions | true;
|
|
2258
2247
|
}
|
|
2259
2248
|
/**
|
|
2260
|
-
* Observability snapshot returned by {@link MllpClient.getStats}
|
|
2249
|
+
* Observability snapshot returned by {@link MllpClient.getStats}.
|
|
2261
2250
|
*
|
|
2262
|
-
* All fields are JSON-serializable
|
|
2251
|
+
* All fields are JSON-serializable, no Buffers, no class instances,
|
|
2263
2252
|
* no Maps, no circular references. `lastConnectedAt` and `lastAckAt` are
|
|
2264
|
-
* **epoch milliseconds** (numbers), NOT `Date` instances
|
|
2265
|
-
* friendly
|
|
2253
|
+
* **epoch milliseconds** (numbers), NOT `Date` instances, log-pipeline
|
|
2254
|
+
* friendly.
|
|
2266
2255
|
*
|
|
2267
2256
|
* `warningsByCode` keys are constrained to the public {@link WarningCode}
|
|
2268
|
-
* union
|
|
2269
|
-
* stable-codes guardrail enforced at the type boundary
|
|
2257
|
+
* union, adding/removing a code is a breaking change (CLAUDE.md
|
|
2258
|
+
* stable-codes guardrail enforced at the type boundary).
|
|
2270
2259
|
*
|
|
2271
2260
|
* @example
|
|
2272
2261
|
* ```typescript
|
|
@@ -2276,7 +2265,7 @@ interface ClientOptions {
|
|
|
2276
2265
|
* ```
|
|
2277
2266
|
*/
|
|
2278
2267
|
interface ClientStats {
|
|
2279
|
-
/** Current FSM state
|
|
2268
|
+
/** Current FSM state, mirrors `client.state`. */
|
|
2280
2269
|
readonly state: ConnectionState;
|
|
2281
2270
|
/** Live Connection's id, or `null` before the first connect (or post-CLOSED). */
|
|
2282
2271
|
readonly connectionId: string | null;
|
|
@@ -2284,11 +2273,11 @@ interface ClientStats {
|
|
|
2284
2273
|
readonly queueDepth: number;
|
|
2285
2274
|
/** Sum of `frame.length` across live correlator entries. */
|
|
2286
2275
|
readonly queueBytes: number;
|
|
2287
|
-
/** Entries with `sentAt !== null
|
|
2276
|
+
/** Entries with `sentAt !== null`, actually written to the wire / awaiting ACK. */
|
|
2288
2277
|
readonly inFlight: number;
|
|
2289
2278
|
/**
|
|
2290
2279
|
* Aggregated warning counts. Keys are constrained to the public
|
|
2291
|
-
* {@link WarningCode} union
|
|
2280
|
+
* {@link WarningCode} union. Connection-level warnings + Correlator
|
|
2292
2281
|
* `MLLP_ACK_*` warnings are merged.
|
|
2293
2282
|
*/
|
|
2294
2283
|
readonly warningsByCode: Partial<Record<WarningCode, number>>;
|
|
@@ -2302,31 +2291,31 @@ interface ClientStats {
|
|
|
2302
2291
|
readonly ackedTotal: number;
|
|
2303
2292
|
/** Total ACK timeouts since construction. */
|
|
2304
2293
|
readonly timedOutTotal: number;
|
|
2305
|
-
/** Total reconnect attempts since construction
|
|
2294
|
+
/** Total reconnect attempts since construction. */
|
|
2306
2295
|
readonly reconnectAttempts: number;
|
|
2307
2296
|
/** Epoch ms of the last `CONNECTED` transition. `null` until first connect. */
|
|
2308
2297
|
readonly lastConnectedAt: number | null;
|
|
2309
2298
|
/** Epoch ms of the most recent successful ACK. `null` until first ACK. */
|
|
2310
2299
|
readonly lastAckAt: number | null;
|
|
2311
|
-
/** Whether this client is configured for TLS
|
|
2300
|
+
/** Whether this client is configured for TLS. Mirrors `ClientOptions.tls` being set. */
|
|
2312
2301
|
readonly tls: boolean;
|
|
2313
2302
|
}
|
|
2314
2303
|
/**
|
|
2315
|
-
* MLLP client
|
|
2304
|
+
* MLLP client, composes a single {@link Connection} over a {@link NetTransport}
|
|
2316
2305
|
* (production) or any other `Transport` (testing via {@link InMemoryTransport}).
|
|
2317
2306
|
*
|
|
2318
|
-
* Public events
|
|
2319
|
-
* - `'stateChange'
|
|
2320
|
-
* - `'connect'
|
|
2321
|
-
* - `'disconnect'
|
|
2322
|
-
* - `'reconnecting'
|
|
2323
|
-
* - `'close'
|
|
2324
|
-
* - `'message'
|
|
2325
|
-
* - `'warning'
|
|
2326
|
-
* - `'securityWarning'
|
|
2307
|
+
* Public events, every payload `Object.freeze`'d before emission:
|
|
2308
|
+
* - `'stateChange'`, `{ from, to, reason? }` from the underlying Connection FSM
|
|
2309
|
+
* - `'connect'`, `{ connectionId }` once the FSM enters `CONNECTED`
|
|
2310
|
+
* - `'disconnect'`, `{ connectionId }` once the FSM enters `DISCONNECTED`
|
|
2311
|
+
* - `'reconnecting'`, `{ connectionId, attempt?, delayMs? }`
|
|
2312
|
+
* - `'close'`, `{ connectionId }` once the FSM enters terminal `CLOSED`
|
|
2313
|
+
* - `'message'`, `{ payload, connectionId, byteOffset, warnings }` for every inbound frame
|
|
2314
|
+
* - `'warning'`, `MllpWarning` enriched with `connectionId` from the Connection layer
|
|
2315
|
+
* - `'securityWarning'`, `SecurityWarning`. Emitted on every successful
|
|
2327
2316
|
* `secureConnect` (initial + every reconnect) when `tls.allowUnverified` is `true`
|
|
2328
|
-
*
|
|
2329
|
-
* - `'error'
|
|
2317
|
+
* with code `MLLP_TLS_VERIFY_DISABLED`. Also mirrored to `process.emitWarning`.
|
|
2318
|
+
* - `'error'`, re-emitted from Connection. Guarded by `listenerCount('error') > 0` so
|
|
2330
2319
|
* absence of a listener does NOT crash the process (server precedent).
|
|
2331
2320
|
*
|
|
2332
2321
|
* @example
|
|
@@ -2335,7 +2324,7 @@ interface ClientStats {
|
|
|
2335
2324
|
* client.on('stateChange', ({ from, to }) => logger.info({ from, to }));
|
|
2336
2325
|
* client.on('message', ({ payload }) => logger.info({ bytes: payload.length }));
|
|
2337
2326
|
* await client.connect();
|
|
2338
|
-
*
|
|
2327
|
+
* const ack = await client.send(payloadBuffer);
|
|
2339
2328
|
* await client.close();
|
|
2340
2329
|
* ```
|
|
2341
2330
|
*/
|
|
@@ -2348,15 +2337,15 @@ declare class MllpClient extends EventEmitter {
|
|
|
2348
2337
|
* is attached, `state` mirrors `_connection.state`.
|
|
2349
2338
|
*/
|
|
2350
2339
|
private _state;
|
|
2351
|
-
/** Per-message ACK timeout in ms
|
|
2340
|
+
/** Per-message ACK timeout in ms. Resolved at construction. */
|
|
2352
2341
|
private readonly _ackTimeoutMs;
|
|
2353
|
-
/** controlId-mode flag
|
|
2342
|
+
/** controlId-mode flag. `false` → FIFO. */
|
|
2354
2343
|
private readonly _correlateByControlId;
|
|
2355
|
-
/** Unified ACK correlator
|
|
2344
|
+
/** Unified ACK correlator. Built during `_attachConnection`. */
|
|
2356
2345
|
private _correlator;
|
|
2357
2346
|
/**
|
|
2358
2347
|
* Periodic ACK-timeout sweep timer. Drives `_correlator.expireDue()` because
|
|
2359
|
-
* the Correlator is timer-free
|
|
2348
|
+
* the Correlator is timer-free. Cleared on close/destroy.
|
|
2360
2349
|
*/
|
|
2361
2350
|
private _ackSweepTimer;
|
|
2362
2351
|
private readonly _autoReconnect;
|
|
@@ -2368,12 +2357,12 @@ declare class MllpClient extends EventEmitter {
|
|
|
2368
2357
|
/** 0-indexed attempt counter for the current reconnect cycle. */
|
|
2369
2358
|
private _attempt;
|
|
2370
2359
|
/**
|
|
2371
|
-
*
|
|
2360
|
+
* Total reconnect attempts since construction, surfaced as
|
|
2372
2361
|
* `getStats().reconnectAttempts`. Incremented at the entry of every
|
|
2373
2362
|
* `_handleDisconnect` invocation that proceeds to schedule a backoff.
|
|
2374
2363
|
*/
|
|
2375
2364
|
private _reconnectAttempts;
|
|
2376
|
-
/** Epoch ms of the last successful ACK. Drives
|
|
2365
|
+
/** Epoch ms of the last successful ACK. Drives backoff-reset. */
|
|
2377
2366
|
private _lastSuccessAt;
|
|
2378
2367
|
/** Epoch ms when the current reconnect cycle began. `null` outside a cycle. */
|
|
2379
2368
|
private _reconnectCycleStartedAt;
|
|
@@ -2389,12 +2378,12 @@ declare class MllpClient extends EventEmitter {
|
|
|
2389
2378
|
private readonly _onBackpressure;
|
|
2390
2379
|
/** Pipeline flag; default `true` (parallel up to highWaterMark). */
|
|
2391
2380
|
private readonly _pipeline;
|
|
2392
|
-
/** Dead-peer idle timer
|
|
2381
|
+
/** Dead-peer idle timer. `null` when not armed. */
|
|
2393
2382
|
private _deadPeerTimer;
|
|
2394
2383
|
/** True once the self-'ack' listener that resets the dead-peer timer
|
|
2395
2384
|
* has been attached. Guards against duplicate listeners on reconnect. */
|
|
2396
2385
|
private _ackResetWired;
|
|
2397
|
-
/** Most recently bound `connect()` signal. Reread on every RetryContext build
|
|
2386
|
+
/** Most recently bound `connect()` signal. Reread on every RetryContext build. */
|
|
2398
2387
|
private _connectSignal;
|
|
2399
2388
|
/** Set when close()/destroy()/abort fires; reconnect handler short-circuits. */
|
|
2400
2389
|
private _userClosed;
|
|
@@ -2403,7 +2392,7 @@ declare class MllpClient extends EventEmitter {
|
|
|
2403
2392
|
/** Listener-removal handle for the abort listener bound in connect(). */
|
|
2404
2393
|
private _abortListener;
|
|
2405
2394
|
/**
|
|
2406
|
-
* Test-only seam
|
|
2395
|
+
* Test-only seam, when set, `_beginReconnectAttempt` builds the new
|
|
2407
2396
|
* Connection through this factory instead of opening a real net.Socket.
|
|
2408
2397
|
*
|
|
2409
2398
|
* @internal
|
|
@@ -2422,7 +2411,7 @@ declare class MllpClient extends EventEmitter {
|
|
|
2422
2411
|
/**
|
|
2423
2412
|
* Aggregated Correlator-emitted warning counts (MLLP_ACK_*). Connection-level
|
|
2424
2413
|
* warnings are read directly from `_connection.getStats().warningsByCode`
|
|
2425
|
-
* at observation time and merged into the snapshot
|
|
2414
|
+
* at observation time and merged into the snapshot.
|
|
2426
2415
|
*/
|
|
2427
2416
|
private _aggregatedWarningsByCode;
|
|
2428
2417
|
/**
|
|
@@ -2439,9 +2428,9 @@ declare class MllpClient extends EventEmitter {
|
|
|
2439
2428
|
*/
|
|
2440
2429
|
get state(): ConnectionState;
|
|
2441
2430
|
/**
|
|
2442
|
-
* Open a TCP (or TLS
|
|
2443
|
-
* and attach a
|
|
2444
|
-
* enters `CONNECTED
|
|
2431
|
+
* Open a TCP (or TLS) connection to the configured `host:port`
|
|
2432
|
+
* and attach a {@link Connection} to it. Resolves once the FSM
|
|
2433
|
+
* enters `CONNECTED`, for TLS, on `'secureConnect'` (handshake complete,
|
|
2445
2434
|
* including certificate verification when it is on).
|
|
2446
2435
|
*
|
|
2447
2436
|
* Rejects with:
|
|
@@ -2461,11 +2450,11 @@ declare class MllpClient extends EventEmitter {
|
|
|
2461
2450
|
*
|
|
2462
2451
|
* **TLS 1.3 + mutual TLS caveat (RFC 8446 §4.4.2):** `connect()` resolving
|
|
2463
2452
|
* does NOT guarantee that a `clientAuth: 'MUST'` server accepted your
|
|
2464
|
-
* client certificate. Under TLS 1.3 the client's handshake
|
|
2465
|
-
* `'secureConnect'
|
|
2453
|
+
* client certificate. Under TLS 1.3 the client's handshake, and its
|
|
2454
|
+
* `'secureConnect'`, can complete before the server finishes validating
|
|
2466
2455
|
* the certificate; a rejection then surfaces moments later as a typed
|
|
2467
2456
|
* post-connect error (`'error'` event with an `ERR_SSL_*`/alert cause,
|
|
2468
|
-
* classified **permanent
|
|
2457
|
+
* classified **permanent**, no auto-reconnect loop). ACK correlation via
|
|
2469
2458
|
* {@link MllpClient.send} remains the delivery guarantee: no send resolves
|
|
2470
2459
|
* without its ACK, so a rejected session can never silently "deliver".
|
|
2471
2460
|
*
|
|
@@ -2483,8 +2472,8 @@ declare class MllpClient extends EventEmitter {
|
|
|
2483
2472
|
* Build the raw socket + `Transport` pair for a connect / reconnect attempt.
|
|
2484
2473
|
*
|
|
2485
2474
|
* Plaintext (`ClientOptions.tls` unset): a `net.Socket` wrapped in
|
|
2486
|
-
* `NetTransport`. TLS
|
|
2487
|
-
* `TlsTransport
|
|
2475
|
+
* `NetTransport`. TLS: a `tls.TLSSocket` wrapped in
|
|
2476
|
+
* `TlsTransport`, verification defaults **on**
|
|
2488
2477
|
* (`rejectUnauthorized: !allowUnverified`), floor `minVersion: 'TLSv1.2'`
|
|
2489
2478
|
* (the IHE ATNA ITI-19 BCP195 floor), `servername` defaulting to
|
|
2490
2479
|
* `ClientOptions.host`.
|
|
@@ -2492,14 +2481,14 @@ declare class MllpClient extends EventEmitter {
|
|
|
2492
2481
|
private _createSocketAndTransport;
|
|
2493
2482
|
/**
|
|
2494
2483
|
* Wrap a connect-phase socket error as `MllpConnectionError`, classifying
|
|
2495
|
-
* TLS failures
|
|
2484
|
+
* TLS failures into the additive `connectionCause`:
|
|
2496
2485
|
*
|
|
2497
|
-
* - `'tls-verify'
|
|
2486
|
+
* - `'tls-verify'`, certificate-verification failures
|
|
2498
2487
|
* ({@link isTlsVerificationErrorCode}).
|
|
2499
|
-
* - `'tls-handshake'
|
|
2488
|
+
* - `'tls-handshake'`, TLS-**protocol**-shaped failures only
|
|
2500
2489
|
* ({@link isTlsProtocolError}): `ERR_SSL_*`, `EPROTO`, OpenSSL
|
|
2501
2490
|
* alert-bearing errors.
|
|
2502
|
-
* - **No `connectionCause
|
|
2491
|
+
* - **No `connectionCause`**, pure TCP-level failures (`ECONNREFUSED`,
|
|
2503
2492
|
* `ETIMEDOUT`, …) even on a TLS-configured connection; these carry the
|
|
2504
2493
|
* same shape as plaintext connect failures.
|
|
2505
2494
|
*/
|
|
@@ -2507,7 +2496,7 @@ declare class MllpClient extends EventEmitter {
|
|
|
2507
2496
|
/**
|
|
2508
2497
|
* Emit a client event, containing a throwing subscriber.
|
|
2509
2498
|
*
|
|
2510
|
-
* Every event this client emits is reached from a callback we do not own
|
|
2499
|
+
* Every event this client emits is reached from a callback we do not own, a socket's
|
|
2511
2500
|
* `'connect'`/`'secureConnect'`/`'data'`/`'error'` listener, or a backoff timer. A throwing
|
|
2512
2501
|
* subscriber would unwind into it and kill the process, and on several paths it would also skip
|
|
2513
2502
|
* the work queued after the emit (the `resolve()` of `connect()`, the backoff scheduling).
|
|
@@ -2516,8 +2505,8 @@ declare class MllpClient extends EventEmitter {
|
|
|
2516
2505
|
*/
|
|
2517
2506
|
private _emitContained;
|
|
2518
2507
|
/**
|
|
2519
|
-
* Emit the per-connection insecure-TLS warning
|
|
2520
|
-
* `tls.allowUnverified === true
|
|
2508
|
+
* Emit the per-connection insecure-TLS warning when
|
|
2509
|
+
* `tls.allowUnverified === true`, fires on EVERY successful
|
|
2521
2510
|
* `secureConnect`, initial connect and every reconnect. Emits both a frozen
|
|
2522
2511
|
* `'securityWarning'` event and `process.emitWarning`. No-op for plaintext
|
|
2523
2512
|
* connections or when verification is on.
|
|
@@ -2525,11 +2514,11 @@ declare class MllpClient extends EventEmitter {
|
|
|
2525
2514
|
private _emitInsecureWarningIfNeeded;
|
|
2526
2515
|
/**
|
|
2527
2516
|
* Wire a Connection's events through to this MllpClient. Every re-emitted
|
|
2528
|
-
* payload is `Object.freeze`'d before emission
|
|
2529
|
-
* Connection layer already freezes
|
|
2517
|
+
* payload is `Object.freeze`'d before emission, even though the
|
|
2518
|
+
* Connection layer already freezes, defense-in-depth, harmless on
|
|
2530
2519
|
* already-frozen objects.
|
|
2531
2520
|
*
|
|
2532
|
-
* Builds the unified `Correlator`
|
|
2521
|
+
* Builds the unified `Correlator` bound to this Connection and
|
|
2533
2522
|
* arms the periodic ACK-timeout sweep. The Correlator is teardown-aware:
|
|
2534
2523
|
* `close()` / `destroy()` clear the sweep timer and reject pending sends.
|
|
2535
2524
|
*
|
|
@@ -2537,37 +2526,36 @@ declare class MllpClient extends EventEmitter {
|
|
|
2537
2526
|
*/
|
|
2538
2527
|
private _attachConnection;
|
|
2539
2528
|
/**
|
|
2540
|
-
* Arm (or re-arm) the dead-peer idle timer
|
|
2529
|
+
* Arm (or re-arm) the dead-peer idle timer.
|
|
2541
2530
|
* No-op when `deadPeerTimeoutMs` is unset.
|
|
2542
2531
|
*/
|
|
2543
2532
|
private _armDeadPeerTimer;
|
|
2544
2533
|
/**
|
|
2545
|
-
* Clear the dead-peer idle timer
|
|
2546
|
-
* every transition out of CONNECTED).
|
|
2534
|
+
* Clear the dead-peer idle timer on every transition out of CONNECTED.
|
|
2547
2535
|
*/
|
|
2548
2536
|
private _clearDeadPeerTimer;
|
|
2549
2537
|
/**
|
|
2550
|
-
* Disconnect handler
|
|
2538
|
+
* Disconnect handler, reconnect FSM core.
|
|
2551
2539
|
*
|
|
2552
2540
|
* Implements:
|
|
2553
|
-
* -
|
|
2541
|
+
* - Hybrid in-flight handling: controlId mode preserves
|
|
2554
2542
|
* pending sends for resend; FIFO mode rejects in-flight with
|
|
2555
2543
|
* `connectionCause: 'in-flight-orphan'` and queued with `'fifo-unsafe'`.
|
|
2556
|
-
* -
|
|
2544
|
+
* - Classifier-first: permanent errors
|
|
2557
2545
|
* transition straight to CLOSED without invoking `retryStrategy`.
|
|
2558
|
-
* -
|
|
2546
|
+
* - Backoff-reset on recent success: first disconnect after a
|
|
2559
2547
|
* successful ACK on the prior session resets `_attempt` to 0.
|
|
2560
|
-
* -
|
|
2561
|
-
* -
|
|
2562
|
-
* -
|
|
2548
|
+
* - `_reconnectAttempts` counter increment.
|
|
2549
|
+
* - Frozen RetryContext; a `null` return from the strategy halts reconnection.
|
|
2550
|
+
* - Default exponential strategy.
|
|
2563
2551
|
*
|
|
2564
|
-
* Invoked from the SINGLE `_onStateChange` hook
|
|
2565
|
-
*
|
|
2552
|
+
* Invoked from the SINGLE `_onStateChange` hook, no parallel listener.
|
|
2553
|
+
* Idempotent across same-cycle re-entry: cycle-start flag
|
|
2566
2554
|
* coordinates first-disconnect vs subsequent-within-cycle behavior.
|
|
2567
2555
|
*/
|
|
2568
2556
|
private _handleDisconnect;
|
|
2569
2557
|
/**
|
|
2570
|
-
* Default retry strategy
|
|
2558
|
+
* Default retry strategy: `min(maxDelay, initialDelay * multiplier^attempt)`
|
|
2571
2559
|
* with ±jitter applied.
|
|
2572
2560
|
*/
|
|
2573
2561
|
private _defaultRetryStrategy;
|
|
@@ -2594,7 +2582,7 @@ declare class MllpClient extends EventEmitter {
|
|
|
2594
2582
|
*/
|
|
2595
2583
|
private _afterReconnectArmed;
|
|
2596
2584
|
/**
|
|
2597
|
-
* Test seam
|
|
2585
|
+
* Test seam, install a factory that produces the next reconnect Connection.
|
|
2598
2586
|
*
|
|
2599
2587
|
* @internal
|
|
2600
2588
|
*/
|
|
@@ -2603,7 +2591,7 @@ declare class MllpClient extends EventEmitter {
|
|
|
2603
2591
|
arm: () => void;
|
|
2604
2592
|
}): void;
|
|
2605
2593
|
/**
|
|
2606
|
-
* Test seam
|
|
2594
|
+
* Test seam, capture or rebind the connect-signal mid-flight.
|
|
2607
2595
|
*
|
|
2608
2596
|
* @internal
|
|
2609
2597
|
*/
|
|
@@ -2613,57 +2601,50 @@ declare class MllpClient extends EventEmitter {
|
|
|
2613
2601
|
*
|
|
2614
2602
|
* - FIFO mode: passes `null` controlId so `matchAck` returns the head of
|
|
2615
2603
|
* the live store.
|
|
2616
|
-
* - controlId mode: extracts MSA-2
|
|
2617
|
-
*
|
|
2604
|
+
* - controlId mode: extracts MSA-2 and passes it to `matchAck` for keyed
|
|
2605
|
+
* lookup.
|
|
2618
2606
|
*
|
|
2619
2607
|
* Called from the SINGLE `'message'` listener registered in `_attachConnection`.
|
|
2620
|
-
* No parallel listener is registered
|
|
2621
|
-
* anchor (B-04).
|
|
2608
|
+
* No parallel listener is registered.
|
|
2622
2609
|
*/
|
|
2623
2610
|
private _onAckPayload;
|
|
2624
2611
|
/**
|
|
2625
2612
|
* Single source-of-truth for a successfully matched ACK (live-store hit).
|
|
2626
2613
|
*
|
|
2627
|
-
*
|
|
2628
|
-
*
|
|
2629
|
-
* PLAN-06 extends at HOOK_EXTENSION_POINT: ack-matched to bump _ackedTotal and
|
|
2630
|
-
* set _lastAckAt.
|
|
2614
|
+
* Emits the frozen 'ack' event and resolves the pending send. Updates
|
|
2615
|
+
* `_lastSuccessAt`, `_ackedTotal` and `_lastAckAt`.
|
|
2631
2616
|
*
|
|
2632
2617
|
* Called from _onAckPayload when matchAck() returns a non-null PendingAck.
|
|
2633
2618
|
*/
|
|
2634
2619
|
private _onAckMatched;
|
|
2635
2620
|
/**
|
|
2636
2621
|
* Emit a frozen `'drain'` event when the queue depth and bytes fall below
|
|
2637
|
-
* both configured caps
|
|
2622
|
+
* both configured caps. Called from `_onAckMatched`
|
|
2638
2623
|
* (every successful ACK) and from the Correlator's `onTimeout` callback
|
|
2639
|
-
* (every expired send)
|
|
2624
|
+
* (every expired send), both code paths free a live-store slot.
|
|
2640
2625
|
*/
|
|
2641
2626
|
private _maybeEmitDrain;
|
|
2642
2627
|
/**
|
|
2643
2628
|
* Single source-of-truth for Connection FSM transitions.
|
|
2644
2629
|
*
|
|
2645
|
-
*
|
|
2646
|
-
*
|
|
2647
|
-
*
|
|
2648
|
-
* CONNECTED → DISCONNECTED|RECONNECTING and trigger _handleDisconnect.
|
|
2649
|
-
* Plan 05 extends at HOOK_EXTENSION_POINT: state-change to clear/arm
|
|
2650
|
-
* dead-peer timer on transitions out of / into CONNECTED.
|
|
2630
|
+
* Re-emits a frozen 'stateChange' event, detects
|
|
2631
|
+
* CONNECTED → DISCONNECTED|RECONNECTING to trigger `_handleDisconnect`, and
|
|
2632
|
+
* clears or arms the dead-peer timer on transitions out of / into CONNECTED.
|
|
2651
2633
|
*
|
|
2652
2634
|
* Called from the SINGLE 'stateChange' listener registered in _attachConnection.
|
|
2653
2635
|
*/
|
|
2654
2636
|
private _onStateChange;
|
|
2655
2637
|
/**
|
|
2656
|
-
* Send an MLLP-framed payload and await the inbound ACK
|
|
2638
|
+
* Send an MLLP-framed payload and await the inbound ACK.
|
|
2657
2639
|
*
|
|
2658
2640
|
* Resolves with the ACK Buffer (framing stripped). Rejects with:
|
|
2659
|
-
* - `DOMException('Aborted', 'AbortError')` if `signal` aborts before ACK
|
|
2660
|
-
*
|
|
2661
|
-
* - `MllpTimeoutError` if no ACK arrives within `ackTimeoutMs` (ERR-02).
|
|
2641
|
+
* - `DOMException('Aborted', 'AbortError')` if `signal` aborts before the ACK.
|
|
2642
|
+
* - `MllpTimeoutError` if no ACK arrives within `ackTimeoutMs`.
|
|
2662
2643
|
* The clock starts at the underlying `write()` flush callback, NOT at
|
|
2663
|
-
* the `send()` call
|
|
2644
|
+
* the `send()` call.
|
|
2664
2645
|
* - `MllpConnectionError({ phase: 'send' })` if the client is not connected.
|
|
2665
2646
|
*
|
|
2666
|
-
* Emits a frozen `'ack'` event on every successful match
|
|
2647
|
+
* Emits a frozen `'ack'` event on every successful match.
|
|
2667
2648
|
*
|
|
2668
2649
|
* @example
|
|
2669
2650
|
* ```typescript
|
|
@@ -2672,19 +2653,19 @@ declare class MllpClient extends EventEmitter {
|
|
|
2672
2653
|
* ```
|
|
2673
2654
|
*
|
|
2674
2655
|
* @param payload Raw bytes; MLLP framing is added internally via `encodeFrame`.
|
|
2675
|
-
* @param opts.signal AbortSignal
|
|
2656
|
+
* @param opts.signal AbortSignal, aborting cancels the ACK wait.
|
|
2676
2657
|
*/
|
|
2677
2658
|
send(payload: Buffer, opts?: {
|
|
2678
2659
|
signal?: AbortSignal;
|
|
2679
2660
|
ackTimeoutMs?: number;
|
|
2680
2661
|
}): Promise<Buffer>;
|
|
2681
2662
|
/**
|
|
2682
|
-
* 'wait'-mode backpressure handler
|
|
2663
|
+
* 'wait'-mode backpressure handler.
|
|
2683
2664
|
*
|
|
2684
2665
|
* Awaits one of three terminating signals, in order:
|
|
2685
2666
|
* - `'drain'` event → re-enter `send()` (the gate will now pass).
|
|
2686
2667
|
* - `ackTimeoutMs` elapses → reject with `MllpTimeoutError`.
|
|
2687
|
-
* - Caller's `signal` aborts → reject with `AbortError
|
|
2668
|
+
* - Caller's `signal` aborts → reject with `AbortError`. Cleanup
|
|
2688
2669
|
* removes the drain listener AND the abort listener AND clears the
|
|
2689
2670
|
* timer to prevent leaks.
|
|
2690
2671
|
*/
|
|
@@ -2695,7 +2676,7 @@ declare class MllpClient extends EventEmitter {
|
|
|
2695
2676
|
*/
|
|
2696
2677
|
private _teardownCorrelator;
|
|
2697
2678
|
/**
|
|
2698
|
-
* **Test seam
|
|
2679
|
+
* **Test seam**, attach an externally-built {@link Connection} directly,
|
|
2699
2680
|
* bypassing the `net.createConnection` + `NetTransport` path. Used by
|
|
2700
2681
|
* lifecycle tests driving `InMemoryTransport.pair()` for determinism.
|
|
2701
2682
|
*
|
|
@@ -2722,7 +2703,7 @@ declare class MllpClient extends EventEmitter {
|
|
|
2722
2703
|
signal?: AbortSignal;
|
|
2723
2704
|
}): Promise<void>;
|
|
2724
2705
|
/**
|
|
2725
|
-
* Abruptly destroy the client
|
|
2706
|
+
* Abruptly destroy the client, force-transitions the underlying Connection
|
|
2726
2707
|
* to `CLOSED` immediately. No-op if no Connection is attached. Idempotent.
|
|
2727
2708
|
*
|
|
2728
2709
|
* @example
|
|
@@ -2732,9 +2713,9 @@ declare class MllpClient extends EventEmitter {
|
|
|
2732
2713
|
*/
|
|
2733
2714
|
destroy(reason?: Error): void;
|
|
2734
2715
|
/**
|
|
2735
|
-
* Returns a JSON-serializable observability snapshot
|
|
2716
|
+
* Returns a JSON-serializable observability snapshot.
|
|
2736
2717
|
*
|
|
2737
|
-
* All fields are plain values
|
|
2718
|
+
* All fields are plain values, no Buffers, no class instances, no Maps,
|
|
2738
2719
|
* no circular refs. Safe to `JSON.stringify` directly.
|
|
2739
2720
|
*
|
|
2740
2721
|
* `inFlight` is the count of correlator entries with `sentAt !== null`
|
|
@@ -2749,7 +2730,7 @@ declare class MllpClient extends EventEmitter {
|
|
|
2749
2730
|
*/
|
|
2750
2731
|
getStats(): ClientStats;
|
|
2751
2732
|
/**
|
|
2752
|
-
* Async disposal
|
|
2733
|
+
* Async disposal, delegates to {@link MllpClient.close} for `await using` support.
|
|
2753
2734
|
*
|
|
2754
2735
|
* @example
|
|
2755
2736
|
* ```typescript
|
|
@@ -2769,15 +2750,15 @@ declare class MllpClient extends EventEmitter {
|
|
|
2769
2750
|
*
|
|
2770
2751
|
* const client = createClient({ host: 'localhost', port: 2575 });
|
|
2771
2752
|
* await client.connect();
|
|
2772
|
-
*
|
|
2753
|
+
* const ack = await client.send(payloadBuffer);
|
|
2773
2754
|
* await client.close();
|
|
2774
2755
|
* ```
|
|
2775
2756
|
*/
|
|
2776
2757
|
declare function createClient(opts: ClientOptions): MllpClient;
|
|
2777
2758
|
/**
|
|
2778
|
-
* Options for {@link createStarterClient}
|
|
2759
|
+
* Options for {@link createStarterClient}.
|
|
2779
2760
|
*
|
|
2780
|
-
* The starter applies opinionated
|
|
2761
|
+
* The starter applies opinionated defaults on top of `ClientOptions`,
|
|
2781
2762
|
* so every override here is optional except `host` + `port`. The
|
|
2782
2763
|
* starter-specific addition is `handleSignals` (mirrors `createStarterServer`).
|
|
2783
2764
|
*
|
|
@@ -2814,41 +2795,40 @@ interface StarterClientOptions {
|
|
|
2814
2795
|
readonly onBackpressure?: "reject" | "wait";
|
|
2815
2796
|
/** Override default `true` (auto-reconnect on transient errors). */
|
|
2816
2797
|
readonly autoReconnect?: boolean;
|
|
2817
|
-
/** Custom reconnect-backoff hook
|
|
2798
|
+
/** Custom reconnect-backoff hook. */
|
|
2818
2799
|
readonly retryStrategy?: RetryStrategy;
|
|
2819
2800
|
/** Drain timeout for `close()` (default `30_000`). */
|
|
2820
2801
|
readonly drainTimeoutMs?: number;
|
|
2821
2802
|
/** FrameReader options (passthrough). */
|
|
2822
2803
|
readonly framing?: ClientOptions["framing"];
|
|
2823
|
-
/** TCP keepalive interval ms
|
|
2804
|
+
/** TCP keepalive interval ms. */
|
|
2824
2805
|
readonly keepaliveIntervalMs?: number;
|
|
2825
|
-
/** Application-idle dead-peer timeout ms
|
|
2806
|
+
/** Application-idle dead-peer timeout ms. */
|
|
2826
2807
|
readonly deadPeerTimeoutMs?: number;
|
|
2827
|
-
/** Enable TLS (MLLPS) for this connection
|
|
2808
|
+
/** Enable TLS (MLLPS) for this connection. Passthrough to `ClientOptions.tls`. */
|
|
2828
2809
|
readonly tls?: TlsOptions | true;
|
|
2829
2810
|
/**
|
|
2830
2811
|
* Register process SIGTERM/SIGINT handlers that close the client. Default
|
|
2831
|
-
* `false
|
|
2832
|
-
* and exit the process. Handlers self-deregister on `'close'
|
|
2812
|
+
* `false`. When `true`, SIGTERM/SIGINT both call `client.close()`
|
|
2813
|
+
* and exit the process. Handlers self-deregister on `'close'`.
|
|
2833
2814
|
*/
|
|
2834
2815
|
readonly handleSignals?: boolean;
|
|
2835
2816
|
}
|
|
2836
2817
|
/**
|
|
2837
|
-
* Three-line MLLP client with batteries-included defaults
|
|
2838
|
-
*
|
|
2839
|
-
* has been awaited.
|
|
2818
|
+
* Three-line MLLP client with batteries-included defaults. The returned
|
|
2819
|
+
* client is already CONNECTED, `connect()` has been awaited.
|
|
2840
2820
|
*
|
|
2841
|
-
*
|
|
2821
|
+
* Defaults:
|
|
2842
2822
|
* - `autoReconnect: true`
|
|
2843
2823
|
* - `ackTimeoutMs: 30_000`
|
|
2844
|
-
* - `correlateByControlId: false` (FIFO mode
|
|
2824
|
+
* - `correlateByControlId: false` (FIFO mode, simplest mental model)
|
|
2845
2825
|
* - `pipeline: true`
|
|
2846
2826
|
* - `highWaterMark: 64`
|
|
2847
2827
|
* - `onBackpressure: 'reject'`
|
|
2848
2828
|
* - `handleSignals: false` (opt-in)
|
|
2849
2829
|
*
|
|
2850
2830
|
* The factory is **async**, so the literal three-line north-star snippet
|
|
2851
|
-
* has an explicit `await` BEFORE `createStarterClient(...)
|
|
2831
|
+
* has an explicit `await` BEFORE `createStarterClient(...)`, without it,
|
|
2852
2832
|
* the `using` declaration would receive a `Promise`, not an `MllpClient`,
|
|
2853
2833
|
* and `Symbol.asyncDispose` would not run at scope exit.
|
|
2854
2834
|
*
|
|
@@ -2865,9 +2845,9 @@ declare function createStarterClient(opts: StarterClientOptions): Promise<MllpCl
|
|
|
2865
2845
|
* MLLP Client typed errors and classifiers.
|
|
2866
2846
|
*
|
|
2867
2847
|
* Exports:
|
|
2868
|
-
* - `MllpTimeoutError
|
|
2869
|
-
* - `MllpBackpressureError
|
|
2870
|
-
* - `isTransientConnectionError
|
|
2848
|
+
* - `MllpTimeoutError`, ACK timeout
|
|
2849
|
+
* - `MllpBackpressureError`, high-water mark exceeded
|
|
2850
|
+
* - `isTransientConnectionError`, transient/permanent classifier
|
|
2871
2851
|
*
|
|
2872
2852
|
* Re-exported from `src/client/index.ts` and `src/index.ts`.
|
|
2873
2853
|
*
|
|
@@ -2875,11 +2855,10 @@ declare function createStarterClient(opts: StarterClientOptions): Promise<MllpCl
|
|
|
2875
2855
|
*/
|
|
2876
2856
|
/**
|
|
2877
2857
|
* Thrown (or rejects the `send()` promise) when an ACK does not arrive within
|
|
2878
|
-
* the configured `ackTimeoutMs
|
|
2858
|
+
* the configured `ackTimeoutMs`.
|
|
2879
2859
|
*
|
|
2880
2860
|
* The timeout clock starts at the underlying `write()` flush callback, NOT
|
|
2881
|
-
* at the `send()` call
|
|
2882
|
-
* (CLIENT-04).
|
|
2861
|
+
* at the `send()` call, pre-flush queue time is not charged to the peer.
|
|
2883
2862
|
*
|
|
2884
2863
|
* @example
|
|
2885
2864
|
* ```typescript
|
|
@@ -2914,10 +2893,10 @@ declare class MllpTimeoutError extends Error {
|
|
|
2914
2893
|
}
|
|
2915
2894
|
/**
|
|
2916
2895
|
* Set of Node/OpenSSL error codes that indicate a **certificate-verification**
|
|
2917
|
-
* failure (as opposed to some other TLS handshake failure)
|
|
2896
|
+
* failure (as opposed to some other TLS handshake failure), untrusted chain,
|
|
2918
2897
|
* expired/not-yet-valid certificate, hostname mismatch, revocation, etc.
|
|
2919
2898
|
*
|
|
2920
|
-
* Used by `MllpClient`
|
|
2899
|
+
* Used by `MllpClient` to classify a TLS connect failure's
|
|
2921
2900
|
* `connectionCause` as `'tls-verify'` (this set) vs `'tls-handshake'`
|
|
2922
2901
|
* (everything else observed before `'secureConnect'`). Exported so callers
|
|
2923
2902
|
* can apply the same classification to their own error handling.
|
|
@@ -2932,8 +2911,8 @@ declare class MllpTimeoutError extends Error {
|
|
|
2932
2911
|
*/
|
|
2933
2912
|
declare function isTlsVerificationErrorCode(code: string): boolean;
|
|
2934
2913
|
/**
|
|
2935
|
-
* Detects **TLS-protocol-shaped** errors
|
|
2936
|
-
* itself, as opposed to plain TCP-level network failures
|
|
2914
|
+
* Detects **TLS-protocol-shaped** errors, failures of the TLS protocol
|
|
2915
|
+
* itself, as opposed to plain TCP-level network failures.
|
|
2937
2916
|
*
|
|
2938
2917
|
* Apply this only to errors raised on a **TLS** connection; `MllpClient`
|
|
2939
2918
|
* does exactly that (the predicate is consulted only when `ClientOptions.tls`
|
|
@@ -2941,12 +2920,12 @@ declare function isTlsVerificationErrorCode(code: string): boolean;
|
|
|
2941
2920
|
*
|
|
2942
2921
|
* **TLS-protocol-shaped (`true`):**
|
|
2943
2922
|
* - `code` starting `ERR_SSL_` (Node's TLS alert codes, e.g.
|
|
2944
|
-
* `ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED
|
|
2923
|
+
* `ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED`, a `clientAuth: 'MUST'`
|
|
2945
2924
|
* server rejecting the client's certificate).
|
|
2946
|
-
* - `code === 'EPROTO'
|
|
2925
|
+
* - `code === 'EPROTO'`, on a TLS connection this is OpenSSL failing the
|
|
2947
2926
|
* handshake (protocol version mismatch, no shared cipher, a TLS ≤1.2
|
|
2948
2927
|
* mTLS rejection).
|
|
2949
|
-
* - `message` containing `ssl` or `alert` (`/\bssl\b|\balert\b/i
|
|
2928
|
+
* - `message` containing `ssl` or `alert` (`/\bssl\b|\balert\b/i`, "SSL
|
|
2950
2929
|
* routines", "tlsv13 alert certificate required", …). This message check
|
|
2951
2930
|
* is a **heuristic backstop** over the code-based checks above, not a
|
|
2952
2931
|
* precise boundary: it exists to catch OpenSSL errors that surface without
|
|
@@ -2954,19 +2933,19 @@ declare function isTlsVerificationErrorCode(code: string): boolean;
|
|
|
2954
2933
|
* TLS is configured. An arbitrary non-TLS error whose message happens to
|
|
2955
2934
|
* contain those words would also match.
|
|
2956
2935
|
*
|
|
2957
|
-
* **NOT TLS-protocol-shaped (`false`)
|
|
2936
|
+
* **NOT TLS-protocol-shaped (`false`), plain network failures, which stay
|
|
2958
2937
|
* transient for the reconnect classifier:** `ECONNREFUSED`, `ETIMEDOUT`,
|
|
2959
2938
|
* `EHOSTUNREACH`, `ENETUNREACH`, `EPIPE`, and a plain `ECONNRESET` carrying
|
|
2960
|
-
* no TLS alert context
|
|
2939
|
+
* no TLS alert context, a network blip during (or after) a handshake
|
|
2961
2940
|
* should still auto-heal.
|
|
2962
2941
|
*
|
|
2963
|
-
* Certificate-**verification** failures are a separate class
|
|
2942
|
+
* Certificate-**verification** failures are a separate class, see
|
|
2964
2943
|
* {@link isTlsVerificationErrorCode}; `MllpClient` checks that first and
|
|
2965
2944
|
* labels those `connectionCause: 'tls-verify'`.
|
|
2966
2945
|
*
|
|
2967
2946
|
* Why this matters: under TLS 1.3 (RFC 8446 §4.4.2) a `clientAuth: 'MUST'`
|
|
2968
2947
|
* server can reject the client's certificate AFTER the client's own
|
|
2969
|
-
* `'secureConnect'
|
|
2948
|
+
* `'secureConnect'`, the rejection then surfaces as a post-connect socket
|
|
2970
2949
|
* error. A misconfigured mTLS client must never auto-reconnect-loop against
|
|
2971
2950
|
* a server that will always reject it, so `MllpClient` classifies
|
|
2972
2951
|
* TLS-protocol-shaped errors as **permanent**.
|
|
@@ -2976,7 +2955,7 @@ declare function isTlsVerificationErrorCode(code: string): boolean;
|
|
|
2976
2955
|
* import { isTlsProtocolError, MllpConnectionError } from '@cosyte/mllp';
|
|
2977
2956
|
* client.on('error', ({ error }) => {
|
|
2978
2957
|
* if (error instanceof MllpConnectionError && isTlsProtocolError(error.cause)) {
|
|
2979
|
-
* // TLS protocol failure
|
|
2958
|
+
* // TLS protocol failure, a configuration problem, not a network blip.
|
|
2980
2959
|
* }
|
|
2981
2960
|
* });
|
|
2982
2961
|
* ```
|
|
@@ -2986,32 +2965,32 @@ declare function isTlsProtocolError(err: unknown): boolean;
|
|
|
2986
2965
|
* Classifies a connection error as transient (eligible for auto-reconnect)
|
|
2987
2966
|
* or permanent (halts auto-reconnect, transitions to CLOSED).
|
|
2988
2967
|
*
|
|
2989
|
-
* Used internally by `MllpClient` BEFORE invoking `retryStrategy`
|
|
2990
|
-
*
|
|
2968
|
+
* Used internally by `MllpClient` BEFORE invoking `retryStrategy`
|
|
2969
|
+
* (see `RetryContext.classifiedAs`). Re-exported so callers can implement
|
|
2991
2970
|
* their own retry policies.
|
|
2992
2971
|
*
|
|
2993
|
-
* Classification table
|
|
2972
|
+
* Classification table:
|
|
2994
2973
|
* - `ENOTFOUND`, `EACCES` → **permanent** (`false`)
|
|
2995
2974
|
* - `ECONNREFUSED`, `ECONNRESET`, `ETIMEDOUT`, `EHOSTUNREACH`,
|
|
2996
2975
|
* `ENETUNREACH`, `EPIPE` → **transient** (`true`)
|
|
2997
2976
|
* - `CERT_*` and `UNABLE_TO_VERIFY_LEAF_SIGNATURE` /
|
|
2998
2977
|
* `DEPTH_ZERO_SELF_SIGNED_CERT` / `SELF_SIGNED_CERT_IN_CHAIN`
|
|
2999
|
-
* (any {@link isTlsVerificationErrorCode} code) → **permanent** (`false`)
|
|
3000
|
-
* never auto-reconnect-loop into a misconfigured or MITM'd endpoint
|
|
3001
|
-
* - `ERR_SSL_*` (Node TLS alert codes) → **permanent** (`false`)
|
|
2978
|
+
* (any {@link isTlsVerificationErrorCode} code) → **permanent** (`false`),
|
|
2979
|
+
* never auto-reconnect-loop into a misconfigured or MITM'd endpoint.
|
|
2980
|
+
* - `ERR_SSL_*` (Node TLS alert codes) → **permanent** (`false`), a TLS
|
|
3002
2981
|
* protocol failure such as a `clientAuth: 'MUST'` server rejecting the
|
|
3003
|
-
* client certificate recurs on every attempt
|
|
2982
|
+
* client certificate recurs on every attempt. On TLS-configured
|
|
3004
2983
|
* connections `MllpClient` additionally consults {@link isTlsProtocolError},
|
|
3005
2984
|
* which also catches `EPROTO`/alert-bearing OpenSSL errors that this
|
|
3006
2985
|
* generic classifier (which cannot know the connection was TLS) leaves
|
|
3007
2986
|
* transient.
|
|
3008
|
-
* - `MLLP_*` (any {@link MllpFramingError} code
|
|
3009
|
-
* `connectionCause: 'framing-fatal'`) → **permanent** (`false`)
|
|
3010
|
-
* speaking MLLP
|
|
2987
|
+
* - `MLLP_*` (any {@link MllpFramingError} code, a fatal decoder throw, surfaced with
|
|
2988
|
+
* `connectionCause: 'framing-fatal'`) → **permanent** (`false`). The peer is not
|
|
2989
|
+
* speaking MLLP, an HTTP probe, a health check, a wrong-port misconfiguration, or is emitting
|
|
3011
2990
|
* frames past `maxFrameSizeBytes`. Every reconnect meets the same bytes, so retrying is an
|
|
3012
2991
|
* unbounded storm against a peer that is already misconfigured. If a peer's quirk is *expected*,
|
|
3013
|
-
* the decoder's tolerance opt-ins are the supported answer
|
|
3014
|
-
* - non-Error / unknown / no-code → **transient** (`true`)
|
|
2992
|
+
* the decoder's tolerance opt-ins are the supported answer, they make it a warning, not a fatal.
|
|
2993
|
+
* - non-Error / unknown / no-code → **transient** (`true`), Postel's Law
|
|
3015
2994
|
* default. Reconnect attempts are bounded by `retryStrategy` and the
|
|
3016
2995
|
* 30s backoff cap, so the default is safe.
|
|
3017
2996
|
*
|
|
@@ -3030,11 +3009,10 @@ declare function isTlsProtocolError(err: unknown): boolean;
|
|
|
3030
3009
|
declare function isTransientConnectionError(err: unknown): boolean;
|
|
3031
3010
|
/**
|
|
3032
3011
|
* Thrown (or rejects the `send()` promise) when the in-flight queue exceeds
|
|
3033
|
-
* the configured high-water mark and `onBackpressure: 'reject'` is set
|
|
3034
|
-
* (CLIENT-07, ERR-04).
|
|
3012
|
+
* the configured high-water mark and `onBackpressure: 'reject'` is set.
|
|
3035
3013
|
*
|
|
3036
|
-
* `highWaterMark` accepts a count cap, a byte cap, or both
|
|
3037
|
-
* present, the stricter-of-two trigger wins
|
|
3014
|
+
* `highWaterMark` accepts a count cap, a byte cap, or both, when both are
|
|
3015
|
+
* present, the stricter-of-two trigger wins.
|
|
3038
3016
|
*
|
|
3039
3017
|
* @example
|
|
3040
3018
|
* ```typescript
|
|
@@ -3057,7 +3035,7 @@ declare class MllpBackpressureError extends Error {
|
|
|
3057
3035
|
readonly queueDepth: number;
|
|
3058
3036
|
/** Total bytes of in-flight + queued frames at the moment of rejection. */
|
|
3059
3037
|
readonly queueBytes: number;
|
|
3060
|
-
/** The high-water-mark configuration that was triggered
|
|
3038
|
+
/** The high-water-mark configuration that was triggered. */
|
|
3061
3039
|
readonly highWaterMark: {
|
|
3062
3040
|
readonly count?: number;
|
|
3063
3041
|
readonly bytes?: number;
|
|
@@ -3079,7 +3057,7 @@ declare class MllpBackpressureError extends Error {
|
|
|
3079
3057
|
}
|
|
3080
3058
|
|
|
3081
3059
|
/**
|
|
3082
|
-
* `@cosyte/mllp
|
|
3060
|
+
* `@cosyte/mllp`, Production-grade MLLP client and server for Node.js.
|
|
3083
3061
|
*
|
|
3084
3062
|
* Transport-only sibling to `@cosyte/hl7`. Handles framing, ACKs, reconnects,
|
|
3085
3063
|
* backpressure, and TLS without requiring knowledge of the MLLP spec.
|
|
@@ -3096,7 +3074,7 @@ declare class MllpBackpressureError extends Error {
|
|
|
3096
3074
|
* Package version marker exported from the `@cosyte/mllp` root.
|
|
3097
3075
|
*
|
|
3098
3076
|
* Kept in lockstep with `package.json` by `scripts/sync-version.mjs`, which the `version` script
|
|
3099
|
-
* runs immediately after `changeset version`. The `: string` annotation is deliberate
|
|
3077
|
+
* runs immediately after `changeset version`. The `: string` annotation is deliberate, without it
|
|
3100
3078
|
* TypeScript infers the *literal* type (`declare const VERSION = "0.0.0"`), which leaks the current
|
|
3101
3079
|
* release into consumers' types and makes an equality check against any other version a compile
|
|
3102
3080
|
* error.
|