@glion/mllp 0.15.1 → 0.15.2

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 (2) hide show
  1. package/README.md +134 -297
  2. package/package.json +12 -8
package/README.md CHANGED
@@ -1,232 +1,123 @@
1
1
  # @glion/mllp
2
2
 
3
- MLLP (Minimal Lower Layer Protocol) transport for HL7v2 messaging — primitives, streaming, and a [Hono](https://hono.dev)-style middleware server.
3
+ Transport-agnostic MLLP engine and middleware-driven MLLP server for HL7v2 messaging.
4
4
 
5
- ## Overview
5
+ ## What it does
6
6
 
7
- This package provides everything needed to send and receive HL7v2 messages over MLLP/TCP:
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
- 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:
9
+ ## Install
39
10
 
40
11
  ```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
12
+ npm install @glion/mllp
49
13
  ```
50
14
 
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
15
+ ### Package exports
54
16
 
55
- For more control, create a `glion.config.ts`:
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
- ```typescript
58
- // glion.config.ts
59
- import { defineConfig } from "@glion/mllp/config";
22
+ ## Use
60
23
 
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
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
- export default new Mllp()
29
+ const app = new Mllp()
77
30
  .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
- ```
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
- ```json
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
- ### 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
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
- If no `glion.config.*` file is found, glion looks for a conventional entry file in this order:
42
+ ## API
122
43
 
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`
44
+ ### `Mllp`
129
45
 
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.
46
+ The server class. Built with a fluent chainable API:
131
47
 
132
- ### Programmatic API
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
- To embed glion in another tool, use the `runGlion` function:
56
+ ### `serve(app, options)` — from `@glion/mllp/node`
135
57
 
136
- ```typescript
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
- 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);
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
- // 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
- });
64
+ ### Primitives
192
65
 
193
- const server = serve(app, { port: 2575 });
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
- ### Routing Patterns
73
+ ### Types
197
74
 
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
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 **first-match-wins** — register specific routes before catch-alls.
100
+ Routes are matched first-match-wins — register specific routes before catch-alls.
207
101
 
208
- ### Middleware
102
+ ## Middleware
209
103
 
210
- Middleware follows the Hono/Koa onion model:
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
- ```typescript
213
- // Global middleware — runs for all messages
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 messages
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
- Middleware can short-circuit by returning a response without calling `next()`:
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
- ### Context
128
+ ## Context
238
129
 
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.
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
- #### Sync properties (always available, zero cost)
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., `"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 |
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
- #### Async methods (lazy, trigger pipeline stages on first call)
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., JSON from hl7v2Jsonify) |
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()` — Choosing the Right One
269
-
270
- **Use `ctx.ast`** when you only need the raw message structure:
159
+ ### `ctx.ast` vs `await ctx.tree()`
271
160
 
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
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
- ```typescript
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
- **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
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
- ```typescript
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
- **Use `await ctx.result()`** when you need the compiled output:
182
+ Use `await ctx.result()` when you need the compiled output:
301
183
 
302
- ```typescript
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 Middleware — Best Practices
192
+ ### Writing middleware — prefer `ctx.ast`
312
193
 
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.
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
- ```typescript
316
- // ✅ Good — sync, fast, no pipeline cost
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
- // ✅ Good — ACK middleware uses ctx.ast (pre-transform tree)
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
- // ⚠️ Only when needed — triggers transform pipeline
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
- ### Error Handling
227
+ ## Error handling
349
228
 
350
- ```typescript
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 behavior. See the [FAQ](#faq) for the rationale behind this design.
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
- ### TLS
238
+ ## TLS
360
239
 
361
240
  TLS is supported via `serve()` options:
362
241
 
363
- ```typescript
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
- ```typescript
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
- ```typescript
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
- ## API Reference
292
+ ## Design notes
414
293
 
415
- ### Server
294
+ ### Why no default error response?
416
295
 
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 |
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
- ### Types
298
+ Instead, the `Mllp` engine is middleware-first:
425
299
 
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 |
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
- ### 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
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
- 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.
314
+ ### Why no built-in logging?
478
315
 
479
- ## Requirements
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
- - Node.js 18+ or Bun
482
- - ESM only
318
+ ## Part of Glion
483
319
 
484
- ## License
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
- MIT
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.1",
3
+ "version": "0.15.2",
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.1",
48
- "@glion/util-query": "0.15.1",
49
- "@glion/parser": "0.15.1"
47
+ "@glion/parser": "0.15.2",
48
+ "@glion/ast": "0.15.2",
49
+ "@glion/util-query": "0.15.2"
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/testing": "0.15.1",
58
- "@glion/hl7v2": "0.15.1",
59
- "@glion/tsconfig": "0.15.1"
58
+ "@glion/check-readme": "0.15.2",
59
+ "@glion/hl7v2": "0.15.2",
60
+ "@glion/testing": "0.15.2",
61
+ "@glion/tsconfig": "0.15.2"
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
  }