@medplum/agent 2.0.29 → 2.0.31
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 +25 -0
- package/dist/cjs/index.cjs +3916 -1758
- package/installer.nsi +199 -0
- package/package.json +8 -3
- package/src/__mocks__/ws.ts +1 -0
- package/src/main.test.ts +68 -6
- package/src/main.ts +159 -32
package/installer.nsi
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
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 baseUrl
|
|
34
|
+
Var clientId
|
|
35
|
+
Var clientSecret
|
|
36
|
+
Var agentId
|
|
37
|
+
|
|
38
|
+
Page custom WelcomePage
|
|
39
|
+
Page custom InputPage InputPageLeave
|
|
40
|
+
Page instfiles
|
|
41
|
+
|
|
42
|
+
# The WelcomePage is a simple static screen that displays a friendly message.
|
|
43
|
+
Function WelcomePage
|
|
44
|
+
nsDialogs::Create 1018
|
|
45
|
+
Pop $WelcomeDialog
|
|
46
|
+
|
|
47
|
+
${If} $WelcomeDialog == error
|
|
48
|
+
Abort
|
|
49
|
+
${EndIf}
|
|
50
|
+
|
|
51
|
+
${NSD_CreateLabel} 0 0 100% 50u "Welcome to the ${APP_NAME} Installer!$\r$\n$\r$\nClick next to continue."
|
|
52
|
+
Pop $WelcomeLabel
|
|
53
|
+
|
|
54
|
+
nsDialogs::Show
|
|
55
|
+
FunctionEnd
|
|
56
|
+
|
|
57
|
+
# The InputPage captures all of the user input for the agent.
|
|
58
|
+
Function InputPage
|
|
59
|
+
nsDialogs::Create 1018
|
|
60
|
+
Pop $0
|
|
61
|
+
|
|
62
|
+
StrCpy $baseUrl "${DEFAULT_BASE_URL}"
|
|
63
|
+
${NSD_CreateLabel} 0 0 30% 12u "Base URL:"
|
|
64
|
+
Pop $R0
|
|
65
|
+
${NSD_CreateText} 35% 0 65% 12u $baseUrl
|
|
66
|
+
Pop $R1
|
|
67
|
+
|
|
68
|
+
${NSD_CreateLabel} 0 15u 30% 12u "Client ID:"
|
|
69
|
+
Pop $R2
|
|
70
|
+
${NSD_CreateText} 35% 15u 65% 12u $clientId
|
|
71
|
+
Pop $R3
|
|
72
|
+
|
|
73
|
+
${NSD_CreateLabel} 0 30u 30% 12u "Client Secret:"
|
|
74
|
+
Pop $R4
|
|
75
|
+
${NSD_CreateText} 35% 30u 65% 12u $clientSecret
|
|
76
|
+
Pop $R5
|
|
77
|
+
|
|
78
|
+
${NSD_CreateLabel} 0 45u 30% 12u "Agent ID:"
|
|
79
|
+
Pop $R6
|
|
80
|
+
${NSD_CreateText} 35% 45u 65% 12u $agentId
|
|
81
|
+
Pop $R7
|
|
82
|
+
|
|
83
|
+
${NSD_SetFocus} $R3
|
|
84
|
+
nsDialogs::Show
|
|
85
|
+
FunctionEnd
|
|
86
|
+
|
|
87
|
+
Function InputPageLeave
|
|
88
|
+
${NSD_GetText} $R1 $baseUrl
|
|
89
|
+
${NSD_GetText} $R3 $clientId
|
|
90
|
+
${NSD_GetText} $R5 $clientSecret
|
|
91
|
+
${NSD_GetText} $R7 $agentId
|
|
92
|
+
FunctionEnd
|
|
93
|
+
|
|
94
|
+
# Do the actual installation.
|
|
95
|
+
# Install all of the files.
|
|
96
|
+
# Install the Windows Service.
|
|
97
|
+
Section
|
|
98
|
+
DetailPrint "${APP_NAME}"
|
|
99
|
+
|
|
100
|
+
# Call userInfo plugin to get user info. The plugin puts the result in the stack
|
|
101
|
+
userInfo::getAccountType
|
|
102
|
+
|
|
103
|
+
# Pop the result from the stack into $0
|
|
104
|
+
Pop $0
|
|
105
|
+
|
|
106
|
+
# Compare the result with the string "Admin" to see if the user is admin.
|
|
107
|
+
# If match, jump 3 lines down.
|
|
108
|
+
strCmp $0 "Admin" +3
|
|
109
|
+
|
|
110
|
+
# If there is not a match, print message and return
|
|
111
|
+
DetailPrint "User is not admin: $0"
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Otherwise, confirm and return
|
|
115
|
+
DetailPrint "User is admin"
|
|
116
|
+
|
|
117
|
+
# Print user input
|
|
118
|
+
DetailPrint "Base URL: $baseUrl"
|
|
119
|
+
DetailPrint "Client ID: $clientId"
|
|
120
|
+
DetailPrint "Client Secret: $clientSecret"
|
|
121
|
+
DetailPrint "Agent ID: $agentId"
|
|
122
|
+
|
|
123
|
+
# Copy the service files to the root directory
|
|
124
|
+
SetOutPath "$INSTDIR"
|
|
125
|
+
File ..\..\node_modules\node-windows\bin\winsw\winsw.exe
|
|
126
|
+
File dist\medplum-agent-win-x64.exe
|
|
127
|
+
File README.md
|
|
128
|
+
|
|
129
|
+
# Create the winsw.xml config file
|
|
130
|
+
# See config file format: https://github.com/winsw/winsw/blob/v3/docs/xml-config-file.md
|
|
131
|
+
FileOpen $9 winsw.xml w
|
|
132
|
+
FileWrite $9 "<service>$\r$\n"
|
|
133
|
+
FileWrite $9 "<id>${SERVICE_NAME}</id>$\r$\n"
|
|
134
|
+
FileWrite $9 "<name>${APP_NAME}</name>$\r$\n"
|
|
135
|
+
FileWrite $9 "<description>Securely connects local devices to ${COMPANY_NAME} cloud</description>$\r$\n"
|
|
136
|
+
FileWrite $9 "<executable>$INSTDIR\medplum-agent-win-x64.exe</executable>$\r$\n"
|
|
137
|
+
FileWrite $9 "<arguments>$\"$baseUrl$\" $\"$clientId$\" $\"$clientSecret$\" $\"$agentId$\"</arguments>$\r$\n"
|
|
138
|
+
FileWrite $9 "<startmode>Automatic</startmode>$\r$\n"
|
|
139
|
+
FileWrite $9 "</service>$\r$\n"
|
|
140
|
+
FileClose $9
|
|
141
|
+
|
|
142
|
+
# Install the service
|
|
143
|
+
DetailPrint "Installing service..."
|
|
144
|
+
StrCpy $0 "winsw.exe install"
|
|
145
|
+
#DetailPrint "$0"
|
|
146
|
+
ExecWait $0 $1
|
|
147
|
+
DetailPrint "Install returned $1"
|
|
148
|
+
|
|
149
|
+
# Start the service
|
|
150
|
+
DetailPrint "Starting service..."
|
|
151
|
+
StrCpy $0 "winsw.exe start"
|
|
152
|
+
#DetailPrint "$0"
|
|
153
|
+
ExecWait $0 $1
|
|
154
|
+
DetailPrint "Start service returned $1"
|
|
155
|
+
|
|
156
|
+
# Create the uninstaller
|
|
157
|
+
DetailPrint "Creating the uninstaller..."
|
|
158
|
+
SetOutPath $INSTDIR
|
|
159
|
+
WriteUninstaller "$INSTDIR\uninstall.exe"
|
|
160
|
+
|
|
161
|
+
# Register the uninstaller
|
|
162
|
+
DetailPrint "Registering the uninstaller..."
|
|
163
|
+
WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "DisplayName" "${APP_NAME}"
|
|
164
|
+
WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
|
165
|
+
WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
|
166
|
+
DetailPrint "Uninstaller complete"
|
|
167
|
+
|
|
168
|
+
# Create Start menu shortcuts
|
|
169
|
+
DetailPrint "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk"
|
|
170
|
+
CreateDirectory "$SMPROGRAMS\${APP_NAME}"
|
|
171
|
+
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk" "$INSTDIR\uninstall.exe"
|
|
172
|
+
|
|
173
|
+
# default section end
|
|
174
|
+
SectionEnd
|
|
175
|
+
|
|
176
|
+
# Start the uninstaller
|
|
177
|
+
Section Uninstall
|
|
178
|
+
|
|
179
|
+
# Uninstall the service
|
|
180
|
+
DetailPrint "Uninstalling service..."
|
|
181
|
+
SetOutPath "$INSTDIR"
|
|
182
|
+
StrCpy $0 "winsw.exe uninstall"
|
|
183
|
+
#DetailPrint "$0"
|
|
184
|
+
ExecWait $0 $1
|
|
185
|
+
DetailPrint "Uninstall returned $1"
|
|
186
|
+
|
|
187
|
+
# Get out of the service directory so we can delete it
|
|
188
|
+
SetOutPath "$PROGRAMFILES64"
|
|
189
|
+
|
|
190
|
+
# Uninstall the Start menu shortcuts
|
|
191
|
+
RMDir /r /REBOOTOK "$SMPROGRAMS\${APP_NAME}"
|
|
192
|
+
|
|
193
|
+
# Delete the files
|
|
194
|
+
RMDir /r /REBOOTOK "$INSTDIR"
|
|
195
|
+
|
|
196
|
+
# Unregister the program
|
|
197
|
+
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}"
|
|
198
|
+
|
|
199
|
+
SectionEnd
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medplum/agent",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.31",
|
|
4
4
|
"description": "Medplum Agent",
|
|
5
5
|
"author": "Medplum <hello@medplum.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,17 +17,22 @@
|
|
|
17
17
|
"clean": "rimraf dist",
|
|
18
18
|
"build": "npm run clean && tsc && node esbuild.mjs",
|
|
19
19
|
"test": "jest",
|
|
20
|
-
"
|
|
20
|
+
"agent": "ts-node src/main.ts",
|
|
21
|
+
"package": "pkg ./dist/cjs/index.cjs --targets node18-win-x64 --output dist/medplum-agent-win-x64.exe --options no-warnings",
|
|
22
|
+
"installer": "makensis installer.nsi"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
23
25
|
"@medplum/core": "*",
|
|
24
26
|
"@medplum/hl7": "*",
|
|
25
|
-
"node-windows": "1.0.0-beta.8"
|
|
27
|
+
"node-windows": "1.0.0-beta.8",
|
|
28
|
+
"ws": "8.13.0"
|
|
26
29
|
},
|
|
27
30
|
"devDependencies": {
|
|
28
31
|
"@medplum/fhirtypes": "*",
|
|
29
32
|
"@medplum/mock": "*",
|
|
30
33
|
"@types/node-windows": "0.1.2",
|
|
34
|
+
"@types/ws": "8.5.5",
|
|
35
|
+
"mock-socket": "9.2.1",
|
|
31
36
|
"pkg": "5.8.1"
|
|
32
37
|
}
|
|
33
38
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WebSocket as default } from 'mock-socket';
|
package/src/main.test.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { allOk, Hl7Message } from '@medplum/core';
|
|
2
|
-
import { Bot, Resource } from '@medplum/fhirtypes';
|
|
1
|
+
import { allOk, createReference, Hl7Message } from '@medplum/core';
|
|
2
|
+
import { Agent, Bot, Endpoint, Resource } from '@medplum/fhirtypes';
|
|
3
3
|
import { Hl7Client } from '@medplum/hl7';
|
|
4
4
|
import { MockClient } from '@medplum/mock';
|
|
5
|
+
import { Server } from 'mock-socket';
|
|
5
6
|
import { App } from './main';
|
|
6
7
|
|
|
7
8
|
jest.mock('node-windows');
|
|
8
9
|
|
|
9
10
|
const medplum = new MockClient();
|
|
10
11
|
let bot: Bot;
|
|
12
|
+
let endpoint: Endpoint;
|
|
11
13
|
|
|
12
14
|
describe('Agent', () => {
|
|
13
15
|
beforeAll(async () => {
|
|
@@ -18,17 +20,73 @@ describe('Agent', () => {
|
|
|
18
20
|
});
|
|
19
21
|
|
|
20
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:56000',
|
|
27
|
+
});
|
|
21
28
|
});
|
|
22
29
|
|
|
23
30
|
test('Runs successfully', async () => {
|
|
24
|
-
const
|
|
25
|
-
|
|
31
|
+
const agent = await medplum.createResource<Agent>({
|
|
32
|
+
resourceType: 'Agent',
|
|
33
|
+
channel: [
|
|
34
|
+
{
|
|
35
|
+
endpoint: createReference(endpoint),
|
|
36
|
+
targetReference: createReference(bot),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const app = new App(medplum, agent.id as string);
|
|
42
|
+
await app.start();
|
|
43
|
+
app.stop();
|
|
26
44
|
app.stop();
|
|
27
45
|
});
|
|
28
46
|
|
|
29
47
|
test('Send and receive', async () => {
|
|
30
|
-
const
|
|
31
|
-
|
|
48
|
+
const mockServer = new Server('wss://example.com/ws/agent');
|
|
49
|
+
|
|
50
|
+
mockServer.on('connection', (socket) => {
|
|
51
|
+
socket.on('message', (data) => {
|
|
52
|
+
const command = JSON.parse((data as Buffer).toString('utf8'));
|
|
53
|
+
if (command.type === 'connect') {
|
|
54
|
+
socket.send(
|
|
55
|
+
Buffer.from(
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
type: 'connected',
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command.type === 'transmit') {
|
|
64
|
+
const hl7Message = Hl7Message.parse(command.message);
|
|
65
|
+
const ackMessage = hl7Message.buildAck();
|
|
66
|
+
socket.send(
|
|
67
|
+
Buffer.from(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
type: 'transmit',
|
|
70
|
+
message: ackMessage.toString(),
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const agent = await medplum.createResource<Agent>({
|
|
79
|
+
resourceType: 'Agent',
|
|
80
|
+
channel: [
|
|
81
|
+
{
|
|
82
|
+
endpoint: createReference(endpoint),
|
|
83
|
+
targetReference: createReference(bot),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const app = new App(medplum, agent.id as string);
|
|
89
|
+
await app.start();
|
|
32
90
|
|
|
33
91
|
const client = new Hl7Client({
|
|
34
92
|
host: 'localhost',
|
|
@@ -46,8 +104,12 @@ describe('Agent', () => {
|
|
|
46
104
|
)
|
|
47
105
|
);
|
|
48
106
|
expect(response).toBeDefined();
|
|
107
|
+
expect(response.header.getComponent(9, 1)).toBe('ACK');
|
|
108
|
+
expect(response.segments).toHaveLength(2);
|
|
109
|
+
expect(response.segments[1].name).toBe('MSA');
|
|
49
110
|
|
|
50
111
|
client.close();
|
|
51
112
|
app.stop();
|
|
113
|
+
mockServer.stop();
|
|
52
114
|
});
|
|
53
115
|
});
|
package/src/main.ts
CHANGED
|
@@ -1,61 +1,188 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Bot } from '@medplum/fhirtypes';
|
|
3
|
-
import { Hl7MessageEvent, Hl7Server } from '@medplum/hl7';
|
|
1
|
+
import { Hl7Message, MedplumClient, resolveId } from '@medplum/core';
|
|
2
|
+
import { AgentChannel, Bot, Endpoint, Reference } from '@medplum/fhirtypes';
|
|
3
|
+
import { Hl7Connection, Hl7MessageEvent, Hl7Server } from '@medplum/hl7';
|
|
4
4
|
import { EventLogger } from 'node-windows';
|
|
5
|
-
|
|
6
|
-
const log = new EventLogger({
|
|
7
|
-
source: 'MedplumService',
|
|
8
|
-
eventLog: 'SYSTEM',
|
|
9
|
-
});
|
|
5
|
+
import WebSocket from 'ws';
|
|
10
6
|
|
|
11
7
|
export class App {
|
|
12
8
|
readonly log: EventLogger;
|
|
13
|
-
readonly
|
|
9
|
+
readonly channels: AgentHl7Channel[];
|
|
14
10
|
|
|
15
|
-
constructor(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
constructor(
|
|
12
|
+
readonly medplum: MedplumClient,
|
|
13
|
+
readonly agentId: string
|
|
14
|
+
) {
|
|
15
|
+
this.log = {
|
|
16
|
+
info: console.log,
|
|
17
|
+
warn: console.warn,
|
|
18
|
+
error: console.error,
|
|
19
|
+
} as EventLogger;
|
|
20
20
|
|
|
21
|
-
this.
|
|
22
|
-
this.server.addEventListener('message', (event) => this.handler(event));
|
|
21
|
+
this.channels = [];
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
start(): void {
|
|
24
|
+
async start(): Promise<void> {
|
|
26
25
|
this.log.info('Medplum service starting...');
|
|
27
|
-
|
|
26
|
+
|
|
27
|
+
const agent = await this.medplum.readResource('Agent', this.agentId);
|
|
28
|
+
|
|
29
|
+
for (const definition of agent.channel as AgentChannel[]) {
|
|
30
|
+
const endpoint = await this.medplum.readReference(definition.endpoint as Reference<Endpoint>);
|
|
31
|
+
const channel = new AgentHl7Channel(this, definition, endpoint);
|
|
32
|
+
channel.start();
|
|
33
|
+
this.channels.push(channel);
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
this.log.info('Medplum service started successfully');
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
stop(): void {
|
|
32
40
|
this.log.info('Medplum service stopping...');
|
|
33
|
-
this.
|
|
41
|
+
this.channels.forEach((channel) => channel.stop());
|
|
34
42
|
this.log.info('Medplum service stopped successfully');
|
|
35
43
|
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class AgentHl7Channel {
|
|
47
|
+
readonly server: Hl7Server;
|
|
48
|
+
readonly connections: AgentHl7ChannelConnection[] = [];
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
readonly app: App,
|
|
52
|
+
readonly definition: AgentChannel,
|
|
53
|
+
readonly endpoint: Endpoint
|
|
54
|
+
) {
|
|
55
|
+
this.server = new Hl7Server((connection) => {
|
|
56
|
+
this.app.log.info('HL7 connection established');
|
|
57
|
+
this.connections.push(new AgentHl7ChannelConnection(this, connection));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
start(): void {
|
|
62
|
+
const address = new URL(this.endpoint.address as string);
|
|
63
|
+
this.app.log.info(`Channel starting on ${address}`);
|
|
64
|
+
this.server.start(parseInt(address.port, 10));
|
|
65
|
+
this.app.log.info('Channel started successfully');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
stop(): void {
|
|
69
|
+
this.app.log.info('Channel stopping...');
|
|
70
|
+
for (const connection of this.connections) {
|
|
71
|
+
connection.close();
|
|
72
|
+
}
|
|
73
|
+
this.server.stop();
|
|
74
|
+
this.app.log.info('Channel stopped successfully');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class AgentHl7ChannelConnection {
|
|
79
|
+
readonly webSocket: WebSocket;
|
|
80
|
+
readonly webSocketQueue: Hl7Message[] = [];
|
|
81
|
+
readonly hl7ConnectionQueue: Hl7Message[] = [];
|
|
82
|
+
live = false;
|
|
83
|
+
|
|
84
|
+
constructor(
|
|
85
|
+
readonly channel: AgentHl7Channel,
|
|
86
|
+
readonly hl7Connection: Hl7Connection
|
|
87
|
+
) {
|
|
88
|
+
const app = channel.app;
|
|
89
|
+
const medplum = app.medplum;
|
|
90
|
+
|
|
91
|
+
// Add listener immediately to handle incoming messages
|
|
92
|
+
this.hl7Connection.addEventListener('message', (event) => this.handler(event));
|
|
93
|
+
|
|
94
|
+
const webSocketUrl = new URL(medplum.getBaseUrl());
|
|
95
|
+
webSocketUrl.protocol = webSocketUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
96
|
+
webSocketUrl.pathname = '/ws/agent';
|
|
97
|
+
console.log('Connecting to WebSocket:', webSocketUrl.href);
|
|
98
|
+
|
|
99
|
+
this.webSocket = new WebSocket(webSocketUrl);
|
|
100
|
+
this.webSocket.binaryType = 'nodebuffer';
|
|
101
|
+
this.webSocket.addEventListener('error', console.error);
|
|
102
|
+
this.webSocket.addEventListener('open', () => {
|
|
103
|
+
this.webSocket.send(
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
type: 'connect',
|
|
106
|
+
accessToken: medplum.getAccessToken(),
|
|
107
|
+
botId: resolveId(channel.definition.targetReference as Reference<Bot>),
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
this.webSocket.addEventListener('message', (e) => {
|
|
113
|
+
try {
|
|
114
|
+
const data = e.data as Buffer;
|
|
115
|
+
const str = data.toString('utf8');
|
|
116
|
+
console.log('Received from WebSocket:', str.replaceAll('\r', '\n'));
|
|
117
|
+
const command = JSON.parse(str);
|
|
118
|
+
switch (command.type) {
|
|
119
|
+
case 'connected':
|
|
120
|
+
this.live = true;
|
|
121
|
+
this.trySendToWebSocket();
|
|
122
|
+
break;
|
|
123
|
+
case 'transmit':
|
|
124
|
+
this.hl7ConnectionQueue.push(Hl7Message.parse(command.message));
|
|
125
|
+
this.trySendToHl7Connection();
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.log('WebSocket error', err);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
36
133
|
|
|
37
134
|
private async handler(event: Hl7MessageEvent): Promise<void> {
|
|
38
135
|
try {
|
|
39
136
|
console.log('Received:');
|
|
40
137
|
console.log(event.message.toString().replaceAll('\r', '\n'));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.medplum.fhirUrl('Bot', this.bot.id as string, '$execute'),
|
|
44
|
-
event.message.toString(),
|
|
45
|
-
ContentType.HL7_V2
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const ack = event.message.buildAck();
|
|
49
|
-
console.log('Response:');
|
|
50
|
-
console.log(ack.toString().replaceAll('\r', '\n'));
|
|
51
|
-
event.send(ack);
|
|
138
|
+
this.webSocketQueue.push(event.message);
|
|
139
|
+
this.trySendToWebSocket();
|
|
52
140
|
} catch (err) {
|
|
53
141
|
console.log('HL7 error', err);
|
|
54
|
-
log.error(normalizeErrorString(err));
|
|
55
142
|
}
|
|
56
143
|
}
|
|
144
|
+
|
|
145
|
+
private trySendToWebSocket(): void {
|
|
146
|
+
if (this.live) {
|
|
147
|
+
while (this.webSocketQueue.length > 0) {
|
|
148
|
+
const msg = this.webSocketQueue.shift();
|
|
149
|
+
if (msg) {
|
|
150
|
+
this.webSocket.send(
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
type: 'transmit',
|
|
153
|
+
message: msg.toString(),
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private trySendToHl7Connection(): void {
|
|
162
|
+
while (this.hl7ConnectionQueue.length > 0) {
|
|
163
|
+
const msg = this.hl7ConnectionQueue.shift();
|
|
164
|
+
if (msg) {
|
|
165
|
+
this.hl7Connection.send(msg);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
close(): void {
|
|
171
|
+
this.hl7Connection.close();
|
|
172
|
+
this.webSocket.close();
|
|
173
|
+
}
|
|
57
174
|
}
|
|
58
175
|
|
|
59
176
|
if (typeof require !== 'undefined' && require.main === module) {
|
|
60
|
-
|
|
177
|
+
if (process.argv.length < 6) {
|
|
178
|
+
console.log('Usage: node medplum-agent.js <baseUrl> <clientId> <clientSecret> <agentId>');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const [_node, _script, baseUrl, clientId, clientSecret, agentId] = process.argv;
|
|
183
|
+
const medplum = new MedplumClient({ baseUrl, clientId });
|
|
184
|
+
medplum
|
|
185
|
+
.startClientLogin(clientId, clientSecret)
|
|
186
|
+
.then(() => new App(medplum, agentId).start())
|
|
187
|
+
.catch(console.error);
|
|
61
188
|
}
|