@glion/mllp 0.14.1

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +486 -0
  3. package/dist/errors.d.ts +22 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +38 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/mllp.js +791 -0
  10. package/dist/mllp.js.map +1 -0
  11. package/dist/node/adapter.d.ts +75 -0
  12. package/dist/node/adapter.d.ts.map +1 -0
  13. package/dist/node/index.d.ts +15 -0
  14. package/dist/node/index.d.ts.map +1 -0
  15. package/dist/node/serve.d.ts +150 -0
  16. package/dist/node/serve.d.ts.map +1 -0
  17. package/dist/node.js +324 -0
  18. package/dist/node.js.map +1 -0
  19. package/dist/server/adapter.d.ts +64 -0
  20. package/dist/server/adapter.d.ts.map +1 -0
  21. package/dist/server/compose.d.ts +15 -0
  22. package/dist/server/compose.d.ts.map +1 -0
  23. package/dist/server/context.d.ts +33 -0
  24. package/dist/server/context.d.ts.map +1 -0
  25. package/dist/server/mllp.d.ts +102 -0
  26. package/dist/server/mllp.d.ts.map +1 -0
  27. package/dist/server/router.d.ts +49 -0
  28. package/dist/server/router.d.ts.map +1 -0
  29. package/dist/server/types.d.ts +139 -0
  30. package/dist/server/types.d.ts.map +1 -0
  31. package/dist/transport/constants.d.ts +24 -0
  32. package/dist/transport/constants.d.ts.map +1 -0
  33. package/dist/transport/decoder-stream.d.ts +49 -0
  34. package/dist/transport/decoder-stream.d.ts.map +1 -0
  35. package/dist/transport/decoder.d.ts +18 -0
  36. package/dist/transport/decoder.d.ts.map +1 -0
  37. package/dist/transport/dynamic-buffer.d.ts +28 -0
  38. package/dist/transport/dynamic-buffer.d.ts.map +1 -0
  39. package/dist/transport/encoder.d.ts +14 -0
  40. package/dist/transport/encoder.d.ts.map +1 -0
  41. package/dist/transport/errors.d.ts +13 -0
  42. package/dist/transport/errors.d.ts.map +1 -0
  43. package/dist/transport/index.d.ts +8 -0
  44. package/dist/transport/index.d.ts.map +1 -0
  45. package/dist/transport/types.d.ts +49 -0
  46. package/dist/transport/types.d.ts.map +1 -0
  47. package/package.json +68 -0
package/dist/mllp.js ADDED
@@ -0,0 +1,791 @@
1
+ import { value } from "@glion/util-query";
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
+ //#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
+ //#region src/server/compose.ts
421
+ /**
422
+ * Compose an array of middleware into a single function that executes
423
+ * them in onion-model order (like Hono/Koa).
424
+ *
425
+ * Each middleware receives (ctx, next). Calling next() proceeds to the
426
+ * next middleware. If a middleware returns a Response without
427
+ * calling next(), it short-circuits the chain.
428
+ *
429
+ * Response handling follows Hono's pattern: return values are written
430
+ * to ctx.res, and middleware can also set ctx.res directly.
431
+ * Last write wins.
432
+ */
433
+ function compose(middlewares) {
434
+ return async (ctx) => {
435
+ let index = -1;
436
+ async function dispatch(i) {
437
+ if (i <= index) throw new Error("next() called multiple times");
438
+ index = i;
439
+ if (i >= middlewares.length) return;
440
+ const middleware = middlewares[i];
441
+ const result = await middleware(ctx, () => dispatch(i + 1));
442
+ if (result && typeof result === "object" && "raw" in result && typeof result.raw === "string") ctx.res = result;
443
+ }
444
+ await dispatch(0);
445
+ };
446
+ }
447
+ //#endregion
448
+ //#region src/server/context.ts
449
+ /**
450
+ * Create a new Context for an incoming message.
451
+ *
452
+ * Only the **parse** step runs eagerly (sync, fast). Routing fields are
453
+ * extracted as strings from the parsed tree immediately — these cached
454
+ * strings always reflect the original wire values (see ADR-0013).
455
+ *
456
+ * The expensive pipeline stages are lazy:
457
+ *
458
+ * - `ctx.tree()` — triggers `run()` (transformers: annotations, escape decoding,
459
+ * lint). Cached after first call.
460
+ * - `ctx.result()` — triggers `run()` + `stringify()` (compilation, e.g., JSON
461
+ * via hl7v2Jsonify). Cached after first call.
462
+ *
463
+ * If no handler matches or middleware short-circuits before accessing
464
+ * the tree, the transform and compile steps never execute.
465
+ */
466
+ function createContext(options) {
467
+ const { raw, bytes, connection, processor } = options;
468
+ const variables = /* @__PURE__ */ new Map();
469
+ let varSnapshot;
470
+ const file = new VFile(raw);
471
+ const parsed = processor.parse(file);
472
+ const controlId = value(parsed, "MSH-10")?.value ?? "";
473
+ const messageType = value(parsed, "MSH-9.1")?.value ?? "";
474
+ const triggerEvent = value(parsed, "MSH-9.2")?.value ?? "";
475
+ const messageStructure = value(parsed, "MSH-9.3")?.value ?? "";
476
+ const version = value(parsed, "MSH-12.1")?.value ?? "";
477
+ let transformedTree;
478
+ let transformPromise;
479
+ function getTree() {
480
+ if (transformedTree) return Promise.resolve(transformedTree);
481
+ if (!transformPromise) transformPromise = (async () => {
482
+ const result = await processor.run(parsed, file);
483
+ transformedTree = result;
484
+ return result;
485
+ })();
486
+ return transformPromise;
487
+ }
488
+ let resultPromise;
489
+ function getResult() {
490
+ if (!resultPromise) resultPromise = (async () => {
491
+ const tree = await getTree();
492
+ return processor.compiler ? processor.stringify(tree, file) : void 0;
493
+ })();
494
+ return resultPromise;
495
+ }
496
+ return {
497
+ ast: parsed,
498
+ connection: Object.freeze(connection),
499
+ controlId,
500
+ file,
501
+ get(key) {
502
+ return variables.get(key);
503
+ },
504
+ messageStructure,
505
+ messageType,
506
+ req: Object.freeze({
507
+ bytes,
508
+ raw
509
+ }),
510
+ res: void 0,
511
+ result: getResult,
512
+ set(key, value) {
513
+ variables.set(key, value);
514
+ varSnapshot = void 0;
515
+ },
516
+ tree: getTree,
517
+ triggerEvent,
518
+ get var() {
519
+ if (!varSnapshot) varSnapshot = Object.freeze(Object.fromEntries(variables));
520
+ return varSnapshot;
521
+ },
522
+ version
523
+ };
524
+ }
525
+ //#endregion
526
+ //#region src/server/types.ts
527
+ /**
528
+ * Parse a pattern string like "ADT^A01", "ADT^*", "*^A01", or "*"
529
+ * into a structured RoutePattern.
530
+ */
531
+ function parsePattern(pattern) {
532
+ if (pattern === "*") return {
533
+ messageType: "*",
534
+ source: pattern,
535
+ triggerEvent: "*"
536
+ };
537
+ const caretIndex = pattern.indexOf("^");
538
+ if (caretIndex === -1) return {
539
+ messageType: pattern,
540
+ source: pattern,
541
+ triggerEvent: "*"
542
+ };
543
+ return {
544
+ messageType: pattern.slice(0, caretIndex) || "*",
545
+ source: pattern,
546
+ triggerEvent: pattern.slice(caretIndex + 1) || "*"
547
+ };
548
+ }
549
+ /**
550
+ * Test whether a route pattern matches a given message type and trigger event.
551
+ */
552
+ function matchPattern(pattern, messageType, triggerEvent) {
553
+ const typeMatch = pattern.messageType === "*" || pattern.messageType === messageType;
554
+ const eventMatch = pattern.triggerEvent === "*" || pattern.triggerEvent === triggerEvent;
555
+ return typeMatch && eventMatch;
556
+ }
557
+ //#endregion
558
+ //#region src/server/router.ts
559
+ /**
560
+ * Create a matcher function from a string pattern.
561
+ */
562
+ function patternMatcher(pattern) {
563
+ const parsed = parsePattern(pattern);
564
+ return (ctx) => matchPattern(parsed, ctx.messageType, ctx.triggerEvent);
565
+ }
566
+ /**
567
+ * Pattern-matching router for HL7v2 message types.
568
+ *
569
+ * Routes are matched in registration order (first match wins).
570
+ * Supports string patterns with wildcards or filter functions.
571
+ *
572
+ * String patterns:
573
+ *
574
+ * - `"ADT^A01"` — exact match
575
+ * - `"ADT^*"` — any ADT message
576
+ * - `"*"` — catch-all
577
+ *
578
+ * Filter functions:
579
+ *
580
+ * - `(ctx) => ctx.messageType === "ADT" && ctx.version === "2.5.1"`
581
+ */
582
+ var Router = class {
583
+ #routes = [];
584
+ #globalMiddleware = [];
585
+ #scopedMiddleware = [];
586
+ /**
587
+ * Register a route handler with a string pattern or filter function.
588
+ */
589
+ add(patternOrFilter, handler) {
590
+ const filter = typeof patternOrFilter === "string" ? patternMatcher(patternOrFilter) : patternOrFilter;
591
+ this.#routes.push({
592
+ filter,
593
+ handler
594
+ });
595
+ }
596
+ addMiddleware(patternOrFilterOrMiddleware, middlewareOrPrepend) {
597
+ if (typeof patternOrFilterOrMiddleware === "function" && (middlewareOrPrepend === void 0 || typeof middlewareOrPrepend === "boolean")) {
598
+ if (middlewareOrPrepend === true) this.#globalMiddleware.unshift(patternOrFilterOrMiddleware);
599
+ else this.#globalMiddleware.push(patternOrFilterOrMiddleware);
600
+ return;
601
+ }
602
+ if (typeof middlewareOrPrepend === "function") {
603
+ const middleware = middlewareOrPrepend;
604
+ const filter = typeof patternOrFilterOrMiddleware === "string" ? patternMatcher(patternOrFilterOrMiddleware) : patternOrFilterOrMiddleware;
605
+ this.#scopedMiddleware.push({
606
+ filter,
607
+ middleware
608
+ });
609
+ }
610
+ }
611
+ /**
612
+ * Match a context against registered routes and middleware.
613
+ * Returns the first matching handler and all applicable middleware.
614
+ */
615
+ match(ctx) {
616
+ let handler;
617
+ for (const route of this.#routes) if (route.filter(ctx)) {
618
+ handler = route.handler;
619
+ break;
620
+ }
621
+ const middlewares = [...this.#globalMiddleware];
622
+ for (const scoped of this.#scopedMiddleware) if (scoped.filter(ctx)) middlewares.push(scoped.middleware);
623
+ return {
624
+ handler,
625
+ middlewares
626
+ };
627
+ }
628
+ };
629
+ //#endregion
630
+ //#region src/server/mllp.ts
631
+ /**
632
+ * Internal store for associating message routing info with errors.
633
+ * Uses a WeakMap so error objects can be garbage-collected normally.
634
+ */
635
+ const errorMessageInfo = /* @__PURE__ */ new WeakMap();
636
+ /**
637
+ * Retrieve the {@link MessageInfo} associated with an error thrown
638
+ * by `Mllp.handle()`. Returns `undefined` for errors that did not
639
+ * originate from message processing (e.g., lifecycle callback errors).
640
+ */
641
+ function getMessageInfo(error) {
642
+ return errorMessageInfo.get(error);
643
+ }
644
+ /**
645
+ * MLLP application for HL7v2 messaging.
646
+ *
647
+ * A pure routing and middleware engine with no TCP/server concerns.
648
+ * Use `serve()` to bind this to a TCP server.
649
+ *
650
+ * @example
651
+ * ```typescript
652
+ * import { Mllp } from "@glion/mllp";
653
+ * import { parseHL7v2 } from "@glion/hl7v2";
654
+ *
655
+ * const app = new Mllp().parser(parseHL7v2).on("ADT^A01", async (ctx) => {
656
+ * const tree = await ctx.tree();
657
+ * return { raw: "..." };
658
+ * });
659
+ * ```;
660
+ */
661
+ var Mllp = class {
662
+ #router = new Router();
663
+ #processor;
664
+ #errorHandler;
665
+ /**
666
+ * Register an HL7v2 processor for incoming messages.
667
+ *
668
+ * Accepts a unified `Processor` — either a pre-built one like
669
+ * `parseHL7v2` from `@glion/hl7v2`, or a custom composition
670
+ * via `unified().use(hl7v2Parser).use(...)`.
671
+ *
672
+ * Must be called before `handle()`. Calling multiple times replaces
673
+ * the previous processor (last-write-wins).
674
+ */
675
+ parser(processor) {
676
+ this.#processor = processor;
677
+ return this;
678
+ }
679
+ use(first, second) {
680
+ if ((typeof first === "string" || typeof first === "function") && typeof second === "function") {
681
+ this.#router.addMiddleware(first, second);
682
+ return this;
683
+ }
684
+ if (typeof first === "function") {
685
+ const prepend = second && typeof second === "object" ? second.prepend : false;
686
+ this.#router.addMiddleware(first, prepend);
687
+ return this;
688
+ }
689
+ return this;
690
+ }
691
+ /**
692
+ * Register a route handler for a message type pattern or filter function.
693
+ *
694
+ * String patterns:
695
+ *
696
+ * - `"ADT^A01"` — exact match
697
+ * - `"ADT^*"` — any ADT message
698
+ * - `"*^A01"` — any type with A01 trigger
699
+ * - `"*"` — catch-all
700
+ *
701
+ * Filter functions:
702
+ *
703
+ * - `(ctx) => ctx.messageType === "ADT" && ctx.version === "2.5.1"`
704
+ */
705
+ on(patternOrFilter, handler) {
706
+ this.#router.add(patternOrFilter, handler);
707
+ return this;
708
+ }
709
+ /**
710
+ * Register a global error handler.
711
+ *
712
+ * Called when middleware or a handler throws. Without an error handler,
713
+ * the error is re-thrown to the caller (e.g. `serve()`).
714
+ *
715
+ * If the error handler returns a response, it is used as the reply.
716
+ * If the error handler itself throws, the new error is re-thrown.
717
+ */
718
+ onError(handler) {
719
+ this.#errorHandler = handler;
720
+ return this;
721
+ }
722
+ /**
723
+ * Process a raw HL7v2 message through the middleware chain and router.
724
+ * This is the integration point — analogous to Hono's `fetch()`.
725
+ *
726
+ * The pipeline is lazy (see ADR-0013):
727
+ * 1. Parse (sync, fast) — always runs, extracts routing fields
728
+ * 2. Route match — uses pre-transform routing fields
729
+ * 3. If no match → return undefined (no transform/compile cost)
730
+ * 4. Transform/compile — only when handlers access ctx.tree()/ctx.result()
731
+ *
732
+ * Throws `MllpError` if no processor has been registered via `app.parser()`.
733
+ */
734
+ async handle(raw, bytes, connection) {
735
+ if (!this.#processor) throw new MllpError("ERR_NO_PARSER", "No parser registered. Call app.parser() before handling messages.");
736
+ const ctx = createContext({
737
+ bytes,
738
+ connection,
739
+ processor: this.#processor,
740
+ raw
741
+ });
742
+ try {
743
+ const match = this.#router.match(ctx);
744
+ const middlewares = [...match.middlewares];
745
+ if (match.handler) {
746
+ const handler = match.handler;
747
+ middlewares.push((handlerCtx) => handler(handlerCtx));
748
+ }
749
+ await compose(middlewares)(ctx);
750
+ return ctx.res;
751
+ } catch (error) {
752
+ return this.#handleError(error instanceof Error ? error : new Error(String(error)), ctx);
753
+ }
754
+ }
755
+ /**
756
+ * Handle an error during message processing.
757
+ *
758
+ * If a custom error handler is registered, delegates to it.
759
+ * If the error handler itself throws, re-throws the error handler's error.
760
+ * If no error handler is registered, re-throws the original error.
761
+ *
762
+ * In both re-throw cases, message routing fields are attached to the
763
+ * error via {@link getMessageInfo} so callers can include message
764
+ * context in error reporting.
765
+ *
766
+ * Callers (e.g. `serve()`) are responsible for catching these errors
767
+ * and deciding how to handle them at the transport level.
768
+ */
769
+ async #handleError(err, ctx) {
770
+ const info = {
771
+ controlId: ctx.controlId,
772
+ messageStructure: ctx.messageStructure,
773
+ messageType: ctx.messageType,
774
+ triggerEvent: ctx.triggerEvent,
775
+ version: ctx.version
776
+ };
777
+ if (this.#errorHandler) try {
778
+ return await this.#errorHandler(err, ctx);
779
+ } catch (handlerError) {
780
+ const e = handlerError instanceof Error ? handlerError : new Error(String(handlerError));
781
+ errorMessageInfo.set(e, info);
782
+ throw e;
783
+ }
784
+ errorMessageInfo.set(err, info);
785
+ throw err;
786
+ }
787
+ };
788
+ //#endregion
789
+ export { encode as a, createDecoderStream as c, MllpError as d, MLLP_END_BYTE_1 as f, MLLP_TRAILER as g, MLLP_START_BYTE as h, parsePattern as i, TransportErrorCode as l, MLLP_HEADER as m, getMessageInfo as n, encodeMultiple as o, MLLP_END_BYTE_2 as p, matchPattern as r, MLLPDecoderStream as s, Mllp as t, TransportError as u };
790
+
791
+ //# sourceMappingURL=mllp.js.map