@medplum/agent 2.1.5 → 2.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medplum/agent",
3
- "version": "2.1.5",
3
+ "version": "2.1.7",
4
4
  "description": "Medplum Agent",
5
5
  "author": "Medplum <hello@medplum.com>",
6
6
  "license": "Apache-2.0",
@@ -25,13 +25,13 @@
25
25
  "@medplum/core": "*",
26
26
  "@medplum/hl7": "*",
27
27
  "node-windows": "1.0.0-beta.8",
28
- "ws": "8.14.1"
28
+ "ws": "8.14.2"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@medplum/fhirtypes": "*",
32
32
  "@medplum/mock": "*",
33
- "@types/node-windows": "0.1.3",
34
- "@types/ws": "8.5.5",
33
+ "@types/node-windows": "0.1.4",
34
+ "@types/ws": "8.5.7",
35
35
  "mock-socket": "9.3.1",
36
36
  "pkg": "5.8.1"
37
37
  }
package/src/main.test.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { allOk, createReference, Hl7Message } from '@medplum/core';
1
+ import { allOk, createReference, Hl7Message, sleep } from '@medplum/core';
2
2
  import { Agent, Bot, Endpoint, Resource } from '@medplum/fhirtypes';
3
- import { Hl7Client } from '@medplum/hl7';
3
+ import { Hl7Client, Hl7Server } from '@medplum/hl7';
4
4
  import { MockClient } from '@medplum/mock';
5
- import { Server } from 'mock-socket';
5
+ import { Client, Server } from 'mock-socket';
6
6
  import { App } from './main';
7
7
 
8
8
  jest.mock('node-windows');
@@ -23,11 +23,28 @@ describe('Agent', () => {
23
23
 
24
24
  endpoint = await medplum.createResource<Endpoint>({
25
25
  resourceType: 'Endpoint',
26
- address: 'mllp://0.0.0.0:56000',
26
+ address: 'mllp://0.0.0.0:57000',
27
27
  });
28
28
  });
29
29
 
