diff --git a/README.md b/README.md deleted file mode 100644 index eb6274f..0000000 --- a/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# 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. Copy the .env.example files to .env files -3. Execute `bun install` and `bun run dev` in the root directory -4. 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`. diff --git a/packages/client/.env.example b/packages/client/.env.example deleted file mode 100644 index 88f609e..0000000 --- a/packages/client/.env.example +++ /dev/null @@ -1,2 +0,0 @@ - -PUBLIC_SERVER=localhost:8080 diff --git a/packages/client/src/routes/+page.svelte b/packages/client/src/routes/+page.svelte index 585b194..bb06a88 100644 --- a/packages/client/src/routes/+page.svelte +++ b/packages/client/src/routes/+page.svelte @@ -16,16 +16,15 @@ import { Label } from '$lib/components/ui/dropdown-menu'; import { env } from '$env/dynamic/public'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '$lib/components/ui/tooltip'; - import { dev } from '$app/environment'; // connection state - let ws = $state(undefined); // websocket client - let stationId = $state(undefined); // stationId assigned by server + let ws = $state(undefined); + let userId = $state(undefined); let connectionState = $state(ConnectionState.disconnected); // client state - let composedMessage = $state(''); // content of text-box - let sentMessages = $state([]); // sent messages stored as type ReceiveHl7v2Message because of the timestamp + let composedMessage = $state(''); + let sentMessages = $state([]); // storing sent messages with client-timestamp for now let receivedMessages = $state([]); let isSending = $state(false); let copySuccess = $state(false); @@ -60,33 +59,28 @@ }; const segmentTypes = Object.keys(segmentTemplates) as Array; - // handles the connection process and message handling function connectToServer() { console.log('Connecting to server...'); connectionState = ConnectionState.connecting; - const socket = new WebSocket(`${dev ? 'ws' : 'wss'}://${env.PUBLIC_SERVER}`); + const socket = new WebSocket(`wss://${env.PUBLIC_SERVER}`); - // store websocket on successful connection socket.onopen = () => { console.log('WebSocket connection established.'); ws = socket; }; - // register message handlers socket.onmessage = (event) => { - // parse every message as type Message const message = JSON.parse(event.data) as Message; console.log('Message received from server:', message); - // react based on message type switch (message.type) { // initial message from server assigning ID case MessageType.assign_id: - stationId = message.payload.stationId; + userId = message.payload.userId; composedMessage = segmentTemplates.MSH.template(); connectionState = ConnectionState.connected; break; @@ -104,14 +98,12 @@ } }; - // reset connection state and websocket client on disconnect socket.onclose = () => { console.log('WebSocket connection closed.'); connectionState = ConnectionState.disconnected; ws = undefined; }; - // reset connection state and websocket client on error socket.onerror = (error) => { console.error('WebSocket error:', error); connectionState = ConnectionState.disconnected; @@ -119,7 +111,7 @@ }; } - // reset connection state and websocket client on tab closed + // clean up on close $effect(() => { return () => { if (ws && ws.readyState === WebSocket.OPEN) { @@ -129,26 +121,21 @@ }; }); - // adds the template of the provided segment type at the bottom of the textbox function addSegment(type: keyof typeof segmentTemplates) { const template = segmentTemplates[type].template(); composedMessage += `\r\n${template}`; } - // handles sending a message to the websocket server function handleSendMessage(message: string) { - // check for active connection - if (!ws || ws.readyState !== WebSocket.OPEN || isSending || !stationId) { + if (!ws || ws.readyState !== WebSocket.OPEN || isSending || !userId) { console.log('Socket not ready'); return; } - // set UI state isSending = true; deliveryError = ''; - // construct Message object and send it const messageToSend = { type: 'send_hl7v2', payload: { message }, @@ -160,15 +147,12 @@ payload: { message, timestamp: new Date().toISOString() }, } as ReceiveHl7v2Message, ...sentMessages]; composedMessage = segmentTemplates.MSH.template(); - - // reset UI state isSending = false; } - // copies the stationId to the clipboard and handles UI state - function copyStationId() { - if (stationId) { - navigator.clipboard.writeText(stationId).then(() => { + function copyUserId() { + if (userId) { + navigator.clipboard.writeText(userId).then(() => { copySuccess = true; setTimeout(() => copySuccess = false, 2000); }); @@ -178,7 +162,6 @@ - {#snippet message(msg: ReceiveHl7v2Message)}

