This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-25755-jms-request-reply in repository https://gitbox.apache.org/repos/asf/camel-jbang-examples.git
commit fe873eed97068d7cf7e9afd0e2250e24b766603a Author: Claus Ibsen <[email protected]> AuthorDate: Sun Oct 18 13:09:20 2026 +0200 CAMEL-25755: connect-service/jms-request-reply - request-reply, a dead letter queue and a topic on Artemis Traditional messaging beyond a queue: each order asks a stock service over JMS request-reply (InOut, the answer kept in a variable), ORD-1003 cannot be reserved and goes to orders.dead after three redeliveries, and the reserved orders are published on a topic with two subscribers. Placed after artemis in the group, with a Citrus YAML test in the CI matrix. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .github/workflows/build.yml | 2 + README.md | 1 + camel-jbang-example-catalog.json | 65 +++++++++- connect-service/README.md | 1 + connect-service/camel-1-tribute/metadata.json | 2 +- connect-service/ftp/metadata.json | 2 +- connect-service/jms-request-reply/README.md | 104 ++++++++++++++++ .../jms-request-reply/application.properties | 23 ++++ .../jms-request-reply/jms-request-reply.camel.yaml | 134 +++++++++++++++++++++ connect-service/jms-request-reply/metadata.json | 46 +++++++ .../jms-request-reply/orders/order-1001.json | 2 + .../jms-request-reply/orders/order-1002.json | 2 + .../jms-request-reply/orders/order-1003.json | 2 + connect-service/jms-request-reply/stock.json | 5 + .../test/jms-request-reply.citrus.it.yaml | 41 +++++++ connect-service/kafka-orders/metadata.json | 2 +- connect-service/mqtt/metadata.json | 2 +- 17 files changed, 428 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05cc0cd..1c615e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,8 @@ jobs: path: connect-service/kafka-orders - name: Artemis queue path: connect-service/artemis + - name: JMS request-reply + path: connect-service/jms-request-reply - name: FTP courier path: connect-service/ftp - name: Camel 1.0 tribute diff --git a/README.md b/README.md index 290139b..93d7cba 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ SQL, JMS, MQTT, Kafka and FTP against a service the Camel CLI starts for you wit |---|---|---| | [SQL database](connect-service/sql/) | Against a Postgres started with camel infra, a customers table is created, the three orders register their customers with an insert that counts orders on conflict, and every ten seconds a select lists the customers; run it twice and the order counts go up. | `camel infra run postgres` | | [Artemis queue](connect-service/artemis/) | The three orders are put on the orders queue of an ActiveMQ Artemis broker started with camel infra, and a consumer route takes them off and logs the customer and line count; the connection factory is declared in application.properties. | `camel infra run artemis` | +| [JMS request-reply](connect-service/jms-request-reply/) | The three orders go on a queue of an ActiveMQ Artemis broker started with camel infra; a consumer asks a stock service over JMS request-reply for each, publishes the reserved ones on a topic that shipping and invoicing both subscribe to, and ORD-1003, which the stock service cannot reserve, is redelivered three times and then moved to a dead letter queue. | `camel infra run artemis` | | [MQTT sensors](connect-service/mqtt/) | Temperature sensors in the warehouse's cold rooms publish JSON readings on an MQTT topic of a Mosquitto broker started with camel infra; a consumer logs each reading and raises a warning when a room is above 8 degrees. | `camel infra run mosquitto` | | [Kafka orders](connect-service/kafka-orders/) | The three orders go through a shared validation route to a Kafka topic on a broker started with camel infra; the pending one is stopped, the paid ones are dispatched to the fulfilment and notifications topics, whose consumers log the picking and the email; camel cmd route-topology draws the flow. | `camel infra run kafka` | | [FTP courier](connect-service/ftp/) | Each order on the shipments queue of an Artemis broker becomes a file on the courier's FTP server, both started with camel infra; the log shows each shipment uploaded and camel infra get ftp shows where the files landed. | `camel infra run artemis ftp` | diff --git a/camel-jbang-example-catalog.json b/camel-jbang-example-catalog.json index 0be9e36..fda1cd7 100644 --- a/camel-jbang-example-catalog.json +++ b/camel-jbang-example-catalog.json @@ -299,7 +299,7 @@ "infraServices": [ "artemis" ], - "order": 6 + "order": 7 }, { "name": "connect-service/ftp", @@ -343,7 +343,64 @@ "artemis", "ftp" ], - "order": 5 + "order": 6 + }, + { + "name": "connect-service/jms-request-reply", + "title": "JMS request-reply", + "description": "The three orders go on a queue of an ActiveMQ Artemis broker started with camel infra; a consumer asks a stock service over JMS request-reply for each, publishes the reserved ones on a topic that shipping and invoicing both subscribe to, and ORD-1003, which the stock service cannot reserve, is redelivered three times and then moved to a dead letter queue.", + "level": "connect-service", + "teaches": { + "components": [ + "jms", + "file", + "log" + ], + "eips": [ + "errorHandler", + "deadLetterChannel", + "redeliveryPolicy", + "filter", + "throwException", + "setVariable", + "setHeader", + "unmarshal" + ], + "languages": [ + "simple", + "jsonpath", + "groovy" + ], + "dataformats": [ + "json" + ] + }, + "tags": [ + "shop", + "jms", + "artemis", + "queue", + "topic", + "request-reply", + "dead-letter", + "messaging" + ], + "bundled": false, + "requiresDocker": true, + "hasCitrusTests": true, + "files": [ + "README.md", + "application.properties", + "jms-request-reply.camel.yaml", + "orders/order-1001.json", + "orders/order-1002.json", + "orders/order-1003.json", + "stock.json" + ], + "infraServices": [ + "artemis" + ], + "order": 3 }, { "name": "connect-service/kafka-orders", @@ -392,7 +449,7 @@ "infraServices": [ "kafka" ], - "order": 4 + "order": 5 }, { "name": "connect-service/mqtt", @@ -433,7 +490,7 @@ "infraServices": [ "mosquitto" ], - "order": 3 + "order": 4 }, { "name": "connect-service/sql", diff --git a/connect-service/README.md b/connect-service/README.md index a03f15c..83d71ab 100644 --- a/connect-service/README.md +++ b/connect-service/README.md @@ -7,6 +7,7 @@ SQL, JMS, MQTT, Kafka and FTP against a service the Camel CLI starts for you wit |---|---|---| | [SQL database](sql/) | Against a Postgres started with camel infra, a customers table is created, the three orders register their customers with an insert that counts orders on conflict, and every ten seconds a select lists the customers; run it twice and the order counts go up. | `camel infra run postgres` | | [Artemis queue](artemis/) | The three orders are put on the orders queue of an ActiveMQ Artemis broker started with camel infra, and a consumer route takes them off and logs the customer and line count; the connection factory is declared in application.properties. | `camel infra run artemis` | +| [JMS request-reply](jms-request-reply/) | The three orders go on a queue of an ActiveMQ Artemis broker started with camel infra; a consumer asks a stock service over JMS request-reply for each, publishes the reserved ones on a topic that shipping and invoicing both subscribe to, and ORD-1003, which the stock service cannot reserve, is redelivered three times and then moved to a dead letter queue. | `camel infra run artemis` | | [MQTT sensors](mqtt/) | Temperature sensors in the warehouse's cold rooms publish JSON readings on an MQTT topic of a Mosquitto broker started with camel infra; a consumer logs each reading and raises a warning when a room is above 8 degrees. | `camel infra run mosquitto` | | [Kafka orders](kafka-orders/) | The three orders go through a shared validation route to a Kafka topic on a broker started with camel infra; the pending one is stopped, the paid ones are dispatched to the fulfilment and notifications topics, whose consumers log the picking and the email; camel cmd route-topology draws the flow. | `camel infra run kafka` | | [FTP courier](ftp/) | Each order on the shipments queue of an Artemis broker becomes a file on the courier's FTP server, both started with camel infra; the log shows each shipment uploaded and camel infra get ftp shows where the files landed. | `camel infra run artemis ftp` | diff --git a/connect-service/camel-1-tribute/metadata.json b/connect-service/camel-1-tribute/metadata.json index 0d90ac0..7b307c6 100644 --- a/connect-service/camel-1-tribute/metadata.json +++ b/connect-service/camel-1-tribute/metadata.json @@ -2,7 +2,7 @@ "title": "Camel 1.0 tribute", "description": "The very first Camel example of 2007, JMS to file: a timer sends ten messages to a queue on an Artemis broker started with camel infra, and the consumer writes each message to a file in outbox; the log shows both sides.", "level": "connect-service", - "order": 6, + "order": 7, "teaches": { "components": [ "jms", diff --git a/connect-service/ftp/metadata.json b/connect-service/ftp/metadata.json index 79b0da1..22d3318 100644 --- a/connect-service/ftp/metadata.json +++ b/connect-service/ftp/metadata.json @@ -2,7 +2,7 @@ "title": "FTP courier", "description": "Each order on the shipments queue of an Artemis broker becomes a file on the courier's FTP server, both started with camel infra; the log shows each shipment uploaded and camel infra get ftp shows where the files landed.", "level": "connect-service", - "order": 5, + "order": 6, "teaches": { "components": [ "ftp", diff --git a/connect-service/jms-request-reply/README.md b/connect-service/jms-request-reply/README.md new file mode 100644 index 0000000..e0067bb --- /dev/null +++ b/connect-service/jms-request-reply/README.md @@ -0,0 +1,104 @@ +# JMS request-reply + +Traditional messaging with a broker. The orders go on a queue, each order asks a stock service for a reservation +and waits for the answer, and a reserved order is published on a topic that two departments subscribe to. An order +that cannot be reserved is tried three more times and then moved to a dead letter queue. + +## What you will see + +```text +INFO ... jms-request-reply.camel.yaml:28 : Order ORD-1001 put on the queue +INFO ... jms-request-reply.camel.yaml:47 : Reserving ORD-1001 for customer C-482 +INFO ... jms-request-reply.camel.yaml:56 : Stock service answered for ORD-1001: reserved +INFO ... jms-request-reply.camel.yaml:122 : Shipping ORD-1001 +INFO ... jms-request-reply.camel.yaml:133 : Invoicing ORD-1001 +... +INFO ... jms-request-reply.camel.yaml:56 : Stock service answered for ORD-1003: not enough CAMEL-CAP +INFO ... jms-request-reply.camel.yaml:110 : Dead order ORD-1003 +``` + +## Install Camel CLI + +Install [JBang](https://www.jbang.dev/download/) and the Camel CLI as described in the +[root README](../../README.md#install-the-camel-cli); `camel --version` confirms the install. + +## Run it + +The example needs a running artemis, which the Camel CLI starts for you in a container +(Docker or Podman must be running). In one terminal: + +```shell +camel infra run artemis +``` + +It prints the connection details as JSON; they match `application.properties`. In another terminal: + +```shell +camel run * +``` + +The broker's web console is at http://localhost:8161 (user `artemis`, password `artemis`). It shows the queues +`orders`, `stock` and `orders.dead`, and the topic `orders.reserved`. + +Stop the example with `ctrl` + `c` and the service with `camel infra stop artemis`. + +## How it works + +- `application.properties` declares the connection factory as a bean, as in the [artemis](../artemis/) example, + and wires it into the `jms` component. +- `order-intake` reads each order, puts the order id and the customer in headers (JMS headers travel with the + message), and sends the order to the `orders` queue. +- `reserve-order` takes the orders off the queue and asks the stock service. `exchangePattern: InOut` makes it a + request-reply: the `jms` producer sends to the `stock` queue with a temporary reply queue and waits for the + answer (20 seconds by default, the `requestTimeout` option). `variableReceive: answer` keeps the answer in a + variable, so the order stays the body. +- `stock-service` is an ordinary consumer of the `stock` queue. Its answer is the body the route ends with, which + the `jms` consumer sends to the reply queue the request named. +- An order the stock service cannot reserve throws an exception. The route's `deadLetterChannel` redelivers it + three times, one second apart, and then sends the original order (`useOriginalMessage`) to the `orders.dead` + queue, where `dead-orders` logs it. +- A reserved order goes to the topic `orders.reserved`. Unlike a queue, a topic gives every subscriber a copy, so + `shipping` and `invoicing` both get it. + +## Build it step by step + +Ask your assistant, or type it yourself, one step at a time, and run after each: + +1. A file route on `orders` that puts the order id and the customer in headers and logs each order. +2. Connect to the broker with the connection factory in `application.properties`, put the orders on the + `orders` queue, and add a route `reserve-order` that takes them off and logs the customer. +3. Add the stock service on the `stock` queue, and ask it from `reserve-order` with request-reply. +4. Fail an order the stock service cannot reserve: redeliver it three times, then move it to the dead letter + queue `orders.dead`, with a route that logs it. +5. Publish the reserved orders on the topic `orders.reserved`, with two subscriber routes. + +## Try changing + +- Give `CAMEL-CAP` a stock of 5 in `stock.json`: ORD-1003 is reserved and shipped. +- Stop the stock service with `camel cmd stop-route stock-service` and drop in a new order: the request waits + for the `requestTimeout`, fails, and the order goes to the dead letter queue after the redeliveries. +- Remove the `dead-orders` route and restart: ORD-1003 stays on `orders.dead` in the console, as the broker keeps + what no one has taken. +- Add a third subscriber to the topic. + +## Integration testing + +The example comes with a test in the [Citrus](https://citrusframework.org/) YAML DSL, +`test/jms-request-reply.citrus.it.yaml`, which the Camel CLI runs. The test starts the service itself +with the same `camel infra` mechanism, so nothing must be running beforehand: + +```shell +camel test run test/jms-request-reply.citrus.it.yaml +``` + +The test starts Artemis, runs the routes and verifies the answers of the stock service and the dead letter queue. + +## Help and contributions + +If you hit any problem using Camel or have some feedback, then please +[let us know](https://camel.apache.org/community/support/). + +We also love contributors, so +[get involved](https://camel.apache.org/community/contributing/) :-) + +The Camel riders! diff --git a/connect-service/jms-request-reply/application.properties b/connect-service/jms-request-reply/application.properties new file mode 100644 index 0000000..18969e7 --- /dev/null +++ b/connect-service/jms-request-reply/application.properties @@ -0,0 +1,23 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The broker started by: camel infra run artemis +camel.beans.artemisCF = #class:org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory +camel.beans.artemisCF.brokerURL = tcp://localhost:61616 +camel.beans.artemisCF.user = artemis +camel.beans.artemisCF.password = artemis +camel.component.jms.connection-factory = #bean:artemisCF diff --git a/connect-service/jms-request-reply/jms-request-reply.camel.yaml b/connect-service/jms-request-reply/jms-request-reply.camel.yaml new file mode 100644 index 0000000..f4e34da --- /dev/null +++ b/connect-service/jms-request-reply/jms-request-reply.camel.yaml @@ -0,0 +1,134 @@ +# Each order asks a stock service over JMS request-reply whether its lines can be reserved. An order that cannot be +# reserved is redelivered and then moved to a dead letter queue; a reserved order is published on a topic, where +# shipping and invoicing each get a copy. Start the broker with: camel infra run artemis +- route: + id: order-intake + from: + uri: file + parameters: + directoryName: orders + noop: true + sortBy: "file:name" + steps: + - setHeader: + name: orderId + expression: + jsonpath: + expression: "$.orderId" + - setHeader: + name: customer + expression: + jsonpath: + expression: "$.customer" + - to: + uri: jms + parameters: + destinationType: queue + destinationName: orders + - log: + message: "Order ${header.orderId} put on the queue" + +# Takes the orders off the queue and asks the stock service; the reply goes to a variable, so the order stays the body +- route: + id: reserve-order + errorHandler: + deadLetterChannel: + deadLetterUri: "jms:queue:orders.dead" + useOriginalMessage: true + redeliveryPolicy: + maximumRedeliveries: 3 + redeliveryDelay: 1000 + from: + uri: jms + parameters: + destinationType: queue + destinationName: orders + steps: + - log: + message: "Reserving ${header.orderId} for customer ${header.customer}" + - to: + uri: jms + parameters: + destinationType: queue + destinationName: stock + exchangePattern: InOut + variableReceive: answer + - log: + message: "Stock service answered for ${header.orderId}: ${variable.answer}" + - filter: + expression: + simple: + expression: "${variable.answer} != 'reserved'" + steps: + - throwException: + exceptionType: java.lang.IllegalStateException + message: "${variable.answer} for ${header.orderId}" + - to: + uri: jms + parameters: + destinationType: topic + destinationName: orders.reserved + +# Answers "reserved" when stock.json has enough of every line of the order, or "not enough <sku>" +- route: + id: stock-service + from: + uri: jms + parameters: + destinationType: queue + destinationName: stock + steps: + - unmarshal: + json: + library: Jackson + - setVariable: + name: order + expression: + simple: + expression: "${body}" + - setBody: + expression: + constant: + expression: "resource:file:stock.json" + - unmarshal: + json: + library: Jackson + - setBody: + expression: + groovy: + expression: "def missing = variables.order.lines.find { line -> (body.find { it.sku == line.sku }?.qty ?: 0) < line.qty }; missing ? 'not enough ' + missing.sku : 'reserved'" + +# The orders that could not be reserved, after three redeliveries +- route: + id: dead-orders + from: + uri: jms + parameters: + destinationType: queue + destinationName: orders.dead + steps: + - log: + message: "Dead order ${header.orderId}" + +# Every subscriber of the topic gets each reserved order +- route: + id: shipping + from: + uri: jms + parameters: + destinationType: topic + destinationName: orders.reserved + steps: + - log: + message: "Shipping ${header.orderId}" + +- route: + id: invoicing + from: + uri: jms + parameters: + destinationType: topic + destinationName: orders.reserved + steps: + - log: + message: "Invoicing ${header.orderId}" diff --git a/connect-service/jms-request-reply/metadata.json b/connect-service/jms-request-reply/metadata.json new file mode 100644 index 0000000..b77cd6b --- /dev/null +++ b/connect-service/jms-request-reply/metadata.json @@ -0,0 +1,46 @@ +{ + "title": "JMS request-reply", + "description": "The three orders go on a queue of an ActiveMQ Artemis broker started with camel infra; a consumer asks a stock service over JMS request-reply for each, publishes the reserved ones on a topic that shipping and invoicing both subscribe to, and ORD-1003, which the stock service cannot reserve, is redelivered three times and then moved to a dead letter queue.", + "level": "connect-service", + "order": 3, + "teaches": { + "components": [ + "jms", + "file", + "log" + ], + "eips": [ + "errorHandler", + "deadLetterChannel", + "redeliveryPolicy", + "filter", + "throwException", + "setVariable", + "setHeader", + "unmarshal" + ], + "languages": [ + "simple", + "jsonpath", + "groovy" + ], + "dataformats": [ + "json" + ] + }, + "tags": [ + "shop", + "jms", + "artemis", + "queue", + "topic", + "request-reply", + "dead-letter", + "messaging" + ], + "infraServices": [ + "artemis" + ], + "requiresDocker": true, + "bundled": false +} diff --git a/connect-service/jms-request-reply/orders/order-1001.json b/connect-service/jms-request-reply/orders/order-1001.json new file mode 100644 index 0000000..3eb5f03 --- /dev/null +++ b/connect-service/jms-request-reply/orders/order-1001.json @@ -0,0 +1,2 @@ +{"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status": "paid", + "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku": "CAMEL-MUG", "qty": 1, "price": 9.50}]} diff --git a/connect-service/jms-request-reply/orders/order-1002.json b/connect-service/jms-request-reply/orders/order-1002.json new file mode 100644 index 0000000..bb7aa0e --- /dev/null +++ b/connect-service/jms-request-reply/orders/order-1002.json @@ -0,0 +1,2 @@ +{"orderId": "ORD-1002", "customer": "C-207", "country": "DE", "status": "paid", + "lines": [{"sku": "CAMEL-MUG", "qty": 3, "price": 9.50}]} diff --git a/connect-service/jms-request-reply/orders/order-1003.json b/connect-service/jms-request-reply/orders/order-1003.json new file mode 100644 index 0000000..9edab76 --- /dev/null +++ b/connect-service/jms-request-reply/orders/order-1003.json @@ -0,0 +1,2 @@ +{"orderId": "ORD-1003", "customer": "C-134", "country": "US", "status": "pending", + "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1, "price": 19.95}, {"sku": "CAMEL-CAP", "qty": 1, "price": 14.00}, {"sku": "CAMEL-MUG", "qty": 2, "price": 9.50}]} diff --git a/connect-service/jms-request-reply/stock.json b/connect-service/jms-request-reply/stock.json new file mode 100644 index 0000000..efab5de --- /dev/null +++ b/connect-service/jms-request-reply/stock.json @@ -0,0 +1,5 @@ +[ + {"sku": "CAMEL-TSHIRT", "qty": 120}, + {"sku": "CAMEL-MUG", "qty": 42}, + {"sku": "CAMEL-CAP", "qty": 0} +] diff --git a/connect-service/jms-request-reply/test/jms-request-reply.citrus.it.yaml b/connect-service/jms-request-reply/test/jms-request-reply.citrus.it.yaml new file mode 100644 index 0000000..c1312ba --- /dev/null +++ b/connect-service/jms-request-reply/test/jms-request-reply.citrus.it.yaml @@ -0,0 +1,41 @@ +name: jms-request-reply-test +description: The jms-request-reply example logs what its README promises +actions: + - camel: + infra: + run: + service: artemis + fixedPort: true + - camel: + jbang: + run: + integration: + name: "jms-request-reply" + file: "../jms-request-reply.camel.yaml" + systemProperties: + file: "../application.properties" + resources: + - "orders/order-1001.json" + - "orders/order-1002.json" + - "orders/order-1003.json" + - "stock.json" + - camel: + jbang: + verify: + integration: "jms-request-reply" + logMessage: "Stock service answered for ORD-1001: reserved" + - camel: + jbang: + verify: + integration: "jms-request-reply" + logMessage: "Invoicing ORD-1002" + - camel: + jbang: + verify: + integration: "jms-request-reply" + logMessage: "Stock service answered for ORD-1003: not enough CAMEL-CAP" + - camel: + jbang: + verify: + integration: "jms-request-reply" + logMessage: "Dead order ORD-1003" diff --git a/connect-service/kafka-orders/metadata.json b/connect-service/kafka-orders/metadata.json index 48d479f..b6a3521 100644 --- a/connect-service/kafka-orders/metadata.json +++ b/connect-service/kafka-orders/metadata.json @@ -2,7 +2,7 @@ "title": "Kafka orders", "description": "The three orders go through a shared validation route to a Kafka topic on a broker started with camel infra; the pending one is stopped, the paid ones are dispatched to the fulfilment and notifications topics, whose consumers log the picking and the email; camel cmd route-topology draws the flow.", "level": "connect-service", - "order": 4, + "order": 5, "teaches": { "components": [ "kafka", diff --git a/connect-service/mqtt/metadata.json b/connect-service/mqtt/metadata.json index 1e79ac8..683553d 100644 --- a/connect-service/mqtt/metadata.json +++ b/connect-service/mqtt/metadata.json @@ -2,7 +2,7 @@ "title": "MQTT sensors", "description": "Temperature sensors in the warehouse's cold rooms publish JSON readings on an MQTT topic of a Mosquitto broker started with camel infra; a consumer logs each reading and raises a warning when a room is above 8 degrees.", "level": "connect-service", - "order": 3, + "order": 4, "teaches": { "components": [ "paho-mqtt5",
