@glion/mllp-client 0.27.1 → 0.28.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 CHANGED
@@ -489,7 +489,7 @@ Thrown before anything is opened or sent. A configuration bug, not a runtime con
489
489
 
490
490
  `MllpInvalidMessageError` · delivery `not-sent` · field: `cause`
491
491
 
492
- The message cannot be sent as it stands: no MSH-10 control ID, or it could not be serialized, or its content contains a byte MLLP reserves as a frame marker.
492
+ The message cannot be sent as it stands: no MSH-10 control ID, or it could not be serialized, or a Segment ID or MSH-1/MSH-2 holds a byte MLLP reserves as a frame marker. A reserved byte in a value is written as `\Xdd\` and sent.
493
493
 
494
494
  **Nothing reached the wire and the connection is still in step**, so the next message can go out on it. Fix or quarantine the message; do not reconnect.
495
495
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#listeners","#socket","#connect","#policy","#sendTimeoutMs","#queue","#state","#dial","#send","#destroy","#sendFailed"],"sources":["../src/constants.ts","../src/errors/mllp-client-closed-error.ts","../src/errors/mllp-connection-error.ts","../src/errors/mllp-connection-failed-error.ts","../src/errors/mllp-connection-timeout-error.ts","../src/errors/mllp-connection-lost-error.ts","../src/errors/mllp-invalid-message-error.ts","../src/errors/mllp-invalid-response-error.ts","../src/utils.ts","../src/errors/nak-exception.ts","../src/errors/mllp-send-aborted-error.ts","../src/errors/mllp-send-timeout-error.ts","../src/assertions.ts","../src/events.ts","../src/messages.ts","../src/queue.ts","../src/reconnect.ts","../src/session.ts","../src/client.ts"],"sourcesContent":["/**\n * The bounds and defaults the client is configured by. Constants only.\n *\n * @module\n */\n\n/** Default time to wait for a connection to open, in milliseconds. */\nexport const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;\n\n/** Default time to wait for an acknowledgment after sending, in milliseconds. */\nexport const DEFAULT_SEND_TIMEOUT_MS = 30_000;\n\n/** Default maximum bytes buffered while receiving one message (16 MiB). */\nexport const DEFAULT_MAX_BUFFERED_BYTES = 16 * 1024 * 1024;\n\n/**\n * Default number of reconnect attempts after a failed one. With the default\n * delay, the last one starts about 30 seconds after the first failure.\n */\nexport const DEFAULT_RECONNECT_ATTEMPTS = 5;\n\n/**\n * Maximum timeout `setTimeout` supports, in milliseconds (2^31 − 1). Larger\n * values, including `Infinity`, are rejected: the platform would clamp them\n * to about 1 ms.\n */\nexport const MAX_TIMEOUT_MS = 2 ** 31 - 1;\n","import { MllpClientError, MllpErrorCode } from \"./base\";\n\n/**\n * The call cannot be served: the client is closed, by `close()`, by\n * `destroy()`, or by a failure. The failure is on `cause`.\n */\nexport class MllpClientClosedError extends MllpClientError {\n override readonly name = \"MllpClientClosedError\";\n readonly code = MllpErrorCode.CLOSED;\n readonly delivery = \"not-sent\";\n\n constructor(cause: MllpClientError | null = null) {\n super(\n cause === null\n ? \"The client is closed — construct a new MllpClient to send again.\"\n : \"The client closed on a failure, see cause — construct a new MllpClient to send again.\",\n { cause: cause ?? undefined }\n );\n }\n}\n","import { MllpClientError } from \"./base\";\n\n/**\n * A failure of the wire: the connection could not be opened, gave no reply in\n * time, or was lost. Raised by the layer that holds the socket; the client\n * relays it unchanged.\n */\nexport abstract class MllpConnectionError extends MllpClientError {}\n","import { MllpErrorCode } from \"./base\";\nimport { MllpConnectionError } from \"./mllp-connection-error\";\n\n/**\n * The connection could not be opened: the host was unreachable, refused the\n * connection, or failed DNS or TLS. The socket's error is on `cause`.\n */\nexport class MllpConnectionFailedError extends MllpConnectionError {\n override readonly name = \"MllpConnectionFailedError\";\n readonly code = MllpErrorCode.CONNECTION_FAILED;\n readonly delivery = \"not-sent\";\n\n constructor(cause: unknown) {\n super(\n \"Connecting failed — check that the host is reachable and the port is listening.\",\n { cause }\n );\n }\n}\n","import { MllpErrorCode } from \"./base\";\nimport { MllpConnectionError } from \"./mllp-connection-error\";\n\n/**\n * The remote system did not accept the connection within `connectTimeoutMs`.\n * The host may be down, overloaded, or silently dropping packets.\n */\nexport class MllpConnectionTimeoutError extends MllpConnectionError {\n override readonly name = \"MllpConnectionTimeoutError\";\n readonly code = MllpErrorCode.CONNECTION_TIMEOUT;\n readonly delivery = \"not-sent\";\n readonly timeoutMs: number;\n\n constructor(timeoutMs: number) {\n super(\n \"Connecting timed out — check that the host is reachable and the port is listening.\"\n );\n this.timeoutMs = timeoutMs;\n }\n}\n","import { MllpErrorCode } from \"./base\";\nimport { MllpConnectionError } from \"./mllp-connection-error\";\n\n/**\n * The connection was lost during a send: the remote system hung up, or the\n * network broke. The remote system may or may not have received the message.\n * A stream error, when there was one, is on `cause`.\n */\nexport class MllpConnectionLostError extends MllpConnectionError {\n override readonly name = \"MllpConnectionLostError\";\n readonly code = MllpErrorCode.CONNECTION_LOST;\n readonly delivery = \"unknown\";\n\n constructor(cause?: unknown) {\n super(\n \"The connection was lost while a message was waiting for its acknowledgment — the remote system may or may not have received it; resend only if the message is safe to repeat.\",\n { cause }\n );\n }\n}\n","import { MllpClientError, MllpErrorCode } from \"./base\";\n\n/**\n * The message cannot be sent as-is: it has no MSH-10 control ID, it could not\n * be serialized, or it contains a byte MLLP reserves. Nothing was written; the\n * reason is on `cause`. Fix the message and send again.\n */\nexport class MllpInvalidMessageError extends MllpClientError {\n override readonly name = \"MllpInvalidMessageError\";\n readonly code = MllpErrorCode.INVALID_MESSAGE;\n readonly delivery = \"not-sent\";\n\n constructor(cause: unknown) {\n super(\n \"The message could not be prepared for sending; nothing was written.\",\n { cause }\n );\n }\n}\n","import { MllpClientError, MllpErrorCode } from \"./base\";\n\n/**\n * The reply was not a usable acknowledgment of the message that was waiting:\n * it could not be decoded, its acknowledgment code was missing or unknown, or\n * it answered a different message. The connection is ended. The reason is on\n * `cause`.\n */\nexport class MllpInvalidResponseError extends MllpClientError {\n override readonly name = \"MllpInvalidResponseError\";\n readonly code = MllpErrorCode.INVALID_RESPONSE;\n readonly delivery = \"unknown\";\n /** MSH-10 of the message that was waiting, when the thrower knew it. */\n readonly controlId?: string;\n\n constructor(cause: unknown, controlId?: string) {\n super(\n \"The reply is not a usable acknowledgment of the message that was waiting — the connection has been closed.\",\n { cause }\n );\n this.controlId = controlId;\n }\n}\n","/**\n * Reading a single value out of an HL7v2 tree.\n *\n * @module\n */\n\nimport type { Root } from \"@glion/ast\";\nimport { value } from \"@glion/util-query\";\n\n/** The value at `path`, or `\"\"` when the field is absent. */\nexport function read(tree: Root, path: string): string {\n return value(tree, path)?.value ?? \"\";\n}\n","/**\n * The exception a NAK carries. Not an {@link MllpClientError}: the remote\n * system understood the message and refused it, and the class is the one\n * `@glion/ack` defines for that MSA-1 code.\n *\n * @module\n */\n\nimport {\n AckApplicationError,\n AckApplicationReject,\n AckCode,\n AckCommitError,\n AckCommitReject,\n} from \"@glion/ack\";\nimport type { AckException, AckExceptionOptions, AckNakCode } from \"@glion/ack\";\nimport type { Root } from \"@glion/ast\";\n\nimport { read } from \"../utils\";\n\n/** The `@glion/ack` exception class for each NAK code. */\n// TODO(#689): replace with the shared reader from @glion/ack once it exists.\nconst NAK_EXCEPTIONS = {\n [AckCode.ApplicationError]: AckApplicationError,\n [AckCode.ApplicationReject]: AckApplicationReject,\n [AckCode.CommitError]: AckCommitError,\n [AckCode.CommitReject]: AckCommitReject,\n} as const;\n\n/** MSA-3, ERR-3, and ERR-4 as one sentence, or a note that none were given. */\nfunction nakMessage(code: AckNakCode, nak: AckExceptionOptions): string {\n const said: string[] = [];\n if (nak.text !== undefined) {\n said.push(nak.text);\n }\n if (nak.errorCode !== undefined) {\n said.push(`ERR-3 ${nak.errorCode}`);\n }\n if (nak.severity !== undefined) {\n said.push(`ERR-4 ${nak.severity}`);\n }\n const reason =\n said.length === 0 ? \"It gave no reason.\" : `${said.join(\"; \")}.`;\n return `The remote system did not accept message ${nak.controlId} (MSA-1 ${code}). ${reason}`;\n}\n\n/**\n * The exception a NAK of `code` carries, read from `tree`.\n *\n * `code` picks the class; MSA-2 gives `controlId`, ERR-3 `errorCode`, ERR-4\n * `severity`, and MSA-3 (or ERR-8) `text`.\n */\nexport function nakException(tree: Root, code: AckNakCode): AckException {\n const nak: AckExceptionOptions = {\n controlId: read(tree, \"MSA.2[1].1.1\"),\n errorCode: read(tree, \"ERR.3[1].1.1\") || undefined,\n severity: read(tree, \"ERR.4[1].1.1\") || undefined,\n // ERR-8 stands in for MSA-3 when the remote system left MSA-3 empty.\n text: read(tree, \"MSA.3[1].1.1\") || read(tree, \"ERR.8[1].1.1\") || undefined,\n };\n return new NAK_EXCEPTIONS[code](nakMessage(code, nak), nak);\n}\n","import { MllpClientError, MllpErrorCode } from \"./base\";\n\n/**\n * `destroy()` cut off the message in flight. The remote system may or may not\n * have received it.\n */\nexport class MllpSendAbortedError extends MllpClientError {\n override readonly name = \"MllpSendAbortedError\";\n readonly code = MllpErrorCode.SEND_ABORTED;\n readonly delivery = \"unknown\";\n\n constructor() {\n super(\n \"The send was cut off by destroy() — the remote system may or may not have received the message; resend only if the message is safe to repeat.\"\n );\n }\n}\n","import { MllpErrorCode } from \"./base\";\nimport { MllpConnectionError } from \"./mllp-connection-error\";\n\n/**\n * No acknowledgment arrived within the send timeout. The connection is ended.\n * Whether the remote system received the message is unknown.\n */\nexport class MllpSendTimeoutError extends MllpConnectionError {\n override readonly name = \"MllpSendTimeoutError\";\n readonly code = MllpErrorCode.SEND_TIMEOUT;\n readonly delivery = \"unknown\";\n readonly timeoutMs: number;\n\n constructor(timeoutMs: number) {\n super(\n \"No acknowledgment arrived within the send timeout — the connection has been closed, because a late acknowledgment could not be matched safely.\"\n );\n this.timeoutMs = timeoutMs;\n }\n}\n","/**\n * Option validation, and the client's invariants. Each option `assert*`\n * throws {@link MllpInvalidOptionError} for an option a caller passed out of\n * range, before anything happens. {@link assertPhase} throws for a phase the\n * client cannot be in where it is asserted: a bug.\n *\n * @module\n */\n\nimport { MAX_TIMEOUT_MS } from \"./constants\";\nimport { MllpInvalidOptionError } from \"./errors\";\nimport type { State } from \"./state\";\n\nexport function assertTimeoutMs(option: string, ms: number): void {\n if (!Number.isFinite(ms) || ms <= 0 || ms > MAX_TIMEOUT_MS) {\n throw new MllpInvalidOptionError(\n `${option} must be a number of milliseconds between 1 and ${MAX_TIMEOUT_MS}.`\n );\n }\n}\n\nexport function assertByteCap(bytes: number): void {\n if (!Number.isInteger(bytes) || bytes <= 0) {\n throw new MllpInvalidOptionError(\n \"maxBufferedBytes must be a positive integer.\"\n );\n }\n}\n\n/** `state` is in `phase`; `where` names the site for the report. */\nexport function assertPhase<P extends State[\"phase\"]>(\n state: State,\n phase: P,\n where: string\n): asserts state is Extract<State, { phase: P }> {\n if (state.phase !== phase) {\n throw new Error(\n `${where} found the client ${state.phase}. This is a bug in @glion/mllp-client; please report it.`\n );\n }\n}\n\nexport function assertReconnectAttempts(attempts: number): void {\n const unbounded = attempts === Number.POSITIVE_INFINITY;\n if (!unbounded && (!Number.isInteger(attempts) || attempts < 0)) {\n throw new MllpInvalidOptionError(\n \"reconnect.attempts must be a non-negative integer, or Infinity.\"\n );\n }\n}\n","/**\n * What a client reports as it runs, and the listeners that receive it.\n *\n * @module\n */\n\nimport type { MllpClientError } from \"./errors\";\n\nexport interface MllpClientEvents {\n /** The connection is open and ready for messages. */\n connect(): void;\n /**\n * The connection is down and the client will not connect again. `error` is\n * the failure that closed it, or `null` when the owner did.\n */\n close(error: MllpClientError | null): void;\n}\n\n/** The name of one thing a client reports. */\nexport type MllpClientEvent = keyof MllpClientEvents;\n\n/** A listener for `event`. */\nexport type MllpClientListener<E extends MllpClientEvent> = MllpClientEvents[E];\n\n/**\n * The listeners of a client.\n *\n * A listener that throws propagates to whoever triggered the event, the same\n * as Node's `EventEmitter`.\n */\nexport abstract class MllpClientEmitter {\n readonly #listeners = new Map<\n MllpClientEvent,\n Set<MllpClientEvents[MllpClientEvent]>\n >();\n\n /** Adds a listener for `event`, and returns the client so calls chain. */\n on<E extends MllpClientEvent>(\n event: E,\n listener: MllpClientListener<E>\n ): this {\n const listeners = this.#listeners.get(event) ?? new Set();\n listeners.add(listener);\n this.#listeners.set(event, listeners);\n return this;\n }\n\n /** Removes a listener added with {@link on}. */\n off<E extends MllpClientEvent>(\n event: E,\n listener: MllpClientListener<E>\n ): this {\n this.#listeners.get(event)?.delete(listener);\n return this;\n }\n\n /** Calls every listener for `event`, in the order they were added. */\n protected emit<E extends MllpClientEvent>(\n event: E,\n ...args: Parameters<MllpClientEvents[E]>\n ): void {\n for (const listener of this.#listeners.get(event) ?? []) {\n (listener as (...a: Parameters<MllpClientEvents[E]>) => void)(...args);\n }\n }\n}\n","/**\n * Encoding a message to send, and decoding the acknowledgment that answers it.\n * Pure.\n *\n * @module\n */\n\nimport { isAckNakCode, isAckSuccessCode } from \"@glion/ack\";\nimport type { Root } from \"@glion/ast\";\nimport { parseHL7v2 } from \"@glion/parser\";\nimport { toHl7v2 } from \"@glion/to-hl7v2\";\nimport { decodeBytes, encodeBytes } from \"@glion/util-charset\";\n\nimport {\n MllpInvalidMessageError,\n MllpInvalidResponseError,\n nakException,\n} from \"./errors\";\nimport type { MllpClientResponse } from \"./types\";\nimport { read } from \"./utils\";\n\n/** A message ready to go out. */\nexport interface EncodingResponse {\n /** MSH-10, which the acknowledgment echoes in MSA-2. */\n readonly controlId: string;\n /** The message in canonical HL7v2, encoded. Framing belongs to the connection. */\n readonly bytes: Uint8Array;\n}\n\n/**\n * `tree` as the message to send, in canonical HL7v2.\n *\n * @throws {MllpInvalidMessageError} The message has no MSH-10, or it could not\n * be serialized or encoded.\n */\nexport function encode(tree: Root): EncodingResponse {\n const controlId = read(tree, \"MSH.10[1].1.1\");\n if (controlId === \"\") {\n throw new MllpInvalidMessageError(\n \"the message has no MSH-10 control ID, so its acknowledgment could not be matched\"\n );\n }\n\n try {\n return { bytes: encodeBytes(toHl7v2(tree)), controlId };\n } catch (error) {\n throw new MllpInvalidMessageError(error);\n }\n}\n\n/**\n * `bytes` as the acknowledgment of the message `controlId` identifies.\n *\n * MSA-2 is checked before MSA-1 is read: an accept or a NAK is reported only\n * for the message it answers. MSH-9 and the HL7 version are not checked\n * (#668).\n *\n * @throws {AckException} MSA-1 refused the message. The remote system\n * understood it.\n * @throws {MllpInvalidResponseError} Not a usable acknowledgment of this\n * message: unreadable bytes, an MSA-1 outside Table 0008, or an MSA-2\n * naming another message.\n */\nexport function decode(\n bytes: Uint8Array,\n controlId: string\n): MllpClientResponse {\n let raw: string;\n let tree: Root;\n try {\n raw = decodeBytes(bytes);\n tree = parseHL7v2(raw);\n } catch (error) {\n throw new MllpInvalidResponseError(error, controlId);\n }\n\n const answers = read(tree, \"MSA.2[1].1.1\");\n if (answers !== controlId) {\n throw new MllpInvalidResponseError(\n `MSA-2 is \"${answers}\", so it answers a different message — usually a late acknowledgment from an earlier timed-out send.`,\n controlId\n );\n }\n\n const code = read(tree, \"MSA.1[1].1.1\");\n if (isAckNakCode(code)) {\n throw nakException(tree, code);\n }\n if (!isAckSuccessCode(code)) {\n throw new MllpInvalidResponseError(\n code === \"\"\n ? \"MSA-1 is empty, so accept or reject cannot be determined.\"\n : `MSA-1 is \"${code}\", which is not one of AA, AE, AR, CA, CE, or CR.`,\n controlId\n );\n }\n\n return {\n code,\n controlId: answers,\n // MSH-10 of the acknowledgment itself. MUST NOT be used for correlation.\n id: read(tree, \"MSH.10[1].1.1\"),\n raw,\n text: read(tree, \"MSA.3[1].1.1\") || undefined,\n tree,\n };\n}\n","/**\n * The queue of sends: the head is the send in progress, the rest wait their\n * turn in `enter()` order.\n *\n * @module\n */\n\n/** A place in the queue. */\nexport interface Turn {\n /** Resolves when the send may start. `null` when it may start now. */\n readonly wait: Promise<unknown> | null;\n}\n\ninterface Entry extends Turn {\n resolve(): void;\n reject(error: unknown): void;\n}\n\nexport interface Queue {\n /** How many turns wait behind the head. */\n readonly pending: number;\n /** Resolves once every turn has left. At once when none is in. */\n drained(): Promise<void>;\n /** Takes the last place. */\n enter(): Turn;\n /** Gives `turn`'s place up. Starts the next send when `turn` was the head. */\n leave(turn: Turn): void;\n /** Rejects every waiting `Turn` with `error`. The head is not touched. */\n rejectWaiting(error: unknown): void;\n}\n\nexport function createQueue(): Queue {\n const entries: Entry[] = [];\n let empty: PromiseWithResolvers<void> | null = null;\n return {\n async drained() {\n if (entries.length === 0) {\n return;\n }\n empty ??= Promise.withResolvers();\n await empty.promise;\n },\n enter() {\n const { promise, resolve, reject } = Promise.withResolvers();\n const entry: Entry = {\n reject,\n resolve: () => resolve(null),\n wait: entries.length === 0 ? null : promise,\n };\n entries.push(entry);\n return entry;\n },\n leave(turn) {\n const place = entries.indexOf(turn as Entry);\n if (place === -1) {\n throw new Error(\n \"A turn left the queue twice. This is a bug in @glion/mllp-client; please report it.\"\n );\n }\n entries.splice(place, 1);\n if (place === 0) {\n entries[0]?.resolve();\n }\n if (entries.length === 0) {\n empty?.resolve();\n empty = null;\n }\n },\n get pending() {\n return Math.max(0, entries.length - 1);\n },\n rejectWaiting(error) {\n for (const entry of entries.slice(1)) {\n entry.reject(error);\n }\n },\n };\n}\n","/**\n * The reconnect policy, its default delay, and the wait between attempts.\n *\n * @module\n */\n\n/** A reconnect policy with every option resolved. */\nexport interface ReconnectPolicy {\n readonly attempts: number;\n readonly delay: (attempt: number) => number;\n}\n\n/** Ceiling of the first default wait, in milliseconds. Doubles per attempt. */\nconst BACKOFF_BASE_MS = 1000;\n\n/** Ceiling of every default wait, in milliseconds. */\nconst BACKOFF_CAP_MS = 30_000;\n\n/**\n * Full-jitter exponential backoff: a wait drawn uniformly from zero to\n * `min(30 s, 1 s × 2^(attempt − 1))`.\n */\nexport function defaultReconnectDelay(attempt: number): number {\n const ceiling = Math.min(\n BACKOFF_CAP_MS,\n BACKOFF_BASE_MS * 2 ** (attempt - 1)\n );\n return Math.floor(Math.random() * ceiling);\n}\n\n/** Resolves after `ms`, or at once when `signal` aborts first. Never rejects. */\nexport function sleep(ms: number, signal: AbortSignal): Promise<void> {\n if (signal.aborted) {\n return Promise.resolve();\n }\n // oxlint-disable-next-line promise/avoid-new -- wrapping a timer\n return new Promise<void>((resolve) => {\n const done = () => {\n clearTimeout(timer);\n signal.removeEventListener(\"abort\", done);\n // oxlint-disable-next-line promise/no-multiple-resolved -- each path unhooks the other first\n resolve();\n };\n const timer = setTimeout(done, ms);\n signal.addEventListener(\"abort\", done, { once: true });\n });\n}\n","/**\n * One session over a socket: the message layer over its bytes. A message\n * written here is framed, and bytes read here are unframed.\n *\n * @module\n */\n\nimport { MllpCodecError, frame, unframe } from \"@glion/mllp-codec\";\n\nimport {\n MllpConnectionFailedError,\n MllpConnectionTimeoutError,\n MllpConnectionLostError,\n MllpInvalidMessageError,\n MllpSendAbortedError,\n MllpSendTimeoutError,\n} from \"./errors\";\nimport type { MllpSocket, MllpStreams } from \"./types\";\n\n/**\n * One session over one socket, for that socket's lifetime. `ready` resolves\n * once it is open; `destroy()` ends it from any point.\n */\nexport interface MllpSession {\n /**\n * Resolves once the session is open. Rejects with\n * {@link MllpConnectionFailedError} or {@link MllpConnectionTimeoutError};\n * when destroyed while opening, with the former, and only once the socket\n * is down. The owner MUST await it, or observe it, before destroying a\n * session that has not opened.\n */\n readonly ready: Promise<void>;\n /**\n * Sends one message, once the session is open, and returns the next one the\n * remote system sends back, unframed. `timeoutMs` bounds the write and the\n * wait for the reply; it does not cover the opening.\n *\n * @throws {MllpInvalidMessageError} The message contains a reserved MLLP\n * byte, so nothing was written.\n * @throws {MllpConnectionFailedError} The session never opened.\n * @throws {MllpConnectionTimeoutError} The session never opened in time.\n * @throws {MllpSendTimeoutError} Nothing came back within `timeoutMs`. The\n * session is destroyed.\n * @throws {MllpSendAbortedError} `destroy()` ended the session while the\n * reply was awaited.\n * @throws {MllpConnectionLostError} The remote system closed, or the stream\n * failed, before a reply came.\n * @throws {MllpCodecError} The remote system sent bytes that are not an MLLP\n * frame.\n */\n exchange(message: Uint8Array, timeoutMs: number): Promise<Uint8Array>;\n /**\n * Ends the session now, and resolves once the socket is down. A `ready`\n * still pending rejects; so does an `exchange` still waiting.\n *\n * Never rejects. Idempotent. Bounded.\n */\n destroy(): Promise<void>;\n}\n\nexport interface ConnectOptions {\n /** Time the remote system has to accept the connection. */\n readonly timeoutMs: number;\n /** Maximum bytes buffered while reading one frame. */\n readonly maxBufferedBytes: number;\n}\n\n/** The socket's streams, framed and locked for this session's use. */\ninterface Framing {\n readonly reader: ReadableStreamDefaultReader<Uint8Array>;\n readonly writer: WritableStreamDefaultWriter<Uint8Array>;\n}\n\n/**\n * Opens `socket` and takes ownership of its streams.\n *\n * @throws {MllpConnectionTimeoutError} No socket within `timeoutMs`.\n * @throws {MllpConnectionFailedError} The socket failed to open, or `signal`\n * cancelled the attempt; the adapter's rejection is on `cause`.\n */\nasync function openSocketStreams(\n socket: MllpSocket,\n signal: AbortSignal,\n opts: ConnectOptions\n): Promise<Framing> {\n const timeout = AbortSignal.timeout(opts.timeoutMs);\n\n let streams: MllpStreams;\n try {\n streams = await socket.connect(AbortSignal.any([signal, timeout]));\n } catch (error) {\n if (timeout.aborted) {\n throw new MllpConnectionTimeoutError(opts.timeoutMs);\n }\n throw new MllpConnectionFailedError(error);\n }\n\n try {\n return {\n reader: streams.readable\n .pipeThrough(unframe({ maxBufferedBytes: opts.maxBufferedBytes }))\n .getReader(),\n writer: streams.writable.getWriter(),\n };\n } catch (error) {\n // `destroy` ends only an attempt that fulfilled; this one is about to reject.\n await socket.close();\n throw new MllpConnectionFailedError(error);\n }\n}\n\n/**\n * Starts opening a session over `socket`, and hands it back at once. The\n * session ends itself when `signal` aborts.\n *\n * Never throws: every failure, an adapter's synchronous throw included,\n * reaches `ready`. Nothing is left open on any path: an attempt that fails or\n * is cancelled closes the socket before `ready` rejects.\n */\nexport function createSession(\n socket: MllpSocket,\n opts: ConnectOptions,\n signal?: AbortSignal\n): MllpSession {\n const abort = new AbortController();\n const streams = openSocketStreams(socket, abort.signal, opts);\n let closing: Promise<void> | null = null;\n const cancel = () => void destroy();\n\n const ready = (async () => {\n signal?.addEventListener(\"abort\", cancel, { once: true });\n try {\n await streams;\n } catch (error) {\n signal?.removeEventListener(\"abort\", cancel);\n throw error;\n }\n if (closing !== null) {\n // Destroyed while opening: the socket is down before this settles.\n await closing;\n throw new MllpConnectionFailedError(abort.signal.reason);\n }\n })();\n\n const destroy = (): Promise<void> => {\n closing ??= (async () => {\n // Aborting cancels a dial still in flight, through the signal the\n // adapter was given, and marks the session as ended, so that a read or\n // write released below reports SEND_ABORTED rather than a stream error.\n abort.abort();\n signal?.removeEventListener(\"abort\", cancel);\n // Opening settles either way once aborted. An open that failed has\n // already closed whatever it opened; only one that succeeded leaves\n // streams to release and a socket to close.\n const [opened] = await Promise.allSettled([streams]);\n if (opened.status === \"fulfilled\") {\n // Release the streams, do not cancel them: cancelling would destroy\n // the socket under the adapter and skip its graceful close.\n opened.value.reader.releaseLock();\n opened.value.writer.releaseLock();\n await socket.close();\n }\n })();\n return closing;\n };\n\n const exchange = async (\n message: Uint8Array,\n timeoutMs: number\n ): Promise<Uint8Array> => {\n let framed: Uint8Array;\n try {\n framed = frame(message);\n } catch (error) {\n // Raised before the writer is touched: nothing reached the wire.\n throw new MllpInvalidMessageError(error);\n }\n\n await ready;\n\n // Destroying is what wakes the read parked below. `clearTimeout` runs in a\n // microtask and this fires as a macrotask, so a deadline that fires at all\n // is one whose exchange is still in flight.\n let timedOut = false;\n const deadline = setTimeout(() => {\n timedOut = true;\n void destroy();\n }, timeoutMs);\n try {\n const { reader, writer } = await streams;\n await writer.write(framed);\n const reply = await reader.read();\n if (reply.done) {\n throw new MllpConnectionLostError();\n }\n return reply.value;\n } catch (error) {\n if (timedOut) {\n throw new MllpSendTimeoutError(timeoutMs);\n }\n if (abort.signal.aborted) {\n throw new MllpSendAbortedError();\n }\n throw error instanceof MllpCodecError ||\n error instanceof MllpConnectionLostError\n ? error\n : new MllpConnectionLostError(error);\n } finally {\n clearTimeout(deadline);\n }\n };\n\n return { destroy, exchange, ready };\n}\n","import type { Root } from \"@glion/ast\";\nimport { MllpCodecError } from \"@glion/mllp-codec\";\n\nimport {\n assertByteCap,\n assertPhase,\n assertReconnectAttempts,\n assertTimeoutMs,\n} from \"./assertions\";\nimport {\n DEFAULT_CONNECT_TIMEOUT_MS,\n DEFAULT_MAX_BUFFERED_BYTES,\n DEFAULT_RECONNECT_ATTEMPTS,\n DEFAULT_SEND_TIMEOUT_MS,\n} from \"./constants\";\nimport {\n MllpClientClosedError,\n MllpClientError,\n MllpConnectionError,\n MllpInvalidResponseError,\n MllpSendAbortedError,\n} from \"./errors\";\nimport { MllpClientEmitter } from \"./events\";\nimport { decode, encode } from \"./messages\";\nimport { createQueue } from \"./queue\";\nimport { defaultReconnectDelay, sleep } from \"./reconnect\";\nimport type { ReconnectPolicy } from \"./reconnect\";\nimport { createSession } from \"./session\";\nimport type { ConnectOptions } from \"./session\";\nimport type { Open, State } from \"./state\";\nimport type {\n MllpClientOptions,\n MllpClientResponse,\n MllpClientState,\n MllpSendOptions,\n MllpSocket,\n} from \"./types\";\n\n/**\n * Sends HL7v2 messages to one remote system over MLLP and returns each\n * message's acknowledgment.\n *\n * One client owns one socket and puts one message on the wire at a time.\n * `send()` writes the message, waits for the acknowledgment, checks that it\n * answers this message, and resolves with it. A send arriving while another\n * is in flight waits its turn; sends go out in the order they were called.\n *\n * ```ts\n * import { MllpClient } from \"@glion/mllp-client\";\n * import { nodeSocket } from \"@glion/mllp-client/node\";\n *\n * await using client = new MllpClient({\n * socket: nodeSocket({ host: \"hl7.example.org\", port: 2575 }),\n * });\n *\n * const ack = await client.send(adtMessage);\n * ack.code; // \"AA\" or \"CA\"\n * ```\n *\n * ## Lifecycle\n *\n * `send()` connects on first use; `connect()` opens the connection ahead of\n * time. `state` reports the phase. A connection that fails to open is dialed\n * again under the `reconnect` policy. A connection that is lost closes the\n * client: the message in flight rejects and is not sent again. After\n * `close()`, or once the client has closed on a failure, every call rejects\n * with {@link MllpClientClosedError}.\n *\n * ## Errors\n *\n * When the remote system answers `AE`, `AR`, `CE`, or `CR`, `send()` rejects\n * with the matching `@glion/ack` exception and the connection stays open.\n */\nexport class MllpClient extends MllpClientEmitter {\n readonly #socket: MllpSocket;\n readonly #connect: ConnectOptions;\n readonly #policy: ReconnectPolicy;\n readonly #sendTimeoutMs: number;\n #state: State = { phase: \"idle\" };\n readonly #queue = createQueue();\n\n /**\n * @throws {MllpInvalidOptionError} A timeout, byte cap, or attempt count is\n * out of range.\n */\n constructor(opts: MllpClientOptions) {\n super();\n const connectTimeoutMs =\n opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;\n const maxBufferedBytes =\n opts.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;\n const attempts =\n opts.reconnect === false\n ? 0\n : (opts.reconnect?.attempts ?? DEFAULT_RECONNECT_ATTEMPTS);\n const delay =\n opts.reconnect === false\n ? defaultReconnectDelay\n : (opts.reconnect?.delay ?? defaultReconnectDelay);\n this.#sendTimeoutMs = opts.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS;\n\n assertTimeoutMs(\"connectTimeoutMs\", connectTimeoutMs);\n assertTimeoutMs(\"sendTimeoutMs\", this.#sendTimeoutMs);\n assertByteCap(maxBufferedBytes);\n assertReconnectAttempts(attempts);\n\n this.#socket = opts.socket;\n this.#connect = { maxBufferedBytes, timeoutMs: connectTimeoutMs };\n this.#policy = { attempts, delay };\n }\n\n /**\n * The current phase: `idle`, `connecting`, `connected`, `sending`,\n * `closing`, or `closed`.\n */\n get state(): MllpClientState {\n return this.#state.phase;\n }\n\n /** How many sends wait their turn, the one in flight excluded. */\n get pending(): number {\n return this.#queue.pending;\n }\n\n /** Whether the connection is open. True in both `connected` and `sending`. */\n get connected(): boolean {\n const { phase } = this.#state;\n return phase === \"connected\" || phase === \"sending\";\n }\n\n /**\n * Opens the connection ahead of the first `send()`, dialing again under the\n * `reconnect` policy when an attempt fails.\n *\n * Idempotent: a connected client resolves at once, and a call arriving while\n * an attempt is in flight waits for that attempt and shares its outcome.\n *\n * @throws {MllpConnectionFailedError} The last attempt could not open the\n * socket.\n * @throws {MllpConnectionTimeoutError} The last attempt did not open in time.\n * @throws {MllpClientClosedError} The client is closed, or `close()` or\n * `destroy()` arrived while the connection was still being established.\n */\n async connect(): Promise<void> {\n const state = this.#state;\n let opening: Promise<void>;\n switch (state.phase) {\n case \"connected\":\n case \"sending\": {\n return;\n }\n case \"closing\":\n case \"closed\": {\n throw new MllpClientClosedError(state.reason);\n }\n case \"connecting\": {\n opening = state.opening;\n break;\n }\n case \"idle\": {\n const abort = new AbortController();\n opening = this.#dial(abort.signal);\n this.#state = { abort, opening, phase: \"connecting\" };\n break;\n }\n }\n try {\n await opening;\n } catch (error) {\n if (error instanceof MllpConnectionError) {\n await this.destroy(error);\n }\n throw error;\n }\n }\n\n /**\n * Sends one message and returns its acknowledgment. Connects first when the\n * client is not connected yet, and waits for the send in flight, if any,\n * before writing.\n *\n * Sends go out in the order `send()` was called. The queue has no bound.\n * `timeoutMs` runs from the moment the write starts, not from the call. A\n * send still waiting when the client closes rejects with\n * {@link MllpClientClosedError}; one waiting behind a dial that fails\n * rejects with the dial's error, as `connect()` does.\n *\n * Nothing is written when an option, phase, or message error is thrown.\n * Every wire failure also closes the client; a NAK does not.\n *\n * @throws {AckException} The remote system answered with a NAK. The\n * connection stays open.\n * @throws {MllpInvalidOptionError} `timeoutMs` is out of range.\n * @throws {MllpInvalidMessageError} The message cannot be sent as-is.\n * @throws {MllpClientClosedError} The client is closed, or closed while\n * this send was waiting its turn.\n * @throws {MllpConnectionFailedError} The connection could not be opened.\n * @throws {MllpConnectionTimeoutError} The connection did not open in time.\n * @throws {MllpSendTimeoutError} No acknowledgment arrived in time.\n * @throws {MllpConnectionLostError} The connection was lost.\n * @throws {MllpSendAbortedError} `destroy()` cut the send off.\n * @throws {MllpInvalidResponseError} The reply is not a usable\n * acknowledgment of this message.\n */\n async send(\n message: Root,\n opts: MllpSendOptions = {}\n ): Promise<MllpClientResponse> {\n const timeoutMs = opts.timeoutMs ?? this.#sendTimeoutMs;\n assertTimeoutMs(\"timeoutMs\", timeoutMs); // throws {MllpInvalidOptionError}\n\n const { bytes, controlId } = encode(message); // throws {MllpInvalidMessageError}\n\n // Refused before taking a place, and again by connect() at its turn.\n const at = this.#state;\n if (at.phase === \"closing\" || at.phase === \"closed\") {\n throw new MllpClientClosedError(at.reason);\n }\n\n const turn = this.#queue.enter();\n try {\n if (turn.wait !== null) {\n await turn.wait;\n }\n if (!this.connected) {\n await this.connect();\n }\n return await this.#send(controlId, bytes, timeoutMs);\n } finally {\n this.#queue.leave(turn);\n }\n }\n\n /**\n * Ends the connection once the message in flight is acknowledged, and\n * resolves when it is down. New sends, and sends waiting their turn, are\n * refused from the moment this is called. An attempt to connect stops at\n * once.\n *\n * Resolves from any phase. Never rejects. Idempotent. The wait is bounded by\n * the in-flight send's own deadline; {@link destroy} does not wait at all.\n * If the message it waits for fails, the client closes with that failure;\n * otherwise with `null`.\n */\n async close(): Promise<void> {\n const from = this.#state;\n switch (from.phase) {\n case \"closing\": {\n await from.closed;\n return;\n }\n case \"sending\": {\n const closed = (async () => {\n await this.#queue.drained();\n await this.#destroy(from, null);\n })();\n this.#state = {\n closed,\n phase: \"closing\",\n reason: null,\n session: from.session,\n };\n this.#queue.rejectWaiting(new MllpClientClosedError());\n await closed;\n return;\n }\n case \"idle\":\n case \"connecting\":\n case \"connected\":\n case \"closed\": {\n await this.destroy();\n }\n }\n }\n\n /**\n * Ends the connection now, and resolves when it is down. A message in flight\n * rejects with {@link MllpSendAbortedError}. An attempt to connect stops at\n * once. The client closes with `reason`: the failure it reports on `close`\n * and on every later call, or `null` for the owner's own decision.\n *\n * Resolves from any phase. Never rejects. Idempotent. Sends waiting their\n * turn are rejected with `reason` when its delivery is `not-sent`, and with\n * {@link MllpClientClosedError} carrying `reason` otherwise. Arriving while\n * the client is already closing, it cuts the message in flight and joins\n * the ending under way; `reason` is recorded when no failure is known yet.\n */\n async destroy(reason: MllpClientError | null = null): Promise<void> {\n const from = this.#state;\n switch (from.phase) {\n case \"closed\": {\n return;\n }\n case \"closing\": {\n if (from.reason === null && reason !== null) {\n this.#state = { ...from, reason };\n }\n // A close() waiting out the message in flight: cut it.\n await from.session?.destroy();\n await from.closed;\n return;\n }\n case \"idle\": {\n this.#state = { phase: \"closed\", reason };\n this.emit(\"close\", reason);\n return;\n }\n case \"connecting\":\n case \"connected\":\n case \"sending\": {\n const closed = this.#destroy(from, reason);\n this.#state = {\n closed,\n phase: \"closing\",\n reason,\n session: \"session\" in from ? from.session : null,\n };\n // A waiter's message was never written, so a failure of unknown\n // delivery is not its own.\n this.#queue.rejectWaiting(\n reason?.delivery === \"not-sent\"\n ? reason\n : new MllpClientClosedError(reason)\n );\n await closed;\n }\n }\n }\n\n /** Calls {@link close}. Enables `await using`. */\n async [Symbol.asyncDispose](): Promise<void> {\n await this.close();\n }\n\n // ── The connection ──────────────────────────────────────────────────\n\n /**\n * Dials under the policy until a session opens, then moves to `connected`\n * and reports `connect`. Each attempt after the first waits\n * `delay(attempt)` first.\n *\n * @throws {MllpClientClosedError} `signal` aborted: the owner closed the\n * client. Nothing is left open.\n * @throws {MllpConnectionFailedError} The policy gave up; the last attempt\n * could not open the socket.\n * @throws {MllpConnectionTimeoutError} The policy gave up; the last attempt\n * did not open in time.\n */\n async #dial(signal: AbortSignal): Promise<void> {\n for (let attempt = 0; ; attempt += 1) {\n if (attempt > 0) {\n await sleep(this.#policy.delay(attempt), signal);\n }\n if (signal.aborted) {\n throw new MllpClientClosedError(signal.reason);\n }\n const session = createSession(this.#socket, this.#connect, signal);\n try {\n await session.ready;\n } catch (error) {\n if (signal.aborted) {\n throw new MllpClientClosedError(signal.reason);\n }\n if (attempt >= this.#policy.attempts) {\n throw error;\n }\n continue;\n }\n if (signal.aborted) {\n // Aborted between the socket opening and this running; the session\n // ends itself on the signal.\n await session.destroy();\n throw new MllpClientClosedError(signal.reason);\n }\n this.#state = { phase: \"connected\", session };\n this.emit(\"connect\");\n return;\n }\n }\n\n // ── The end ─────────────────────────────────────────────────────────\n\n /**\n * Ends what `from` holds, the dial or the session, then moves to `closed`\n * with the reason `closing` holds by then and reports `close`. A dial is\n * aborted with `reason`.\n *\n * MUST NOT touch `#state` before its first `await`: the caller moves to\n * `closing` after calling it.\n */\n async #destroy(from: Open, reason: MllpClientError | null): Promise<void> {\n switch (from.phase) {\n case \"connecting\": {\n from.abort.abort(reason);\n // Its outcome belongs to the calls waiting on it.\n await Promise.allSettled([from.opening]);\n break;\n }\n case \"connected\":\n case \"sending\": {\n await from.session.destroy();\n break;\n }\n }\n const ending = this.#state;\n assertPhase(ending, \"closing\", \"The teardown\");\n this.#state = { phase: \"closed\", reason: ending.reason };\n this.emit(\"close\", ending.reason);\n }\n\n // ── The exchange ────────────────────────────────────────────────────\n\n /**\n * The `sending` phase: the message on the wire and its acknowledgment,\n * from `connected` and back to it.\n *\n * A failure of unknown delivery closes the client with it. A NAK, or a\n * message refused before the write, leaves the connection in step.\n */\n async #send(\n controlId: string,\n bytes: Uint8Array,\n timeoutMs: number\n ): Promise<MllpClientResponse> {\n const at = this.#state;\n assertPhase(at, \"connected\", \"send() at its turn\");\n const { session } = at;\n this.#state = { phase: \"sending\", session };\n try {\n const reply = await session.exchange(bytes, timeoutMs);\n return decode(reply, controlId);\n } catch (error) {\n return await this.#sendFailed(error, controlId);\n } finally {\n if (this.#state.phase === \"sending\") {\n this.#state = { phase: \"connected\", session };\n }\n }\n }\n\n /**\n * The send of `controlId` failed with `error`. Throws it as the client\n * reports it: a reply the codec could not read is\n * {@link MllpInvalidResponseError}. A failure of unknown delivery, other\n * than a send cut off by `destroy()`, closes the client first.\n *\n * From `closing`, the ending under way is waiting for this send: started,\n * not awaited.\n */\n async #sendFailed(error: unknown, controlId: string): Promise<never> {\n // Bytes that are not MLLP are not an acknowledgment of this message.\n const reason =\n error instanceof MllpCodecError\n ? new MllpInvalidResponseError(error, controlId)\n : error;\n\n // A NAK, or a message refused before the write: the wire is in step.\n if (!(reason instanceof MllpClientError) || reason.delivery !== \"unknown\") {\n throw reason;\n }\n\n // Cut off by destroy(): that call's ending, not this send's failure.\n if (reason instanceof MllpSendAbortedError) {\n throw reason;\n }\n\n // The wire is at an unknown position: the client closes with the failure.\n // From `closing`, the ending under way is waiting for this send, so it is\n // started, not awaited.\n if (this.#state.phase === \"sending\") {\n await this.destroy(reason);\n } else {\n void this.destroy(reason);\n }\n throw reason;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#listeners","#socket","#connect","#policy","#sendTimeoutMs","#queue","#state","#dial","#send","#destroy","#sendFailed"],"sources":["../src/constants.ts","../src/errors/mllp-client-closed-error.ts","../src/errors/mllp-connection-error.ts","../src/errors/mllp-connection-failed-error.ts","../src/errors/mllp-connection-timeout-error.ts","../src/errors/mllp-connection-lost-error.ts","../src/errors/mllp-invalid-message-error.ts","../src/errors/mllp-invalid-response-error.ts","../src/utils.ts","../src/errors/nak-exception.ts","../src/errors/mllp-send-aborted-error.ts","../src/errors/mllp-send-timeout-error.ts","../src/assertions.ts","../src/events.ts","../src/messages.ts","../src/queue.ts","../src/reconnect.ts","../src/session.ts","../src/client.ts"],"sourcesContent":["/**\n * The bounds and defaults the client is configured by. Constants only.\n *\n * @module\n */\n\n/** Default time to wait for a connection to open, in milliseconds. */\nexport const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;\n\n/** Default time to wait for an acknowledgment after sending, in milliseconds. */\nexport const DEFAULT_SEND_TIMEOUT_MS = 30_000;\n\n/** Default maximum bytes buffered while receiving one message (16 MiB). */\nexport const DEFAULT_MAX_BUFFERED_BYTES = 16 * 1024 * 1024;\n\n/**\n * Default number of reconnect attempts after a failed one. With the default\n * delay, the last one starts about 30 seconds after the first failure.\n */\nexport const DEFAULT_RECONNECT_ATTEMPTS = 5;\n\n/**\n * Maximum timeout `setTimeout` supports, in milliseconds (2^31 − 1). Larger\n * values, including `Infinity`, are rejected: the platform would clamp them\n * to about 1 ms.\n */\nexport const MAX_TIMEOUT_MS = 2 ** 31 - 1;\n","import { MllpClientError, MllpErrorCode } from \"./base\";\n\n/**\n * The call cannot be served: the client is closed, by `close()`, by\n * `destroy()`, or by a failure. The failure is on `cause`.\n */\nexport class MllpClientClosedError extends MllpClientError {\n override readonly name = \"MllpClientClosedError\";\n readonly code = MllpErrorCode.CLOSED;\n readonly delivery = \"not-sent\";\n\n constructor(cause: MllpClientError | null = null) {\n super(\n cause === null\n ? \"The client is closed — construct a new MllpClient to send again.\"\n : \"The client closed on a failure, see cause — construct a new MllpClient to send again.\",\n { cause: cause ?? undefined }\n );\n }\n}\n","import { MllpClientError } from \"./base\";\n\n/**\n * A failure of the wire: the connection could not be opened, gave no reply in\n * time, or was lost. Raised by the layer that holds the socket; the client\n * relays it unchanged.\n */\nexport abstract class MllpConnectionError extends MllpClientError {}\n","import { MllpErrorCode } from \"./base\";\nimport { MllpConnectionError } from \"./mllp-connection-error\";\n\n/**\n * The connection could not be opened: the host was unreachable, refused the\n * connection, or failed DNS or TLS. The socket's error is on `cause`.\n */\nexport class MllpConnectionFailedError extends MllpConnectionError {\n override readonly name = \"MllpConnectionFailedError\";\n readonly code = MllpErrorCode.CONNECTION_FAILED;\n readonly delivery = \"not-sent\";\n\n constructor(cause: unknown) {\n super(\n \"Connecting failed — check that the host is reachable and the port is listening.\",\n { cause }\n );\n }\n}\n","import { MllpErrorCode } from \"./base\";\nimport { MllpConnectionError } from \"./mllp-connection-error\";\n\n/**\n * The remote system did not accept the connection within `connectTimeoutMs`.\n * The host may be down, overloaded, or silently dropping packets.\n */\nexport class MllpConnectionTimeoutError extends MllpConnectionError {\n override readonly name = \"MllpConnectionTimeoutError\";\n readonly code = MllpErrorCode.CONNECTION_TIMEOUT;\n readonly delivery = \"not-sent\";\n readonly timeoutMs: number;\n\n constructor(timeoutMs: number) {\n super(\n \"Connecting timed out — check that the host is reachable and the port is listening.\"\n );\n this.timeoutMs = timeoutMs;\n }\n}\n","import { MllpErrorCode } from \"./base\";\nimport { MllpConnectionError } from \"./mllp-connection-error\";\n\n/**\n * The connection was lost during a send: the remote system hung up, or the\n * network broke. The remote system may or may not have received the message.\n * A stream error, when there was one, is on `cause`.\n */\nexport class MllpConnectionLostError extends MllpConnectionError {\n override readonly name = \"MllpConnectionLostError\";\n readonly code = MllpErrorCode.CONNECTION_LOST;\n readonly delivery = \"unknown\";\n\n constructor(cause?: unknown) {\n super(\n \"The connection was lost while a message was waiting for its acknowledgment — the remote system may or may not have received it; resend only if the message is safe to repeat.\",\n { cause }\n );\n }\n}\n","import { MllpClientError, MllpErrorCode } from \"./base\";\n\n/**\n * The message cannot be sent as-is: it has no MSH-10 control ID, it could not\n * be serialized, or it contains a byte MLLP reserves. Nothing was written; the\n * reason is on `cause`. Fix the message and send again.\n */\nexport class MllpInvalidMessageError extends MllpClientError {\n override readonly name = \"MllpInvalidMessageError\";\n readonly code = MllpErrorCode.INVALID_MESSAGE;\n readonly delivery = \"not-sent\";\n\n constructor(cause: unknown) {\n super(\n \"The message could not be prepared for sending; nothing was written.\",\n { cause }\n );\n }\n}\n","import { MllpClientError, MllpErrorCode } from \"./base\";\n\n/**\n * The reply was not a usable acknowledgment of the message that was waiting:\n * it could not be decoded, its acknowledgment code was missing or unknown, or\n * it answered a different message. The connection is ended. The reason is on\n * `cause`.\n */\nexport class MllpInvalidResponseError extends MllpClientError {\n override readonly name = \"MllpInvalidResponseError\";\n readonly code = MllpErrorCode.INVALID_RESPONSE;\n readonly delivery = \"unknown\";\n /** MSH-10 of the message that was waiting, when the thrower knew it. */\n readonly controlId?: string;\n\n constructor(cause: unknown, controlId?: string) {\n super(\n \"The reply is not a usable acknowledgment of the message that was waiting — the connection has been closed.\",\n { cause }\n );\n this.controlId = controlId;\n }\n}\n","/**\n * Reading a single value out of an HL7v2 tree.\n *\n * @module\n */\n\nimport type { Root } from \"@glion/ast\";\nimport { value } from \"@glion/util-query\";\n\n/** The value at `path`, or `\"\"` when the field is absent. */\nexport function read(tree: Root, path: string): string {\n return value(tree, path)?.value ?? \"\";\n}\n","/**\n * The exception a NAK carries. Not an {@link MllpClientError}: the remote\n * system understood the message and refused it, and the class is the one\n * `@glion/ack` defines for that MSA-1 code.\n *\n * @module\n */\n\nimport {\n AckApplicationError,\n AckApplicationReject,\n AckCode,\n AckCommitError,\n AckCommitReject,\n} from \"@glion/ack\";\nimport type { AckException, AckExceptionOptions, AckNakCode } from \"@glion/ack\";\nimport type { Root } from \"@glion/ast\";\n\nimport { read } from \"../utils\";\n\n/** The `@glion/ack` exception class for each NAK code. */\n// TODO(#689): replace with the shared reader from @glion/ack once it exists.\nconst NAK_EXCEPTIONS = {\n [AckCode.ApplicationError]: AckApplicationError,\n [AckCode.ApplicationReject]: AckApplicationReject,\n [AckCode.CommitError]: AckCommitError,\n [AckCode.CommitReject]: AckCommitReject,\n} as const;\n\n/** MSA-3, ERR-3, and ERR-4 as one sentence, or a note that none were given. */\nfunction nakMessage(code: AckNakCode, nak: AckExceptionOptions): string {\n const said: string[] = [];\n if (nak.text !== undefined) {\n said.push(nak.text);\n }\n if (nak.errorCode !== undefined) {\n said.push(`ERR-3 ${nak.errorCode}`);\n }\n if (nak.severity !== undefined) {\n said.push(`ERR-4 ${nak.severity}`);\n }\n const reason =\n said.length === 0 ? \"It gave no reason.\" : `${said.join(\"; \")}.`;\n return `The remote system did not accept message ${nak.controlId} (MSA-1 ${code}). ${reason}`;\n}\n\n/**\n * The exception a NAK of `code` carries, read from `tree`.\n *\n * `code` picks the class; MSA-2 gives `controlId`, ERR-3 `errorCode`, ERR-4\n * `severity`, and MSA-3 (or ERR-8) `text`.\n */\nexport function nakException(tree: Root, code: AckNakCode): AckException {\n const nak: AckExceptionOptions = {\n controlId: read(tree, \"MSA.2[1].1.1\"),\n errorCode: read(tree, \"ERR.3[1].1.1\") || undefined,\n severity: read(tree, \"ERR.4[1].1.1\") || undefined,\n // ERR-8 stands in for MSA-3 when the remote system left MSA-3 empty.\n text: read(tree, \"MSA.3[1].1.1\") || read(tree, \"ERR.8[1].1.1\") || undefined,\n };\n return new NAK_EXCEPTIONS[code](nakMessage(code, nak), nak);\n}\n","import { MllpClientError, MllpErrorCode } from \"./base\";\n\n/**\n * `destroy()` cut off the message in flight. The remote system may or may not\n * have received it.\n */\nexport class MllpSendAbortedError extends MllpClientError {\n override readonly name = \"MllpSendAbortedError\";\n readonly code = MllpErrorCode.SEND_ABORTED;\n readonly delivery = \"unknown\";\n\n constructor() {\n super(\n \"The send was cut off by destroy() — the remote system may or may not have received the message; resend only if the message is safe to repeat.\"\n );\n }\n}\n","import { MllpErrorCode } from \"./base\";\nimport { MllpConnectionError } from \"./mllp-connection-error\";\n\n/**\n * No acknowledgment arrived within the send timeout. The connection is ended.\n * Whether the remote system received the message is unknown.\n */\nexport class MllpSendTimeoutError extends MllpConnectionError {\n override readonly name = \"MllpSendTimeoutError\";\n readonly code = MllpErrorCode.SEND_TIMEOUT;\n readonly delivery = \"unknown\";\n readonly timeoutMs: number;\n\n constructor(timeoutMs: number) {\n super(\n \"No acknowledgment arrived within the send timeout — the connection has been closed, because a late acknowledgment could not be matched safely.\"\n );\n this.timeoutMs = timeoutMs;\n }\n}\n","/**\n * Option validation, and the client's invariants. Each option `assert*`\n * throws {@link MllpInvalidOptionError} for an option a caller passed out of\n * range, before anything happens. {@link assertPhase} throws for a phase the\n * client cannot be in where it is asserted: a bug.\n *\n * @module\n */\n\nimport { MAX_TIMEOUT_MS } from \"./constants\";\nimport { MllpInvalidOptionError } from \"./errors\";\nimport type { State } from \"./state\";\n\nexport function assertTimeoutMs(option: string, ms: number): void {\n if (!Number.isFinite(ms) || ms <= 0 || ms > MAX_TIMEOUT_MS) {\n throw new MllpInvalidOptionError(\n `${option} must be a number of milliseconds between 1 and ${MAX_TIMEOUT_MS}.`\n );\n }\n}\n\nexport function assertByteCap(bytes: number): void {\n if (!Number.isInteger(bytes) || bytes <= 0) {\n throw new MllpInvalidOptionError(\n \"maxBufferedBytes must be a positive integer.\"\n );\n }\n}\n\n/** `state` is in `phase`; `where` names the site for the report. */\nexport function assertPhase<P extends State[\"phase\"]>(\n state: State,\n phase: P,\n where: string\n): asserts state is Extract<State, { phase: P }> {\n if (state.phase !== phase) {\n throw new Error(\n `${where} found the client ${state.phase}. This is a bug in @glion/mllp-client; please report it.`\n );\n }\n}\n\nexport function assertReconnectAttempts(attempts: number): void {\n const unbounded = attempts === Number.POSITIVE_INFINITY;\n if (!unbounded && (!Number.isInteger(attempts) || attempts < 0)) {\n throw new MllpInvalidOptionError(\n \"reconnect.attempts must be a non-negative integer, or Infinity.\"\n );\n }\n}\n","/**\n * What a client reports as it runs, and the listeners that receive it.\n *\n * @module\n */\n\nimport type { MllpClientError } from \"./errors\";\n\nexport interface MllpClientEvents {\n /** The connection is open and ready for messages. */\n connect(): void;\n /**\n * The connection is down and the client will not connect again. `error` is\n * the failure that closed it, or `null` when the owner did.\n */\n close(error: MllpClientError | null): void;\n}\n\n/** The name of one thing a client reports. */\nexport type MllpClientEvent = keyof MllpClientEvents;\n\n/** A listener for `event`. */\nexport type MllpClientListener<E extends MllpClientEvent> = MllpClientEvents[E];\n\n/**\n * The listeners of a client.\n *\n * A listener that throws propagates to whoever triggered the event, the same\n * as Node's `EventEmitter`.\n */\nexport abstract class MllpClientEmitter {\n readonly #listeners = new Map<\n MllpClientEvent,\n Set<MllpClientEvents[MllpClientEvent]>\n >();\n\n /** Adds a listener for `event`, and returns the client so calls chain. */\n on<E extends MllpClientEvent>(\n event: E,\n listener: MllpClientListener<E>\n ): this {\n const listeners = this.#listeners.get(event) ?? new Set();\n listeners.add(listener);\n this.#listeners.set(event, listeners);\n return this;\n }\n\n /** Removes a listener added with {@link on}. */\n off<E extends MllpClientEvent>(\n event: E,\n listener: MllpClientListener<E>\n ): this {\n this.#listeners.get(event)?.delete(listener);\n return this;\n }\n\n /** Calls every listener for `event`, in the order they were added. */\n protected emit<E extends MllpClientEvent>(\n event: E,\n ...args: Parameters<MllpClientEvents[E]>\n ): void {\n for (const listener of this.#listeners.get(event) ?? []) {\n (listener as (...a: Parameters<MllpClientEvents[E]>) => void)(...args);\n }\n }\n}\n","/**\n * Encoding a message to send, and decoding the acknowledgment that answers it.\n * Pure.\n *\n * @module\n */\n\nimport { isAckNakCode, isAckSuccessCode } from \"@glion/ack\";\nimport type { Root } from \"@glion/ast\";\nimport { parseHL7v2 } from \"@glion/parser\";\nimport { toHl7v2 } from \"@glion/to-hl7v2\";\nimport { decodeBytes, encodeBytes } from \"@glion/util-charset\";\n\nimport {\n MllpInvalidMessageError,\n MllpInvalidResponseError,\n nakException,\n} from \"./errors\";\nimport type { MllpClientResponse } from \"./types\";\nimport { read } from \"./utils\";\n\n/** A message ready to go out. */\nexport interface EncodingResponse {\n /** MSH-10, which the acknowledgment echoes in MSA-2. */\n readonly controlId: string;\n /** The message in canonical HL7v2, encoded. Framing belongs to the connection. */\n readonly bytes: Uint8Array;\n}\n\n/**\n * `tree` as the message to send, in canonical HL7v2.\n *\n * @throws {MllpInvalidMessageError} The message has no MSH-10, or it could not\n * be serialized or encoded.\n */\nexport function encode(tree: Root): EncodingResponse {\n const controlId = read(tree, \"MSH.10[1].1.1\");\n if (controlId === \"\") {\n throw new MllpInvalidMessageError(\n \"the message has no MSH-10 control ID, so its acknowledgment could not be matched\"\n );\n }\n\n try {\n return { bytes: encodeBytes(toHl7v2(tree)), controlId };\n } catch (error) {\n throw new MllpInvalidMessageError(error);\n }\n}\n\n/**\n * `bytes` as the acknowledgment of the message `controlId` identifies.\n *\n * MSA-2 is checked before MSA-1 is read: an accept or a NAK is reported only\n * for the message it answers. MSH-9 and the HL7 version are not checked\n * (#668).\n *\n * @throws {AckException} MSA-1 refused the message. The remote system\n * understood it.\n * @throws {MllpInvalidResponseError} Not a usable acknowledgment of this\n * message: unreadable bytes, an MSA-1 outside Table 0008, or an MSA-2\n * naming another message.\n */\nexport function decode(\n bytes: Uint8Array,\n controlId: string\n): MllpClientResponse {\n let raw: string;\n let tree: Root;\n try {\n raw = decodeBytes(bytes);\n tree = parseHL7v2(raw);\n } catch (error) {\n throw new MllpInvalidResponseError(error, controlId);\n }\n\n const answers = read(tree, \"MSA.2[1].1.1\");\n if (answers !== controlId) {\n throw new MllpInvalidResponseError(\n `MSA-2 is \"${answers}\", so it answers a different message — usually a late acknowledgment from an earlier timed-out send.`,\n controlId\n );\n }\n\n const code = read(tree, \"MSA.1[1].1.1\");\n if (isAckNakCode(code)) {\n throw nakException(tree, code);\n }\n if (!isAckSuccessCode(code)) {\n throw new MllpInvalidResponseError(\n code === \"\"\n ? \"MSA-1 is empty, so accept or reject cannot be determined.\"\n : `MSA-1 is \"${code}\", which is not one of AA, AE, AR, CA, CE, or CR.`,\n controlId\n );\n }\n\n return {\n code,\n controlId: answers,\n // MSH-10 of the acknowledgment itself. MUST NOT be used for correlation.\n id: read(tree, \"MSH.10[1].1.1\"),\n raw,\n text: read(tree, \"MSA.3[1].1.1\") || undefined,\n tree,\n };\n}\n","/**\n * The queue of sends: the head is the send in progress, the rest wait their\n * turn in `enter()` order.\n *\n * @module\n */\n\n/** A place in the queue. */\nexport interface Turn {\n /** Resolves when the send may start. `null` when it may start now. */\n readonly wait: Promise<unknown> | null;\n}\n\ninterface Entry extends Turn {\n resolve(): void;\n reject(error: unknown): void;\n}\n\nexport interface Queue {\n /** How many turns wait behind the head. */\n readonly pending: number;\n /** Resolves once every turn has left. At once when none is in. */\n drained(): Promise<void>;\n /** Takes the last place. */\n enter(): Turn;\n /** Gives `turn`'s place up. Starts the next send when `turn` was the head. */\n leave(turn: Turn): void;\n /** Rejects every waiting `Turn` with `error`. The head is not touched. */\n rejectWaiting(error: unknown): void;\n}\n\nexport function createQueue(): Queue {\n const entries: Entry[] = [];\n let empty: PromiseWithResolvers<void> | null = null;\n return {\n async drained() {\n if (entries.length === 0) {\n return;\n }\n empty ??= Promise.withResolvers();\n await empty.promise;\n },\n enter() {\n const { promise, resolve, reject } = Promise.withResolvers();\n const entry: Entry = {\n reject,\n resolve: () => resolve(null),\n wait: entries.length === 0 ? null : promise,\n };\n entries.push(entry);\n return entry;\n },\n leave(turn) {\n const place = entries.indexOf(turn as Entry);\n if (place === -1) {\n throw new Error(\n \"A turn left the queue twice. This is a bug in @glion/mllp-client; please report it.\"\n );\n }\n entries.splice(place, 1);\n if (place === 0) {\n entries[0]?.resolve();\n }\n if (entries.length === 0) {\n empty?.resolve();\n empty = null;\n }\n },\n get pending() {\n return Math.max(0, entries.length - 1);\n },\n rejectWaiting(error) {\n for (const entry of entries.slice(1)) {\n entry.reject(error);\n }\n },\n };\n}\n","/**\n * The reconnect policy, its default delay, and the wait between attempts.\n *\n * @module\n */\n\n/** A reconnect policy with every option resolved. */\nexport interface ReconnectPolicy {\n readonly attempts: number;\n readonly delay: (attempt: number) => number;\n}\n\n/** Ceiling of the first default wait, in milliseconds. Doubles per attempt. */\nconst BACKOFF_BASE_MS = 1000;\n\n/** Ceiling of every default wait, in milliseconds. */\nconst BACKOFF_CAP_MS = 30_000;\n\n/**\n * Full-jitter exponential backoff: a wait drawn uniformly from zero to\n * `min(30 s, 1 s × 2^(attempt − 1))`.\n */\nexport function defaultReconnectDelay(attempt: number): number {\n const ceiling = Math.min(\n BACKOFF_CAP_MS,\n BACKOFF_BASE_MS * 2 ** (attempt - 1)\n );\n return Math.floor(Math.random() * ceiling);\n}\n\n/** Resolves after `ms`, or at once when `signal` aborts first. Never rejects. */\nexport function sleep(ms: number, signal: AbortSignal): Promise<void> {\n if (signal.aborted) {\n return Promise.resolve();\n }\n // oxlint-disable-next-line promise/avoid-new -- wrapping a timer\n return new Promise<void>((resolve) => {\n const done = () => {\n clearTimeout(timer);\n signal.removeEventListener(\"abort\", done);\n // oxlint-disable-next-line promise/no-multiple-resolved -- each path unhooks the other first\n resolve();\n };\n const timer = setTimeout(done, ms);\n signal.addEventListener(\"abort\", done, { once: true });\n });\n}\n","/**\n * One session over a socket: the message layer over its bytes. A message\n * written here is framed, and bytes read here are unframed.\n *\n * @module\n */\n\nimport { MllpCodecError, frame, unframe } from \"@glion/mllp-codec\";\n\nimport {\n MllpConnectionFailedError,\n MllpConnectionTimeoutError,\n MllpConnectionLostError,\n MllpInvalidMessageError,\n MllpSendAbortedError,\n MllpSendTimeoutError,\n} from \"./errors\";\nimport type { MllpSocket, MllpStreams } from \"./types\";\n\n/**\n * One session over one socket, for that socket's lifetime. `ready` resolves\n * once it is open; `destroy()` ends it from any point.\n */\nexport interface MllpSession {\n /**\n * Resolves once the session is open. Rejects with\n * {@link MllpConnectionFailedError} or {@link MllpConnectionTimeoutError};\n * when destroyed while opening, with the former, and only once the socket\n * is down. The owner MUST await it, or observe it, before destroying a\n * session that has not opened.\n */\n readonly ready: Promise<void>;\n /**\n * Sends one message, once the session is open, and returns the next one the\n * remote system sends back, unframed. `timeoutMs` bounds the write and the\n * wait for the reply; it does not cover the opening.\n *\n * @throws {MllpInvalidMessageError} The serialized message contains a\n * reserved MLLP byte, so nothing was written.\n * @throws {MllpConnectionFailedError} The session never opened.\n * @throws {MllpConnectionTimeoutError} The session never opened in time.\n * @throws {MllpSendTimeoutError} Nothing came back within `timeoutMs`. The\n * session is destroyed.\n * @throws {MllpSendAbortedError} `destroy()` ended the session while the\n * reply was awaited.\n * @throws {MllpConnectionLostError} The remote system closed, or the stream\n * failed, before a reply came.\n * @throws {MllpCodecError} The remote system sent bytes that are not an MLLP\n * frame.\n */\n exchange(message: Uint8Array, timeoutMs: number): Promise<Uint8Array>;\n /**\n * Ends the session now, and resolves once the socket is down. A `ready`\n * still pending rejects; so does an `exchange` still waiting.\n *\n * Never rejects. Idempotent. Bounded.\n */\n destroy(): Promise<void>;\n}\n\nexport interface ConnectOptions {\n /** Time the remote system has to accept the connection. */\n readonly timeoutMs: number;\n /** Maximum bytes buffered while reading one frame. */\n readonly maxBufferedBytes: number;\n}\n\n/** The socket's streams, framed and locked for this session's use. */\ninterface Framing {\n readonly reader: ReadableStreamDefaultReader<Uint8Array>;\n readonly writer: WritableStreamDefaultWriter<Uint8Array>;\n}\n\n/**\n * Opens `socket` and takes ownership of its streams.\n *\n * @throws {MllpConnectionTimeoutError} No socket within `timeoutMs`.\n * @throws {MllpConnectionFailedError} The socket failed to open, or `signal`\n * cancelled the attempt; the adapter's rejection is on `cause`.\n */\nasync function openSocketStreams(\n socket: MllpSocket,\n signal: AbortSignal,\n opts: ConnectOptions\n): Promise<Framing> {\n const timeout = AbortSignal.timeout(opts.timeoutMs);\n\n let streams: MllpStreams;\n try {\n streams = await socket.connect(AbortSignal.any([signal, timeout]));\n } catch (error) {\n if (timeout.aborted) {\n throw new MllpConnectionTimeoutError(opts.timeoutMs);\n }\n throw new MllpConnectionFailedError(error);\n }\n\n try {\n return {\n reader: streams.readable\n .pipeThrough(unframe({ maxBufferedBytes: opts.maxBufferedBytes }))\n .getReader(),\n writer: streams.writable.getWriter(),\n };\n } catch (error) {\n // `destroy` ends only an attempt that fulfilled; this one is about to reject.\n await socket.close();\n throw new MllpConnectionFailedError(error);\n }\n}\n\n/**\n * Starts opening a session over `socket`, and hands it back at once. The\n * session ends itself when `signal` aborts.\n *\n * Never throws: every failure, an adapter's synchronous throw included,\n * reaches `ready`. Nothing is left open on any path: an attempt that fails or\n * is cancelled closes the socket before `ready` rejects.\n */\nexport function createSession(\n socket: MllpSocket,\n opts: ConnectOptions,\n signal?: AbortSignal\n): MllpSession {\n const abort = new AbortController();\n const streams = openSocketStreams(socket, abort.signal, opts);\n let closing: Promise<void> | null = null;\n const cancel = () => void destroy();\n\n const ready = (async () => {\n signal?.addEventListener(\"abort\", cancel, { once: true });\n try {\n await streams;\n } catch (error) {\n signal?.removeEventListener(\"abort\", cancel);\n throw error;\n }\n if (closing !== null) {\n // Destroyed while opening: the socket is down before this settles.\n await closing;\n throw new MllpConnectionFailedError(abort.signal.reason);\n }\n })();\n\n const destroy = (): Promise<void> => {\n closing ??= (async () => {\n // Aborting cancels a dial still in flight, through the signal the\n // adapter was given, and marks the session as ended, so that a read or\n // write released below reports SEND_ABORTED rather than a stream error.\n abort.abort();\n signal?.removeEventListener(\"abort\", cancel);\n // Opening settles either way once aborted. An open that failed has\n // already closed whatever it opened; only one that succeeded leaves\n // streams to release and a socket to close.\n const [opened] = await Promise.allSettled([streams]);\n if (opened.status === \"fulfilled\") {\n // Release the streams, do not cancel them: cancelling would destroy\n // the socket under the adapter and skip its graceful close.\n opened.value.reader.releaseLock();\n opened.value.writer.releaseLock();\n await socket.close();\n }\n })();\n return closing;\n };\n\n const exchange = async (\n message: Uint8Array,\n timeoutMs: number\n ): Promise<Uint8Array> => {\n let framed: Uint8Array;\n try {\n framed = frame(message);\n } catch (error) {\n // Raised before the writer is touched: nothing reached the wire.\n throw new MllpInvalidMessageError(error);\n }\n\n await ready;\n\n // Destroying is what wakes the read parked below. `clearTimeout` runs in a\n // microtask and this fires as a macrotask, so a deadline that fires at all\n // is one whose exchange is still in flight.\n let timedOut = false;\n const deadline = setTimeout(() => {\n timedOut = true;\n void destroy();\n }, timeoutMs);\n try {\n const { reader, writer } = await streams;\n await writer.write(framed);\n const reply = await reader.read();\n if (reply.done) {\n throw new MllpConnectionLostError();\n }\n return reply.value;\n } catch (error) {\n if (timedOut) {\n throw new MllpSendTimeoutError(timeoutMs);\n }\n if (abort.signal.aborted) {\n throw new MllpSendAbortedError();\n }\n throw error instanceof MllpCodecError ||\n error instanceof MllpConnectionLostError\n ? error\n : new MllpConnectionLostError(error);\n } finally {\n clearTimeout(deadline);\n }\n };\n\n return { destroy, exchange, ready };\n}\n","import type { Root } from \"@glion/ast\";\nimport { MllpCodecError } from \"@glion/mllp-codec\";\n\nimport {\n assertByteCap,\n assertPhase,\n assertReconnectAttempts,\n assertTimeoutMs,\n} from \"./assertions\";\nimport {\n DEFAULT_CONNECT_TIMEOUT_MS,\n DEFAULT_MAX_BUFFERED_BYTES,\n DEFAULT_RECONNECT_ATTEMPTS,\n DEFAULT_SEND_TIMEOUT_MS,\n} from \"./constants\";\nimport {\n MllpClientClosedError,\n MllpClientError,\n MllpConnectionError,\n MllpInvalidResponseError,\n MllpSendAbortedError,\n} from \"./errors\";\nimport { MllpClientEmitter } from \"./events\";\nimport { decode, encode } from \"./messages\";\nimport { createQueue } from \"./queue\";\nimport { defaultReconnectDelay, sleep } from \"./reconnect\";\nimport type { ReconnectPolicy } from \"./reconnect\";\nimport { createSession } from \"./session\";\nimport type { ConnectOptions } from \"./session\";\nimport type { Open, State } from \"./state\";\nimport type {\n MllpClientOptions,\n MllpClientResponse,\n MllpClientState,\n MllpSendOptions,\n MllpSocket,\n} from \"./types\";\n\n/**\n * Sends HL7v2 messages to one remote system over MLLP and returns each\n * message's acknowledgment.\n *\n * One client owns one socket and puts one message on the wire at a time.\n * `send()` writes the message, waits for the acknowledgment, checks that it\n * answers this message, and resolves with it. A send arriving while another\n * is in flight waits its turn; sends go out in the order they were called.\n *\n * ```ts\n * import { MllpClient } from \"@glion/mllp-client\";\n * import { nodeSocket } from \"@glion/mllp-client/node\";\n *\n * await using client = new MllpClient({\n * socket: nodeSocket({ host: \"hl7.example.org\", port: 2575 }),\n * });\n *\n * const ack = await client.send(adtMessage);\n * ack.code; // \"AA\" or \"CA\"\n * ```\n *\n * ## Lifecycle\n *\n * `send()` connects on first use; `connect()` opens the connection ahead of\n * time. `state` reports the phase. A connection that fails to open is dialed\n * again under the `reconnect` policy. A connection that is lost closes the\n * client: the message in flight rejects and is not sent again. After\n * `close()`, or once the client has closed on a failure, every call rejects\n * with {@link MllpClientClosedError}.\n *\n * ## Errors\n *\n * When the remote system answers `AE`, `AR`, `CE`, or `CR`, `send()` rejects\n * with the matching `@glion/ack` exception and the connection stays open.\n */\nexport class MllpClient extends MllpClientEmitter {\n readonly #socket: MllpSocket;\n readonly #connect: ConnectOptions;\n readonly #policy: ReconnectPolicy;\n readonly #sendTimeoutMs: number;\n #state: State = { phase: \"idle\" };\n readonly #queue = createQueue();\n\n /**\n * @throws {MllpInvalidOptionError} A timeout, byte cap, or attempt count is\n * out of range.\n */\n constructor(opts: MllpClientOptions) {\n super();\n const connectTimeoutMs =\n opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;\n const maxBufferedBytes =\n opts.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;\n const attempts =\n opts.reconnect === false\n ? 0\n : (opts.reconnect?.attempts ?? DEFAULT_RECONNECT_ATTEMPTS);\n const delay =\n opts.reconnect === false\n ? defaultReconnectDelay\n : (opts.reconnect?.delay ?? defaultReconnectDelay);\n this.#sendTimeoutMs = opts.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS;\n\n assertTimeoutMs(\"connectTimeoutMs\", connectTimeoutMs);\n assertTimeoutMs(\"sendTimeoutMs\", this.#sendTimeoutMs);\n assertByteCap(maxBufferedBytes);\n assertReconnectAttempts(attempts);\n\n this.#socket = opts.socket;\n this.#connect = { maxBufferedBytes, timeoutMs: connectTimeoutMs };\n this.#policy = { attempts, delay };\n }\n\n /**\n * The current phase: `idle`, `connecting`, `connected`, `sending`,\n * `closing`, or `closed`.\n */\n get state(): MllpClientState {\n return this.#state.phase;\n }\n\n /** How many sends wait their turn, the one in flight excluded. */\n get pending(): number {\n return this.#queue.pending;\n }\n\n /** Whether the connection is open. True in both `connected` and `sending`. */\n get connected(): boolean {\n const { phase } = this.#state;\n return phase === \"connected\" || phase === \"sending\";\n }\n\n /**\n * Opens the connection ahead of the first `send()`, dialing again under the\n * `reconnect` policy when an attempt fails.\n *\n * Idempotent: a connected client resolves at once, and a call arriving while\n * an attempt is in flight waits for that attempt and shares its outcome.\n *\n * @throws {MllpConnectionFailedError} The last attempt could not open the\n * socket.\n * @throws {MllpConnectionTimeoutError} The last attempt did not open in time.\n * @throws {MllpClientClosedError} The client is closed, or `close()` or\n * `destroy()` arrived while the connection was still being established.\n */\n async connect(): Promise<void> {\n const state = this.#state;\n let opening: Promise<void>;\n switch (state.phase) {\n case \"connected\":\n case \"sending\": {\n return;\n }\n case \"closing\":\n case \"closed\": {\n throw new MllpClientClosedError(state.reason);\n }\n case \"connecting\": {\n opening = state.opening;\n break;\n }\n case \"idle\": {\n const abort = new AbortController();\n opening = this.#dial(abort.signal);\n this.#state = { abort, opening, phase: \"connecting\" };\n break;\n }\n }\n try {\n await opening;\n } catch (error) {\n if (error instanceof MllpConnectionError) {\n await this.destroy(error);\n }\n throw error;\n }\n }\n\n /**\n * Sends one message and returns its acknowledgment. Connects first when the\n * client is not connected yet, and waits for the send in flight, if any,\n * before writing.\n *\n * Sends go out in the order `send()` was called. The queue has no bound.\n * `timeoutMs` runs from the moment the write starts, not from the call. A\n * send still waiting when the client closes rejects with\n * {@link MllpClientClosedError}; one waiting behind a dial that fails\n * rejects with the dial's error, as `connect()` does.\n *\n * Nothing is written when an option, phase, or message error is thrown.\n * Every wire failure also closes the client; a NAK does not.\n *\n * @throws {AckException} The remote system answered with a NAK. The\n * connection stays open.\n * @throws {MllpInvalidOptionError} `timeoutMs` is out of range.\n * @throws {MllpInvalidMessageError} The message cannot be sent as-is.\n * @throws {MllpClientClosedError} The client is closed, or closed while\n * this send was waiting its turn.\n * @throws {MllpConnectionFailedError} The connection could not be opened.\n * @throws {MllpConnectionTimeoutError} The connection did not open in time.\n * @throws {MllpSendTimeoutError} No acknowledgment arrived in time.\n * @throws {MllpConnectionLostError} The connection was lost.\n * @throws {MllpSendAbortedError} `destroy()` cut the send off.\n * @throws {MllpInvalidResponseError} The reply is not a usable\n * acknowledgment of this message.\n */\n async send(\n message: Root,\n opts: MllpSendOptions = {}\n ): Promise<MllpClientResponse> {\n const timeoutMs = opts.timeoutMs ?? this.#sendTimeoutMs;\n assertTimeoutMs(\"timeoutMs\", timeoutMs); // throws {MllpInvalidOptionError}\n\n const { bytes, controlId } = encode(message); // throws {MllpInvalidMessageError}\n\n // Refused before taking a place, and again by connect() at its turn.\n const at = this.#state;\n if (at.phase === \"closing\" || at.phase === \"closed\") {\n throw new MllpClientClosedError(at.reason);\n }\n\n const turn = this.#queue.enter();\n try {\n if (turn.wait !== null) {\n await turn.wait;\n }\n if (!this.connected) {\n await this.connect();\n }\n return await this.#send(controlId, bytes, timeoutMs);\n } finally {\n this.#queue.leave(turn);\n }\n }\n\n /**\n * Ends the connection once the message in flight is acknowledged, and\n * resolves when it is down. New sends, and sends waiting their turn, are\n * refused from the moment this is called. An attempt to connect stops at\n * once.\n *\n * Resolves from any phase. Never rejects. Idempotent. The wait is bounded by\n * the in-flight send's own deadline; {@link destroy} does not wait at all.\n * If the message it waits for fails, the client closes with that failure;\n * otherwise with `null`.\n */\n async close(): Promise<void> {\n const from = this.#state;\n switch (from.phase) {\n case \"closing\": {\n await from.closed;\n return;\n }\n case \"sending\": {\n const closed = (async () => {\n await this.#queue.drained();\n await this.#destroy(from, null);\n })();\n this.#state = {\n closed,\n phase: \"closing\",\n reason: null,\n session: from.session,\n };\n this.#queue.rejectWaiting(new MllpClientClosedError());\n await closed;\n return;\n }\n case \"idle\":\n case \"connecting\":\n case \"connected\":\n case \"closed\": {\n await this.destroy();\n }\n }\n }\n\n /**\n * Ends the connection now, and resolves when it is down. A message in flight\n * rejects with {@link MllpSendAbortedError}. An attempt to connect stops at\n * once. The client closes with `reason`: the failure it reports on `close`\n * and on every later call, or `null` for the owner's own decision.\n *\n * Resolves from any phase. Never rejects. Idempotent. Sends waiting their\n * turn are rejected with `reason` when its delivery is `not-sent`, and with\n * {@link MllpClientClosedError} carrying `reason` otherwise. Arriving while\n * the client is already closing, it cuts the message in flight and joins\n * the ending under way; `reason` is recorded when no failure is known yet.\n */\n async destroy(reason: MllpClientError | null = null): Promise<void> {\n const from = this.#state;\n switch (from.phase) {\n case \"closed\": {\n return;\n }\n case \"closing\": {\n if (from.reason === null && reason !== null) {\n this.#state = { ...from, reason };\n }\n // A close() waiting out the message in flight: cut it.\n await from.session?.destroy();\n await from.closed;\n return;\n }\n case \"idle\": {\n this.#state = { phase: \"closed\", reason };\n this.emit(\"close\", reason);\n return;\n }\n case \"connecting\":\n case \"connected\":\n case \"sending\": {\n const closed = this.#destroy(from, reason);\n this.#state = {\n closed,\n phase: \"closing\",\n reason,\n session: \"session\" in from ? from.session : null,\n };\n // A waiter's message was never written, so a failure of unknown\n // delivery is not its own.\n this.#queue.rejectWaiting(\n reason?.delivery === \"not-sent\"\n ? reason\n : new MllpClientClosedError(reason)\n );\n await closed;\n }\n }\n }\n\n /** Calls {@link close}. Enables `await using`. */\n async [Symbol.asyncDispose](): Promise<void> {\n await this.close();\n }\n\n // ── The connection ──────────────────────────────────────────────────\n\n /**\n * Dials under the policy until a session opens, then moves to `connected`\n * and reports `connect`. Each attempt after the first waits\n * `delay(attempt)` first.\n *\n * @throws {MllpClientClosedError} `signal` aborted: the owner closed the\n * client. Nothing is left open.\n * @throws {MllpConnectionFailedError} The policy gave up; the last attempt\n * could not open the socket.\n * @throws {MllpConnectionTimeoutError} The policy gave up; the last attempt\n * did not open in time.\n */\n async #dial(signal: AbortSignal): Promise<void> {\n for (let attempt = 0; ; attempt += 1) {\n if (attempt > 0) {\n await sleep(this.#policy.delay(attempt), signal);\n }\n if (signal.aborted) {\n throw new MllpClientClosedError(signal.reason);\n }\n const session = createSession(this.#socket, this.#connect, signal);\n try {\n await session.ready;\n } catch (error) {\n if (signal.aborted) {\n throw new MllpClientClosedError(signal.reason);\n }\n if (attempt >= this.#policy.attempts) {\n throw error;\n }\n continue;\n }\n if (signal.aborted) {\n // Aborted between the socket opening and this running; the session\n // ends itself on the signal.\n await session.destroy();\n throw new MllpClientClosedError(signal.reason);\n }\n this.#state = { phase: \"connected\", session };\n this.emit(\"connect\");\n return;\n }\n }\n\n // ── The end ─────────────────────────────────────────────────────────\n\n /**\n * Ends what `from` holds, the dial or the session, then moves to `closed`\n * with the reason `closing` holds by then and reports `close`. A dial is\n * aborted with `reason`.\n *\n * MUST NOT touch `#state` before its first `await`: the caller moves to\n * `closing` after calling it.\n */\n async #destroy(from: Open, reason: MllpClientError | null): Promise<void> {\n switch (from.phase) {\n case \"connecting\": {\n from.abort.abort(reason);\n // Its outcome belongs to the calls waiting on it.\n await Promise.allSettled([from.opening]);\n break;\n }\n case \"connected\":\n case \"sending\": {\n await from.session.destroy();\n break;\n }\n }\n const ending = this.#state;\n assertPhase(ending, \"closing\", \"The teardown\");\n this.#state = { phase: \"closed\", reason: ending.reason };\n this.emit(\"close\", ending.reason);\n }\n\n // ── The exchange ────────────────────────────────────────────────────\n\n /**\n * The `sending` phase: the message on the wire and its acknowledgment,\n * from `connected` and back to it.\n *\n * A failure of unknown delivery closes the client with it. A NAK, or a\n * message refused before the write, leaves the connection in step.\n */\n async #send(\n controlId: string,\n bytes: Uint8Array,\n timeoutMs: number\n ): Promise<MllpClientResponse> {\n const at = this.#state;\n assertPhase(at, \"connected\", \"send() at its turn\");\n const { session } = at;\n this.#state = { phase: \"sending\", session };\n try {\n const reply = await session.exchange(bytes, timeoutMs);\n return decode(reply, controlId);\n } catch (error) {\n return await this.#sendFailed(error, controlId);\n } finally {\n if (this.#state.phase === \"sending\") {\n this.#state = { phase: \"connected\", session };\n }\n }\n }\n\n /**\n * The send of `controlId` failed with `error`. Throws it as the client\n * reports it: a reply the codec could not read is\n * {@link MllpInvalidResponseError}. A failure of unknown delivery, other\n * than a send cut off by `destroy()`, closes the client first.\n *\n * From `closing`, the ending under way is waiting for this send: started,\n * not awaited.\n */\n async #sendFailed(error: unknown, controlId: string): Promise<never> {\n // Bytes that are not MLLP are not an acknowledgment of this message.\n const reason =\n error instanceof MllpCodecError\n ? new MllpInvalidResponseError(error, controlId)\n : error;\n\n // A NAK, or a message refused before the write: the wire is in step.\n if (!(reason instanceof MllpClientError) || reason.delivery !== \"unknown\") {\n throw reason;\n }\n\n // Cut off by destroy(): that call's ending, not this send's failure.\n if (reason instanceof MllpSendAbortedError) {\n throw reason;\n }\n\n // The wire is at an unknown position: the client closes with the failure.\n // From `closing`, the ending under way is waiting for this send, so it is\n // started, not awaited.\n if (this.#state.phase === \"sending\") {\n await this.destroy(reason);\n } else {\n void this.destroy(reason);\n }\n throw reason;\n }\n}\n"],"mappings":"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"}
package/dist/session.d.ts CHANGED
@@ -23,8 +23,8 @@ export interface MllpSession {
23
23
  * remote system sends back, unframed. `timeoutMs` bounds the write and the
24
24
  * wait for the reply; it does not cover the opening.
25
25
  *
26
- * @throws {MllpInvalidMessageError} The message contains a reserved MLLP
27
- * byte, so nothing was written.
26
+ * @throws {MllpInvalidMessageError} The serialized message contains a
27
+ * reserved MLLP byte, so nothing was written.
28
28
  * @throws {MllpConnectionFailedError} The session never opened.
29
29
  * @throws {MllpConnectionTimeoutError} The session never opened in time.
30
30
  * @throws {MllpSendTimeoutError} Nothing came back within `timeoutMs`. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glion/mllp-client",
3
- "version": "0.27.1",
3
+ "version": "0.28.0",
4
4
  "description": "Persistent, single-flight MLLP client for HL7v2",
5
5
  "keywords": [
6
6
  "health",
@@ -36,13 +36,13 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@glion/ack": "0.27.1",
40
- "@glion/ast": "0.27.1",
41
- "@glion/mllp-codec": "0.27.1",
42
- "@glion/parser": "0.27.1",
43
- "@glion/to-hl7v2": "0.27.1",
44
- "@glion/util-charset": "0.27.1",
45
- "@glion/util-query": "0.27.1"
39
+ "@glion/ack": "0.28.0",
40
+ "@glion/ast": "0.28.0",
41
+ "@glion/mllp-codec": "0.28.0",
42
+ "@glion/parser": "0.28.0",
43
+ "@glion/to-hl7v2": "0.28.0",
44
+ "@glion/util-charset": "0.28.0",
45
+ "@glion/util-query": "0.28.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@cloudflare/vitest-plugin": "1.4.0",