@glion/mllp 0.15.1 → 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -297
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -1,232 +1,123 @@
|
|
|
1
1
|
# @glion/mllp
|
|
2
2
|
|
|
3
|
-
MLLP
|
|
3
|
+
Transport-agnostic MLLP engine and middleware-driven MLLP server for HL7v2 messaging.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`@glion/mllp` provides everything you need to send and receive HL7v2 messages over MLLP/TCP: frame encoding and decoding primitives, streaming TransformStreams for chunked TCP, and an `Mllp` class with pattern-based routing, composable middleware, and first-class `unified` processor integration. The server is transport-agnostic at its core and ships with a `serve()` helper for Node.js and Bun.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
2. **Server** — Hono-style `Mllp` class with middleware, pattern-based routing, and unified processor integration
|
|
11
|
-
|
|
12
|
-
**Key characteristics:**
|
|
13
|
-
|
|
14
|
-
- **Hono-style API** — `.use()` middleware, `.on()` pattern routing, fluent chaining
|
|
15
|
-
- **Unified integration** — pass a unified processor directly to `.use()`
|
|
16
|
-
- **Web Streams** — built on `ReadableStream`/`WritableStream` throughout
|
|
17
|
-
- **Dual API** — simple functions for one-shot operations + streaming for TCP
|
|
18
|
-
|
|
19
|
-
## Installation
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
pnpm add @glion/mllp
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### Package Exports
|
|
26
|
-
|
|
27
|
-
| Subpath | Description |
|
|
28
|
-
| ------------------ | ---------------------------------- |
|
|
29
|
-
| `@glion/mllp` | Core `Mllp` class and primitives |
|
|
30
|
-
| `@glion/mllp/node` | `serve()` function for Node.js/Bun |
|
|
31
|
-
|
|
32
|
-
## Glion CLI — dev and start
|
|
33
|
-
|
|
34
|
-
The `glion` CLI provides a zero-config development server with live reload and a production-ready start command.
|
|
35
|
-
|
|
36
|
-
### Quick Start (Zero-Config)
|
|
37
|
-
|
|
38
|
-
Create a single entry file and run:
|
|
9
|
+
## Install
|
|
39
10
|
|
|
40
11
|
```bash
|
|
41
|
-
|
|
42
|
-
import { Mllp } from "@glion/mllp";
|
|
43
|
-
import { parseHL7v2 } from "@glion/hl7v2";
|
|
44
|
-
export default new Mllp().parser(parseHL7v2);
|
|
45
|
-
EOF
|
|
46
|
-
|
|
47
|
-
pnpm add -D @glion/mllp @glion/hl7v2
|
|
48
|
-
pnpm glion dev
|
|
12
|
+
npm install @glion/mllp
|
|
49
13
|
```
|
|
50
14
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
### With an Explicit Config File
|
|
15
|
+
### Package exports
|
|
54
16
|
|
|
55
|
-
|
|
17
|
+
| Subpath | Description |
|
|
18
|
+
| ------------------ | --------------------------------- |
|
|
19
|
+
| `@glion/mllp` | Core `Mllp` class and primitives |
|
|
20
|
+
| `@glion/mllp/node` | `serve()` helper for Node and Bun |
|
|
56
21
|
|
|
57
|
-
|
|
58
|
-
// glion.config.ts
|
|
59
|
-
import { defineConfig } from "@glion/mllp/config";
|
|
22
|
+
## Use
|
|
60
23
|
|
|
61
|
-
|
|
62
|
-
entry: "./src/app.ts",
|
|
63
|
-
port: 2575,
|
|
64
|
-
hostname: "0.0.0.0",
|
|
65
|
-
// tls: { cert: "./certs/server.pem", key: "./certs/server.key" },
|
|
66
|
-
// watch: ["./src"],
|
|
67
|
-
// gracefulCloseMs: 5000,
|
|
68
|
-
});
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
72
|
-
// src/app.ts
|
|
24
|
+
```ts
|
|
73
25
|
import { Mllp } from "@glion/mllp";
|
|
26
|
+
import { serve } from "@glion/mllp/node";
|
|
74
27
|
import { parseHL7v2 } from "@glion/hl7v2";
|
|
75
28
|
|
|
76
|
-
|
|
29
|
+
const app = new Mllp()
|
|
77
30
|
.parser(parseHL7v2)
|
|
78
|
-
.on("ADT^A01", async (ctx) => ({ raw:
|
|
79
|
-
.on("ORU^R01", async (ctx) => ({ raw:
|
|
80
|
-
.on("*", async (ctx) => ({
|
|
81
|
-
|
|
31
|
+
.on("ADT^A01", async (ctx) => ({ raw: buildAckFor(ctx) }))
|
|
32
|
+
.on("ORU^R01", async (ctx) => ({ raw: buildAckFor(ctx) }))
|
|
33
|
+
.on("*", async (ctx) => ({
|
|
34
|
+
raw: buildNakFor(ctx, "Unsupported message type"),
|
|
35
|
+
}));
|
|
82
36
|
|
|
83
|
-
|
|
84
|
-
// package.json
|
|
85
|
-
{
|
|
86
|
-
"scripts": {
|
|
87
|
-
"dev": "glion dev",
|
|
88
|
-
"start": "glion start"
|
|
89
|
-
}
|
|
90
|
-
}
|
|
37
|
+
const server = serve(app, { port: 2575 });
|
|
91
38
|
```
|
|
92
39
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
**`glion dev`** — Development mode with a live Ink TUI, file watcher, and cold restarts on save:
|
|
96
|
-
|
|
97
|
-
- Shows request/response counts, uptime, and error summaries in the TUI
|
|
98
|
-
- Watches the entry file (and configured paths) for changes
|
|
99
|
-
- Cold-restarts the server on save (~100–300ms interruption in-flight TCP sessions)
|
|
100
|
-
- Falls back to log-only mode when stdout is not a TTY (CI, piped output)
|
|
101
|
-
|
|
102
|
-
**`glion start`** — Production mode with no TUI and graceful shutdown:
|
|
103
|
-
|
|
104
|
-
- Runs without a watcher
|
|
105
|
-
- Emits JSON-line events to stdout for log aggregators
|
|
106
|
-
- Handles SIGTERM with graceful drain (configurable via `gracefulCloseMs`, default 5000ms)
|
|
107
|
-
- No in-flight connection interruption — existing TCP sessions complete before exit
|
|
108
|
-
|
|
109
|
-
### Cross-Runtime Usage
|
|
110
|
-
|
|
111
|
-
The glion bin ships with `#!/usr/bin/env node` — the standard for Node.js. Bun and Deno require explicit opt-in because they default to honoring the Node shebang.
|
|
112
|
-
|
|
113
|
-
| Runtime | Invocation |
|
|
114
|
-
| -------------- | ------------------------------------------------------------------------ |
|
|
115
|
-
| Node (default) | `pnpm dev` or `npm run dev` or `npx glion dev` |
|
|
116
|
-
| Bun | `bun --bun run dev` (from package.json script) or `bunx --bun glion dev` |
|
|
117
|
-
| Deno | `deno task dev` with a `deno.json` task that runs the compiled bin |
|
|
118
|
-
|
|
119
|
-
### Zero-Config Fallback
|
|
40
|
+
To run the app with live reload during development and a production start command, use the `@glion/cli` package — it provides the `glion dev` / `glion start` binary and configuration loader. This README focuses on the server primitives themselves.
|
|
120
41
|
|
|
121
|
-
|
|
42
|
+
## API
|
|
122
43
|
|
|
123
|
-
|
|
124
|
-
2. `glion.app.mts`
|
|
125
|
-
3. `glion.app.mjs`
|
|
126
|
-
4. `glion.app.js`
|
|
127
|
-
5. `src/glion.app.ts`
|
|
128
|
-
6. `src/glion.app.js`
|
|
44
|
+
### `Mllp`
|
|
129
45
|
|
|
130
|
-
|
|
46
|
+
The server class. Built with a fluent chainable API:
|
|
131
47
|
|
|
132
|
-
|
|
48
|
+
| Method | Description |
|
|
49
|
+
| --------------------------- | --------------------------------------------------------------------------- |
|
|
50
|
+
| `.parser(processor)` | Attach a `unified` processor (or `@glion/parser` alone) as the parse stage. |
|
|
51
|
+
| `.use(middleware)` | Register global middleware that runs for every message. |
|
|
52
|
+
| `.use(pattern, middleware)` | Register scoped middleware that runs only for matching message types. |
|
|
53
|
+
| `.on(pattern, handler)` | Register a terminal route handler for a message type or trigger event. |
|
|
54
|
+
| `.onError(errorHandler)` | Register a catch-all error handler. |
|
|
133
55
|
|
|
134
|
-
|
|
56
|
+
### `serve(app, options)` — from `@glion/mllp/node`
|
|
135
57
|
|
|
136
|
-
|
|
137
|
-
import { runGlion } from "@glion/mllp/cli";
|
|
138
|
-
import { defineConfig, type GlionConfig } from "@glion/mllp/config";
|
|
58
|
+
Start a Node.js or Bun TCP server that dispatches incoming MLLP frames through the `Mllp` instance.
|
|
139
59
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## Server
|
|
147
|
-
|
|
148
|
-
### Quick Start
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
import { Mllp } from "@glion/mllp";
|
|
152
|
-
import { parseHL7v2 } from "@glion/hl7v2";
|
|
153
|
-
import { serve } from "@glion/mllp/node";
|
|
154
|
-
|
|
155
|
-
const app = new Mllp().parser(parseHL7v2);
|
|
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.
|
|
156
63
|
|
|
157
|
-
|
|
158
|
-
app.on("ADT^A01", async (ctx) => {
|
|
159
|
-
// Handle patient admission
|
|
160
|
-
return { raw: buildAckFor(ctx) };
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
app.on("ORU^R01", async (ctx) => {
|
|
164
|
-
// Handle lab results
|
|
165
|
-
return { raw: buildAckFor(ctx) };
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
app.on("*", async (ctx) => {
|
|
169
|
-
return { raw: buildNakFor(ctx, "Unsupported message type") };
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const server = serve(app, { port: 2575 });
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Unified Processor Integration
|
|
176
|
-
|
|
177
|
-
Pass a unified processor directly to `.parser()` — the server runs `parse()` eagerly for routing, then `run()` and `stringify()` lazily when handlers access `ctx.tree()` or `ctx.result()`:
|
|
178
|
-
|
|
179
|
-
```typescript
|
|
180
|
-
import { Mllp } from "@glion/mllp";
|
|
181
|
-
import { serve } from "@glion/mllp/node";
|
|
182
|
-
import { parseHL7v2 } from "@glion/hl7v2";
|
|
183
|
-
|
|
184
|
-
const app = new Mllp().parser(parseHL7v2);
|
|
185
|
-
|
|
186
|
-
app.on("ADT^A01", async (ctx) => {
|
|
187
|
-
const tree = await ctx.tree(); // transformed AST (escape decoding, annotations, lint)
|
|
188
|
-
const result = await ctx.result(); // compiled output (e.g., JSON from hl7v2Jsonify)
|
|
189
|
-
console.log(ctx.file.messages); // lint warnings
|
|
190
|
-
return { raw: "..." };
|
|
191
|
-
});
|
|
64
|
+
### Primitives
|
|
192
65
|
|
|
193
|
-
|
|
194
|
-
|
|
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. |
|
|
195
72
|
|
|
196
|
-
###
|
|
73
|
+
### Types
|
|
197
74
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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. |
|
|
87
|
+
|
|
88
|
+
## Routing
|
|
89
|
+
|
|
90
|
+
Register routes by message type, trigger event, or pattern:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
app.on("ADT^A01", handler); // exact match
|
|
94
|
+
app.on("ADT^*", handler); // any ADT trigger event
|
|
95
|
+
app.on("*^A01", handler); // any message type with A01
|
|
96
|
+
app.on("ADT", handler); // any ADT (same as ADT^*)
|
|
97
|
+
app.on("*", handler); // catch-all
|
|
204
98
|
```
|
|
205
99
|
|
|
206
|
-
Routes are matched
|
|
100
|
+
Routes are matched first-match-wins — register specific routes before catch-alls.
|
|
207
101
|
|
|
208
|
-
|
|
102
|
+
## Middleware
|
|
209
103
|
|
|
210
|
-
Middleware
|
|
104
|
+
Middleware functions receive the request context and a `next()` callback. Pre-work runs before `next()`; post-work runs after. Returning a response from any middleware short-circuits the handler.
|
|
211
105
|
|
|
212
|
-
```
|
|
213
|
-
// Global middleware — runs for
|
|
106
|
+
```ts
|
|
107
|
+
// Global middleware — runs for every message.
|
|
214
108
|
app.use(async (ctx, next) => {
|
|
215
109
|
const start = Date.now();
|
|
216
110
|
await next();
|
|
217
111
|
console.log(`Processed in ${Date.now() - start}ms`);
|
|
218
112
|
});
|
|
219
113
|
|
|
220
|
-
// Scoped middleware — only for matching
|
|
114
|
+
// Scoped middleware — only for matching message types.
|
|
221
115
|
app.use("ADT^*", async (ctx, next) => {
|
|
222
116
|
ctx.set("isAdmission", true);
|
|
223
117
|
await next();
|
|
224
118
|
});
|
|
225
|
-
```
|
|
226
119
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
120
|
+
// Short-circuit: return a response without calling next().
|
|
230
121
|
app.use(async (ctx) => {
|
|
231
122
|
if (!isAuthorized(ctx.connection.remoteAddress)) {
|
|
232
123
|
return { raw: buildNakFor(ctx, "Unauthorized") };
|
|
@@ -234,62 +125,53 @@ app.use(async (ctx) => {
|
|
|
234
125
|
});
|
|
235
126
|
```
|
|
236
127
|
|
|
237
|
-
|
|
128
|
+
## Context
|
|
238
129
|
|
|
239
|
-
|
|
130
|
+
Available in every middleware and handler. The pipeline is lazy: only the parse step runs eagerly; transforms and compilation are deferred until accessed.
|
|
240
131
|
|
|
241
|
-
|
|
132
|
+
### Sync properties
|
|
242
133
|
|
|
243
|
-
| Property | Description
|
|
244
|
-
| ---------------------- |
|
|
245
|
-
| `ctx.req.raw` | Original HL7v2 message string |
|
|
246
|
-
| `ctx.req.bytes` | Raw bytes from the MLLP frame |
|
|
247
|
-
| `ctx.connection` | `{ remoteAddress, remotePort, localPort, secure }
|
|
248
|
-
| `ctx.messageType` | MSH-9.1 (e.g
|
|
249
|
-
| `ctx.triggerEvent` | MSH-9.2 (e.g
|
|
250
|
-
| `ctx.messageStructure` | MSH-9.3 (e.g
|
|
251
|
-
| `ctx.version` | MSH-12 (e.g
|
|
252
|
-
| `ctx.controlId` | MSH-10 message control ID |
|
|
253
|
-
| `ctx.ast` | Raw parsed AST — pre-transform, straight from the wire |
|
|
254
|
-
| `ctx.file` | VFile (diagnostics accumulate after `tree()`) |
|
|
255
|
-
| `ctx.set(key, value)` | Store a variable |
|
|
256
|
-
| `ctx.get(key)` | Retrieve a variable |
|
|
257
|
-
| `ctx.var` | Read-only snapshot of all variables |
|
|
134
|
+
| Property | Description |
|
|
135
|
+
| ---------------------- | ------------------------------------------------------- |
|
|
136
|
+
| `ctx.req.raw` | Original HL7v2 message string. |
|
|
137
|
+
| `ctx.req.bytes` | Raw bytes from the MLLP frame. |
|
|
138
|
+
| `ctx.connection` | `{ remoteAddress, remotePort, localPort, secure }`. |
|
|
139
|
+
| `ctx.messageType` | MSH-9.1 (e.g. `"ADT"`). |
|
|
140
|
+
| `ctx.triggerEvent` | MSH-9.2 (e.g. `"A01"`). |
|
|
141
|
+
| `ctx.messageStructure` | MSH-9.3 (e.g. `"ADT_A01"`). |
|
|
142
|
+
| `ctx.version` | MSH-12 (e.g. `"2.5.1"`). |
|
|
143
|
+
| `ctx.controlId` | MSH-10 message control ID. |
|
|
144
|
+
| `ctx.ast` | Raw parsed AST — pre-transform, straight from the wire. |
|
|
145
|
+
| `ctx.file` | VFile (diagnostics accumulate after `tree()`). |
|
|
146
|
+
| `ctx.set(key, value)` | Store a variable. |
|
|
147
|
+
| `ctx.get(key)` | Retrieve a variable. |
|
|
148
|
+
| `ctx.var` | Read-only snapshot of all variables. |
|
|
258
149
|
|
|
259
|
-
|
|
150
|
+
### Async methods
|
|
260
151
|
|
|
261
|
-
| Method | Triggers | Description
|
|
262
|
-
| -------------------- | ----------------------- |
|
|
263
|
-
| `await ctx.tree()` | `run()` (transform) | Transformed AST — escape decoding, annotations, lint |
|
|
264
|
-
| `await ctx.result()` | `run()` + `stringify()` | Compiled output (e.g
|
|
152
|
+
| Method | Triggers | Description |
|
|
153
|
+
| -------------------- | ----------------------- | ----------------------------------------------------- |
|
|
154
|
+
| `await ctx.tree()` | `run()` (transform) | Transformed AST — escape decoding, annotations, lint. |
|
|
155
|
+
| `await ctx.result()` | `run()` + `stringify()` | Compiled output (e.g. JSON from `hl7v2Jsonify`). |
|
|
265
156
|
|
|
266
157
|
Both are cached — subsequent calls return the same value instantly.
|
|
267
158
|
|
|
268
|
-
### `ctx.ast` vs `await ctx.tree()`
|
|
269
|
-
|
|
270
|
-
**Use `ctx.ast`** when you only need the raw message structure:
|
|
159
|
+
### `ctx.ast` vs `await ctx.tree()`
|
|
271
160
|
|
|
272
|
-
|
|
273
|
-
- Building ACK/NAK responses
|
|
274
|
-
- Route filter functions
|
|
275
|
-
- Middleware that doesn't need escape-decoded values
|
|
161
|
+
Use `ctx.ast` when you only need the raw message structure — reading MSH fields, building ACK/NAK responses, route filter functions, or middleware that doesn't need escape-decoded values:
|
|
276
162
|
|
|
277
|
-
```
|
|
278
|
-
// Fast — no pipeline cost
|
|
163
|
+
```ts
|
|
164
|
+
// Fast — no pipeline cost.
|
|
279
165
|
app.use((ctx, next) => {
|
|
280
166
|
console.log(`Received ${ctx.messageType}^${ctx.triggerEvent}`);
|
|
281
167
|
return next();
|
|
282
168
|
});
|
|
283
169
|
```
|
|
284
170
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
- Business logic that reads decoded field values
|
|
288
|
-
- Handlers that inspect annotations or resolved message structures
|
|
289
|
-
- Any operation that depends on transformer output
|
|
171
|
+
Use `await ctx.tree()` when you need the fully processed tree — business logic that reads decoded field values, or handlers that inspect annotations or resolved message structures:
|
|
290
172
|
|
|
291
|
-
```
|
|
292
|
-
// Triggers transform pipeline on first call
|
|
173
|
+
```ts
|
|
174
|
+
// Triggers transform pipeline on first call.
|
|
293
175
|
app.on("ADT^A01", async (ctx) => {
|
|
294
176
|
const tree = await ctx.tree();
|
|
295
177
|
// tree has escape sequences decoded, message structure resolved, etc.
|
|
@@ -297,23 +179,22 @@ app.on("ADT^A01", async (ctx) => {
|
|
|
297
179
|
});
|
|
298
180
|
```
|
|
299
181
|
|
|
300
|
-
|
|
182
|
+
Use `await ctx.result()` when you need the compiled output:
|
|
301
183
|
|
|
302
|
-
```
|
|
184
|
+
```ts
|
|
303
185
|
app.on("ORU^R01", async (ctx) => {
|
|
304
186
|
const json = await ctx.result(); // triggers transform + compile
|
|
305
|
-
// json is the Hl7v2JsonResult from hl7v2Jsonify
|
|
306
187
|
await saveToDatabase(json);
|
|
307
188
|
return { raw: "..." };
|
|
308
189
|
});
|
|
309
190
|
```
|
|
310
191
|
|
|
311
|
-
### Writing
|
|
192
|
+
### Writing middleware — prefer `ctx.ast`
|
|
312
193
|
|
|
313
|
-
|
|
194
|
+
Most middleware only needs routing fields or raw MSH data — both available synchronously from `ctx.ast`. Reach for `await ctx.tree()` only when you genuinely need the transformed tree.
|
|
314
195
|
|
|
315
|
-
```
|
|
316
|
-
//
|
|
196
|
+
```ts
|
|
197
|
+
// Good — sync, fast, no pipeline cost.
|
|
317
198
|
function authMiddleware(): Middleware {
|
|
318
199
|
return (ctx, next) => {
|
|
319
200
|
if (!isAuthorized(ctx.connection.remoteAddress)) {
|
|
@@ -323,8 +204,7 @@ function authMiddleware(): Middleware {
|
|
|
323
204
|
};
|
|
324
205
|
}
|
|
325
206
|
|
|
326
|
-
//
|
|
327
|
-
// The acknowledge() function only reads MSH fields
|
|
207
|
+
// Good — ACK middleware reads MSH fields from the pre-transform tree.
|
|
328
208
|
function ackMiddleware(): Middleware {
|
|
329
209
|
return async (ctx, next) => {
|
|
330
210
|
await next();
|
|
@@ -332,11 +212,10 @@ function ackMiddleware(): Middleware {
|
|
|
332
212
|
};
|
|
333
213
|
}
|
|
334
214
|
|
|
335
|
-
//
|
|
215
|
+
// Only when needed — triggers transform pipeline.
|
|
336
216
|
function validationMiddleware(): Middleware {
|
|
337
217
|
return async (ctx, next) => {
|
|
338
218
|
const tree = await ctx.tree();
|
|
339
|
-
// tree has escape sequences decoded — needed for value validation
|
|
340
219
|
if (!isValid(tree)) {
|
|
341
220
|
return { raw: buildNak(ctx.ast, "Invalid") };
|
|
342
221
|
}
|
|
@@ -345,22 +224,22 @@ function validationMiddleware(): Middleware {
|
|
|
345
224
|
}
|
|
346
225
|
```
|
|
347
226
|
|
|
348
|
-
|
|
227
|
+
## Error handling
|
|
349
228
|
|
|
350
|
-
```
|
|
229
|
+
```ts
|
|
351
230
|
app.onError(async (err, ctx) => {
|
|
352
231
|
console.error(`Error processing ${ctx.controlId}:`, err.message);
|
|
353
232
|
return { raw: buildNakFor(ctx, err.message) };
|
|
354
233
|
});
|
|
355
234
|
```
|
|
356
235
|
|
|
357
|
-
Without an error handler, errors are absorbed and no response is sent. The sending system will time out and retry per standard MLLP
|
|
236
|
+
Without an error handler, errors are absorbed and no response is sent. The sending system will time out and retry per standard MLLP behaviour. See the [design notes](#design-notes) below for the rationale.
|
|
358
237
|
|
|
359
|
-
|
|
238
|
+
## TLS
|
|
360
239
|
|
|
361
240
|
TLS is supported via `serve()` options:
|
|
362
241
|
|
|
363
|
-
```
|
|
242
|
+
```ts
|
|
364
243
|
import fs from "node:fs";
|
|
365
244
|
import { Mllp } from "@glion/mllp";
|
|
366
245
|
import { parseHL7v2 } from "@glion/hl7v2";
|
|
@@ -381,7 +260,7 @@ const server = serve(app, {
|
|
|
381
260
|
|
|
382
261
|
### Simple API
|
|
383
262
|
|
|
384
|
-
```
|
|
263
|
+
```ts
|
|
385
264
|
import { encode, decode, encodeMultiple } from "@glion/mllp";
|
|
386
265
|
|
|
387
266
|
const mllpFrame = encode(hl7Message);
|
|
@@ -393,7 +272,7 @@ const frames = encodeMultiple(["MSH|1", "MSH|2"]);
|
|
|
393
272
|
|
|
394
273
|
### Streaming API
|
|
395
274
|
|
|
396
|
-
```
|
|
275
|
+
```ts
|
|
397
276
|
import { createDecoderStream } from "@glion/mllp";
|
|
398
277
|
|
|
399
278
|
const decoder = createDecoderStream({
|
|
@@ -410,77 +289,35 @@ tcpSocket.readable.pipeThrough(decoder).pipeTo(
|
|
|
410
289
|
);
|
|
411
290
|
```
|
|
412
291
|
|
|
413
|
-
##
|
|
292
|
+
## Design notes
|
|
414
293
|
|
|
415
|
-
###
|
|
294
|
+
### Why no default error response?
|
|
416
295
|
|
|
417
|
-
|
|
418
|
-
| ------------------------------------ | -------------------------------- |
|
|
419
|
-
| `Mllp` | Hono-style MLLP server class |
|
|
420
|
-
| `serve()` (from `/node`) | Start a Node.js/Bun TCP server |
|
|
421
|
-
| `parsePattern(pattern)` | Parse a route pattern string |
|
|
422
|
-
| `matchPattern(pattern, type, event)` | Test a pattern against a message |
|
|
296
|
+
HL7v2 has no universal error-response format. An ACK/NAK is version-dependent, varies by message type, and requires access to the inbound MSH segment to construct correctly. Building that into the core would couple the routing engine to HL7v2 message construction — the wrong layer of abstraction.
|
|
423
297
|
|
|
424
|
-
|
|
298
|
+
Instead, the `Mllp` engine is middleware-first:
|
|
425
299
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
| `Hl7v2Processor` | Unified `Processor` type for HL7v2 (`Processor<Root, Root, Root>`) |
|
|
431
|
-
| `Middleware` | Middleware function `(ctx, next) => ...` |
|
|
432
|
-
| `Handler` | Terminal route handler `(ctx) => Response` |
|
|
433
|
-
| `ErrorHandler` | Error handler `(err, ctx) => Response` |
|
|
434
|
-
| `RouteFilter` | Filter function `(ctx) => boolean` for routing |
|
|
435
|
-
| `MiddlewareReturn` | Return type of middleware functions |
|
|
436
|
-
| `ConnectionInfo` | Connection metadata |
|
|
437
|
-
| `RoutePattern` | Parsed route pattern |
|
|
300
|
+
- **Default behaviour** — no response is sent; the sending system times out and retries, which is valid MLLP behaviour.
|
|
301
|
+
- **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.
|
|
303
|
+
- **Custom error responses** — use `app.onError()` for application-specific handling.
|
|
438
304
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
| Function | Description |
|
|
442
|
-
| ------------------------------- | --------------------------------- |
|
|
443
|
-
| `encode(message)` | Encode a message to an MLLP frame |
|
|
444
|
-
| `decode(frame)` | Decode a single MLLP frame |
|
|
445
|
-
| `encodeMultiple(messages)` | Encode multiple messages |
|
|
446
|
-
| `createDecoderStream(options?)` | Streaming decoder TransformStream |
|
|
447
|
-
|
|
448
|
-
## FAQ
|
|
449
|
-
|
|
450
|
-
### Why doesn't the server return an error response by default?
|
|
451
|
-
|
|
452
|
-
HTTP servers like [Hono](https://hono.dev) return a generic `500 Internal Server Error` when a handler throws. This works because HTTP has a universal error response format that every client understands.
|
|
453
|
-
|
|
454
|
-
HL7v2 has no such universal format. An ACK/NAK message is version-dependent, varies by message type, and requires knowledge of the original MSH segment to construct correctly. Building ACK generation into the core would couple the routing engine to HL7v2 message construction — the wrong layer of abstraction.
|
|
455
|
-
|
|
456
|
-
Instead, the `Mllp` class follows a **middleware-first** design:
|
|
457
|
-
|
|
458
|
-
- **Default behavior**: No response is sent. The sending system times out and retries, which is valid and expected in MLLP.
|
|
459
|
-
- **Logging**: Add a logger middleware to make errors observable.
|
|
460
|
-
- **ACK/NAK**: Add an acknowledgment middleware to translate errors into proper NAK responses.
|
|
461
|
-
- **Custom error handling**: Use `app.onError()` for application-specific error responses.
|
|
462
|
-
|
|
463
|
-
```typescript
|
|
305
|
+
```ts
|
|
464
306
|
const app = new Mllp().parser(parseHL7v2);
|
|
465
307
|
|
|
466
|
-
// Compose the behavior you need
|
|
467
308
|
app.use(logger()); // observability — provided by middleware
|
|
468
309
|
app.use(ackMiddleware()); // error → NAK translation — @glion/mllp-ack
|
|
469
310
|
|
|
470
311
|
app.on("ADT^A01", handler);
|
|
471
312
|
```
|
|
472
313
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
### Why is there no built-in logging?
|
|
476
|
-
|
|
477
|
-
Same philosophy as Hono — the core has zero `console.log` or `console.error` calls. Logging is an opt-in middleware concern. This gives you full control over log format, destination, and verbosity without the core making assumptions about your observability stack.
|
|
314
|
+
### Why no built-in logging?
|
|
478
315
|
|
|
479
|
-
|
|
316
|
+
The core has zero `console.log` or `console.error` calls. Logging is an opt-in middleware concern, giving you full control over format, destination, and verbosity without the core making assumptions about your observability stack.
|
|
480
317
|
|
|
481
|
-
|
|
482
|
-
- ESM only
|
|
318
|
+
## Part of Glion
|
|
483
319
|
|
|
484
|
-
|
|
320
|
+
`@glion/mllp` is part of **[Glion]**, the application framework for HL7v2. See the [Glion README] for the full package catalog and architecture.
|
|
485
321
|
|
|
486
|
-
|
|
322
|
+
[Glion]: https://github.com/rethinkhealth/glion#readme
|
|
323
|
+
[Glion README]: https://github.com/rethinkhealth/glion#readme
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glion/mllp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"description": "Transport-agnostic MLLP (Minimal Lower Layer Protocol) engine for HL7v2 messaging",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"health",
|
|
@@ -44,19 +44,21 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"unified": "11.0.5",
|
|
46
46
|
"vfile": "6.0.3",
|
|
47
|
-
"@glion/ast": "0.15.
|
|
48
|
-
"@glion/
|
|
49
|
-
"@glion/
|
|
47
|
+
"@glion/ast": "0.15.3",
|
|
48
|
+
"@glion/parser": "0.15.3",
|
|
49
|
+
"@glion/util-query": "0.15.3"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.5.0",
|
|
53
53
|
"@vitest/coverage-v8": "4.1.2",
|
|
54
|
+
"publint": "0.3.18",
|
|
54
55
|
"tsdown": "0.21.7",
|
|
55
56
|
"typescript": "^5.9.3",
|
|
56
57
|
"vitest": "4.1.2",
|
|
57
|
-
"@glion/
|
|
58
|
-
"@glion/hl7v2": "0.15.
|
|
59
|
-
"@glion/tsconfig": "0.15.
|
|
58
|
+
"@glion/check-readme": "0.15.3",
|
|
59
|
+
"@glion/hl7v2": "0.15.3",
|
|
60
|
+
"@glion/tsconfig": "0.15.3",
|
|
61
|
+
"@glion/testing": "0.15.3"
|
|
60
62
|
},
|
|
61
63
|
"engines": {
|
|
62
64
|
"node": ">=18"
|
|
@@ -65,9 +67,11 @@
|
|
|
65
67
|
"scripts": {
|
|
66
68
|
"build": "tsdown && tsc --emitDeclarationOnly",
|
|
67
69
|
"check-types": "tsc --noEmit",
|
|
70
|
+
"check:readme": "glion-check-readme",
|
|
68
71
|
"bench": "vitest bench --run",
|
|
69
72
|
"test": "vitest run",
|
|
70
73
|
"test:coverage": "vitest run --coverage",
|
|
71
|
-
"test:watch": "vitest"
|
|
74
|
+
"test:watch": "vitest",
|
|
75
|
+
"publint": "publint --strict"
|
|
72
76
|
}
|
|
73
77
|
}
|