@medplum/agent 2.1.17 → 2.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,14 @@ On-prem agent for device connectivity.
9
9
 
10
10
  ## Building
11
11
 
12
+ Build everything:
13
+
14
+ ```bash
15
+ npm run build:all
16
+ ```
17
+
18
+ Or, build individual components:
19
+
12
20
  Build the JS output:
13
21
 
14
22
  ```bash
@@ -18,11 +26,11 @@ npm run build
18
26
  Build the `.exe` file using [Vercel `pkg`](https://github.com/vercel/pkg):
19
27
 
20
28
  ```bash
21
- npm run package
29
+ npm run build:exe
22
30
  ```
23
31
 
24
32
  Build the installer using [NSIS](https://nsis.sourceforge.io/) (requires `makensis` on your PATH):
25
33
 
26
34
  ```bash
27
- npm run installer
35
+ npm run build:installer
28
36
  ```
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@medplum/agent",
3
- "version": "2.1.17",
3
+ "version": "2.1.19",
4
4
  "description": "Medplum Agent",
5
5
  "homepage": "https://www.medplum.com/",
6
+ "bugs": {
7
+ "url": "https://github.com/medplum/medplum/issues"
8
+ },
6
9
  "repository": {
7
10
  "type": "git",
8
11
  "url": "git+https://github.com/medplum/medplum.git",
@@ -10,12 +13,15 @@
10
13
  },
11
14
  "license": "Apache-2.0",
12
15
  "author": "Medplum <hello@medplum.com>",
16
+ "files": [
17
+ "dist"
18
+ ],
13
19
  "scripts": {
14
20
  "agent": "ts-node src/main.ts",
15
21
  "build": "npm run clean && tsc && node esbuild.mjs",
22
+ "build:all": "npm run build && npm run build:exe && npm run build:installer",
16
23
  "build:exe": "pkg ./dist/cjs/index.cjs --targets node18-win-x64 --output dist/medplum-agent-win-x64.exe --options no-warnings",
17
24
  "build:installer": "makensis installer.nsi",
18
- "build:all": "npm run build && npm run build:exe && npm run build:installer",
19
25
  "clean": "rimraf dist",
20
26
  "test": "jest"
21
27
  },
package/babel.config.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "presets": [["@babel/preset-env", { "targets": { "node": "current" } }], "@babel/preset-typescript"]
3
- }
package/esbuild.mjs DELETED
@@ -1,28 +0,0 @@
1
- /* global console */
2
- /* eslint no-console: "off" */
3
-
4
- import esbuild from 'esbuild';
5
- import { writeFileSync } from 'fs';
6
-
7
- const options = {
8
- entryPoints: ['./src/main.ts'],
9
- bundle: true,
10
- platform: 'node',
11
- loader: { '.js': 'js', '.ts': 'ts' },
12
- resolveExtensions: ['.js', '.ts'],
13
- target: 'es2021',
14
- tsconfig: 'tsconfig.json',
15
- external: ['iconv-lite', 'pdfmake'],
16
- };
17
-
18
- // The single executable application feature only supports running a single embedded CommonJS file.
19
- // https://nodejs.org/dist/latest-v18.x/docs/api/single-executable-applications.html
20
-
21
- esbuild
22
- .build({
23
- ...options,
24
- format: 'cjs',
25
- outfile: './dist/cjs/index.cjs',
26
- })
27
- .then(() => writeFileSync('./dist/cjs/package.json', '{"type": "commonjs"}'))
28
- .catch(console.error);
package/installer.nsi DELETED
@@ -1,238 +0,0 @@
1
- # Medplum Agent Installer Builder
2
- # For use with NSIS 3.0+
3
- # See: https://nsis.sourceforge.io/
4
-
5
- !define COMPANY_NAME "Medplum"
6
- !define APP_NAME "Medplum Agent"
7
- !define SERVICE_NAME "MedplumAgent"
8
- !define INSTALLER_FILE_NAME "medplum-agent-installer.exe"
9
- !define DEFAULT_BASE_URL "https://api.medplum.com/"
10
-
11
- Name "${APP_NAME}"
12
- OutFile "${INSTALLER_FILE_NAME}"
13
- VIProductVersion "1.0.0.0"
14
- VIAddVersionKey ProductName "${APP_NAME}"
15
- VIAddVersionKey Comments "${APP_NAME}"
16
- VIAddVersionKey CompanyName "${COMPANY_NAME}"
17
- VIAddVersionKey LegalCopyright "${COMPANY_NAME}"
18
- VIAddVersionKey FileDescription "${APP_NAME}"
19
- VIAddVersionKey FileVersion 1
20
- VIAddVersionKey ProductVersion 1
21
- VIAddVersionKey InternalName "${APP_NAME}"
22
- VIAddVersionKey LegalTrademarks "${COMPANY_NAME}"
23
- VIAddVersionKey OriginalFilename "${INSTALLER_FILE_NAME}"
24
-
25
- InstallDir "$PROGRAMFILES64\${APP_NAME}"
26
-
27
- !include "nsDialogs.nsh"
28
-
29
- RequestExecutionLevel admin
30
-
31
- Var WelcomeDialog
32
- Var WelcomeLabel
33
- Var alreadyInstalled
34
- Var baseUrl
35
- Var clientId
36
- Var clientSecret
37
- Var agentId
38
-
39
- # The onInit handler is called when the installer is nearly finished initializing.
40
- # See: https://nsis.sourceforge.io/Reference/.onInit
41
- Function .onInit
42
- ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Services\${SERVICE_NAME}" "ImagePath"
43
- ${If} $0 != ""
44
- StrCpy $alreadyInstalled 1
45
- ${EndIf}
46
- FunctionEnd
47
-
48
- Page custom WelcomePage
49
- Page custom InputPage InputPageLeave
50
- Page instfiles
51
-
52
- # The WelcomePage is a simple static screen that displays a friendly message.
53
- Function WelcomePage
54
- nsDialogs::Create 1018
55
- Pop $WelcomeDialog
56
-
57
- ${If} $WelcomeDialog == error
58
- Abort
59
- ${EndIf}
60
-
61
- ${NSD_CreateLabel} 0 0 100% 50u "Welcome to the ${APP_NAME} Installer!$\r$\n$\r$\nClick next to continue."
62
- Pop $WelcomeLabel
63
-
64
- nsDialogs::Show
65
- FunctionEnd
66
-
67
- # The InputPage captures all of the user input for the agent.
68
- Function InputPage
69
- ${If} $alreadyInstalled == 1
70
- Abort ; This skips the page
71
- ${EndIf}
72
-
73
- nsDialogs::Create 1018
74
- Pop $0
75
-
76
- StrCpy $baseUrl "${DEFAULT_BASE_URL}"
77
- ${NSD_CreateLabel} 0 0 30% 12u "Base URL:"
78
- Pop $R0
79
- ${NSD_CreateText} 35% 0 65% 12u $baseUrl
80
- Pop $R1
81
-
82
- ${NSD_CreateLabel} 0 15u 30% 12u "Client ID:"
83
- Pop $R2
84
- ${NSD_CreateText} 35% 15u 65% 12u $clientId
85
- Pop $R3
86
-
87
- ${NSD_CreateLabel} 0 30u 30% 12u "Client Secret:"
88
- Pop $R4
89
- ${NSD_CreateText} 35% 30u 65% 12u $clientSecret
90
- Pop $R5
91
-
92
- ${NSD_CreateLabel} 0 45u 30% 12u "Agent ID:"
93
- Pop $R6
94
- ${NSD_CreateText} 35% 45u 65% 12u $agentId
95
- Pop $R7
96
-
97
- ${NSD_SetFocus} $R3
98
- nsDialogs::Show
99
- FunctionEnd
100
-
101
- Function InputPageLeave
102
- ${NSD_GetText} $R1 $baseUrl
103
- ${NSD_GetText} $R3 $clientId
104
- ${NSD_GetText} $R5 $clientSecret
105
- ${NSD_GetText} $R7 $agentId
106
- FunctionEnd
107
-
108
- # Main installation entry point.
109
- Section
110
- DetailPrint "${APP_NAME}"
111
- SetOutPath "$INSTDIR"
112
-
113
- ${If} $alreadyInstalled == 1
114
- Call UpgradeApp
115
- ${Else}
116
- Call InstallApp
117
- ${EndIf}
118
-
119
- SectionEnd
120
-
121
- # Upgrade an existing installation.
122
- # This only copies files, and restarts the Windows Service.
123
- # It does not modify the existing configuration settings.
124
- Function UpgradeApp
125
-
126
- # Stop the service
127
- DetailPrint "Stopping service..."
128
- ExecWait "sc.exe stop ${SERVICE_NAME}" $1
129
- DetailPrint "Exit code $1"
130
-
131
- # Sleep for 3 seconds to let the service fully stop
132
- # We cannot write the new version of the exe while the process is running
133
- DetailPrint "Sleeping..."
134
- Sleep 3000
135
-
136
- # Copy the new files to the installation directory
137
- File dist\medplum-agent-win-x64.exe
138
- File README.md
139
-
140
- # Start the service
141
- DetailPrint "Starting service..."
142
- ExecWait "sc.exe start ${SERVICE_NAME}" $1
143
- DetailPrint "Start service returned $1"
144
-
145
- FunctionEnd
146
-
147
- # Do the actual installation.
148
- # Install all of the files.
149
- # Install the Windows Service.
150
- Function InstallApp
151
- # Print user input
152
- DetailPrint "Base URL: $baseUrl"
153
- DetailPrint "Client ID: $clientId"
154
- DetailPrint "Client Secret: $clientSecret"
155
- DetailPrint "Agent ID: $agentId"
156
-
157
- # Copy the service files to the root directory
158
- File ..\..\node_modules\node-shawl\bin\shawl-v1.3.0-legal.txt
159
- File ..\..\node_modules\node-shawl\bin\shawl-v1.3.0-win64.exe
160
- File dist\medplum-agent-win-x64.exe
161
- File README.md
162
-
163
- # Create the service
164
- DetailPrint "Creating service..."
165
- ExecWait "shawl-v1.3.0-win64.exe add --name $\"${SERVICE_NAME}$\" -- $\"$INSTDIR\medplum-agent-win-x64.exe$\" $\"$baseUrl$\" $\"$clientId$\" $\"$clientSecret$\" $\"$agentId$\"" $1
166
- DetailPrint "Exit code $1"
167
-
168
- # Set service display name
169
- DetailPrint "Setting service display name..."
170
- ExecWait "sc.exe config $\"${SERVICE_NAME}$\" displayname= $\"${APP_NAME}$\"" $1
171
- DetailPrint "Exit code $1"
172
-
173
- # Set service description
174
- DetailPrint "Setting service description..."
175
- ExecWait "sc.exe description $\"${SERVICE_NAME}$\" $\"Securely connects local devices to ${COMPANY_NAME} cloud$\"" $1
176
- DetailPrint "Exit code $1"
177
-
178
- # Set service to start automatically
179
- DetailPrint "Setting service to start automatically..."
180
- ExecWait "sc.exe config $\"${SERVICE_NAME}$\" start= auto" $1
181
- DetailPrint "Exit code $1"
182
-
183
- # Start the service
184
- DetailPrint "Starting service..."
185
- ExecWait "sc.exe start $\"${SERVICE_NAME}$\"" $1
186
- DetailPrint "Exit code $1"
187
-
188
- # Create the uninstaller
189
- DetailPrint "Creating the uninstaller..."
190
- SetOutPath $INSTDIR
191
- WriteUninstaller "$INSTDIR\uninstall.exe"
192
-
193
- # Register the uninstaller
194
- DetailPrint "Registering the uninstaller..."
195
- WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "DisplayName" "${APP_NAME}"
196
- WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
197
- WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
198
- DetailPrint "Uninstaller complete"
199
-
200
- # Create Start menu shortcuts
201
- DetailPrint "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk"
202
- CreateDirectory "$SMPROGRAMS\${APP_NAME}"
203
- CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk" "$INSTDIR\uninstall.exe"
204
-
205
- FunctionEnd
206
-
207
- # Start the uninstaller
208
- Section Uninstall
209
-
210
- # Stop the service
211
- DetailPrint "Stopping service..."
212
- ExecWait "sc.exe stop ${SERVICE_NAME}" $1
213
- DetailPrint "Exit code $1"
214
-
215
- # Sleep for 3 seconds to let the service fully stop
216
- # We cannot delete the file until the service is fully stopped
217
- DetailPrint "Sleeping..."
218
- Sleep 3000
219
-
220
- # Deleting the service
221
- DetailPrint "Deleting service..."
222
- ExecWait "sc.exe delete ${SERVICE_NAME}" $1
223
- DetailPrint "Exit code $1"
224
-
225
- # Get out of the service directory so we can delete it
226
- SetOutPath "$PROGRAMFILES64"
227
-
228
- # Uninstall the Start menu shortcuts
229
- RMDir /r /REBOOTOK "$SMPROGRAMS\${APP_NAME}"
230
-
231
- # Delete the files
232
- RMDir /r /REBOOTOK "$INSTDIR"
233
-
234
- # Unregister the program
235
- DeleteRegKey HKLM "SYSTEM\CurrentControlSet\Services\${SERVICE_NAME}"
236
- DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}"
237
-
238
- SectionEnd
package/jest.config.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "testEnvironment": "node",
3
- "transform": {
4
- "^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
5
- },
6
- "moduleFileExtensions": ["ts", "js", "json", "node"],
7
- "testMatch": ["**/src/**/*.test.ts"]
8
- }
@@ -1,9 +0,0 @@
1
- export class EventLogger {
2
- info(): void {
3
- //
4
- }
5
-
6
- error(): void {
7
- //
8
- }
9
- }
@@ -1 +0,0 @@
1
- export { WebSocket as default } from 'mock-socket';
package/src/main.test.ts DELETED
@@ -1,220 +0,0 @@
1
- import { allOk, createReference, Hl7Message, sleep } from '@medplum/core';
2
- import { Agent, Bot, Endpoint, Resource } from '@medplum/fhirtypes';
3
- import { Hl7Client, Hl7Server } from '@medplum/hl7';
4
- import { MockClient } from '@medplum/mock';
5
- import { Client, Server } from 'mock-socket';
6
- import { App } from './main';
7
-
8
- jest.mock('node-windows');
9
-
10
- const medplum = new MockClient();
11
- let bot: Bot;
12
- let endpoint: Endpoint;
13
-
14
- describe('Agent', () => {
15
- beforeAll(async () => {
16
- console.log = jest.fn();
17
-
18
- medplum.router.router.add('POST', ':resourceType/:id/$execute', async () => {
19
- return [allOk, {} as Resource];
20
- });
21
-
22
- bot = await medplum.createResource<Bot>({ resourceType: 'Bot' });
23
-
24
- endpoint = await medplum.createResource<Endpoint>({
25
- resourceType: 'Endpoint',
26
- address: 'mllp://0.0.0.0:57000',
27
- });
28
- });
29
-
30
- test('Runs successfully', async () => {
31
- const mockServer = new Server('wss://example.com/ws/agent');
32
-
33
- mockServer.on('connection', (socket) => {
34
- socket.on('message', (data) => {
35
- const command = JSON.parse((data as Buffer).toString('utf8'));
36
- if (command.type === 'connect') {
37
- socket.send(
38
- Buffer.from(
39
- JSON.stringify({
40
- type: 'connected',
41
- })
42
- )
43
- );
44
- }
45
- });
46
- });
47
-
48
- const agent = await medplum.createResource<Agent>({
49
- resourceType: 'Agent',
50
- channel: [
51
- {
52
- endpoint: createReference(endpoint),
53
- targetReference: createReference(bot),
54
- },
55
- ],
56
- });
57
-
58
- const app = new App(medplum, agent.id as string);
59
- await app.start();
60
- app.stop();
61
- app.stop();
62
- mockServer.stop();
63
- });
64
-
65
- test('Send and receive', async () => {
66
- const mockServer = new Server('wss://example.com/ws/agent');
67
-
68
- mockServer.on('connection', (socket) => {
69
- socket.on('message', (data) => {
70
- const command = JSON.parse((data as Buffer).toString('utf8'));
71
- if (command.type === 'connect') {
72
- socket.send(
73
- Buffer.from(
74
- JSON.stringify({
75
- type: 'connected',
76
- })
77
- )
78
- );
79
- }
80
-
81
- if (command.type === 'transmit') {
82
- const hl7Message = Hl7Message.parse(command.body);
83
- const ackMessage = hl7Message.buildAck();
84
- socket.send(
85
- Buffer.from(
86
- JSON.stringify({
87
- type: 'transmit',
88
- channel: command.channel,
89
- remote: command.remote,
90
- body: ackMessage.toString(),
91
- })
92
- )
93
- );
94
- }
95
- });
96
- });
97
-
98
- const agent = await medplum.createResource<Agent>({
99
- resourceType: 'Agent',
100
- channel: [
101
- {
102
- name: 'test',
103
- endpoint: createReference(endpoint),
104
- targetReference: createReference(bot),
105
- },
106
- ],
107
- });
108
-
109
- const app = new App(medplum, agent.id as string);
110
- await app.start();
111
-
112
- const client = new Hl7Client({
113
- host: 'localhost',
114
- port: 57000,
115
- });
116
-
117
- const response = await client.sendAndWait(
118
- Hl7Message.parse(
119
- 'MSH|^~\\&|ADT1|MCM|LABADT|MCM|198808181126|SECURITY|ADT^A01|MSG00001|P|2.2\r' +
120
- 'PID|||PATID1234^5^M11||JONES^WILLIAM^A^III||19610615|M-\r' +
121
- 'NK1|1|JONES^BARBARA^K|SPO|||||20011105\r' +
122
- 'PV1|1|I|2000^2012^01||||004777^LEBAUER^SIDNEY^J.|||SUR||-||1|A0-'
123
- )
124
- );
125
- expect(response).toBeDefined();
126
- expect(response.header.getComponent(9, 1)).toBe('ACK');
127
- expect(response.segments).toHaveLength(2);
128
- expect(response.segments[1].name).toBe('MSA');
129
-
130
- client.close();
131
- app.stop();
132
- mockServer.stop();
133
- });
134
-
135
- test('Push', async () => {
136
- const mockServer = new Server('wss://example.com/ws/agent');
137
- let mySocket: Client | undefined = undefined;
138
-
139
- mockServer.on('connection', (socket) => {
140
- mySocket = socket;
141
- socket.on('message', (data) => {
142
- const command = JSON.parse((data as Buffer).toString('utf8'));
143
- if (command.type === 'connect') {
144
- socket.send(
145
- Buffer.from(
146
- JSON.stringify({
147
- type: 'connected',
148
- })
149
- )
150
- );
151
- }
152
- });
153
- });
154
-
155
- const agent = await medplum.createResource<Agent>({
156
- resourceType: 'Agent',
157
- channel: [
158
- {
159
- endpoint: createReference(endpoint),
160
- targetReference: createReference(bot),
161
- },
162
- ],
163
- });
164
-
165
- // Start an HL7 listener
166
- const hl7Messages = [];
167
- const hl7Server = new Hl7Server((conn) => {
168
- conn.addEventListener('message', ({ message }) => {
169
- hl7Messages.push(message);
170
- conn.send(message.buildAck());
171
- });
172
- });
173
- hl7Server.start(57001);
174
-
175
- // Wait for server to start listening
176
- while (!hl7Server.server?.listening) {
177
- await sleep(100);
178
- }
179
-
180
- // Start the app
181
- const app = new App(medplum, agent.id as string);
182
- await app.start();
183
-
184
- // Wait for the WebSocket to connect
185
- // eslint-disable-next-line no-unmodified-loop-condition
186
- while (!mySocket) {
187
- await sleep(100);
188
- }
189
-
190
- // At this point, we expect the websocket to be connected
191
- expect(mySocket).toBeDefined();
192
-
193
- // Send a push message
194
- const wsClient = mySocket as unknown as Client;
195
- wsClient.send(
196
- Buffer.from(
197
- JSON.stringify({
198
- type: 'push',
199
- body:
200
- 'MSH|^~\\&|ADT1|MCM|LABADT|MCM|198808181126|SECURITY|ADT^A01|MSG00001|P|2.2\r' +
201
- 'PID|||PATID1234^5^M11||JONES^WILLIAM^A^III||19610615|M-\r' +
202
- 'NK1|1|JONES^BARBARA^K|SPO|||||20011105\r' +
203
- 'PV1|1|I|2000^2012^01||||004777^LEBAUER^SIDNEY^J.|||SUR||-||1|A0-',
204
- remote: 'mllp://localhost:57001',
205
- })
206
- )
207
- );
208
-
209
- // Wait for the HL7 message to be received
210
- while (hl7Messages.length < 1) {
211
- await sleep(100);
212
- }
213
- expect(hl7Messages.length).toBe(1);
214
-
215
- // Shutdown everything
216
- hl7Server.stop();
217
- app.stop();
218
- mockServer.stop();
219
- });
220
- });
package/src/main.ts DELETED
@@ -1,244 +0,0 @@
1
- import { Hl7Message, MedplumClient, normalizeErrorString } from '@medplum/core';
2
- import { AgentChannel, Endpoint, Reference } from '@medplum/fhirtypes';
3
- import { Hl7Client, Hl7Connection, Hl7MessageEvent, Hl7Server } from '@medplum/hl7';
4
- import { EventLogger } from 'node-windows';
5
- import WebSocket from 'ws';
6
-
7
- interface QueueItem {
8
- channel: string;
9
- remote: string;
10
- body: string;
11
- }
12
-
13
- export class App {
14
- readonly log: EventLogger;
15
- readonly webSocket: WebSocket;
16
- readonly webSocketQueue: QueueItem[] = [];
17
- readonly channels = new Map<string, AgentHl7Channel>();
18
- readonly hl7Queue: QueueItem[] = [];
19
- live = false;
20
-
21
- constructor(
22
- readonly medplum: MedplumClient,
23
- readonly agentId: string
24
- ) {
25
- this.log = {
26
- info: console.log,
27
- warn: console.warn,
28
- error: console.error,
29
- } as EventLogger;
30
-
31
- const webSocketUrl = new URL(medplum.getBaseUrl());
32
- webSocketUrl.protocol = webSocketUrl.protocol === 'https:' ? 'wss:' : 'ws:';
33
- webSocketUrl.pathname = '/ws/agent';
34
- this.log.info(`Connecting to WebSocket: ${webSocketUrl.href}`);
35
-
36
- this.webSocket = new WebSocket(webSocketUrl);
37
- this.webSocket.binaryType = 'nodebuffer';
38
- this.webSocket.addEventListener('error', (err) => this.log.error(err.message));
39
- this.webSocket.addEventListener('open', () => {
40
- this.webSocket.send(
41
- JSON.stringify({
42
- type: 'connect',
43
- accessToken: medplum.getAccessToken(),
44
- agentId,
45
- })
46
- );
47
- });
48
-
49
- this.webSocket.addEventListener('message', (e) => {
50
- try {
51
- const data = e.data as Buffer;
52
- const str = data.toString('utf8');
53
- this.log.info(`Received from WebSocket: ${str.replaceAll('\r', '\n')}`);
54
- const command = JSON.parse(str);
55
- switch (command.type) {
56
- case 'connected':
57
- this.live = true;
58
- this.trySendToWebSocket();
59
- break;
60
- case 'transmit':
61
- this.addToHl7Queue(command);
62
- break;
63
- case 'push':
64
- this.pushMessage(command);
65
- break;
66
- }
67
- } catch (err) {
68
- this.log.error(`WebSocket error: ${normalizeErrorString(err)}`);
69
- }
70
- });
71
- }
72
-
73
- async start(): Promise<void> {
74
- this.log.info('Medplum service starting...');
75
-
76
- const agent = await this.medplum.readResource('Agent', this.agentId);
77
-
78
- for (const definition of agent.channel as AgentChannel[]) {
79
- const endpoint = await this.medplum.readReference(definition.endpoint as Reference<Endpoint>);
80
- const channel = new AgentHl7Channel(this, definition, endpoint);
81
- channel.start();
82
- this.channels.set(definition.name as string, channel);
83
- }
84
-
85
- this.log.info('Medplum service started successfully');
86
- }
87
-
88
- stop(): void {
89
- this.log.info('Medplum service stopping...');
90
- this.channels.forEach((channel) => channel.stop());
91
- this.log.info('Medplum service stopped successfully');
92
- }
93
-
94
- addToWebSocketQueue(message: QueueItem): void {
95
- this.webSocketQueue.push(message);
96
- this.trySendToWebSocket();
97
- }
98
-
99
- addToHl7Queue(message: QueueItem): void {
100
- this.hl7Queue.push(message);
101
- this.trySendToHl7Connection();
102
- }
103
-
104
- private trySendToWebSocket(): void {
105
- if (this.live) {
106
- while (this.webSocketQueue.length > 0) {
107
- const msg = this.webSocketQueue.shift();
108
- if (msg) {
109
- this.webSocket.send(
110
- JSON.stringify({
111
- type: 'transmit',
112
- accessToken: this.medplum.getAccessToken(),
113
- ...msg,
114
- })
115
- );
116
- }
117
- }
118
- }
119
- }
120
-
121
- private trySendToHl7Connection(): void {
122
- while (this.hl7Queue.length > 0) {
123
- const msg = this.hl7Queue.shift();
124
- if (msg) {
125
- const channel = this.channels.get(msg.channel);
126
- if (channel) {
127
- const connection = channel.connections.get(msg.remote);
128
- if (connection) {
129
- connection.hl7Connection.send(Hl7Message.parse(msg.body));
130
- }
131
- }
132
- }
133
- }
134
- }
135
-
136
- private pushMessage(message: QueueItem): void {
137
- const address = new URL(message.remote);
138
- const client = new Hl7Client({
139
- host: address.hostname,
140
- port: parseInt(address.port, 10),
141
- });
142
-
143
- client
144
- .sendAndWait(Hl7Message.parse(message.body))
145
- .then((response) => {
146
- this.log.info(`Response: ${response.toString().replaceAll('\r', '\n')}`);
147
- })
148
- .catch((err) => {
149
- this.log.error(`HL7 error: ${normalizeErrorString(err)}`);
150
- })
151
- .finally(() => {
152
- client.close();
153
- });
154
- }
155
- }
156
-
157
- export class AgentHl7Channel {
158
- readonly server: Hl7Server;
159
- readonly connections = new Map<string, AgentHl7ChannelConnection>();
160
-
161
- constructor(
162
- readonly app: App,
163
- readonly definition: AgentChannel,
164
- readonly endpoint: Endpoint
165
- ) {
166
- this.server = new Hl7Server((connection) => this.handleNewConnection(connection));
167
- }
168
-
169
- start(): void {
170
- const address = new URL(this.endpoint.address as string);
171
- this.app.log.info(`Channel starting on ${address}`);
172
- this.server.start(parseInt(address.port, 10));
173
- this.app.log.info('Channel started successfully');
174
- }
175
-
176
- stop(): void {
177
- this.app.log.info('Channel stopping...');
178
- this.connections.forEach((connection) => connection.close());
179
- this.server.stop();
180
- this.app.log.info('Channel stopped successfully');
181
- }
182
-
183
- private handleNewConnection(connection: Hl7Connection): void {
184
- const c = new AgentHl7ChannelConnection(this, connection);
185
- this.app.log.info(`HL7 connection established: ${c.remote}`);
186
- this.connections.set(c.remote, c);
187
- }
188
- }
189
-
190
- export class AgentHl7ChannelConnection {
191
- readonly remote: string;
192
-
193
- constructor(
194
- readonly channel: AgentHl7Channel,
195
- readonly hl7Connection: Hl7Connection
196
- ) {
197
- this.remote = `${hl7Connection.socket.remoteAddress}:${hl7Connection.socket.remotePort}`;
198
-
199
- // Add listener immediately to handle incoming messages
200
- this.hl7Connection.addEventListener('message', (event) => this.handler(event));
201
- }
202
-
203
- private async handler(event: Hl7MessageEvent): Promise<void> {
204
- try {
205
- this.channel.app.log.info('Received:');
206
- this.channel.app.log.info(event.message.toString().replaceAll('\r', '\n'));
207
- this.channel.app.addToWebSocketQueue({
208
- channel: this.channel.definition.name as string,
209
- remote: this.remote,
210
- body: event.message.toString(),
211
- });
212
- } catch (err) {
213
- this.channel.app.log.error(`HL7 error: ${normalizeErrorString(err)}`);
214
- }
215
- }
216
-
217
- close(): void {
218
- this.hl7Connection.close();
219
- }
220
- }
221
-
222
- async function main(argv: string[]): Promise<void> {
223
- if (argv.length < 6) {
224
- console.log('Usage: node medplum-agent.js <baseUrl> <clientId> <clientSecret> <agentId>');
225
- process.exit(1);
226
- }
227
- const [_node, _script, baseUrl, clientId, clientSecret, agentId] = argv;
228
-
229
- const medplum = new MedplumClient({ baseUrl, clientId });
230
- await medplum.startClientLogin(clientId, clientSecret);
231
-
232
- const app = new App(medplum, agentId);
233
- await app.start();
234
-
235
- process.on('SIGINT', () => {
236
- console.log('\ngracefully shutting down from SIGINT (Crtl-C)');
237
- app.stop();
238
- process.exit();
239
- });
240
- }
241
-
242
- if (typeof require !== 'undefined' && require.main === module) {
243
- main(process.argv).catch(console.error);
244
- }
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "lib": ["esnext"]
6
- },
7
- "include": ["src/**/*.ts"]
8
- }