@glion/mllp 0.16.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -59
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/mllp.js +49 -4
- package/dist/mllp.js.map +1 -1
- package/dist/node/adapter.d.ts.map +1 -1
- package/dist/node/serve.d.ts +6 -4
- package/dist/node/serve.d.ts.map +1 -1
- package/dist/node.js +25 -12
- package/dist/node.js.map +1 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/mllp.d.ts +4 -2
- package/dist/server/mllp.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -9
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -57,33 +57,46 @@ The server class. Built with a fluent chainable API:
|
|
|
57
57
|
|
|
58
58
|
Start a Node.js or Bun TCP server that dispatches incoming MLLP frames through the `Mllp` instance.
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
|
|
60
|
+
| Option | Type | Description |
|
|
61
|
+
| ----------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `port` | `number` | TCP port to listen on. |
|
|
63
|
+
| `hostname` | `string`, optional | Interface to bind. Defaults to all interfaces. |
|
|
64
|
+
| `tls` | `{ cert, key, ca?, passphrase? }`, optional | Enable MLLP over TLS. |
|
|
65
|
+
| `keepAlive` | `boolean`, optional | Enable TCP keep-alive on accepted sockets. Defaults to `true`. |
|
|
66
|
+
| `keepAliveInitialDelay` | `number`, optional | Delay in ms before the first keep-alive probe on an idle socket. Defaults to `60000`. |
|
|
67
|
+
| `socketTimeout` | `number`, optional | Socket inactivity timeout in ms — an idle socket past it is destroyed. `0` (default) disables. |
|
|
68
|
+
| `onConnect` | `ConnectionCallback`, optional | Called when a new TCP connection is accepted. |
|
|
69
|
+
| `onDisconnect` | `ConnectionCallback`, optional | Called when a connection closes — always fires, even if `onConnect` threw. |
|
|
70
|
+
| `onError` | `ErrorCallback`, optional | Called when a handler error goes unhandled or a lifecycle callback throws. |
|
|
71
|
+
|
|
72
|
+
### Wire framing
|
|
73
|
+
|
|
74
|
+
MLLP framing lives in [`@glion/mllp-codec`](https://github.com/rethinkhealth/glion/tree/main/packages/mllp-codec) — `frame()` wraps outbound messages, `unframe()` streams inbound bytes back into them. `@glion/mllp` consumes it internally and re-exports nothing from it; import it directly when you need the codec.
|
|
72
75
|
|
|
73
76
|
### Types
|
|
74
77
|
|
|
75
|
-
| Type | Description
|
|
76
|
-
| ------------------ |
|
|
77
|
-
| `Context` | Request context with message data and routing fields.
|
|
78
|
-
| `Response` | Response object `{ raw: string }`.
|
|
79
|
-
| `Hl7v2Processor` | Unified `Processor` type for HL7v2 (`Processor<Root, Root, Root>`). |
|
|
80
|
-
| `Middleware` | Middleware function `(ctx, next) => ...`.
|
|
81
|
-
| `Handler` | Terminal route handler `(ctx) => Response`.
|
|
82
|
-
| `ErrorHandler` | Error handler `(err, ctx) => Response`.
|
|
83
|
-
| `RouteFilter` | Filter function `(ctx) => boolean` used for routing.
|
|
84
|
-
| `MiddlewareReturn` | Return type of middleware functions.
|
|
85
|
-
| `ConnectionInfo` | Connection metadata (remote/local address, TLS flag).
|
|
86
|
-
| `RoutePattern` | Parsed route pattern.
|
|
78
|
+
| Type | Description |
|
|
79
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `Context` | Request context with message data and routing fields. |
|
|
81
|
+
| `Response` | Response object `{ raw: string }`. |
|
|
82
|
+
| `Hl7v2Processor` | Unified `Processor` type for HL7v2 (`Processor<Root, Root \| undefined, Root \| undefined>` — head/tail admit `undefined` so the bare `unified().use(hl7v2Parser).freeze()` is assignable). |
|
|
83
|
+
| `Middleware` | Middleware function `(ctx, next) => ...`. |
|
|
84
|
+
| `Handler` | Terminal route handler `(ctx) => Response`. |
|
|
85
|
+
| `ErrorHandler` | Error handler `(err, ctx) => Response`. |
|
|
86
|
+
| `RouteFilter` | Filter function `(ctx) => boolean` used for routing. |
|
|
87
|
+
| `MiddlewareReturn` | Return type of middleware functions. |
|
|
88
|
+
| `ConnectionInfo` | Connection metadata (remote/local address, TLS flag). |
|
|
89
|
+
| `RoutePattern` | Parsed route pattern. |
|
|
90
|
+
|
|
91
|
+
### Errors
|
|
92
|
+
|
|
93
|
+
Server-side failures are raised (or passed to `onError`) as `MllpServerError` — branch on its stable `code`; the underlying codec or charset error rides on `cause`:
|
|
94
|
+
|
|
95
|
+
| Code | Meaning |
|
|
96
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `NO_PARSER` | `app.handle()` ran before a parser was registered via `app.parser()`. |
|
|
98
|
+
| `INCOMPATIBLE_CHARSET` | An inbound message could not be decoded as UTF-8 (the `CharsetError` is on `cause`). |
|
|
99
|
+
| `PROTOCOL_VIOLATION` | The MLLP byte stream was violated and the connection closed (the `MllpCodecError` is on `cause`). |
|
|
87
100
|
|
|
88
101
|
## Routing
|
|
89
102
|
|
|
@@ -256,39 +269,6 @@ const server = serve(app, {
|
|
|
256
269
|
});
|
|
257
270
|
```
|
|
258
271
|
|
|
259
|
-
## Primitives
|
|
260
|
-
|
|
261
|
-
### Simple API
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
import { encode, decode, encodeMultiple } from "@glion/mllp";
|
|
265
|
-
|
|
266
|
-
const mllpFrame = encode(hl7Message);
|
|
267
|
-
const decoded = decode(mllpFrame);
|
|
268
|
-
console.log(decoded.text);
|
|
269
|
-
|
|
270
|
-
const frames = encodeMultiple(["MSH|1", "MSH|2"]);
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### Streaming API
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
import { createDecoderStream } from "@glion/mllp";
|
|
277
|
-
|
|
278
|
-
const decoder = createDecoderStream({
|
|
279
|
-
maxMessageSize: 1024 * 1024,
|
|
280
|
-
onError: (error) => console.warn(`[${error.code}] ${error.message}`),
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
tcpSocket.readable.pipeThrough(decoder).pipeTo(
|
|
284
|
-
new WritableStream({
|
|
285
|
-
write(message) {
|
|
286
|
-
console.log("Received:", message.text);
|
|
287
|
-
},
|
|
288
|
-
})
|
|
289
|
-
);
|
|
290
|
-
```
|
|
291
|
-
|
|
292
272
|
## Design notes
|
|
293
273
|
|
|
294
274
|
### Why no default error response?
|
|
@@ -299,14 +279,13 @@ Instead, the `Mllp` engine is middleware-first:
|
|
|
299
279
|
|
|
300
280
|
- **Default behaviour** — no response is sent; the sending system times out and retries, which is valid MLLP behaviour.
|
|
301
281
|
- **Logging** — add a logger middleware to make errors observable.
|
|
302
|
-
- **ACK/NAK** —
|
|
282
|
+
- **ACK/NAK** — register your own acknowledgment middleware to translate outcomes into ACK/NAK responses. Built-in acknowledgment translation at the framework's error boundary is landing next (see ADR-0019).
|
|
303
283
|
- **Custom error responses** — use `app.onError()` for application-specific handling.
|
|
304
284
|
|
|
305
285
|
```ts
|
|
306
286
|
const app = new Mllp().parser(parseHL7v2);
|
|
307
287
|
|
|
308
288
|
app.use(logger()); // observability — provided by middleware
|
|
309
|
-
app.use(ackMiddleware()); // error → NAK translation — @glion/mllp-ack
|
|
310
289
|
|
|
311
290
|
app.on("ADT^A01", handler);
|
|
312
291
|
```
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors raised by the `@glion/mllp` server itself.
|
|
3
|
+
*
|
|
4
|
+
* Per ADR 0018: one base error class discriminated by a stable `code` string.
|
|
5
|
+
* A NAK the server *builds* in response to a message is an `@glion/ack`
|
|
6
|
+
* `AckException` — a different concern from a server-lifecycle/config failure,
|
|
7
|
+
* which is what `MllpServerError` represents.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
export declare const MllpServerErrorCode: {
|
|
12
|
+
/**
|
|
13
|
+
* An inbound message could not be decoded as UTF-8 — the only character set
|
|
14
|
+
* supported for now. The underlying `@glion/util-charset` `CharsetError` is
|
|
15
|
+
* kept on `cause`; callers branch on this code without importing that
|
|
16
|
+
* package.
|
|
17
|
+
*/
|
|
18
|
+
readonly INCOMPATIBLE_CHARSET: "INCOMPATIBLE_CHARSET";
|
|
19
|
+
/**
|
|
20
|
+
* `app.handle()` was called before a parser was registered via
|
|
21
|
+
* `app.parser()`.
|
|
22
|
+
*/
|
|
23
|
+
readonly NO_PARSER: "NO_PARSER";
|
|
24
|
+
/**
|
|
25
|
+
* The MLLP byte stream itself was violated and the connection closed:
|
|
26
|
+
* the remote system sent bytes outside a message, glued or unterminated
|
|
27
|
+
* messages, an over-cap message — or a handler's response contained a
|
|
28
|
+
* reserved VT/FS byte and could not be framed. The underlying
|
|
29
|
+
* `@glion/mllp-codec` `MllpCodecError` is kept on `cause`; branch on this
|
|
30
|
+
* code without importing that package.
|
|
31
|
+
*/
|
|
32
|
+
readonly PROTOCOL_VIOLATION: "PROTOCOL_VIOLATION";
|
|
33
|
+
};
|
|
34
|
+
export type MllpServerErrorCode = (typeof MllpServerErrorCode)[keyof typeof MllpServerErrorCode];
|
|
35
|
+
/** The one error class the `@glion/mllp` server raises. Branch on `code`. */
|
|
36
|
+
export declare class MllpServerError extends Error {
|
|
37
|
+
readonly code: MllpServerErrorCode;
|
|
38
|
+
constructor(code: MllpServerErrorCode, message: string, opts?: {
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,eAAO,MAAM,mBAAmB;IAC9B;;;;;OAKG;;IAEH;;;OAGG;;IAEH;;;;;;;OAOG;;CAEK,CAAC;AACX,MAAM,MAAM,mBAAmB,GAC7B,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,OAAO,mBAAmB,CAAC,CAAC;AAEjE,6EAA6E;AAC7E,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;gBAEjC,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAM7B"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type { DecodedMessage, DecoderOptions, EncoderInput, EncoderOptions, MllpErrorOptions, } from "@glion/mllp-transport";
|
|
1
|
+
export { MllpServerError, MllpServerErrorCode } from "./errors";
|
|
3
2
|
export { getMessageInfo, Mllp } from "./server/mllp";
|
|
4
3
|
export type { MessageInfo } from "./server/mllp";
|
|
5
4
|
export type { ConnectionInfo, Context, ErrorHandler, Handler, Hl7v2Processor, Middleware, MiddlewareReturn, Response, RouteFilter, RoutePattern, } from "./server/types";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrD,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,YAAY,EACV,cAAc,EACd,OAAO,EACP,YAAY,EACZ,OAAO,EACP,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { i as parsePattern, n as getMessageInfo, r as matchPattern, t as Mllp } from "./mllp.js";
|
|
2
|
-
|
|
3
|
-
export { 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 };
|
|
1
|
+
import { a as MllpServerError, i as parsePattern, n as getMessageInfo, o as MllpServerErrorCode, r as matchPattern, t as Mllp } from "./mllp.js";
|
|
2
|
+
export { Mllp, MllpServerError, MllpServerErrorCode, getMessageInfo, matchPattern, parsePattern };
|
package/dist/mllp.js
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
|
-
import { MllpError } from "@glion/mllp-transport";
|
|
2
1
|
import { value } from "@glion/util-query";
|
|
3
2
|
import { VFile } from "vfile";
|
|
3
|
+
//#region src/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* Errors raised by the `@glion/mllp` server itself.
|
|
6
|
+
*
|
|
7
|
+
* Per ADR 0018: one base error class discriminated by a stable `code` string.
|
|
8
|
+
* A NAK the server *builds* in response to a message is an `@glion/ack`
|
|
9
|
+
* `AckException` — a different concern from a server-lifecycle/config failure,
|
|
10
|
+
* which is what `MllpServerError` represents.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
const MllpServerErrorCode = {
|
|
15
|
+
/**
|
|
16
|
+
* An inbound message could not be decoded as UTF-8 — the only character set
|
|
17
|
+
* supported for now. The underlying `@glion/util-charset` `CharsetError` is
|
|
18
|
+
* kept on `cause`; callers branch on this code without importing that
|
|
19
|
+
* package.
|
|
20
|
+
*/
|
|
21
|
+
INCOMPATIBLE_CHARSET: "INCOMPATIBLE_CHARSET",
|
|
22
|
+
/**
|
|
23
|
+
* `app.handle()` was called before a parser was registered via
|
|
24
|
+
* `app.parser()`.
|
|
25
|
+
*/
|
|
26
|
+
NO_PARSER: "NO_PARSER",
|
|
27
|
+
/**
|
|
28
|
+
* The MLLP byte stream itself was violated and the connection closed:
|
|
29
|
+
* the remote system sent bytes outside a message, glued or unterminated
|
|
30
|
+
* messages, an over-cap message — or a handler's response contained a
|
|
31
|
+
* reserved VT/FS byte and could not be framed. The underlying
|
|
32
|
+
* `@glion/mllp-codec` `MllpCodecError` is kept on `cause`; branch on this
|
|
33
|
+
* code without importing that package.
|
|
34
|
+
*/
|
|
35
|
+
PROTOCOL_VIOLATION: "PROTOCOL_VIOLATION"
|
|
36
|
+
};
|
|
37
|
+
/** The one error class the `@glion/mllp` server raises. Branch on `code`. */
|
|
38
|
+
var MllpServerError = class extends Error {
|
|
39
|
+
code;
|
|
40
|
+
constructor(code, message, opts) {
|
|
41
|
+
super(message, opts);
|
|
42
|
+
this.name = "MllpServerError";
|
|
43
|
+
this.code = code;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
4
47
|
//#region src/server/compose.ts
|
|
5
48
|
/**
|
|
6
49
|
* Compose an array of middleware into a single function that executes
|
|
@@ -308,15 +351,17 @@ var Mllp = class {
|
|
|
308
351
|
* This is the integration point — analogous to Hono's `fetch()`.
|
|
309
352
|
*
|
|
310
353
|
* The pipeline is lazy (see ADR-0013):
|
|
354
|
+
*
|
|
311
355
|
* 1. Parse (sync, fast) — always runs, extracts routing fields
|
|
312
356
|
* 2. Route match — uses pre-transform routing fields
|
|
313
357
|
* 3. If no match → return undefined (no transform/compile cost)
|
|
314
358
|
* 4. Transform/compile — only when handlers access ctx.tree()/ctx.result()
|
|
315
359
|
*
|
|
316
|
-
* Throws `
|
|
360
|
+
* Throws `MllpServerError` (`NO_PARSER`) if no processor has been registered
|
|
361
|
+
* via `app.parser()`.
|
|
317
362
|
*/
|
|
318
363
|
async handle(raw, bytes, connection) {
|
|
319
|
-
if (!this.#processor) throw new
|
|
364
|
+
if (!this.#processor) throw new MllpServerError(MllpServerErrorCode.NO_PARSER, "No parser registered. Call app.parser() before handling messages.");
|
|
320
365
|
const ctx = createContext({
|
|
321
366
|
bytes,
|
|
322
367
|
connection,
|
|
@@ -370,6 +415,6 @@ var Mllp = class {
|
|
|
370
415
|
}
|
|
371
416
|
};
|
|
372
417
|
//#endregion
|
|
373
|
-
export { parsePattern as i, getMessageInfo as n, matchPattern as r, Mllp as t };
|
|
418
|
+
export { MllpServerError as a, parsePattern as i, getMessageInfo as n, MllpServerErrorCode as o, matchPattern as r, Mllp as t };
|
|
374
419
|
|
|
375
420
|
//# sourceMappingURL=mllp.js.map
|
package/dist/mllp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mllp.js","names":["queryValue","#routes","#globalMiddleware","#scopedMiddleware","#router","#processor","#errorHandler","#handleError"],"sources":["../src/server/compose.ts","../src/server/context.ts","../src/server/types.ts","../src/server/router.ts","../src/server/mllp.ts"],"sourcesContent":["import type { Context, Middleware } from \"./types\";\n\n/**\n * Compose an array of middleware into a single function that executes\n * them in onion-model order (like Hono/Koa).\n *\n * Each middleware receives (ctx, next). Calling next() proceeds to the\n * next middleware. If a middleware returns a Response without\n * calling next(), it short-circuits the chain.\n *\n * Response handling follows Hono's pattern: return values are written\n * to ctx.res, and middleware can also set ctx.res directly.\n * Last write wins.\n */\nexport function compose(\n middlewares: Middleware[]\n): (ctx: Context) => Promise<void> {\n return async (ctx: Context): Promise<void> => {\n let index = -1;\n\n async function dispatch(i: number): Promise<void> {\n if (i <= index) {\n throw new Error(\"next() called multiple times\");\n }\n index = i;\n\n if (i >= middlewares.length) {\n return;\n }\n\n const middleware = middlewares[i] as Middleware;\n const result = await middleware(ctx, () => dispatch(i + 1));\n\n // If middleware returned a response, set ctx.res (last write wins)\n if (\n result &&\n typeof result === \"object\" &&\n \"raw\" in result &&\n typeof result.raw === \"string\"\n ) {\n ctx.res = result;\n }\n }\n\n await dispatch(0);\n };\n}\n","import type { Root } from \"@glion/ast\";\nimport { value as queryValue } from \"@glion/util-query\";\nimport { VFile } from \"vfile\";\n\nimport type {\n ConnectionInfo,\n Context,\n Hl7v2Processor,\n Response,\n} from \"./types\";\n\n/**\n * Options for creating a Context instance.\n */\nexport interface CreateContextOptions {\n /** Raw HL7 message string */\n raw: string;\n /** Raw bytes from the MLLP frame payload */\n bytes: Uint8Array;\n /** Connection metadata */\n connection: ConnectionInfo;\n /** HL7v2 unified processor */\n processor: Hl7v2Processor;\n}\n\n/**\n * Create a new Context for an incoming message.\n *\n * Only the **parse** step runs eagerly (sync, fast). Routing fields are\n * extracted as strings from the parsed tree immediately — these cached\n * strings always reflect the original wire values (see ADR-0013).\n *\n * The expensive pipeline stages are lazy:\n *\n * - `ctx.tree()` — triggers `run()` (transformers: annotations, escape decoding,\n * lint). Cached after first call.\n * - `ctx.result()` — triggers `run()` + `stringify()` (compilation, e.g., JSON\n * via hl7v2Jsonify). Cached after first call.\n *\n * If no handler matches or middleware short-circuits before accessing\n * the tree, the transform and compile steps never execute.\n */\nexport function createContext(options: CreateContextOptions): Context {\n const { raw, bytes, connection, processor } = options;\n const variables = new Map<string, unknown>();\n let varSnapshot: Readonly<Record<string, unknown>> | undefined;\n\n // ── Eager: parse (sync, fast) ──────────────────────────────────────\n // VFile carries the input through the pipeline and collects diagnostics.\n const file = new VFile(raw);\n const parsed = processor.parse(file);\n\n // Extract routing fields as strings from the parsed tree.\n // These are cached immediately and always reflect the original\n // wire values, even after transformers enrich the tree in-place.\n const controlId = queryValue(parsed, \"MSH-10\")?.value ?? \"\";\n const messageType = queryValue(parsed, \"MSH-9.1\")?.value ?? \"\";\n const triggerEvent = queryValue(parsed, \"MSH-9.2\")?.value ?? \"\";\n const messageStructure = queryValue(parsed, \"MSH-9.3\")?.value ?? \"\";\n const version = queryValue(parsed, \"MSH-12.1\")?.value ?? \"\";\n\n // ── Lazy: transform (async, cached) ────────────────────────────────\n // run() applies all transformers: annotations, escape decoding, lint.\n // Only executes on first call to ctx.tree() or ctx.result().\n let transformedTree: Root | undefined;\n let transformPromise: Promise<Root> | undefined;\n\n function getTree(): Promise<Root> {\n if (transformedTree) {\n return Promise.resolve(transformedTree);\n }\n if (!transformPromise) {\n transformPromise = (async () => {\n const result = await processor.run(parsed, file);\n transformedTree = result;\n return result;\n })();\n }\n return transformPromise;\n }\n\n // ── Lazy: compile (async, cached) ──────────────────────────────────\n // stringify() compiles the tree into the final format (e.g., JSON).\n // Only executes on first call to ctx.result(). Requires tree first.\n // Promise is cached (including rejections) to match getTree() behavior.\n let resultPromise: Promise<unknown | undefined> | undefined;\n\n function getResult(): Promise<unknown | undefined> {\n if (!resultPromise) {\n resultPromise = (async () => {\n const tree = await getTree();\n return processor.compiler ? processor.stringify(tree, file) : undefined;\n })();\n }\n return resultPromise;\n }\n\n // ── Build context ──────────────────────────────────────────────────\n const ctx: Context = {\n ast: parsed,\n connection: Object.freeze(connection),\n controlId,\n file,\n get<K extends string>(key: K): unknown {\n return variables.get(key);\n },\n messageStructure,\n messageType,\n req: Object.freeze({ bytes, raw }),\n res: undefined as Response | undefined,\n result: getResult,\n set<K extends string>(key: K, value: unknown): void {\n variables.set(key, value);\n varSnapshot = undefined;\n },\n tree: getTree,\n triggerEvent,\n get var(): Readonly<Record<string, unknown>> {\n if (!varSnapshot) {\n varSnapshot = Object.freeze(Object.fromEntries(variables));\n }\n return varSnapshot;\n },\n version,\n };\n\n return ctx;\n}\n","import type { Root } from \"@glion/ast\";\nimport type { Processor } from \"unified\";\nimport type { VFile } from \"vfile\";\n\n/**\n * Connection metadata for an MLLP socket.\n */\nexport interface ConnectionInfo {\n readonly id: number;\n readonly remoteAddress: string;\n readonly remotePort: number;\n readonly localPort: number;\n readonly secure: boolean;\n readonly state: Map<string, unknown>;\n}\n\n/**\n * MLLP response — a raw HL7v2 string that the server encodes\n * with MLLP framing before sending back over TCP.\n *\n * Acknowledgment helpers (ack/nak) are not built in — register\n * an acknowledgment middleware via `.use()` to generate ACK/NAK responses.\n */\nexport interface Response {\n readonly raw: string;\n}\n\n/**\n * The MLLP context object.\n *\n * Routing fields (messageType, triggerEvent, etc.) are extracted from\n * the **pre-transform** parsed tree and are always available synchronously.\n *\n * The transformed tree, VFile, and compiled result are lazy — they\n * trigger pipeline stages on first access. See ADR-0013.\n */\nexport interface Context {\n /** Incoming message data */\n readonly req: {\n /** Original HL7 message string (without MLLP framing) */\n readonly raw: string;\n /** Raw bytes from MLLP frame payload */\n readonly bytes: Uint8Array;\n };\n\n /** TCP connection metadata */\n readonly connection: ConnectionInfo;\n\n /**\n * Parsed AST — always available synchronously. Use this for reading\n * MSH fields, building ACKs, routing, or any operation that doesn't\n * need to wait for transforms to complete.\n *\n * Note: this is the same object reference as the tree returned by\n * `tree()`. After `tree()` resolves, transformers may have enriched\n * it in-place (e.g., decoded escapes, resolved message structure).\n * The routing fields (messageType, triggerEvent, etc.) are cached\n * as separate string properties and always reflect the original\n * wire values regardless of transforms.\n */\n readonly ast: Root;\n\n /**\n * Transformed AST. Triggers `run()` (transformers) on first call.\n * Cached after first access — subsequent calls return the same tree.\n */\n tree(): Promise<Root>;\n\n /**\n * VFile with diagnostics and lint messages. Created during context\n * creation. Diagnostics accumulate after `tree()` triggers transformers.\n */\n file: VFile;\n\n /**\n * Compiled result from the processor's compiler (e.g., JSON from\n * hl7v2Jsonify). Triggers both `run()` and `stringify()` on first call.\n * Undefined when the processor has no compiler.\n * Cached after first access.\n */\n result(): Promise<unknown | undefined>;\n\n /** Response to send back. Set by middleware or handler. */\n res: Response | undefined;\n\n /** MSH-9.1 message type (e.g., \"ADT\") — from pre-transform parse */\n messageType: string;\n /** MSH-9.2 trigger event (e.g., \"A01\") — from pre-transform parse */\n triggerEvent: string;\n /** MSH-9.3 message structure (e.g., \"ADT_A01\") — from pre-transform parse */\n messageStructure: string;\n /** MSH-12 version (e.g., \"2.5.1\") — from pre-transform parse */\n version: string;\n /** MSH-10 message control ID — from pre-transform parse */\n controlId: string;\n\n /** Store a variable */\n set<K extends string>(key: K, value: unknown): void;\n /** Retrieve a variable */\n get<K extends string>(key: K): unknown;\n /** Read-only snapshot of all variables */\n readonly var: Readonly<Record<string, unknown>>;\n}\n\n/**\n * A unified `Processor` that parses HL7v2 messages into `Root` trees.\n *\n * Only `ParseTree` is constrained — the remaining type params are left\n * loose so that any processor built with `unified().use(hl7v2Parser)`\n * is assignable regardless of which transformer/compiler plugins are added.\n */\nexport type Hl7v2Processor = Processor<Root, Root, Root>;\n\n/**\n * Middleware function signature (Hono/Koa onion model).\n */\nexport type MiddlewareReturn =\n // oxlint-disable-next-line typescript/no-invalid-void-type\n | Promise<Response | undefined | void>\n | Promise<void>\n | Response\n | undefined\n // oxlint-disable-next-line typescript/no-invalid-void-type\n | void;\n\nexport type Middleware = (\n ctx: Context,\n next: () => Promise<void>\n) => MiddlewareReturn;\n\n/**\n * Terminal route handler — may return a response or void (no response).\n */\nexport type Handler = (\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n) => Promise<Response | undefined | void> | Response | undefined | void;\n\n/**\n * Error handler — receives the error and context, returns a response.\n */\nexport type ErrorHandler = (\n err: Error,\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n) => Promise<Response | undefined | void> | Response | undefined | void;\n\n/**\n * A filter function for route or middleware matching.\n * Receives the full context and returns true if the route/middleware applies.\n */\nexport type RouteFilter = (ctx: Context) => boolean;\n\n/**\n * Parsed route pattern for matching against incoming messages.\n */\nexport interface RoutePattern {\n /** Original pattern string (e.g., \"ADT^A01\") */\n readonly source: string;\n /** Message type to match, or \"*\" for wildcard */\n readonly messageType: string;\n /** Trigger event to match, or \"*\" for wildcard */\n readonly triggerEvent: string;\n}\n\n/**\n * Parse a pattern string like \"ADT^A01\", \"ADT^*\", \"*^A01\", or \"*\"\n * into a structured RoutePattern.\n */\nexport function parsePattern(pattern: string): RoutePattern {\n if (pattern === \"*\") {\n return { messageType: \"*\", source: pattern, triggerEvent: \"*\" };\n }\n\n const caretIndex = pattern.indexOf(\"^\");\n if (caretIndex === -1) {\n return { messageType: pattern, source: pattern, triggerEvent: \"*\" };\n }\n\n return {\n messageType: pattern.slice(0, caretIndex) || \"*\",\n source: pattern,\n triggerEvent: pattern.slice(caretIndex + 1) || \"*\",\n };\n}\n\n/**\n * Test whether a route pattern matches a given message type and trigger event.\n */\nexport function matchPattern(\n pattern: RoutePattern,\n messageType: string,\n triggerEvent: string\n): boolean {\n const typeMatch =\n pattern.messageType === \"*\" || pattern.messageType === messageType;\n const eventMatch =\n pattern.triggerEvent === \"*\" || pattern.triggerEvent === triggerEvent;\n return typeMatch && eventMatch;\n}\n","import { matchPattern, parsePattern } from \"./types\";\nimport type { Context, Handler, Middleware, RouteFilter } from \"./types\";\n\n/**\n * A route entry: matcher + handler.\n */\ninterface Route {\n filter: (ctx: Context) => boolean;\n handler: Handler;\n}\n\n/**\n * A scoped middleware entry: matcher + middleware.\n */\ninterface ScopedMiddleware {\n filter: (ctx: Context) => boolean;\n middleware: Middleware;\n}\n\n/**\n * Result of matching a message against registered routes and middleware.\n */\nexport interface MatchResult {\n /** The matched handler, or undefined if no route matches */\n handler: Handler | undefined;\n /** Middleware that applies to this message (global + scoped matches) */\n middlewares: Middleware[];\n}\n\n/**\n * Create a matcher function from a string pattern.\n */\nfunction patternMatcher(pattern: string): (ctx: Context) => boolean {\n const parsed = parsePattern(pattern);\n return (ctx) => matchPattern(parsed, ctx.messageType, ctx.triggerEvent);\n}\n\n/**\n * Pattern-matching router for HL7v2 message types.\n *\n * Routes are matched in registration order (first match wins).\n * Supports string patterns with wildcards or filter functions.\n *\n * String patterns:\n *\n * - `\"ADT^A01\"` — exact match\n * - `\"ADT^*\"` — any ADT message\n * - `\"*\"` — catch-all\n *\n * Filter functions:\n *\n * - `(ctx) => ctx.messageType === \"ADT\" && ctx.version === \"2.5.1\"`\n */\nexport class Router {\n readonly #routes: Route[] = [];\n readonly #globalMiddleware: Middleware[] = [];\n readonly #scopedMiddleware: ScopedMiddleware[] = [];\n\n /**\n * Register a route handler with a string pattern or filter function.\n */\n add(patternOrFilter: string | RouteFilter, handler: Handler): void {\n const filter =\n typeof patternOrFilter === \"string\"\n ? patternMatcher(patternOrFilter)\n : patternOrFilter;\n\n this.#routes.push({ filter, handler });\n }\n\n /**\n * Register global middleware (runs for all messages).\n * When `prepend` is true, the middleware is inserted at the front\n * of the chain so it executes outermost (first in, last out).\n */\n addMiddleware(middleware: Middleware, prepend?: boolean): void;\n /**\n * Register scoped middleware with a string pattern or filter function.\n */\n addMiddleware(\n patternOrFilter: string | RouteFilter,\n middleware: Middleware\n ): void;\n addMiddleware(\n patternOrFilterOrMiddleware: string | RouteFilter | Middleware,\n middlewareOrPrepend?: Middleware | boolean\n ): void {\n // Global middleware: addMiddleware(fn) or addMiddleware(fn, true)\n if (\n typeof patternOrFilterOrMiddleware === \"function\" &&\n (middlewareOrPrepend === undefined ||\n typeof middlewareOrPrepend === \"boolean\")\n ) {\n if (middlewareOrPrepend === true) {\n this.#globalMiddleware.unshift(\n patternOrFilterOrMiddleware as Middleware\n );\n } else {\n this.#globalMiddleware.push(patternOrFilterOrMiddleware as Middleware);\n }\n return;\n }\n\n // Scoped middleware: addMiddleware(pattern/filter, fn)\n if (typeof middlewareOrPrepend === \"function\") {\n const middleware = middlewareOrPrepend;\n const filter =\n typeof patternOrFilterOrMiddleware === \"string\"\n ? patternMatcher(patternOrFilterOrMiddleware)\n : (patternOrFilterOrMiddleware as RouteFilter);\n\n this.#scopedMiddleware.push({ filter, middleware });\n }\n }\n\n /**\n * Match a context against registered routes and middleware.\n * Returns the first matching handler and all applicable middleware.\n */\n match(ctx: Context): MatchResult {\n // Find first matching route (registration order)\n let handler: Handler | undefined;\n for (const route of this.#routes) {\n if (route.filter(ctx)) {\n handler = route.handler;\n break;\n }\n }\n\n // Collect applicable middleware: global + matching scoped\n const middlewares: Middleware[] = [...this.#globalMiddleware];\n for (const scoped of this.#scopedMiddleware) {\n if (scoped.filter(ctx)) {\n middlewares.push(scoped.middleware);\n }\n }\n\n return { handler, middlewares };\n }\n}\n","import { MllpError } from \"@glion/mllp-transport\";\n\nimport { compose } from \"./compose\";\nimport { createContext } from \"./context\";\nimport { Router } from \"./router\";\nimport type {\n ConnectionInfo,\n Context,\n ErrorHandler,\n Handler,\n Hl7v2Processor,\n Middleware,\n Response,\n RouteFilter,\n} from \"./types\";\n\n/**\n * Routing fields from the message that caused an error.\n * Attached to errors thrown by `Mllp.handle()` so that callers\n * (e.g., `serve()`) can include message context in error callbacks.\n */\nexport interface MessageInfo {\n readonly messageType: string;\n readonly triggerEvent: string;\n readonly messageStructure: string;\n readonly version: string;\n readonly controlId: string;\n}\n\n/**\n * Internal store for associating message routing info with errors.\n * Uses a WeakMap so error objects can be garbage-collected normally.\n */\nconst errorMessageInfo = new WeakMap<Error, MessageInfo>();\n\n/**\n * Retrieve the {@link MessageInfo} associated with an error thrown\n * by `Mllp.handle()`. Returns `undefined` for errors that did not\n * originate from message processing (e.g., lifecycle callback errors).\n */\nexport function getMessageInfo(error: Error): MessageInfo | undefined {\n return errorMessageInfo.get(error);\n}\n\n/**\n * MLLP application for HL7v2 messaging.\n *\n * A pure routing and middleware engine with no TCP/server concerns.\n * Use `serve()` to bind this to a TCP server.\n *\n * @example\n * ```typescript\n * import { Mllp } from \"@glion/mllp\";\n * import { parseHL7v2 } from \"@glion/hl7v2\";\n *\n * const app = new Mllp().parser(parseHL7v2).on(\"ADT^A01\", async (ctx) => {\n * const tree = await ctx.tree();\n * return { raw: \"...\" };\n * });\n * ```;\n */\nexport class Mllp {\n readonly #router = new Router();\n #processor: Hl7v2Processor | undefined;\n #errorHandler: ErrorHandler | undefined;\n\n /**\n * Register an HL7v2 processor for incoming messages.\n *\n * Accepts a unified `Processor` — either a pre-built one like\n * `parseHL7v2` from `@glion/hl7v2`, or a custom composition\n * via `unified().use(hl7v2Parser).use(...)`.\n *\n * Must be called before `handle()`. Calling multiple times replaces\n * the previous processor (last-write-wins).\n */\n parser(processor: Hl7v2Processor): this {\n this.#processor = processor;\n return this;\n }\n\n /**\n * Register middleware.\n *\n * Accepts:\n *\n * - A middleware function: `(ctx, next) => { ... }`\n * - A scoped middleware: `app.use('ADT^*', middleware)` or `app.use(filter,\n * middleware)`\n */\n use(middleware: Middleware, options?: { prepend?: boolean }): this;\n use(patternOrFilter: string | RouteFilter, middleware: Middleware): this;\n use(\n first: string | RouteFilter | Middleware,\n second?: Middleware | { prepend?: boolean }\n ): this {\n // Scoped middleware: use('ADT^*', middleware) or use(filter, middleware)\n if (\n (typeof first === \"string\" || typeof first === \"function\") &&\n typeof second === \"function\"\n ) {\n this.#router.addMiddleware(first as string | RouteFilter, second);\n return this;\n }\n\n // Standard middleware: use((ctx, next) => { ... })\n if (typeof first === \"function\") {\n const prepend =\n second && typeof second === \"object\" ? second.prepend : false;\n this.#router.addMiddleware(first as Middleware, prepend);\n return this;\n }\n\n return this;\n }\n\n /**\n * Register a route handler for a message type pattern or filter function.\n *\n * String patterns:\n *\n * - `\"ADT^A01\"` — exact match\n * - `\"ADT^*\"` — any ADT message\n * - `\"*^A01\"` — any type with A01 trigger\n * - `\"*\"` — catch-all\n *\n * Filter functions:\n *\n * - `(ctx) => ctx.messageType === \"ADT\" && ctx.version === \"2.5.1\"`\n */\n on(patternOrFilter: string | RouteFilter, handler: Handler): this {\n this.#router.add(patternOrFilter, handler);\n return this;\n }\n\n /**\n * Register a global error handler.\n *\n * Called when middleware or a handler throws. Without an error handler,\n * the error is re-thrown to the caller (e.g. `serve()`).\n *\n * If the error handler returns a response, it is used as the reply.\n * If the error handler itself throws, the new error is re-thrown.\n */\n onError(handler: ErrorHandler): this {\n this.#errorHandler = handler;\n return this;\n }\n\n /**\n * Process a raw HL7v2 message through the middleware chain and router.\n * This is the integration point — analogous to Hono's `fetch()`.\n *\n * The pipeline is lazy (see ADR-0013):\n * 1. Parse (sync, fast) — always runs, extracts routing fields\n * 2. Route match — uses pre-transform routing fields\n * 3. If no match → return undefined (no transform/compile cost)\n * 4. Transform/compile — only when handlers access ctx.tree()/ctx.result()\n *\n * Throws `MllpError` if no processor has been registered via `app.parser()`.\n */\n async handle(\n raw: string,\n bytes: Uint8Array,\n connection: ConnectionInfo\n // oxlint-disable-next-line typescript/no-invalid-void-type\n ): Promise<Response | undefined | void> {\n if (!this.#processor) {\n throw new MllpError(\n \"ERR_NO_PARSER\",\n \"No parser registered. Call app.parser() before handling messages.\"\n );\n }\n\n // Context creation is sync — only parse() runs here.\n // Transform and compile are deferred to ctx.tree() / ctx.result().\n const ctx = createContext({\n bytes,\n connection,\n processor: this.#processor,\n raw,\n });\n\n try {\n // Match route and collect middleware\n const match = this.#router.match(ctx);\n const middlewares = [...match.middlewares];\n\n // Add the handler as the terminal middleware\n if (match.handler) {\n const handler = match.handler;\n middlewares.push((handlerCtx: Context) => handler(handlerCtx));\n }\n\n // Compose and execute\n await compose(middlewares)(ctx);\n\n return ctx.res;\n } catch (error) {\n return this.#handleError(\n error instanceof Error ? error : new Error(String(error)),\n ctx\n );\n }\n }\n\n /**\n * Handle an error during message processing.\n *\n * If a custom error handler is registered, delegates to it.\n * If the error handler itself throws, re-throws the error handler's error.\n * If no error handler is registered, re-throws the original error.\n *\n * In both re-throw cases, message routing fields are attached to the\n * error via {@link getMessageInfo} so callers can include message\n * context in error reporting.\n *\n * Callers (e.g. `serve()`) are responsible for catching these errors\n * and deciding how to handle them at the transport level.\n */\n async #handleError(\n err: Error,\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n ): Promise<Response | undefined | void> {\n const info: MessageInfo = {\n controlId: ctx.controlId,\n messageStructure: ctx.messageStructure,\n messageType: ctx.messageType,\n triggerEvent: ctx.triggerEvent,\n version: ctx.version,\n };\n\n if (this.#errorHandler) {\n try {\n return await this.#errorHandler(err, ctx);\n } catch (handlerError) {\n const e =\n handlerError instanceof Error\n ? handlerError\n : new Error(String(handlerError));\n errorMessageInfo.set(e, info);\n throw e;\n }\n }\n\n errorMessageInfo.set(err, info);\n throw err;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mllp.js","names":["queryValue","#routes","#globalMiddleware","#scopedMiddleware","#router","#processor","#errorHandler","#handleError"],"sources":["../src/errors.ts","../src/server/compose.ts","../src/server/context.ts","../src/server/types.ts","../src/server/router.ts","../src/server/mllp.ts"],"sourcesContent":["/**\n * Errors raised by the `@glion/mllp` server itself.\n *\n * Per ADR 0018: one base error class discriminated by a stable `code` string.\n * A NAK the server *builds* in response to a message is an `@glion/ack`\n * `AckException` — a different concern from a server-lifecycle/config failure,\n * which is what `MllpServerError` represents.\n *\n * @module\n */\n\nexport const MllpServerErrorCode = {\n /**\n * An inbound message could not be decoded as UTF-8 — the only character set\n * supported for now. The underlying `@glion/util-charset` `CharsetError` is\n * kept on `cause`; callers branch on this code without importing that\n * package.\n */\n INCOMPATIBLE_CHARSET: \"INCOMPATIBLE_CHARSET\",\n /**\n * `app.handle()` was called before a parser was registered via\n * `app.parser()`.\n */\n NO_PARSER: \"NO_PARSER\",\n /**\n * The MLLP byte stream itself was violated and the connection closed:\n * the remote system sent bytes outside a message, glued or unterminated\n * messages, an over-cap message — or a handler's response contained a\n * reserved VT/FS byte and could not be framed. The underlying\n * `@glion/mllp-codec` `MllpCodecError` is kept on `cause`; branch on this\n * code without importing that package.\n */\n PROTOCOL_VIOLATION: \"PROTOCOL_VIOLATION\",\n} as const;\nexport type MllpServerErrorCode =\n (typeof MllpServerErrorCode)[keyof typeof MllpServerErrorCode];\n\n/** The one error class the `@glion/mllp` server raises. Branch on `code`. */\nexport class MllpServerError extends Error {\n readonly code: MllpServerErrorCode;\n constructor(\n code: MllpServerErrorCode,\n message: string,\n opts?: { cause?: unknown }\n ) {\n super(message, opts);\n this.name = \"MllpServerError\";\n this.code = code;\n }\n}\n","import type { Context, Middleware } from \"./types\";\n\n/**\n * Compose an array of middleware into a single function that executes\n * them in onion-model order (like Hono/Koa).\n *\n * Each middleware receives (ctx, next). Calling next() proceeds to the\n * next middleware. If a middleware returns a Response without\n * calling next(), it short-circuits the chain.\n *\n * Response handling follows Hono's pattern: return values are written\n * to ctx.res, and middleware can also set ctx.res directly.\n * Last write wins.\n */\nexport function compose(\n middlewares: Middleware[]\n): (ctx: Context) => Promise<void> {\n return async (ctx: Context): Promise<void> => {\n let index = -1;\n\n async function dispatch(i: number): Promise<void> {\n if (i <= index) {\n throw new Error(\"next() called multiple times\");\n }\n index = i;\n\n if (i >= middlewares.length) {\n return;\n }\n\n const middleware = middlewares[i] as Middleware;\n const result = await middleware(ctx, () => dispatch(i + 1));\n\n // If middleware returned a response, set ctx.res (last write wins)\n if (\n result &&\n typeof result === \"object\" &&\n \"raw\" in result &&\n typeof result.raw === \"string\"\n ) {\n ctx.res = result;\n }\n }\n\n await dispatch(0);\n };\n}\n","import type { Root } from \"@glion/ast\";\nimport { value as queryValue } from \"@glion/util-query\";\nimport { VFile } from \"vfile\";\n\nimport type {\n ConnectionInfo,\n Context,\n Hl7v2Processor,\n Response,\n} from \"./types\";\n\n/**\n * Options for creating a Context instance.\n */\nexport interface CreateContextOptions {\n /** Raw HL7 message string */\n raw: string;\n /** Raw bytes from the MLLP frame payload */\n bytes: Uint8Array;\n /** Connection metadata */\n connection: ConnectionInfo;\n /** HL7v2 unified processor */\n processor: Hl7v2Processor;\n}\n\n/**\n * Create a new Context for an incoming message.\n *\n * Only the **parse** step runs eagerly (sync, fast). Routing fields are\n * extracted as strings from the parsed tree immediately — these cached\n * strings always reflect the original wire values (see ADR-0013).\n *\n * The expensive pipeline stages are lazy:\n *\n * - `ctx.tree()` — triggers `run()` (transformers: annotations, escape decoding,\n * lint). Cached after first call.\n * - `ctx.result()` — triggers `run()` + `stringify()` (compilation, e.g., JSON\n * via hl7v2Jsonify). Cached after first call.\n *\n * If no handler matches or middleware short-circuits before accessing\n * the tree, the transform and compile steps never execute.\n */\nexport function createContext(options: CreateContextOptions): Context {\n const { raw, bytes, connection, processor } = options;\n const variables = new Map<string, unknown>();\n let varSnapshot: Readonly<Record<string, unknown>> | undefined;\n\n // ── Eager: parse (sync, fast) ──────────────────────────────────────\n // VFile carries the input through the pipeline and collects diagnostics.\n const file = new VFile(raw);\n const parsed = processor.parse(file);\n\n // Extract routing fields as strings from the parsed tree.\n // These are cached immediately and always reflect the original\n // wire values, even after transformers enrich the tree in-place.\n const controlId = queryValue(parsed, \"MSH-10\")?.value ?? \"\";\n const messageType = queryValue(parsed, \"MSH-9.1\")?.value ?? \"\";\n const triggerEvent = queryValue(parsed, \"MSH-9.2\")?.value ?? \"\";\n const messageStructure = queryValue(parsed, \"MSH-9.3\")?.value ?? \"\";\n const version = queryValue(parsed, \"MSH-12.1\")?.value ?? \"\";\n\n // ── Lazy: transform (async, cached) ────────────────────────────────\n // run() applies all transformers: annotations, escape decoding, lint.\n // Only executes on first call to ctx.tree() or ctx.result().\n let transformedTree: Root | undefined;\n let transformPromise: Promise<Root> | undefined;\n\n function getTree(): Promise<Root> {\n if (transformedTree) {\n return Promise.resolve(transformedTree);\n }\n if (!transformPromise) {\n transformPromise = (async () => {\n // Hl7v2Processor's tail admits `undefined` (so the bare parser is\n // assignable), which makes unified type run()'s result as\n // `Root | Node`; an HL7v2 pipeline always yields the (possibly\n // transformed) Root.\n const result = (await processor.run(parsed, file)) as Root;\n transformedTree = result;\n return result;\n })();\n }\n return transformPromise;\n }\n\n // ── Lazy: compile (async, cached) ──────────────────────────────────\n // stringify() compiles the tree into the final format (e.g., JSON).\n // Only executes on first call to ctx.result(). Requires tree first.\n // Promise is cached (including rejections) to match getTree() behavior.\n let resultPromise: Promise<unknown | undefined> | undefined;\n\n function getResult(): Promise<unknown | undefined> {\n if (!resultPromise) {\n resultPromise = (async () => {\n const tree = await getTree();\n return processor.compiler ? processor.stringify(tree, file) : undefined;\n })();\n }\n return resultPromise;\n }\n\n // ── Build context ──────────────────────────────────────────────────\n const ctx: Context = {\n ast: parsed,\n connection: Object.freeze(connection),\n controlId,\n file,\n get<K extends string>(key: K): unknown {\n return variables.get(key);\n },\n messageStructure,\n messageType,\n req: Object.freeze({ bytes, raw }),\n res: undefined as Response | undefined,\n result: getResult,\n set<K extends string>(key: K, value: unknown): void {\n variables.set(key, value);\n varSnapshot = undefined;\n },\n tree: getTree,\n triggerEvent,\n get var(): Readonly<Record<string, unknown>> {\n if (!varSnapshot) {\n varSnapshot = Object.freeze(Object.fromEntries(variables));\n }\n return varSnapshot;\n },\n version,\n };\n\n return ctx;\n}\n","import type { Root } from \"@glion/ast\";\nimport type { VFile } from \"vfile\";\n\n/**\n * Connection metadata for an MLLP socket.\n */\nexport interface ConnectionInfo {\n readonly id: number;\n readonly remoteAddress: string;\n readonly remotePort: number;\n readonly localPort: number;\n readonly secure: boolean;\n readonly state: Map<string, unknown>;\n}\n\n/**\n * MLLP response — a raw HL7v2 string that the server encodes\n * with MLLP framing before sending back over TCP.\n *\n * Acknowledgment helpers (ack/nak) are not built in — register\n * an acknowledgment middleware via `.use()` to generate ACK/NAK responses.\n */\nexport interface Response {\n readonly raw: string;\n}\n\n/**\n * The MLLP context object.\n *\n * Routing fields (messageType, triggerEvent, etc.) are extracted from\n * the **pre-transform** parsed tree and are always available synchronously.\n *\n * The transformed tree, VFile, and compiled result are lazy — they\n * trigger pipeline stages on first access. See ADR-0013.\n */\nexport interface Context {\n /** Incoming message data */\n readonly req: {\n /** Original HL7 message string (without MLLP framing) */\n readonly raw: string;\n /** Raw bytes from MLLP frame payload */\n readonly bytes: Uint8Array;\n };\n\n /** TCP connection metadata */\n readonly connection: ConnectionInfo;\n\n /**\n * Parsed AST — always available synchronously. Use this for reading\n * MSH fields, building ACKs, routing, or any operation that doesn't\n * need to wait for transforms to complete.\n *\n * Note: this is the same object reference as the tree returned by\n * `tree()`. After `tree()` resolves, transformers may have enriched\n * it in-place (e.g., decoded escapes, resolved message structure).\n * The routing fields (messageType, triggerEvent, etc.) are cached\n * as separate string properties and always reflect the original\n * wire values regardless of transforms.\n */\n readonly ast: Root;\n\n /**\n * Transformed AST. Triggers `run()` (transformers) on first call.\n * Cached after first access — subsequent calls return the same tree.\n */\n tree(): Promise<Root>;\n\n /**\n * VFile with diagnostics and lint messages. Created during context\n * creation. Diagnostics accumulate after `tree()` triggers transformers.\n */\n file: VFile;\n\n /**\n * Compiled result from the processor's compiler (e.g., JSON from\n * hl7v2Jsonify). Triggers both `run()` and `stringify()` on first call.\n * Undefined when the processor has no compiler.\n * Cached after first access.\n */\n result(): Promise<unknown | undefined>;\n\n /** Response to send back. Set by middleware or handler. */\n res: Response | undefined;\n\n /** MSH-9.1 message type (e.g., \"ADT\") — from pre-transform parse */\n messageType: string;\n /** MSH-9.2 trigger event (e.g., \"A01\") — from pre-transform parse */\n triggerEvent: string;\n /** MSH-9.3 message structure (e.g., \"ADT_A01\") — from pre-transform parse */\n messageStructure: string;\n /** MSH-12 version (e.g., \"2.5.1\") — from pre-transform parse */\n version: string;\n /** MSH-10 message control ID — from pre-transform parse */\n controlId: string;\n\n /** Store a variable */\n set<K extends string>(key: K, value: unknown): void;\n /** Retrieve a variable */\n get<K extends string>(key: K): unknown;\n /** Read-only snapshot of all variables */\n readonly var: Readonly<Record<string, unknown>>;\n}\n\n// Re-exported from @glion/parser — the ecosystem home of \"a unified\n// processor over HL7v2 trees\"; the client's `parser` option shares it.\nexport type { Hl7v2Processor } from \"@glion/parser\";\n\n/**\n * Middleware function signature (Hono/Koa onion model).\n */\nexport type MiddlewareReturn =\n // oxlint-disable-next-line typescript/no-invalid-void-type\n | Promise<Response | undefined | void>\n | Promise<void>\n | Response\n | undefined\n // oxlint-disable-next-line typescript/no-invalid-void-type\n | void;\n\nexport type Middleware = (\n ctx: Context,\n next: () => Promise<void>\n) => MiddlewareReturn;\n\n/**\n * Terminal route handler — may return a response or void (no response).\n */\nexport type Handler = (\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n) => Promise<Response | undefined | void> | Response | undefined | void;\n\n/**\n * Error handler — receives the error and context, returns a response.\n */\nexport type ErrorHandler = (\n err: Error,\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n) => Promise<Response | undefined | void> | Response | undefined | void;\n\n/**\n * A filter function for route or middleware matching.\n * Receives the full context and returns true if the route/middleware applies.\n */\nexport type RouteFilter = (ctx: Context) => boolean;\n\n/**\n * Parsed route pattern for matching against incoming messages.\n */\nexport interface RoutePattern {\n /** Original pattern string (e.g., \"ADT^A01\") */\n readonly source: string;\n /** Message type to match, or \"*\" for wildcard */\n readonly messageType: string;\n /** Trigger event to match, or \"*\" for wildcard */\n readonly triggerEvent: string;\n}\n\n/**\n * Parse a pattern string like \"ADT^A01\", \"ADT^*\", \"*^A01\", or \"*\"\n * into a structured RoutePattern.\n */\nexport function parsePattern(pattern: string): RoutePattern {\n if (pattern === \"*\") {\n return { messageType: \"*\", source: pattern, triggerEvent: \"*\" };\n }\n\n const caretIndex = pattern.indexOf(\"^\");\n if (caretIndex === -1) {\n return { messageType: pattern, source: pattern, triggerEvent: \"*\" };\n }\n\n return {\n messageType: pattern.slice(0, caretIndex) || \"*\",\n source: pattern,\n triggerEvent: pattern.slice(caretIndex + 1) || \"*\",\n };\n}\n\n/**\n * Test whether a route pattern matches a given message type and trigger event.\n */\nexport function matchPattern(\n pattern: RoutePattern,\n messageType: string,\n triggerEvent: string\n): boolean {\n const typeMatch =\n pattern.messageType === \"*\" || pattern.messageType === messageType;\n const eventMatch =\n pattern.triggerEvent === \"*\" || pattern.triggerEvent === triggerEvent;\n return typeMatch && eventMatch;\n}\n","import { matchPattern, parsePattern } from \"./types\";\nimport type { Context, Handler, Middleware, RouteFilter } from \"./types\";\n\n/**\n * A route entry: matcher + handler.\n */\ninterface Route {\n filter: (ctx: Context) => boolean;\n handler: Handler;\n}\n\n/**\n * A scoped middleware entry: matcher + middleware.\n */\ninterface ScopedMiddleware {\n filter: (ctx: Context) => boolean;\n middleware: Middleware;\n}\n\n/**\n * Result of matching a message against registered routes and middleware.\n */\nexport interface MatchResult {\n /** The matched handler, or undefined if no route matches */\n handler: Handler | undefined;\n /** Middleware that applies to this message (global + scoped matches) */\n middlewares: Middleware[];\n}\n\n/**\n * Create a matcher function from a string pattern.\n */\nfunction patternMatcher(pattern: string): (ctx: Context) => boolean {\n const parsed = parsePattern(pattern);\n return (ctx) => matchPattern(parsed, ctx.messageType, ctx.triggerEvent);\n}\n\n/**\n * Pattern-matching router for HL7v2 message types.\n *\n * Routes are matched in registration order (first match wins).\n * Supports string patterns with wildcards or filter functions.\n *\n * String patterns:\n *\n * - `\"ADT^A01\"` — exact match\n * - `\"ADT^*\"` — any ADT message\n * - `\"*\"` — catch-all\n *\n * Filter functions:\n *\n * - `(ctx) => ctx.messageType === \"ADT\" && ctx.version === \"2.5.1\"`\n */\nexport class Router {\n readonly #routes: Route[] = [];\n readonly #globalMiddleware: Middleware[] = [];\n readonly #scopedMiddleware: ScopedMiddleware[] = [];\n\n /**\n * Register a route handler with a string pattern or filter function.\n */\n add(patternOrFilter: string | RouteFilter, handler: Handler): void {\n const filter =\n typeof patternOrFilter === \"string\"\n ? patternMatcher(patternOrFilter)\n : patternOrFilter;\n\n this.#routes.push({ filter, handler });\n }\n\n /**\n * Register global middleware (runs for all messages).\n * When `prepend` is true, the middleware is inserted at the front\n * of the chain so it executes outermost (first in, last out).\n */\n addMiddleware(middleware: Middleware, prepend?: boolean): void;\n /**\n * Register scoped middleware with a string pattern or filter function.\n */\n addMiddleware(\n patternOrFilter: string | RouteFilter,\n middleware: Middleware\n ): void;\n addMiddleware(\n patternOrFilterOrMiddleware: string | RouteFilter | Middleware,\n middlewareOrPrepend?: Middleware | boolean\n ): void {\n // Global middleware: addMiddleware(fn) or addMiddleware(fn, true)\n if (\n typeof patternOrFilterOrMiddleware === \"function\" &&\n (middlewareOrPrepend === undefined ||\n typeof middlewareOrPrepend === \"boolean\")\n ) {\n if (middlewareOrPrepend === true) {\n this.#globalMiddleware.unshift(\n patternOrFilterOrMiddleware as Middleware\n );\n } else {\n this.#globalMiddleware.push(patternOrFilterOrMiddleware as Middleware);\n }\n return;\n }\n\n // Scoped middleware: addMiddleware(pattern/filter, fn)\n if (typeof middlewareOrPrepend === \"function\") {\n const middleware = middlewareOrPrepend;\n const filter =\n typeof patternOrFilterOrMiddleware === \"string\"\n ? patternMatcher(patternOrFilterOrMiddleware)\n : (patternOrFilterOrMiddleware as RouteFilter);\n\n this.#scopedMiddleware.push({ filter, middleware });\n }\n }\n\n /**\n * Match a context against registered routes and middleware.\n * Returns the first matching handler and all applicable middleware.\n */\n match(ctx: Context): MatchResult {\n // Find first matching route (registration order)\n let handler: Handler | undefined;\n for (const route of this.#routes) {\n if (route.filter(ctx)) {\n handler = route.handler;\n break;\n }\n }\n\n // Collect applicable middleware: global + matching scoped\n const middlewares: Middleware[] = [...this.#globalMiddleware];\n for (const scoped of this.#scopedMiddleware) {\n if (scoped.filter(ctx)) {\n middlewares.push(scoped.middleware);\n }\n }\n\n return { handler, middlewares };\n }\n}\n","import { MllpServerError, MllpServerErrorCode } from \"../errors\";\nimport { compose } from \"./compose\";\nimport { createContext } from \"./context\";\nimport { Router } from \"./router\";\nimport type {\n ConnectionInfo,\n Context,\n ErrorHandler,\n Handler,\n Hl7v2Processor,\n Middleware,\n Response,\n RouteFilter,\n} from \"./types\";\n\n/**\n * Routing fields from the message that caused an error.\n * Attached to errors thrown by `Mllp.handle()` so that callers\n * (e.g., `serve()`) can include message context in error callbacks.\n */\nexport interface MessageInfo {\n readonly messageType: string;\n readonly triggerEvent: string;\n readonly messageStructure: string;\n readonly version: string;\n readonly controlId: string;\n}\n\n/**\n * Internal store for associating message routing info with errors.\n * Uses a WeakMap so error objects can be garbage-collected normally.\n */\nconst errorMessageInfo = new WeakMap<Error, MessageInfo>();\n\n/**\n * Retrieve the {@link MessageInfo} associated with an error thrown\n * by `Mllp.handle()`. Returns `undefined` for errors that did not\n * originate from message processing (e.g., lifecycle callback errors).\n */\nexport function getMessageInfo(error: unknown): MessageInfo | undefined {\n // A WeakMap lookup; a non-error key simply misses and yields undefined.\n return errorMessageInfo.get(error as Error);\n}\n\n/**\n * MLLP application for HL7v2 messaging.\n *\n * A pure routing and middleware engine with no TCP/server concerns.\n * Use `serve()` to bind this to a TCP server.\n *\n * @example\n * ```typescript\n * import { Mllp } from \"@glion/mllp\";\n * import { parseHL7v2 } from \"@glion/hl7v2\";\n *\n * const app = new Mllp().parser(parseHL7v2).on(\"ADT^A01\", async (ctx) => {\n * const tree = await ctx.tree();\n * return { raw: \"...\" };\n * });\n * ```;\n */\nexport class Mllp {\n readonly #router = new Router();\n #processor: Hl7v2Processor | undefined;\n #errorHandler: ErrorHandler | undefined;\n\n /**\n * Register an HL7v2 processor for incoming messages.\n *\n * Accepts a unified `Processor` — either a pre-built one like\n * `parseHL7v2` from `@glion/hl7v2`, or a custom composition\n * via `unified().use(hl7v2Parser).use(...)`.\n *\n * Must be called before `handle()`. Calling multiple times replaces\n * the previous processor (last-write-wins).\n */\n parser(processor: Hl7v2Processor): this {\n this.#processor = processor;\n return this;\n }\n\n /**\n * Register middleware.\n *\n * Accepts:\n *\n * - A middleware function: `(ctx, next) => { ... }`\n * - A scoped middleware: `app.use('ADT^*', middleware)` or `app.use(filter,\n * middleware)`\n */\n use(middleware: Middleware, options?: { prepend?: boolean }): this;\n use(patternOrFilter: string | RouteFilter, middleware: Middleware): this;\n use(\n first: string | RouteFilter | Middleware,\n second?: Middleware | { prepend?: boolean }\n ): this {\n // Scoped middleware: use('ADT^*', middleware) or use(filter, middleware)\n if (\n (typeof first === \"string\" || typeof first === \"function\") &&\n typeof second === \"function\"\n ) {\n this.#router.addMiddleware(first as string | RouteFilter, second);\n return this;\n }\n\n // Standard middleware: use((ctx, next) => { ... })\n if (typeof first === \"function\") {\n const prepend =\n second && typeof second === \"object\" ? second.prepend : false;\n this.#router.addMiddleware(first as Middleware, prepend);\n return this;\n }\n\n return this;\n }\n\n /**\n * Register a route handler for a message type pattern or filter function.\n *\n * String patterns:\n *\n * - `\"ADT^A01\"` — exact match\n * - `\"ADT^*\"` — any ADT message\n * - `\"*^A01\"` — any type with A01 trigger\n * - `\"*\"` — catch-all\n *\n * Filter functions:\n *\n * - `(ctx) => ctx.messageType === \"ADT\" && ctx.version === \"2.5.1\"`\n */\n on(patternOrFilter: string | RouteFilter, handler: Handler): this {\n this.#router.add(patternOrFilter, handler);\n return this;\n }\n\n /**\n * Register a global error handler.\n *\n * Called when middleware or a handler throws. Without an error handler,\n * the error is re-thrown to the caller (e.g. `serve()`).\n *\n * If the error handler returns a response, it is used as the reply.\n * If the error handler itself throws, the new error is re-thrown.\n */\n onError(handler: ErrorHandler): this {\n this.#errorHandler = handler;\n return this;\n }\n\n /**\n * Process a raw HL7v2 message through the middleware chain and router.\n * This is the integration point — analogous to Hono's `fetch()`.\n *\n * The pipeline is lazy (see ADR-0013):\n *\n * 1. Parse (sync, fast) — always runs, extracts routing fields\n * 2. Route match — uses pre-transform routing fields\n * 3. If no match → return undefined (no transform/compile cost)\n * 4. Transform/compile — only when handlers access ctx.tree()/ctx.result()\n *\n * Throws `MllpServerError` (`NO_PARSER`) if no processor has been registered\n * via `app.parser()`.\n */\n async handle(\n raw: string,\n bytes: Uint8Array,\n connection: ConnectionInfo\n // oxlint-disable-next-line typescript/no-invalid-void-type\n ): Promise<Response | undefined | void> {\n if (!this.#processor) {\n throw new MllpServerError(\n MllpServerErrorCode.NO_PARSER,\n \"No parser registered. Call app.parser() before handling messages.\"\n );\n }\n\n // Context creation is sync — only parse() runs here.\n // Transform and compile are deferred to ctx.tree() / ctx.result().\n const ctx = createContext({\n bytes,\n connection,\n processor: this.#processor,\n raw,\n });\n\n try {\n // Match route and collect middleware\n const match = this.#router.match(ctx);\n const middlewares = [...match.middlewares];\n\n // Add the handler as the terminal middleware\n if (match.handler) {\n const handler = match.handler;\n middlewares.push((handlerCtx: Context) => handler(handlerCtx));\n }\n\n // Compose and execute\n await compose(middlewares)(ctx);\n\n return ctx.res;\n } catch (error) {\n return this.#handleError(\n error instanceof Error ? error : new Error(String(error)),\n ctx\n );\n }\n }\n\n /**\n * Handle an error during message processing.\n *\n * If a custom error handler is registered, delegates to it.\n * If the error handler itself throws, re-throws the error handler's error.\n * If no error handler is registered, re-throws the original error.\n *\n * In both re-throw cases, message routing fields are attached to the\n * error via {@link getMessageInfo} so callers can include message\n * context in error reporting.\n *\n * Callers (e.g. `serve()`) are responsible for catching these errors\n * and deciding how to handle them at the transport level.\n */\n async #handleError(\n err: Error,\n ctx: Context\n // oxlint-disable-next-line typescript/no-invalid-void-type\n ): Promise<Response | undefined | void> {\n const info: MessageInfo = {\n controlId: ctx.controlId,\n messageStructure: ctx.messageStructure,\n messageType: ctx.messageType,\n triggerEvent: ctx.triggerEvent,\n version: ctx.version,\n };\n\n if (this.#errorHandler) {\n try {\n return await this.#errorHandler(err, ctx);\n } catch (handlerError) {\n const e =\n handlerError instanceof Error\n ? handlerError\n : new Error(String(handlerError));\n errorMessageInfo.set(e, info);\n throw e;\n }\n }\n\n errorMessageInfo.set(err, info);\n throw err;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/node/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,KAAK,EAIV,UAAU,EAEX,MAAM,mBAAmB,CAAC;AAE3B;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/node/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,KAAK,EAIV,UAAU,EAEX,MAAM,mBAAmB,CAAC;AAE3B;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AA4ED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,UAAU,CAiGpE"}
|
package/dist/node/serve.d.ts
CHANGED
|
@@ -25,10 +25,12 @@ export type ConnectionCallback = (connection: ConnectionInfo) => void | Promise<
|
|
|
25
25
|
* For lifecycle callback errors (onConnect/onDisconnect), `messageInfo`
|
|
26
26
|
* is `undefined`.
|
|
27
27
|
*
|
|
28
|
-
* This fires
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* This fires for application-level errors (handler/middleware throws with no
|
|
29
|
+
* app-level error handler, or the app-level error handler itself throws) and
|
|
30
|
+
* for a server-level failure decoding an inbound message — surfaced as an
|
|
31
|
+
* {@link MllpServerError} (`code` `INCOMPATIBLE_CHARSET`), never the codec's
|
|
32
|
+
* own error. Stream-level errors (connection reset) do not trigger this
|
|
33
|
+
* callback.
|
|
32
34
|
*/
|
|
33
35
|
export type ErrorCallback = (error: Error, connection: ConnectionInfo, messageInfo: MessageInfo | undefined) => void | Promise<void>;
|
|
34
36
|
/**
|
package/dist/node/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/node/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/node/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAC/B,UAAU,EAAE,cAAc,KACvB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,cAAc,EAC1B,WAAW,EAAE,WAAW,GAAG,SAAS,KACjC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAE/B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAElC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,GAAG,CAAC,EAAE;QACJ,sEAAsE;QACtE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACrB,8BAA8B;QAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QACtB,kDAAkD;QAClD,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;QACrB,+CAA+C;QAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAElC;;;OAGG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CA+B9D"}
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { n as getMessageInfo } from "./mllp.js";
|
|
2
|
-
import {
|
|
1
|
+
import { a as MllpServerError, n as getMessageInfo, o as MllpServerErrorCode } from "./mllp.js";
|
|
2
|
+
import { MllpCodecError, frame, unframe } from "@glion/mllp-codec";
|
|
3
|
+
import { CharsetError, decodeBytes, encodeBytes } from "@glion/util-charset";
|
|
3
4
|
import { once } from "node:events";
|
|
4
5
|
import { createServer } from "node:net";
|
|
5
6
|
import { Readable } from "node:stream";
|
|
@@ -52,7 +53,8 @@ async function writeToSocket(socket, chunk) {
|
|
|
52
53
|
function wrapNodeSocket(socket, secure) {
|
|
53
54
|
return {
|
|
54
55
|
close() {
|
|
55
|
-
socket.
|
|
56
|
+
socket.resume();
|
|
57
|
+
socket.end();
|
|
56
58
|
},
|
|
57
59
|
get localPort() {
|
|
58
60
|
return socket.localPort ?? 0;
|
|
@@ -279,8 +281,7 @@ async function reportError(error, connection, lifecycle, messageInfo) {
|
|
|
279
281
|
* @param lifecycle - Optional lifecycle callbacks from ServeOptions.
|
|
280
282
|
*/
|
|
281
283
|
function handleConnection(app, socket, lifecycle) {
|
|
282
|
-
const
|
|
283
|
-
const reader = socket.readable.pipeThrough(decoder).getReader();
|
|
284
|
+
const reader = socket.readable.pipeThrough(unframe()).getReader();
|
|
284
285
|
const writer = socket.writable.getWriter();
|
|
285
286
|
const connection = {
|
|
286
287
|
id: nextConnectionId++,
|
|
@@ -300,19 +301,22 @@ function handleConnection(app, socket, lifecycle) {
|
|
|
300
301
|
}
|
|
301
302
|
try {
|
|
302
303
|
while (true) {
|
|
303
|
-
const { done, value:
|
|
304
|
+
const { done, value: payload } = await reader.read();
|
|
304
305
|
if (done) break;
|
|
305
306
|
let response;
|
|
306
307
|
try {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
const text = decodeBytes(payload);
|
|
309
|
+
response = await app.handle(text, payload, connection);
|
|
310
|
+
} catch (messageError) {
|
|
311
|
+
const error = toMessageError(messageError);
|
|
312
|
+
await reportError(error, connection, lifecycle, getMessageInfo(error));
|
|
311
313
|
continue;
|
|
312
314
|
}
|
|
313
|
-
if (response) await writer.write(
|
|
315
|
+
if (response) await writer.write(frame(encodeBytes(response.raw)));
|
|
314
316
|
}
|
|
315
|
-
} catch {
|
|
317
|
+
} catch (streamError) {
|
|
318
|
+
if (streamError instanceof MllpCodecError) await reportError(new MllpServerError(MllpServerErrorCode.PROTOCOL_VIOLATION, "The MLLP byte stream was violated and the connection is closing (see the error's cause).", { cause: streamError }), connection, lifecycle);
|
|
319
|
+
}
|
|
316
320
|
} finally {
|
|
317
321
|
try {
|
|
318
322
|
reader.releaseLock();
|
|
@@ -330,6 +334,15 @@ function handleConnection(app, socket, lifecycle) {
|
|
|
330
334
|
};
|
|
331
335
|
processMessages();
|
|
332
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* A decode failure is the server's own error — translated into an
|
|
339
|
+
* MllpServerError so the codec's CharsetError never reaches onError (it is
|
|
340
|
+
* kept on `cause`). A handler or middleware throw is the consumer's own error
|
|
341
|
+
* and passes through unchanged.
|
|
342
|
+
*/
|
|
343
|
+
function toMessageError(error) {
|
|
344
|
+
return error instanceof CharsetError ? new MllpServerError(MllpServerErrorCode.INCOMPATIBLE_CHARSET, "The inbound message is not valid UTF-8; only UTF-8 is supported.", { cause: error }) : error;
|
|
345
|
+
}
|
|
333
346
|
//#endregion
|
|
334
347
|
export { serve };
|
|
335
348
|
|
package/dist/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.js","names":["createTlsServer"],"sources":["../src/node/adapter.ts","../src/node/serve.ts"],"sourcesContent":["/**\n * Node.js TCP/TLS adapter for the MLLP server.\n *\n * Implements the {@link TcpAdapter} interface using Node.js built-in\n * `net` and `tls` modules. Incoming `net.Socket` instances are wrapped\n * into Web Streams-based {@link AdapterSocket} objects so that the rest\n * of the MLLP stack can work with a platform-agnostic streaming API.\n *\n * This module is internal to the `@glion/mllp` package and\n * is consumed by {@link serve} in `./serve.ts`.\n *\n * @module\n */\n\nimport { once } from \"node:events\";\nimport type { Server, Socket } from \"node:net\";\nimport { createServer } from \"node:net\";\nimport { Readable } from \"node:stream\";\nimport { createServer as createTlsServer } from \"node:tls\";\nimport { promisify } from \"node:util\";\n\nimport type {\n AdapterSocket,\n ConnectionHandler,\n ListenOptions,\n TcpAdapter,\n TcpHandle,\n} from \"../server/adapter\";\n\n/**\n * Options specific to the Node.js adapter that control per-socket behaviour.\n *\n * @internal\n */\nexport interface NodeAdapterOptions {\n /**\n * Socket inactivity timeout in milliseconds.\n *\n * When set to a value greater than `0`, sockets that remain idle for\n * longer than this duration are automatically destroyed. Set to `0`\n * (the default) to disable idle timeouts entirely.\n *\n * @default 0\n */\n socketTimeout?: number;\n\n /**\n * Whether to enable TCP keep-alive probes on accepted sockets.\n *\n * When `true` the operating system will periodically send keep-alive\n * packets to detect dead peers and prevent intermediate network\n * equipment from dropping idle connections.\n *\n * @default true\n */\n keepAlive?: boolean;\n\n /**\n * The initial delay in milliseconds before the first TCP keep-alive\n * probe is sent on an idle socket. Only meaningful when\n * {@link keepAlive} is `true`.\n *\n * @default 60000\n */\n keepAliveInitialDelay?: number;\n}\n\n/**\n * Write a chunk to a Node.js socket, waiting for backpressure to clear.\n *\n * `socket.write()` returns `false` when the internal buffer is full,\n * indicating that the caller should wait for the `\"drain\"` event before\n * writing again. This helper wraps that pattern in a promise so it can\n * be used with `await`.\n *\n * @param socket - The Node.js TCP socket to write to.\n * @param chunk - The bytes to write.\n * @returns A promise that resolves once the chunk has been flushed or the\n * socket's internal buffer has drained.\n */\nasync function writeToSocket(socket: Socket, chunk: Uint8Array): Promise<void> {\n const ok = socket.write(chunk);\n if (!ok) {\n await once(socket, \"drain\");\n }\n}\n\n/**\n * Wrap a Node.js `net.Socket` into a platform-agnostic {@link AdapterSocket}.\n *\n * The readable side is created via `Readable.toWeb()` which converts the\n * Node.js stream into a WHATWG `ReadableStream<Uint8Array>`. The writable\n * side is a custom `WritableStream` that delegates writes to\n * {@link writeToSocket} and handles `close` / `abort` by ending or\n * destroying the underlying socket.\n *\n * @param socket - The raw Node.js socket to wrap.\n * @param secure - Whether this socket was accepted over TLS.\n * @returns An {@link AdapterSocket} exposing Web Streams and connection\n * metadata.\n */\nfunction wrapNodeSocket(socket: Socket, secure: boolean): AdapterSocket {\n const readable = Readable.toWeb(socket) as ReadableStream<Uint8Array>;\n\n const writable = new WritableStream<Uint8Array>({\n abort() {\n socket.destroy();\n },\n close() {\n socket.end();\n },\n async write(chunk) {\n await writeToSocket(socket, chunk);\n },\n });\n\n return {\n close() {\n socket.destroy();\n },\n get localPort() {\n return socket.localPort ?? 0;\n },\n readable,\n get remoteAddress() {\n return socket.remoteAddress ?? \"unknown\";\n },\n get remotePort() {\n return socket.remotePort ?? 0;\n },\n secure,\n writable,\n };\n}\n\n/**\n * Create a Node.js {@link TcpAdapter} that can listen for TCP or TLS\n * connections using the built-in `net` and `tls` modules.\n *\n * When the returned adapter's `listen()` method is called with a `tls`\n * option, a TLS server (`tls.createServer`) is used; otherwise a plain\n * TCP server (`net.createServer`) is created.\n *\n * Each accepted socket is configured via {@link configureSocket} (timeouts,\n * keep-alive, Nagle disabled) and then wrapped into an {@link AdapterSocket}\n * before being handed to the provided {@link ConnectionHandler}.\n *\n * @example\n * ```typescript\n * const adapter = nodeAdapter({ socketTimeout: 30_000 });\n * const handle = adapter.listen({ port: 2575 }, (socket) => {\n * // handle connection\n * });\n * ```;\n *\n * @param options - Optional per-socket configuration (timeouts, keep-alive).\n * @returns A {@link TcpAdapter} ready to bind to a port.\n * @internal\n */\nexport function nodeAdapter(options?: NodeAdapterOptions): TcpAdapter {\n const {\n socketTimeout = 0,\n keepAlive = true,\n keepAliveInitialDelay = 60_000,\n } = options ?? {};\n\n /**\n * Apply common socket options to an accepted connection.\n *\n * Configures the following on the raw `net.Socket`:\n *\n * - **Idle timeout** -- If `socketTimeout > 0`, sets `setTimeout()` and\n * destroys the socket on the `\"timeout\"` event.\n * - **TCP keep-alive** -- Enables keep-alive probes with the configured initial\n * delay when `keepAlive` is `true`.\n * - **Nagle disabled** -- Calls `setNoDelay(true)` so that small MLLP frames\n * are sent immediately without buffering.\n *\n * @param socket - The raw Node.js socket to configure.\n */\n function configureSocket(socket: Socket): void {\n if (socketTimeout > 0) {\n socket.setTimeout(socketTimeout);\n socket.on(\"timeout\", () => socket.destroy());\n }\n if (keepAlive) {\n socket.setKeepAlive(true, keepAliveInitialDelay);\n }\n socket.setNoDelay(true);\n }\n\n return {\n listen(listenOpts: ListenOptions, handler: ConnectionHandler): TcpHandle {\n const onConnection = (socket: Socket) => {\n configureSocket(socket);\n const secure = !!listenOpts.tls;\n handler(wrapNodeSocket(socket, secure));\n };\n\n const server: Server = listenOpts.tls\n ? createTlsServer(\n {\n ca: listenOpts.tls.ca,\n cert: listenOpts.tls.cert,\n key: listenOpts.tls.key,\n passphrase: listenOpts.tls.passphrase,\n },\n onConnection\n )\n : createServer(onConnection);\n\n let resolveListening!: (value: true) => void;\n let rejectListening!: (reason: unknown) => void;\n // oxlint-disable-next-line promise/avoid-new -- Node 18 compat (no Promise.withResolvers)\n const listeningInternal = new Promise<true>((resolve, reject) => {\n resolveListening = resolve;\n rejectListening = reject;\n });\n // Public type is Promise<void>; the sentinel return value is an\n // implementation detail never observed by consumers.\n const listening = listeningInternal as Promise<unknown> as Promise<void>;\n\n server.once(\"listening\", () => {\n resolveListening(true);\n });\n // Persistent error listener: server errors that fire before\n // `listening` reject the startup promise; later errors (TLS,\n // shutdown) propagate via the already-returned handle's lifecycle\n // rather than crashing the process via an unhandled emitter error.\n let listenFailed = false;\n server.on(\"error\", (err) => {\n if (!listenFailed) {\n listenFailed = true;\n rejectListening(err);\n // Close the server handle so the OS descriptor is released\n // even if the caller never awaits `listening` again.\n server.close();\n }\n });\n\n server.listen(listenOpts.port, listenOpts.hostname, listenOpts.backlog);\n\n const closeServer = promisify(server.close.bind(server));\n\n return {\n async close() {\n await closeServer();\n },\n listening,\n get port() {\n const addr = server.address();\n return addr && typeof addr === \"object\" ? addr.port : listenOpts.port;\n },\n };\n },\n };\n}\n","/**\n * Node.js MLLP server entry point.\n *\n * Binds an {@link Mllp} application to a TCP (or TLS) socket using the\n * Node.js `net` / `tls` modules. Incoming bytes are decoded from the MLLP\n * framing protocol, dispatched through the application's middleware stack,\n * and the resulting acknowledgements are encoded and written back to the\n * client.\n *\n * @module\n */\n\nimport { createDecoderStream, encode } from \"@glion/mllp-transport\";\n\nimport type { AdapterSocket } from \"../server/adapter\";\nimport type { MessageInfo, Mllp } from \"../server/mllp\";\nimport { getMessageInfo } from \"../server/mllp\";\nimport type { ConnectionInfo } from \"../server/types\";\nimport { nodeAdapter } from \"./adapter\";\n\n/** Monotonically-increasing connection ID counter. */\n// oxlint-disable-next-line prefer-const\nlet nextConnectionId = 1;\n\n/**\n * Lifecycle callback invoked for connection events.\n */\nexport type ConnectionCallback = (\n connection: ConnectionInfo\n) => void | Promise<void>;\n\n/**\n * Error callback invoked when a message handler error is unhandled.\n * Receives the error and the connection it occurred on.\n *\n * For handler errors, `messageInfo` contains the routing fields\n * (messageType, triggerEvent, controlId, version) of the message that\n * caused the error — useful for audit logging in healthcare contexts.\n * For lifecycle callback errors (onConnect/onDisconnect), `messageInfo`\n * is `undefined`.\n *\n * This fires only for application-level errors (handler/middleware throws\n * with no app-level error handler, or app-level error handler itself throws).\n * Transport-level errors (connection reset, decode failures) do not trigger\n * this callback.\n */\nexport type ErrorCallback = (\n error: Error,\n connection: ConnectionInfo,\n messageInfo: MessageInfo | undefined\n) => void | Promise<void>;\n\n/**\n * Options for starting an MLLP server with {@link serve}.\n */\nexport interface ServeOptions {\n /**\n * The hostname or IP address to bind the server to.\n * When omitted the OS will bind to all available interfaces (`0.0.0.0`).\n */\n hostname?: string;\n\n /**\n * Whether to enable TCP keep-alive on accepted sockets.\n * Defaults to the adapter default (`true`).\n */\n keepAlive?: boolean;\n\n /**\n * The initial delay in milliseconds before the first TCP keep-alive probe\n * is sent on an idle socket. Only meaningful when `keepAlive` is `true`.\n * Defaults to the adapter default (`60 000`).\n */\n keepAliveInitialDelay?: number;\n\n /**\n * Called when a new TCP connection is accepted. Receives the connection\n * metadata including the unique connection ID and mutable state map.\n *\n * If this callback throws, the connection is torn down and\n * `onDisconnect` is still called.\n */\n onConnect?: ConnectionCallback;\n\n /**\n * Called when a TCP connection is closed (including force-close).\n * Always fires, even if `onConnect` threw.\n *\n * If this callback throws, the error is routed to `onError`\n * (or `console.error` as last resort).\n */\n onDisconnect?: ConnectionCallback;\n\n /**\n * Called when a message handler error is unhandled — either no app-level\n * error handler is registered, or the app-level error handler itself\n * threw. Also called when lifecycle callbacks (`onConnect`,\n * `onDisconnect`) throw.\n *\n * Only logs `error.message` and connection ID to avoid leaking PHI\n * in the `console.error` fallback. Production deployments should\n * always provide this callback.\n */\n onError?: ErrorCallback;\n\n /**\n * The TCP port number to listen on.\n */\n port: number;\n\n /**\n * Socket inactivity timeout in milliseconds. A socket that has been idle\n * for longer than this value will be destroyed. Set to `0` (the default)\n * to disable the timeout.\n */\n socketTimeout?: number;\n\n /**\n * TLS configuration. When provided the server will create a TLS socket\n * instead of a plain TCP socket.\n */\n tls?: {\n /** Optional CA certificate(s) for client certificate verification. */\n ca?: string | Buffer;\n /** The server certificate. */\n cert: string | Buffer;\n /** The private key for the server certificate. */\n key: string | Buffer;\n /** Optional passphrase for the private key. */\n passphrase?: string;\n };\n}\n\n/**\n * A running MLLP server handle returned by {@link serve}.\n */\nexport interface Server {\n /** The port the server is currently listening on. */\n readonly port: number;\n\n /**\n * Resolves when the underlying TCP server has bound and started listening.\n * Await this before attempting to connect to ensure the port is ready.\n */\n readonly listening: Promise<void>;\n\n /**\n * Gracefully close the server. No new connections will be accepted and the\n * returned promise resolves once all underlying resources are released.\n */\n close(): Promise<void>;\n}\n\n/**\n * Start an MLLP server bound to a TCP (or TLS) port.\n *\n * The server accepts incoming connections, decodes MLLP-framed HL7v2\n * messages, passes them through the provided {@link Mllp} application, and\n * writes back any response (typically an ACK/NAK) using MLLP framing.\n *\n * @example\n * ```typescript\n * import { Mllp } from \"@glion/mllp\";\n * import { parseHL7v2 } from \"@glion/hl7v2\";\n * import { serve } from \"@glion/mllp/node\";\n *\n * const app = new Mllp()\n * .parser(parseHL7v2)\n * .on(\"ADT^A01\", async (ctx) => ({ raw: \"...\" }));\n *\n * const server = serve(app, { port: 2575 });\n * console.log(`Listening on port ${server.port}`);\n * ```\n *\n * @param app - The {@link Mllp} application that will handle each message.\n * @param options - Server configuration (port, TLS, timeouts, etc.).\n * @returns A {@link Server} handle that exposes the bound port and a `close()`\n * method for graceful shutdown.\n */\nexport function serve(app: Mllp, options: ServeOptions): Server {\n const adapter = nodeAdapter({\n keepAlive: options.keepAlive,\n keepAliveInitialDelay: options.keepAliveInitialDelay,\n socketTimeout: options.socketTimeout,\n });\n\n const lifecycle: LifecycleOptions = {\n onConnect: options.onConnect,\n onDisconnect: options.onDisconnect,\n onError: options.onError,\n };\n\n const handle = adapter.listen(\n {\n hostname: options.hostname,\n port: options.port,\n tls: options.tls,\n },\n (socket) => handleConnection(app, socket, lifecycle)\n );\n\n return {\n async close() {\n await handle.close();\n },\n listening: handle.listening,\n get port() {\n return handle.port;\n },\n };\n}\n\n/** Lifecycle callback options extracted from ServeOptions. */\ninterface LifecycleOptions {\n onConnect?: ConnectionCallback;\n onDisconnect?: ConnectionCallback;\n onError?: ErrorCallback;\n}\n\n/**\n * Route an error to the `onError` callback, falling back to a safe\n * `console.error` that logs only the error message and connection ID\n * to avoid leaking PHI from HL7v2 message content.\n */\nasync function reportError(\n error: unknown,\n connection: ConnectionInfo,\n lifecycle: LifecycleOptions,\n messageInfo?: MessageInfo\n): Promise<void> {\n const err = error instanceof Error ? error : new Error(String(error));\n try {\n if (lifecycle.onError) {\n await lifecycle.onError(err, connection, messageInfo);\n } else {\n // Safe fallback: only message + connection ID, no PHI\n console.error(\n `[mllp] Unhandled error on connection ${connection.id}: ${err.message}`\n );\n }\n } catch {\n // onError itself threw — last resort\n console.error(\n `[mllp] Unhandled error on connection ${connection.id}: ${err.message}`\n );\n }\n}\n\n/**\n * Handle a single MLLP connection.\n *\n * Sets up a decode-handle-encode loop for the lifetime of the socket:\n *\n * 1. **Decode** — Raw bytes from the socket's readable stream are piped through an\n * MLLP decoder (`TransformStream`) that strips the MLLP start/end block\n * characters and emits complete HL7v2 messages.\n * 2. **Handle** — Each decoded message is passed to the {@link Mllp} application\n * which runs its middleware stack and returns an optional response (e.g. an\n * ACK or NAK).\n * 3. **Encode** — If the application produced a response, the raw response bytes\n * are MLLP-encoded (wrapped in start/end block characters) and written back\n * to the socket.\n *\n * The loop runs until the remote end closes the connection or an\n * unrecoverable error occurs. Stream locks are released in a `finally`\n * block to prevent resource leaks.\n *\n * Lifecycle callbacks fire at connection boundaries:\n *\n * - `onConnect` after the connection is established\n * - `onDisconnect` when the connection closes (always fires)\n * - `onError` when a handler error is unhandled (not for transport errors)\n *\n * @param app - The MLLP application to dispatch messages to.\n * @param socket - The adapter socket wrapping the underlying TCP connection.\n * @param lifecycle - Optional lifecycle callbacks from ServeOptions.\n */\nfunction handleConnection(\n app: Mllp,\n socket: AdapterSocket,\n lifecycle: LifecycleOptions\n): void {\n const decoder = createDecoderStream();\n const reader = socket.readable.pipeThrough(decoder).getReader();\n const writer = socket.writable.getWriter();\n\n const connection: ConnectionInfo = {\n id: nextConnectionId++,\n localPort: socket.localPort,\n remoteAddress: socket.remoteAddress,\n remotePort: socket.remotePort,\n secure: socket.secure,\n state: new Map(),\n };\n\n const processMessages = async () => {\n try {\n // ── onConnect ──────────────────────────────────────────────────\n try {\n await lifecycle.onConnect?.(connection);\n } catch (connectError) {\n await reportError(connectError, connection, lifecycle);\n // Tear down — onDisconnect still fires in the outer finally\n return;\n }\n\n // ── Message loop ───────────────────────────────────────────────\n try {\n while (true) {\n const { done, value: message } = await reader.read();\n if (done) {\n break;\n }\n\n // Inner try/catch separates handler errors from stream errors.\n // Only handler errors route to onError; stream errors (connection\n // reset, decode failure) flow to the outer catch for cleanup.\n let response: Awaited<ReturnType<Mllp[\"handle\"]>>;\n try {\n response = await app.handle(message.text, message.data, connection);\n } catch (handlerError) {\n const err =\n handlerError instanceof Error\n ? handlerError\n : new Error(String(handlerError));\n await reportError(err, connection, lifecycle, getMessageInfo(err));\n // Continue processing — connection stays alive\n continue;\n }\n\n // Write is outside the handler error catch — write failures\n // are transport errors and flow to the outer catch.\n if (response) {\n await writer.write(encode(response.raw));\n }\n }\n } catch {\n // Transport-level: connection closed, stream errored, decode failure.\n // Not routed to onError — these are infrastructure, not application errors.\n }\n } finally {\n // ── Cleanup & onDisconnect ─────────────────────────────────────\n // Always runs, even if onConnect threw.\n try {\n reader.releaseLock();\n } catch {\n /* lock may already be released */\n }\n try {\n writer.releaseLock();\n } catch {\n /* lock may already be released */\n }\n\n try {\n await lifecycle.onDisconnect?.(connection);\n } catch (disconnectError) {\n await reportError(disconnectError, connection, lifecycle);\n }\n\n socket.close();\n }\n };\n\n // oxlint-disable-next-line no-void\n void processMessages();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"node.js","names":["createTlsServer"],"sources":["../src/node/adapter.ts","../src/node/serve.ts"],"sourcesContent":["/**\n * Node.js TCP/TLS adapter for the MLLP server.\n *\n * Implements the {@link TcpAdapter} interface using Node.js built-in\n * `net` and `tls` modules. Incoming `net.Socket` instances are wrapped\n * into Web Streams-based {@link AdapterSocket} objects so that the rest\n * of the MLLP stack can work with a platform-agnostic streaming API.\n *\n * This module is internal to the `@glion/mllp` package and\n * is consumed by {@link serve} in `./serve.ts`.\n *\n * @module\n */\n\nimport { once } from \"node:events\";\nimport type { Server, Socket } from \"node:net\";\nimport { createServer } from \"node:net\";\nimport { Readable } from \"node:stream\";\nimport { createServer as createTlsServer } from \"node:tls\";\nimport { promisify } from \"node:util\";\n\nimport type {\n AdapterSocket,\n ConnectionHandler,\n ListenOptions,\n TcpAdapter,\n TcpHandle,\n} from \"../server/adapter\";\n\n/**\n * Options specific to the Node.js adapter that control per-socket behaviour.\n *\n * @internal\n */\nexport interface NodeAdapterOptions {\n /**\n * Socket inactivity timeout in milliseconds.\n *\n * When set to a value greater than `0`, sockets that remain idle for\n * longer than this duration are automatically destroyed. Set to `0`\n * (the default) to disable idle timeouts entirely.\n *\n * @default 0\n */\n socketTimeout?: number;\n\n /**\n * Whether to enable TCP keep-alive probes on accepted sockets.\n *\n * When `true` the operating system will periodically send keep-alive\n * packets to detect dead peers and prevent intermediate network\n * equipment from dropping idle connections.\n *\n * @default true\n */\n keepAlive?: boolean;\n\n /**\n * The initial delay in milliseconds before the first TCP keep-alive\n * probe is sent on an idle socket. Only meaningful when\n * {@link keepAlive} is `true`.\n *\n * @default 60000\n */\n keepAliveInitialDelay?: number;\n}\n\n/**\n * Write a chunk to a Node.js socket, waiting for backpressure to clear.\n *\n * `socket.write()` returns `false` when the internal buffer is full,\n * indicating that the caller should wait for the `\"drain\"` event before\n * writing again. This helper wraps that pattern in a promise so it can\n * be used with `await`.\n *\n * @param socket - The Node.js TCP socket to write to.\n * @param chunk - The bytes to write.\n * @returns A promise that resolves once the chunk has been flushed or the\n * socket's internal buffer has drained.\n */\nasync function writeToSocket(socket: Socket, chunk: Uint8Array): Promise<void> {\n const ok = socket.write(chunk);\n if (!ok) {\n await once(socket, \"drain\");\n }\n}\n\n/**\n * Wrap a Node.js `net.Socket` into a platform-agnostic {@link AdapterSocket}.\n *\n * The readable side is created via `Readable.toWeb()` which converts the\n * Node.js stream into a WHATWG `ReadableStream<Uint8Array>`. The writable\n * side is a custom `WritableStream` that delegates writes to\n * {@link writeToSocket} and handles `close` / `abort` by ending or\n * destroying the underlying socket.\n *\n * @param socket - The raw Node.js socket to wrap.\n * @param secure - Whether this socket was accepted over TLS.\n * @returns An {@link AdapterSocket} exposing Web Streams and connection\n * metadata.\n */\nfunction wrapNodeSocket(socket: Socket, secure: boolean): AdapterSocket {\n const readable = Readable.toWeb(socket) as ReadableStream<Uint8Array>;\n\n const writable = new WritableStream<Uint8Array>({\n abort() {\n socket.destroy();\n },\n close() {\n socket.end();\n },\n async write(chunk) {\n await writeToSocket(socket, chunk);\n },\n });\n\n return {\n close() {\n // Graceful teardown: `resume()` drains any unread inbound bytes so the\n // following `end()` can send a clean FIN. A bare `destroy()` while data\n // sits unread in the RX buffer (e.g. tearing down after `onConnect`\n // rejects, before the peer's first frame is consumed) makes the kernel\n // send an RST, surfacing as ECONNRESET on the peer.\n socket.resume();\n socket.end();\n },\n get localPort() {\n return socket.localPort ?? 0;\n },\n readable,\n get remoteAddress() {\n return socket.remoteAddress ?? \"unknown\";\n },\n get remotePort() {\n return socket.remotePort ?? 0;\n },\n secure,\n writable,\n };\n}\n\n/**\n * Create a Node.js {@link TcpAdapter} that can listen for TCP or TLS\n * connections using the built-in `net` and `tls` modules.\n *\n * When the returned adapter's `listen()` method is called with a `tls`\n * option, a TLS server (`tls.createServer`) is used; otherwise a plain\n * TCP server (`net.createServer`) is created.\n *\n * Each accepted socket is configured via {@link configureSocket} (timeouts,\n * keep-alive, Nagle disabled) and then wrapped into an {@link AdapterSocket}\n * before being handed to the provided {@link ConnectionHandler}.\n *\n * @example\n * ```typescript\n * const adapter = nodeAdapter({ socketTimeout: 30_000 });\n * const handle = adapter.listen({ port: 2575 }, (socket) => {\n * // handle connection\n * });\n * ```;\n *\n * @param options - Optional per-socket configuration (timeouts, keep-alive).\n * @returns A {@link TcpAdapter} ready to bind to a port.\n * @internal\n */\nexport function nodeAdapter(options?: NodeAdapterOptions): TcpAdapter {\n const {\n socketTimeout = 0,\n keepAlive = true,\n keepAliveInitialDelay = 60_000,\n } = options ?? {};\n\n /**\n * Apply common socket options to an accepted connection.\n *\n * Configures the following on the raw `net.Socket`:\n *\n * - **Idle timeout** -- If `socketTimeout > 0`, sets `setTimeout()` and\n * destroys the socket on the `\"timeout\"` event.\n * - **TCP keep-alive** -- Enables keep-alive probes with the configured initial\n * delay when `keepAlive` is `true`.\n * - **Nagle disabled** -- Calls `setNoDelay(true)` so that small MLLP frames\n * are sent immediately without buffering.\n *\n * @param socket - The raw Node.js socket to configure.\n */\n function configureSocket(socket: Socket): void {\n if (socketTimeout > 0) {\n socket.setTimeout(socketTimeout);\n socket.on(\"timeout\", () => socket.destroy());\n }\n if (keepAlive) {\n socket.setKeepAlive(true, keepAliveInitialDelay);\n }\n socket.setNoDelay(true);\n }\n\n return {\n listen(listenOpts: ListenOptions, handler: ConnectionHandler): TcpHandle {\n const onConnection = (socket: Socket) => {\n configureSocket(socket);\n const secure = !!listenOpts.tls;\n handler(wrapNodeSocket(socket, secure));\n };\n\n const server: Server = listenOpts.tls\n ? createTlsServer(\n {\n ca: listenOpts.tls.ca,\n cert: listenOpts.tls.cert,\n key: listenOpts.tls.key,\n passphrase: listenOpts.tls.passphrase,\n },\n onConnection\n )\n : createServer(onConnection);\n\n let resolveListening!: (value: true) => void;\n let rejectListening!: (reason: unknown) => void;\n // oxlint-disable-next-line promise/avoid-new -- Node 18 compat (no Promise.withResolvers)\n const listeningInternal = new Promise<true>((resolve, reject) => {\n resolveListening = resolve;\n rejectListening = reject;\n });\n // Public type is Promise<void>; the sentinel return value is an\n // implementation detail never observed by consumers.\n const listening = listeningInternal as Promise<unknown> as Promise<void>;\n\n server.once(\"listening\", () => {\n resolveListening(true);\n });\n // Persistent error listener: server errors that fire before\n // `listening` reject the startup promise; later errors (TLS,\n // shutdown) propagate via the already-returned handle's lifecycle\n // rather than crashing the process via an unhandled emitter error.\n let listenFailed = false;\n server.on(\"error\", (err) => {\n if (!listenFailed) {\n listenFailed = true;\n rejectListening(err);\n // Close the server handle so the OS descriptor is released\n // even if the caller never awaits `listening` again.\n server.close();\n }\n });\n\n server.listen(listenOpts.port, listenOpts.hostname, listenOpts.backlog);\n\n const closeServer = promisify(server.close.bind(server));\n\n return {\n async close() {\n await closeServer();\n },\n listening,\n get port() {\n const addr = server.address();\n return addr && typeof addr === \"object\" ? addr.port : listenOpts.port;\n },\n };\n },\n };\n}\n","/**\n * Node.js MLLP server entry point.\n *\n * Binds an {@link Mllp} application to a TCP (or TLS) socket using the\n * Node.js `net` / `tls` modules. Incoming bytes are decoded from the MLLP\n * framing protocol, dispatched through the application's middleware stack,\n * and the resulting acknowledgements are encoded and written back to the\n * client.\n *\n * @module\n */\n\nimport { frame, MllpCodecError, unframe } from \"@glion/mllp-codec\";\nimport { CharsetError, decodeBytes, encodeBytes } from \"@glion/util-charset\";\n\nimport { MllpServerError, MllpServerErrorCode } from \"../errors\";\nimport type { AdapterSocket } from \"../server/adapter\";\nimport type { MessageInfo, Mllp } from \"../server/mllp\";\nimport { getMessageInfo } from \"../server/mllp\";\nimport type { ConnectionInfo } from \"../server/types\";\nimport { nodeAdapter } from \"./adapter\";\n\n/** Monotonically-increasing connection ID counter. */\n// oxlint-disable-next-line prefer-const\nlet nextConnectionId = 1;\n\n/**\n * Lifecycle callback invoked for connection events.\n */\nexport type ConnectionCallback = (\n connection: ConnectionInfo\n) => void | Promise<void>;\n\n/**\n * Error callback invoked when a message handler error is unhandled.\n * Receives the error and the connection it occurred on.\n *\n * For handler errors, `messageInfo` contains the routing fields\n * (messageType, triggerEvent, controlId, version) of the message that\n * caused the error — useful for audit logging in healthcare contexts.\n * For lifecycle callback errors (onConnect/onDisconnect), `messageInfo`\n * is `undefined`.\n *\n * This fires for application-level errors (handler/middleware throws with no\n * app-level error handler, or the app-level error handler itself throws) and\n * for a server-level failure decoding an inbound message — surfaced as an\n * {@link MllpServerError} (`code` `INCOMPATIBLE_CHARSET`), never the codec's\n * own error. Stream-level errors (connection reset) do not trigger this\n * callback.\n */\nexport type ErrorCallback = (\n error: Error,\n connection: ConnectionInfo,\n messageInfo: MessageInfo | undefined\n) => void | Promise<void>;\n\n/**\n * Options for starting an MLLP server with {@link serve}.\n */\nexport interface ServeOptions {\n /**\n * The hostname or IP address to bind the server to.\n * When omitted the OS will bind to all available interfaces (`0.0.0.0`).\n */\n hostname?: string;\n\n /**\n * Whether to enable TCP keep-alive on accepted sockets.\n * Defaults to the adapter default (`true`).\n */\n keepAlive?: boolean;\n\n /**\n * The initial delay in milliseconds before the first TCP keep-alive probe\n * is sent on an idle socket. Only meaningful when `keepAlive` is `true`.\n * Defaults to the adapter default (`60 000`).\n */\n keepAliveInitialDelay?: number;\n\n /**\n * Called when a new TCP connection is accepted. Receives the connection\n * metadata including the unique connection ID and mutable state map.\n *\n * If this callback throws, the connection is torn down and\n * `onDisconnect` is still called.\n */\n onConnect?: ConnectionCallback;\n\n /**\n * Called when a TCP connection is closed (including force-close).\n * Always fires, even if `onConnect` threw.\n *\n * If this callback throws, the error is routed to `onError`\n * (or `console.error` as last resort).\n */\n onDisconnect?: ConnectionCallback;\n\n /**\n * Called when a message handler error is unhandled — either no app-level\n * error handler is registered, or the app-level error handler itself\n * threw. Also called when lifecycle callbacks (`onConnect`,\n * `onDisconnect`) throw.\n *\n * Only logs `error.message` and connection ID to avoid leaking PHI\n * in the `console.error` fallback. Production deployments should\n * always provide this callback.\n */\n onError?: ErrorCallback;\n\n /**\n * The TCP port number to listen on.\n */\n port: number;\n\n /**\n * Socket inactivity timeout in milliseconds. A socket that has been idle\n * for longer than this value will be destroyed. Set to `0` (the default)\n * to disable the timeout.\n */\n socketTimeout?: number;\n\n /**\n * TLS configuration. When provided the server will create a TLS socket\n * instead of a plain TCP socket.\n */\n tls?: {\n /** Optional CA certificate(s) for client certificate verification. */\n ca?: string | Buffer;\n /** The server certificate. */\n cert: string | Buffer;\n /** The private key for the server certificate. */\n key: string | Buffer;\n /** Optional passphrase for the private key. */\n passphrase?: string;\n };\n}\n\n/**\n * A running MLLP server handle returned by {@link serve}.\n */\nexport interface Server {\n /** The port the server is currently listening on. */\n readonly port: number;\n\n /**\n * Resolves when the underlying TCP server has bound and started listening.\n * Await this before attempting to connect to ensure the port is ready.\n */\n readonly listening: Promise<void>;\n\n /**\n * Gracefully close the server. No new connections will be accepted and the\n * returned promise resolves once all underlying resources are released.\n */\n close(): Promise<void>;\n}\n\n/**\n * Start an MLLP server bound to a TCP (or TLS) port.\n *\n * The server accepts incoming connections, decodes MLLP-framed HL7v2\n * messages, passes them through the provided {@link Mllp} application, and\n * writes back any response (typically an ACK/NAK) using MLLP framing.\n *\n * @example\n * ```typescript\n * import { Mllp } from \"@glion/mllp\";\n * import { parseHL7v2 } from \"@glion/hl7v2\";\n * import { serve } from \"@glion/mllp/node\";\n *\n * const app = new Mllp()\n * .parser(parseHL7v2)\n * .on(\"ADT^A01\", async (ctx) => ({ raw: \"...\" }));\n *\n * const server = serve(app, { port: 2575 });\n * console.log(`Listening on port ${server.port}`);\n * ```\n *\n * @param app - The {@link Mllp} application that will handle each message.\n * @param options - Server configuration (port, TLS, timeouts, etc.).\n * @returns A {@link Server} handle that exposes the bound port and a `close()`\n * method for graceful shutdown.\n */\nexport function serve(app: Mllp, options: ServeOptions): Server {\n const adapter = nodeAdapter({\n keepAlive: options.keepAlive,\n keepAliveInitialDelay: options.keepAliveInitialDelay,\n socketTimeout: options.socketTimeout,\n });\n\n const lifecycle: LifecycleOptions = {\n onConnect: options.onConnect,\n onDisconnect: options.onDisconnect,\n onError: options.onError,\n };\n\n const handle = adapter.listen(\n {\n hostname: options.hostname,\n port: options.port,\n tls: options.tls,\n },\n (socket) => handleConnection(app, socket, lifecycle)\n );\n\n return {\n async close() {\n await handle.close();\n },\n listening: handle.listening,\n get port() {\n return handle.port;\n },\n };\n}\n\n/** Lifecycle callback options extracted from ServeOptions. */\ninterface LifecycleOptions {\n onConnect?: ConnectionCallback;\n onDisconnect?: ConnectionCallback;\n onError?: ErrorCallback;\n}\n\n/**\n * Route an error to the `onError` callback, falling back to a safe\n * `console.error` that logs only the error message and connection ID\n * to avoid leaking PHI from HL7v2 message content.\n */\nasync function reportError(\n error: unknown,\n connection: ConnectionInfo,\n lifecycle: LifecycleOptions,\n messageInfo?: MessageInfo\n): Promise<void> {\n const err = error instanceof Error ? error : new Error(String(error));\n try {\n if (lifecycle.onError) {\n await lifecycle.onError(err, connection, messageInfo);\n } else {\n // Safe fallback: only message + connection ID, no PHI\n console.error(\n `[mllp] Unhandled error on connection ${connection.id}: ${err.message}`\n );\n }\n } catch {\n // onError itself threw — last resort\n console.error(\n `[mllp] Unhandled error on connection ${connection.id}: ${err.message}`\n );\n }\n}\n\n/**\n * Handle a single MLLP connection.\n *\n * Sets up a decode-handle-encode loop for the lifetime of the socket:\n *\n * 1. **Decode** — Raw bytes from the socket's readable stream are piped through an\n * MLLP decoder (`TransformStream`) that strips the MLLP start/end block\n * characters and emits complete HL7v2 messages.\n * 2. **Handle** — Each decoded message is passed to the {@link Mllp} application\n * which runs its middleware stack and returns an optional response (e.g. an\n * ACK or NAK).\n * 3. **Encode** — If the application produced a response, the raw response bytes\n * are MLLP-encoded (wrapped in start/end block characters) and written back\n * to the socket.\n *\n * The loop runs until the remote end closes the connection or an\n * unrecoverable error occurs. Stream locks are released in a `finally`\n * block to prevent resource leaks.\n *\n * Lifecycle callbacks fire at connection boundaries:\n *\n * - `onConnect` after the connection is established\n * - `onDisconnect` when the connection closes (always fires)\n * - `onError` when a handler error is unhandled (not for transport errors)\n *\n * @param app - The MLLP application to dispatch messages to.\n * @param socket - The adapter socket wrapping the underlying TCP connection.\n * @param lifecycle - Optional lifecycle callbacks from ServeOptions.\n */\nfunction handleConnection(\n app: Mllp,\n socket: AdapterSocket,\n lifecycle: LifecycleOptions\n): void {\n const reader = socket.readable.pipeThrough(unframe()).getReader();\n const writer = socket.writable.getWriter();\n\n const connection: ConnectionInfo = {\n id: nextConnectionId++,\n localPort: socket.localPort,\n remoteAddress: socket.remoteAddress,\n remotePort: socket.remotePort,\n secure: socket.secure,\n state: new Map(),\n };\n\n const processMessages = async () => {\n try {\n // ── onConnect ──────────────────────────────────────────────────\n try {\n await lifecycle.onConnect?.(connection);\n } catch (connectError) {\n await reportError(connectError, connection, lifecycle);\n // Tear down — onDisconnect still fires in the outer finally\n return;\n }\n\n // ── Message loop ───────────────────────────────────────────────\n try {\n while (true) {\n const { done, value: payload } = await reader.read();\n if (done) {\n break;\n }\n\n // Inner try/catch separates per-message errors from stream errors.\n // Decode failures and handler errors both route to onError and the\n // connection survives; stream errors (connection reset) flow to the\n // outer catch for cleanup.\n let response: Awaited<ReturnType<Mllp[\"handle\"]>>;\n try {\n // unframe() emits the de-framed payload bytes; decode them to\n // text (UTF-8) for the handler, which also receives the raw bytes.\n // A non-UTF-8 feed throws here rather than being silently corrupted.\n const text = decodeBytes(payload);\n response = await app.handle(text, payload, connection);\n } catch (messageError) {\n // The connection survives a per-message error either way.\n const error = toMessageError(messageError);\n await reportError(\n error,\n connection,\n lifecycle,\n getMessageInfo(error)\n );\n continue;\n }\n\n // Write is outside the handler error catch — write failures\n // are transport errors and flow to the outer catch.\n if (response) {\n await writer.write(frame(encodeBytes(response.raw)));\n }\n }\n } catch (streamError) {\n // A codec error is a protocol violation, not plumbing — the remote\n // system broke MLLP framing, or a handler's response carried a\n // reserved VT/FS byte and could not be framed. The server is never\n // silent about it: translate (never leak the codec's own type) and\n // route to onError; the connection still closes below — framing\n // violations are terminal. Everything else here is transport-level\n // (connection reset, stream teardown) and stays unrouted: that is\n // infrastructure, not an application error.\n if (streamError instanceof MllpCodecError) {\n await reportError(\n new MllpServerError(\n MllpServerErrorCode.PROTOCOL_VIOLATION,\n \"The MLLP byte stream was violated and the connection is closing (see the error's cause).\",\n { cause: streamError }\n ),\n connection,\n lifecycle\n );\n }\n }\n } finally {\n // ── Cleanup & onDisconnect ─────────────────────────────────────\n // Always runs, even if onConnect threw.\n try {\n reader.releaseLock();\n } catch {\n /* lock may already be released */\n }\n try {\n writer.releaseLock();\n } catch {\n /* lock may already be released */\n }\n\n try {\n await lifecycle.onDisconnect?.(connection);\n } catch (disconnectError) {\n await reportError(disconnectError, connection, lifecycle);\n }\n\n socket.close();\n }\n };\n\n // oxlint-disable-next-line no-void\n void processMessages();\n}\n\n/**\n * A decode failure is the server's own error — translated into an\n * MllpServerError so the codec's CharsetError never reaches onError (it is\n * kept on `cause`). A handler or middleware throw is the consumer's own error\n * and passes through unchanged.\n */\nfunction toMessageError(error: unknown): unknown {\n return error instanceof CharsetError\n ? new MllpServerError(\n MllpServerErrorCode.INCOMPATIBLE_CHARSET,\n \"The inbound message is not valid UTF-8; only UTF-8 is supported.\",\n { cause: error }\n )\n : error;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/server/context.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EACP,cAAc,EAEf,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,KAAK,EAAE,UAAU,CAAC;IAClB,0BAA0B;IAC1B,UAAU,EAAE,cAAc,CAAC;IAC3B,8BAA8B;IAC9B,SAAS,EAAE,cAAc,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/server/context.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EACP,cAAc,EAEf,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,KAAK,EAAE,UAAU,CAAC;IAClB,0BAA0B;IAC1B,UAAU,EAAE,cAAc,CAAC;IAC3B,8BAA8B;IAC9B,SAAS,EAAE,cAAc,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAyFpE"}
|
package/dist/server/mllp.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface MessageInfo {
|
|
|
16
16
|
* by `Mllp.handle()`. Returns `undefined` for errors that did not
|
|
17
17
|
* originate from message processing (e.g., lifecycle callback errors).
|
|
18
18
|
*/
|
|
19
|
-
export declare function getMessageInfo(error:
|
|
19
|
+
export declare function getMessageInfo(error: unknown): MessageInfo | undefined;
|
|
20
20
|
/**
|
|
21
21
|
* MLLP application for HL7v2 messaging.
|
|
22
22
|
*
|
|
@@ -90,12 +90,14 @@ export declare class Mllp {
|
|
|
90
90
|
* This is the integration point — analogous to Hono's `fetch()`.
|
|
91
91
|
*
|
|
92
92
|
* The pipeline is lazy (see ADR-0013):
|
|
93
|
+
*
|
|
93
94
|
* 1. Parse (sync, fast) — always runs, extracts routing fields
|
|
94
95
|
* 2. Route match — uses pre-transform routing fields
|
|
95
96
|
* 3. If no match → return undefined (no transform/compile cost)
|
|
96
97
|
* 4. Transform/compile — only when handlers access ctx.tree()/ctx.result()
|
|
97
98
|
*
|
|
98
|
-
* Throws `
|
|
99
|
+
* Throws `MllpServerError` (`NO_PARSER`) if no processor has been registered
|
|
100
|
+
* via `app.parser()`.
|
|
99
101
|
*/
|
|
100
102
|
handle(raw: string, bytes: Uint8Array, connection: ConnectionInfo): Promise<Response | undefined | void>;
|
|
101
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mllp.d.ts","sourceRoot":"","sources":["../../src/server/mllp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mllp.d.ts","sourceRoot":"","sources":["../../src/server/mllp.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EAEd,YAAY,EACZ,OAAO,EACP,cAAc,EACd,UAAU,EACV,QAAQ,EACR,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAQD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAGtE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,IAAI;;IAKf;;;;;;;;;OASG;IACH,MAAM,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI;IAKvC;;;;;;;;OAQG;IACH,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IAClE,GAAG,CAAC,eAAe,EAAE,MAAM,GAAG,WAAW,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;IAyBxE;;;;;;;;;;;;;OAaG;IACH,EAAE,CAAC,eAAe,EAAE,MAAM,GAAG,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAKjE;;;;;;;;OAQG;IACH,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAKpC;;;;;;;;;;;;;OAaG;IACG,MAAM,CACV,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,cAAc,GAEzB,OAAO,CAAC,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC;CAmFxC"}
|
package/dist/server/types.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Root } from "@glion/ast";
|
|
2
|
-
import type { Processor } from "unified";
|
|
3
2
|
import type { VFile } from "vfile";
|
|
4
3
|
/**
|
|
5
4
|
* Connection metadata for an MLLP socket.
|
|
@@ -90,14 +89,7 @@ export interface Context {
|
|
|
90
89
|
/** Read-only snapshot of all variables */
|
|
91
90
|
readonly var: Readonly<Record<string, unknown>>;
|
|
92
91
|
}
|
|
93
|
-
|
|
94
|
-
* A unified `Processor` that parses HL7v2 messages into `Root` trees.
|
|
95
|
-
*
|
|
96
|
-
* Only `ParseTree` is constrained — the remaining type params are left
|
|
97
|
-
* loose so that any processor built with `unified().use(hl7v2Parser)`
|
|
98
|
-
* is assignable regardless of which transformer/compiler plugins are added.
|
|
99
|
-
*/
|
|
100
|
-
export type Hl7v2Processor = Processor<Root, Root, Root>;
|
|
92
|
+
export type { Hl7v2Processor } from "@glion/parser";
|
|
101
93
|
/**
|
|
102
94
|
* Middleware function signature (Hono/Koa onion model).
|
|
103
95
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glion/mllp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Transport-agnostic MLLP (Minimal Lower Layer Protocol) engine for HL7v2 messaging",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"health",
|
|
@@ -44,31 +44,31 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"unified": "11.0.5",
|
|
46
46
|
"vfile": "6.0.3",
|
|
47
|
-
"@glion/ast": "0.
|
|
48
|
-
"@glion/mllp-
|
|
49
|
-
"@glion/
|
|
50
|
-
"@glion/
|
|
47
|
+
"@glion/ast": "0.20.0",
|
|
48
|
+
"@glion/mllp-codec": "0.20.0",
|
|
49
|
+
"@glion/parser": "0.20.0",
|
|
50
|
+
"@glion/util-query": "0.20.0",
|
|
51
|
+
"@glion/util-charset": "0.20.0"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@types/node": "25.6.0",
|
|
54
|
-
"@vitest/coverage-v8": "4.1.
|
|
55
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
55
56
|
"publint": "0.3.18",
|
|
56
57
|
"tsdown": "0.21.10",
|
|
57
58
|
"typescript": "6.0.3",
|
|
58
|
-
"vitest": "4.1.
|
|
59
|
-
"@glion/check-readme": "0.
|
|
60
|
-
"@glion/hl7v2": "0.
|
|
61
|
-
"@glion/testing": "0.
|
|
62
|
-
"@glion/tsconfig": "0.
|
|
59
|
+
"vitest": "4.1.11",
|
|
60
|
+
"@glion/check-readme": "0.18.0",
|
|
61
|
+
"@glion/hl7v2": "0.20.0",
|
|
62
|
+
"@glion/testing": "0.18.0",
|
|
63
|
+
"@glion/tsconfig": "0.18.0"
|
|
63
64
|
},
|
|
64
65
|
"engines": {
|
|
65
|
-
"node": ">=
|
|
66
|
+
"node": ">=22"
|
|
66
67
|
},
|
|
67
68
|
"scripts": {
|
|
68
69
|
"build": "tsdown && tsc --emitDeclarationOnly",
|
|
69
|
-
"check-types": "tsc --noEmit",
|
|
70
|
+
"check-types": "tsc --noEmit && tsc --noEmit -p tests/tsconfig.json",
|
|
70
71
|
"check:readme": "glion-check-readme",
|
|
71
|
-
"bench": "vitest bench --run",
|
|
72
72
|
"test": "vitest run",
|
|
73
73
|
"test:coverage": "vitest run --coverage",
|
|
74
74
|
"test:watch": "vitest",
|