@glion/mllp 0.15.3 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -59
- package/dist/errors.d.ts +37 -17
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -38
- package/dist/mllp.js +35 -406
- package/dist/mllp.js.map +1 -1
- package/dist/node/adapter.d.ts.map +1 -1
- package/dist/node/serve.d.ts +6 -4
- package/dist/node/serve.d.ts.map +1 -1
- package/dist/node.js +36 -11
- package/dist/node.js.map +1 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/mllp.d.ts +4 -2
- package/dist/server/mllp.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -9
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +17 -17
- 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/mllp.js
CHANGED
|
@@ -1,421 +1,48 @@
|
|
|
1
1
|
import { value } from "@glion/util-query";
|
|
2
2
|
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
3
|
//#region src/errors.ts
|
|
29
4
|
/**
|
|
30
|
-
*
|
|
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
|
|
5
|
+
* Errors raised by the `@glion/mllp` server itself.
|
|
186
6
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
7
|
+
* Per ADR 0018: one base error class discriminated by a stable `code` string.
|
|
8
|
+
* A NAK the server *builds* in response to a message is an `@glion/ack`
|
|
9
|
+
* `AckException` — a different concern from a server-lifecycle/config failure,
|
|
10
|
+
* which is what `MllpServerError` represents.
|
|
189
11
|
*
|
|
190
|
-
*
|
|
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.
|
|
12
|
+
* @module
|
|
224
13
|
*/
|
|
225
|
-
|
|
226
|
-
const opts = {
|
|
227
|
-
...defaultDecoderOptions,
|
|
228
|
-
...options
|
|
229
|
-
};
|
|
230
|
-
const textDecoder = new TextDecoder(opts.encoding);
|
|
14
|
+
const MllpServerErrorCode = {
|
|
231
15
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
16
|
+
* An inbound message could not be decoded as UTF-8 — the only character set
|
|
17
|
+
* supported for now. The underlying `@glion/util-charset` `CharsetError` is
|
|
18
|
+
* kept on `cause`; callers branch on this code without importing that
|
|
19
|
+
* package.
|
|
234
20
|
*/
|
|
235
|
-
|
|
236
|
-
/** Current state machine position. */
|
|
237
|
-
let state = "WAITING_START";
|
|
21
|
+
INCOMPATIBLE_CHARSET: "INCOMPATIBLE_CHARSET",
|
|
238
22
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
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.
|
|
23
|
+
* `app.handle()` was called before a parser was registered via
|
|
24
|
+
* `app.parser()`.
|
|
258
25
|
*/
|
|
259
|
-
|
|
260
|
-
for (let i = startPos; i < view.length - 1; i++) if (view[i] === 28 && view[i + 1] === 13) return i;
|
|
261
|
-
return -1;
|
|
262
|
-
}
|
|
26
|
+
NO_PARSER: "NO_PARSER",
|
|
263
27
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* The loop uses a local `position` cursor that always resets to 0
|
|
271
|
-
* after a `consume()` call, since consumption shifts the view.
|
|
28
|
+
* The MLLP byte stream itself was violated and the connection closed:
|
|
29
|
+
* the remote system sent bytes outside a message, glued or unterminated
|
|
30
|
+
* messages, an over-cap message — or a handler's response contained a
|
|
31
|
+
* reserved VT/FS byte and could not be framed. The underlying
|
|
32
|
+
* `@glion/mllp-codec` `MllpCodecError` is kept on `cause`; branch on this
|
|
33
|
+
* code without importing that package.
|
|
272
34
|
*/
|
|
273
|
-
|
|
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
|
-
}
|
|
35
|
+
PROTOCOL_VIOLATION: "PROTOCOL_VIOLATION"
|
|
383
36
|
};
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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;
|
|
37
|
+
/** The one error class the `@glion/mllp` server raises. Branch on `code`. */
|
|
38
|
+
var MllpServerError = class extends Error {
|
|
39
|
+
code;
|
|
40
|
+
constructor(code, message, opts) {
|
|
41
|
+
super(message, opts);
|
|
42
|
+
this.name = "MllpServerError";
|
|
43
|
+
this.code = code;
|
|
416
44
|
}
|
|
417
|
-
|
|
418
|
-
}
|
|
45
|
+
};
|
|
419
46
|
//#endregion
|
|
420
47
|
//#region src/server/compose.ts
|
|
421
48
|
/**
|
|
@@ -724,15 +351,17 @@ var Mllp = class {
|
|
|
724
351
|
* This is the integration point — analogous to Hono's `fetch()`.
|
|
725
352
|
*
|
|
726
353
|
* The pipeline is lazy (see ADR-0013):
|
|
354
|
+
*
|
|
727
355
|
* 1. Parse (sync, fast) — always runs, extracts routing fields
|
|
728
356
|
* 2. Route match — uses pre-transform routing fields
|
|
729
357
|
* 3. If no match → return undefined (no transform/compile cost)
|
|
730
358
|
* 4. Transform/compile — only when handlers access ctx.tree()/ctx.result()
|
|
731
359
|
*
|
|
732
|
-
* Throws `
|
|
360
|
+
* Throws `MllpServerError` (`NO_PARSER`) if no processor has been registered
|
|
361
|
+
* via `app.parser()`.
|
|
733
362
|
*/
|
|
734
363
|
async handle(raw, bytes, connection) {
|
|
735
|
-
if (!this.#processor) throw new
|
|
364
|
+
if (!this.#processor) throw new MllpServerError(MllpServerErrorCode.NO_PARSER, "No parser registered. Call app.parser() before handling messages.");
|
|
736
365
|
const ctx = createContext({
|
|
737
366
|
bytes,
|
|
738
367
|
connection,
|
|
@@ -786,6 +415,6 @@ var Mllp = class {
|
|
|
786
415
|
}
|
|
787
416
|
};
|
|
788
417
|
//#endregion
|
|
789
|
-
export {
|
|
418
|
+
export { MllpServerError as a, parsePattern as i, getMessageInfo as n, MllpServerErrorCode as o, matchPattern as r, Mllp as t };
|
|
790
419
|
|
|
791
420
|
//# sourceMappingURL=mllp.js.map
|