diff --git a/__tests__/hl7.client.test.ts b/__tests__/hl7.client.test.ts index d06683d..76c526f 100644 --- a/__tests__/hl7.client.test.ts +++ b/__tests__/hl7.client.test.ts @@ -1,6 +1,6 @@ import portfinder from 'portfinder' -import {Server} from "../../node-hl7-server/src"; -import { Client } from '../src' +import {Server, Listener as ServerListener} from "../../node-hl7-server/src"; +import {Client, Listener, Message} from '../src' import {expectEvent} from "./__utils__/utils"; describe('node hl7 client', () => { @@ -196,7 +196,102 @@ describe('node hl7 client', () => { describe('end to end testing', () => { - test.todo('...send HL7 and get response back') + let waitAck: number = 0 + + let server: Server + let listener: ServerListener + + let client: Client + let outGoing: Listener + + beforeEach(async () => { + + const LISTEN_PORT = await portfinder.getPortPromise({ + port: 3000, + stopPort: 65353 + }) + + server = new Server({bindAddress: 'localhost'}) + listener = server.createListener({port: LISTEN_PORT}, async () => {}) + + client = new Client({hostname: 'localhost'}) + outGoing = client.connectToListener({port: LISTEN_PORT, waitAck: waitAck !== 2}, async () => {}) + + }) + + afterEach(async () => { + await outGoing.close() + await listener.close() + + waitAck = waitAck + 1; + }) + + test('...send simple message, just to make sure it sends, no data checks', async () => { + + let message = new Message({ + messageHeader: { + msh_9: { + msh_9_1: "ADT", + msh_9_2: "A01" + }, + msh_10: 'CONTROL_ID' + } + }) + + await outGoing.sendMessage(message) + + }) + + test('...send simple message twice, fails because no ACK of the first', async () => { + + try { + let message = new Message({ + messageHeader: { + msh_9: { + msh_9_1: "ADT", + msh_9_2: "A01" + }, + msh_10: 'CONTROL_ID' + } + }) + + await outGoing.sendMessage(message) + await outGoing.sendMessage(message) + + } catch (err: any) { + expect(err.message).toBe(`Can't send message while we are waiting for a response.`) + } + + }) + + // Note: For this test to pass, you must run it from the describe block! + test('...send simple message twice, no ACK needed', async () => { + + let message = new Message({ + messageHeader: { + msh_9: { + msh_9_1: "ADT", + msh_9_2: "A01" + }, + msh_10: 'CONTROL_ID' + } + }) + + await outGoing.sendMessage(message) + + message = new Message({ + messageHeader: { + msh_9: { + msh_9_1: "ADT", + msh_9_2: "A01" + }, + msh_10: 'CONTROL_ID' + } + }) + + await outGoing.sendMessage(message) + + }) }) diff --git a/src/builder/batch.ts b/src/builder/batch.ts index 53db01d..26f98b7 100644 --- a/src/builder/batch.ts +++ b/src/builder/batch.ts @@ -1,9 +1,6 @@ import * as Util from '../utils' import { HL7FatalError, HL7ParserError } from '../utils/exception.js' -import { - ClientBuilderBatchOptions, - normalizedClientBatchBuilderOptions -} from '../utils/normalize.js' +import { ClientBuilderBatchOptions, normalizedClientBatchBuilderOptions } from '../utils/normalizedBuilder.js' import { Node } from './interface/node.js' import { Message } from './message.js' import { RootBase } from './modules/rootBase.js' diff --git a/src/builder/message.ts b/src/builder/message.ts index e017b12..c67186d 100644 --- a/src/builder/message.ts +++ b/src/builder/message.ts @@ -1,9 +1,9 @@ import { HL7FatalError } from '../utils/exception.js' +import { ClientBuilderMessageOptions, normalizedClientMessageBuilderOptions } from '../utils/normalizedBuilder.js' import { RootBase } from './modules/rootBase.js' import { Segment } from './modules/segment.js' import { SegmentList } from './modules/segmentList.js' import * as Util from '../utils' -import { ClientBuilderMessageOptions, normalizedClientMessageBuilderOptions } from '../utils/normalize.js' import { Node } from './interface/node.js' /** diff --git a/src/builder/modules/rootBase.ts b/src/builder/modules/rootBase.ts index f3ff2f8..f8c027d 100644 --- a/src/builder/modules/rootBase.ts +++ b/src/builder/modules/rootBase.ts @@ -1,5 +1,6 @@ import { HL7FatalError } from '../../utils/exception.js' -import { ClientBuilderOptions } from '../../utils/normalize.js' + +import { ClientBuilderOptions } from '../../utils/normalizedBuilder' import { Delimiters } from '../decorators/delimiters.js' import { NodeBase } from './nodeBase.js' import * as Util from '../../utils' diff --git a/src/client/client.ts b/src/client/client.ts index 2cec39a..a4c8199 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,6 +1,6 @@ import EventEmitter from 'events' import { Listener } from './listener.js' -import { normalizeClientOptions, ClientListenerOptions, ClientOptions } from '../utils/normalize.js' +import { normalizeClientOptions, ClientListenerOptions, ClientOptions } from '../utils/normalizeClient.js' /** * Client Class diff --git a/src/client/listener.ts b/src/client/listener.ts index 73b88c8..1e9b62b 100644 --- a/src/client/listener.ts +++ b/src/client/listener.ts @@ -2,16 +2,18 @@ import EventEmitter from 'events' import { Socket } from 'net' import * as net from 'net' import * as tls from 'tls' -import {Batch} from "../builder/batch.js"; -import {Message} from "../builder/message.js"; -import {CR, FS, VT} from "../utils/constants.js"; -import {HL7FatalError} from "../utils/exception"; +import { Batch } from '../builder/batch.js' +import { Message } from '../builder/message.js' +import { CR, FS, VT } from '../utils/constants.js' +import { HL7FatalError } from '../utils/exception' import { Client } from './client.js' -import { ClientListenerOptions, normalizeClientListenerOptions } from '../utils/normalize.js' +import { ClientListenerOptions, normalizeClientListenerOptions } from '../utils/normalizeClient.js' /** Listener Class * @since 1.0.0 */ export class Listener extends EventEmitter { + /** @internal */ + _awaitingResponse: boolean /** @internal */ _handler?: any | undefined /** @internal */ @@ -28,6 +30,7 @@ export class Listener extends EventEmitter { constructor (client: Client, props: ClientListenerOptions, handler?: any) { super() this._main = client + this._awaitingResponse = false // process listener options this._opt = normalizeClientListenerOptions(props) @@ -43,41 +46,61 @@ export class Listener extends EventEmitter { * @since 1.0.0 */ async sendMessage (message: Message | Batch): Promise { - this._server?.write(`${VT}${message.toString()}${FS}${CR}`) - } + // if we are waiting for an ack before we can send something else, and we are in that process. + if (this._opt.waitAck && this._awaitingResponse) { + throw new HL7FatalError(500, 'Can\'t send message while we are waiting for a response.') + } + + if (typeof this._socket !== 'undefined' && this._socket.destroyed) { + // if we have auto connection and retry, this might take a while to fire. + throw new HL7FatalError(500, 'The socket/connect has already been destroyed. Please reconnect.') + } + + if (typeof this._socket === 'undefined') { + throw new HL7FatalError(500, 'There is no valid connection.') + } + + // ok, if our options are to wait for an acknowledgement, set the var to "true" + if (this._opt.waitAck) { + this._awaitingResponse = true + } + const toSendData = message.toString() + + this._server?.write(`${VT}${toSendData}${FS}${CR}`) + } /** @internal */ private _connect (): Socket | tls.TLSSocket { let server: Socket | tls.TLSSocket if (this._main._opt.tls != null) { - server = tls.connect({port: this._opt.port}) + server = tls.connect({ port: this._opt.port }) } else { - server = net.createConnection({host: this._main._opt.hostname, port: this._opt.port}, () => { - this._lastUsed = new Date(); - server.setNoDelay(true); + server = net.createConnection({ host: this._main._opt.hostname, port: this._opt.port }, () => { + this._lastUsed = new Date() + server.setNoDelay(true) this._socket = server - this.emit('connect', server); + this.emit('connect', server) }) } server.on('close', () => { - this.emit('close'); - }); + this.emit('close') + }) server.on('data', data => { this.emit('data', data) - }); + }) server.on('error', err => { - this.emit('error', err); + this.emit('error', err) throw new HL7FatalError(500, 'Unable to connect to remote host.') - }); + }) server.on('end', () => { - this.emit('end'); - }); + this.emit('end') + }) server.unref() @@ -90,9 +113,8 @@ export class Listener extends EventEmitter { if (typeof this._socket !== 'undefined') { this._socket.end() this._socket.destroy() - this.emit('client.close'); + this.emit('client.close') } return true } - } diff --git a/src/index.ts b/src/index.ts index 164de5a..5720e7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,5 +24,6 @@ export type { HL7_2_7_MSH } from './specification/2.7.js' /** HL7 Class **/ export { HL7_SPEC, HL7_SPEC_BASE, HL7_2_7 } -export type { ClientOptions, ClientListenerOptions, ClientBuilderOptions, ClientBuilderBatchOptions, ClientBuilderFileOptions, ParserProcessRawData } from './utils/normalize.js' +export type { ClientOptions, ClientListenerOptions, ParserProcessRawData } from './utils/normalizeClient.js' +export type { ClientBuilderFileOptions, ClientBuilderBatchOptions, ClientBuilderOptions } from './utils/normalizedBuilder.js' export type { HL7Error, HL7FatalError, HL7ParserError } from './utils/exception.js' diff --git a/src/utils/normalizeClient.ts b/src/utils/normalizeClient.ts new file mode 100644 index 0000000..48078a7 --- /dev/null +++ b/src/utils/normalizeClient.ts @@ -0,0 +1,137 @@ +import { TcpSocketConnectOpts } from 'node:net' +import type { ConnectionOptions as TLSOptions } from 'node:tls' +import * as Util from './index.js' + +const DEFAULT_CLIENT_OPTS = { + acquireTimeout: 20000, + connectionTimeout: 10000, + waitAck: true +} + +export interface ParserProcessRawData { + /** Data that needs to be processed. */ + data: string +} + +export interface ClientOptions { + /** Milliseconds to wait before aborting a connection attempt + * @default 20_000 */ + acquireTimeout?: number + /** Max wait time, in milliseconds, for a connection attempt + * @default 10_000 */ + connectionTimeout?: number + /** Hostname - You can do a FQDN or the IPv(4|6) address. */ + hostname: string + /** IPv4 - If this is set to true, only IPv4 address will be used and also validated upon installation from the hostname property. + * @default false */ + ipv4?: boolean + /** IPv6 - If this is set to true, only IPv6 address will be used and also validated upon installation from the hostname property. + * @default false */ + ipv6?: boolean + /** Keep the connection alive after sending data and getting a response. + * @default true */ + keepAlive?: boolean + /** Additional options when creating the TCP socket with net.connect(). */ + socket?: TcpSocketConnectOpts + /** Enable TLS, or set TLS specific options like overriding the CA for + * self-signed certificates. */ + tls?: boolean | TLSOptions +} + +export interface ClientListenerOptions { + /** Milliseconds to wait before aborting a connection attempt. + * This will override the overall client connection for this particular connection. + * @default 20_000 */ + acquireTimeout?: number + /** Max wait time, in milliseconds, for a connection attempt. + * This will override the overall client connection for this particular connection. + * @default 10_000 */ + connectionTimeout?: number + /** Keep the connection alive after sending data and getting a response. + * @default true */ + keepAlive?: boolean + /** Additional options when creating the TCP socket with net.connect(). */ + socket?: TcpSocketConnectOpts + /** The port we should connect on the server. */ + port: number + /** Wait for ACK **/ + waitAck?: boolean +} + +type ValidatedClientKeys = + | 'acquireTimeout' + | 'connectionTimeout' + | 'hostname' + +type ValidatedClientListenerKeys = + | 'port' + +interface ValidatedClientOptions extends Pick, ValidatedClientKeys> { + hostname: string + socket?: TcpSocketConnectOpts + tls?: TLSOptions +} + +interface ValidatedClientOptions extends Pick, ValidatedClientKeys> { + hostname: string + socket?: TcpSocketConnectOpts + tls?: TLSOptions +} + +interface ValidatedClientListenerOptions extends Pick, ValidatedClientListenerKeys> { + port: number + waitAck: boolean +} + +/** @internal */ +export function normalizeClientOptions (raw?: ClientOptions): ValidatedClientOptions { + const props: any = { ...DEFAULT_CLIENT_OPTS, ...raw } + + if (typeof props.hostname === 'undefined' || props.hostname.length <= 0) { + throw new Error('hostname is not defined or the length is less than 0.') + } + + if (props.ipv4 === true && props.ipv6 === true) { + throw new Error('ipv4 and ipv6 both can\'t be set to be both used exclusively.') + } + + if (typeof props.hostname !== 'string' && props.ipv4 === false && props.ipv6 === false) { + throw new Error('hostname is not valid string.') + } else if (typeof props.hostname === 'string' && props.ipv4 === true && props.ipv6 === false) { + if (!Util.validIPv4(props.hostname)) { + throw new Error('hostname is not a valid IPv4 address.') + } + } else if (typeof props.hostname === 'string' && props.ipv4 === false && props.ipv6 === true) { + if (!Util.validIPv6(props.hostname)) { + throw new Error('hostname is not a valid IPv6 address.') + } + } + + Util.assertNumber(props, 'acquireTimeout', 0) + Util.assertNumber(props, 'connectionTimeout', 0) + + if (props.tls === true) { + props.tls = {} + } + + return props +} + +/** @internal */ +export function normalizeClientListenerOptions (raw?: ClientListenerOptions): ValidatedClientListenerOptions { + const props: any = { ...DEFAULT_CLIENT_OPTS, ...raw } + + if (typeof props.port === 'undefined') { + throw new Error('port is not defined.') + } + + if (typeof props.port !== 'number') { + throw new Error('port is not valid number.') + } + + Util.assertNumber(props, 'acquireTimeout', 0) + Util.assertNumber(props, 'connectionTimeout', 0) + Util.assertNumber(props, 'port', 0, 65353) + + return props +} diff --git a/src/utils/normalize.ts b/src/utils/normalizedBuilder.ts similarity index 54% rename from src/utils/normalize.ts rename to src/utils/normalizedBuilder.ts index 3455c8d..cb69e92 100644 --- a/src/utils/normalize.ts +++ b/src/utils/normalizedBuilder.ts @@ -1,14 +1,6 @@ -import { TcpSocketConnectOpts } from 'node:net' -import type { ConnectionOptions as TLSOptions } from 'node:tls' -import { HL7_2_7 } from '../specification/2.7.js' -import { BSH, MSH } from '../specification/specification.js' -import * as Util from './index.js' -import { ParserPlan } from './parserPlan.js' - -const DEFAULT_CLIENT_OPTS = { - acquireTimeout: 20000, - connectionTimeout: 10000 -} +import { HL7_2_7 } from '../specification/2.7' +import { BSH, MSH } from '../specification/specification' +import { ParserPlan } from './parserPlan' const DEFAULT_CLIENT_BUILDER_OPTS = { newLine: '\r', @@ -22,54 +14,6 @@ const DEFAULT_CLIENT_BUILDER_OPTS = { text: '' } -export interface ParserProcessRawData { - /** Data that needs to be processed. */ - data: string -} - -export interface ClientOptions { - /** Milliseconds to wait before aborting a connection attempt - * @default 20_000 */ - acquireTimeout?: number - /** Max wait time, in milliseconds, for a connection attempt - * @default 10_000 */ - connectionTimeout?: number - /** Hostname - You can do a FQDN or the IPv(4|6) address. */ - hostname: string - /** IPv4 - If this is set to true, only IPv4 address will be used and also validated upon installation from the hostname property. - * @default false */ - ipv4?: boolean - /** IPv6 - If this is set to true, only IPv6 address will be used and also validated upon installation from the hostname property. - * @default false */ - ipv6?: boolean - /** Keep the connection alive after sending data and getting a response. - * @default true */ - keepAlive?: boolean - /** Additional options when creating the TCP socket with net.connect(). */ - socket?: TcpSocketConnectOpts - /** Enable TLS, or set TLS specific options like overriding the CA for - * self-signed certificates. */ - tls?: boolean | TLSOptions -} - -export interface ClientListenerOptions { - /** Milliseconds to wait before aborting a connection attempt. - * This will override the overall client connection for this particular connection. - * @default 20_000 */ - acquireTimeout?: number - /** Max wait time, in milliseconds, for a connection attempt. - * This will override the overall client connection for this particular connection. - * @default 10_000 */ - connectionTimeout?: number - /** Keep the connection alive after sending data and getting a response. - * @default true */ - keepAlive?: boolean - /** Additional options when creating the TCP socket with net.connect(). */ - socket?: TcpSocketConnectOpts - /** The port we should connect on the server. */ - port: number -} - /** * Client Builder Options * @description Used to specific default paramaters around building an HL7 message if that is @@ -139,64 +83,6 @@ export interface ClientBuilderFileOptions extends ClientBuilderOptions { fileHeader?: any } -type ValidatedClientKeys = - | 'acquireTimeout' - | 'connectionTimeout' - | 'hostname' - -type ValidatedClientListenerKeys = - | 'port' - -interface ValidatedClientOptions extends Pick, ValidatedClientKeys> { - hostname: string - socket?: TcpSocketConnectOpts - tls?: TLSOptions -} - -interface ValidatedClientOptions extends Pick, ValidatedClientKeys> { - hostname: string - socket?: TcpSocketConnectOpts - tls?: TLSOptions -} - -interface ValidatedClientListenerOptions extends Pick, ValidatedClientListenerKeys> { - port: number -} - -/** @internal */ -export function normalizeClientOptions (raw?: ClientOptions): ValidatedClientOptions { - const props: any = { ...DEFAULT_CLIENT_OPTS, ...raw } - - if (typeof props.hostname === 'undefined' || props.hostname.length <= 0) { - throw new Error('hostname is not defined or the length is less than 0.') - } - - if (props.ipv4 === true && props.ipv6 === true) { - throw new Error('ipv4 and ipv6 both can\'t be set to be both used exclusively.') - } - - if (typeof props.hostname !== 'string' && props.ipv4 === false && props.ipv6 === false) { - throw new Error('hostname is not valid string.') - } else if (typeof props.hostname === 'string' && props.ipv4 === true && props.ipv6 === false) { - if (!Util.validIPv4(props.hostname)) { - throw new Error('hostname is not a valid IPv4 address.') - } - } else if (typeof props.hostname === 'string' && props.ipv4 === false && props.ipv6 === true) { - if (!Util.validIPv6(props.hostname)) { - throw new Error('hostname is not a valid IPv6 address.') - } - } - - Util.assertNumber(props, 'acquireTimeout', 0) - Util.assertNumber(props, 'connectionTimeout', 0) - - if (props.tls === true) { - props.tls = {} - } - - return props -} - export function normalizedClientMessageBuilderOptions (raw?: ClientBuilderMessageOptions): ClientBuilderMessageOptions { const props: ClientBuilderMessageOptions = { ...DEFAULT_CLIENT_BUILDER_OPTS, ...raw } @@ -270,22 +156,3 @@ export function normalizedClientFileBuilderOptions (raw?: ClientBuilderFileOptio return props } - -/** @internal */ -export function normalizeClientListenerOptions (raw?: ClientListenerOptions): ValidatedClientListenerOptions { - const props: any = { ...DEFAULT_CLIENT_OPTS, ...raw } - - if (typeof props.port === 'undefined') { - throw new Error('port is not defined.') - } - - if (typeof props.port !== 'number') { - throw new Error('port is not valid number.') - } - - Util.assertNumber(props, 'acquireTimeout', 0) - Util.assertNumber(props, 'connectionTimeout', 0) - Util.assertNumber(props, 'port', 0, 65353) - - return props -}