@glion/mllp 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +486 -0
  3. package/dist/errors.d.ts +22 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +38 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/mllp.js +791 -0
  10. package/dist/mllp.js.map +1 -0
  11. package/dist/node/adapter.d.ts +75 -0
  12. package/dist/node/adapter.d.ts.map +1 -0
  13. package/dist/node/index.d.ts +15 -0
  14. package/dist/node/index.d.ts.map +1 -0
  15. package/dist/node/serve.d.ts +150 -0
  16. package/dist/node/serve.d.ts.map +1 -0
  17. package/dist/node.js +324 -0
  18. package/dist/node.js.map +1 -0
  19. package/dist/server/adapter.d.ts +64 -0
  20. package/dist/server/adapter.d.ts.map +1 -0
  21. package/dist/server/compose.d.ts +15 -0
  22. package/dist/server/compose.d.ts.map +1 -0
  23. package/dist/server/context.d.ts +33 -0
  24. package/dist/server/context.d.ts.map +1 -0
  25. package/dist/server/mllp.d.ts +102 -0
  26. package/dist/server/mllp.d.ts.map +1 -0
  27. package/dist/server/router.d.ts +49 -0
  28. package/dist/server/router.d.ts.map +1 -0
  29. package/dist/server/types.d.ts +139 -0
  30. package/dist/server/types.d.ts.map +1 -0
  31. package/dist/transport/constants.d.ts +24 -0
  32. package/dist/transport/constants.d.ts.map +1 -0
  33. package/dist/transport/decoder-stream.d.ts +49 -0
  34. package/dist/transport/decoder-stream.d.ts.map +1 -0
  35. package/dist/transport/decoder.d.ts +18 -0
  36. package/dist/transport/decoder.d.ts.map +1 -0
  37. package/dist/transport/dynamic-buffer.d.ts +28 -0
  38. package/dist/transport/dynamic-buffer.d.ts.map +1 -0
  39. package/dist/transport/encoder.d.ts +14 -0
  40. package/dist/transport/encoder.d.ts.map +1 -0
  41. package/dist/transport/errors.d.ts +13 -0
  42. package/dist/transport/errors.d.ts.map +1 -0
  43. package/dist/transport/index.d.ts +8 -0
  44. package/dist/transport/index.d.ts.map +1 -0
  45. package/dist/transport/types.d.ts +49 -0
  46. package/dist/transport/types.d.ts.map +1 -0
  47. package/package.json +68 -0
package/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
@@ -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"}
@@ -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"}