Conversation
Co-authored-by: Loksly <1235649+Loksly@users.noreply.github.com>
Co-authored-by: Loksly <1235649+Loksly@users.noreply.github.com>
Co-authored-by: Loksly <1235649+Loksly@users.noreply.github.com>
Co-authored-by: Loksly <1235649+Loksly@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This pull request adds MLLP (Minimal Lower Layer Protocol) transport layer support to nodehl7, enabling real-time HL7 message exchange over TCP/IP with optional TLS encryption. The implementation includes framing functions for wrapping/unwrapping messages with MLLP delimiters, an event-driven MLLPServer for handling incoming connections, and a promise-based MLLPClient for sending messages.
Changes:
- Added MLLP framing implementation with
wrap()andunwrap()functions supporting VT start byte and FS+CR end markers - Implemented MLLPServer and MLLPClient classes with optional TLS/SSL support
- Added comprehensive test suite with 14 tests covering framing, fragmentation, concurrency, event-based API, and TLS scenarios
Reviewed changes
Copilot reviewed 4 out of 20 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/mllp.ts | New MLLP module implementing framing functions, MLLPServer, and MLLPClient with TLS support |
| src/hl7.ts | Updated to re-export MLLP classes and functions for both CommonJS and ES module consumers |
| test/testMllp.js | Comprehensive test suite covering MLLP framing, server/client functionality, and TLS encryption |
| package.json | Version bump to 1.1.0 (note: discrepancy with PR description stating 0.3.0) |
| dist/* | Compiled JavaScript and TypeScript definition files for both CommonJS and ES modules |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| "name": "nodehl7", | ||
| "version": "0.2.6", | ||
| "version": "1.1.0", |
There was a problem hiding this comment.
The PR description states "Version bump to 0.3.0" but the actual package.json shows version 1.1.0. This is a significant discrepancy that needs to be resolved. Either the description needs to be updated or the version in package.json should match the description.
|
|
||
| const onData = (chunk: Buffer): void => { | ||
| buffer = Buffer.concat([buffer, chunk]); | ||
| const { messages } = unwrap(buffer); | ||
| if (messages.length > 0) { | ||
| socket.removeListener('data', onData); | ||
| socket.removeListener('error', onError); | ||
| resolve(messages[0]); | ||
| } | ||
| }; | ||
|
|
||
| const onError = (err: Error): void => { | ||
| socket.removeListener('data', onData); | ||
| reject(err); | ||
| }; | ||
|
|
||
| socket.on('data', onData); | ||
| socket.on('error', onError); |
There was a problem hiding this comment.
The MLLPClient's send() method doesn't clean up event listeners if the buffer never receives a complete message. If the server sends partial data but never completes the message, the onData and onError listeners will remain attached to the socket indefinitely, causing a memory leak. Consider adding a timeout mechanism or ensuring cleanup happens when the connection closes.
| const onData = (chunk: Buffer): void => { | |
| buffer = Buffer.concat([buffer, chunk]); | |
| const { messages } = unwrap(buffer); | |
| if (messages.length > 0) { | |
| socket.removeListener('data', onData); | |
| socket.removeListener('error', onError); | |
| resolve(messages[0]); | |
| } | |
| }; | |
| const onError = (err: Error): void => { | |
| socket.removeListener('data', onData); | |
| reject(err); | |
| }; | |
| socket.on('data', onData); | |
| socket.on('error', onError); | |
| let settled = false; | |
| let timeout: NodeJS.Timeout | null = null; | |
| const cleanup = (): void => { | |
| if (timeout) { | |
| clearTimeout(timeout); | |
| timeout = null; | |
| } | |
| socket.removeListener('data', onData); | |
| socket.removeListener('error', onError); | |
| socket.removeListener('close', onClose); | |
| }; | |
| const settleResolve = (message: Buffer): void => { | |
| if (settled) { | |
| return; | |
| } | |
| settled = true; | |
| cleanup(); | |
| resolve(message); | |
| }; | |
| const settleReject = (err: Error): void => { | |
| if (settled) { | |
| return; | |
| } | |
| settled = true; | |
| cleanup(); | |
| reject(err); | |
| }; | |
| const onData = (chunk: Buffer): void => { | |
| buffer = Buffer.concat([buffer, chunk]); | |
| const { messages } = unwrap(buffer); | |
| if (messages.length > 0) { | |
| settleResolve(messages[0]); | |
| } | |
| }; | |
| const onError = (err: Error): void => { | |
| settleReject(err); | |
| }; | |
| const onClose = (): void => { | |
| if (!settled) { | |
| settleReject(new Error('Connection closed before complete MLLP response was received')); | |
| } | |
| }; | |
| // Set a timeout to avoid hanging indefinitely if a complete message is never received. | |
| timeout = setTimeout(() => { | |
| if (!settled) { | |
| settleReject(new Error('Timed out waiting for MLLP response')); | |
| } | |
| }, 30000); | |
| socket.on('data', onData); | |
| socket.on('error', onError); | |
| socket.on('close', onClose); |
| } | ||
|
|
||
| socket.on('error', (err: Error) => { | ||
| this._connected = false; |
There was a problem hiding this comment.
The MLLPClient reuses sockets via the _connect() method's check on line 202. However, after a socket encounters an error (line 222-226), the _connected flag is set to false but the _socket reference might not be null if the error occurs before the 'close' event fires. This could lead to attempting to reuse a socket in an error state. Consider also setting _socket to null in the error handler.
| this._connected = false; | |
| this._connected = false; | |
| this._socket = null; |
| before(function () { | ||
| // Generate self-signed certificate for testing | ||
| var keys = crypto.generateKeyPairSync('rsa', { | ||
| modulusLength: 2048, | ||
| publicKeyEncoding: { type: 'spki', format: 'pem' }, | ||
| privateKeyEncoding: { type: 'pkcs8', format: 'pem' } | ||
| }); | ||
|
|
||
| var execSync = require('child_process').execSync; | ||
| var fs = require('fs'); | ||
| var tmpDir = require('os').tmpdir(); | ||
| var keyPath = path.join(tmpDir, 'mllp-test-key.pem'); | ||
| var certPath = path.join(tmpDir, 'mllp-test-cert.pem'); | ||
| fs.writeFileSync(keyPath, keys.privateKey); | ||
| execSync('openssl req -new -x509 -key ' + keyPath + ' -out ' + certPath + ' -days 1 -subj "/CN=localhost" 2>/dev/null'); | ||
| tlsCerts = { | ||
| key: keys.privateKey, | ||
| cert: fs.readFileSync(certPath, 'utf8') | ||
| }; |
There was a problem hiding this comment.
The TLS certificate generation test setup relies on the openssl command-line tool being available in the PATH (line 256). This makes the tests platform-dependent and they will fail on systems without OpenSSL installed or in CI environments that don't have it. Consider using a pure Node.js solution like the node-forge library or the built-in crypto.X509Certificate API to generate self-signed certificates, or include pre-generated test certificates in the repository.
| const mllp = require('./mllp'); | ||
| module.exports = hl7Parser; | ||
| module.exports.MLLPServer = mllp.MLLPServer; | ||
| module.exports.MLLPClient = mllp.MLLPClient; | ||
| module.exports.mllpWrap = mllp.wrap; | ||
| module.exports.mllpUnwrap = mllp.unwrap; |
There was a problem hiding this comment.
The CommonJS export approach uses require('./mllp') in an ES module context (line 497). While this may work in some build configurations, it's mixing module systems and could cause issues in strict ESM environments or with certain bundlers. Consider using a dynamic import or ensuring the build process handles this correctly. A cleaner approach would be to import the MLLP exports at the top of the file and re-export them consistently.
|
|
||
| socket.on('error', (err: Error) => { | ||
| this.emit('error', err); | ||
| }); |
There was a problem hiding this comment.
The MLLPServer doesn't handle the socket 'close' event. When a client disconnects unexpectedly, the server should clean up the per-socket buffer to prevent memory leaks. Consider adding a 'close' event handler in the connectionHandler to properly clean up resources.
| }); | |
| }); | |
| socket.on('close', () => { | |
| // Clear any remaining per-socket buffer data on disconnect. | |
| buffer = Buffer.alloc(0); | |
| }); |
| { | ||
| "name": "nodehl7", | ||
| "version": "0.2.6", | ||
| "version": "1.1.0", |
There was a problem hiding this comment.
The version bump from 0.2.6 to 1.1.0 is inconsistent with semantic versioning. According to semver, this should be either 0.3.0 (as mentioned in the PR description) or 1.0.0 if it's considered a major release. The version 1.1.0 suggests this is the second minor release after 1.0.0, which hasn't occurred yet. The PR description states "Version bump to 0.3.0" but the actual version in package.json is 1.1.0, creating a discrepancy.
| "version": "1.1.0", | |
| "version": "0.3.0", |
Implements the MLLP (Minimal Lower Layer Protocol) transport layer for nodehl7, enabling real-time HL7 message exchange over TCP/IP with optional TLS encryption.
MLLP framing (
src/mllp.ts)wrap(data)/unwrap(buffer)— MLLP framing using VT (0x0B) start and FS+CR (0x1C 0x0D) end markers, with correct handling of fragmented streams and multiple messages per packetMLLPServer
net.Serverortls.Serverdepending on optionshl7_messageevents; also accepts a handler callback in constructor{ tls: { key, cert } }optionsMLLPClient
send()returning the server's response{ tls: { rejectUnauthorized, ca } }optionsUsage
Other
nodehl7entry point (CJS + ESM)0.3.0🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.