@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 CHANGED
@@ -57,33 +57,46 @@ The server class. Built with a fluent chainable API:
57
57
 
58
58
  Start a Node.js or Bun TCP server that dispatches incoming MLLP frames through the `Mllp` instance.
59
59
 
60
- - `port` (`number`) — TCP port to listen on.
61
- - `hostname` (`string`, optional) — interface to bind. Defaults to all interfaces.
62
- - `tls` (`{ cert, key }`, optional) — enable MLLP over TLS.
63
-
64
- ### Primitives
65
-
66
- | Function | Description |
67
- | ------------------------------- | ------------------------------------------------------ |
68
- | `encode(message)` | Encode a message string into an MLLP frame. |
69
- | `decode(frame)` | Decode a single MLLP frame to its message. |
70
- | `encodeMultiple(messages)` | Encode an array of messages in one pass. |
71
- | `createDecoderStream(options?)` | TransformStream for streaming decode from chunked TCP. |
60
+ | Option | Type | Description |
61
+ | ----------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------- |
62
+ | `port` | `number` | TCP port to listen on. |
63
+ | `hostname` | `string`, optional | Interface to bind. Defaults to all interfaces. |
64
+ | `tls` | `{ cert, key, ca?, passphrase? }`, optional | Enable MLLP over TLS. |
65
+ | `keepAlive` | `boolean`, optional | Enable TCP keep-alive on accepted sockets. Defaults to `true`. |
66
+ | `keepAliveInitialDelay` | `number`, optional | Delay in ms before the first keep-alive probe on an idle socket. Defaults to `60000`. |
67
+ | `socketTimeout` | `number`, optional | Socket inactivity timeout in ms — an idle socket past it is destroyed. `0` (default) disables. |
68
+ | `onConnect` | `ConnectionCallback`, optional | Called when a new TCP connection is accepted. |
69
+ | `onDisconnect` | `ConnectionCallback`, optional | Called when a connection closes — always fires, even if `onConnect` threw. |
70
+ | `onError` | `ErrorCallback`, optional | Called when a handler error goes unhandled or a lifecycle callback throws. |
71
+
72
+ ### Wire framing
73
+
74
+ MLLP framing lives in [`@glion/mllp-codec`](https://github.com/rethinkhealth/glion/tree/main/packages/mllp-codec) — `frame()` wraps outbound messages, `unframe()` streams inbound bytes back into them. `@glion/mllp` consumes it internally and re-exports nothing from it; import it directly when you need the codec.
72
75
 
73
76
  ### Types
74
77
 
75
- | Type | Description |
76
- | ------------------ | ------------------------------------------------------------------- |
77
- | `Context` | Request context with message data and routing fields. |
78
- | `Response` | Response object `{ raw: string }`. |
79
- | `Hl7v2Processor` | Unified `Processor` type for HL7v2 (`Processor<Root, Root, Root>`). |
80
- | `Middleware` | Middleware function `(ctx, next) => ...`. |
81
- | `Handler` | Terminal route handler `(ctx) => Response`. |
82
- | `ErrorHandler` | Error handler `(err, ctx) => Response`. |
83
- | `RouteFilter` | Filter function `(ctx) => boolean` used for routing. |
84
- | `MiddlewareReturn` | Return type of middleware functions. |
85
- | `ConnectionInfo` | Connection metadata (remote/local address, TLS flag). |
86
- | `RoutePattern` | Parsed route pattern. |
78
+ | Type | Description |
79
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | `Context` | Request context with message data and routing fields. |
81
+ | `Response` | Response object `{ raw: string }`. |
82
+ | `Hl7v2Processor` | Unified `Processor` type for HL7v2 (`Processor<Root, Root \| undefined, Root \| undefined>` — head/tail admit `undefined` so the bare `unified().use(hl7v2Parser).freeze()` is assignable). |
83
+ | `Middleware` | Middleware function `(ctx, next) => ...`. |
84
+ | `Handler` | Terminal route handler `(ctx) => Response`. |
85
+ | `ErrorHandler` | Error handler `(err, ctx) => Response`. |
86
+ | `RouteFilter` | Filter function `(ctx) => boolean` used for routing. |
87
+ | `MiddlewareReturn` | Return type of middleware functions. |
88
+ | `ConnectionInfo` | Connection metadata (remote/local address, TLS flag). |
89
+ | `RoutePattern` | Parsed route pattern. |
90
+
91
+ ### Errors
92
+
93
+ Server-side failures are raised (or passed to `onError`) as `MllpServerError` — branch on its stable `code`; the underlying codec or charset error rides on `cause`:
94
+
95
+ | Code | Meaning |
96
+ | ---------------------- | ------------------------------------------------------------------------------------------------- |
97
+ | `NO_PARSER` | `app.handle()` ran before a parser was registered via `app.parser()`. |
98
+ | `INCOMPATIBLE_CHARSET` | An inbound message could not be decoded as UTF-8 (the `CharsetError` is on `cause`). |
99
+ | `PROTOCOL_VIOLATION` | The MLLP byte stream was violated and the connection closed (the `MllpCodecError` is on `cause`). |
87
100
 
88
101
  ## Routing
89
102
 
@@ -256,39 +269,6 @@ const server = serve(app, {
256
269
  });
257
270
  ```
258
271
 
259
- ## Primitives
260
-
261
- ### Simple API
262
-
263
- ```ts
264
- import { encode, decode, encodeMultiple } from "@glion/mllp";
265
-
266
- const mllpFrame = encode(hl7Message);
267
- const decoded = decode(mllpFrame);
268
- console.log(decoded.text);
269
-
270
- const frames = encodeMultiple(["MSH|1", "MSH|2"]);
271
- ```
272
-
273
- ### Streaming API
274
-
275
- ```ts
276
- import { createDecoderStream } from "@glion/mllp";
277
-
278
- const decoder = createDecoderStream({
279
- maxMessageSize: 1024 * 1024,
280
- onError: (error) => console.warn(`[${error.code}] ${error.message}`),
281
- });
282
-
283
- tcpSocket.readable.pipeThrough(decoder).pipeTo(
284
- new WritableStream({
285
- write(message) {
286
- console.log("Received:", message.text);
287
- },
288
- })
289
- );
290
- ```
291
-
292
272
  ## Design notes
293
273
 
294
274
  ### Why no default error response?
@@ -299,14 +279,13 @@ Instead, the `Mllp` engine is middleware-first:
299
279
 
300
280
  - **Default behaviour** — no response is sent; the sending system times out and retries, which is valid MLLP behaviour.
301
281
  - **Logging** — add a logger middleware to make errors observable.
302
- - **ACK/NAK** — add an acknowledgment middleware to translate errors into proper NAK responses. `@glion/mllp-ack` provides this out of the box.
282
+ - **ACK/NAK** — register your own acknowledgment middleware to translate outcomes into ACK/NAK responses. Built-in acknowledgment translation at the framework's error boundary is landing next (see ADR-0019).
303
283
  - **Custom error responses** — use `app.onError()` for application-specific handling.
304
284
 
305
285
  ```ts
306
286
  const app = new Mllp().parser(parseHL7v2);
307
287
 
308
288
  app.use(logger()); // observability — provided by middleware
309
- app.use(ackMiddleware()); // error → NAK translation — @glion/mllp-ack
310
289
 
311
290
  app.on("ADT^A01", handler);
312
291
  ```
package/dist/errors.d.ts CHANGED
@@ -1,22 +1,42 @@
1
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.
2
+ * Errors raised by the `@glion/mllp` server itself.
3
+ *
4
+ * Per ADR 0018: one base error class discriminated by a stable `code` string.
5
+ * A NAK the server *builds* in response to a message is an `@glion/ack`
6
+ * `AckException` — a different concern from a server-lifecycle/config failure,
7
+ * which is what `MllpServerError` represents.
10
8
  *
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).
9
+ * @module
14
10
  */
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);
11
+ export declare const MllpServerErrorCode: {
12
+ /**
13
+ * An inbound message could not be decoded as UTF-8 — the only character set
14
+ * supported for now. The underlying `@glion/util-charset` `CharsetError` is
15
+ * kept on `cause`; callers branch on this code without importing that
16
+ * package.
17
+ */
18
+ readonly INCOMPATIBLE_CHARSET: "INCOMPATIBLE_CHARSET";
19
+ /**
20
+ * `app.handle()` was called before a parser was registered via
21
+ * `app.parser()`.
22
+ */
23
+ readonly NO_PARSER: "NO_PARSER";
24
+ /**
25
+ * The MLLP byte stream itself was violated and the connection closed:
26
+ * the remote system sent bytes outside a message, glued or unterminated
27
+ * messages, an over-cap message — or a handler's response contained a
28
+ * reserved VT/FS byte and could not be framed. The underlying
29
+ * `@glion/mllp-codec` `MllpCodecError` is kept on `cause`; branch on this
30
+ * code without importing that package.
31
+ */
32
+ readonly PROTOCOL_VIOLATION: "PROTOCOL_VIOLATION";
33
+ };
34
+ export type MllpServerErrorCode = (typeof MllpServerErrorCode)[keyof typeof MllpServerErrorCode];
35
+ /** The one error class the `@glion/mllp` server raises. Branch on `code`. */
36
+ export declare class MllpServerError extends Error {
37
+ readonly code: MllpServerErrorCode;
38
+ constructor(code: MllpServerErrorCode, message: string, opts?: {
39
+ cause?: unknown;
40
+ });
21
41
  }
22
42
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,eAAO,MAAM,mBAAmB;IAC9B;;;;;OAKG;;IAEH;;;OAGG;;IAEH;;;;;;;OAOG;;CAEK,CAAC;AACX,MAAM,MAAM,mBAAmB,GAC7B,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,OAAO,mBAAmB,CAAC,CAAC;AAEjE,6EAA6E;AAC7E,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;gBAEjC,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAM7B"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,4 @@
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 "./transport";
2
- export type { DecodedMessage, DecoderOptions, EncoderInput, EncoderOptions, } from "./transport";
3
- export { MllpError } from "./errors";
4
- export type { MllpErrorOptions } from "./errors";
1
+ export { MllpServerError, MllpServerErrorCode } from "./errors";
5
2
  export { getMessageInfo, Mllp } from "./server/mllp";
6
3
  export type { MessageInfo } from "./server/mllp";
7
4
  export type { ConnectionInfo, Context, ErrorHandler, Handler, Hl7v2Processor, Middleware, MiddlewareReturn, Response, RouteFilter, RoutePattern, } from "./server/types";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,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,GAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,cAAc,EACd,cAAc,EACd,YAAY,EACZ,cAAc,GACf,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAKjD,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAChE,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,2 @@
1
- import { a as encode, c as createDecoderStream, d as MllpError, f as MLLP_END_BYTE_1, g as MLLP_TRAILER, h as MLLP_START_BYTE, i as parsePattern, l as TransportErrorCode, m as MLLP_HEADER, n as getMessageInfo, o as encodeMultiple, p as MLLP_END_BYTE_2, r as matchPattern, s as MLLPDecoderStream, t as Mllp, u as TransportError } from "./mllp.js";
2
- //#region src/transport/decoder.ts
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 { a as MllpServerError, i as parsePattern, n as getMessageInfo, o as MllpServerErrorCode, r as matchPattern, t as Mllp } from "./mllp.js";
2
+ export { Mllp, MllpServerError, MllpServerErrorCode, getMessageInfo, matchPattern, parsePattern };