@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.
- package/LICENSE +21 -0
- package/README.md +486 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/mllp.js +791 -0
- package/dist/mllp.js.map +1 -0
- package/dist/node/adapter.d.ts +75 -0
- package/dist/node/adapter.d.ts.map +1 -0
- package/dist/node/index.d.ts +15 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/serve.d.ts +150 -0
- package/dist/node/serve.d.ts.map +1 -0
- package/dist/node.js +324 -0
- package/dist/node.js.map +1 -0
- package/dist/server/adapter.d.ts +64 -0
- package/dist/server/adapter.d.ts.map +1 -0
- package/dist/server/compose.d.ts +15 -0
- package/dist/server/compose.d.ts.map +1 -0
- package/dist/server/context.d.ts +33 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/mllp.d.ts +102 -0
- package/dist/server/mllp.d.ts.map +1 -0
- package/dist/server/router.d.ts +49 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/types.d.ts +139 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/transport/constants.d.ts +24 -0
- package/dist/transport/constants.d.ts.map +1 -0
- package/dist/transport/decoder-stream.d.ts +49 -0
- package/dist/transport/decoder-stream.d.ts.map +1 -0
- package/dist/transport/decoder.d.ts +18 -0
- package/dist/transport/decoder.d.ts.map +1 -0
- package/dist/transport/dynamic-buffer.d.ts +28 -0
- package/dist/transport/dynamic-buffer.d.ts.map +1 -0
- package/dist/transport/encoder.d.ts +14 -0
- package/dist/transport/encoder.d.ts.map +1 -0
- package/dist/transport/errors.d.ts +13 -0
- package/dist/transport/errors.d.ts.map +1 -0
- package/dist/transport/index.d.ts +8 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/types.d.ts +49 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Rethink Health
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# @glion/mllp
|
|
2
|
+
|
|
3
|
+
MLLP (Minimal Lower Layer Protocol) transport for HL7v2 messaging — primitives, streaming, and a [Hono](https://hono.dev)-style middleware server.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides everything needed to send and receive HL7v2 messages over MLLP/TCP:
|
|
8
|
+
|
|
9
|
+
1. **Primitives** — Frame encoding/decoding, streaming TransformStreams
|
|
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:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cat > glion.app.ts <<'EOF'
|
|
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
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The server starts on port `2575` listening on all interfaces. The TUI shows a `zero-config` badge to indicate no config file was loaded.
|
|
52
|
+
|
|
53
|
+
### With an Explicit Config File
|
|
54
|
+
|
|
55
|
+
For more control, create a `glion.config.ts`:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// glion.config.ts
|
|
59
|
+
import { defineConfig } from "@glion/mllp/config";
|
|
60
|
+
|
|
61
|
+
export default defineConfig({
|
|
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
|
|
73
|
+
import { Mllp } from "@glion/mllp";
|
|
74
|
+
import { parseHL7v2 } from "@glion/hl7v2";
|
|
75
|
+
|
|
76
|
+
export default new Mllp()
|
|
77
|
+
.parser(parseHL7v2)
|
|
78
|
+
.on("ADT^A01", async (ctx) => ({ raw: buildAck(ctx) }))
|
|
79
|
+
.on("ORU^R01", async (ctx) => ({ raw: buildAck(ctx) }))
|
|
80
|
+
.on("*", async (ctx) => ({ raw: buildNak(ctx, "Unsupported") }));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
// package.json
|
|
85
|
+
{
|
|
86
|
+
"scripts": {
|
|
87
|
+
"dev": "glion dev",
|
|
88
|
+
"start": "glion start"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Two Verbs
|
|
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
|
|
120
|
+
|
|
121
|
+
If no `glion.config.*` file is found, glion looks for a conventional entry file in this order:
|
|
122
|
+
|
|
123
|
+
1. `glion.app.ts`
|
|
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`
|
|
129
|
+
|
|
130
|
+
When found, it starts with sensible defaults: port `2575`, hostname `0.0.0.0`, no TLS, and watches `dirname(entry)`. The TUI header shows a `zero-config` badge to make this discoverable.
|
|
131
|
+
|
|
132
|
+
### Programmatic API
|
|
133
|
+
|
|
134
|
+
To embed glion in another tool, use the `runGlion` function:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { runGlion } from "@glion/mllp/cli";
|
|
138
|
+
import { defineConfig, type GlionConfig } from "@glion/mllp/config";
|
|
139
|
+
|
|
140
|
+
const exitCode = await runGlion({
|
|
141
|
+
argv: ["dev"],
|
|
142
|
+
cwd: process.cwd(),
|
|
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);
|
|
156
|
+
|
|
157
|
+
// Route by message type
|
|
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
|
+
});
|
|
192
|
+
|
|
193
|
+
const server = serve(app, { port: 2575 });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Routing Patterns
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
app.on("ADT^A01", handler); // Exact match
|
|
200
|
+
app.on("ADT^*", handler); // Any ADT trigger event
|
|
201
|
+
app.on("*^A01", handler); // Any message type with A01
|
|
202
|
+
app.on("ADT", handler); // Any ADT (same as ADT^*)
|
|
203
|
+
app.on("*", handler); // Catch-all
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Routes are matched **first-match-wins** — register specific routes before catch-alls.
|
|
207
|
+
|
|
208
|
+
### Middleware
|
|
209
|
+
|
|
210
|
+
Middleware follows the Hono/Koa onion model:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// Global middleware — runs for all messages
|
|
214
|
+
app.use(async (ctx, next) => {
|
|
215
|
+
const start = Date.now();
|
|
216
|
+
await next();
|
|
217
|
+
console.log(`Processed in ${Date.now() - start}ms`);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Scoped middleware — only for matching messages
|
|
221
|
+
app.use("ADT^*", async (ctx, next) => {
|
|
222
|
+
ctx.set("isAdmission", true);
|
|
223
|
+
await next();
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Middleware can short-circuit by returning a response without calling `next()`:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
app.use(async (ctx) => {
|
|
231
|
+
if (!isAuthorized(ctx.connection.remoteAddress)) {
|
|
232
|
+
return { raw: buildNakFor(ctx, "Unauthorized") };
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Context
|
|
238
|
+
|
|
239
|
+
The `Context` object is available in all middleware and handlers. The pipeline is **lazy** — only the parse step runs eagerly. Transforms and compilation are deferred until accessed.
|
|
240
|
+
|
|
241
|
+
#### Sync properties (always available, zero cost)
|
|
242
|
+
|
|
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., `"ADT"`) |
|
|
249
|
+
| `ctx.triggerEvent` | MSH-9.2 (e.g., `"A01"`) |
|
|
250
|
+
| `ctx.messageStructure` | MSH-9.3 (e.g., `"ADT_A01"`) |
|
|
251
|
+
| `ctx.version` | MSH-12 (e.g., `"2.5.1"`) |
|
|
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 |
|
|
258
|
+
|
|
259
|
+
#### Async methods (lazy, trigger pipeline stages on first call)
|
|
260
|
+
|
|
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., JSON from hl7v2Jsonify) |
|
|
265
|
+
|
|
266
|
+
Both are cached — subsequent calls return the same value instantly.
|
|
267
|
+
|
|
268
|
+
### `ctx.ast` vs `await ctx.tree()` — Choosing the Right One
|
|
269
|
+
|
|
270
|
+
**Use `ctx.ast`** when you only need the raw message structure:
|
|
271
|
+
|
|
272
|
+
- Reading MSH fields (message type, version, control ID)
|
|
273
|
+
- Building ACK/NAK responses
|
|
274
|
+
- Route filter functions
|
|
275
|
+
- Middleware that doesn't need escape-decoded values
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// Fast — no pipeline cost
|
|
279
|
+
app.use((ctx, next) => {
|
|
280
|
+
console.log(`Received ${ctx.messageType}^${ctx.triggerEvent}`);
|
|
281
|
+
return next();
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Use `await ctx.tree()`** when you need the fully processed tree:
|
|
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
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// Triggers transform pipeline on first call
|
|
293
|
+
app.on("ADT^A01", async (ctx) => {
|
|
294
|
+
const tree = await ctx.tree();
|
|
295
|
+
// tree has escape sequences decoded, message structure resolved, etc.
|
|
296
|
+
return { raw: "..." };
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Use `await ctx.result()`** when you need the compiled output:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
app.on("ORU^R01", async (ctx) => {
|
|
304
|
+
const json = await ctx.result(); // triggers transform + compile
|
|
305
|
+
// json is the Hl7v2JsonResult from hl7v2Jsonify
|
|
306
|
+
await saveToDatabase(json);
|
|
307
|
+
return { raw: "..." };
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Writing Middleware — Best Practices
|
|
312
|
+
|
|
313
|
+
**Prefer `ctx.ast` over `await ctx.tree()` in middleware.** Most middleware only needs routing fields or raw MSH data — both available synchronously from `ctx.ast`. This keeps the middleware fast and avoids triggering the transform pipeline unnecessarily.
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// ✅ Good — sync, fast, no pipeline cost
|
|
317
|
+
function authMiddleware(): Middleware {
|
|
318
|
+
return (ctx, next) => {
|
|
319
|
+
if (!isAuthorized(ctx.connection.remoteAddress)) {
|
|
320
|
+
return { raw: buildNak(ctx.ast, "Unauthorized") };
|
|
321
|
+
}
|
|
322
|
+
return next();
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ✅ Good — ACK middleware uses ctx.ast (pre-transform tree)
|
|
327
|
+
// The acknowledge() function only reads MSH fields
|
|
328
|
+
function ackMiddleware(): Middleware {
|
|
329
|
+
return async (ctx, next) => {
|
|
330
|
+
await next();
|
|
331
|
+
ctx.res = { raw: toHl7v2(acknowledge(ctx.ast)) };
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ⚠️ Only when needed — triggers transform pipeline
|
|
336
|
+
function validationMiddleware(): Middleware {
|
|
337
|
+
return async (ctx, next) => {
|
|
338
|
+
const tree = await ctx.tree();
|
|
339
|
+
// tree has escape sequences decoded — needed for value validation
|
|
340
|
+
if (!isValid(tree)) {
|
|
341
|
+
return { raw: buildNak(ctx.ast, "Invalid") };
|
|
342
|
+
}
|
|
343
|
+
return next();
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Error Handling
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
app.onError(async (err, ctx) => {
|
|
352
|
+
console.error(`Error processing ${ctx.controlId}:`, err.message);
|
|
353
|
+
return { raw: buildNakFor(ctx, err.message) };
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Without an error handler, errors are absorbed and no response is sent. The sending system will time out and retry per standard MLLP behavior. See the [FAQ](#faq) for the rationale behind this design.
|
|
358
|
+
|
|
359
|
+
### TLS
|
|
360
|
+
|
|
361
|
+
TLS is supported via `serve()` options:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import fs from "node:fs";
|
|
365
|
+
import { Mllp } from "@glion/mllp";
|
|
366
|
+
import { parseHL7v2 } from "@glion/hl7v2";
|
|
367
|
+
import { serve } from "@glion/mllp/node";
|
|
368
|
+
|
|
369
|
+
const app = new Mllp().parser(parseHL7v2);
|
|
370
|
+
|
|
371
|
+
const server = serve(app, {
|
|
372
|
+
port: 2575,
|
|
373
|
+
tls: {
|
|
374
|
+
cert: fs.readFileSync("cert.pem"),
|
|
375
|
+
key: fs.readFileSync("key.pem"),
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Primitives
|
|
381
|
+
|
|
382
|
+
### Simple API
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { encode, decode, encodeMultiple } from "@glion/mllp";
|
|
386
|
+
|
|
387
|
+
const mllpFrame = encode(hl7Message);
|
|
388
|
+
const decoded = decode(mllpFrame);
|
|
389
|
+
console.log(decoded.text);
|
|
390
|
+
|
|
391
|
+
const frames = encodeMultiple(["MSH|1", "MSH|2"]);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Streaming API
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { createDecoderStream } from "@glion/mllp";
|
|
398
|
+
|
|
399
|
+
const decoder = createDecoderStream({
|
|
400
|
+
maxMessageSize: 1024 * 1024,
|
|
401
|
+
onError: (error) => console.warn(`[${error.code}] ${error.message}`),
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
tcpSocket.readable.pipeThrough(decoder).pipeTo(
|
|
405
|
+
new WritableStream({
|
|
406
|
+
write(message) {
|
|
407
|
+
console.log("Received:", message.text);
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
);
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## API Reference
|
|
414
|
+
|
|
415
|
+
### Server
|
|
416
|
+
|
|
417
|
+
| Export | Description |
|
|
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 |
|
|
423
|
+
|
|
424
|
+
### Types
|
|
425
|
+
|
|
426
|
+
| Type | Description |
|
|
427
|
+
| ------------------ | ------------------------------------------------------------------ |
|
|
428
|
+
| `Context` | Request context with message data and routing fields |
|
|
429
|
+
| `Response` | Response object `{ raw: string }` |
|
|
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 |
|
|
438
|
+
|
|
439
|
+
### Primitives
|
|
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
|
|
464
|
+
const app = new Mllp().parser(parseHL7v2);
|
|
465
|
+
|
|
466
|
+
// Compose the behavior you need
|
|
467
|
+
app.use(logger()); // observability — provided by middleware
|
|
468
|
+
app.use(ackMiddleware()); // error → NAK translation — @glion/mllp-ack
|
|
469
|
+
|
|
470
|
+
app.on("ADT^A01", handler);
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
This keeps the core engine simple and protocol-agnostic, while middleware handles the HL7v2-specific concerns.
|
|
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.
|
|
478
|
+
|
|
479
|
+
## Requirements
|
|
480
|
+
|
|
481
|
+
- Node.js 18+ or Bun
|
|
482
|
+
- ESM only
|
|
483
|
+
|
|
484
|
+
## License
|
|
485
|
+
|
|
486
|
+
MIT
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for constructing an MllpError.
|
|
3
|
+
*/
|
|
4
|
+
export interface MllpErrorOptions extends ErrorOptions {
|
|
5
|
+
/** Error condition string (e.g., for ERR segment in ACK) */
|
|
6
|
+
errorCondition?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Base error class for all MLLP errors.
|
|
10
|
+
*
|
|
11
|
+
* All error types in the MLLP package extend this class.
|
|
12
|
+
* Provides a common `code` field so that any MLLP error can be
|
|
13
|
+
* programmatically identified and translated (e.g., into ACK/NAK responses).
|
|
14
|
+
*/
|
|
15
|
+
export declare class MllpError extends Error {
|
|
16
|
+
/** Machine-readable error code identifying the error type */
|
|
17
|
+
readonly code: string;
|
|
18
|
+
/** Error condition string (e.g., for ERR segment in ACK) */
|
|
19
|
+
readonly errorCondition?: string;
|
|
20
|
+
constructor(code: string, message: string, options?: MllpErrorOptions);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
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";
|
|
5
|
+
export { getMessageInfo, Mllp } from "./server/mllp";
|
|
6
|
+
export type { MessageInfo } from "./server/mllp";
|
|
7
|
+
export type { ConnectionInfo, Context, ErrorHandler, Handler, Hl7v2Processor, Middleware, MiddlewareReturn, Response, RouteFilter, RoutePattern, } from "./server/types";
|
|
8
|
+
export { matchPattern, parsePattern } from "./server/types";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/transport/decoder.ts"],"sourcesContent":["import { MLLP_END_BYTE_1, MLLP_END_BYTE_2, MLLP_START_BYTE } from \"./constants\";\nimport { TransportError } from \"./errors\";\nimport type { DecodedMessage, DecoderOptions } from \"./types\";\nimport { TransportErrorCode } from \"./types\";\n\n/**\n * Decode a single MLLP frame (VT ... FS CR) into a message.\n *\n * Validates framing, strips the envelope, and returns the payload as\n * both raw bytes and decoded text. Throws {@link TransportError} on\n * any structural issue.\n *\n * For streaming / chunked TCP data, use {@link createDecoderStream} instead.\n *\n * @throws TransportError with `INVALID_START_BYTE` if byte 0 is not 0x0B\n * @throws TransportError with `INVALID_END_SEQUENCE` if frame doesn't end with\n * 0x1C 0x0D\n * @throws TransportError with `MESSAGE_TOO_LARGE` if payload exceeds\n * `maxMessageSize`\n */\nexport function decode(\n frame: Uint8Array,\n options?: DecoderOptions\n): DecodedMessage {\n if (frame.length < 3) {\n throw new TransportError(\n TransportErrorCode.INVALID_END_SEQUENCE,\n \"Frame too short to contain valid MLLP structure\",\n 0\n );\n }\n\n const startByte = frame[0];\n if (startByte !== MLLP_START_BYTE) {\n throw new TransportError(\n TransportErrorCode.INVALID_START_BYTE,\n `Invalid start byte: expected 0x0B, got 0x${(startByte ?? 0).toString(16).padStart(2, \"0\")}`,\n 0\n );\n }\n\n const len = frame.length;\n const endByte1 = frame[len - 2];\n const endByte2 = frame[len - 1];\n if (endByte1 !== MLLP_END_BYTE_1 || endByte2 !== MLLP_END_BYTE_2) {\n throw new TransportError(\n TransportErrorCode.INVALID_END_SEQUENCE,\n `Invalid end sequence: expected 0x1C 0x0D, got 0x${(endByte1 ?? 0).toString(16).padStart(2, \"0\")} 0x${(endByte2 ?? 0).toString(16).padStart(2, \"0\")}`,\n len - 2\n );\n }\n\n const messageLength = len - 3;\n if (\n options?.maxMessageSize !== undefined &&\n messageLength > options.maxMessageSize\n ) {\n throw new TransportError(\n TransportErrorCode.MESSAGE_TOO_LARGE,\n `Message size ${messageLength} exceeds maximum ${options.maxMessageSize}`,\n 0\n );\n }\n\n const data = frame.subarray(1, len - 2);\n const text = new TextDecoder(options?.encoding ?? \"utf8\").decode(data);\n\n return {\n byteLength: messageLength,\n data,\n text,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,SAAgB,OACd,OACA,SACgB;AAChB,KAAI,MAAM,SAAS,EACjB,OAAM,IAAI,eACR,mBAAmB,sBACnB,mDACA,EACD;CAGH,MAAM,YAAY,MAAM;AACxB,KAAI,cAAA,GACF,OAAM,IAAI,eACR,mBAAmB,oBACnB,6CAA6C,aAAa,GAAG,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,IAC1F,EACD;CAGH,MAAM,MAAM,MAAM;CAClB,MAAM,WAAW,MAAM,MAAM;CAC7B,MAAM,WAAW,MAAM,MAAM;AAC7B,KAAI,aAAA,MAAgC,aAAA,GAClC,OAAM,IAAI,eACR,mBAAmB,sBACnB,oDAAoD,YAAY,GAAG,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,YAAY,GAAG,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,IACnJ,MAAM,EACP;CAGH,MAAM,gBAAgB,MAAM;AAC5B,KACE,SAAS,mBAAmB,KAAA,KAC5B,gBAAgB,QAAQ,eAExB,OAAM,IAAI,eACR,mBAAmB,mBACnB,gBAAgB,cAAc,mBAAmB,QAAQ,kBACzD,EACD;CAGH,MAAM,OAAO,MAAM,SAAS,GAAG,MAAM,EAAE;AAGvC,QAAO;EACL,YAAY;EACZ;EACA,MALW,IAAI,YAAY,SAAS,YAAY,OAAO,CAAC,OAAO,KAAK;EAMrE"}
|