@medplum/agent 2.1.6 → 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.
@@ -3523,7 +3523,7 @@ var require_websocket_server = __commonJS({
3523
3523
  socket.once("finish", socket.destroy);
3524
3524
  socket.end(
3525
3525
  `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r
3526
- ` + Object.keys(headers).map((h2) => `${h2}: ${headers[h2]}`).join("\r\n") + "\r\n\r\n" + message
3526
+ ` + Object.keys(headers).map((h3) => `${h3}: ${headers[h3]}`).join("\r\n") + "\r\n\r\n" + message
3527
3527
  );
3528
3528
  }
3529
3529
  function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) {
@@ -3652,9 +3652,6 @@ function K(r4) {
3652
3652
  function $t(r4) {
3653
3653
  return r4.resourceType + "/" + r4.id;
3654
3654
  }
3655
- function So(r4) {
3656
- return r4?.reference?.split("/")[1];
3657
- }
3658
3655
  function un(r4) {
3659
3656
  return r4.resourceType === "Patient" || r4.resourceType === "Practitioner" || r4.resourceType === "RelatedPerson";
3660
3657
  }
@@ -4576,7 +4573,7 @@ function ue() {
4576
4573
  }
4577
4574
  var ri = ue();
4578
4575
  var Cr = ((p2) => (p2.BOOLEAN = "BOOLEAN", p2.NUMBER = "NUMBER", p2.QUANTITY = "QUANTITY", p2.TEXT = "TEXT", p2.REFERENCE = "REFERENCE", p2.CANONICAL = "CANONICAL", p2.DATE = "DATE", p2.DATETIME = "DATETIME", p2.PERIOD = "PERIOD", p2.UUID = "UUID", p2))(Cr || {});
4579
- var ui = ((g) => (g.EQUALS = "eq", g.NOT_EQUALS = "ne", g.GREATER_THAN = "gt", g.LESS_THAN = "lt", g.GREATER_THAN_OR_EQUALS = "ge", g.LESS_THAN_OR_EQUALS = "le", g.STARTS_AFTER = "sa", g.ENDS_BEFORE = "eb", g.APPROXIMATELY = "ap", g.CONTAINS = "contains", g.EXACT = "exact", g.TEXT = "text", g.NOT = "not", g.ABOVE = "above", g.BELOW = "below", g.IN = "in", g.NOT_IN = "not-in", g.OF_TYPE = "of-type", g.MISSING = "missing", g.IDENTIFIER = "identifier", g.ITERATE = "iterate", g))(ui || {});
4576
+ var ui = ((g2) => (g2.EQUALS = "eq", g2.NOT_EQUALS = "ne", g2.GREATER_THAN = "gt", g2.LESS_THAN = "lt", g2.GREATER_THAN_OR_EQUALS = "ge", g2.LESS_THAN_OR_EQUALS = "le", g2.STARTS_AFTER = "sa", g2.ENDS_BEFORE = "eb", g2.APPROXIMATELY = "ap", g2.CONTAINS = "contains", g2.EXACT = "exact", g2.TEXT = "text", g2.NOT = "not", g2.ABOVE = "above", g2.BELOW = "below", g2.IN = "in", g2.NOT_IN = "not-in", g2.OF_TYPE = "of-type", g2.MISSING = "missing", g2.IDENTIFIER = "identifier", g2.ITERATE = "iterate", g2))(ui || {});
4580
4577
  var ki = ((x) => (x.READ = "read", x.VREAD = "vread", x.UPDATE = "update", x.PATCH = "patch", x.DELETE = "delete", x.HISTORY = "history", x.HISTORY_INSTANCE = "history-instance", x.HISTORY_TYPE = "history-type", x.HISTORY_SYSTEM = "history-system", x.CREATE = "create", x.SEARCH = "search", x.SEARCH_TYPE = "search-type", x.SEARCH_SYSTEM = "search-system", x.SEARCH_COMPARTMENT = "search-compartment", x.CAPABILITIES = "capabilities", x.TRANSACTION = "transaction", x.BATCH = "batch", x.OPERATION = "operation", x))(ki || {});
4581
4578
  function Mr(r4) {
4582
4579
  if (typeof window < "u")
@@ -4858,20 +4855,21 @@ var Vt = class {
4858
4855
  return Array.from(this.data.keys())[e];
4859
4856
  }
4860
4857
  };
4861
- var Ki = "https://api.medplum.com/";
4862
- var zi = 1e3;
4863
- var Ji = 6e4;
4858
+ var Ki = F.FHIR_JSON + ", */*; q=0.1";
4859
+ var zi = "https://api.medplum.com/";
4860
+ var Ji = 1e3;
4861
+ var Yi = 6e4;
4864
4862
  var Gr = "Binary/";
4865
4863
  var Kr = { resourceType: "Device", id: "system", deviceName: [{ name: "System" }] };
4866
- var Yi = ((o) => (o.ClientCredentials = "client_credentials", o.AuthorizationCode = "authorization_code", o.RefreshToken = "refresh_token", o.JwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer", o.TokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange", o))(Yi || {});
4867
- var Xi = ((o) => (o.AccessToken = "urn:ietf:params:oauth:token-type:access_token", o.RefreshToken = "urn:ietf:params:oauth:token-type:refresh_token", o.IdToken = "urn:ietf:params:oauth:token-type:id_token", o.Saml1Token = "urn:ietf:params:oauth:token-type:saml1", o.Saml2Token = "urn:ietf:params:oauth:token-type:saml2", o))(Xi || {});
4868
- var Zi = ((e) => (e.JwtBearer = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", e))(Zi || {});
4864
+ var Xi = ((o) => (o.ClientCredentials = "client_credentials", o.AuthorizationCode = "authorization_code", o.RefreshToken = "refresh_token", o.JwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer", o.TokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange", o))(Xi || {});
4865
+ var Zi = ((o) => (o.AccessToken = "urn:ietf:params:oauth:token-type:access_token", o.RefreshToken = "urn:ietf:params:oauth:token-type:refresh_token", o.IdToken = "urn:ietf:params:oauth:token-type:id_token", o.Saml1Token = "urn:ietf:params:oauth:token-type:saml1", o.Saml2Token = "urn:ietf:params:oauth:token-type:saml2", o))(Zi || {});
4866
+ var eo = ((e) => (e.JwtBearer = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", e))(eo || {});
4869
4867
  var zr = class extends fe {
4870
4868
  constructor(t) {
4871
4869
  super();
4872
4870
  if (t?.baseUrl && !t.baseUrl.startsWith("http"))
4873
4871
  throw new Error("Base URL must start with http or https");
4874
- if (this.options = t ?? {}, this.fetch = t?.fetch ?? eo(), this.storage = t?.storage ?? new ze(), this.createPdfImpl = t?.createPdf, this.baseUrl = Yr(t?.baseUrl ?? Ki), this.fhirBaseUrl = Yr(Je(this.baseUrl, t?.fhirUrlPath ?? "fhir/R4/")), this.authorizeUrl = Je(this.baseUrl, t?.authorizeUrl ?? "oauth2/authorize"), this.tokenUrl = Je(this.baseUrl, t?.tokenUrl ?? "oauth2/token"), this.logoutUrl = Je(this.baseUrl, t?.logoutUrl ?? "oauth2/logout"), this.clientId = t?.clientId ?? "", this.clientSecret = t?.clientSecret ?? "", this.onUnauthenticated = t?.onUnauthenticated, this.cacheTime = t?.cacheTime ?? Ji, this.cacheTime > 0 ? this.requestCache = new Qe(t?.resourceCacheSize ?? zi) : this.requestCache = void 0, t?.autoBatchTime ? (this.autoBatchTime = t.autoBatchTime, this.autoBatchQueue = []) : (this.autoBatchTime = 0, this.autoBatchQueue = void 0), t?.accessToken)
4872
+ if (this.options = t ?? {}, this.fetch = t?.fetch ?? to(), this.storage = t?.storage ?? new ze(), this.createPdfImpl = t?.createPdf, this.baseUrl = Yr(t?.baseUrl ?? zi), this.fhirBaseUrl = Yr(Je(this.baseUrl, t?.fhirUrlPath ?? "fhir/R4/")), this.authorizeUrl = Je(this.baseUrl, t?.authorizeUrl ?? "oauth2/authorize"), this.tokenUrl = Je(this.baseUrl, t?.tokenUrl ?? "oauth2/token"), this.logoutUrl = Je(this.baseUrl, t?.logoutUrl ?? "oauth2/logout"), this.clientId = t?.clientId ?? "", this.clientSecret = t?.clientSecret ?? "", this.onUnauthenticated = t?.onUnauthenticated, this.cacheTime = t?.cacheTime ?? Yi, this.cacheTime > 0 ? this.requestCache = new Qe(t?.resourceCacheSize ?? Ji) : this.requestCache = void 0, t?.autoBatchTime ? (this.autoBatchTime = t.autoBatchTime, this.autoBatchQueue = []) : (this.autoBatchTime = 0, this.autoBatchQueue = void 0), t?.accessToken)
4875
4873
  this.setAccessToken(t.accessToken);
4876
4874
  else {
4877
4875
  let n = this.getActiveLogin();
@@ -5256,7 +5254,7 @@ var zr = class extends fe {
5256
5254
  }
5257
5255
  async uploadMedia(t, n, i2, o, s) {
5258
5256
  let a = await this.createBinary(t, i2, n);
5259
- return this.createResource({ ...o, resourceType: "Media", content: { contentType: n, url: Gr + a.id, title: i2 } }, s);
5257
+ return this.createResource({ resourceType: "Media", status: "completed", content: { contentType: n, url: Gr + a.id, title: i2 }, ...o }, s);
5260
5258
  }
5261
5259
  async bulkExport(t = "", n, i2, o) {
5262
5260
  let s = t && `${t}/`, a = this.fhirUrl(`${s}$export`);
@@ -5376,7 +5374,7 @@ var zr = class extends fe {
5376
5374
  }
5377
5375
  addFetchOptionsDefaults(t) {
5378
5376
  let n = t.headers;
5379
- n || (n = {}, t.headers = n), n.Accept = F.FHIR_JSON, n["X-Medplum"] = "extended", t.body && !n["Content-Type"] && (n["Content-Type"] = F.FHIR_JSON), this.accessToken ? n.Authorization = "Bearer " + this.accessToken : this.basicAuth && (n.Authorization = "Basic " + this.basicAuth), t.cache || (t.cache = "no-cache"), t.credentials || (t.credentials = "include");
5377
+ n || (n = {}, t.headers = n), n.Accept || (n.Accept = Ki), n["X-Medplum"] = "extended", t.body && !n["Content-Type"] && (n["Content-Type"] = F.FHIR_JSON), this.accessToken ? n.Authorization = "Bearer " + this.accessToken : this.basicAuth && (n.Authorization = "Basic " + this.basicAuth), t.cache || (t.cache = "no-cache"), t.credentials || (t.credentials = "include");
5380
5378
  }
5381
5379
  setRequestContentType(t, n) {
5382
5380
  t.headers || (t.headers = {});
@@ -5505,7 +5503,7 @@ var zr = class extends fe {
5505
5503
  throw i2;
5506
5504
  }
5507
5505
  };
5508
- function eo() {
5506
+ function to() {
5509
5507
  if (!globalThis.fetch)
5510
5508
  throw new Error("Fetch not available in this environment");
5511
5509
  return globalThis.fetch.bind(globalThis);
@@ -5534,10 +5532,10 @@ function Zr(r4) {
5534
5532
  let e = r4.entry?.map((t) => t.resource) ?? [];
5535
5533
  return Object.assign(e, { bundle: r4 });
5536
5534
  }
5537
- var to = [...ce, "->", "<<", ">>"];
5538
- var ro = ue().registerInfix("->", { precedence: f.Arrow }).registerInfix(";", { precedence: f.Semicolon });
5539
- var no = [...ce, "eq", "ne", "co"];
5540
- var so = ue();
5535
+ var ro = [...ce, "->", "<<", ">>"];
5536
+ var no = ue().registerInfix("->", { precedence: f.Arrow }).registerInfix(";", { precedence: f.Semicolon });
5537
+ var io = [...ce, "eq", "ne", "co"];
5538
+ var ao = ue();
5541
5539
  var G = class {
5542
5540
  constructor(e = "\r", t = "|", n = "^", i2 = "~", o = "\\", s = "&") {
5543
5541
  this.segmentSeparator = e;
@@ -5578,7 +5576,7 @@ var nn = class r {
5578
5576
  }
5579
5577
  buildAck() {
5580
5578
  let e = /* @__PURE__ */ new Date(), t = this.getSegment("MSH"), n = t?.getField(3)?.toString() ?? "", i2 = t?.getField(4)?.toString() ?? "", o = t?.getField(5)?.toString() ?? "", s = t?.getField(6)?.toString() ?? "", a = t?.getField(10)?.toString() ?? "", c2 = t?.getField(12)?.toString() ?? "2.5.1";
5581
- return new r([new me(["MSH", this.context.getMsh2(), o, s, n, i2, co(e), "", this.buildAckMessageType(t), e.getTime().toString(), "P", c2], this.context), new me(["MSA", "AA", a, "OK"], this.context)]);
5579
+ return new r([new me(["MSH", this.context.getMsh2(), o, s, n, i2, uo(e), "", this.buildAckMessageType(t), e.getTime().toString(), "P", c2], this.context), new me(["MSA", "AA", a, "OK"], this.context)]);
5582
5580
  }
5583
5581
  buildAckMessageType(e) {
5584
5582
  let t = e?.getField(9), n = t?.getComponent(2), i2 = t?.getComponent(3), o = "ACK";
@@ -5639,13 +5637,14 @@ var oe = class r3 {
5639
5637
  return new r3(e.split(t.repetitionSeparator).map((n) => n.split(t.componentSeparator)), t);
5640
5638
  }
5641
5639
  };
5642
- function co(r4) {
5640
+ function uo(r4) {
5643
5641
  let e = r4 instanceof Date ? r4 : new Date(r4), n = e.toISOString().replace(/[-:T]/g, "").replace(/(\.\d+)?Z$/, ""), i2 = e.getUTCMilliseconds();
5644
5642
  return i2 > 0 && (n += "." + i2.toString()), n;
5645
5643
  }
5646
5644
 
5647
5645
  // ../hl7/dist/esm/index.mjs
5648
- var import_net = __toESM(require("net"), 1);
5646
+ var import_net = require("net");
5647
+ var import_net2 = __toESM(require("net"), 1);
5649
5648
  var i = class extends EventTarget {
5650
5649
  addEventListener(n, e, t) {
5651
5650
  super.addEventListener(n, e, t);
@@ -5705,12 +5704,34 @@ var c = class extends i {
5705
5704
  this.socket.end(), this.socket.destroy();
5706
5705
  }
5707
5706
  };
5707
+ var h2 = class extends i {
5708
+ constructor(e) {
5709
+ super();
5710
+ this.options = e, this.host = this.options.host, this.port = this.options.port;
5711
+ }
5712
+ connect() {
5713
+ return this.connection ? Promise.resolve(this.connection) : new Promise((e) => {
5714
+ let t = (0, import_net.connect)({ host: this.host, port: this.port }, () => {
5715
+ this.connection = new c(t), e(this.connection);
5716
+ });
5717
+ });
5718
+ }
5719
+ async send(e) {
5720
+ return (await this.connect()).send(e);
5721
+ }
5722
+ async sendAndWait(e) {
5723
+ return (await this.connect()).sendAndWait(e);
5724
+ }
5725
+ close() {
5726
+ this.connection && (this.connection.close(), delete this.connection);
5727
+ }
5728
+ };
5708
5729
  var E2 = class {
5709
5730
  constructor(n) {
5710
5731
  this.handler = n;
5711
5732
  }
5712
5733
  start(n, e) {
5713
- let t = import_net.default.createServer((o) => {
5734
+ let t = import_net2.default.createServer((o) => {
5714
5735
  let s = new c(o, e);
5715
5736
  this.handler(s);
5716
5737
  });
@@ -5734,80 +5755,28 @@ var App = class {
5734
5755
  constructor(medplum, agentId) {
5735
5756
  this.medplum = medplum;
5736
5757
  this.agentId = agentId;
5758
+ this.webSocketQueue = [];
5759
+ this.channels = /* @__PURE__ */ new Map();
5760
+ this.hl7Queue = [];
5761
+ this.live = false;
5737
5762
  this.log = {
5738
5763
  info: console.log,
5739
5764
  warn: console.warn,
5740
5765
  error: console.error
5741
5766
  };
5742
- this.channels = [];
5743
- }
5744
- async start() {
5745
- this.log.info("Medplum service starting...");
5746
- const agent = await this.medplum.readResource("Agent", this.agentId);
5747
- for (const definition of agent.channel) {
5748
- const endpoint = await this.medplum.readReference(definition.endpoint);
5749
- const channel = new AgentHl7Channel(this, definition, endpoint);
5750
- channel.start();
5751
- this.channels.push(channel);
5752
- }
5753
- this.log.info("Medplum service started successfully");
5754
- }
5755
- stop() {
5756
- this.log.info("Medplum service stopping...");
5757
- this.channels.forEach((channel) => channel.stop());
5758
- this.log.info("Medplum service stopped successfully");
5759
- }
5760
- };
5761
- var AgentHl7Channel = class {
5762
- constructor(app, definition, endpoint) {
5763
- this.app = app;
5764
- this.definition = definition;
5765
- this.endpoint = endpoint;
5766
- this.connections = [];
5767
- this.server = new E2((connection) => {
5768
- this.app.log.info("HL7 connection established");
5769
- this.connections.push(new AgentHl7ChannelConnection(this, connection));
5770
- });
5771
- }
5772
- start() {
5773
- const address = new URL(this.endpoint.address);
5774
- this.app.log.info(`Channel starting on ${address}`);
5775
- this.server.start(parseInt(address.port, 10));
5776
- this.app.log.info("Channel started successfully");
5777
- }
5778
- stop() {
5779
- this.app.log.info("Channel stopping...");
5780
- for (const connection of this.connections) {
5781
- connection.close();
5782
- }
5783
- this.server.stop();
5784
- this.app.log.info("Channel stopped successfully");
5785
- }
5786
- };
5787
- var AgentHl7ChannelConnection = class {
5788
- constructor(channel, hl7Connection) {
5789
- this.channel = channel;
5790
- this.hl7Connection = hl7Connection;
5791
- this.webSocketQueue = [];
5792
- this.hl7ConnectionQueue = [];
5793
- this.live = false;
5794
- const app = channel.app;
5795
- const medplum = app.medplum;
5796
- this.hl7Connection.addEventListener("message", (event) => this.handler(event));
5797
5767
  const webSocketUrl = new URL(medplum.getBaseUrl());
5798
5768
  webSocketUrl.protocol = webSocketUrl.protocol === "https:" ? "wss:" : "ws:";
5799
5769
  webSocketUrl.pathname = "/ws/agent";
5800
- console.log("Connecting to WebSocket:", webSocketUrl.href);
5770
+ this.log.info(`Connecting to WebSocket: ${webSocketUrl.href}`);
5801
5771
  this.webSocket = new wrapper_default(webSocketUrl);
5802
5772
  this.webSocket.binaryType = "nodebuffer";
5803
- this.webSocket.addEventListener("error", console.error);
5773
+ this.webSocket.addEventListener("error", (err) => this.log.error(err.message));
5804
5774
  this.webSocket.addEventListener("open", () => {
5805
5775
  this.webSocket.send(
5806
5776
  JSON.stringify({
5807
5777
  type: "connect",
5808
5778
  accessToken: medplum.getAccessToken(),
5809
- agentId: channel.app.agentId,
5810
- botId: So(channel.definition.targetReference)
5779
+ agentId
5811
5780
  })
5812
5781
  );
5813
5782
  });
@@ -5815,7 +5784,7 @@ var AgentHl7ChannelConnection = class {
5815
5784
  try {
5816
5785
  const data = e.data;
5817
5786
  const str = data.toString("utf8");
5818
- console.log("Received from WebSocket:", str.replaceAll("\r", "\n"));
5787
+ this.log.info(`Received from WebSocket: ${str.replaceAll("\r", "\n")}`);
5819
5788
  const command = JSON.parse(str);
5820
5789
  switch (command.type) {
5821
5790
  case "connected":
@@ -5823,24 +5792,40 @@ var AgentHl7ChannelConnection = class {
5823
5792
  this.trySendToWebSocket();
5824
5793
  break;
5825
5794
  case "transmit":
5826
- this.hl7ConnectionQueue.push(nn.parse(command.message));
5827
- this.trySendToHl7Connection();
5795
+ this.addToHl7Queue(command);
5796
+ break;
5797
+ case "push":
5798
+ this.pushMessage(command);
5828
5799
  break;
5829
5800
  }
5830
5801
  } catch (err) {
5831
- console.log("WebSocket error", err);
5802
+ this.log.error(`WebSocket error: ${qn(err)}`);
5832
5803
  }
5833
5804
  });
5834
5805
  }
5835
- async handler(event) {
5836
- try {
5837
- console.log("Received:");
5838
- console.log(event.message.toString().replaceAll("\r", "\n"));
5839
- this.webSocketQueue.push(event.message);
5840
- this.trySendToWebSocket();
5841
- } catch (err) {
5842
- console.log("HL7 error", err);
5806
+ async start() {
5807
+ this.log.info("Medplum service starting...");
5808
+ const agent = await this.medplum.readResource("Agent", this.agentId);
5809
+ for (const definition of agent.channel) {
5810
+ const endpoint = await this.medplum.readReference(definition.endpoint);
5811
+ const channel = new AgentHl7Channel(this, definition, endpoint);
5812
+ channel.start();
5813
+ this.channels.set(definition.name, channel);
5843
5814
  }
5815
+ this.log.info("Medplum service started successfully");
5816
+ }
5817
+ stop() {
5818
+ this.log.info("Medplum service stopping...");
5819
+ this.channels.forEach((channel) => channel.stop());
5820
+ this.log.info("Medplum service stopped successfully");
5821
+ }
5822
+ addToWebSocketQueue(message) {
5823
+ this.webSocketQueue.push(message);
5824
+ this.trySendToWebSocket();
5825
+ }
5826
+ addToHl7Queue(message) {
5827
+ this.hl7Queue.push(message);
5828
+ this.trySendToHl7Connection();
5844
5829
  }
5845
5830
  trySendToWebSocket() {
5846
5831
  if (this.live) {
@@ -5850,8 +5835,8 @@ var AgentHl7ChannelConnection = class {
5850
5835
  this.webSocket.send(
5851
5836
  JSON.stringify({
5852
5837
  type: "transmit",
5853
- forwardedFor: this.hl7Connection.socket.remoteAddress,
5854
- message: msg.toString()
5838
+ accessToken: this.medplum.getAccessToken(),
5839
+ ...msg
5855
5840
  })
5856
5841
  );
5857
5842
  }
@@ -5859,16 +5844,82 @@ var AgentHl7ChannelConnection = class {
5859
5844
  }
5860
5845
  }
5861
5846
  trySendToHl7Connection() {
5862
- while (this.hl7ConnectionQueue.length > 0) {
5863
- const msg = this.hl7ConnectionQueue.shift();
5847
+ while (this.hl7Queue.length > 0) {
5848
+ const msg = this.hl7Queue.shift();
5864
5849
  if (msg) {
5865
- this.hl7Connection.send(msg);
5850
+ const channel = this.channels.get(msg.channel);
5851
+ if (channel) {
5852
+ const connection = channel.connections.get(msg.remote);
5853
+ if (connection) {
5854
+ connection.hl7Connection.send(nn.parse(msg.body));
5855
+ }
5856
+ }
5866
5857
  }
5867
5858
  }
5868
5859
  }
5860
+ pushMessage(message) {
5861
+ const address = new URL(message.remote);
5862
+ const client = new h2({
5863
+ host: address.hostname,
5864
+ port: parseInt(address.port, 10)
5865
+ });
5866
+ client.sendAndWait(nn.parse(message.body)).then((response) => {
5867
+ this.log.info(`Response: ${response.toString().replaceAll("\r", "\n")}`);
5868
+ }).catch((err) => {
5869
+ this.log.error(`HL7 error: ${qn(err)}`);
5870
+ }).finally(() => {
5871
+ client.close();
5872
+ });
5873
+ }
5874
+ };
5875
+ var AgentHl7Channel = class {
5876
+ constructor(app, definition, endpoint) {
5877
+ this.app = app;
5878
+ this.definition = definition;
5879
+ this.endpoint = endpoint;
5880
+ this.connections = /* @__PURE__ */ new Map();
5881
+ this.server = new E2((connection) => this.handleNewConnection(connection));
5882
+ }
5883
+ start() {
5884
+ const address = new URL(this.endpoint.address);
5885
+ this.app.log.info(`Channel starting on ${address}`);
5886
+ this.server.start(parseInt(address.port, 10));
5887
+ this.app.log.info("Channel started successfully");
5888
+ }
5889
+ stop() {
5890
+ this.app.log.info("Channel stopping...");
5891
+ this.connections.forEach((connection) => connection.close());
5892
+ this.server.stop();
5893
+ this.app.log.info("Channel stopped successfully");
5894
+ }
5895
+ handleNewConnection(connection) {
5896
+ const c2 = new AgentHl7ChannelConnection(this, connection);
5897
+ this.app.log.info(`HL7 connection established: ${c2.remote}`);
5898
+ this.connections.set(c2.remote, c2);
5899
+ }
5900
+ };
5901
+ var AgentHl7ChannelConnection = class {
5902
+ constructor(channel, hl7Connection) {
5903
+ this.channel = channel;
5904
+ this.hl7Connection = hl7Connection;
5905
+ this.remote = `${hl7Connection.socket.remoteAddress}:${hl7Connection.socket.remotePort}`;
5906
+ this.hl7Connection.addEventListener("message", (event) => this.handler(event));
5907
+ }
5908
+ async handler(event) {
5909
+ try {
5910
+ this.channel.app.log.info("Received:");
5911
+ this.channel.app.log.info(event.message.toString().replaceAll("\r", "\n"));
5912
+ this.channel.app.addToWebSocketQueue({
5913
+ channel: this.channel.definition.name,
5914
+ remote: this.remote,
5915
+ body: event.message.toString()
5916
+ });
5917
+ } catch (err) {
5918
+ this.channel.app.log.error(`HL7 error: ${qn(err)}`);
5919
+ }
5920
+ }
5869
5921
  close() {
5870
5922
  this.hl7Connection.close();
5871
- this.webSocket.close();
5872
5923
  }
5873
5924
  };
5874
5925
  if (typeof require !== "undefined" && require.main === module) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medplum/agent",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "Medplum Agent",
5
5
  "author": "Medplum <hello@medplum.com>",
6
6
  "license": "Apache-2.0",
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