@glion/mllp 0.15.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -38
- package/dist/mllp.js +2 -418
- package/dist/mllp.js.map +1 -1
- package/dist/node/serve.d.ts.map +1 -1
- package/dist/node.js +13 -1
- package/dist/node.js.map +1 -1
- package/dist/server/mllp.d.ts.map +1 -1
- package/package.json +15 -15
- package/dist/errors.d.ts +0 -22
- package/dist/errors.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/transport/constants.d.ts +0 -24
- package/dist/transport/constants.d.ts.map +0 -1
- package/dist/transport/decoder-stream.d.ts +0 -49
- package/dist/transport/decoder-stream.d.ts.map +0 -1
- package/dist/transport/decoder.d.ts +0 -18
- package/dist/transport/decoder.d.ts.map +0 -1
- package/dist/transport/dynamic-buffer.d.ts +0 -28
- package/dist/transport/dynamic-buffer.d.ts.map +0 -1
- package/dist/transport/encoder.d.ts +0 -14
- package/dist/transport/encoder.d.ts.map +0 -1
- package/dist/transport/errors.d.ts +0 -13
- package/dist/transport/errors.d.ts.map +0 -1
- package/dist/transport/index.d.ts +0 -8
- package/dist/transport/index.d.ts.map +0 -1
- package/dist/transport/types.d.ts +0 -49
- package/dist/transport/types.d.ts.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
export { createDecoderStream, decode, encode, encodeMultiple, TransportError, ErrorCode, MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_HEADER, MLLP_START_BYTE, MLLP_TRAILER, MLLPDecoderStream, } from "
|
|
2
|
-
export type { DecodedMessage, DecoderOptions, EncoderInput, EncoderOptions, } from "
|
|
3
|
-
export { MllpError } from "./errors";
|
|
4
|
-
export type { MllpErrorOptions } from "./errors";
|
|
1
|
+
export { createDecoderStream, decode, encode, encodeMultiple, TransportError, ErrorCode, MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_HEADER, MLLP_START_BYTE, MLLP_TRAILER, MLLPDecoderStream, MllpError, } from "@glion/mllp-transport";
|
|
2
|
+
export type { DecodedMessage, DecoderOptions, EncoderInput, EncoderOptions, MllpErrorOptions, } from "@glion/mllp-transport";
|
|
5
3
|
export { getMessageInfo, Mllp } from "./server/mllp";
|
|
6
4
|
export type { MessageInfo } from "./server/mllp";
|
|
7
5
|
export type { ConnectionInfo, Context, ErrorHandler, Handler, Hl7v2Processor, Middleware, MiddlewareReturn, Response, RouteFilter, RoutePattern, } from "./server/types";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,mBAAmB,EACnB,MAAM,EACN,MAAM,EACN,cAAc,EACd,cAAc,EACd,SAAS,EACT,eAAe,EACf,eAAe,EACf,WAAW,EACX,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,SAAS,GACV,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,cAAc,EACd,cAAc,EACd,YAAY,EACZ,cAAc,EACd,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAK/B,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrD,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,YAAY,EACV,cAAc,EACd,OAAO,EACP,YAAY,EACZ,OAAO,EACP,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,38 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Decode a single MLLP frame (VT ... FS CR) into a message.
|
|
5
|
-
*
|
|
6
|
-
* Validates framing, strips the envelope, and returns the payload as
|
|
7
|
-
* both raw bytes and decoded text. Throws {@link TransportError} on
|
|
8
|
-
* any structural issue.
|
|
9
|
-
*
|
|
10
|
-
* For streaming / chunked TCP data, use {@link createDecoderStream} instead.
|
|
11
|
-
*
|
|
12
|
-
* @throws TransportError with `INVALID_START_BYTE` if byte 0 is not 0x0B
|
|
13
|
-
* @throws TransportError with `INVALID_END_SEQUENCE` if frame doesn't end with
|
|
14
|
-
* 0x1C 0x0D
|
|
15
|
-
* @throws TransportError with `MESSAGE_TOO_LARGE` if payload exceeds
|
|
16
|
-
* `maxMessageSize`
|
|
17
|
-
*/
|
|
18
|
-
function decode(frame, options) {
|
|
19
|
-
if (frame.length < 3) throw new TransportError(TransportErrorCode.INVALID_END_SEQUENCE, "Frame too short to contain valid MLLP structure", 0);
|
|
20
|
-
const startByte = frame[0];
|
|
21
|
-
if (startByte !== 11) throw new TransportError(TransportErrorCode.INVALID_START_BYTE, `Invalid start byte: expected 0x0B, got 0x${(startByte ?? 0).toString(16).padStart(2, "0")}`, 0);
|
|
22
|
-
const len = frame.length;
|
|
23
|
-
const endByte1 = frame[len - 2];
|
|
24
|
-
const endByte2 = frame[len - 1];
|
|
25
|
-
if (endByte1 !== 28 || endByte2 !== 13) throw new TransportError(TransportErrorCode.INVALID_END_SEQUENCE, `Invalid end sequence: expected 0x1C 0x0D, got 0x${(endByte1 ?? 0).toString(16).padStart(2, "0")} 0x${(endByte2 ?? 0).toString(16).padStart(2, "0")}`, len - 2);
|
|
26
|
-
const messageLength = len - 3;
|
|
27
|
-
if (options?.maxMessageSize !== void 0 && messageLength > options.maxMessageSize) throw new TransportError(TransportErrorCode.MESSAGE_TOO_LARGE, `Message size ${messageLength} exceeds maximum ${options.maxMessageSize}`, 0);
|
|
28
|
-
const data = frame.subarray(1, len - 2);
|
|
29
|
-
return {
|
|
30
|
-
byteLength: messageLength,
|
|
31
|
-
data,
|
|
32
|
-
text: new TextDecoder(options?.encoding ?? "utf8").decode(data)
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
//#endregion
|
|
36
|
-
export { TransportErrorCode as ErrorCode, MLLPDecoderStream, MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_HEADER, MLLP_START_BYTE, MLLP_TRAILER, Mllp, MllpError, TransportError, createDecoderStream, decode, encode, encodeMultiple, getMessageInfo, matchPattern, parsePattern };
|
|
37
|
-
|
|
38
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import { i as parsePattern, n as getMessageInfo, r as matchPattern, t as Mllp } from "./mllp.js";
|
|
2
|
+
import { ErrorCode, MLLPDecoderStream, MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_HEADER, MLLP_START_BYTE, MLLP_TRAILER, MllpError, TransportError, createDecoderStream, decode, encode, encodeMultiple } from "@glion/mllp-transport";
|
|
3
|
+
export { ErrorCode, MLLPDecoderStream, MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_HEADER, MLLP_START_BYTE, MLLP_TRAILER, Mllp, MllpError, TransportError, createDecoderStream, decode, encode, encodeMultiple, getMessageInfo, matchPattern, parsePattern };
|
package/dist/mllp.js
CHANGED
|
@@ -1,422 +1,6 @@
|
|
|
1
|
+
import { MllpError } from "@glion/mllp-transport";
|
|
1
2
|
import { value } from "@glion/util-query";
|
|
2
3
|
import { VFile } from "vfile";
|
|
3
|
-
//#region src/transport/constants.ts
|
|
4
|
-
/**
|
|
5
|
-
* MLLP Start Byte - Vertical Tab (VT)
|
|
6
|
-
* Marks the beginning of an MLLP frame
|
|
7
|
-
*/
|
|
8
|
-
const MLLP_START_BYTE = 11;
|
|
9
|
-
/**
|
|
10
|
-
* MLLP End Byte 1 - File Separator (FS)
|
|
11
|
-
* First byte of the end sequence
|
|
12
|
-
*/
|
|
13
|
-
const MLLP_END_BYTE_1 = 28;
|
|
14
|
-
/**
|
|
15
|
-
* MLLP End Byte 2 - Carriage Return (CR)
|
|
16
|
-
* Second byte of the end sequence
|
|
17
|
-
*/
|
|
18
|
-
const MLLP_END_BYTE_2 = 13;
|
|
19
|
-
/**
|
|
20
|
-
* MLLP header as a Uint8Array containing the start byte
|
|
21
|
-
*/
|
|
22
|
-
const MLLP_HEADER = new Uint8Array([11]);
|
|
23
|
-
/**
|
|
24
|
-
* MLLP trailer as a Uint8Array containing the end sequence
|
|
25
|
-
*/
|
|
26
|
-
const MLLP_TRAILER = new Uint8Array([28, 13]);
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region src/errors.ts
|
|
29
|
-
/**
|
|
30
|
-
* Base error class for all MLLP errors.
|
|
31
|
-
*
|
|
32
|
-
* All error types in the MLLP package extend this class.
|
|
33
|
-
* Provides a common `code` field so that any MLLP error can be
|
|
34
|
-
* programmatically identified and translated (e.g., into ACK/NAK responses).
|
|
35
|
-
*/
|
|
36
|
-
var MllpError = class extends Error {
|
|
37
|
-
/** Machine-readable error code identifying the error type */
|
|
38
|
-
code;
|
|
39
|
-
/** Error condition string (e.g., for ERR segment in ACK) */
|
|
40
|
-
errorCondition;
|
|
41
|
-
constructor(code, message, options) {
|
|
42
|
-
super(message, options);
|
|
43
|
-
this.name = "MllpError";
|
|
44
|
-
this.code = code;
|
|
45
|
-
this.errorCondition = options?.errorCondition;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
//#endregion
|
|
49
|
-
//#region src/transport/errors.ts
|
|
50
|
-
/**
|
|
51
|
-
* Transport-level error for MLLP framing issues (invalid framing, incomplete
|
|
52
|
-
* messages, etc.)
|
|
53
|
-
*/
|
|
54
|
-
var TransportError = class extends MllpError {
|
|
55
|
-
/** Position in the data where the error occurred (if applicable) */
|
|
56
|
-
position;
|
|
57
|
-
constructor(code, message, position) {
|
|
58
|
-
super(code, message);
|
|
59
|
-
this.name = "TransportError";
|
|
60
|
-
this.position = position;
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
//#endregion
|
|
64
|
-
//#region src/transport/types.ts
|
|
65
|
-
/**
|
|
66
|
-
* Error codes for MLLP frame-level errors
|
|
67
|
-
*/
|
|
68
|
-
const TransportErrorCode = {
|
|
69
|
-
INCOMPLETE_MESSAGE: "INCOMPLETE_MESSAGE",
|
|
70
|
-
INVALID_END_SEQUENCE: "INVALID_END_SEQUENCE",
|
|
71
|
-
INVALID_START_BYTE: "INVALID_START_BYTE",
|
|
72
|
-
MESSAGE_TOO_LARGE: "MESSAGE_TOO_LARGE"
|
|
73
|
-
};
|
|
74
|
-
//#endregion
|
|
75
|
-
//#region src/transport/dynamic-buffer.ts
|
|
76
|
-
/**
|
|
77
|
-
* A growable byte buffer with geometric capacity doubling.
|
|
78
|
-
*
|
|
79
|
-
* Designed for protocol decoders that accumulate bytes from arbitrarily
|
|
80
|
-
* chunked TCP streams. Amortizes copy cost to O(n) total — the same
|
|
81
|
-
* strategy used by pg-protocol, NATS.js, and Java's ArrayList.
|
|
82
|
-
*
|
|
83
|
-
* The buffer maintains a read offset so consumed bytes can be skipped
|
|
84
|
-
* without shifting data. When the wasted prefix exceeds half the
|
|
85
|
-
* capacity, the buffer auto-compacts by shifting remaining bytes to
|
|
86
|
-
* the front.
|
|
87
|
-
*
|
|
88
|
-
* @internal
|
|
89
|
-
*/
|
|
90
|
-
var DynamicBuffer = class {
|
|
91
|
-
_buffer;
|
|
92
|
-
_length = 0;
|
|
93
|
-
_offset = 0;
|
|
94
|
-
constructor(initialCapacity = 4096) {
|
|
95
|
-
this._buffer = new Uint8Array(initialCapacity);
|
|
96
|
-
}
|
|
97
|
-
get capacity() {
|
|
98
|
-
return this._buffer.length;
|
|
99
|
-
}
|
|
100
|
-
get available() {
|
|
101
|
-
return this._length - this._offset;
|
|
102
|
-
}
|
|
103
|
-
append(chunk) {
|
|
104
|
-
const needed = this._length + chunk.length;
|
|
105
|
-
if (needed > this._buffer.length) {
|
|
106
|
-
const newCapacity = Math.max(this._buffer.length * 2, needed);
|
|
107
|
-
const newBuffer = new Uint8Array(newCapacity);
|
|
108
|
-
newBuffer.set(this._buffer.subarray(0, this._length));
|
|
109
|
-
this._buffer = newBuffer;
|
|
110
|
-
}
|
|
111
|
-
this._buffer.set(chunk, this._length);
|
|
112
|
-
this._length += chunk.length;
|
|
113
|
-
}
|
|
114
|
-
view() {
|
|
115
|
-
return this._buffer.subarray(this._offset, this._length);
|
|
116
|
-
}
|
|
117
|
-
consume(n) {
|
|
118
|
-
this._offset += n;
|
|
119
|
-
if (this._offset >= this._length) {
|
|
120
|
-
this._offset = 0;
|
|
121
|
-
this._length = 0;
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (this._offset > this._buffer.length / 2) this._compact();
|
|
125
|
-
}
|
|
126
|
-
reset() {
|
|
127
|
-
this._offset = 0;
|
|
128
|
-
this._length = 0;
|
|
129
|
-
}
|
|
130
|
-
_compact() {
|
|
131
|
-
const remaining = this._length - this._offset;
|
|
132
|
-
this._buffer.copyWithin(0, this._offset, this._length);
|
|
133
|
-
this._offset = 0;
|
|
134
|
-
this._length = remaining;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
//#endregion
|
|
138
|
-
//#region src/transport/decoder-stream.ts
|
|
139
|
-
function defaultOnError(error) {
|
|
140
|
-
console.warn(`MLLP decode error: [${error.code}] ${error.message}`);
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Default decoder options, merged with user-provided values via spread.
|
|
144
|
-
*
|
|
145
|
-
* - `encoding` — `"utf8"`, the standard encoding for HL7v2.
|
|
146
|
-
* - `onError` — logs to `console.warn`; the stream continues processing.
|
|
147
|
-
*/
|
|
148
|
-
const defaultDecoderOptions = {
|
|
149
|
-
encoding: "utf8",
|
|
150
|
-
onError: defaultOnError
|
|
151
|
-
};
|
|
152
|
-
/**
|
|
153
|
-
* Build the {@link Transformer} that powers both {@link createDecoderStream}
|
|
154
|
-
* and {@link MLLPDecoderStream}.
|
|
155
|
-
*
|
|
156
|
-
* ## How MLLP framing works
|
|
157
|
-
*
|
|
158
|
-
* Every HL7v2 message on the wire is wrapped in a minimal envelope:
|
|
159
|
-
*
|
|
160
|
-
* <VT> message-payload <FS><CR>
|
|
161
|
-
* 0x0B 0x1C 0x0D
|
|
162
|
-
*
|
|
163
|
-
* Multiple frames can arrive back-to-back within the same TCP stream.
|
|
164
|
-
* The decoder's job is to find each `<VT>…<FS><CR>` envelope, strip
|
|
165
|
-
* the framing bytes, and emit the inner payload as a {@link DecodedMessage}.
|
|
166
|
-
*
|
|
167
|
-
* ## State machine
|
|
168
|
-
*
|
|
169
|
-
* The decoder alternates between two states:
|
|
170
|
-
*
|
|
171
|
-
* ┌──────────────┐ VT found ┌────────────┐
|
|
172
|
-
* │ WAITING_START │──────────────▶│ IN_MESSAGE │
|
|
173
|
-
* └──────┬───────┘ └─────┬──────┘
|
|
174
|
-
* ▲ garbage → onError │
|
|
175
|
-
* │ │ FS CR found → emit message
|
|
176
|
-
* └─────────────────────────────┘
|
|
177
|
-
*
|
|
178
|
-
* 1. **WAITING_START** — Scans byte-by-byte for a VT start byte. Any bytes
|
|
179
|
-
* encountered before the VT are garbage (e.g. noise on the line or a partial
|
|
180
|
-
* connection reset); they are reported via `onError` and discarded.
|
|
181
|
-
* 2. **IN_MESSAGE** — Scans for the two-byte FS CR end sequence. Once found, the
|
|
182
|
-
* bytes between VT and FS CR are the message payload. The decoder emits the
|
|
183
|
-
* message and transitions back to WAITING_START.
|
|
184
|
-
*
|
|
185
|
-
* ## Chunk boundaries
|
|
186
|
-
*
|
|
187
|
-
* TCP delivers data in arbitrarily-sized chunks that rarely align with
|
|
188
|
-
* MLLP frame boundaries. A single chunk may contain:
|
|
189
|
-
*
|
|
190
|
-
* - A fraction of one frame (the rest arrives in the next chunk)
|
|
191
|
-
* - Exactly one frame
|
|
192
|
-
* - Multiple complete frames
|
|
193
|
-
* - A mix of complete frames and a trailing partial frame
|
|
194
|
-
*
|
|
195
|
-
* The transformer handles all of these by appending every incoming
|
|
196
|
-
* chunk to an internal `buffer`, then running `processBuffer()` to
|
|
197
|
-
* extract as many complete frames as possible. Any leftover bytes
|
|
198
|
-
* (a partial frame) stay in the buffer until the next chunk arrives.
|
|
199
|
-
*
|
|
200
|
-
* ## Error recovery
|
|
201
|
-
*
|
|
202
|
-
* The stream is **resilient** — it never throws. All framing errors
|
|
203
|
-
* are reported via the `onError` callback, and the decoder continues
|
|
204
|
-
* scanning for the next valid frame. Error scenarios:
|
|
205
|
-
*
|
|
206
|
-
* - **Garbage before VT** — bytes are skipped, `INVALID_START_BYTE` reported.
|
|
207
|
-
* - **Oversized message** — frame is abandoned, `MESSAGE_TOO_LARGE` reported,
|
|
208
|
-
* scanning resumes.
|
|
209
|
-
* - **Incomplete frame at stream end** — `INCOMPLETE_MESSAGE` reported during
|
|
210
|
-
* `flush()`. This is the only error that can't be recovered from, since there
|
|
211
|
-
* is no more data coming.
|
|
212
|
-
*
|
|
213
|
-
* ## Size enforcement
|
|
214
|
-
*
|
|
215
|
-
* When `maxMessageSize` is set, the decoder checks the payload length
|
|
216
|
-
* in two places to bound memory usage:
|
|
217
|
-
*
|
|
218
|
-
* 1. **Mid-accumulation** — while waiting for the end sequence, if the buffered
|
|
219
|
-
* payload already exceeds the limit, the frame is abandoned immediately.
|
|
220
|
-
* This prevents a missing end sequence from causing unbounded buffer
|
|
221
|
-
* growth.
|
|
222
|
-
* 2. **On completion** — once the end sequence is found, the final payload length
|
|
223
|
-
* is checked before the message is enqueued.
|
|
224
|
-
*/
|
|
225
|
-
function createDecoderTransformer(options) {
|
|
226
|
-
const opts = {
|
|
227
|
-
...defaultDecoderOptions,
|
|
228
|
-
...options
|
|
229
|
-
};
|
|
230
|
-
const textDecoder = new TextDecoder(opts.encoding);
|
|
231
|
-
/**
|
|
232
|
-
* Accumulation buffer — grows as chunks arrive, shrinks as frames are
|
|
233
|
-
* consumed.
|
|
234
|
-
*/
|
|
235
|
-
const dynBuffer = new DynamicBuffer();
|
|
236
|
-
/** Current state machine position. */
|
|
237
|
-
let state = "WAITING_START";
|
|
238
|
-
/**
|
|
239
|
-
* Byte offset within the buffer view where the current frame's VT start byte
|
|
240
|
-
* lives. Only meaningful when `state === "IN_MESSAGE"`.
|
|
241
|
-
*/
|
|
242
|
-
let messageStartPos = 0;
|
|
243
|
-
/**
|
|
244
|
-
* Scan `view` for the next VT (0x0B) at or after `startPos`.
|
|
245
|
-
*
|
|
246
|
-
* @returns The index of the VT byte, or -1 if not found.
|
|
247
|
-
*/
|
|
248
|
-
function findStartByte(view, startPos) {
|
|
249
|
-
for (let i = startPos; i < view.length; i++) if (view[i] === 11) return i;
|
|
250
|
-
return -1;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Scan `view` for the two-byte FS CR (0x1C 0x0D) end sequence
|
|
254
|
-
* at or after `startPos`.
|
|
255
|
-
*
|
|
256
|
-
* @returns The index of the FS byte (first byte of the pair), or -1 if not
|
|
257
|
-
* found.
|
|
258
|
-
*/
|
|
259
|
-
function findEndSequence(view, startPos) {
|
|
260
|
-
for (let i = startPos; i < view.length - 1; i++) if (view[i] === 28 && view[i + 1] === 13) return i;
|
|
261
|
-
return -1;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Drain as many complete frames from the buffer as possible, enqueuing
|
|
265
|
-
* each decoded message into `controller`.
|
|
266
|
-
*
|
|
267
|
-
* Returns when the buffer contains only an incomplete frame (or is
|
|
268
|
-
* empty). The incomplete bytes are kept for the next `transform()` call.
|
|
269
|
-
*
|
|
270
|
-
* The loop uses a local `position` cursor that always resets to 0
|
|
271
|
-
* after a `consume()` call, since consumption shifts the view.
|
|
272
|
-
*/
|
|
273
|
-
function processBuffer(controller) {
|
|
274
|
-
let position = 0;
|
|
275
|
-
while (true) {
|
|
276
|
-
const view = dynBuffer.view();
|
|
277
|
-
if (position >= view.length) break;
|
|
278
|
-
if (state === "WAITING_START") {
|
|
279
|
-
const startPos = findStartByte(view, position);
|
|
280
|
-
if (startPos === -1) {
|
|
281
|
-
if (view.length > 0) opts.onError(new TransportError(TransportErrorCode.INVALID_START_BYTE, `Skipped ${view.length - position} bytes before finding start byte`, position));
|
|
282
|
-
dynBuffer.reset();
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (startPos > position) opts.onError(new TransportError(TransportErrorCode.INVALID_START_BYTE, `Skipped ${startPos - position} bytes before start byte`, position));
|
|
286
|
-
state = "IN_MESSAGE";
|
|
287
|
-
messageStartPos = startPos;
|
|
288
|
-
position = startPos + 1;
|
|
289
|
-
}
|
|
290
|
-
if (state === "IN_MESSAGE") {
|
|
291
|
-
const endPos = findEndSequence(view, position);
|
|
292
|
-
if (endPos === -1) {
|
|
293
|
-
const currentSize = view.length - messageStartPos - 1;
|
|
294
|
-
if (opts.maxMessageSize !== void 0 && currentSize > opts.maxMessageSize) {
|
|
295
|
-
opts.onError(new TransportError(TransportErrorCode.MESSAGE_TOO_LARGE, `Message size ${currentSize} exceeds maximum ${opts.maxMessageSize}`, messageStartPos));
|
|
296
|
-
state = "WAITING_START";
|
|
297
|
-
position = messageStartPos + 1;
|
|
298
|
-
dynBuffer.consume(position);
|
|
299
|
-
position = 0;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (messageStartPos > 0) {
|
|
303
|
-
dynBuffer.consume(messageStartPos);
|
|
304
|
-
messageStartPos = 0;
|
|
305
|
-
}
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
const messageData = view.slice(messageStartPos + 1, endPos);
|
|
309
|
-
const messageLength = messageData.length;
|
|
310
|
-
if (opts.maxMessageSize !== void 0 && messageLength > opts.maxMessageSize) opts.onError(new TransportError(TransportErrorCode.MESSAGE_TOO_LARGE, `Message size ${messageLength} exceeds maximum ${opts.maxMessageSize}`, messageStartPos));
|
|
311
|
-
else controller.enqueue({
|
|
312
|
-
byteLength: messageLength,
|
|
313
|
-
data: messageData,
|
|
314
|
-
text: textDecoder.decode(messageData)
|
|
315
|
-
});
|
|
316
|
-
position = endPos + 2;
|
|
317
|
-
state = "WAITING_START";
|
|
318
|
-
dynBuffer.consume(position);
|
|
319
|
-
position = 0;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
flush(_controller) {
|
|
325
|
-
if (state === "IN_MESSAGE" && dynBuffer.available > 0) opts.onError(new TransportError(TransportErrorCode.INCOMPLETE_MESSAGE, "Stream ended with incomplete MLLP message", messageStartPos));
|
|
326
|
-
},
|
|
327
|
-
transform(chunk, controller) {
|
|
328
|
-
dynBuffer.append(chunk);
|
|
329
|
-
processBuffer(controller);
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Create a `TransformStream` that extracts MLLP messages from a raw
|
|
335
|
-
* byte stream.
|
|
336
|
-
*
|
|
337
|
-
* Pipe a TCP socket's `ReadableStream<Uint8Array>` through this stream
|
|
338
|
-
* to receive one {@link DecodedMessage} per MLLP frame, regardless of
|
|
339
|
-
* how the bytes are chunked on the wire.
|
|
340
|
-
*
|
|
341
|
-
* @example
|
|
342
|
-
* ```typescript
|
|
343
|
-
* import { createDecoderStream } from "@glion/mllp";
|
|
344
|
-
*
|
|
345
|
-
* const decoder = createDecoderStream({
|
|
346
|
-
* maxMessageSize: 1024 * 1024, // 1 MB limit
|
|
347
|
-
* onError: (err) => log.warn(err), // custom error handler
|
|
348
|
-
* });
|
|
349
|
-
*
|
|
350
|
-
* const messages = socket.readable.pipeThrough(decoder).getReader();
|
|
351
|
-
*
|
|
352
|
-
* while (true) {
|
|
353
|
-
* const { done, value } = await messages.read();
|
|
354
|
-
* if (done) break;
|
|
355
|
-
* console.log(value.text); // HL7v2 message string
|
|
356
|
-
* }
|
|
357
|
-
* ```;
|
|
358
|
-
*
|
|
359
|
-
* @param options - Optional decoder settings (max size, encoding, error
|
|
360
|
-
* handler).
|
|
361
|
-
* @returns A `TransformStream<Uint8Array, DecodedMessage>`.
|
|
362
|
-
*/
|
|
363
|
-
function createDecoderStream(options) {
|
|
364
|
-
return new TransformStream(createDecoderTransformer(options));
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Class-based variant of {@link createDecoderStream} for cases where
|
|
368
|
-
* a reusable instance is preferred over the factory function.
|
|
369
|
-
*
|
|
370
|
-
* Functionally identical — both delegate to the same internal
|
|
371
|
-
* transformer.
|
|
372
|
-
*
|
|
373
|
-
* @example
|
|
374
|
-
* ```typescript
|
|
375
|
-
* const decoder = new MLLPDecoderStream({ maxMessageSize: 512_000 });
|
|
376
|
-
* socket.readable.pipeThrough(decoder);
|
|
377
|
-
* ```;
|
|
378
|
-
*/
|
|
379
|
-
var MLLPDecoderStream = class extends TransformStream {
|
|
380
|
-
constructor(options) {
|
|
381
|
-
super(createDecoderTransformer(options));
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/transport/encoder.ts
|
|
386
|
-
/** Module-level singleton — avoids allocation per encode() call. */
|
|
387
|
-
const textEncoder = new TextEncoder();
|
|
388
|
-
/**
|
|
389
|
-
* Encode a single HL7v2 message into an MLLP frame.
|
|
390
|
-
*
|
|
391
|
-
* Wraps the message with MLLP framing:
|
|
392
|
-
* - Start byte: 0x0B (VT)
|
|
393
|
-
* - End sequence: 0x1C 0x0D (FS + CR)
|
|
394
|
-
*/
|
|
395
|
-
function encode(message, _options) {
|
|
396
|
-
const messageBytes = typeof message === "string" ? textEncoder.encode(message) : message;
|
|
397
|
-
const frame = new Uint8Array(messageBytes.length + 3);
|
|
398
|
-
frame[0] = 11;
|
|
399
|
-
frame.set(messageBytes, 1);
|
|
400
|
-
frame[frame.length - 2] = 28;
|
|
401
|
-
frame[frame.length - 1] = 13;
|
|
402
|
-
return frame;
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Encode multiple HL7v2 messages into concatenated MLLP frames.
|
|
406
|
-
*/
|
|
407
|
-
function encodeMultiple(messages, options) {
|
|
408
|
-
const frames = messages.map((msg) => encode(msg, options));
|
|
409
|
-
let totalLength = 0;
|
|
410
|
-
for (const frame of frames) totalLength += frame.length;
|
|
411
|
-
const result = new Uint8Array(totalLength);
|
|
412
|
-
let offset = 0;
|
|
413
|
-
for (const frame of frames) {
|
|
414
|
-
result.set(frame, offset);
|
|
415
|
-
offset += frame.length;
|
|
416
|
-
}
|
|
417
|
-
return result;
|
|
418
|
-
}
|
|
419
|
-
//#endregion
|
|
420
4
|
//#region src/server/compose.ts
|
|
421
5
|
/**
|
|
422
6
|
* Compose an array of middleware into a single function that executes
|
|
@@ -786,6 +370,6 @@ var Mllp = class {
|
|
|
786
370
|
}
|
|
787
371
|
};
|
|
788
372
|
//#endregion
|
|
789
|
-
export {
|
|
373
|
+
export { parsePattern as i, getMessageInfo as n, matchPattern as r, Mllp as t };
|
|
790
374
|
|
|
791
375
|
//# sourceMappingURL=mllp.js.map
|
package/dist/mllp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mllp.js","names":["queryValue","#routes","#globalMiddleware","#scopedMiddleware","#router","#processor","#errorHandler","#handleError"],"sources":["../src/transport/constants.ts","../src/errors.ts","../src/transport/errors.ts","../src/transport/types.ts","../src/transport/dynamic-buffer.ts","../src/transport/decoder-stream.ts","../src/transport/encoder.ts","../src/server/compose.ts","../src/server/context.ts","../src/server/types.ts","../src/server/router.ts","../src/server/mllp.ts"],"sourcesContent":["// -------------\n// MLLP Frame Bytes\n// -------------\n\n/**\n * MLLP Start Byte - Vertical Tab (VT)\n * Marks the beginning of an MLLP frame\n */\nexport const MLLP_START_BYTE = 0x0b;\n\n/**\n * MLLP End Byte 1 - File Separator (FS)\n * First byte of the end sequence\n */\nexport const MLLP_END_BYTE_1 = 0x1c;\n\n/**\n * MLLP End Byte 2 - Carriage Return (CR)\n * Second byte of the end sequence\n */\nexport const MLLP_END_BYTE_2 = 0x0d;\n\n// -------------\n// Pre-computed Uint8Arrays for efficiency\n// -------------\n\n/**\n * MLLP header as a Uint8Array containing the start byte\n */\nexport const MLLP_HEADER = new Uint8Array([MLLP_START_BYTE]);\n\n/**\n * MLLP trailer as a Uint8Array containing the end sequence\n */\nexport const MLLP_TRAILER = new Uint8Array([MLLP_END_BYTE_1, MLLP_END_BYTE_2]);\n","/**\n * Options for constructing an MllpError.\n */\nexport interface MllpErrorOptions extends ErrorOptions {\n /** Error condition string (e.g., for ERR segment in ACK) */\n errorCondition?: string;\n}\n\n/**\n * Base error class for all MLLP errors.\n *\n * All error types in the MLLP package extend this class.\n * Provides a common `code` field so that any MLLP error can be\n * programmatically identified and translated (e.g., into ACK/NAK responses).\n */\nexport class MllpError extends Error {\n /** Machine-readable error code identifying the error type */\n readonly code: string;\n /** Error condition string (e.g., for ERR segment in ACK) */\n readonly errorCondition?: string;\n\n constructor(code: string, message: string, options?: MllpErrorOptions) {\n super(message, options);\n this.name = \"MllpError\";\n this.code = code;\n this.errorCondition = options?.errorCondition;\n }\n}\n","import { MllpError } from \"../errors\";\nimport type { TransportErrorCode } from \"./types\";\n\n/**\n * Transport-level error for MLLP framing issues (invalid framing, incomplete\n * messages, etc.)\n */\nexport class TransportError extends MllpError {\n declare readonly code: TransportErrorCode;\n /** Position in the data where the error occurred (if applicable) */\n readonly position?: number;\n\n constructor(code: TransportErrorCode, message: string, position?: number) {\n super(code, message);\n this.name = \"TransportError\";\n this.position = position;\n }\n}\n","import type { TransportError } from \"./errors\";\n\n/**\n * Input types for encoding - supports both string and binary\n */\nexport type EncoderInput = string | Uint8Array;\n\n/**\n * Decoded message result from MLLP frame\n */\nexport interface DecodedMessage {\n /** The raw message payload (without MLLP framing) */\n data: Uint8Array;\n /** Message decoded as string (convenience) */\n text: string;\n /** Byte length of the original message */\n byteLength: number;\n}\n\n/**\n * Options for the MLLP encoder\n */\nexport interface EncoderOptions {\n /** Encoding for string input (default: 'utf-8') */\n encoding?: string;\n}\n\n/**\n * Options for the MLLP decoder\n */\nexport interface DecoderOptions {\n /** Maximum message size in bytes (optional, no default limit) */\n maxMessageSize?: number;\n /** Encoding for text conversion (default: 'utf-8') */\n encoding?: string;\n /** Error callback for logging/handling errors */\n onError?: (error: TransportError) => void;\n}\n\n/**\n * Error codes for MLLP frame-level errors\n */\nexport const TransportErrorCode = {\n /** Stream ended with incomplete MLLP message */\n INCOMPLETE_MESSAGE: \"INCOMPLETE_MESSAGE\",\n /** Frame does not end with MLLP end sequence (0x1C 0x0D) */\n INVALID_END_SEQUENCE: \"INVALID_END_SEQUENCE\",\n /** Frame does not start with MLLP start byte (0x0B) */\n INVALID_START_BYTE: \"INVALID_START_BYTE\",\n /** Message exceeds configured maximum size */\n MESSAGE_TOO_LARGE: \"MESSAGE_TOO_LARGE\",\n} as const;\n\nexport type TransportErrorCode =\n (typeof TransportErrorCode)[keyof typeof TransportErrorCode];\n","/**\n * A growable byte buffer with geometric capacity doubling.\n *\n * Designed for protocol decoders that accumulate bytes from arbitrarily\n * chunked TCP streams. Amortizes copy cost to O(n) total — the same\n * strategy used by pg-protocol, NATS.js, and Java's ArrayList.\n *\n * The buffer maintains a read offset so consumed bytes can be skipped\n * without shifting data. When the wasted prefix exceeds half the\n * capacity, the buffer auto-compacts by shifting remaining bytes to\n * the front.\n *\n * @internal\n */\nexport class DynamicBuffer {\n private _buffer: Uint8Array;\n private _length = 0;\n private _offset = 0;\n\n constructor(initialCapacity = 4096) {\n this._buffer = new Uint8Array(initialCapacity);\n }\n\n get capacity(): number {\n return this._buffer.length;\n }\n\n get available(): number {\n return this._length - this._offset;\n }\n\n append(chunk: Uint8Array): void {\n const needed = this._length + chunk.length;\n if (needed > this._buffer.length) {\n const newCapacity = Math.max(this._buffer.length * 2, needed);\n const newBuffer = new Uint8Array(newCapacity);\n newBuffer.set(this._buffer.subarray(0, this._length));\n this._buffer = newBuffer;\n }\n this._buffer.set(chunk, this._length);\n this._length += chunk.length;\n }\n\n view(): Uint8Array {\n return this._buffer.subarray(this._offset, this._length);\n }\n\n consume(n: number): void {\n this._offset += n;\n if (this._offset >= this._length) {\n this._offset = 0;\n this._length = 0;\n return;\n }\n if (this._offset > this._buffer.length / 2) {\n this._compact();\n }\n }\n\n reset(): void {\n this._offset = 0;\n this._length = 0;\n }\n\n private _compact(): void {\n const remaining = this._length - this._offset;\n this._buffer.copyWithin(0, this._offset, this._length);\n this._offset = 0;\n this._length = remaining;\n }\n}\n","import { MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_START_BYTE } from \"./constants\";\nimport { DynamicBuffer } from \"./dynamic-buffer\";\nimport { TransportError } from \"./errors\";\nimport type { DecodedMessage, DecoderOptions } from \"./types\";\nimport { TransportErrorCode } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Internal state of the two-state frame decoder.\n *\n * - `WAITING_START` — scanning for VT (0x0B) start byte; any non-start bytes are\n * reported via `onError` and discarded.\n * - `IN_MESSAGE` — accumulating payload bytes until the FS CR (0x1C 0x0D) end\n * sequence is found.\n */\ntype DecoderState = \"WAITING_START\" | \"IN_MESSAGE\";\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nfunction defaultOnError(error: TransportError): void {\n console.warn(`MLLP decode error: [${error.code}] ${error.message}`);\n}\n\n/**\n * Default decoder options, merged with user-provided values via spread.\n *\n * - `encoding` — `\"utf8\"`, the standard encoding for HL7v2.\n * - `onError` — logs to `console.warn`; the stream continues processing.\n */\nconst defaultDecoderOptions: Required<\n Pick<DecoderOptions, \"encoding\" | \"onError\">\n> = {\n encoding: \"utf8\",\n onError: defaultOnError,\n};\n\n// ---------------------------------------------------------------------------\n// Transformer factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build the {@link Transformer} that powers both {@link createDecoderStream}\n * and {@link MLLPDecoderStream}.\n *\n * ## How MLLP framing works\n *\n * Every HL7v2 message on the wire is wrapped in a minimal envelope:\n *\n * <VT> message-payload <FS><CR>\n * 0x0B 0x1C 0x0D\n *\n * Multiple frames can arrive back-to-back within the same TCP stream.\n * The decoder's job is to find each `<VT>…<FS><CR>` envelope, strip\n * the framing bytes, and emit the inner payload as a {@link DecodedMessage}.\n *\n * ## State machine\n *\n * The decoder alternates between two states:\n *\n * ┌──────────────┐ VT found ┌────────────┐\n * │ WAITING_START │──────────────▶│ IN_MESSAGE │\n * └──────┬───────┘ └─────┬──────┘\n * ▲ garbage → onError │\n * │ │ FS CR found → emit message\n * └─────────────────────────────┘\n *\n * 1. **WAITING_START** — Scans byte-by-byte for a VT start byte. Any bytes\n * encountered before the VT are garbage (e.g. noise on the line or a partial\n * connection reset); they are reported via `onError` and discarded.\n * 2. **IN_MESSAGE** — Scans for the two-byte FS CR end sequence. Once found, the\n * bytes between VT and FS CR are the message payload. The decoder emits the\n * message and transitions back to WAITING_START.\n *\n * ## Chunk boundaries\n *\n * TCP delivers data in arbitrarily-sized chunks that rarely align with\n * MLLP frame boundaries. A single chunk may contain:\n *\n * - A fraction of one frame (the rest arrives in the next chunk)\n * - Exactly one frame\n * - Multiple complete frames\n * - A mix of complete frames and a trailing partial frame\n *\n * The transformer handles all of these by appending every incoming\n * chunk to an internal `buffer`, then running `processBuffer()` to\n * extract as many complete frames as possible. Any leftover bytes\n * (a partial frame) stay in the buffer until the next chunk arrives.\n *\n * ## Error recovery\n *\n * The stream is **resilient** — it never throws. All framing errors\n * are reported via the `onError` callback, and the decoder continues\n * scanning for the next valid frame. Error scenarios:\n *\n * - **Garbage before VT** — bytes are skipped, `INVALID_START_BYTE` reported.\n * - **Oversized message** — frame is abandoned, `MESSAGE_TOO_LARGE` reported,\n * scanning resumes.\n * - **Incomplete frame at stream end** — `INCOMPLETE_MESSAGE` reported during\n * `flush()`. This is the only error that can't be recovered from, since there\n * is no more data coming.\n *\n * ## Size enforcement\n *\n * When `maxMessageSize` is set, the decoder checks the payload length\n * in two places to bound memory usage:\n *\n * 1. **Mid-accumulation** — while waiting for the end sequence, if the buffered\n * payload already exceeds the limit, the frame is abandoned immediately.\n * This prevents a missing end sequence from causing unbounded buffer\n * growth.\n * 2. **On completion** — once the end sequence is found, the final payload length\n * is checked before the message is enqueued.\n */\nfunction createDecoderTransformer(options?: DecoderOptions) {\n const opts = { ...defaultDecoderOptions, ...options };\n const textDecoder = new TextDecoder(opts.encoding);\n\n // -------------------------------------------------------------------------\n // Mutable decoder state (scoped to this transformer instance)\n // -------------------------------------------------------------------------\n\n /**\n * Accumulation buffer — grows as chunks arrive, shrinks as frames are\n * consumed.\n */\n const dynBuffer = new DynamicBuffer();\n\n /** Current state machine position. */\n let state: DecoderState = \"WAITING_START\";\n\n /**\n * Byte offset within the buffer view where the current frame's VT start byte\n * lives. Only meaningful when `state === \"IN_MESSAGE\"`.\n */\n let messageStartPos = 0;\n\n // -------------------------------------------------------------------------\n // Buffer scanning helpers\n // -------------------------------------------------------------------------\n\n /**\n * Scan `view` for the next VT (0x0B) at or after `startPos`.\n *\n * @returns The index of the VT byte, or -1 if not found.\n */\n function findStartByte(view: Uint8Array, startPos: number): number {\n for (let i = startPos; i < view.length; i++) {\n if (view[i] === MLLP_START_BYTE) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n * Scan `view` for the two-byte FS CR (0x1C 0x0D) end sequence\n * at or after `startPos`.\n *\n * @returns The index of the FS byte (first byte of the pair), or -1 if not\n * found.\n */\n function findEndSequence(view: Uint8Array, startPos: number): number {\n for (let i = startPos; i < view.length - 1; i++) {\n if (view[i] === MLLP_END_BYTE_1 && view[i + 1] === MLLP_END_BYTE_2) {\n return i;\n }\n }\n return -1;\n }\n\n // -------------------------------------------------------------------------\n // Core processing loop\n // -------------------------------------------------------------------------\n\n /**\n * Drain as many complete frames from the buffer as possible, enqueuing\n * each decoded message into `controller`.\n *\n * Returns when the buffer contains only an incomplete frame (or is\n * empty). The incomplete bytes are kept for the next `transform()` call.\n *\n * The loop uses a local `position` cursor that always resets to 0\n * after a `consume()` call, since consumption shifts the view.\n */\n function processBuffer(\n controller: TransformStreamDefaultController<DecodedMessage>\n ): void {\n let position = 0;\n\n while (true) {\n const view = dynBuffer.view();\n if (position >= view.length) {\n break;\n }\n\n // ── WAITING_START ──────────────────────────────────────────────\n // Look for a VT byte that begins a new MLLP frame.\n if (state === \"WAITING_START\") {\n const startPos = findStartByte(view, position);\n\n if (startPos === -1) {\n // No VT anywhere in the remaining buffer — all garbage.\n if (view.length > 0) {\n opts.onError(\n new TransportError(\n TransportErrorCode.INVALID_START_BYTE,\n `Skipped ${view.length - position} bytes before finding start byte`,\n position\n )\n );\n }\n dynBuffer.reset();\n return;\n }\n\n // Report any garbage bytes that appeared before the VT.\n if (startPos > position) {\n opts.onError(\n new TransportError(\n TransportErrorCode.INVALID_START_BYTE,\n `Skipped ${startPos - position} bytes before start byte`,\n position\n )\n );\n }\n\n // Transition: we found VT, now accumulate the payload.\n state = \"IN_MESSAGE\";\n messageStartPos = startPos;\n position = startPos + 1; // advance past VT\n }\n\n // ── IN_MESSAGE ─────────────────────────────────────────────────\n // We have a VT; now look for the FS CR end sequence.\n if (state === \"IN_MESSAGE\") {\n const endPos = findEndSequence(view, position);\n\n if (endPos === -1) {\n // End sequence not yet in buffer — need more data from the wire.\n //\n // Before returning, check if the partial payload already exceeds\n // maxMessageSize. If so, abandon this frame early to avoid\n // unbounded buffer growth (e.g. a missing end sequence on a\n // very large or malicious stream).\n const currentSize = view.length - messageStartPos - 1;\n if (\n opts.maxMessageSize !== undefined &&\n currentSize > opts.maxMessageSize\n ) {\n opts.onError(\n new TransportError(\n TransportErrorCode.MESSAGE_TOO_LARGE,\n `Message size ${currentSize} exceeds maximum ${opts.maxMessageSize}`,\n messageStartPos\n )\n );\n\n // Abandon this frame. Resume scanning for a new VT starting\n // one byte after the current frame's VT (it might have been\n // a false start).\n state = \"WAITING_START\";\n position = messageStartPos + 1;\n dynBuffer.consume(position);\n position = 0;\n continue;\n }\n\n // Compact: shift the partial frame to the start of the buffer\n // so we don't re-scan already-consumed bytes on the next chunk.\n if (messageStartPos > 0) {\n dynBuffer.consume(messageStartPos);\n messageStartPos = 0;\n }\n return; // wait for more data\n }\n\n // ── Complete frame found ───────────────────────────────────\n // Extract the payload: everything between VT and FS CR.\n //\n // view: ... [VT] [payload bytes...] [FS] [CR] ...\n // ^ ^\n // messageStartPos endPos\n //\n // payload = view[messageStartPos+1 .. endPos)\n //\n const messageData = view.slice(messageStartPos + 1, endPos);\n const messageLength = messageData.length;\n\n // Final size check on the complete payload.\n if (\n opts.maxMessageSize !== undefined &&\n messageLength > opts.maxMessageSize\n ) {\n opts.onError(\n new TransportError(\n TransportErrorCode.MESSAGE_TOO_LARGE,\n `Message size ${messageLength} exceeds maximum ${opts.maxMessageSize}`,\n messageStartPos\n )\n );\n } else {\n // Emit the decoded message downstream.\n controller.enqueue({\n byteLength: messageLength,\n data: messageData,\n text: textDecoder.decode(messageData),\n });\n }\n\n // Advance past the FS CR end sequence (2 bytes) and compact\n // the buffer so the next iteration starts fresh.\n position = endPos + 2;\n state = \"WAITING_START\";\n dynBuffer.consume(position);\n position = 0;\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Transformer interface returned to TransformStream\n // -------------------------------------------------------------------------\n\n return {\n /**\n * Called when the writable side of the stream closes (TCP connection\n * ended). If we're mid-frame, it means the connection dropped before\n * a complete MLLP envelope was received — report it via `onError`.\n */\n flush(_controller: TransformStreamDefaultController<DecodedMessage>): void {\n if (state === \"IN_MESSAGE\" && dynBuffer.available > 0) {\n opts.onError(\n new TransportError(\n TransportErrorCode.INCOMPLETE_MESSAGE,\n \"Stream ended with incomplete MLLP message\",\n messageStartPos\n )\n );\n }\n },\n\n /**\n * Called for each chunk of bytes received from the TCP socket.\n * Appends the chunk to the internal buffer and extracts any\n * complete MLLP frames that are now available.\n */\n transform(\n chunk: Uint8Array,\n controller: TransformStreamDefaultController<DecodedMessage>\n ): void {\n dynBuffer.append(chunk);\n processBuffer(controller);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create a `TransformStream` that extracts MLLP messages from a raw\n * byte stream.\n *\n * Pipe a TCP socket's `ReadableStream<Uint8Array>` through this stream\n * to receive one {@link DecodedMessage} per MLLP frame, regardless of\n * how the bytes are chunked on the wire.\n *\n * @example\n * ```typescript\n * import { createDecoderStream } from \"@glion/mllp\";\n *\n * const decoder = createDecoderStream({\n * maxMessageSize: 1024 * 1024, // 1 MB limit\n * onError: (err) => log.warn(err), // custom error handler\n * });\n *\n * const messages = socket.readable.pipeThrough(decoder).getReader();\n *\n * while (true) {\n * const { done, value } = await messages.read();\n * if (done) break;\n * console.log(value.text); // HL7v2 message string\n * }\n * ```;\n *\n * @param options - Optional decoder settings (max size, encoding, error\n * handler).\n * @returns A `TransformStream<Uint8Array, DecodedMessage>`.\n */\nexport function createDecoderStream(\n options?: DecoderOptions\n): TransformStream<Uint8Array, DecodedMessage> {\n return new TransformStream(createDecoderTransformer(options));\n}\n\n/**\n * Class-based variant of {@link createDecoderStream} for cases where\n * a reusable instance is preferred over the factory function.\n *\n * Functionally identical — both delegate to the same internal\n * transformer.\n *\n * @example\n * ```typescript\n * const decoder = new MLLPDecoderStream({ maxMessageSize: 512_000 });\n * socket.readable.pipeThrough(decoder);\n * ```;\n */\nexport class MLLPDecoderStream extends TransformStream<\n Uint8Array,\n DecodedMessage\n> {\n constructor(options?: DecoderOptions) {\n super(createDecoderTransformer(options));\n }\n}\n","import { MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_START_BYTE } from \"./constants\";\nimport type { EncoderInput, EncoderOptions } from \"./types\";\n\n/** Module-level singleton — avoids allocation per encode() call. */\nconst textEncoder = new TextEncoder();\n\n/**\n * Encode a single HL7v2 message into an MLLP frame.\n *\n * Wraps the message with MLLP framing:\n * - Start byte: 0x0B (VT)\n * - End sequence: 0x1C 0x0D (FS + CR)\n */\nexport function encode(\n message: EncoderInput,\n _options?: EncoderOptions\n): Uint8Array {\n const messageBytes =\n typeof message === \"string\" ? textEncoder.encode(message) : message;\n\n // Direct allocation: header (1) + message + trailer (2)\n const frame = new Uint8Array(messageBytes.length + 3);\n frame[0] = MLLP_START_BYTE;\n frame.set(messageBytes, 1);\n frame[frame.length - 2] = MLLP_END_BYTE_1;\n frame[frame.length - 1] = MLLP_END_BYTE_2;\n return frame;\n}\n\n/**\n * Encode multiple HL7v2 messages into concatenated MLLP frames.\n */\nexport function encodeMultiple(\n messages: EncoderInput[],\n options?: EncoderOptions\n): Uint8Array {\n const frames = messages.map((msg) => encode(msg, options));\n let totalLength = 0;\n for (const frame of frames) {\n totalLength += frame.length;\n }\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const frame of frames) {\n result.set(frame, offset);\n offset += frame.length;\n }\n return result;\n}\n","import type { Context, Middleware } from \"./types\";\n\n/**\n * Compose an array of middleware into a single function that executes\n * them in onion-model order (like Hono/Koa).\n *\n * Each middleware receives (ctx, next). Calling next() proceeds to the\n * next middleware. If a middleware returns a Response without\n * calling next(), it short-circuits the chain.\n *\n * Response handling follows Hono's pattern: return values are written\n * to ctx.res, and middleware can also set ctx.res directly.\n * Last write wins.\n */\nexport function compose(\n middlewares: Middleware[]\n): (ctx: Context) => Promise<void> {\n return async (ctx: Context): Promise<void> => {\n let index = -1;\n\n async function dispatch(i: number): Promise<void> {\n if (i <= index) {\n throw new Error(\"next() called multiple times\");\n }\n index = i;\n\n if (i >= middlewares.length) {\n return;\n }\n\n const middleware = middlewares[i] as Middleware;\n const result = await middleware(ctx, () => dispatch(i + 1));\n\n // If middleware returned a response, set ctx.res (last write wins)\n if (\n result &&\n typeof result === \"object\" &&\n \"raw\" in result &&\n typeof result.raw === \"string\"\n ) {\n ctx.res = result;\n }\n }\n\n await dispatch(0);\n };\n}\n","import type { Root } from \"@glion/ast\";\nimport { value as queryValue } from \"@glion/util-query\";\nimport { VFile } from \"vfile\";\n\nimport type {\n ConnectionInfo,\n Context,\n Hl7v2Processor,\n Response,\n} from \"./types\";\n\n/**\n * Options for creating a Context instance.\n */\nexport interface CreateContextOptions {\n /** Raw HL7 message string */\n raw: string;\n /** Raw bytes from the MLLP frame payload */\n bytes: Uint8Array;\n /** Connection metadata */\n connection: ConnectionInfo;\n /** HL7v2 unified processor */\n processor: Hl7v2Processor;\n}\n\n/**\n * Create a new Context for an incoming message.\n *\n * Only the **parse** step runs eagerly (sync, fast). Routing fields are\n * extracted as strings from the parsed tree immediately — these cached\n * strings always reflect the original wire values (see ADR-0013).\n *\n * The expensive pipeline stages are lazy:\n *\n * - `ctx.tree()` — triggers `run()` (transformers: annotations, escape decoding,\n * lint). Cached after first call.\n * - `ctx.result()` — triggers `run()` + `stringify()` (compilation, e.g., JSON\n * via hl7v2Jsonify). Cached after first call.\n *\n * If no handler matches or middleware short-circuits before accessing\n * the tree, the transform and compile steps never execute.\n */\nexport function createContext(options: CreateContextOptions): Context {\n const { raw, bytes, connection, processor } = options;\n const variables = new Map<string, unknown>();\n let varSnapshot: Readonly<Record<string, unknown>> | undefined;\n\n // ── Eager: parse (sync, fast) ──────────────────────────────────────\n // VFile carries the input through the pipeline and collects diagnostics.\n const file = new VFile(raw);\n const parsed = processor.parse(file);\n\n // Extract routing fields as strings from the parsed tree.\n // These are cached immediately and always reflect the original\n // wire values, even after transformers enrich the tree in-place.\n const controlId = queryValue(parsed, \"MSH-10\")?.value ?? \"\";\n const messageType = queryValue(parsed, \"MSH-9.1\")?.value ?? \"\";\n const triggerEvent = queryValue(parsed, \"MSH-9.2\")?.value ?? \"\";\n const messageStructure = queryValue(parsed, \"MSH-9.3\")?.value ?? \"\";\n const version = queryValue(parsed, \"MSH-12.1\")?.value ?? \"\";\n\n // ── Lazy: transform (async, cached) ────────────────────────────────\n // run() applies all transformers: annotations, escape decoding, lint.\n // Only executes on first call to ctx.tree() or ctx.result().\n let transformedTree: Root | undefined;\n let transformPromise: Promise<Root> | undefined;\n\n function getTree(): Promise<Root> {\n if (transformedTree) {\n return Promise.resolve(transformedTree);\n }\n if (!transformPromise) {\n transformPromise = (async () => {\n const result = await processor.run(parsed, file);\n transformedTree = result;\n return result;\n })();\n }\n return transformPromise;\n }\n\n // ── Lazy: compile (async, cached) ──────────────────────────────────\n // stringify() compiles the tree into the final format (e.g., JSON).\n // Only executes on first call to ctx.result(). Requires tree first.\n // Promise is cached (including rejections) to match getTree() behavior.\n let resultPromise: Promise<unknown | undefined> | undefined;\n\n function getResult(): Promise<unknown | undefined> {\n if (!resultPromise) {\n resultPromise = (async () => {\n const tree = await getTree();\n return processor.compiler ? processor.stringify(tree, file) : undefined;\n })();\n }\n return resultPromise;\n }\n\n // ── Build context ──────────────────────────────────────────────────\n const ctx: Context = {\n ast: parsed,\n connection: Object.freeze(connection),\n controlId,\n file,\n get<K extends string>(key: K): unknown {\n return variables.get(key);\n },\n messageStructure,\n messageType,\n req: Object.freeze({ bytes, raw }),\n res: undefined as Response | undefined,\n result: getResult,\n set<K extends string>(key: K, value: unknown): void {\n variables.set(key, value);\n varSnapshot = undefined;\n },\n tree: getTree,\n triggerEvent,\n get var(): Readonly<Record<string, unknown>> {\n if (!varSnapshot) {\n varSnapshot = Object.freeze(Object.fromEntries(variables));\n }\n return varSnapshot;\n },\n version,\n };\n\n return ctx;\n}\n","import type { Root } from \"@glion/ast\";\nimport type { Processor } from \"unified\";\nimport type { VFile } from \"vfile\";\n\n/**\n * Connection metadata for an MLLP socket.\n */\nexport interface ConnectionInfo {\n readonly id: number;\n readonly remoteAddress: string;\n readonly remotePort: number;\n readonly localPort: number;\n readonly secure: boolean;\n readonly state: Map<string, unknown>;\n}\n\n/**\n * MLLP response — a raw HL7v2 string that the server encodes\n * with MLLP framing before sending back over TCP.\n *\n * Acknowledgment helpers (ack/nak) are not built in — register\n * an acknowledgment middleware via `.use()` to generate ACK/NAK responses.\n */\nexport interface Response {\n readonly raw: string;\n}\n\n/**\n * The MLLP context object.\n *\n * Routing fields (messageType, triggerEvent, etc.) are extracted from\n * the **pre-transform** parsed tree and are always available synchronously.\n *\n * The transformed tree, VFile, and compiled result are lazy — they\n * trigger pipeline stages on first access. See ADR-0013.\n */\nexport interface Context {\n /** Incoming message data */\n readonly req: {\n /** Original HL7 message string (without MLLP framing) */\n readonly raw: string;\n /** Raw bytes from MLLP frame payload */\n readonly bytes: Uint8Array;\n };\n\n /** TCP connection metadata */\n readonly connection: ConnectionInfo;\n\n /**\n * Parsed AST — always available synchronously. Use this for reading\n * MSH fields, building ACKs, routing, or any operation that doesn't\n * need to wait for transforms to complete.\n *\n * Note: this is the same object reference as the tree returned by\n * `tree()`. After `tree()` resolves, transformers may have enriched\n * it in-place (e.g., decoded escapes, resolved message structure).\n * The routing fields (messageType, triggerEvent, etc.) are cached\n * as separate string properties and always reflect the original\n * wire values regardless of transforms.\n */\n readonly ast: Root;\n\n /**\n * Transformed AST. Triggers `run()` (transformers) on first call.\n * Cached after first access — subsequent calls return the same tree.\n */\n tree(): Promise<Root>;\n\n /**\n * VFile with diagnostics and lint messages. Created during context\n * creation. Diagnostics accumulate after `tree()` triggers transformers.\n */\n file: VFile;\n\n /**\n * Compiled result from the processor's compiler (e.g., JSON from\n * hl7v2Jsonify). Triggers both `run()` and `stringify()` on first call.\n * Undefined when the processor has no compiler.\n * Cached after first access.\n */\n result(): Promise<unknown | undefined>;\n\n /** Response to send back. Set by middleware or handler. */\n res: Response | undefined;\n\n /** MSH-9.1 message type (e.g., \"ADT\") — from pre-transform parse */\n messageType: string;\n /** MSH-9.2 trigger event (e.g., \"A01\") — from pre-transform parse */\n triggerEvent: string;\n /** MSH-9.3 message structure (e.g., \"ADT_A01\") — from pre-transform parse */\n messageStructure: string;\n /** MSH-12 version (e.g., \"2.5.1\") — from pre-transform parse */\n version: string;\n /** MSH-10 message control ID — from pre-transform parse */\n controlId: string;\n\n /** Store a variable */\n set<K extends string>(key: K, value: unknown): void;\n /** Retrieve a variable */\n get<K extends string>(key: K): unknown;\n /** Read-only snapshot of all variables */\n readonly var: Readonly<Record<string, unknown>>;\n}\n\n/**\n * A unified `Processor` that parses HL7v2 messages into `Root` trees.\n *\n * Only `ParseTree` is constrained — the remaining type params are left\n * loose so that any processor built with `unified().use(hl7v2Parser)`\n * is assignable regardless of which transformer/compiler plugins are added.\n */\nexport type Hl7v2Processor = Processor<Root, Root, Root>;\n\n/**\n * Middleware function signature (Hono/Koa onion model).\n */\nexport type MiddlewareReturn =\n // oxlint-disable-next-line typescript/no-invalid-void-type\n | Promise<Response | undefined | void>\n | Promise<void>\n | Response\n | undefined\n // oxlint-disable-next-line typescript/no-invalid-void-type\n | void;\n\nexport type Middleware = (\n ctx: Context,\n next: () => Promise<void>\n) => MiddlewareReturn;\n\n/**\n * Terminal route handler — may return a response or void (no response).\n */\nexport type Handler = (\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n) => Promise<Response | undefined | void> | Response | undefined | void;\n\n/**\n * Error handler — receives the error and context, returns a response.\n */\nexport type ErrorHandler = (\n err: Error,\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n) => Promise<Response | undefined | void> | Response | undefined | void;\n\n/**\n * A filter function for route or middleware matching.\n * Receives the full context and returns true if the route/middleware applies.\n */\nexport type RouteFilter = (ctx: Context) => boolean;\n\n/**\n * Parsed route pattern for matching against incoming messages.\n */\nexport interface RoutePattern {\n /** Original pattern string (e.g., \"ADT^A01\") */\n readonly source: string;\n /** Message type to match, or \"*\" for wildcard */\n readonly messageType: string;\n /** Trigger event to match, or \"*\" for wildcard */\n readonly triggerEvent: string;\n}\n\n/**\n * Parse a pattern string like \"ADT^A01\", \"ADT^*\", \"*^A01\", or \"*\"\n * into a structured RoutePattern.\n */\nexport function parsePattern(pattern: string): RoutePattern {\n if (pattern === \"*\") {\n return { messageType: \"*\", source: pattern, triggerEvent: \"*\" };\n }\n\n const caretIndex = pattern.indexOf(\"^\");\n if (caretIndex === -1) {\n return { messageType: pattern, source: pattern, triggerEvent: \"*\" };\n }\n\n return {\n messageType: pattern.slice(0, caretIndex) || \"*\",\n source: pattern,\n triggerEvent: pattern.slice(caretIndex + 1) || \"*\",\n };\n}\n\n/**\n * Test whether a route pattern matches a given message type and trigger event.\n */\nexport function matchPattern(\n pattern: RoutePattern,\n messageType: string,\n triggerEvent: string\n): boolean {\n const typeMatch =\n pattern.messageType === \"*\" || pattern.messageType === messageType;\n const eventMatch =\n pattern.triggerEvent === \"*\" || pattern.triggerEvent === triggerEvent;\n return typeMatch && eventMatch;\n}\n","import { matchPattern, parsePattern } from \"./types\";\nimport type { Context, Handler, Middleware, RouteFilter } from \"./types\";\n\n/**\n * A route entry: matcher + handler.\n */\ninterface Route {\n filter: (ctx: Context) => boolean;\n handler: Handler;\n}\n\n/**\n * A scoped middleware entry: matcher + middleware.\n */\ninterface ScopedMiddleware {\n filter: (ctx: Context) => boolean;\n middleware: Middleware;\n}\n\n/**\n * Result of matching a message against registered routes and middleware.\n */\nexport interface MatchResult {\n /** The matched handler, or undefined if no route matches */\n handler: Handler | undefined;\n /** Middleware that applies to this message (global + scoped matches) */\n middlewares: Middleware[];\n}\n\n/**\n * Create a matcher function from a string pattern.\n */\nfunction patternMatcher(pattern: string): (ctx: Context) => boolean {\n const parsed = parsePattern(pattern);\n return (ctx) => matchPattern(parsed, ctx.messageType, ctx.triggerEvent);\n}\n\n/**\n * Pattern-matching router for HL7v2 message types.\n *\n * Routes are matched in registration order (first match wins).\n * Supports string patterns with wildcards or filter functions.\n *\n * String patterns:\n *\n * - `\"ADT^A01\"` — exact match\n * - `\"ADT^*\"` — any ADT message\n * - `\"*\"` — catch-all\n *\n * Filter functions:\n *\n * - `(ctx) => ctx.messageType === \"ADT\" && ctx.version === \"2.5.1\"`\n */\nexport class Router {\n readonly #routes: Route[] = [];\n readonly #globalMiddleware: Middleware[] = [];\n readonly #scopedMiddleware: ScopedMiddleware[] = [];\n\n /**\n * Register a route handler with a string pattern or filter function.\n */\n add(patternOrFilter: string | RouteFilter, handler: Handler): void {\n const filter =\n typeof patternOrFilter === \"string\"\n ? patternMatcher(patternOrFilter)\n : patternOrFilter;\n\n this.#routes.push({ filter, handler });\n }\n\n /**\n * Register global middleware (runs for all messages).\n * When `prepend` is true, the middleware is inserted at the front\n * of the chain so it executes outermost (first in, last out).\n */\n addMiddleware(middleware: Middleware, prepend?: boolean): void;\n /**\n * Register scoped middleware with a string pattern or filter function.\n */\n addMiddleware(\n patternOrFilter: string | RouteFilter,\n middleware: Middleware\n ): void;\n addMiddleware(\n patternOrFilterOrMiddleware: string | RouteFilter | Middleware,\n middlewareOrPrepend?: Middleware | boolean\n ): void {\n // Global middleware: addMiddleware(fn) or addMiddleware(fn, true)\n if (\n typeof patternOrFilterOrMiddleware === \"function\" &&\n (middlewareOrPrepend === undefined ||\n typeof middlewareOrPrepend === \"boolean\")\n ) {\n if (middlewareOrPrepend === true) {\n this.#globalMiddleware.unshift(\n patternOrFilterOrMiddleware as Middleware\n );\n } else {\n this.#globalMiddleware.push(patternOrFilterOrMiddleware as Middleware);\n }\n return;\n }\n\n // Scoped middleware: addMiddleware(pattern/filter, fn)\n if (typeof middlewareOrPrepend === \"function\") {\n const middleware = middlewareOrPrepend;\n const filter =\n typeof patternOrFilterOrMiddleware === \"string\"\n ? patternMatcher(patternOrFilterOrMiddleware)\n : (patternOrFilterOrMiddleware as RouteFilter);\n\n this.#scopedMiddleware.push({ filter, middleware });\n }\n }\n\n /**\n * Match a context against registered routes and middleware.\n * Returns the first matching handler and all applicable middleware.\n */\n match(ctx: Context): MatchResult {\n // Find first matching route (registration order)\n let handler: Handler | undefined;\n for (const route of this.#routes) {\n if (route.filter(ctx)) {\n handler = route.handler;\n break;\n }\n }\n\n // Collect applicable middleware: global + matching scoped\n const middlewares: Middleware[] = [...this.#globalMiddleware];\n for (const scoped of this.#scopedMiddleware) {\n if (scoped.filter(ctx)) {\n middlewares.push(scoped.middleware);\n }\n }\n\n return { handler, middlewares };\n }\n}\n","import { MllpError } from \"../errors\";\nimport { compose } from \"./compose\";\nimport { createContext } from \"./context\";\nimport { Router } from \"./router\";\nimport type {\n ConnectionInfo,\n Context,\n ErrorHandler,\n Handler,\n Hl7v2Processor,\n Middleware,\n Response,\n RouteFilter,\n} from \"./types\";\n\n/**\n * Routing fields from the message that caused an error.\n * Attached to errors thrown by `Mllp.handle()` so that callers\n * (e.g., `serve()`) can include message context in error callbacks.\n */\nexport interface MessageInfo {\n readonly messageType: string;\n readonly triggerEvent: string;\n readonly messageStructure: string;\n readonly version: string;\n readonly controlId: string;\n}\n\n/**\n * Internal store for associating message routing info with errors.\n * Uses a WeakMap so error objects can be garbage-collected normally.\n */\nconst errorMessageInfo = new WeakMap<Error, MessageInfo>();\n\n/**\n * Retrieve the {@link MessageInfo} associated with an error thrown\n * by `Mllp.handle()`. Returns `undefined` for errors that did not\n * originate from message processing (e.g., lifecycle callback errors).\n */\nexport function getMessageInfo(error: Error): MessageInfo | undefined {\n return errorMessageInfo.get(error);\n}\n\n/**\n * MLLP application for HL7v2 messaging.\n *\n * A pure routing and middleware engine with no TCP/server concerns.\n * Use `serve()` to bind this to a TCP server.\n *\n * @example\n * ```typescript\n * import { Mllp } from \"@glion/mllp\";\n * import { parseHL7v2 } from \"@glion/hl7v2\";\n *\n * const app = new Mllp().parser(parseHL7v2).on(\"ADT^A01\", async (ctx) => {\n * const tree = await ctx.tree();\n * return { raw: \"...\" };\n * });\n * ```;\n */\nexport class Mllp {\n readonly #router = new Router();\n #processor: Hl7v2Processor | undefined;\n #errorHandler: ErrorHandler | undefined;\n\n /**\n * Register an HL7v2 processor for incoming messages.\n *\n * Accepts a unified `Processor` — either a pre-built one like\n * `parseHL7v2` from `@glion/hl7v2`, or a custom composition\n * via `unified().use(hl7v2Parser).use(...)`.\n *\n * Must be called before `handle()`. Calling multiple times replaces\n * the previous processor (last-write-wins).\n */\n parser(processor: Hl7v2Processor): this {\n this.#processor = processor;\n return this;\n }\n\n /**\n * Register middleware.\n *\n * Accepts:\n *\n * - A middleware function: `(ctx, next) => { ... }`\n * - A scoped middleware: `app.use('ADT^*', middleware)` or `app.use(filter,\n * middleware)`\n */\n use(middleware: Middleware, options?: { prepend?: boolean }): this;\n use(patternOrFilter: string | RouteFilter, middleware: Middleware): this;\n use(\n first: string | RouteFilter | Middleware,\n second?: Middleware | { prepend?: boolean }\n ): this {\n // Scoped middleware: use('ADT^*', middleware) or use(filter, middleware)\n if (\n (typeof first === \"string\" || typeof first === \"function\") &&\n typeof second === \"function\"\n ) {\n this.#router.addMiddleware(first as string | RouteFilter, second);\n return this;\n }\n\n // Standard middleware: use((ctx, next) => { ... })\n if (typeof first === \"function\") {\n const prepend =\n second && typeof second === \"object\" ? second.prepend : false;\n this.#router.addMiddleware(first as Middleware, prepend);\n return this;\n }\n\n return this;\n }\n\n /**\n * Register a route handler for a message type pattern or filter function.\n *\n * String patterns:\n *\n * - `\"ADT^A01\"` — exact match\n * - `\"ADT^*\"` — any ADT message\n * - `\"*^A01\"` — any type with A01 trigger\n * - `\"*\"` — catch-all\n *\n * Filter functions:\n *\n * - `(ctx) => ctx.messageType === \"ADT\" && ctx.version === \"2.5.1\"`\n */\n on(patternOrFilter: string | RouteFilter, handler: Handler): this {\n this.#router.add(patternOrFilter, handler);\n return this;\n }\n\n /**\n * Register a global error handler.\n *\n * Called when middleware or a handler throws. Without an error handler,\n * the error is re-thrown to the caller (e.g. `serve()`).\n *\n * If the error handler returns a response, it is used as the reply.\n * If the error handler itself throws, the new error is re-thrown.\n */\n onError(handler: ErrorHandler): this {\n this.#errorHandler = handler;\n return this;\n }\n\n /**\n * Process a raw HL7v2 message through the middleware chain and router.\n * This is the integration point — analogous to Hono's `fetch()`.\n *\n * The pipeline is lazy (see ADR-0013):\n * 1. Parse (sync, fast) — always runs, extracts routing fields\n * 2. Route match — uses pre-transform routing fields\n * 3. If no match → return undefined (no transform/compile cost)\n * 4. Transform/compile — only when handlers access ctx.tree()/ctx.result()\n *\n * Throws `MllpError` if no processor has been registered via `app.parser()`.\n */\n async handle(\n raw: string,\n bytes: Uint8Array,\n connection: ConnectionInfo\n // oxlint-disable-next-line typescript/no-invalid-void-type\n ): Promise<Response | undefined | void> {\n if (!this.#processor) {\n throw new MllpError(\n \"ERR_NO_PARSER\",\n \"No parser registered. Call app.parser() before handling messages.\"\n );\n }\n\n // Context creation is sync — only parse() runs here.\n // Transform and compile are deferred to ctx.tree() / ctx.result().\n const ctx = createContext({\n bytes,\n connection,\n processor: this.#processor,\n raw,\n });\n\n try {\n // Match route and collect middleware\n const match = this.#router.match(ctx);\n const middlewares = [...match.middlewares];\n\n // Add the handler as the terminal middleware\n if (match.handler) {\n const handler = match.handler;\n middlewares.push((handlerCtx: Context) => handler(handlerCtx));\n }\n\n // Compose and execute\n await compose(middlewares)(ctx);\n\n return ctx.res;\n } catch (error) {\n return this.#handleError(\n error instanceof Error ? error : new Error(String(error)),\n ctx\n );\n }\n }\n\n /**\n * Handle an error during message processing.\n *\n * If a custom error handler is registered, delegates to it.\n * If the error handler itself throws, re-throws the error handler's error.\n * If no error handler is registered, re-throws the original error.\n *\n * In both re-throw cases, message routing fields are attached to the\n * error via {@link getMessageInfo} so callers can include message\n * context in error reporting.\n *\n * Callers (e.g. `serve()`) are responsible for catching these errors\n * and deciding how to handle them at the transport level.\n */\n async #handleError(\n err: Error,\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n ): Promise<Response | undefined | void> {\n const info: MessageInfo = {\n controlId: ctx.controlId,\n messageStructure: ctx.messageStructure,\n messageType: ctx.messageType,\n triggerEvent: ctx.triggerEvent,\n version: ctx.version,\n };\n\n if (this.#errorHandler) {\n try {\n return await this.#errorHandler(err, ctx);\n } catch (handlerError) {\n const e =\n handlerError instanceof Error\n ? handlerError\n : new Error(String(handlerError));\n errorMessageInfo.set(e, info);\n throw e;\n }\n }\n\n errorMessageInfo.set(err, info);\n throw err;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mllp.js","names":["queryValue","#routes","#globalMiddleware","#scopedMiddleware","#router","#processor","#errorHandler","#handleError"],"sources":["../src/server/compose.ts","../src/server/context.ts","../src/server/types.ts","../src/server/router.ts","../src/server/mllp.ts"],"sourcesContent":["import type { Context, Middleware } from \"./types\";\n\n/**\n * Compose an array of middleware into a single function that executes\n * them in onion-model order (like Hono/Koa).\n *\n * Each middleware receives (ctx, next). Calling next() proceeds to the\n * next middleware. If a middleware returns a Response without\n * calling next(), it short-circuits the chain.\n *\n * Response handling follows Hono's pattern: return values are written\n * to ctx.res, and middleware can also set ctx.res directly.\n * Last write wins.\n */\nexport function compose(\n middlewares: Middleware[]\n): (ctx: Context) => Promise<void> {\n return async (ctx: Context): Promise<void> => {\n let index = -1;\n\n async function dispatch(i: number): Promise<void> {\n if (i <= index) {\n throw new Error(\"next() called multiple times\");\n }\n index = i;\n\n if (i >= middlewares.length) {\n return;\n }\n\n const middleware = middlewares[i] as Middleware;\n const result = await middleware(ctx, () => dispatch(i + 1));\n\n // If middleware returned a response, set ctx.res (last write wins)\n if (\n result &&\n typeof result === \"object\" &&\n \"raw\" in result &&\n typeof result.raw === \"string\"\n ) {\n ctx.res = result;\n }\n }\n\n await dispatch(0);\n };\n}\n","import type { Root } from \"@glion/ast\";\nimport { value as queryValue } from \"@glion/util-query\";\nimport { VFile } from \"vfile\";\n\nimport type {\n ConnectionInfo,\n Context,\n Hl7v2Processor,\n Response,\n} from \"./types\";\n\n/**\n * Options for creating a Context instance.\n */\nexport interface CreateContextOptions {\n /** Raw HL7 message string */\n raw: string;\n /** Raw bytes from the MLLP frame payload */\n bytes: Uint8Array;\n /** Connection metadata */\n connection: ConnectionInfo;\n /** HL7v2 unified processor */\n processor: Hl7v2Processor;\n}\n\n/**\n * Create a new Context for an incoming message.\n *\n * Only the **parse** step runs eagerly (sync, fast). Routing fields are\n * extracted as strings from the parsed tree immediately — these cached\n * strings always reflect the original wire values (see ADR-0013).\n *\n * The expensive pipeline stages are lazy:\n *\n * - `ctx.tree()` — triggers `run()` (transformers: annotations, escape decoding,\n * lint). Cached after first call.\n * - `ctx.result()` — triggers `run()` + `stringify()` (compilation, e.g., JSON\n * via hl7v2Jsonify). Cached after first call.\n *\n * If no handler matches or middleware short-circuits before accessing\n * the tree, the transform and compile steps never execute.\n */\nexport function createContext(options: CreateContextOptions): Context {\n const { raw, bytes, connection, processor } = options;\n const variables = new Map<string, unknown>();\n let varSnapshot: Readonly<Record<string, unknown>> | undefined;\n\n // ── Eager: parse (sync, fast) ──────────────────────────────────────\n // VFile carries the input through the pipeline and collects diagnostics.\n const file = new VFile(raw);\n const parsed = processor.parse(file);\n\n // Extract routing fields as strings from the parsed tree.\n // These are cached immediately and always reflect the original\n // wire values, even after transformers enrich the tree in-place.\n const controlId = queryValue(parsed, \"MSH-10\")?.value ?? \"\";\n const messageType = queryValue(parsed, \"MSH-9.1\")?.value ?? \"\";\n const triggerEvent = queryValue(parsed, \"MSH-9.2\")?.value ?? \"\";\n const messageStructure = queryValue(parsed, \"MSH-9.3\")?.value ?? \"\";\n const version = queryValue(parsed, \"MSH-12.1\")?.value ?? \"\";\n\n // ── Lazy: transform (async, cached) ────────────────────────────────\n // run() applies all transformers: annotations, escape decoding, lint.\n // Only executes on first call to ctx.tree() or ctx.result().\n let transformedTree: Root | undefined;\n let transformPromise: Promise<Root> | undefined;\n\n function getTree(): Promise<Root> {\n if (transformedTree) {\n return Promise.resolve(transformedTree);\n }\n if (!transformPromise) {\n transformPromise = (async () => {\n const result = await processor.run(parsed, file);\n transformedTree = result;\n return result;\n })();\n }\n return transformPromise;\n }\n\n // ── Lazy: compile (async, cached) ──────────────────────────────────\n // stringify() compiles the tree into the final format (e.g., JSON).\n // Only executes on first call to ctx.result(). Requires tree first.\n // Promise is cached (including rejections) to match getTree() behavior.\n let resultPromise: Promise<unknown | undefined> | undefined;\n\n function getResult(): Promise<unknown | undefined> {\n if (!resultPromise) {\n resultPromise = (async () => {\n const tree = await getTree();\n return processor.compiler ? processor.stringify(tree, file) : undefined;\n })();\n }\n return resultPromise;\n }\n\n // ── Build context ──────────────────────────────────────────────────\n const ctx: Context = {\n ast: parsed,\n connection: Object.freeze(connection),\n controlId,\n file,\n get<K extends string>(key: K): unknown {\n return variables.get(key);\n },\n messageStructure,\n messageType,\n req: Object.freeze({ bytes, raw }),\n res: undefined as Response | undefined,\n result: getResult,\n set<K extends string>(key: K, value: unknown): void {\n variables.set(key, value);\n varSnapshot = undefined;\n },\n tree: getTree,\n triggerEvent,\n get var(): Readonly<Record<string, unknown>> {\n if (!varSnapshot) {\n varSnapshot = Object.freeze(Object.fromEntries(variables));\n }\n return varSnapshot;\n },\n version,\n };\n\n return ctx;\n}\n","import type { Root } from \"@glion/ast\";\nimport type { Processor } from \"unified\";\nimport type { VFile } from \"vfile\";\n\n/**\n * Connection metadata for an MLLP socket.\n */\nexport interface ConnectionInfo {\n readonly id: number;\n readonly remoteAddress: string;\n readonly remotePort: number;\n readonly localPort: number;\n readonly secure: boolean;\n readonly state: Map<string, unknown>;\n}\n\n/**\n * MLLP response — a raw HL7v2 string that the server encodes\n * with MLLP framing before sending back over TCP.\n *\n * Acknowledgment helpers (ack/nak) are not built in — register\n * an acknowledgment middleware via `.use()` to generate ACK/NAK responses.\n */\nexport interface Response {\n readonly raw: string;\n}\n\n/**\n * The MLLP context object.\n *\n * Routing fields (messageType, triggerEvent, etc.) are extracted from\n * the **pre-transform** parsed tree and are always available synchronously.\n *\n * The transformed tree, VFile, and compiled result are lazy — they\n * trigger pipeline stages on first access. See ADR-0013.\n */\nexport interface Context {\n /** Incoming message data */\n readonly req: {\n /** Original HL7 message string (without MLLP framing) */\n readonly raw: string;\n /** Raw bytes from MLLP frame payload */\n readonly bytes: Uint8Array;\n };\n\n /** TCP connection metadata */\n readonly connection: ConnectionInfo;\n\n /**\n * Parsed AST — always available synchronously. Use this for reading\n * MSH fields, building ACKs, routing, or any operation that doesn't\n * need to wait for transforms to complete.\n *\n * Note: this is the same object reference as the tree returned by\n * `tree()`. After `tree()` resolves, transformers may have enriched\n * it in-place (e.g., decoded escapes, resolved message structure).\n * The routing fields (messageType, triggerEvent, etc.) are cached\n * as separate string properties and always reflect the original\n * wire values regardless of transforms.\n */\n readonly ast: Root;\n\n /**\n * Transformed AST. Triggers `run()` (transformers) on first call.\n * Cached after first access — subsequent calls return the same tree.\n */\n tree(): Promise<Root>;\n\n /**\n * VFile with diagnostics and lint messages. Created during context\n * creation. Diagnostics accumulate after `tree()` triggers transformers.\n */\n file: VFile;\n\n /**\n * Compiled result from the processor's compiler (e.g., JSON from\n * hl7v2Jsonify). Triggers both `run()` and `stringify()` on first call.\n * Undefined when the processor has no compiler.\n * Cached after first access.\n */\n result(): Promise<unknown | undefined>;\n\n /** Response to send back. Set by middleware or handler. */\n res: Response | undefined;\n\n /** MSH-9.1 message type (e.g., \"ADT\") — from pre-transform parse */\n messageType: string;\n /** MSH-9.2 trigger event (e.g., \"A01\") — from pre-transform parse */\n triggerEvent: string;\n /** MSH-9.3 message structure (e.g., \"ADT_A01\") — from pre-transform parse */\n messageStructure: string;\n /** MSH-12 version (e.g., \"2.5.1\") — from pre-transform parse */\n version: string;\n /** MSH-10 message control ID — from pre-transform parse */\n controlId: string;\n\n /** Store a variable */\n set<K extends string>(key: K, value: unknown): void;\n /** Retrieve a variable */\n get<K extends string>(key: K): unknown;\n /** Read-only snapshot of all variables */\n readonly var: Readonly<Record<string, unknown>>;\n}\n\n/**\n * A unified `Processor` that parses HL7v2 messages into `Root` trees.\n *\n * Only `ParseTree` is constrained — the remaining type params are left\n * loose so that any processor built with `unified().use(hl7v2Parser)`\n * is assignable regardless of which transformer/compiler plugins are added.\n */\nexport type Hl7v2Processor = Processor<Root, Root, Root>;\n\n/**\n * Middleware function signature (Hono/Koa onion model).\n */\nexport type MiddlewareReturn =\n // oxlint-disable-next-line typescript/no-invalid-void-type\n | Promise<Response | undefined | void>\n | Promise<void>\n | Response\n | undefined\n // oxlint-disable-next-line typescript/no-invalid-void-type\n | void;\n\nexport type Middleware = (\n ctx: Context,\n next: () => Promise<void>\n) => MiddlewareReturn;\n\n/**\n * Terminal route handler — may return a response or void (no response).\n */\nexport type Handler = (\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n) => Promise<Response | undefined | void> | Response | undefined | void;\n\n/**\n * Error handler — receives the error and context, returns a response.\n */\nexport type ErrorHandler = (\n err: Error,\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n) => Promise<Response | undefined | void> | Response | undefined | void;\n\n/**\n * A filter function for route or middleware matching.\n * Receives the full context and returns true if the route/middleware applies.\n */\nexport type RouteFilter = (ctx: Context) => boolean;\n\n/**\n * Parsed route pattern for matching against incoming messages.\n */\nexport interface RoutePattern {\n /** Original pattern string (e.g., \"ADT^A01\") */\n readonly source: string;\n /** Message type to match, or \"*\" for wildcard */\n readonly messageType: string;\n /** Trigger event to match, or \"*\" for wildcard */\n readonly triggerEvent: string;\n}\n\n/**\n * Parse a pattern string like \"ADT^A01\", \"ADT^*\", \"*^A01\", or \"*\"\n * into a structured RoutePattern.\n */\nexport function parsePattern(pattern: string): RoutePattern {\n if (pattern === \"*\") {\n return { messageType: \"*\", source: pattern, triggerEvent: \"*\" };\n }\n\n const caretIndex = pattern.indexOf(\"^\");\n if (caretIndex === -1) {\n return { messageType: pattern, source: pattern, triggerEvent: \"*\" };\n }\n\n return {\n messageType: pattern.slice(0, caretIndex) || \"*\",\n source: pattern,\n triggerEvent: pattern.slice(caretIndex + 1) || \"*\",\n };\n}\n\n/**\n * Test whether a route pattern matches a given message type and trigger event.\n */\nexport function matchPattern(\n pattern: RoutePattern,\n messageType: string,\n triggerEvent: string\n): boolean {\n const typeMatch =\n pattern.messageType === \"*\" || pattern.messageType === messageType;\n const eventMatch =\n pattern.triggerEvent === \"*\" || pattern.triggerEvent === triggerEvent;\n return typeMatch && eventMatch;\n}\n","import { matchPattern, parsePattern } from \"./types\";\nimport type { Context, Handler, Middleware, RouteFilter } from \"./types\";\n\n/**\n * A route entry: matcher + handler.\n */\ninterface Route {\n filter: (ctx: Context) => boolean;\n handler: Handler;\n}\n\n/**\n * A scoped middleware entry: matcher + middleware.\n */\ninterface ScopedMiddleware {\n filter: (ctx: Context) => boolean;\n middleware: Middleware;\n}\n\n/**\n * Result of matching a message against registered routes and middleware.\n */\nexport interface MatchResult {\n /** The matched handler, or undefined if no route matches */\n handler: Handler | undefined;\n /** Middleware that applies to this message (global + scoped matches) */\n middlewares: Middleware[];\n}\n\n/**\n * Create a matcher function from a string pattern.\n */\nfunction patternMatcher(pattern: string): (ctx: Context) => boolean {\n const parsed = parsePattern(pattern);\n return (ctx) => matchPattern(parsed, ctx.messageType, ctx.triggerEvent);\n}\n\n/**\n * Pattern-matching router for HL7v2 message types.\n *\n * Routes are matched in registration order (first match wins).\n * Supports string patterns with wildcards or filter functions.\n *\n * String patterns:\n *\n * - `\"ADT^A01\"` — exact match\n * - `\"ADT^*\"` — any ADT message\n * - `\"*\"` — catch-all\n *\n * Filter functions:\n *\n * - `(ctx) => ctx.messageType === \"ADT\" && ctx.version === \"2.5.1\"`\n */\nexport class Router {\n readonly #routes: Route[] = [];\n readonly #globalMiddleware: Middleware[] = [];\n readonly #scopedMiddleware: ScopedMiddleware[] = [];\n\n /**\n * Register a route handler with a string pattern or filter function.\n */\n add(patternOrFilter: string | RouteFilter, handler: Handler): void {\n const filter =\n typeof patternOrFilter === \"string\"\n ? patternMatcher(patternOrFilter)\n : patternOrFilter;\n\n this.#routes.push({ filter, handler });\n }\n\n /**\n * Register global middleware (runs for all messages).\n * When `prepend` is true, the middleware is inserted at the front\n * of the chain so it executes outermost (first in, last out).\n */\n addMiddleware(middleware: Middleware, prepend?: boolean): void;\n /**\n * Register scoped middleware with a string pattern or filter function.\n */\n addMiddleware(\n patternOrFilter: string | RouteFilter,\n middleware: Middleware\n ): void;\n addMiddleware(\n patternOrFilterOrMiddleware: string | RouteFilter | Middleware,\n middlewareOrPrepend?: Middleware | boolean\n ): void {\n // Global middleware: addMiddleware(fn) or addMiddleware(fn, true)\n if (\n typeof patternOrFilterOrMiddleware === \"function\" &&\n (middlewareOrPrepend === undefined ||\n typeof middlewareOrPrepend === \"boolean\")\n ) {\n if (middlewareOrPrepend === true) {\n this.#globalMiddleware.unshift(\n patternOrFilterOrMiddleware as Middleware\n );\n } else {\n this.#globalMiddleware.push(patternOrFilterOrMiddleware as Middleware);\n }\n return;\n }\n\n // Scoped middleware: addMiddleware(pattern/filter, fn)\n if (typeof middlewareOrPrepend === \"function\") {\n const middleware = middlewareOrPrepend;\n const filter =\n typeof patternOrFilterOrMiddleware === \"string\"\n ? patternMatcher(patternOrFilterOrMiddleware)\n : (patternOrFilterOrMiddleware as RouteFilter);\n\n this.#scopedMiddleware.push({ filter, middleware });\n }\n }\n\n /**\n * Match a context against registered routes and middleware.\n * Returns the first matching handler and all applicable middleware.\n */\n match(ctx: Context): MatchResult {\n // Find first matching route (registration order)\n let handler: Handler | undefined;\n for (const route of this.#routes) {\n if (route.filter(ctx)) {\n handler = route.handler;\n break;\n }\n }\n\n // Collect applicable middleware: global + matching scoped\n const middlewares: Middleware[] = [...this.#globalMiddleware];\n for (const scoped of this.#scopedMiddleware) {\n if (scoped.filter(ctx)) {\n middlewares.push(scoped.middleware);\n }\n }\n\n return { handler, middlewares };\n }\n}\n","import { MllpError } from \"@glion/mllp-transport\";\n\nimport { compose } from \"./compose\";\nimport { createContext } from \"./context\";\nimport { Router } from \"./router\";\nimport type {\n ConnectionInfo,\n Context,\n ErrorHandler,\n Handler,\n Hl7v2Processor,\n Middleware,\n Response,\n RouteFilter,\n} from \"./types\";\n\n/**\n * Routing fields from the message that caused an error.\n * Attached to errors thrown by `Mllp.handle()` so that callers\n * (e.g., `serve()`) can include message context in error callbacks.\n */\nexport interface MessageInfo {\n readonly messageType: string;\n readonly triggerEvent: string;\n readonly messageStructure: string;\n readonly version: string;\n readonly controlId: string;\n}\n\n/**\n * Internal store for associating message routing info with errors.\n * Uses a WeakMap so error objects can be garbage-collected normally.\n */\nconst errorMessageInfo = new WeakMap<Error, MessageInfo>();\n\n/**\n * Retrieve the {@link MessageInfo} associated with an error thrown\n * by `Mllp.handle()`. Returns `undefined` for errors that did not\n * originate from message processing (e.g., lifecycle callback errors).\n */\nexport function getMessageInfo(error: Error): MessageInfo | undefined {\n return errorMessageInfo.get(error);\n}\n\n/**\n * MLLP application for HL7v2 messaging.\n *\n * A pure routing and middleware engine with no TCP/server concerns.\n * Use `serve()` to bind this to a TCP server.\n *\n * @example\n * ```typescript\n * import { Mllp } from \"@glion/mllp\";\n * import { parseHL7v2 } from \"@glion/hl7v2\";\n *\n * const app = new Mllp().parser(parseHL7v2).on(\"ADT^A01\", async (ctx) => {\n * const tree = await ctx.tree();\n * return { raw: \"...\" };\n * });\n * ```;\n */\nexport class Mllp {\n readonly #router = new Router();\n #processor: Hl7v2Processor | undefined;\n #errorHandler: ErrorHandler | undefined;\n\n /**\n * Register an HL7v2 processor for incoming messages.\n *\n * Accepts a unified `Processor` — either a pre-built one like\n * `parseHL7v2` from `@glion/hl7v2`, or a custom composition\n * via `unified().use(hl7v2Parser).use(...)`.\n *\n * Must be called before `handle()`. Calling multiple times replaces\n * the previous processor (last-write-wins).\n */\n parser(processor: Hl7v2Processor): this {\n this.#processor = processor;\n return this;\n }\n\n /**\n * Register middleware.\n *\n * Accepts:\n *\n * - A middleware function: `(ctx, next) => { ... }`\n * - A scoped middleware: `app.use('ADT^*', middleware)` or `app.use(filter,\n * middleware)`\n */\n use(middleware: Middleware, options?: { prepend?: boolean }): this;\n use(patternOrFilter: string | RouteFilter, middleware: Middleware): this;\n use(\n first: string | RouteFilter | Middleware,\n second?: Middleware | { prepend?: boolean }\n ): this {\n // Scoped middleware: use('ADT^*', middleware) or use(filter, middleware)\n if (\n (typeof first === \"string\" || typeof first === \"function\") &&\n typeof second === \"function\"\n ) {\n this.#router.addMiddleware(first as string | RouteFilter, second);\n return this;\n }\n\n // Standard middleware: use((ctx, next) => { ... })\n if (typeof first === \"function\") {\n const prepend =\n second && typeof second === \"object\" ? second.prepend : false;\n this.#router.addMiddleware(first as Middleware, prepend);\n return this;\n }\n\n return this;\n }\n\n /**\n * Register a route handler for a message type pattern or filter function.\n *\n * String patterns:\n *\n * - `\"ADT^A01\"` — exact match\n * - `\"ADT^*\"` — any ADT message\n * - `\"*^A01\"` — any type with A01 trigger\n * - `\"*\"` — catch-all\n *\n * Filter functions:\n *\n * - `(ctx) => ctx.messageType === \"ADT\" && ctx.version === \"2.5.1\"`\n */\n on(patternOrFilter: string | RouteFilter, handler: Handler): this {\n this.#router.add(patternOrFilter, handler);\n return this;\n }\n\n /**\n * Register a global error handler.\n *\n * Called when middleware or a handler throws. Without an error handler,\n * the error is re-thrown to the caller (e.g. `serve()`).\n *\n * If the error handler returns a response, it is used as the reply.\n * If the error handler itself throws, the new error is re-thrown.\n */\n onError(handler: ErrorHandler): this {\n this.#errorHandler = handler;\n return this;\n }\n\n /**\n * Process a raw HL7v2 message through the middleware chain and router.\n * This is the integration point — analogous to Hono's `fetch()`.\n *\n * The pipeline is lazy (see ADR-0013):\n * 1. Parse (sync, fast) — always runs, extracts routing fields\n * 2. Route match — uses pre-transform routing fields\n * 3. If no match → return undefined (no transform/compile cost)\n * 4. Transform/compile — only when handlers access ctx.tree()/ctx.result()\n *\n * Throws `MllpError` if no processor has been registered via `app.parser()`.\n */\n async handle(\n raw: string,\n bytes: Uint8Array,\n connection: ConnectionInfo\n // oxlint-disable-next-line typescript/no-invalid-void-type\n ): Promise<Response | undefined | void> {\n if (!this.#processor) {\n throw new MllpError(\n \"ERR_NO_PARSER\",\n \"No parser registered. Call app.parser() before handling messages.\"\n );\n }\n\n // Context creation is sync — only parse() runs here.\n // Transform and compile are deferred to ctx.tree() / ctx.result().\n const ctx = createContext({\n bytes,\n connection,\n processor: this.#processor,\n raw,\n });\n\n try {\n // Match route and collect middleware\n const match = this.#router.match(ctx);\n const middlewares = [...match.middlewares];\n\n // Add the handler as the terminal middleware\n if (match.handler) {\n const handler = match.handler;\n middlewares.push((handlerCtx: Context) => handler(handlerCtx));\n }\n\n // Compose and execute\n await compose(middlewares)(ctx);\n\n return ctx.res;\n } catch (error) {\n return this.#handleError(\n error instanceof Error ? error : new Error(String(error)),\n ctx\n );\n }\n }\n\n /**\n * Handle an error during message processing.\n *\n * If a custom error handler is registered, delegates to it.\n * If the error handler itself throws, re-throws the error handler's error.\n * If no error handler is registered, re-throws the original error.\n *\n * In both re-throw cases, message routing fields are attached to the\n * error via {@link getMessageInfo} so callers can include message\n * context in error reporting.\n *\n * Callers (e.g. `serve()`) are responsible for catching these errors\n * and deciding how to handle them at the transport level.\n */\n async #handleError(\n err: Error,\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n ): Promise<Response | undefined | void> {\n const info: MessageInfo = {\n controlId: ctx.controlId,\n messageStructure: ctx.messageStructure,\n messageType: ctx.messageType,\n triggerEvent: ctx.triggerEvent,\n version: ctx.version,\n };\n\n if (this.#errorHandler) {\n try {\n return await this.#errorHandler(err, ctx);\n } catch (handlerError) {\n const e =\n handlerError instanceof Error\n ? handlerError\n : new Error(String(handlerError));\n errorMessageInfo.set(e, info);\n throw e;\n }\n }\n\n errorMessageInfo.set(err, info);\n throw err;\n }\n}\n"],"mappings":"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"}
|
package/dist/node/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/node/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/node/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAC/B,UAAU,EAAE,cAAc,KACvB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,cAAc,EAC1B,WAAW,EAAE,WAAW,GAAG,SAAS,KACjC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAE/B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAElC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,GAAG,CAAC,EAAE;QACJ,sEAAsE;QACtE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACrB,8BAA8B;QAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QACtB,kDAAkD;QAClD,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;QACrB,+CAA+C;QAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAElC;;;OAGG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CA+B9D"}
|
package/dist/node.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as getMessageInfo } from "./mllp.js";
|
|
2
|
+
import { createDecoderStream, encode } from "@glion/mllp-transport";
|
|
2
3
|
import { once } from "node:events";
|
|
3
4
|
import { createServer } from "node:net";
|
|
4
5
|
import { Readable } from "node:stream";
|
|
@@ -169,6 +170,17 @@ function nodeAdapter(options) {
|
|
|
169
170
|
}
|
|
170
171
|
//#endregion
|
|
171
172
|
//#region src/node/serve.ts
|
|
173
|
+
/**
|
|
174
|
+
* Node.js MLLP server entry point.
|
|
175
|
+
*
|
|
176
|
+
* Binds an {@link Mllp} application to a TCP (or TLS) socket using the
|
|
177
|
+
* Node.js `net` / `tls` modules. Incoming bytes are decoded from the MLLP
|
|
178
|
+
* framing protocol, dispatched through the application's middleware stack,
|
|
179
|
+
* and the resulting acknowledgements are encoded and written back to the
|
|
180
|
+
* client.
|
|
181
|
+
*
|
|
182
|
+
* @module
|
|
183
|
+
*/
|
|
172
184
|
/** Monotonically-increasing connection ID counter. */
|
|
173
185
|
let nextConnectionId = 1;
|
|
174
186
|
/**
|
package/dist/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.js","names":["createTlsServer"],"sources":["../src/node/adapter.ts","../src/node/serve.ts"],"sourcesContent":["/**\n * Node.js TCP/TLS adapter for the MLLP server.\n *\n * Implements the {@link TcpAdapter} interface using Node.js built-in\n * `net` and `tls` modules. Incoming `net.Socket` instances are wrapped\n * into Web Streams-based {@link AdapterSocket} objects so that the rest\n * of the MLLP stack can work with a platform-agnostic streaming API.\n *\n * This module is internal to the `@glion/mllp` package and\n * is consumed by {@link serve} in `./serve.ts`.\n *\n * @module\n */\n\nimport { once } from \"node:events\";\nimport type { Server, Socket } from \"node:net\";\nimport { createServer } from \"node:net\";\nimport { Readable } from \"node:stream\";\nimport { createServer as createTlsServer } from \"node:tls\";\nimport { promisify } from \"node:util\";\n\nimport type {\n AdapterSocket,\n ConnectionHandler,\n ListenOptions,\n TcpAdapter,\n TcpHandle,\n} from \"../server/adapter\";\n\n/**\n * Options specific to the Node.js adapter that control per-socket behaviour.\n *\n * @internal\n */\nexport interface NodeAdapterOptions {\n /**\n * Socket inactivity timeout in milliseconds.\n *\n * When set to a value greater than `0`, sockets that remain idle for\n * longer than this duration are automatically destroyed. Set to `0`\n * (the default) to disable idle timeouts entirely.\n *\n * @default 0\n */\n socketTimeout?: number;\n\n /**\n * Whether to enable TCP keep-alive probes on accepted sockets.\n *\n * When `true` the operating system will periodically send keep-alive\n * packets to detect dead peers and prevent intermediate network\n * equipment from dropping idle connections.\n *\n * @default true\n */\n keepAlive?: boolean;\n\n /**\n * The initial delay in milliseconds before the first TCP keep-alive\n * probe is sent on an idle socket. Only meaningful when\n * {@link keepAlive} is `true`.\n *\n * @default 60000\n */\n keepAliveInitialDelay?: number;\n}\n\n/**\n * Write a chunk to a Node.js socket, waiting for backpressure to clear.\n *\n * `socket.write()` returns `false` when the internal buffer is full,\n * indicating that the caller should wait for the `\"drain\"` event before\n * writing again. This helper wraps that pattern in a promise so it can\n * be used with `await`.\n *\n * @param socket - The Node.js TCP socket to write to.\n * @param chunk - The bytes to write.\n * @returns A promise that resolves once the chunk has been flushed or the\n * socket's internal buffer has drained.\n */\nasync function writeToSocket(socket: Socket, chunk: Uint8Array): Promise<void> {\n const ok = socket.write(chunk);\n if (!ok) {\n await once(socket, \"drain\");\n }\n}\n\n/**\n * Wrap a Node.js `net.Socket` into a platform-agnostic {@link AdapterSocket}.\n *\n * The readable side is created via `Readable.toWeb()` which converts the\n * Node.js stream into a WHATWG `ReadableStream<Uint8Array>`. The writable\n * side is a custom `WritableStream` that delegates writes to\n * {@link writeToSocket} and handles `close` / `abort` by ending or\n * destroying the underlying socket.\n *\n * @param socket - The raw Node.js socket to wrap.\n * @param secure - Whether this socket was accepted over TLS.\n * @returns An {@link AdapterSocket} exposing Web Streams and connection\n * metadata.\n */\nfunction wrapNodeSocket(socket: Socket, secure: boolean): AdapterSocket {\n const readable = Readable.toWeb(socket) as ReadableStream<Uint8Array>;\n\n const writable = new WritableStream<Uint8Array>({\n abort() {\n socket.destroy();\n },\n close() {\n socket.end();\n },\n async write(chunk) {\n await writeToSocket(socket, chunk);\n },\n });\n\n return {\n close() {\n socket.destroy();\n },\n get localPort() {\n return socket.localPort ?? 0;\n },\n readable,\n get remoteAddress() {\n return socket.remoteAddress ?? \"unknown\";\n },\n get remotePort() {\n return socket.remotePort ?? 0;\n },\n secure,\n writable,\n };\n}\n\n/**\n * Create a Node.js {@link TcpAdapter} that can listen for TCP or TLS\n * connections using the built-in `net` and `tls` modules.\n *\n * When the returned adapter's `listen()` method is called with a `tls`\n * option, a TLS server (`tls.createServer`) is used; otherwise a plain\n * TCP server (`net.createServer`) is created.\n *\n * Each accepted socket is configured via {@link configureSocket} (timeouts,\n * keep-alive, Nagle disabled) and then wrapped into an {@link AdapterSocket}\n * before being handed to the provided {@link ConnectionHandler}.\n *\n * @example\n * ```typescript\n * const adapter = nodeAdapter({ socketTimeout: 30_000 });\n * const handle = adapter.listen({ port: 2575 }, (socket) => {\n * // handle connection\n * });\n * ```;\n *\n * @param options - Optional per-socket configuration (timeouts, keep-alive).\n * @returns A {@link TcpAdapter} ready to bind to a port.\n * @internal\n */\nexport function nodeAdapter(options?: NodeAdapterOptions): TcpAdapter {\n const {\n socketTimeout = 0,\n keepAlive = true,\n keepAliveInitialDelay = 60_000,\n } = options ?? {};\n\n /**\n * Apply common socket options to an accepted connection.\n *\n * Configures the following on the raw `net.Socket`:\n *\n * - **Idle timeout** -- If `socketTimeout > 0`, sets `setTimeout()` and\n * destroys the socket on the `\"timeout\"` event.\n * - **TCP keep-alive** -- Enables keep-alive probes with the configured initial\n * delay when `keepAlive` is `true`.\n * - **Nagle disabled** -- Calls `setNoDelay(true)` so that small MLLP frames\n * are sent immediately without buffering.\n *\n * @param socket - The raw Node.js socket to configure.\n */\n function configureSocket(socket: Socket): void {\n if (socketTimeout > 0) {\n socket.setTimeout(socketTimeout);\n socket.on(\"timeout\", () => socket.destroy());\n }\n if (keepAlive) {\n socket.setKeepAlive(true, keepAliveInitialDelay);\n }\n socket.setNoDelay(true);\n }\n\n return {\n listen(listenOpts: ListenOptions, handler: ConnectionHandler): TcpHandle {\n const onConnection = (socket: Socket) => {\n configureSocket(socket);\n const secure = !!listenOpts.tls;\n handler(wrapNodeSocket(socket, secure));\n };\n\n const server: Server = listenOpts.tls\n ? createTlsServer(\n {\n ca: listenOpts.tls.ca,\n cert: listenOpts.tls.cert,\n key: listenOpts.tls.key,\n passphrase: listenOpts.tls.passphrase,\n },\n onConnection\n )\n : createServer(onConnection);\n\n let resolveListening!: (value: true) => void;\n let rejectListening!: (reason: unknown) => void;\n // oxlint-disable-next-line promise/avoid-new -- Node 18 compat (no Promise.withResolvers)\n const listeningInternal = new Promise<true>((resolve, reject) => {\n resolveListening = resolve;\n rejectListening = reject;\n });\n // Public type is Promise<void>; the sentinel return value is an\n // implementation detail never observed by consumers.\n const listening = listeningInternal as Promise<unknown> as Promise<void>;\n\n server.once(\"listening\", () => {\n resolveListening(true);\n });\n // Persistent error listener: server errors that fire before\n // `listening` reject the startup promise; later errors (TLS,\n // shutdown) propagate via the already-returned handle's lifecycle\n // rather than crashing the process via an unhandled emitter error.\n let listenFailed = false;\n server.on(\"error\", (err) => {\n if (!listenFailed) {\n listenFailed = true;\n rejectListening(err);\n // Close the server handle so the OS descriptor is released\n // even if the caller never awaits `listening` again.\n server.close();\n }\n });\n\n server.listen(listenOpts.port, listenOpts.hostname, listenOpts.backlog);\n\n const closeServer = promisify(server.close.bind(server));\n\n return {\n async close() {\n await closeServer();\n },\n listening,\n get port() {\n const addr = server.address();\n return addr && typeof addr === \"object\" ? addr.port : listenOpts.port;\n },\n };\n },\n };\n}\n","/**\n * Node.js MLLP server entry point.\n *\n * Binds an {@link Mllp} application to a TCP (or TLS) socket using the\n * Node.js `net` / `tls` modules. Incoming bytes are decoded from the MLLP\n * framing protocol, dispatched through the application's middleware stack,\n * and the resulting acknowledgements are encoded and written back to the\n * client.\n *\n * @module\n */\n\nimport type { AdapterSocket } from \"../server/adapter\";\nimport type { MessageInfo, Mllp } from \"../server/mllp\";\nimport { getMessageInfo } from \"../server/mllp\";\nimport type { ConnectionInfo } from \"../server/types\";\nimport { createDecoderStream } from \"../transport/decoder-stream\";\nimport { encode } from \"../transport/encoder\";\nimport { nodeAdapter } from \"./adapter\";\n\n/** Monotonically-increasing connection ID counter. */\n// oxlint-disable-next-line prefer-const\nlet nextConnectionId = 1;\n\n/**\n * Lifecycle callback invoked for connection events.\n */\nexport type ConnectionCallback = (\n connection: ConnectionInfo\n) => void | Promise<void>;\n\n/**\n * Error callback invoked when a message handler error is unhandled.\n * Receives the error and the connection it occurred on.\n *\n * For handler errors, `messageInfo` contains the routing fields\n * (messageType, triggerEvent, controlId, version) of the message that\n * caused the error — useful for audit logging in healthcare contexts.\n * For lifecycle callback errors (onConnect/onDisconnect), `messageInfo`\n * is `undefined`.\n *\n * This fires only for application-level errors (handler/middleware throws\n * with no app-level error handler, or app-level error handler itself throws).\n * Transport-level errors (connection reset, decode failures) do not trigger\n * this callback.\n */\nexport type ErrorCallback = (\n error: Error,\n connection: ConnectionInfo,\n messageInfo: MessageInfo | undefined\n) => void | Promise<void>;\n\n/**\n * Options for starting an MLLP server with {@link serve}.\n */\nexport interface ServeOptions {\n /**\n * The hostname or IP address to bind the server to.\n * When omitted the OS will bind to all available interfaces (`0.0.0.0`).\n */\n hostname?: string;\n\n /**\n * Whether to enable TCP keep-alive on accepted sockets.\n * Defaults to the adapter default (`true`).\n */\n keepAlive?: boolean;\n\n /**\n * The initial delay in milliseconds before the first TCP keep-alive probe\n * is sent on an idle socket. Only meaningful when `keepAlive` is `true`.\n * Defaults to the adapter default (`60 000`).\n */\n keepAliveInitialDelay?: number;\n\n /**\n * Called when a new TCP connection is accepted. Receives the connection\n * metadata including the unique connection ID and mutable state map.\n *\n * If this callback throws, the connection is torn down and\n * `onDisconnect` is still called.\n */\n onConnect?: ConnectionCallback;\n\n /**\n * Called when a TCP connection is closed (including force-close).\n * Always fires, even if `onConnect` threw.\n *\n * If this callback throws, the error is routed to `onError`\n * (or `console.error` as last resort).\n */\n onDisconnect?: ConnectionCallback;\n\n /**\n * Called when a message handler error is unhandled — either no app-level\n * error handler is registered, or the app-level error handler itself\n * threw. Also called when lifecycle callbacks (`onConnect`,\n * `onDisconnect`) throw.\n *\n * Only logs `error.message` and connection ID to avoid leaking PHI\n * in the `console.error` fallback. Production deployments should\n * always provide this callback.\n */\n onError?: ErrorCallback;\n\n /**\n * The TCP port number to listen on.\n */\n port: number;\n\n /**\n * Socket inactivity timeout in milliseconds. A socket that has been idle\n * for longer than this value will be destroyed. Set to `0` (the default)\n * to disable the timeout.\n */\n socketTimeout?: number;\n\n /**\n * TLS configuration. When provided the server will create a TLS socket\n * instead of a plain TCP socket.\n */\n tls?: {\n /** Optional CA certificate(s) for client certificate verification. */\n ca?: string | Buffer;\n /** The server certificate. */\n cert: string | Buffer;\n /** The private key for the server certificate. */\n key: string | Buffer;\n /** Optional passphrase for the private key. */\n passphrase?: string;\n };\n}\n\n/**\n * A running MLLP server handle returned by {@link serve}.\n */\nexport interface Server {\n /** The port the server is currently listening on. */\n readonly port: number;\n\n /**\n * Resolves when the underlying TCP server has bound and started listening.\n * Await this before attempting to connect to ensure the port is ready.\n */\n readonly listening: Promise<void>;\n\n /**\n * Gracefully close the server. No new connections will be accepted and the\n * returned promise resolves once all underlying resources are released.\n */\n close(): Promise<void>;\n}\n\n/**\n * Start an MLLP server bound to a TCP (or TLS) port.\n *\n * The server accepts incoming connections, decodes MLLP-framed HL7v2\n * messages, passes them through the provided {@link Mllp} application, and\n * writes back any response (typically an ACK/NAK) using MLLP framing.\n *\n * @example\n * ```typescript\n * import { Mllp } from \"@glion/mllp\";\n * import { parseHL7v2 } from \"@glion/hl7v2\";\n * import { serve } from \"@glion/mllp/node\";\n *\n * const app = new Mllp()\n * .parser(parseHL7v2)\n * .on(\"ADT^A01\", async (ctx) => ({ raw: \"...\" }));\n *\n * const server = serve(app, { port: 2575 });\n * console.log(`Listening on port ${server.port}`);\n * ```\n *\n * @param app - The {@link Mllp} application that will handle each message.\n * @param options - Server configuration (port, TLS, timeouts, etc.).\n * @returns A {@link Server} handle that exposes the bound port and a `close()`\n * method for graceful shutdown.\n */\nexport function serve(app: Mllp, options: ServeOptions): Server {\n const adapter = nodeAdapter({\n keepAlive: options.keepAlive,\n keepAliveInitialDelay: options.keepAliveInitialDelay,\n socketTimeout: options.socketTimeout,\n });\n\n const lifecycle: LifecycleOptions = {\n onConnect: options.onConnect,\n onDisconnect: options.onDisconnect,\n onError: options.onError,\n };\n\n const handle = adapter.listen(\n {\n hostname: options.hostname,\n port: options.port,\n tls: options.tls,\n },\n (socket) => handleConnection(app, socket, lifecycle)\n );\n\n return {\n async close() {\n await handle.close();\n },\n listening: handle.listening,\n get port() {\n return handle.port;\n },\n };\n}\n\n/** Lifecycle callback options extracted from ServeOptions. */\ninterface LifecycleOptions {\n onConnect?: ConnectionCallback;\n onDisconnect?: ConnectionCallback;\n onError?: ErrorCallback;\n}\n\n/**\n * Route an error to the `onError` callback, falling back to a safe\n * `console.error` that logs only the error message and connection ID\n * to avoid leaking PHI from HL7v2 message content.\n */\nasync function reportError(\n error: unknown,\n connection: ConnectionInfo,\n lifecycle: LifecycleOptions,\n messageInfo?: MessageInfo\n): Promise<void> {\n const err = error instanceof Error ? error : new Error(String(error));\n try {\n if (lifecycle.onError) {\n await lifecycle.onError(err, connection, messageInfo);\n } else {\n // Safe fallback: only message + connection ID, no PHI\n console.error(\n `[mllp] Unhandled error on connection ${connection.id}: ${err.message}`\n );\n }\n } catch {\n // onError itself threw — last resort\n console.error(\n `[mllp] Unhandled error on connection ${connection.id}: ${err.message}`\n );\n }\n}\n\n/**\n * Handle a single MLLP connection.\n *\n * Sets up a decode-handle-encode loop for the lifetime of the socket:\n *\n * 1. **Decode** — Raw bytes from the socket's readable stream are piped through an\n * MLLP decoder (`TransformStream`) that strips the MLLP start/end block\n * characters and emits complete HL7v2 messages.\n * 2. **Handle** — Each decoded message is passed to the {@link Mllp} application\n * which runs its middleware stack and returns an optional response (e.g. an\n * ACK or NAK).\n * 3. **Encode** — If the application produced a response, the raw response bytes\n * are MLLP-encoded (wrapped in start/end block characters) and written back\n * to the socket.\n *\n * The loop runs until the remote end closes the connection or an\n * unrecoverable error occurs. Stream locks are released in a `finally`\n * block to prevent resource leaks.\n *\n * Lifecycle callbacks fire at connection boundaries:\n *\n * - `onConnect` after the connection is established\n * - `onDisconnect` when the connection closes (always fires)\n * - `onError` when a handler error is unhandled (not for transport errors)\n *\n * @param app - The MLLP application to dispatch messages to.\n * @param socket - The adapter socket wrapping the underlying TCP connection.\n * @param lifecycle - Optional lifecycle callbacks from ServeOptions.\n */\nfunction handleConnection(\n app: Mllp,\n socket: AdapterSocket,\n lifecycle: LifecycleOptions\n): void {\n const decoder = createDecoderStream();\n const reader = socket.readable.pipeThrough(decoder).getReader();\n const writer = socket.writable.getWriter();\n\n const connection: ConnectionInfo = {\n id: nextConnectionId++,\n localPort: socket.localPort,\n remoteAddress: socket.remoteAddress,\n remotePort: socket.remotePort,\n secure: socket.secure,\n state: new Map(),\n };\n\n const processMessages = async () => {\n try {\n // ── onConnect ──────────────────────────────────────────────────\n try {\n await lifecycle.onConnect?.(connection);\n } catch (connectError) {\n await reportError(connectError, connection, lifecycle);\n // Tear down — onDisconnect still fires in the outer finally\n return;\n }\n\n // ── Message loop ───────────────────────────────────────────────\n try {\n while (true) {\n const { done, value: message } = await reader.read();\n if (done) {\n break;\n }\n\n // Inner try/catch separates handler errors from stream errors.\n // Only handler errors route to onError; stream errors (connection\n // reset, decode failure) flow to the outer catch for cleanup.\n let response: Awaited<ReturnType<Mllp[\"handle\"]>>;\n try {\n response = await app.handle(message.text, message.data, connection);\n } catch (handlerError) {\n const err =\n handlerError instanceof Error\n ? handlerError\n : new Error(String(handlerError));\n await reportError(err, connection, lifecycle, getMessageInfo(err));\n // Continue processing — connection stays alive\n continue;\n }\n\n // Write is outside the handler error catch — write failures\n // are transport errors and flow to the outer catch.\n if (response) {\n await writer.write(encode(response.raw));\n }\n }\n } catch {\n // Transport-level: connection closed, stream errored, decode failure.\n // Not routed to onError — these are infrastructure, not application errors.\n }\n } finally {\n // ── Cleanup & onDisconnect ─────────────────────────────────────\n // Always runs, even if onConnect threw.\n try {\n reader.releaseLock();\n } catch {\n /* lock may already be released */\n }\n try {\n writer.releaseLock();\n } catch {\n /* lock may already be released */\n }\n\n try {\n await lifecycle.onDisconnect?.(connection);\n } catch (disconnectError) {\n await reportError(disconnectError, connection, lifecycle);\n }\n\n socket.close();\n }\n };\n\n // oxlint-disable-next-line no-void\n void processMessages();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,eAAe,cAAc,QAAgB,OAAkC;AAE7E,KAAI,CADO,OAAO,MAAM,MAAM,CAE5B,OAAM,KAAK,QAAQ,QAAQ;;;;;;;;;;;;;;;;AAkB/B,SAAS,eAAe,QAAgB,QAAgC;AAetE,QAAO;EACL,QAAQ;AACN,UAAO,SAAS;;EAElB,IAAI,YAAY;AACd,UAAO,OAAO,aAAa;;EAE7B,UArBe,SAAS,MAAM,OAAO;EAsBrC,IAAI,gBAAgB;AAClB,UAAO,OAAO,iBAAiB;;EAEjC,IAAI,aAAa;AACf,UAAO,OAAO,cAAc;;EAE9B;EACA,UA3Be,IAAI,eAA2B;GAC9C,QAAQ;AACN,WAAO,SAAS;;GAElB,QAAQ;AACN,WAAO,KAAK;;GAEd,MAAM,MAAM,OAAO;AACjB,UAAM,cAAc,QAAQ,MAAM;;GAErC,CAAC;EAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BH,SAAgB,YAAY,SAA0C;CACpE,MAAM,EACJ,gBAAgB,GAChB,YAAY,MACZ,wBAAwB,QACtB,WAAW,EAAE;;;;;;;;;;;;;;;CAgBjB,SAAS,gBAAgB,QAAsB;AAC7C,MAAI,gBAAgB,GAAG;AACrB,UAAO,WAAW,cAAc;AAChC,UAAO,GAAG,iBAAiB,OAAO,SAAS,CAAC;;AAE9C,MAAI,UACF,QAAO,aAAa,MAAM,sBAAsB;AAElD,SAAO,WAAW,KAAK;;AAGzB,QAAO,EACL,OAAO,YAA2B,SAAuC;EACvE,MAAM,gBAAgB,WAAmB;AACvC,mBAAgB,OAAO;AAEvB,WAAQ,eAAe,QADR,CAAC,CAAC,WAAW,IACU,CAAC;;EAGzC,MAAM,SAAiB,WAAW,MAC9BA,eACE;GACE,IAAI,WAAW,IAAI;GACnB,MAAM,WAAW,IAAI;GACrB,KAAK,WAAW,IAAI;GACpB,YAAY,WAAW,IAAI;GAC5B,EACD,aACD,GACD,aAAa,aAAa;EAE9B,IAAI;EACJ,IAAI;EAQJ,MAAM,YANoB,IAAI,SAAe,SAAS,WAAW;AAC/D,sBAAmB;AACnB,qBAAkB;IAClB;AAKF,SAAO,KAAK,mBAAmB;AAC7B,oBAAiB,KAAK;IACtB;EAKF,IAAI,eAAe;AACnB,SAAO,GAAG,UAAU,QAAQ;AAC1B,OAAI,CAAC,cAAc;AACjB,mBAAe;AACf,oBAAgB,IAAI;AAGpB,WAAO,OAAO;;IAEhB;AAEF,SAAO,OAAO,WAAW,MAAM,WAAW,UAAU,WAAW,QAAQ;EAEvE,MAAM,cAAc,UAAU,OAAO,MAAM,KAAK,OAAO,CAAC;AAExD,SAAO;GACL,MAAM,QAAQ;AACZ,UAAM,aAAa;;GAErB;GACA,IAAI,OAAO;IACT,MAAM,OAAO,OAAO,SAAS;AAC7B,WAAO,QAAQ,OAAO,SAAS,WAAW,KAAK,OAAO,WAAW;;GAEpE;IAEJ;;;;;ACzOH,IAAI,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6JvB,SAAgB,MAAM,KAAW,SAA+B;CAC9D,MAAM,UAAU,YAAY;EAC1B,WAAW,QAAQ;EACnB,uBAAuB,QAAQ;EAC/B,eAAe,QAAQ;EACxB,CAAC;CAEF,MAAM,YAA8B;EAClC,WAAW,QAAQ;EACnB,cAAc,QAAQ;EACtB,SAAS,QAAQ;EAClB;CAED,MAAM,SAAS,QAAQ,OACrB;EACE,UAAU,QAAQ;EAClB,MAAM,QAAQ;EACd,KAAK,QAAQ;EACd,GACA,WAAW,iBAAiB,KAAK,QAAQ,UAAU,CACrD;AAED,QAAO;EACL,MAAM,QAAQ;AACZ,SAAM,OAAO,OAAO;;EAEtB,WAAW,OAAO;EAClB,IAAI,OAAO;AACT,UAAO,OAAO;;EAEjB;;;;;;;AAeH,eAAe,YACb,OACA,YACA,WACA,aACe;CACf,MAAM,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;AACrE,KAAI;AACF,MAAI,UAAU,QACZ,OAAM,UAAU,QAAQ,KAAK,YAAY,YAAY;MAGrD,SAAQ,MACN,wCAAwC,WAAW,GAAG,IAAI,IAAI,UAC/D;SAEG;AAEN,UAAQ,MACN,wCAAwC,WAAW,GAAG,IAAI,IAAI,UAC/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCL,SAAS,iBACP,KACA,QACA,WACM;CACN,MAAM,UAAU,qBAAqB;CACrC,MAAM,SAAS,OAAO,SAAS,YAAY,QAAQ,CAAC,WAAW;CAC/D,MAAM,SAAS,OAAO,SAAS,WAAW;CAE1C,MAAM,aAA6B;EACjC,IAAI;EACJ,WAAW,OAAO;EAClB,eAAe,OAAO;EACtB,YAAY,OAAO;EACnB,QAAQ,OAAO;EACf,uBAAO,IAAI,KAAK;EACjB;CAED,MAAM,kBAAkB,YAAY;AAClC,MAAI;AAEF,OAAI;AACF,UAAM,UAAU,YAAY,WAAW;YAChC,cAAc;AACrB,UAAM,YAAY,cAAc,YAAY,UAAU;AAEtD;;AAIF,OAAI;AACF,WAAO,MAAM;KACX,MAAM,EAAE,MAAM,OAAO,YAAY,MAAM,OAAO,MAAM;AACpD,SAAI,KACF;KAMF,IAAI;AACJ,SAAI;AACF,iBAAW,MAAM,IAAI,OAAO,QAAQ,MAAM,QAAQ,MAAM,WAAW;cAC5D,cAAc;MACrB,MAAM,MACJ,wBAAwB,QACpB,eACA,IAAI,MAAM,OAAO,aAAa,CAAC;AACrC,YAAM,YAAY,KAAK,YAAY,WAAW,eAAe,IAAI,CAAC;AAElE;;AAKF,SAAI,SACF,OAAM,OAAO,MAAM,OAAO,SAAS,IAAI,CAAC;;WAGtC;YAIA;AAGR,OAAI;AACF,WAAO,aAAa;WACd;AAGR,OAAI;AACF,WAAO,aAAa;WACd;AAIR,OAAI;AACF,UAAM,UAAU,eAAe,WAAW;YACnC,iBAAiB;AACxB,UAAM,YAAY,iBAAiB,YAAY,UAAU;;AAG3D,UAAO,OAAO;;;AAKb,kBAAiB"}
|
|
1
|
+
{"version":3,"file":"node.js","names":["createTlsServer"],"sources":["../src/node/adapter.ts","../src/node/serve.ts"],"sourcesContent":["/**\n * Node.js TCP/TLS adapter for the MLLP server.\n *\n * Implements the {@link TcpAdapter} interface using Node.js built-in\n * `net` and `tls` modules. Incoming `net.Socket` instances are wrapped\n * into Web Streams-based {@link AdapterSocket} objects so that the rest\n * of the MLLP stack can work with a platform-agnostic streaming API.\n *\n * This module is internal to the `@glion/mllp` package and\n * is consumed by {@link serve} in `./serve.ts`.\n *\n * @module\n */\n\nimport { once } from \"node:events\";\nimport type { Server, Socket } from \"node:net\";\nimport { createServer } from \"node:net\";\nimport { Readable } from \"node:stream\";\nimport { createServer as createTlsServer } from \"node:tls\";\nimport { promisify } from \"node:util\";\n\nimport type {\n AdapterSocket,\n ConnectionHandler,\n ListenOptions,\n TcpAdapter,\n TcpHandle,\n} from \"../server/adapter\";\n\n/**\n * Options specific to the Node.js adapter that control per-socket behaviour.\n *\n * @internal\n */\nexport interface NodeAdapterOptions {\n /**\n * Socket inactivity timeout in milliseconds.\n *\n * When set to a value greater than `0`, sockets that remain idle for\n * longer than this duration are automatically destroyed. Set to `0`\n * (the default) to disable idle timeouts entirely.\n *\n * @default 0\n */\n socketTimeout?: number;\n\n /**\n * Whether to enable TCP keep-alive probes on accepted sockets.\n *\n * When `true` the operating system will periodically send keep-alive\n * packets to detect dead peers and prevent intermediate network\n * equipment from dropping idle connections.\n *\n * @default true\n */\n keepAlive?: boolean;\n\n /**\n * The initial delay in milliseconds before the first TCP keep-alive\n * probe is sent on an idle socket. Only meaningful when\n * {@link keepAlive} is `true`.\n *\n * @default 60000\n */\n keepAliveInitialDelay?: number;\n}\n\n/**\n * Write a chunk to a Node.js socket, waiting for backpressure to clear.\n *\n * `socket.write()` returns `false` when the internal buffer is full,\n * indicating that the caller should wait for the `\"drain\"` event before\n * writing again. This helper wraps that pattern in a promise so it can\n * be used with `await`.\n *\n * @param socket - The Node.js TCP socket to write to.\n * @param chunk - The bytes to write.\n * @returns A promise that resolves once the chunk has been flushed or the\n * socket's internal buffer has drained.\n */\nasync function writeToSocket(socket: Socket, chunk: Uint8Array): Promise<void> {\n const ok = socket.write(chunk);\n if (!ok) {\n await once(socket, \"drain\");\n }\n}\n\n/**\n * Wrap a Node.js `net.Socket` into a platform-agnostic {@link AdapterSocket}.\n *\n * The readable side is created via `Readable.toWeb()` which converts the\n * Node.js stream into a WHATWG `ReadableStream<Uint8Array>`. The writable\n * side is a custom `WritableStream` that delegates writes to\n * {@link writeToSocket} and handles `close` / `abort` by ending or\n * destroying the underlying socket.\n *\n * @param socket - The raw Node.js socket to wrap.\n * @param secure - Whether this socket was accepted over TLS.\n * @returns An {@link AdapterSocket} exposing Web Streams and connection\n * metadata.\n */\nfunction wrapNodeSocket(socket: Socket, secure: boolean): AdapterSocket {\n const readable = Readable.toWeb(socket) as ReadableStream<Uint8Array>;\n\n const writable = new WritableStream<Uint8Array>({\n abort() {\n socket.destroy();\n },\n close() {\n socket.end();\n },\n async write(chunk) {\n await writeToSocket(socket, chunk);\n },\n });\n\n return {\n close() {\n socket.destroy();\n },\n get localPort() {\n return socket.localPort ?? 0;\n },\n readable,\n get remoteAddress() {\n return socket.remoteAddress ?? \"unknown\";\n },\n get remotePort() {\n return socket.remotePort ?? 0;\n },\n secure,\n writable,\n };\n}\n\n/**\n * Create a Node.js {@link TcpAdapter} that can listen for TCP or TLS\n * connections using the built-in `net` and `tls` modules.\n *\n * When the returned adapter's `listen()` method is called with a `tls`\n * option, a TLS server (`tls.createServer`) is used; otherwise a plain\n * TCP server (`net.createServer`) is created.\n *\n * Each accepted socket is configured via {@link configureSocket} (timeouts,\n * keep-alive, Nagle disabled) and then wrapped into an {@link AdapterSocket}\n * before being handed to the provided {@link ConnectionHandler}.\n *\n * @example\n * ```typescript\n * const adapter = nodeAdapter({ socketTimeout: 30_000 });\n * const handle = adapter.listen({ port: 2575 }, (socket) => {\n * // handle connection\n * });\n * ```;\n *\n * @param options - Optional per-socket configuration (timeouts, keep-alive).\n * @returns A {@link TcpAdapter} ready to bind to a port.\n * @internal\n */\nexport function nodeAdapter(options?: NodeAdapterOptions): TcpAdapter {\n const {\n socketTimeout = 0,\n keepAlive = true,\n keepAliveInitialDelay = 60_000,\n } = options ?? {};\n\n /**\n * Apply common socket options to an accepted connection.\n *\n * Configures the following on the raw `net.Socket`:\n *\n * - **Idle timeout** -- If `socketTimeout > 0`, sets `setTimeout()` and\n * destroys the socket on the `\"timeout\"` event.\n * - **TCP keep-alive** -- Enables keep-alive probes with the configured initial\n * delay when `keepAlive` is `true`.\n * - **Nagle disabled** -- Calls `setNoDelay(true)` so that small MLLP frames\n * are sent immediately without buffering.\n *\n * @param socket - The raw Node.js socket to configure.\n */\n function configureSocket(socket: Socket): void {\n if (socketTimeout > 0) {\n socket.setTimeout(socketTimeout);\n socket.on(\"timeout\", () => socket.destroy());\n }\n if (keepAlive) {\n socket.setKeepAlive(true, keepAliveInitialDelay);\n }\n socket.setNoDelay(true);\n }\n\n return {\n listen(listenOpts: ListenOptions, handler: ConnectionHandler): TcpHandle {\n const onConnection = (socket: Socket) => {\n configureSocket(socket);\n const secure = !!listenOpts.tls;\n handler(wrapNodeSocket(socket, secure));\n };\n\n const server: Server = listenOpts.tls\n ? createTlsServer(\n {\n ca: listenOpts.tls.ca,\n cert: listenOpts.tls.cert,\n key: listenOpts.tls.key,\n passphrase: listenOpts.tls.passphrase,\n },\n onConnection\n )\n : createServer(onConnection);\n\n let resolveListening!: (value: true) => void;\n let rejectListening!: (reason: unknown) => void;\n // oxlint-disable-next-line promise/avoid-new -- Node 18 compat (no Promise.withResolvers)\n const listeningInternal = new Promise<true>((resolve, reject) => {\n resolveListening = resolve;\n rejectListening = reject;\n });\n // Public type is Promise<void>; the sentinel return value is an\n // implementation detail never observed by consumers.\n const listening = listeningInternal as Promise<unknown> as Promise<void>;\n\n server.once(\"listening\", () => {\n resolveListening(true);\n });\n // Persistent error listener: server errors that fire before\n // `listening` reject the startup promise; later errors (TLS,\n // shutdown) propagate via the already-returned handle's lifecycle\n // rather than crashing the process via an unhandled emitter error.\n let listenFailed = false;\n server.on(\"error\", (err) => {\n if (!listenFailed) {\n listenFailed = true;\n rejectListening(err);\n // Close the server handle so the OS descriptor is released\n // even if the caller never awaits `listening` again.\n server.close();\n }\n });\n\n server.listen(listenOpts.port, listenOpts.hostname, listenOpts.backlog);\n\n const closeServer = promisify(server.close.bind(server));\n\n return {\n async close() {\n await closeServer();\n },\n listening,\n get port() {\n const addr = server.address();\n return addr && typeof addr === \"object\" ? addr.port : listenOpts.port;\n },\n };\n },\n };\n}\n","/**\n * Node.js MLLP server entry point.\n *\n * Binds an {@link Mllp} application to a TCP (or TLS) socket using the\n * Node.js `net` / `tls` modules. Incoming bytes are decoded from the MLLP\n * framing protocol, dispatched through the application's middleware stack,\n * and the resulting acknowledgements are encoded and written back to the\n * client.\n *\n * @module\n */\n\nimport { createDecoderStream, encode } from \"@glion/mllp-transport\";\n\nimport type { AdapterSocket } from \"../server/adapter\";\nimport type { MessageInfo, Mllp } from \"../server/mllp\";\nimport { getMessageInfo } from \"../server/mllp\";\nimport type { ConnectionInfo } from \"../server/types\";\nimport { nodeAdapter } from \"./adapter\";\n\n/** Monotonically-increasing connection ID counter. */\n// oxlint-disable-next-line prefer-const\nlet nextConnectionId = 1;\n\n/**\n * Lifecycle callback invoked for connection events.\n */\nexport type ConnectionCallback = (\n connection: ConnectionInfo\n) => void | Promise<void>;\n\n/**\n * Error callback invoked when a message handler error is unhandled.\n * Receives the error and the connection it occurred on.\n *\n * For handler errors, `messageInfo` contains the routing fields\n * (messageType, triggerEvent, controlId, version) of the message that\n * caused the error — useful for audit logging in healthcare contexts.\n * For lifecycle callback errors (onConnect/onDisconnect), `messageInfo`\n * is `undefined`.\n *\n * This fires only for application-level errors (handler/middleware throws\n * with no app-level error handler, or app-level error handler itself throws).\n * Transport-level errors (connection reset, decode failures) do not trigger\n * this callback.\n */\nexport type ErrorCallback = (\n error: Error,\n connection: ConnectionInfo,\n messageInfo: MessageInfo | undefined\n) => void | Promise<void>;\n\n/**\n * Options for starting an MLLP server with {@link serve}.\n */\nexport interface ServeOptions {\n /**\n * The hostname or IP address to bind the server to.\n * When omitted the OS will bind to all available interfaces (`0.0.0.0`).\n */\n hostname?: string;\n\n /**\n * Whether to enable TCP keep-alive on accepted sockets.\n * Defaults to the adapter default (`true`).\n */\n keepAlive?: boolean;\n\n /**\n * The initial delay in milliseconds before the first TCP keep-alive probe\n * is sent on an idle socket. Only meaningful when `keepAlive` is `true`.\n * Defaults to the adapter default (`60 000`).\n */\n keepAliveInitialDelay?: number;\n\n /**\n * Called when a new TCP connection is accepted. Receives the connection\n * metadata including the unique connection ID and mutable state map.\n *\n * If this callback throws, the connection is torn down and\n * `onDisconnect` is still called.\n */\n onConnect?: ConnectionCallback;\n\n /**\n * Called when a TCP connection is closed (including force-close).\n * Always fires, even if `onConnect` threw.\n *\n * If this callback throws, the error is routed to `onError`\n * (or `console.error` as last resort).\n */\n onDisconnect?: ConnectionCallback;\n\n /**\n * Called when a message handler error is unhandled — either no app-level\n * error handler is registered, or the app-level error handler itself\n * threw. Also called when lifecycle callbacks (`onConnect`,\n * `onDisconnect`) throw.\n *\n * Only logs `error.message` and connection ID to avoid leaking PHI\n * in the `console.error` fallback. Production deployments should\n * always provide this callback.\n */\n onError?: ErrorCallback;\n\n /**\n * The TCP port number to listen on.\n */\n port: number;\n\n /**\n * Socket inactivity timeout in milliseconds. A socket that has been idle\n * for longer than this value will be destroyed. Set to `0` (the default)\n * to disable the timeout.\n */\n socketTimeout?: number;\n\n /**\n * TLS configuration. When provided the server will create a TLS socket\n * instead of a plain TCP socket.\n */\n tls?: {\n /** Optional CA certificate(s) for client certificate verification. */\n ca?: string | Buffer;\n /** The server certificate. */\n cert: string | Buffer;\n /** The private key for the server certificate. */\n key: string | Buffer;\n /** Optional passphrase for the private key. */\n passphrase?: string;\n };\n}\n\n/**\n * A running MLLP server handle returned by {@link serve}.\n */\nexport interface Server {\n /** The port the server is currently listening on. */\n readonly port: number;\n\n /**\n * Resolves when the underlying TCP server has bound and started listening.\n * Await this before attempting to connect to ensure the port is ready.\n */\n readonly listening: Promise<void>;\n\n /**\n * Gracefully close the server. No new connections will be accepted and the\n * returned promise resolves once all underlying resources are released.\n */\n close(): Promise<void>;\n}\n\n/**\n * Start an MLLP server bound to a TCP (or TLS) port.\n *\n * The server accepts incoming connections, decodes MLLP-framed HL7v2\n * messages, passes them through the provided {@link Mllp} application, and\n * writes back any response (typically an ACK/NAK) using MLLP framing.\n *\n * @example\n * ```typescript\n * import { Mllp } from \"@glion/mllp\";\n * import { parseHL7v2 } from \"@glion/hl7v2\";\n * import { serve } from \"@glion/mllp/node\";\n *\n * const app = new Mllp()\n * .parser(parseHL7v2)\n * .on(\"ADT^A01\", async (ctx) => ({ raw: \"...\" }));\n *\n * const server = serve(app, { port: 2575 });\n * console.log(`Listening on port ${server.port}`);\n * ```\n *\n * @param app - The {@link Mllp} application that will handle each message.\n * @param options - Server configuration (port, TLS, timeouts, etc.).\n * @returns A {@link Server} handle that exposes the bound port and a `close()`\n * method for graceful shutdown.\n */\nexport function serve(app: Mllp, options: ServeOptions): Server {\n const adapter = nodeAdapter({\n keepAlive: options.keepAlive,\n keepAliveInitialDelay: options.keepAliveInitialDelay,\n socketTimeout: options.socketTimeout,\n });\n\n const lifecycle: LifecycleOptions = {\n onConnect: options.onConnect,\n onDisconnect: options.onDisconnect,\n onError: options.onError,\n };\n\n const handle = adapter.listen(\n {\n hostname: options.hostname,\n port: options.port,\n tls: options.tls,\n },\n (socket) => handleConnection(app, socket, lifecycle)\n );\n\n return {\n async close() {\n await handle.close();\n },\n listening: handle.listening,\n get port() {\n return handle.port;\n },\n };\n}\n\n/** Lifecycle callback options extracted from ServeOptions. */\ninterface LifecycleOptions {\n onConnect?: ConnectionCallback;\n onDisconnect?: ConnectionCallback;\n onError?: ErrorCallback;\n}\n\n/**\n * Route an error to the `onError` callback, falling back to a safe\n * `console.error` that logs only the error message and connection ID\n * to avoid leaking PHI from HL7v2 message content.\n */\nasync function reportError(\n error: unknown,\n connection: ConnectionInfo,\n lifecycle: LifecycleOptions,\n messageInfo?: MessageInfo\n): Promise<void> {\n const err = error instanceof Error ? error : new Error(String(error));\n try {\n if (lifecycle.onError) {\n await lifecycle.onError(err, connection, messageInfo);\n } else {\n // Safe fallback: only message + connection ID, no PHI\n console.error(\n `[mllp] Unhandled error on connection ${connection.id}: ${err.message}`\n );\n }\n } catch {\n // onError itself threw — last resort\n console.error(\n `[mllp] Unhandled error on connection ${connection.id}: ${err.message}`\n );\n }\n}\n\n/**\n * Handle a single MLLP connection.\n *\n * Sets up a decode-handle-encode loop for the lifetime of the socket:\n *\n * 1. **Decode** — Raw bytes from the socket's readable stream are piped through an\n * MLLP decoder (`TransformStream`) that strips the MLLP start/end block\n * characters and emits complete HL7v2 messages.\n * 2. **Handle** — Each decoded message is passed to the {@link Mllp} application\n * which runs its middleware stack and returns an optional response (e.g. an\n * ACK or NAK).\n * 3. **Encode** — If the application produced a response, the raw response bytes\n * are MLLP-encoded (wrapped in start/end block characters) and written back\n * to the socket.\n *\n * The loop runs until the remote end closes the connection or an\n * unrecoverable error occurs. Stream locks are released in a `finally`\n * block to prevent resource leaks.\n *\n * Lifecycle callbacks fire at connection boundaries:\n *\n * - `onConnect` after the connection is established\n * - `onDisconnect` when the connection closes (always fires)\n * - `onError` when a handler error is unhandled (not for transport errors)\n *\n * @param app - The MLLP application to dispatch messages to.\n * @param socket - The adapter socket wrapping the underlying TCP connection.\n * @param lifecycle - Optional lifecycle callbacks from ServeOptions.\n */\nfunction handleConnection(\n app: Mllp,\n socket: AdapterSocket,\n lifecycle: LifecycleOptions\n): void {\n const decoder = createDecoderStream();\n const reader = socket.readable.pipeThrough(decoder).getReader();\n const writer = socket.writable.getWriter();\n\n const connection: ConnectionInfo = {\n id: nextConnectionId++,\n localPort: socket.localPort,\n remoteAddress: socket.remoteAddress,\n remotePort: socket.remotePort,\n secure: socket.secure,\n state: new Map(),\n };\n\n const processMessages = async () => {\n try {\n // ── onConnect ──────────────────────────────────────────────────\n try {\n await lifecycle.onConnect?.(connection);\n } catch (connectError) {\n await reportError(connectError, connection, lifecycle);\n // Tear down — onDisconnect still fires in the outer finally\n return;\n }\n\n // ── Message loop ───────────────────────────────────────────────\n try {\n while (true) {\n const { done, value: message } = await reader.read();\n if (done) {\n break;\n }\n\n // Inner try/catch separates handler errors from stream errors.\n // Only handler errors route to onError; stream errors (connection\n // reset, decode failure) flow to the outer catch for cleanup.\n let response: Awaited<ReturnType<Mllp[\"handle\"]>>;\n try {\n response = await app.handle(message.text, message.data, connection);\n } catch (handlerError) {\n const err =\n handlerError instanceof Error\n ? handlerError\n : new Error(String(handlerError));\n await reportError(err, connection, lifecycle, getMessageInfo(err));\n // Continue processing — connection stays alive\n continue;\n }\n\n // Write is outside the handler error catch — write failures\n // are transport errors and flow to the outer catch.\n if (response) {\n await writer.write(encode(response.raw));\n }\n }\n } catch {\n // Transport-level: connection closed, stream errored, decode failure.\n // Not routed to onError — these are infrastructure, not application errors.\n }\n } finally {\n // ── Cleanup & onDisconnect ─────────────────────────────────────\n // Always runs, even if onConnect threw.\n try {\n reader.releaseLock();\n } catch {\n /* lock may already be released */\n }\n try {\n writer.releaseLock();\n } catch {\n /* lock may already be released */\n }\n\n try {\n await lifecycle.onDisconnect?.(connection);\n } catch (disconnectError) {\n await reportError(disconnectError, connection, lifecycle);\n }\n\n socket.close();\n }\n };\n\n // oxlint-disable-next-line no-void\n void processMessages();\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mllp.d.ts","sourceRoot":"","sources":["../../src/server/mllp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mllp.d.ts","sourceRoot":"","sources":["../../src/server/mllp.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,cAAc,EAEd,YAAY,EACZ,OAAO,EACP,cAAc,EACd,UAAU,EACV,QAAQ,EACR,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAQD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW,GAAG,SAAS,CAEpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,IAAI;;IAKf;;;;;;;;;OASG;IACH,MAAM,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI;IAKvC;;;;;;;;OAQG;IACH,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IAClE,GAAG,CAAC,eAAe,EAAE,MAAM,GAAG,WAAW,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;IAyBxE;;;;;;;;;;;;;OAaG;IACH,EAAE,CAAC,eAAe,EAAE,MAAM,GAAG,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAKjE;;;;;;;;OAQG;IACH,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAKpC;;;;;;;;;;;OAWG;IACG,MAAM,CACV,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,cAAc,GAEzB,OAAO,CAAC,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC;CAmFxC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glion/mllp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Transport-agnostic MLLP (Minimal Lower Layer Protocol) engine for HL7v2 messaging",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"health",
|
|
@@ -44,26 +44,26 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"unified": "11.0.5",
|
|
46
46
|
"vfile": "6.0.3",
|
|
47
|
-
"@glion/
|
|
48
|
-
"@glion/
|
|
49
|
-
"@glion/util-query": "0.
|
|
47
|
+
"@glion/ast": "0.16.0",
|
|
48
|
+
"@glion/mllp-transport": "0.16.0",
|
|
49
|
+
"@glion/util-query": "0.16.0",
|
|
50
|
+
"@glion/parser": "0.16.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
|
-
"@types/node": "
|
|
53
|
-
"@vitest/coverage-v8": "4.1.
|
|
53
|
+
"@types/node": "25.6.0",
|
|
54
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
54
55
|
"publint": "0.3.18",
|
|
55
|
-
"tsdown": "0.21.
|
|
56
|
-
"typescript": "
|
|
57
|
-
"vitest": "4.1.
|
|
58
|
-
"@glion/check-readme": "0.
|
|
59
|
-
"@glion/hl7v2": "0.
|
|
60
|
-
"@glion/testing": "0.
|
|
61
|
-
"@glion/tsconfig": "0.
|
|
56
|
+
"tsdown": "0.21.10",
|
|
57
|
+
"typescript": "6.0.3",
|
|
58
|
+
"vitest": "4.1.5",
|
|
59
|
+
"@glion/check-readme": "0.16.0",
|
|
60
|
+
"@glion/hl7v2": "0.16.0",
|
|
61
|
+
"@glion/testing": "0.16.0",
|
|
62
|
+
"@glion/tsconfig": "0.16.0"
|
|
62
63
|
},
|
|
63
64
|
"engines": {
|
|
64
|
-
"node": ">=
|
|
65
|
+
"node": ">=20"
|
|
65
66
|
},
|
|
66
|
-
"packageManager": "pnpm@10.14.0",
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsdown && tsc --emitDeclarationOnly",
|
|
69
69
|
"check-types": "tsc --noEmit",
|
package/dist/errors.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Options for constructing an MllpError.
|
|
3
|
-
*/
|
|
4
|
-
export interface MllpErrorOptions extends ErrorOptions {
|
|
5
|
-
/** Error condition string (e.g., for ERR segment in ACK) */
|
|
6
|
-
errorCondition?: string;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Base error class for all MLLP errors.
|
|
10
|
-
*
|
|
11
|
-
* All error types in the MLLP package extend this class.
|
|
12
|
-
* Provides a common `code` field so that any MLLP error can be
|
|
13
|
-
* programmatically identified and translated (e.g., into ACK/NAK responses).
|
|
14
|
-
*/
|
|
15
|
-
export declare class MllpError extends Error {
|
|
16
|
-
/** Machine-readable error code identifying the error type */
|
|
17
|
-
readonly code: string;
|
|
18
|
-
/** Error condition string (e.g., for ERR segment in ACK) */
|
|
19
|
-
readonly errorCondition?: string;
|
|
20
|
-
constructor(code: string, message: string, options?: MllpErrorOptions);
|
|
21
|
-
}
|
|
22
|
-
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,4DAA4D;IAC5D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;gBAErB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAMtE"}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/transport/decoder.ts"],"sourcesContent":["import { MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_START_BYTE } from \"./constants\";\nimport { TransportError } from \"./errors\";\nimport type { DecodedMessage, DecoderOptions } from \"./types\";\nimport { TransportErrorCode } from \"./types\";\n\n/**\n * Decode a single MLLP frame (VT ... FS CR) into a message.\n *\n * Validates framing, strips the envelope, and returns the payload as\n * both raw bytes and decoded text. Throws {@link TransportError} on\n * any structural issue.\n *\n * For streaming / chunked TCP data, use {@link createDecoderStream} instead.\n *\n * @throws TransportError with `INVALID_START_BYTE` if byte 0 is not 0x0B\n * @throws TransportError with `INVALID_END_SEQUENCE` if frame doesn't end with\n * 0x1C 0x0D\n * @throws TransportError with `MESSAGE_TOO_LARGE` if payload exceeds\n * `maxMessageSize`\n */\nexport function decode(\n frame: Uint8Array,\n options?: DecoderOptions\n): DecodedMessage {\n if (frame.length < 3) {\n throw new TransportError(\n TransportErrorCode.INVALID_END_SEQUENCE,\n \"Frame too short to contain valid MLLP structure\",\n 0\n );\n }\n\n const startByte = frame[0];\n if (startByte !== MLLP_START_BYTE) {\n throw new TransportError(\n TransportErrorCode.INVALID_START_BYTE,\n `Invalid start byte: expected 0x0B, got 0x${(startByte ?? 0).toString(16).padStart(2, \"0\")}`,\n 0\n );\n }\n\n const len = frame.length;\n const endByte1 = frame[len - 2];\n const endByte2 = frame[len - 1];\n if (endByte1 !== MLLP_END_BYTE_1 || endByte2 !== MLLP_END_BYTE_2) {\n throw new TransportError(\n TransportErrorCode.INVALID_END_SEQUENCE,\n `Invalid end sequence: expected 0x1C 0x0D, got 0x${(endByte1 ?? 0).toString(16).padStart(2, \"0\")} 0x${(endByte2 ?? 0).toString(16).padStart(2, \"0\")}`,\n len - 2\n );\n }\n\n const messageLength = len - 3;\n if (\n options?.maxMessageSize !== undefined &&\n messageLength > options.maxMessageSize\n ) {\n throw new TransportError(\n TransportErrorCode.MESSAGE_TOO_LARGE,\n `Message size ${messageLength} exceeds maximum ${options.maxMessageSize}`,\n 0\n );\n }\n\n const data = frame.subarray(1, len - 2);\n const text = new TextDecoder(options?.encoding ?? \"utf8\").decode(data);\n\n return {\n byteLength: messageLength,\n data,\n text,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,SAAgB,OACd,OACA,SACgB;AAChB,KAAI,MAAM,SAAS,EACjB,OAAM,IAAI,eACR,mBAAmB,sBACnB,mDACA,EACD;CAGH,MAAM,YAAY,MAAM;AACxB,KAAI,cAAA,GACF,OAAM,IAAI,eACR,mBAAmB,oBACnB,6CAA6C,aAAa,GAAG,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,IAC1F,EACD;CAGH,MAAM,MAAM,MAAM;CAClB,MAAM,WAAW,MAAM,MAAM;CAC7B,MAAM,WAAW,MAAM,MAAM;AAC7B,KAAI,aAAA,MAAgC,aAAA,GAClC,OAAM,IAAI,eACR,mBAAmB,sBACnB,oDAAoD,YAAY,GAAG,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,YAAY,GAAG,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,IACnJ,MAAM,EACP;CAGH,MAAM,gBAAgB,MAAM;AAC5B,KACE,SAAS,mBAAmB,KAAA,KAC5B,gBAAgB,QAAQ,eAExB,OAAM,IAAI,eACR,mBAAmB,mBACnB,gBAAgB,cAAc,mBAAmB,QAAQ,kBACzD,EACD;CAGH,MAAM,OAAO,MAAM,SAAS,GAAG,MAAM,EAAE;AAGvC,QAAO;EACL,YAAY;EACZ;EACA,MALW,IAAI,YAAY,SAAS,YAAY,OAAO,CAAC,OAAO,KAAK;EAMrE"}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MLLP Start Byte - Vertical Tab (VT)
|
|
3
|
-
* Marks the beginning of an MLLP frame
|
|
4
|
-
*/
|
|
5
|
-
export declare const MLLP_START_BYTE = 11;
|
|
6
|
-
/**
|
|
7
|
-
* MLLP End Byte 1 - File Separator (FS)
|
|
8
|
-
* First byte of the end sequence
|
|
9
|
-
*/
|
|
10
|
-
export declare const MLLP_END_BYTE_1 = 28;
|
|
11
|
-
/**
|
|
12
|
-
* MLLP End Byte 2 - Carriage Return (CR)
|
|
13
|
-
* Second byte of the end sequence
|
|
14
|
-
*/
|
|
15
|
-
export declare const MLLP_END_BYTE_2 = 13;
|
|
16
|
-
/**
|
|
17
|
-
* MLLP header as a Uint8Array containing the start byte
|
|
18
|
-
*/
|
|
19
|
-
export declare const MLLP_HEADER: Uint8Array<ArrayBuffer>;
|
|
20
|
-
/**
|
|
21
|
-
* MLLP trailer as a Uint8Array containing the end sequence
|
|
22
|
-
*/
|
|
23
|
-
export declare const MLLP_TRAILER: Uint8Array<ArrayBuffer>;
|
|
24
|
-
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/transport/constants.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,eAAO,MAAM,eAAe,KAAO,CAAC;AAEpC;;;GAGG;AACH,eAAO,MAAM,eAAe,KAAO,CAAC;AAEpC;;;GAGG;AACH,eAAO,MAAM,eAAe,KAAO,CAAC;AAMpC;;GAEG;AACH,eAAO,MAAM,WAAW,yBAAoC,CAAC;AAE7D;;GAEG;AACH,eAAO,MAAM,YAAY,yBAAqD,CAAC"}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { DecodedMessage, DecoderOptions } from "./types";
|
|
2
|
-
/**
|
|
3
|
-
* Create a `TransformStream` that extracts MLLP messages from a raw
|
|
4
|
-
* byte stream.
|
|
5
|
-
*
|
|
6
|
-
* Pipe a TCP socket's `ReadableStream<Uint8Array>` through this stream
|
|
7
|
-
* to receive one {@link DecodedMessage} per MLLP frame, regardless of
|
|
8
|
-
* how the bytes are chunked on the wire.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* import { createDecoderStream } from "@glion/mllp";
|
|
13
|
-
*
|
|
14
|
-
* const decoder = createDecoderStream({
|
|
15
|
-
* maxMessageSize: 1024 * 1024, // 1 MB limit
|
|
16
|
-
* onError: (err) => log.warn(err), // custom error handler
|
|
17
|
-
* });
|
|
18
|
-
*
|
|
19
|
-
* const messages = socket.readable.pipeThrough(decoder).getReader();
|
|
20
|
-
*
|
|
21
|
-
* while (true) {
|
|
22
|
-
* const { done, value } = await messages.read();
|
|
23
|
-
* if (done) break;
|
|
24
|
-
* console.log(value.text); // HL7v2 message string
|
|
25
|
-
* }
|
|
26
|
-
* ```;
|
|
27
|
-
*
|
|
28
|
-
* @param options - Optional decoder settings (max size, encoding, error
|
|
29
|
-
* handler).
|
|
30
|
-
* @returns A `TransformStream<Uint8Array, DecodedMessage>`.
|
|
31
|
-
*/
|
|
32
|
-
export declare function createDecoderStream(options?: DecoderOptions): TransformStream<Uint8Array, DecodedMessage>;
|
|
33
|
-
/**
|
|
34
|
-
* Class-based variant of {@link createDecoderStream} for cases where
|
|
35
|
-
* a reusable instance is preferred over the factory function.
|
|
36
|
-
*
|
|
37
|
-
* Functionally identical — both delegate to the same internal
|
|
38
|
-
* transformer.
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```typescript
|
|
42
|
-
* const decoder = new MLLPDecoderStream({ maxMessageSize: 512_000 });
|
|
43
|
-
* socket.readable.pipeThrough(decoder);
|
|
44
|
-
* ```;
|
|
45
|
-
*/
|
|
46
|
-
export declare class MLLPDecoderStream extends TransformStream<Uint8Array, DecodedMessage> {
|
|
47
|
-
constructor(options?: DecoderOptions);
|
|
48
|
-
}
|
|
49
|
-
//# sourceMappingURL=decoder-stream.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"decoder-stream.d.ts","sourceRoot":"","sources":["../../src/transport/decoder-stream.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AA0W9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,CAAC,EAAE,cAAc,GACvB,eAAe,CAAC,UAAU,EAAE,cAAc,CAAC,CAE7C;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,iBAAkB,SAAQ,eAAe,CACpD,UAAU,EACV,cAAc,CACf;gBACa,OAAO,CAAC,EAAE,cAAc;CAGrC"}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { DecodedMessage, DecoderOptions } from "./types";
|
|
2
|
-
/**
|
|
3
|
-
* Decode a single MLLP frame (VT ... FS CR) into a message.
|
|
4
|
-
*
|
|
5
|
-
* Validates framing, strips the envelope, and returns the payload as
|
|
6
|
-
* both raw bytes and decoded text. Throws {@link TransportError} on
|
|
7
|
-
* any structural issue.
|
|
8
|
-
*
|
|
9
|
-
* For streaming / chunked TCP data, use {@link createDecoderStream} instead.
|
|
10
|
-
*
|
|
11
|
-
* @throws TransportError with `INVALID_START_BYTE` if byte 0 is not 0x0B
|
|
12
|
-
* @throws TransportError with `INVALID_END_SEQUENCE` if frame doesn't end with
|
|
13
|
-
* 0x1C 0x0D
|
|
14
|
-
* @throws TransportError with `MESSAGE_TOO_LARGE` if payload exceeds
|
|
15
|
-
* `maxMessageSize`
|
|
16
|
-
*/
|
|
17
|
-
export declare function decode(frame: Uint8Array, options?: DecoderOptions): DecodedMessage;
|
|
18
|
-
//# sourceMappingURL=decoder.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../../src/transport/decoder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,MAAM,CACpB,KAAK,EAAE,UAAU,EACjB,OAAO,CAAC,EAAE,cAAc,GACvB,cAAc,CAiDhB"}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A growable byte buffer with geometric capacity doubling.
|
|
3
|
-
*
|
|
4
|
-
* Designed for protocol decoders that accumulate bytes from arbitrarily
|
|
5
|
-
* chunked TCP streams. Amortizes copy cost to O(n) total — the same
|
|
6
|
-
* strategy used by pg-protocol, NATS.js, and Java's ArrayList.
|
|
7
|
-
*
|
|
8
|
-
* The buffer maintains a read offset so consumed bytes can be skipped
|
|
9
|
-
* without shifting data. When the wasted prefix exceeds half the
|
|
10
|
-
* capacity, the buffer auto-compacts by shifting remaining bytes to
|
|
11
|
-
* the front.
|
|
12
|
-
*
|
|
13
|
-
* @internal
|
|
14
|
-
*/
|
|
15
|
-
export declare class DynamicBuffer {
|
|
16
|
-
private _buffer;
|
|
17
|
-
private _length;
|
|
18
|
-
private _offset;
|
|
19
|
-
constructor(initialCapacity?: number);
|
|
20
|
-
get capacity(): number;
|
|
21
|
-
get available(): number;
|
|
22
|
-
append(chunk: Uint8Array): void;
|
|
23
|
-
view(): Uint8Array;
|
|
24
|
-
consume(n: number): void;
|
|
25
|
-
reset(): void;
|
|
26
|
-
private _compact;
|
|
27
|
-
}
|
|
28
|
-
//# sourceMappingURL=dynamic-buffer.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-buffer.d.ts","sourceRoot":"","sources":["../../src/transport/dynamic-buffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,OAAO,CAAK;gBAER,eAAe,SAAO;IAIlC,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAY/B,IAAI,IAAI,UAAU;IAIlB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAYxB,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,QAAQ;CAMjB"}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { EncoderInput, EncoderOptions } from "./types";
|
|
2
|
-
/**
|
|
3
|
-
* Encode a single HL7v2 message into an MLLP frame.
|
|
4
|
-
*
|
|
5
|
-
* Wraps the message with MLLP framing:
|
|
6
|
-
* - Start byte: 0x0B (VT)
|
|
7
|
-
* - End sequence: 0x1C 0x0D (FS + CR)
|
|
8
|
-
*/
|
|
9
|
-
export declare function encode(message: EncoderInput, _options?: EncoderOptions): Uint8Array;
|
|
10
|
-
/**
|
|
11
|
-
* Encode multiple HL7v2 messages into concatenated MLLP frames.
|
|
12
|
-
*/
|
|
13
|
-
export declare function encodeMultiple(messages: EncoderInput[], options?: EncoderOptions): Uint8Array;
|
|
14
|
-
//# sourceMappingURL=encoder.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"encoder.d.ts","sourceRoot":"","sources":["../../src/transport/encoder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK5D;;;;;;GAMG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,YAAY,EACrB,QAAQ,CAAC,EAAE,cAAc,GACxB,UAAU,CAWZ;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,YAAY,EAAE,EACxB,OAAO,CAAC,EAAE,cAAc,GACvB,UAAU,CAaZ"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { MllpError } from "../errors";
|
|
2
|
-
import type { TransportErrorCode } from "./types";
|
|
3
|
-
/**
|
|
4
|
-
* Transport-level error for MLLP framing issues (invalid framing, incomplete
|
|
5
|
-
* messages, etc.)
|
|
6
|
-
*/
|
|
7
|
-
export declare class TransportError extends MllpError {
|
|
8
|
-
readonly code: TransportErrorCode;
|
|
9
|
-
/** Position in the data where the error occurred (if applicable) */
|
|
10
|
-
readonly position?: number;
|
|
11
|
-
constructor(code: TransportErrorCode, message: string, position?: number);
|
|
12
|
-
}
|
|
13
|
-
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/transport/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;GAGG;AACH,qBAAa,cAAe,SAAQ,SAAS;IAC3C,SAAiB,IAAI,EAAE,kBAAkB,CAAC;IAC1C,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;gBAEf,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;CAKzE"}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_HEADER, MLLP_START_BYTE, MLLP_TRAILER, } from "./constants";
|
|
2
|
-
export { decode } from "./decoder";
|
|
3
|
-
export { createDecoderStream, MLLPDecoderStream } from "./decoder-stream";
|
|
4
|
-
export { encode, encodeMultiple } from "./encoder";
|
|
5
|
-
export { TransportError } from "./errors";
|
|
6
|
-
export { TransportErrorCode as ErrorCode } from "./types";
|
|
7
|
-
export type { DecodedMessage, DecoderOptions, EncoderInput, EncoderOptions, } from "./types";
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transport/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,eAAe,EACf,WAAW,EACX,eAAe,EACf,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,kBAAkB,IAAI,SAAS,EAAE,MAAM,SAAS,CAAC;AAC1D,YAAY,EACV,cAAc,EACd,cAAc,EACd,YAAY,EACZ,cAAc,GACf,MAAM,SAAS,CAAC"}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { TransportError } from "./errors";
|
|
2
|
-
/**
|
|
3
|
-
* Input types for encoding - supports both string and binary
|
|
4
|
-
*/
|
|
5
|
-
export type EncoderInput = string | Uint8Array;
|
|
6
|
-
/**
|
|
7
|
-
* Decoded message result from MLLP frame
|
|
8
|
-
*/
|
|
9
|
-
export interface DecodedMessage {
|
|
10
|
-
/** The raw message payload (without MLLP framing) */
|
|
11
|
-
data: Uint8Array;
|
|
12
|
-
/** Message decoded as string (convenience) */
|
|
13
|
-
text: string;
|
|
14
|
-
/** Byte length of the original message */
|
|
15
|
-
byteLength: number;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Options for the MLLP encoder
|
|
19
|
-
*/
|
|
20
|
-
export interface EncoderOptions {
|
|
21
|
-
/** Encoding for string input (default: 'utf-8') */
|
|
22
|
-
encoding?: string;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Options for the MLLP decoder
|
|
26
|
-
*/
|
|
27
|
-
export interface DecoderOptions {
|
|
28
|
-
/** Maximum message size in bytes (optional, no default limit) */
|
|
29
|
-
maxMessageSize?: number;
|
|
30
|
-
/** Encoding for text conversion (default: 'utf-8') */
|
|
31
|
-
encoding?: string;
|
|
32
|
-
/** Error callback for logging/handling errors */
|
|
33
|
-
onError?: (error: TransportError) => void;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Error codes for MLLP frame-level errors
|
|
37
|
-
*/
|
|
38
|
-
export declare const TransportErrorCode: {
|
|
39
|
-
/** Stream ended with incomplete MLLP message */
|
|
40
|
-
readonly INCOMPLETE_MESSAGE: "INCOMPLETE_MESSAGE";
|
|
41
|
-
/** Frame does not end with MLLP end sequence (0x1C 0x0D) */
|
|
42
|
-
readonly INVALID_END_SEQUENCE: "INVALID_END_SEQUENCE";
|
|
43
|
-
/** Frame does not start with MLLP start byte (0x0B) */
|
|
44
|
-
readonly INVALID_START_BYTE: "INVALID_START_BYTE";
|
|
45
|
-
/** Message exceeds configured maximum size */
|
|
46
|
-
readonly MESSAGE_TOO_LARGE: "MESSAGE_TOO_LARGE";
|
|
47
|
-
};
|
|
48
|
-
export type TransportErrorCode = (typeof TransportErrorCode)[keyof typeof TransportErrorCode];
|
|
49
|
-
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/transport/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,IAAI,EAAE,UAAU,CAAC;IACjB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC3C;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB;IAC7B,gDAAgD;;IAEhD,4DAA4D;;IAE5D,uDAAuD;;IAEvD,8CAA8C;;CAEtC,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAC5B,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC"}
|