From 3e00cf88e7a76d3fca74f69bf125211c47526046 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Wed, 30 Jul 2025 12:29:58 +0200 Subject: [PATCH 1/5] HL7-1: add README.md --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..06185d2 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# HL7v2 + +HL7v2 is a medical communication protocol. This application tries to be a hands-on method for students to get to know +the communication foundation of almost every medical institution, typically hidden behind software forms. + +## How it works + +The application is web-based and features an individual number of clients. Every client has an input to construct a +raw HL7v2 message, build of the message header and an optional number of segments. Each editor has predefined segment +templates with missing values, which have to be filled in to be sent the message to a different client. + +## Architecture + +The stack features a SvelteKit application and a Bun websocket server. Communication between clients is only the +web-socket server, validating each message for a valid format. Only if the message was valid and delivered successfully +to a different client, the server sends back an acknowledgement and notifies the sending client. + +# Getting started + +1. Download the sources of the project +2. Execute `bun install` and `bun run dev` in the root directory +3. Start customizing the code to your needs + +# Deployment + +1. Download the compose.yaml file to the machine you want the application to run on. +2. Copy the sample .env file by running `cp .env.example .env` and adjust the new file to your needs. + + ```env + -- public URL of the client used by traefik + CLIENT_URL=my.app.com + + -- public URL of the server used by the client to connect and traefik + SERVER_URL=server.my.app.com + + -- the port the server listens on + PORT=8080 + + -- number of ID prefixes (no effect on pool-size) + PREFIXES=STA,LAB + + -- amount of available IDs + POOL_SIZE=100 + ``` + +3. The compose.yaml file configures Traefik routers with TLS encryption. Customize the settings as needed or just remove + them if you are using a different reverse proxy. +4. Start up the containers using `podman|docker compose up -d`. From bb3a6a5d8ead90dabe6dee8bf07e0c876d4521d7 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Wed, 30 Jul 2025 12:49:02 +0200 Subject: [PATCH 2/5] HL7-1: rename userId to stationId --- packages/client/src/routes/+page.svelte | 22 ++++++------- packages/server/index.ts | 42 ++++++++++++------------- packages/shared/types.ts | 2 +- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/client/src/routes/+page.svelte b/packages/client/src/routes/+page.svelte index bb06a88..acbb02d 100644 --- a/packages/client/src/routes/+page.svelte +++ b/packages/client/src/routes/+page.svelte @@ -19,7 +19,7 @@ // connection state let ws = $state(undefined); - let userId = $state(undefined); + let stationId = $state(undefined); // stationId assigned by server let connectionState = $state(ConnectionState.disconnected); // client state @@ -80,7 +80,7 @@ // initial message from server assigning ID case MessageType.assign_id: - userId = message.payload.userId; + stationId = message.payload.stationId; composedMessage = segmentTemplates.MSH.template(); connectionState = ConnectionState.connected; break; @@ -128,7 +128,7 @@ function handleSendMessage(message: string) { - if (!ws || ws.readyState !== WebSocket.OPEN || isSending || !userId) { + if (!ws || ws.readyState !== WebSocket.OPEN || isSending || !stationId) { console.log('Socket not ready'); return; } @@ -150,9 +150,9 @@ isSending = false; } - function copyUserId() { - if (userId) { - navigator.clipboard.writeText(userId).then(() => { + function copyStationId() { + if (stationId) { + navigator.clipboard.writeText(stationId).then(() => { copySuccess = true; setTimeout(() => copySuccess = false, 2000); }); @@ -174,7 +174,7 @@
- +
@@ -182,11 +182,11 @@ Your Info - {#if connectionState === ConnectionState.connected && userId} + {#if connectionState === ConnectionState.connected && stationId}
- -
- \ No newline at end of file + diff --git a/packages/server/config.ts b/packages/server/config.ts index 006b447..53ab614 100644 --- a/packages/server/config.ts +++ b/packages/server/config.ts @@ -1,23 +1,25 @@ + +// define available configurations interface Config { port: number; prefixes: string[]; poolSize: number; } +// construct config object from environment variables export const config: Config = { port: parseInt(process.env.PORT || '8080', 10), prefixes: (process.env.PREFIXES || 'STA').split(","), poolSize: parseInt(process.env.POOL_SIZE || '100', 10), }; +// validate config if (isNaN(config.port) || config.port < 1024 || config.port > 49151) { throw new Error('Invalid PORT environment variable (1024 - 49151)'); } - if (config.prefixes.length === 0) { throw new Error('Invalid PREFIXES environment variable (length > 1)'); } - if (isNaN(config.poolSize) || config.poolSize < config.prefixes.length) { - throw new Error('Invalid PORT environment variable (poolSize >= prefix.length)'); + throw new Error('Invalid pool size environment variable (poolSize >= prefix.length)'); } diff --git a/packages/server/index.ts b/packages/server/index.ts index b4ce411..acbf484 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -17,6 +17,7 @@ for (const prefix of config.prefixes) { } } +// mixes the array randomly to avoid getting the same ID reassigned function shake(arr: any[]) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); @@ -71,9 +72,11 @@ wss.on('connection', (ws) => { console.log(`Client connected. Assigning ID: ${stationId}`); + // listen for messages and defined message handling ws.on('message', (message) => { try { + // parse every message to type Message const parsedMessage: Message = JSON.parse(message.toString()); console.log(`Received message from ${stationId}:`, parsedMessage); @@ -81,6 +84,7 @@ wss.on('connection', (ws) => { if (parsedMessage.type === MessageType.send_hl7v2) { const { message } = parsedMessage.payload; + // TODO: validate message // get sender and recipient ID @@ -98,6 +102,7 @@ wss.on('connection', (ws) => { // Find the recipient's WebSocket connection in our map. const recipientWs = clients.get(recipientId); + // check if recipient exists and is connected if (recipientWs && recipientWs.readyState === WebSocket.OPEN) { // The recipient is connected. Forward the message. @@ -136,6 +141,7 @@ wss.on('connection', (ws) => { if (stationId) { availableIds.push(stationId); } + // shake IDs to avoid getting the same ID reassigned availableIds = shake(availableIds); }); diff --git a/packages/shared/types.ts b/packages/shared/types.ts index 15b05b4..12a45fb 100644 --- a/packages/shared/types.ts +++ b/packages/shared/types.ts @@ -1,9 +1,12 @@ + +// defines different connection states export enum ConnectionState { connecting = 'connecting', connected = 'connected', disconnected = 'disconnected', } +// defines message types export enum MessageType { assign_id = 'assign_id', send_hl7v2 = 'send_hl7v2', @@ -11,10 +14,12 @@ export enum MessageType { delivery_error = 'delivery_error', } +// defines body of message types export type Message = | { type: MessageType.assign_id, payload: { stationId: string }} | { type: MessageType.send_hl7v2, payload: { message: string }} | { type: MessageType.receive_hl7v2, payload: { message: string, timestamp: string }} | { type: MessageType.delivery_error, payload: { error: string }} +// required to define list of this message type export type ReceiveHl7v2Message = Extract;