@medplum/agent 2.0.29 → 2.0.30
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 +5 -0
- package/dist/cjs/index.cjs +3897 -293
- package/package.json +6 -2
- package/src/__mocks__/ws.ts +1 -0
- package/src/main.test.ts +45 -2
- package/src/main.ts +114 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medplum/agent",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.30",
|
|
4
4
|
"description": "Medplum Agent",
|
|
5
5
|
"author": "Medplum <hello@medplum.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,17 +17,21 @@
|
|
|
17
17
|
"clean": "rimraf dist",
|
|
18
18
|
"build": "npm run clean && tsc && node esbuild.mjs",
|
|
19
19
|
"test": "jest",
|
|
20
|
+
"agent": "ts-node src/main.ts",
|
|
20
21
|
"package": "pkg ./dist/cjs/index.cjs --targets node18-win-x64 --output dist/medplum-agent-win-x64.exe"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@medplum/core": "*",
|
|
24
25
|
"@medplum/hl7": "*",
|
|
25
|
-
"node-windows": "1.0.0-beta.8"
|
|
26
|
+
"node-windows": "1.0.0-beta.8",
|
|
27
|
+
"ws": "8.13.0"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@medplum/fhirtypes": "*",
|
|
29
31
|
"@medplum/mock": "*",
|
|
30
32
|
"@types/node-windows": "0.1.2",
|
|
33
|
+
"@types/ws": "8.5.5",
|
|
34
|
+
"mock-socket": "9.2.1",
|
|
31
35
|
"pkg": "5.8.1"
|
|
32
36
|
}
|
|
33
37
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WebSocket as default } from 'mock-socket';
|
package/src/main.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { allOk, Hl7Message } from '@medplum/core';
|
|
|
2
2
|
import { Bot, 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');
|
|
@@ -21,13 +22,51 @@ describe('Agent', () => {
|
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
test('Runs successfully', async () => {
|
|
24
|
-
const app = new App(medplum, bot);
|
|
25
|
+
const app = new App(medplum, { botId: bot.id as string });
|
|
25
26
|
app.start();
|
|
26
27
|
app.stop();
|
|
28
|
+
app.stop();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('Use system event log', async () => {
|
|
32
|
+
const app = new App(medplum, { botId: bot.id as string, useSystemEventLog: true });
|
|
33
|
+
app.start();
|
|
34
|
+
app.stop();
|
|
35
|
+
app.stop();
|
|
27
36
|
});
|
|
28
37
|
|
|
29
38
|
test('Send and receive', async () => {
|
|
30
|
-
const
|
|
39
|
+
const mockServer = new Server('wss://example.com/ws/agent');
|
|
40
|
+
|
|
41
|
+
mockServer.on('connection', (socket) => {
|
|
42
|
+
socket.on('message', (data) => {
|
|
43
|
+
const command = JSON.parse((data as Buffer).toString('utf8'));
|
|
44
|
+
if (command.type === 'connect') {
|
|
45
|
+
socket.send(
|
|
46
|
+
Buffer.from(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
type: 'connected',
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (command.type === 'transmit') {
|
|
55
|
+
const hl7Message = Hl7Message.parse(command.message);
|
|
56
|
+
const ackMessage = hl7Message.buildAck();
|
|
57
|
+
socket.send(
|
|
58
|
+
Buffer.from(
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
type: 'transmit',
|
|
61
|
+
message: ackMessage.toString(),
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const app = new App(medplum, { botId: bot.id as string });
|
|
31
70
|
app.start();
|
|
32
71
|
|
|
33
72
|
const client = new Hl7Client({
|
|
@@ -46,8 +85,12 @@ describe('Agent', () => {
|
|
|
46
85
|
)
|
|
47
86
|
);
|
|
48
87
|
expect(response).toBeDefined();
|
|
88
|
+
expect(response.header.getComponent(9, 1)).toBe('ACK');
|
|
89
|
+
expect(response.segments).toHaveLength(2);
|
|
90
|
+
expect(response.segments[1].name).toBe('MSA');
|
|
49
91
|
|
|
50
92
|
client.close();
|
|
51
93
|
app.stop();
|
|
94
|
+
mockServer.stop();
|
|
52
95
|
});
|
|
53
96
|
});
|
package/src/main.ts
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Hl7Message, MedplumClient } from '@medplum/core';
|
|
2
|
+
import { Hl7Connection, Hl7MessageEvent, Hl7Server } from '@medplum/hl7';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
4
|
import { EventLogger } from 'node-windows';
|
|
5
|
+
import WebSocket from 'ws';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
7
|
+
export interface AgentConfig {
|
|
8
|
+
botId: string;
|
|
9
|
+
useSystemEventLog?: boolean;
|
|
10
|
+
}
|
|
10
11
|
|
|
11
12
|
export class App {
|
|
12
13
|
readonly log: EventLogger;
|
|
13
14
|
readonly server: Hl7Server;
|
|
15
|
+
readonly connections: Connection[] = [];
|
|
14
16
|
|
|
15
|
-
constructor(readonly medplum: MedplumClient, readonly
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
constructor(readonly medplum: MedplumClient, readonly config: AgentConfig) {
|
|
18
|
+
if (config.useSystemEventLog) {
|
|
19
|
+
this.log = new EventLogger({
|
|
20
|
+
source: 'MedplumService',
|
|
21
|
+
eventLog: 'SYSTEM',
|
|
22
|
+
});
|
|
23
|
+
} else {
|
|
24
|
+
this.log = {
|
|
25
|
+
info: console.log,
|
|
26
|
+
warn: console.warn,
|
|
27
|
+
error: console.error,
|
|
28
|
+
} as EventLogger;
|
|
29
|
+
}
|
|
20
30
|
|
|
21
|
-
this.server = new Hl7Server()
|
|
22
|
-
|
|
31
|
+
this.server = new Hl7Server((connection) => {
|
|
32
|
+
this.log.info('HL7 connection established');
|
|
33
|
+
this.connections.push(new Connection(this, connection));
|
|
34
|
+
});
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
start(): void {
|
|
@@ -30,32 +42,107 @@ export class App {
|
|
|
30
42
|
|
|
31
43
|
stop(): void {
|
|
32
44
|
this.log.info('Medplum service stopping...');
|
|
45
|
+
for (const connection of this.connections) {
|
|
46
|
+
connection.close();
|
|
47
|
+
}
|
|
33
48
|
this.server.stop();
|
|
34
49
|
this.log.info('Medplum service stopped successfully');
|
|
35
50
|
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class Connection {
|
|
54
|
+
readonly webSocket: WebSocket;
|
|
55
|
+
readonly webSocketQueue: Hl7Message[] = [];
|
|
56
|
+
readonly hl7ConnectionQueue: Hl7Message[] = [];
|
|
57
|
+
live = false;
|
|
58
|
+
|
|
59
|
+
constructor(readonly app: App, readonly hl7Connection: Hl7Connection) {
|
|
60
|
+
// Add listener immediately to handle incoming messages
|
|
61
|
+
this.hl7Connection.addEventListener('message', (event) => this.handler(event));
|
|
62
|
+
|
|
63
|
+
const webSocketUrl = new URL(this.app.medplum.getBaseUrl());
|
|
64
|
+
webSocketUrl.protocol = webSocketUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
65
|
+
webSocketUrl.pathname = '/ws/agent';
|
|
66
|
+
console.log('Connecting to WebSocket:', webSocketUrl.href);
|
|
67
|
+
|
|
68
|
+
this.webSocket = new WebSocket(webSocketUrl);
|
|
69
|
+
this.webSocket.binaryType = 'nodebuffer';
|
|
70
|
+
this.webSocket.addEventListener('error', console.error);
|
|
71
|
+
this.webSocket.addEventListener('open', () => {
|
|
72
|
+
this.webSocket.send(
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
type: 'connect',
|
|
75
|
+
accessToken: this.app.medplum.getAccessToken(),
|
|
76
|
+
botId: this.app.config.botId,
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.webSocket.addEventListener('message', (e) => {
|
|
82
|
+
try {
|
|
83
|
+
const data = e.data as Buffer;
|
|
84
|
+
const str = data.toString('utf8');
|
|
85
|
+
console.log('Received from WebSocket:', str.replaceAll('\r', '\n'));
|
|
86
|
+
const command = JSON.parse(str);
|
|
87
|
+
switch (command.type) {
|
|
88
|
+
case 'connected':
|
|
89
|
+
this.live = true;
|
|
90
|
+
this.trySendToWebSocket();
|
|
91
|
+
break;
|
|
92
|
+
case 'transmit':
|
|
93
|
+
this.hl7ConnectionQueue.push(Hl7Message.parse(command.message));
|
|
94
|
+
this.trySendToHl7Connection();
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.log('WebSocket error', err);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
36
102
|
|
|
37
103
|
private async handler(event: Hl7MessageEvent): Promise<void> {
|
|
38
104
|
try {
|
|
39
105
|
console.log('Received:');
|
|
40
106
|
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);
|
|
107
|
+
this.webSocketQueue.push(event.message);
|
|
108
|
+
this.trySendToWebSocket();
|
|
52
109
|
} catch (err) {
|
|
53
110
|
console.log('HL7 error', err);
|
|
54
|
-
log.error(normalizeErrorString(err));
|
|
55
111
|
}
|
|
56
112
|
}
|
|
113
|
+
|
|
114
|
+
private trySendToWebSocket(): void {
|
|
115
|
+
if (this.live) {
|
|
116
|
+
while (this.webSocketQueue.length > 0) {
|
|
117
|
+
const msg = this.webSocketQueue.shift();
|
|
118
|
+
if (msg) {
|
|
119
|
+
this.webSocket.send(
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
type: 'transmit',
|
|
122
|
+
message: msg.toString(),
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private trySendToHl7Connection(): void {
|
|
131
|
+
while (this.hl7ConnectionQueue.length > 0) {
|
|
132
|
+
const msg = this.hl7ConnectionQueue.shift();
|
|
133
|
+
if (msg) {
|
|
134
|
+
this.hl7Connection.send(msg);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
close(): void {
|
|
140
|
+
this.hl7Connection.close();
|
|
141
|
+
this.webSocket.close();
|
|
142
|
+
}
|
|
57
143
|
}
|
|
58
144
|
|
|
59
145
|
if (typeof require !== 'undefined' && require.main === module) {
|
|
60
|
-
|
|
146
|
+
const config = JSON.parse(readFileSync('medplum.config.json', 'utf8'));
|
|
147
|
+
new App(new MedplumClient(config), config).start();
|
|
61
148
|
}
|