{new Date(msg.payload.timestamp).toLocaleString()}

@@ -191,7 +174,7 @@
- +
@@ -199,11 +182,11 @@ Your Info - {#if connectionState === ConnectionState.connected && stationId} + {#if connectionState === ConnectionState.connected && userId}
- -
-
+ \ No newline at end of file diff --git a/packages/server/.env.example b/packages/server/.env.example deleted file mode 100644 index 4880b4f..0000000 --- a/packages/server/.env.example +++ /dev/null @@ -1,4 +0,0 @@ - -PORT=8080 -PREFIXES=STA,LAB -POOL_SIZE=100 diff --git a/packages/server/config.ts b/packages/server/config.ts index 53ab614..006b447 100644 --- a/packages/server/config.ts +++ b/packages/server/config.ts @@ -1,25 +1,23 @@ - -// 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 pool size environment variable (poolSize >= prefix.length)'); + throw new Error('Invalid PORT environment variable (poolSize >= prefix.length)'); } diff --git a/packages/server/index.ts b/packages/server/index.ts index acbf484..55ccd71 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -17,7 +17,6 @@ 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)); @@ -50,41 +49,40 @@ wss.on('connection', (ws) => { return; } - // get ID from pool and assign to station - const stationId = availableIds.pop(); - if (!stationId) { + // get ID from pool and assign to user + const userId = availableIds.pop(); + if (!userId) { console.log('Connection rejected: Failed to retrieve ID.'); ws.close(1013, 'Server is full. Please try again later.'); // 1013: Try again later return; } // store client in map - clients.set(stationId, ws); + clients.set(userId, ws); - // send station ID to client - const welcomeMessage = { - type: 'assign_id', - payload: { - stationId: stationId, - }, - } as Message; - ws.send(JSON.stringify(welcomeMessage)); + setTimeout(() => { + // send user ID to client + const welcomeMessage = { + type: 'assign_id', + payload: { + userId: userId, + }, + } as Message; + ws.send(JSON.stringify(welcomeMessage)); - console.log(`Client connected. Assigning ID: ${stationId}`); + console.log(`Client connected. Assigning ID: ${userId}`); + }, 0); - // 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); + console.log(`Received message from ${userId}:`, parsedMessage); // We only expect one type of message from clients: 'send_hl7v2' if (parsedMessage.type === MessageType.send_hl7v2) { const { message } = parsedMessage.payload; - // TODO: validate message // get sender and recipient ID @@ -102,7 +100,6 @@ 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. @@ -130,23 +127,22 @@ wss.on('connection', (ws) => { } } } catch (error) { - console.error(`Failed to process message from ${stationId}:`, error); + console.error(`Failed to process message from ${userId}:`, error); } }); // listen to disconnects to free IDs ws.on('close', () => { - console.log(`Client ${stationId} disconnected.`); - clients.delete(stationId); - if (stationId) { - availableIds.push(stationId); + console.log(`Client ${userId} disconnected.`); + clients.delete(userId); + if (userId) { + availableIds.push(userId); } - // shake IDs to avoid getting the same ID reassigned availableIds = shake(availableIds); }); ws.on('error', (error) => { - console.error(`WebSocket error for client ${stationId}:`, error); + console.error(`WebSocket error for client ${userId}:`, error); }); }); diff --git a/packages/shared/types.ts b/packages/shared/types.ts index 12a45fb..94788e4 100644 --- a/packages/shared/types.ts +++ b/packages/shared/types.ts @@ -1,12 +1,9 @@ - -// 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', @@ -14,12 +11,10 @@ export enum MessageType { delivery_error = 'delivery_error', } -// defines body of message types export type Message = - | { type: MessageType.assign_id, payload: { stationId: string }} + | { type: MessageType.assign_id, payload: { userId: 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;