30
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
+
31
48
  const agent = await medplum.createResource<Agent>({
32
49
  resourceType: 'Agent',
33
50
  channel: [
@@ -42,6 +59,7 @@ describe('Agent', () => {
42
59
  await app.start();
43
60
  app.stop();
44
61
  app.stop();
62
+ mockServer.stop();
45
63
  });
46
64
 
47
65
  test('Send and receive', async () => {
@@ -61,13 +79,15 @@ describe('Agent', () => {
61
79
  }
62
80
 
63
81
  if (command.type === 'transmit') {
64
- const hl7Message = Hl7Message.parse(command.message);
82
+ const hl7Message = Hl7Message.parse(command.body);
65
83
  const ackMessage = hl7Message.buildAck();
66
84
  socket.send(
67
85
  Buffer.from(
68
86
  JSON.stringify({
69
87
  type: 'transmit',
70
- message: ackMessage.toString(),
88
+ channel: command.channel,
89
+ remote: command.remote,
90
+ body: ackMessage.toString(),
71
91
  })
72
92
  )
73
93
  );
@@ -79,6 +99,7 @@ describe('Agent', () => {
79
99
  resourceType: 'Agent',
80
100
  channel: [
81
101
  {
102
+ name: 'test',
82
103
  endpoint: createReference(endpoint),
83
104
  targetReference: createReference(bot),
84
105
  },
@@ -90,11 +111,9 @@ describe('Agent', () => {
90
111
 
91
112
  const client = new Hl7Client({
92
113
  host: 'localhost',
93
- port: 56000,
114
+ port: 57000,
94
115
  });
95
116
 
96
- await client.connect();
97
-
98
117
  const response = await client.sendAndWait(
99
118
  Hl7Message.parse(
100
119
  'MSH|^~\\&|ADT1|MCM|LABADT|MCM|198808181126|SECURITY|ADT^A01|MSG00001|P|2.2\r' +
@@ -112,4 +131,90 @@ describe('Agent', () => {
112
131
  app.stop();
113
132
  mockServer.stop();
114
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
+ });
115
220
  });
package/src/main.ts CHANGED
@@ -1,12 +1,22 @@
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';
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
4
  import { EventLogger } from 'node-windows';
5
5
  import WebSocket from 'ws';
6
6
 
7
+ interface QueueItem {
8
+ channel: string;
9
+ remote: string;
10
+ body: string;
11
+ }
12
+
7
13
  export class App {
8
14
  readonly log: EventLogger;
9
- readonly channels: AgentHl7Channel[];
15
+ readonly webSocket: WebSocket;
16
+ readonly webSocketQueue: QueueItem[] = [];
17
+ readonly channels = new Map<string, AgentHl7Channel>();
18
+ readonly hl7Queue: QueueItem[] = [];
19
+ live = false;
10
20
 
11
21
  constructor(
12
22
  readonly medplum: MedplumClient,
@@ -18,7 +28,46 @@ export class App {
18
28
  error: console.error,
19
29
  } as EventLogger;
20
30
 
21
- this.channels = [];
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
+ });
22
71
  }
23
72
 
24
73
  async start(): Promise<void> {
@@ -30,7 +79,7 @@ export class App {
30
79
  const endpoint = await this.medplum.readReference(definition.endpoint as Reference<Endpoint>);
31
80
  const channel = new AgentHl7Channel(this, definition, endpoint);
32
81
  channel.start();
33
- this.channels.push(channel);
82
+ this.channels.set(definition.name as string, channel);
34
83
  }
35
84
 
36
85
  this.log.info('Medplum service started successfully');
@@ -41,21 +90,80 @@ export class App {
41
90
  this.channels.forEach((channel) => channel.stop());
42
91
  this.log.info('Medplum service stopped successfully');
43
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
+ }
44
155
  }
45
156
 
46
157
  export class AgentHl7Channel {
47
158
  readonly server: Hl7Server;
48
- readonly connections: AgentHl7ChannelConnection[] = [];
159
+ readonly connections = new Map<string, AgentHl7ChannelConnection>();
49
160
 
50
161
  constructor(
51
162
  readonly app: App,
52
163
  readonly definition: AgentChannel,
53
164
  readonly endpoint: Endpoint
54
165
  ) {
55
- this.server = new Hl7Server((connection) => {
56
- this.app.log.info('HL7 connection established');
57
- this.connections.push(new AgentHl7ChannelConnection(this, connection));
58
- });
166
+ this.server = new Hl7Server((connection) => this.handleNewConnection(connection));
59
167
  }
60
168
 
61
169
  start(): void {
@@ -67,111 +175,47 @@ export class AgentHl7Channel {
67
175
 
68
176
  stop(): void {
69
177
  this.app.log.info('Channel stopping...');
70
- for (const connection of this.connections) {
71
- connection.close();
72
- }
178
+ this.connections.forEach((connection) => connection.close());
73
179
  this.server.stop();
74
180
  this.app.log.info('Channel stopped successfully');
75
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
+ }
76
188
  }
77
189
 
78
190
  export class AgentHl7ChannelConnection {
79
- readonly webSocket: WebSocket;
80
- readonly webSocketQueue: Hl7Message[] = [];
81
- readonly hl7ConnectionQueue: Hl7Message[] = [];
82
- live = false;
191
+ readonly remote: string;
83
192
 
84
193
  constructor(
85
194
  readonly channel: AgentHl7Channel,
86
195
  readonly hl7Connection: Hl7Connection
87
196
  ) {
88
- const app = channel.app;
89
- const medplum = app.medplum;
197
+ this.remote = `${hl7Connection.socket.remoteAddress}:${hl7Connection.socket.remotePort}`;
90
198
 
91
199
  // Add listener immediately to handle incoming messages
92
200
  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
- agentId: channel.app.agentId,
108
- botId: resolveId(channel.definition.targetReference as Reference<Bot>),
109
- })
110
- );
111
- });
112
-
113
- this.webSocket.addEventListener('message', (e) => {
114
- try {
115
- const data = e.data as Buffer;
116
- const str = data.toString('utf8');
117
- console.log('Received from WebSocket:', str.replaceAll('\r', '\n'));
118
- const command = JSON.parse(str);
119
- switch (command.type) {
120
- case 'connected':
121
- this.live = true;
122
- this.trySendToWebSocket();
123
- break;
124
- case 'transmit':
125
- this.hl7ConnectionQueue.push(Hl7Message.parse(command.message));
126
- this.trySendToHl7Connection();
127
- break;
128
- }
129
- } catch (err) {
130
- console.log('WebSocket error', err);
131
- }
132
- });
133
201
  }
134
202
 
135
203
  private async handler(event: Hl7MessageEvent): Promise<void> {
136
204
  try {
137
- console.log('Received:');
138
- console.log(event.message.toString().replaceAll('\r', '\n'));
139
- this.webSocketQueue.push(event.message);
140
- this.trySendToWebSocket();
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
+ });
141
212
  } catch (err) {
142
- console.log('HL7 error', err);
143
- }
144
- }
145
-
146
- private trySendToWebSocket(): void {
147
- if (this.live) {
148
- while (this.webSocketQueue.length > 0) {
149
- const msg = this.webSocketQueue.shift();
150
- if (msg) {
151
- this.webSocket.send(
152
- JSON.stringify({
153
- type: 'transmit',
154
- forwardedFor: this.hl7Connection.socket.remoteAddress,
155
- message: msg.toString(),
156
- })
157
- );
158
- }
159
- }
160
- }
161
- }
162
-
163
- private trySendToHl7Connection(): void {
164
- while (this.hl7ConnectionQueue.length > 0) {
165
- const msg = this.hl7ConnectionQueue.shift();
166
- if (msg) {
167
- this.hl7Connection.send(msg);
168
- }
213
+ this.channel.app.log.error(`HL7 error: ${normalizeErrorString(err)}`);
169
214
  }
170
215
  }
171
216
 
172
217
  close(): void {
173
218
  this.hl7Connection.close();
174
- this.webSocket.close();
175
219
  }
176
220
  }
177
221
 
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "../../tsconfig.base.json",
2
+ "extends": "../../tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "outDir": "dist",
5
5
  "lib": ["esnext"]