Initial commit
This commit is contained in:
commit
d9cff2e70c
72 changed files with 2878 additions and 0 deletions
34
packages/server/.gitignore
vendored
Normal file
34
packages/server/.gitignore
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
61
packages/server/Containerfile
Normal file
61
packages/server/Containerfile
Normal file
|
@ -0,0 +1,61 @@
|
|||
# Stage 1: Builder
|
||||
# This stage installs all dependencies and builds all packages.
|
||||
FROM oven/bun:1 AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependency manifests
|
||||
COPY package.json bun.lock ./
|
||||
COPY tsconfig.base.json ./
|
||||
|
||||
# Copy package-specific manifests to leverage Docker cache
|
||||
COPY packages/shared/package.json packages/shared/tsconfig.json ./packages/shared/
|
||||
COPY packages/server/package.json packages/server/tsconfig.json ./packages/server/
|
||||
|
||||
# Install ALL workspace dependencies
|
||||
RUN bun install
|
||||
|
||||
# Copy source code
|
||||
COPY packages/shared ./packages/shared/
|
||||
COPY packages/server ./packages/server/
|
||||
|
||||
# Build the server and its dependencies (i.e., 'shared')
|
||||
RUN bun run --filter=@hnu.de/hl7v2-shared build
|
||||
RUN bun run --filter=@hnu.de/hl7v2-server build
|
||||
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Stage 2: Production
|
||||
FROM oven/bun:1-slim
|
||||
WORKDIR /app
|
||||
|
||||
# Create the full directory structure first
|
||||
RUN mkdir -p packages/shared packages/server
|
||||
|
||||
# Copy workspace configuration
|
||||
COPY --from=builder /app/package.json /app/bun.lock ./
|
||||
|
||||
# Set up shared package with its dist directory
|
||||
COPY --from=builder /app/packages/shared/package.json ./packages/shared/
|
||||
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist
|
||||
|
||||
# Set up server package with its dist directory
|
||||
COPY --from=builder /app/packages/server/package.json ./packages/server/
|
||||
COPY --from=builder /app/packages/server/dist ./packages/server/dist
|
||||
|
||||
# Install ONLY production dependencies
|
||||
ENV NODE_ENV=production
|
||||
RUN bun install \
|
||||
--frozen-lockfile \
|
||||
--production
|
||||
|
||||
# Set environment variables from config.ts
|
||||
ENV PORT=8080 \
|
||||
PREFIXES=STA \
|
||||
POOL_SIZE=100
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the server from its package directory
|
||||
WORKDIR /app/packages/server
|
||||
CMD ["bun", "run", "dist/index.js"]
|
23
packages/server/config.ts
Normal file
23
packages/server/config.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
interface Config {
|
||||
port: number;
|
||||
prefixes: string[];
|
||||
poolSize: number;
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
|
||||
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)');
|
||||
}
|
160
packages/server/index.ts
Normal file
160
packages/server/index.ts
Normal file
|
@ -0,0 +1,160 @@
|
|||
import { WebSocketServer } from 'ws';
|
||||
import { type Message, MessageType } from '@hnu.de/hl7v2-shared';
|
||||
import 'dotenv/config';
|
||||
import { config } from './config';
|
||||
|
||||
|
||||
// ###########################################################################
|
||||
// ### ID Pool
|
||||
// ###########################################################################
|
||||
|
||||
let availableIds: string[] = [];
|
||||
|
||||
for (const prefix of config.prefixes) {
|
||||
for (let i = 1; i <= config.poolSize / config.prefixes.length; i++) {
|
||||
const number = i.toString().padStart(config.poolSize.toString().length, '0');
|
||||
availableIds.push(`${prefix}-${number}`);
|
||||
}
|
||||
}
|
||||
|
||||
function shake(arr: any[]) {
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
availableIds = shake(availableIds);
|
||||
|
||||
|
||||
// ###########################################################################
|
||||
// ### Websocket Server
|
||||
// ###########################################################################
|
||||
|
||||
const wss = new WebSocketServer({ port: config.port });
|
||||
|
||||
const clients = new Map();
|
||||
|
||||
console.log(`Starting WebSocket server ...`);
|
||||
|
||||
console.log("Server configuration:", config);
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
|
||||
// check for available IDs
|
||||
if (availableIds.length === 0) {
|
||||
console.log('Connection rejected: No available IDs.');
|
||||
ws.close(1013, 'Server is full. Please try again later.'); // 1013: Try again later
|
||||
return;
|
||||
}
|
||||
|
||||
// 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(userId, ws);
|
||||
|
||||
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: ${userId}`);
|
||||
}, 0);
|
||||
|
||||
ws.on('message', (message) => {
|
||||
try {
|
||||
|
||||
const parsedMessage: Message = JSON.parse(message.toString());
|
||||
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
|
||||
const recipientId = parseMshField(message, 5);
|
||||
if (!recipientId) {
|
||||
const errorMessage = {
|
||||
type: 'delivery_error',
|
||||
payload: {
|
||||
error: `Message is missing header field 5.`,
|
||||
},
|
||||
} as Message;
|
||||
ws.send(JSON.stringify(errorMessage));
|
||||
}
|
||||
|
||||
// Find the recipient's WebSocket connection in our map.
|
||||
const recipientWs = clients.get(recipientId);
|
||||
|
||||
if (recipientWs && recipientWs.readyState === WebSocket.OPEN) {
|
||||
|
||||
// The recipient is connected. Forward the message.
|
||||
const forwardMessage = {
|
||||
type: MessageType.receive_hl7v2,
|
||||
payload: {
|
||||
message: message,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
} as Message;
|
||||
recipientWs.send(JSON.stringify(forwardMessage));
|
||||
console.log(`Forwarded message to ${recipientId}`);
|
||||
|
||||
} else {
|
||||
|
||||
// The recipient is not connected or not found.
|
||||
console.log(`Recipient ${recipientId} not found or not connected.`);
|
||||
const errorMessage = {
|
||||
type: MessageType.delivery_error,
|
||||
payload: {
|
||||
error: `Station with ID ${recipientId} is not available.`,
|
||||
},
|
||||
} as Message;
|
||||
ws.send(JSON.stringify(errorMessage));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to process message from ${userId}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// listen to disconnects to free IDs
|
||||
ws.on('close', () => {
|
||||
console.log(`Client ${userId} disconnected.`);
|
||||
clients.delete(userId);
|
||||
if (userId) {
|
||||
availableIds.push(userId);
|
||||
}
|
||||
availableIds = shake(availableIds);
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error(`WebSocket error for client ${userId}:`, error);
|
||||
});
|
||||
});
|
||||
|
||||
function parseMshField(message: string, fieldIndex: number) {
|
||||
try {
|
||||
const mshLine = message.split(/[\r\n]+/)[0];
|
||||
if (!mshLine || !mshLine.startsWith('MSH')) return null;
|
||||
const fields = mshLine.split('|');
|
||||
return fields[fieldIndex] || null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`WebSocket server started on port ${config.port}`);
|
21
packages/server/package.json
Normal file
21
packages/server/package.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@hnu.de/hl7v2-server",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "bun --watch index.ts --watch config.ts",
|
||||
"start": "bun index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/ws": "^8.18.1",
|
||||
"typescript": "^5.8.3",
|
||||
},
|
||||
"dependencies": {
|
||||
"@hnu.de/hl7v2-shared": "workspace:*",
|
||||
"dotenv": "^17.2.0",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
19
packages/server/tsconfig.json
Normal file
19
packages/server/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true
|
||||
},
|
||||
"include": [
|
||||
"*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue