@alt-javascript/camel-lite-component-amqp 1.0.2
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 +66 -0
- package/package.json +45 -0
- package/src/Amqp091Consumer.js +89 -0
- package/src/Amqp091Endpoint.js +29 -0
- package/src/Amqp091Producer.js +58 -0
- package/src/Amqp10Consumer.js +100 -0
- package/src/Amqp10Endpoint.js +29 -0
- package/src/Amqp10Producer.js +75 -0
- package/src/AmqpComponent.js +97 -0
- package/src/AmqpEndpointBase.js +37 -0
- package/src/AmqplibClientFactory.js +23 -0
- package/src/JmsMapper.js +121 -0
- package/src/RheaClientFactory.js +19 -0
- package/src/index.js +9 -0
- package/test/amqp.test.js +528 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[](https://opensource.org/licenses/MIT)
|
|
2
|
+
|
|
3
|
+
## What
|
|
4
|
+
|
|
5
|
+
AMQP messaging supporting both AMQP 1.0 ([rhea](https://www.npmjs.com/package/rhea)) and AMQP 0-9-1 ([amqplib](https://www.npmjs.com/package/amqplib)). Producers send exchange body to a queue or topic; consumers receive messages and set them as the exchange body.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install camel-lite-component-amqp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## URI Syntax
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
amqp:queue:name[?protocol=amqp10&url=amqp://localhost]
|
|
17
|
+
amqp:topic:name[?protocol=amqp10&url=amqp://localhost]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Parameter | Default | Description |
|
|
21
|
+
|------------|-----------------|-------------|
|
|
22
|
+
| `protocol` | `amqp10` | Wire protocol: `amqp10` (AMQP 1.0 via rhea) or `amqp091` (AMQP 0-9-1 via amqplib). |
|
|
23
|
+
| `url` | `amqp://localhost` | Broker connection URL. |
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
**Producer — send to a queue:**
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { CamelContext } from 'camel-lite-core';
|
|
31
|
+
import { AmqpComponent } from 'camel-lite-component-amqp';
|
|
32
|
+
|
|
33
|
+
const context = new CamelContext();
|
|
34
|
+
context.addComponent('amqp', new AmqpComponent());
|
|
35
|
+
|
|
36
|
+
context.addRoutes({
|
|
37
|
+
configure(ctx) {
|
|
38
|
+
ctx.from('direct:send')
|
|
39
|
+
.to('amqp:queue:orders?protocol=amqp10&url=amqp://localhost:5672');
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await context.start();
|
|
44
|
+
|
|
45
|
+
const template = context.createProducerTemplate();
|
|
46
|
+
await template.sendBody('direct:send', JSON.stringify({ orderId: 42 }));
|
|
47
|
+
|
|
48
|
+
await context.stop();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Consumer — receive from a queue:**
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
context.addRoutes({
|
|
55
|
+
configure(ctx) {
|
|
56
|
+
ctx.from('amqp:queue:orders?protocol=amqp10&url=amqp://localhost:5672')
|
|
57
|
+
.process(exchange => {
|
|
58
|
+
console.log('Received:', exchange.in.body);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## See Also
|
|
65
|
+
|
|
66
|
+
[camel-lite — root README](../../README.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alt-javascript/camel-lite-component-amqp",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"rhea": "^3.0.4",
|
|
10
|
+
"amqplib": "^0.10.0",
|
|
11
|
+
"@alt-javascript/logger": "^3.0.7",
|
|
12
|
+
"@alt-javascript/config": "^3.0.7",
|
|
13
|
+
"@alt-javascript/common": "^3.0.7",
|
|
14
|
+
"@alt-javascript/camel-lite-core": "1.0.2"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/alt-javascript/camel-lite"
|
|
22
|
+
},
|
|
23
|
+
"author": "Craig Parravicini",
|
|
24
|
+
"contributors": [
|
|
25
|
+
"Claude (Anthropic)",
|
|
26
|
+
"Apache Camel — design inspiration and pattern source"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"alt-javascript",
|
|
30
|
+
"camel",
|
|
31
|
+
"camel-lite",
|
|
32
|
+
"eai",
|
|
33
|
+
"eip",
|
|
34
|
+
"integration",
|
|
35
|
+
"amqp",
|
|
36
|
+
"messaging",
|
|
37
|
+
"rabbitmq",
|
|
38
|
+
"activemq",
|
|
39
|
+
"component"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"registry": "https://registry.npmjs.org/",
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Consumer, Exchange } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
3
|
+
import { JmsMapper } from './JmsMapper.js';
|
|
4
|
+
import { connect } from './AmqplibClientFactory.js';
|
|
5
|
+
|
|
6
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/Amqp091Consumer');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Receives AMQP 0-9-1 messages via amqplib and drives each through the routing pipeline.
|
|
10
|
+
* Maintains a persistent connection from start() to stop().
|
|
11
|
+
*/
|
|
12
|
+
class Amqp091Consumer extends Consumer {
|
|
13
|
+
#endpoint;
|
|
14
|
+
#pipeline;
|
|
15
|
+
#connection = null;
|
|
16
|
+
#channel = null;
|
|
17
|
+
|
|
18
|
+
constructor(endpoint, pipeline) {
|
|
19
|
+
super();
|
|
20
|
+
this.#endpoint = endpoint;
|
|
21
|
+
this.#pipeline = pipeline;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async start() {
|
|
25
|
+
const { host, port, queue, jmsMapping, clientFactory, context, uri } = this.#endpoint;
|
|
26
|
+
const url = `amqp://${host}:${port}`;
|
|
27
|
+
const brokerRef = `${host}:${port}/${queue}`;
|
|
28
|
+
|
|
29
|
+
context.registerConsumer(uri, this);
|
|
30
|
+
|
|
31
|
+
const conn = clientFactory ? await clientFactory(url) : await connect(url);
|
|
32
|
+
this.#connection = conn;
|
|
33
|
+
|
|
34
|
+
const ch = await conn.createChannel();
|
|
35
|
+
this.#channel = ch;
|
|
36
|
+
|
|
37
|
+
await ch.assertQueue(queue, { durable: false });
|
|
38
|
+
|
|
39
|
+
ch.consume(queue, (msg) => {
|
|
40
|
+
if (!msg) return; // consumer cancelled
|
|
41
|
+
|
|
42
|
+
log.debug(`AMQP 0-9-1 received ← ${brokerRef}`);
|
|
43
|
+
|
|
44
|
+
const exchange = new Exchange();
|
|
45
|
+
exchange.in.body = msg.content.toString('utf8');
|
|
46
|
+
|
|
47
|
+
if (jmsMapping) {
|
|
48
|
+
JmsMapper.fromAmqp091(msg, exchange);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ch.ack(msg);
|
|
52
|
+
|
|
53
|
+
this.#pipeline.run(exchange).catch((err) => {
|
|
54
|
+
log.error(`AMQP 0-9-1 pipeline error on ${brokerRef}: ${err.message}`);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
log.info(`AMQP 0-9-1 consumer connected: ${brokerRef}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async stop() {
|
|
62
|
+
const { host, port, queue, context, uri } = this.#endpoint;
|
|
63
|
+
const brokerRef = `${host}:${port}/${queue}`;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (this.#channel) {
|
|
67
|
+
await this.#channel.close().catch(() => { /* already closed */ });
|
|
68
|
+
this.#channel = null;
|
|
69
|
+
}
|
|
70
|
+
if (this.#connection) {
|
|
71
|
+
await this.#connection.close().catch(() => { /* already closed */ });
|
|
72
|
+
this.#connection = null;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
/* swallow — we're stopping */
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
context.registerConsumer(uri, null);
|
|
79
|
+
log.info(`AMQP 0-9-1 consumer stopped: ${brokerRef}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Called by integration tests to inject a single message into the pipeline. */
|
|
83
|
+
async process(exchange) {
|
|
84
|
+
return this.#pipeline.run(exchange);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { Amqp091Consumer };
|
|
89
|
+
export default Amqp091Consumer;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AmqpEndpointBase } from './AmqpEndpointBase.js';
|
|
2
|
+
import { Amqp091Producer } from './Amqp091Producer.js';
|
|
3
|
+
import { Amqp091Consumer } from './Amqp091Consumer.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AMQP 0-9-1 endpoint (backed by amqplib, for RabbitMQ).
|
|
7
|
+
* clientFactory is injected for testability; defaults to the amqplib factory wired in AmqpComponent.
|
|
8
|
+
*/
|
|
9
|
+
class Amqp091Endpoint extends AmqpEndpointBase {
|
|
10
|
+
#clientFactory;
|
|
11
|
+
|
|
12
|
+
constructor(uri, host, port, queue, jmsMapping, context, component, clientFactory) {
|
|
13
|
+
super(uri, host, port, queue, jmsMapping, context, component);
|
|
14
|
+
this.#clientFactory = clientFactory;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get clientFactory() { return this.#clientFactory; }
|
|
18
|
+
|
|
19
|
+
createProducer() {
|
|
20
|
+
return new Amqp091Producer(this);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
createConsumer(pipeline) {
|
|
24
|
+
return new Amqp091Consumer(this, pipeline);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Amqp091Endpoint };
|
|
29
|
+
export default Amqp091Endpoint;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Producer } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
3
|
+
import { JmsMapper } from './JmsMapper.js';
|
|
4
|
+
import { connect } from './AmqplibClientFactory.js';
|
|
5
|
+
|
|
6
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/Amqp091Producer');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sends a single exchange as an AMQP 0-9-1 message via amqplib.
|
|
10
|
+
* Opens a connection per send() call (stateless producer).
|
|
11
|
+
*/
|
|
12
|
+
class Amqp091Producer extends Producer {
|
|
13
|
+
#endpoint;
|
|
14
|
+
|
|
15
|
+
constructor(endpoint) {
|
|
16
|
+
super();
|
|
17
|
+
this.#endpoint = endpoint;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async send(exchange) {
|
|
21
|
+
const { host, port, queue, jmsMapping, clientFactory } = this.#endpoint;
|
|
22
|
+
const url = `amqp://${host}:${port}`;
|
|
23
|
+
const brokerRef = `${host}:${port}/${queue}`;
|
|
24
|
+
|
|
25
|
+
// clientFactory can return a mock connection for tests
|
|
26
|
+
const conn = clientFactory ? await clientFactory(url) : await connect(url);
|
|
27
|
+
try {
|
|
28
|
+
const ch = await conn.createChannel();
|
|
29
|
+
try {
|
|
30
|
+
await ch.assertQueue(queue, { durable: false });
|
|
31
|
+
|
|
32
|
+
const body = exchange.in.body;
|
|
33
|
+
const content = typeof body === 'string'
|
|
34
|
+
? Buffer.from(body, 'utf8')
|
|
35
|
+
: Buffer.isBuffer(body)
|
|
36
|
+
? body
|
|
37
|
+
: Buffer.from(JSON.stringify(body), 'utf8');
|
|
38
|
+
|
|
39
|
+
const options = {};
|
|
40
|
+
if (jmsMapping) {
|
|
41
|
+
JmsMapper.toAmqp091(exchange, options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
log.debug(`AMQP 0-9-1 send → ${brokerRef}`);
|
|
45
|
+
ch.sendToQueue(queue, content, options);
|
|
46
|
+
await ch.close();
|
|
47
|
+
} finally {
|
|
48
|
+
await ch.close().catch(() => { /* already closed */ });
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
await conn.close().catch(() => { /* already closed */ });
|
|
52
|
+
log.debug(`AMQP 0-9-1 connection closed: ${brokerRef}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { Amqp091Producer };
|
|
58
|
+
export default Amqp091Producer;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Consumer, Exchange } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
3
|
+
import { JmsMapper } from './JmsMapper.js';
|
|
4
|
+
import { createContainer } from './RheaClientFactory.js';
|
|
5
|
+
|
|
6
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/Amqp10Consumer');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Receives AMQP 1.0 messages via rhea and drives each through the routing pipeline.
|
|
10
|
+
* Maintains a persistent connection from start() to stop().
|
|
11
|
+
* stop() signals the AbortController and closes the connection gracefully.
|
|
12
|
+
*/
|
|
13
|
+
class Amqp10Consumer extends Consumer {
|
|
14
|
+
#endpoint;
|
|
15
|
+
#pipeline;
|
|
16
|
+
#connection = null;
|
|
17
|
+
|
|
18
|
+
constructor(endpoint, pipeline) {
|
|
19
|
+
super();
|
|
20
|
+
this.#endpoint = endpoint;
|
|
21
|
+
this.#pipeline = pipeline;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async start() {
|
|
25
|
+
const { host, port, queue, jmsMapping, clientFactory, context, uri } = this.#endpoint;
|
|
26
|
+
const brokerRef = `${host}:${port}/${queue}`;
|
|
27
|
+
|
|
28
|
+
context.registerConsumer(uri, this);
|
|
29
|
+
|
|
30
|
+
const container = clientFactory ? clientFactory() : createContainer();
|
|
31
|
+
|
|
32
|
+
await new Promise((resolve, reject) => {
|
|
33
|
+
const conn = container.connect({ host, port: Number(port) });
|
|
34
|
+
this.#connection = conn;
|
|
35
|
+
|
|
36
|
+
conn.on('connection_open', () => {
|
|
37
|
+
conn.open_receiver(queue);
|
|
38
|
+
log.info(`AMQP 1.0 consumer connected: ${brokerRef}`);
|
|
39
|
+
resolve();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
conn.on('message', (ctx) => {
|
|
43
|
+
const msg = ctx.message;
|
|
44
|
+
log.debug(`AMQP 1.0 received ← ${brokerRef}`);
|
|
45
|
+
|
|
46
|
+
const exchange = new Exchange();
|
|
47
|
+
const body = msg.body;
|
|
48
|
+
exchange.in.body = Buffer.isBuffer(body) ? body.toString('utf8') : body;
|
|
49
|
+
|
|
50
|
+
if (jmsMapping) {
|
|
51
|
+
JmsMapper.fromAmqp10(msg, exchange);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fire-and-forget pipeline execution; exceptions land on exchange.exception
|
|
55
|
+
this.#pipeline.run(exchange).catch((err) => {
|
|
56
|
+
log.error(`AMQP 1.0 pipeline error on ${brokerRef}: ${err.message}`);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
conn.on('disconnected', (ctx) => {
|
|
61
|
+
if (ctx.error) {
|
|
62
|
+
log.error(`AMQP 1.0 disconnected from ${brokerRef}: ${ctx.error.message}`);
|
|
63
|
+
reject(ctx.error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async stop() {
|
|
70
|
+
const { host, port, queue, context, uri } = this.#endpoint;
|
|
71
|
+
const brokerRef = `${host}:${port}/${queue}`;
|
|
72
|
+
|
|
73
|
+
if (this.#connection) {
|
|
74
|
+
await new Promise((resolve) => {
|
|
75
|
+
const conn = this.#connection;
|
|
76
|
+
this.#connection = null;
|
|
77
|
+
|
|
78
|
+
conn.once('connection_close', () => resolve());
|
|
79
|
+
conn.once('disconnected', () => resolve());
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
conn.close();
|
|
83
|
+
} catch {
|
|
84
|
+
resolve();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
context.registerConsumer(uri, null);
|
|
90
|
+
log.info(`AMQP 1.0 consumer stopped: ${brokerRef}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Called by integration tests to inject a single message into the pipeline. */
|
|
94
|
+
async process(exchange) {
|
|
95
|
+
return this.#pipeline.run(exchange);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { Amqp10Consumer };
|
|
100
|
+
export default Amqp10Consumer;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AmqpEndpointBase } from './AmqpEndpointBase.js';
|
|
2
|
+
import { Amqp10Producer } from './Amqp10Producer.js';
|
|
3
|
+
import { Amqp10Consumer } from './Amqp10Consumer.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AMQP 1.0 endpoint (backed by rhea).
|
|
7
|
+
* clientFactory is injected for testability; defaults to the rhea factory wired in AmqpComponent.
|
|
8
|
+
*/
|
|
9
|
+
class Amqp10Endpoint extends AmqpEndpointBase {
|
|
10
|
+
#clientFactory;
|
|
11
|
+
|
|
12
|
+
constructor(uri, host, port, queue, jmsMapping, context, component, clientFactory) {
|
|
13
|
+
super(uri, host, port, queue, jmsMapping, context, component);
|
|
14
|
+
this.#clientFactory = clientFactory;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get clientFactory() { return this.#clientFactory; }
|
|
18
|
+
|
|
19
|
+
createProducer() {
|
|
20
|
+
return new Amqp10Producer(this);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
createConsumer(pipeline) {
|
|
24
|
+
return new Amqp10Consumer(this, pipeline);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Amqp10Endpoint };
|
|
29
|
+
export default Amqp10Endpoint;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Producer } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
3
|
+
import { JmsMapper } from './JmsMapper.js';
|
|
4
|
+
import { createContainer } from './RheaClientFactory.js';
|
|
5
|
+
|
|
6
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/Amqp10Producer');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sends a single exchange as an AMQP 1.0 message via rhea.
|
|
10
|
+
* Opens a connection per send() call (stateless — no persistent connection for producers).
|
|
11
|
+
* The connection is closed after the message is sent/rejected/released.
|
|
12
|
+
*/
|
|
13
|
+
class Amqp10Producer extends Producer {
|
|
14
|
+
#endpoint;
|
|
15
|
+
|
|
16
|
+
constructor(endpoint) {
|
|
17
|
+
super();
|
|
18
|
+
this.#endpoint = endpoint;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async send(exchange) {
|
|
22
|
+
const { host, port, queue, jmsMapping, clientFactory } = this.#endpoint;
|
|
23
|
+
const brokerRef = `${host}:${port}/${queue}`;
|
|
24
|
+
|
|
25
|
+
// clientFactory can override container creation for tests
|
|
26
|
+
const container = clientFactory ? clientFactory() : createContainer();
|
|
27
|
+
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const conn = container.connect({ host, port: Number(port) });
|
|
30
|
+
|
|
31
|
+
conn.on('connection_open', () => {
|
|
32
|
+
const sender = conn.open_sender(queue);
|
|
33
|
+
|
|
34
|
+
sender.on('sendable', () => {
|
|
35
|
+
const body = exchange.in.body;
|
|
36
|
+
const content = typeof body === 'string' || Buffer.isBuffer(body)
|
|
37
|
+
? body
|
|
38
|
+
: JSON.stringify(body);
|
|
39
|
+
|
|
40
|
+
const message = { body: content };
|
|
41
|
+
|
|
42
|
+
if (jmsMapping) {
|
|
43
|
+
JmsMapper.toAmqp10(exchange, message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
log.debug(`AMQP 1.0 send → ${brokerRef}`);
|
|
47
|
+
sender.send(message);
|
|
48
|
+
sender.close();
|
|
49
|
+
conn.close();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
sender.on('rejected', (ctx) => {
|
|
53
|
+
conn.close();
|
|
54
|
+
const err = new Error(`AMQP 1.0 message rejected by ${brokerRef}: ${JSON.stringify(ctx.delivery?.remote_state)}`);
|
|
55
|
+
log.error(err.message);
|
|
56
|
+
reject(err);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
conn.on('connection_close', () => {
|
|
61
|
+
resolve();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
conn.on('disconnected', (ctx) => {
|
|
65
|
+
if (ctx.error) {
|
|
66
|
+
log.error(`AMQP 1.0 disconnected from ${brokerRef}: ${ctx.error.message}`);
|
|
67
|
+
reject(ctx.error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { Amqp10Producer };
|
|
75
|
+
export default Amqp10Producer;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Component } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
3
|
+
import { Amqp10Endpoint } from './Amqp10Endpoint.js';
|
|
4
|
+
import { Amqp091Endpoint } from './Amqp091Endpoint.js';
|
|
5
|
+
|
|
6
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/AmqpComponent');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* AmqpComponent — dual-protocol AMQP component.
|
|
10
|
+
*
|
|
11
|
+
* URI format:
|
|
12
|
+
* amqp://host:port/queue?protocol=1.0&jms=false
|
|
13
|
+
* amqp://host:port/queue?protocol=0-9-1&jms=true
|
|
14
|
+
*
|
|
15
|
+
* protocol: '1.0' (default) → rhea / AMQP 1.0 (Artemis, Azure Service Bus)
|
|
16
|
+
* protocol: '0-9-1' → amqplib / AMQP 0-9-1 (RabbitMQ)
|
|
17
|
+
* jms: 'true' → apply JMS 2.x header mapping
|
|
18
|
+
*
|
|
19
|
+
* clientFactory (optional): inject a factory fn for testing.
|
|
20
|
+
* setClientFactory10(fn) — for AMQP 1.0 tests
|
|
21
|
+
* setClientFactory091(fn) — for AMQP 0-9-1 tests
|
|
22
|
+
*/
|
|
23
|
+
class AmqpComponent extends Component {
|
|
24
|
+
#endpoints = new Map();
|
|
25
|
+
#clientFactory10 = null;
|
|
26
|
+
#clientFactory091 = null;
|
|
27
|
+
|
|
28
|
+
/** Inject a custom AMQP 1.0 connection factory (for unit tests). */
|
|
29
|
+
setClientFactory10(fn) {
|
|
30
|
+
this.#clientFactory10 = fn;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Inject a custom AMQP 0-9-1 connection factory (for unit tests). */
|
|
35
|
+
setClientFactory091(fn) {
|
|
36
|
+
this.#clientFactory091 = fn;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
createEndpoint(uri, remaining, parameters, context) {
|
|
41
|
+
if (this.#endpoints.has(uri)) {
|
|
42
|
+
return this.#endpoints.get(uri);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Parse: uri = 'amqp://host:port/queue?...'
|
|
46
|
+
// CamelContext passes us: uri = full URI, remaining = everything after 'amqp:', params = URLSearchParams
|
|
47
|
+
// But remaining may be '//host:port/queue' — we parse from the full URI string.
|
|
48
|
+
const { host, port, queue, protocol, jmsMapping } = AmqpComponent.#parseUri(uri, parameters);
|
|
49
|
+
|
|
50
|
+
log.info(`AmqpComponent creating endpoint: protocol=${protocol}, host=${host}:${port}, queue=${queue}, jms=${jmsMapping}`);
|
|
51
|
+
|
|
52
|
+
let endpoint;
|
|
53
|
+
if (protocol === '0-9-1') {
|
|
54
|
+
endpoint = new Amqp091Endpoint(uri, host, port, queue, jmsMapping, context, this, this.#clientFactory091);
|
|
55
|
+
} else {
|
|
56
|
+
// Default: AMQP 1.0
|
|
57
|
+
endpoint = new Amqp10Endpoint(uri, host, port, queue, jmsMapping, context, this, this.#clientFactory10);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.#endpoints.set(uri, endpoint);
|
|
61
|
+
return endpoint;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static #parseUri(uri, parameters) {
|
|
65
|
+
// uri examples:
|
|
66
|
+
// amqp://localhost:5672/myqueue?protocol=1.0&jms=true
|
|
67
|
+
// amqp://localhost:5672/myqueue
|
|
68
|
+
// We extract host, port, queue from the URI string directly (case-preserving).
|
|
69
|
+
|
|
70
|
+
let working = uri;
|
|
71
|
+
|
|
72
|
+
// Strip scheme
|
|
73
|
+
const schemeEnd = working.indexOf('://');
|
|
74
|
+
if (schemeEnd >= 0) working = working.slice(schemeEnd + 3);
|
|
75
|
+
|
|
76
|
+
// Strip query string (already in parameters)
|
|
77
|
+
const qIdx = working.indexOf('?');
|
|
78
|
+
if (qIdx >= 0) working = working.slice(0, qIdx);
|
|
79
|
+
|
|
80
|
+
// working = 'host:port/queue'
|
|
81
|
+
const slashIdx = working.indexOf('/');
|
|
82
|
+
const hostPort = slashIdx >= 0 ? working.slice(0, slashIdx) : working;
|
|
83
|
+
const queue = slashIdx >= 0 ? working.slice(slashIdx + 1) : 'default';
|
|
84
|
+
|
|
85
|
+
const colonIdx = hostPort.lastIndexOf(':');
|
|
86
|
+
const host = colonIdx >= 0 ? hostPort.slice(0, colonIdx) : hostPort;
|
|
87
|
+
const port = colonIdx >= 0 ? parseInt(hostPort.slice(colonIdx + 1), 10) : 5672;
|
|
88
|
+
|
|
89
|
+
const protocol = parameters.get('protocol') ?? '1.0';
|
|
90
|
+
const jmsMapping = parameters.get('jms') === 'true';
|
|
91
|
+
|
|
92
|
+
return { host, port, queue, protocol, jmsMapping };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { AmqpComponent };
|
|
97
|
+
export default AmqpComponent;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Endpoint } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared base for AMQP endpoints.
|
|
5
|
+
* Holds the parsed URI state; subclasses provide createProducer/createConsumer.
|
|
6
|
+
*/
|
|
7
|
+
class AmqpEndpointBase extends Endpoint {
|
|
8
|
+
#uri;
|
|
9
|
+
#host;
|
|
10
|
+
#port;
|
|
11
|
+
#queue;
|
|
12
|
+
#jmsMapping;
|
|
13
|
+
#context;
|
|
14
|
+
#component;
|
|
15
|
+
|
|
16
|
+
constructor(uri, host, port, queue, jmsMapping, context, component) {
|
|
17
|
+
super();
|
|
18
|
+
this.#uri = uri;
|
|
19
|
+
this.#host = host;
|
|
20
|
+
this.#port = port;
|
|
21
|
+
this.#queue = queue;
|
|
22
|
+
this.#jmsMapping = jmsMapping;
|
|
23
|
+
this.#context = context;
|
|
24
|
+
this.#component = component;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get uri() { return this.#uri; }
|
|
28
|
+
get host() { return this.#host; }
|
|
29
|
+
get port() { return this.#port; }
|
|
30
|
+
get queue() { return this.#queue; }
|
|
31
|
+
get jmsMapping() { return this.#jmsMapping; }
|
|
32
|
+
get context() { return this.#context; }
|
|
33
|
+
get component() { return this.#component; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { AmqpEndpointBase };
|
|
37
|
+
export default AmqpEndpointBase;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ESM/CJS bridge for amqplib (AMQP 0-9-1).
|
|
6
|
+
* Single point of CJS import — keeps the rest of the package pure ESM.
|
|
7
|
+
*/
|
|
8
|
+
let _amqplib;
|
|
9
|
+
|
|
10
|
+
export function getAmqplib() {
|
|
11
|
+
if (!_amqplib) {
|
|
12
|
+
_amqplib = require('amqplib');
|
|
13
|
+
}
|
|
14
|
+
return _amqplib;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} url - amqp://user:pass@host:port/vhost
|
|
19
|
+
* @returns {Promise<import('amqplib').Connection>}
|
|
20
|
+
*/
|
|
21
|
+
export async function connect(url) {
|
|
22
|
+
return getAmqplib().connect(url);
|
|
23
|
+
}
|
package/src/JmsMapper.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JmsMapper — bidirectional mapping between JMS 2.x message properties
|
|
3
|
+
* and AMQP message annotations/properties.
|
|
4
|
+
*
|
|
5
|
+
* JMS 2.x → AMQP 1.0 property names (as used by Qpid JMS / Artemis):
|
|
6
|
+
* JMSMessageID ↔ message-id (application-properties or properties.message_id)
|
|
7
|
+
* JMSCorrelationID ↔ correlation-id (properties.correlation_id)
|
|
8
|
+
* JMSTimestamp ↔ creation-time (properties.creation_time, epoch ms)
|
|
9
|
+
* JMSType ↔ subject (properties.subject)
|
|
10
|
+
* JMSDeliveryMode ↔ durable (header.durable: true = PERSISTENT)
|
|
11
|
+
* JMSPriority ↔ priority (header.priority)
|
|
12
|
+
* JMSExpiration ↔ absolute-expiry-time (properties.absolute_expiry_time)
|
|
13
|
+
*
|
|
14
|
+
* For AMQP 0-9-1 (amqplib) the AMQP message properties map is:
|
|
15
|
+
* JMSMessageID ↔ messageId
|
|
16
|
+
* JMSCorrelationID ↔ correlationId
|
|
17
|
+
* JMSTimestamp ↔ timestamp (Date)
|
|
18
|
+
* JMSType ↔ type
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const JmsMapper = {
|
|
22
|
+
/**
|
|
23
|
+
* Copy JMS headers from exchange to an outbound AMQP 1.0 (rhea) message object.
|
|
24
|
+
* @param {import('@alt-javascript/camel-lite-core').Exchange} exchange
|
|
25
|
+
* @param {object} message - rhea message object (mutated in place)
|
|
26
|
+
*/
|
|
27
|
+
toAmqp10(exchange, message) {
|
|
28
|
+
const h = exchange.in;
|
|
29
|
+
const props = message.properties ?? {};
|
|
30
|
+
message.properties = props;
|
|
31
|
+
|
|
32
|
+
const mid = h.getHeader('JMSMessageID');
|
|
33
|
+
if (mid != null) props.message_id = String(mid);
|
|
34
|
+
|
|
35
|
+
const cid = h.getHeader('JMSCorrelationID');
|
|
36
|
+
if (cid != null) props.correlation_id = String(cid);
|
|
37
|
+
|
|
38
|
+
const ts = h.getHeader('JMSTimestamp');
|
|
39
|
+
if (ts != null) props.creation_time = Number(ts);
|
|
40
|
+
|
|
41
|
+
const type = h.getHeader('JMSType');
|
|
42
|
+
if (type != null) props.subject = String(type);
|
|
43
|
+
|
|
44
|
+
const dm = h.getHeader('JMSDeliveryMode');
|
|
45
|
+
if (dm != null) {
|
|
46
|
+
message.header = message.header ?? {};
|
|
47
|
+
message.header.durable = (dm === 'PERSISTENT' || dm === 2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pri = h.getHeader('JMSPriority');
|
|
51
|
+
if (pri != null) {
|
|
52
|
+
message.header = message.header ?? {};
|
|
53
|
+
message.header.priority = Number(pri);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Copy AMQP 1.0 (rhea) message properties to JMS headers on the exchange.
|
|
59
|
+
* @param {object} message - rhea message object
|
|
60
|
+
* @param {import('@alt-javascript/camel-lite-core').Exchange} exchange
|
|
61
|
+
*/
|
|
62
|
+
fromAmqp10(message, exchange) {
|
|
63
|
+
const props = message.properties ?? {};
|
|
64
|
+
const h = exchange.in;
|
|
65
|
+
|
|
66
|
+
if (props.message_id != null) h.setHeader('JMSMessageID', String(props.message_id));
|
|
67
|
+
if (props.correlation_id != null) h.setHeader('JMSCorrelationID', String(props.correlation_id));
|
|
68
|
+
if (props.creation_time != null) h.setHeader('JMSTimestamp', Number(props.creation_time));
|
|
69
|
+
if (props.subject != null) h.setHeader('JMSType', String(props.subject));
|
|
70
|
+
|
|
71
|
+
const hdr = message.header ?? {};
|
|
72
|
+
if (hdr.durable != null) h.setHeader('JMSDeliveryMode', hdr.durable ? 'PERSISTENT' : 'NON_PERSISTENT');
|
|
73
|
+
if (hdr.priority != null) h.setHeader('JMSPriority', hdr.priority);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Copy JMS headers from exchange to an outbound amqplib 0-9-1 message options object.
|
|
78
|
+
* @param {import('@alt-javascript/camel-lite-core').Exchange} exchange
|
|
79
|
+
* @param {object} options - amqplib sendToQueue options (mutated in place)
|
|
80
|
+
*/
|
|
81
|
+
toAmqp091(exchange, options) {
|
|
82
|
+
const h = exchange.in;
|
|
83
|
+
|
|
84
|
+
const mid = h.getHeader('JMSMessageID');
|
|
85
|
+
if (mid != null) options.messageId = String(mid);
|
|
86
|
+
|
|
87
|
+
const cid = h.getHeader('JMSCorrelationID');
|
|
88
|
+
if (cid != null) options.correlationId = String(cid);
|
|
89
|
+
|
|
90
|
+
const ts = h.getHeader('JMSTimestamp');
|
|
91
|
+
if (ts != null) options.timestamp = Math.floor(Number(ts) / 1000); // amqplib: seconds
|
|
92
|
+
|
|
93
|
+
const type = h.getHeader('JMSType');
|
|
94
|
+
if (type != null) options.type = String(type);
|
|
95
|
+
|
|
96
|
+
const dm = h.getHeader('JMSDeliveryMode');
|
|
97
|
+
if (dm != null) options.deliveryMode = (dm === 'PERSISTENT' || dm === 2) ? 2 : 1;
|
|
98
|
+
|
|
99
|
+
const pri = h.getHeader('JMSPriority');
|
|
100
|
+
if (pri != null) options.priority = Number(pri);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Copy amqplib 0-9-1 message properties to JMS headers on the exchange.
|
|
105
|
+
* @param {object} msg - amqplib message object (msg.properties)
|
|
106
|
+
* @param {import('@alt-javascript/camel-lite-core').Exchange} exchange
|
|
107
|
+
*/
|
|
108
|
+
fromAmqp091(msg, exchange) {
|
|
109
|
+
const props = msg.properties ?? {};
|
|
110
|
+
const h = exchange.in;
|
|
111
|
+
|
|
112
|
+
if (props.messageId != null) h.setHeader('JMSMessageID', String(props.messageId));
|
|
113
|
+
if (props.correlationId != null) h.setHeader('JMSCorrelationID', String(props.correlationId));
|
|
114
|
+
if (props.timestamp != null) h.setHeader('JMSTimestamp', Number(props.timestamp) * 1000); // back to ms
|
|
115
|
+
if (props.type != null) h.setHeader('JMSType', String(props.type));
|
|
116
|
+
if (props.deliveryMode != null) h.setHeader('JMSDeliveryMode', props.deliveryMode === 2 ? 'PERSISTENT' : 'NON_PERSISTENT');
|
|
117
|
+
if (props.priority != null) h.setHeader('JMSPriority', props.priority);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export default JmsMapper;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ESM/CJS bridge for rhea (AMQP 1.0).
|
|
6
|
+
* Single point of CJS import — keeps the rest of the package pure ESM.
|
|
7
|
+
*/
|
|
8
|
+
let _rhea;
|
|
9
|
+
|
|
10
|
+
export function getRhea() {
|
|
11
|
+
if (!_rhea) {
|
|
12
|
+
_rhea = require('rhea');
|
|
13
|
+
}
|
|
14
|
+
return _rhea;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createContainer() {
|
|
18
|
+
return getRhea().create_container();
|
|
19
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { AmqpComponent } from './AmqpComponent.js';
|
|
2
|
+
export { AmqpEndpointBase } from './AmqpEndpointBase.js';
|
|
3
|
+
export { Amqp10Endpoint } from './Amqp10Endpoint.js';
|
|
4
|
+
export { Amqp091Endpoint } from './Amqp091Endpoint.js';
|
|
5
|
+
export { Amqp10Producer } from './Amqp10Producer.js';
|
|
6
|
+
export { Amqp091Producer } from './Amqp091Producer.js';
|
|
7
|
+
export { Amqp10Consumer } from './Amqp10Consumer.js';
|
|
8
|
+
export { Amqp091Consumer } from './Amqp091Consumer.js';
|
|
9
|
+
export { JmsMapper } from './JmsMapper.js';
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { Exchange, CamelContext, RouteBuilder } from '@alt-javascript/camel-lite-core';
|
|
4
|
+
import { AmqpComponent, JmsMapper } from '@alt-javascript/camel-lite-component-amqp';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// T02: AmqpComponent URI parsing + AmqpProducer unit tests
|
|
8
|
+
// T03: AmqpConsumer lifecycle unit tests
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function makeExchange(body = 'hello amqp') {
|
|
14
|
+
const ex = new Exchange();
|
|
15
|
+
ex.in.body = body;
|
|
16
|
+
return ex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── JmsMapper unit tests ─────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
describe('JmsMapper', () => {
|
|
22
|
+
it('toAmqp10: maps JMS headers onto rhea message properties', () => {
|
|
23
|
+
const ex = makeExchange();
|
|
24
|
+
ex.in.setHeader('JMSMessageID', 'ID:12345');
|
|
25
|
+
ex.in.setHeader('JMSCorrelationID', 'corr-abc');
|
|
26
|
+
ex.in.setHeader('JMSTimestamp', 1700000000000);
|
|
27
|
+
ex.in.setHeader('JMSType', 'OrderCreated');
|
|
28
|
+
ex.in.setHeader('JMSDeliveryMode', 'PERSISTENT');
|
|
29
|
+
ex.in.setHeader('JMSPriority', 5);
|
|
30
|
+
|
|
31
|
+
const msg = {};
|
|
32
|
+
JmsMapper.toAmqp10(ex, msg);
|
|
33
|
+
|
|
34
|
+
assert.equal(msg.properties.message_id, 'ID:12345');
|
|
35
|
+
assert.equal(msg.properties.correlation_id, 'corr-abc');
|
|
36
|
+
assert.equal(msg.properties.creation_time, 1700000000000);
|
|
37
|
+
assert.equal(msg.properties.subject, 'OrderCreated');
|
|
38
|
+
assert.equal(msg.header.durable, true);
|
|
39
|
+
assert.equal(msg.header.priority, 5);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('fromAmqp10: maps rhea message properties to JMS headers on exchange', () => {
|
|
43
|
+
const ex = makeExchange();
|
|
44
|
+
const msg = {
|
|
45
|
+
properties: {
|
|
46
|
+
message_id: 'ID:99',
|
|
47
|
+
correlation_id: 'corr-xyz',
|
|
48
|
+
creation_time: 1700000001000,
|
|
49
|
+
subject: 'OrderShipped',
|
|
50
|
+
},
|
|
51
|
+
header: { durable: false, priority: 3 },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
JmsMapper.fromAmqp10(msg, ex);
|
|
55
|
+
|
|
56
|
+
assert.equal(ex.in.getHeader('JMSMessageID'), 'ID:99');
|
|
57
|
+
assert.equal(ex.in.getHeader('JMSCorrelationID'), 'corr-xyz');
|
|
58
|
+
assert.equal(ex.in.getHeader('JMSTimestamp'), 1700000001000);
|
|
59
|
+
assert.equal(ex.in.getHeader('JMSType'), 'OrderShipped');
|
|
60
|
+
assert.equal(ex.in.getHeader('JMSDeliveryMode'), 'NON_PERSISTENT');
|
|
61
|
+
assert.equal(ex.in.getHeader('JMSPriority'), 3);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('toAmqp091: maps JMS headers onto amqplib options', () => {
|
|
65
|
+
const ex = makeExchange();
|
|
66
|
+
ex.in.setHeader('JMSMessageID', 'ID:55');
|
|
67
|
+
ex.in.setHeader('JMSCorrelationID', 'corr-091');
|
|
68
|
+
ex.in.setHeader('JMSTimestamp', 1700000002000);
|
|
69
|
+
ex.in.setHeader('JMSType', 'Ping');
|
|
70
|
+
ex.in.setHeader('JMSDeliveryMode', 'NON_PERSISTENT');
|
|
71
|
+
|
|
72
|
+
const options = {};
|
|
73
|
+
JmsMapper.toAmqp091(ex, options);
|
|
74
|
+
|
|
75
|
+
assert.equal(options.messageId, 'ID:55');
|
|
76
|
+
assert.equal(options.correlationId, 'corr-091');
|
|
77
|
+
assert.equal(options.timestamp, 1700000002); // seconds
|
|
78
|
+
assert.equal(options.type, 'Ping');
|
|
79
|
+
assert.equal(options.deliveryMode, 1); // NON_PERSISTENT
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('fromAmqp091: maps amqplib message properties to JMS headers', () => {
|
|
83
|
+
const ex = makeExchange();
|
|
84
|
+
const msg = {
|
|
85
|
+
properties: {
|
|
86
|
+
messageId: 'ID:77',
|
|
87
|
+
correlationId: 'corr-77',
|
|
88
|
+
timestamp: 1700000003, // seconds
|
|
89
|
+
type: 'Pong',
|
|
90
|
+
deliveryMode: 2,
|
|
91
|
+
priority: 7,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
JmsMapper.fromAmqp091(msg, ex);
|
|
96
|
+
|
|
97
|
+
assert.equal(ex.in.getHeader('JMSMessageID'), 'ID:77');
|
|
98
|
+
assert.equal(ex.in.getHeader('JMSCorrelationID'), 'corr-77');
|
|
99
|
+
assert.equal(ex.in.getHeader('JMSTimestamp'), 1700000003000); // back to ms
|
|
100
|
+
assert.equal(ex.in.getHeader('JMSType'), 'Pong');
|
|
101
|
+
assert.equal(ex.in.getHeader('JMSDeliveryMode'), 'PERSISTENT');
|
|
102
|
+
assert.equal(ex.in.getHeader('JMSPriority'), 7);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('toAmqp10: skips unmapped headers gracefully', () => {
|
|
106
|
+
const msg = {};
|
|
107
|
+
JmsMapper.toAmqp10(makeExchange(), msg); // no JMS headers set
|
|
108
|
+
assert.deepEqual(msg, { properties: {} });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── AmqpComponent URI parsing ────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
describe('AmqpComponent URI parsing', () => {
|
|
115
|
+
const ctx = new CamelContext();
|
|
116
|
+
|
|
117
|
+
it('defaults to AMQP 1.0 when protocol param absent', () => {
|
|
118
|
+
const comp = new AmqpComponent();
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
const ep = comp.createEndpoint('amqp://localhost:5672/testqueue', '//localhost:5672/testqueue', params, ctx);
|
|
121
|
+
assert.equal(ep.host, 'localhost');
|
|
122
|
+
assert.equal(ep.port, 5672);
|
|
123
|
+
assert.equal(ep.queue, 'testqueue');
|
|
124
|
+
assert.equal(ep.jmsMapping, false);
|
|
125
|
+
assert.match(ep.constructor.name, /Amqp10Endpoint/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('creates Amqp10Endpoint for protocol=1.0', () => {
|
|
129
|
+
const comp = new AmqpComponent();
|
|
130
|
+
const params = new URLSearchParams('protocol=1.0&jms=true');
|
|
131
|
+
const ep = comp.createEndpoint('amqp://broker:5672/orders?protocol=1.0&jms=true', '//broker:5672/orders', params, ctx);
|
|
132
|
+
assert.equal(ep.host, 'broker');
|
|
133
|
+
assert.equal(ep.port, 5672);
|
|
134
|
+
assert.equal(ep.queue, 'orders');
|
|
135
|
+
assert.equal(ep.jmsMapping, true);
|
|
136
|
+
assert.match(ep.constructor.name, /Amqp10Endpoint/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('creates Amqp091Endpoint for protocol=0-9-1', () => {
|
|
140
|
+
const comp = new AmqpComponent();
|
|
141
|
+
const params = new URLSearchParams('protocol=0-9-1');
|
|
142
|
+
const ep = comp.createEndpoint('amqp://rabbit:5672/events?protocol=0-9-1', '//rabbit:5672/events', params, ctx);
|
|
143
|
+
assert.equal(ep.queue, 'events');
|
|
144
|
+
assert.match(ep.constructor.name, /Amqp091Endpoint/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns cached endpoint on duplicate URI', () => {
|
|
148
|
+
const comp = new AmqpComponent();
|
|
149
|
+
const params = new URLSearchParams();
|
|
150
|
+
const ep1 = comp.createEndpoint('amqp://localhost:5672/q', '', params, ctx);
|
|
151
|
+
const ep2 = comp.createEndpoint('amqp://localhost:5672/q', '', params, ctx);
|
|
152
|
+
assert.equal(ep1, ep2);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── Amqp10Producer unit test (mock container) ────────────────────────────────
|
|
157
|
+
|
|
158
|
+
describe('Amqp10Producer', () => {
|
|
159
|
+
it('sends exchange body as AMQP 1.0 message via mock container', async () => {
|
|
160
|
+
const sent = [];
|
|
161
|
+
|
|
162
|
+
// Mock rhea container
|
|
163
|
+
function mockFactory() {
|
|
164
|
+
return {
|
|
165
|
+
connect({ host, port }) {
|
|
166
|
+
const listeners = {};
|
|
167
|
+
const conn = {
|
|
168
|
+
on(event, fn) { listeners[event] = fn; return conn; },
|
|
169
|
+
once(event, fn) { listeners[event] = fn; return conn; },
|
|
170
|
+
open_sender(queue) {
|
|
171
|
+
// Emit 'sendable' asynchronously so the producer wires up first
|
|
172
|
+
const senderListeners = {};
|
|
173
|
+
const sender = {
|
|
174
|
+
on(ev, fn) { senderListeners[ev] = fn; return sender; },
|
|
175
|
+
send(msg) { sent.push({ queue, msg }); },
|
|
176
|
+
close() {
|
|
177
|
+
// trigger connection_close after sender close
|
|
178
|
+
setTimeout(() => listeners['connection_close']?.(), 0);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
setTimeout(() => senderListeners['sendable']?.(), 0);
|
|
182
|
+
return sender;
|
|
183
|
+
},
|
|
184
|
+
close() { /* trigger via sender.close above */ },
|
|
185
|
+
};
|
|
186
|
+
// Emit connection_open after a tick
|
|
187
|
+
setTimeout(() => listeners['connection_open']?.(), 0);
|
|
188
|
+
return conn;
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const comp = new AmqpComponent();
|
|
194
|
+
comp.setClientFactory10(mockFactory);
|
|
195
|
+
|
|
196
|
+
const params = new URLSearchParams('protocol=1.0');
|
|
197
|
+
const ctx = new CamelContext();
|
|
198
|
+
const ep = comp.createEndpoint('amqp://localhost:5672/myqueue?protocol=1.0', '', params, ctx);
|
|
199
|
+
const producer = ep.createProducer();
|
|
200
|
+
|
|
201
|
+
const ex = makeExchange('hello from producer');
|
|
202
|
+
await producer.send(ex);
|
|
203
|
+
|
|
204
|
+
assert.equal(sent.length, 1);
|
|
205
|
+
assert.equal(sent[0].queue, 'myqueue');
|
|
206
|
+
assert.equal(sent[0].msg.body, 'hello from producer');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('applies JMS mapping when jmsMapping=true', async () => {
|
|
210
|
+
const sent = [];
|
|
211
|
+
|
|
212
|
+
function mockFactory() {
|
|
213
|
+
return {
|
|
214
|
+
connect() {
|
|
215
|
+
const listeners = {};
|
|
216
|
+
const conn = {
|
|
217
|
+
on(event, fn) { listeners[event] = fn; return conn; },
|
|
218
|
+
once(event, fn) { listeners[event] = fn; return conn; },
|
|
219
|
+
open_sender(queue) {
|
|
220
|
+
const senderListeners = {};
|
|
221
|
+
const sender = {
|
|
222
|
+
on(ev, fn) { senderListeners[ev] = fn; return sender; },
|
|
223
|
+
send(msg) { sent.push(msg); },
|
|
224
|
+
close() { setTimeout(() => listeners['connection_close']?.(), 0); },
|
|
225
|
+
};
|
|
226
|
+
setTimeout(() => senderListeners['sendable']?.(), 0);
|
|
227
|
+
return sender;
|
|
228
|
+
},
|
|
229
|
+
close() {},
|
|
230
|
+
};
|
|
231
|
+
setTimeout(() => listeners['connection_open']?.(), 0);
|
|
232
|
+
return conn;
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const comp = new AmqpComponent();
|
|
238
|
+
comp.setClientFactory10(mockFactory);
|
|
239
|
+
|
|
240
|
+
const params = new URLSearchParams('protocol=1.0&jms=true');
|
|
241
|
+
const ctx = new CamelContext();
|
|
242
|
+
const ep = comp.createEndpoint('amqp://localhost:5672/orders?protocol=1.0&jms=true', '', params, ctx);
|
|
243
|
+
const producer = ep.createProducer();
|
|
244
|
+
|
|
245
|
+
const ex = makeExchange('order payload');
|
|
246
|
+
ex.in.setHeader('JMSMessageID', 'ID:order-001');
|
|
247
|
+
ex.in.setHeader('JMSCorrelationID', 'sess-abc');
|
|
248
|
+
ex.in.setHeader('JMSType', 'OrderCreated');
|
|
249
|
+
|
|
250
|
+
await producer.send(ex);
|
|
251
|
+
|
|
252
|
+
assert.equal(sent.length, 1);
|
|
253
|
+
assert.equal(sent[0].properties.message_id, 'ID:order-001');
|
|
254
|
+
assert.equal(sent[0].properties.correlation_id, 'sess-abc');
|
|
255
|
+
assert.equal(sent[0].properties.subject, 'OrderCreated');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ── Amqp091Producer unit test (mock amqplib) ─────────────────────────────────
|
|
260
|
+
|
|
261
|
+
describe('Amqp091Producer', () => {
|
|
262
|
+
it('sends exchange body as AMQP 0-9-1 message via mock connection', async () => {
|
|
263
|
+
const sent = [];
|
|
264
|
+
|
|
265
|
+
async function mockFactory(url) {
|
|
266
|
+
return {
|
|
267
|
+
async createChannel() {
|
|
268
|
+
return {
|
|
269
|
+
async assertQueue(q) {},
|
|
270
|
+
sendToQueue(q, content, opts) { sent.push({ q, content: content.toString(), opts }); },
|
|
271
|
+
async close() {},
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
async close() {},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const comp = new AmqpComponent();
|
|
279
|
+
comp.setClientFactory091(mockFactory);
|
|
280
|
+
|
|
281
|
+
const params = new URLSearchParams('protocol=0-9-1');
|
|
282
|
+
const ctx = new CamelContext();
|
|
283
|
+
const ep = comp.createEndpoint('amqp://localhost:5672/events?protocol=0-9-1', '', params, ctx);
|
|
284
|
+
const producer = ep.createProducer();
|
|
285
|
+
|
|
286
|
+
const ex = makeExchange('event payload');
|
|
287
|
+
await producer.send(ex);
|
|
288
|
+
|
|
289
|
+
assert.equal(sent.length, 1);
|
|
290
|
+
assert.equal(sent[0].q, 'events');
|
|
291
|
+
assert.equal(sent[0].content, 'event payload');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── Amqp10Consumer unit test (mock container — lifecycle + message dispatch) ──
|
|
296
|
+
|
|
297
|
+
describe('Amqp10Consumer', () => {
|
|
298
|
+
it('delivers message to pipeline and closes cleanly on stop()', async () => {
|
|
299
|
+
const processed = [];
|
|
300
|
+
|
|
301
|
+
// Minimal mock pipeline
|
|
302
|
+
const pipeline = {
|
|
303
|
+
async run(exchange) { processed.push(exchange.in.body); },
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
let capturedMessageHandler = null;
|
|
307
|
+
let capturedCloseHandler = null;
|
|
308
|
+
|
|
309
|
+
function mockFactory() {
|
|
310
|
+
return {
|
|
311
|
+
connect() {
|
|
312
|
+
const listeners = {};
|
|
313
|
+
const conn = {
|
|
314
|
+
on(event, fn) {
|
|
315
|
+
listeners[event] = fn;
|
|
316
|
+
if (event === 'message') capturedMessageHandler = fn;
|
|
317
|
+
if (event === 'connection_close') capturedCloseHandler = fn;
|
|
318
|
+
return conn;
|
|
319
|
+
},
|
|
320
|
+
once(event, fn) {
|
|
321
|
+
listeners['__once_' + event] = fn;
|
|
322
|
+
if (event === 'connection_close') capturedCloseHandler = fn;
|
|
323
|
+
if (event === 'disconnected') {
|
|
324
|
+
// noop
|
|
325
|
+
}
|
|
326
|
+
return conn;
|
|
327
|
+
},
|
|
328
|
+
open_receiver(queue) { return {}; },
|
|
329
|
+
close() {
|
|
330
|
+
// Simulate broker confirming close
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
const h = listeners['__once_connection_close'] ?? listeners['connection_close'];
|
|
333
|
+
if (h) h();
|
|
334
|
+
}, 0);
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
// Emit connection_open after a tick
|
|
338
|
+
setTimeout(() => listeners['connection_open']?.(), 0);
|
|
339
|
+
return conn;
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const comp = new AmqpComponent();
|
|
345
|
+
comp.setClientFactory10(mockFactory);
|
|
346
|
+
|
|
347
|
+
const params = new URLSearchParams('protocol=1.0');
|
|
348
|
+
const ctx = new CamelContext();
|
|
349
|
+
const ep = comp.createEndpoint('amqp://localhost:5672/testq?protocol=1.0', '', params, ctx);
|
|
350
|
+
const consumer = ep.createConsumer(pipeline);
|
|
351
|
+
|
|
352
|
+
await consumer.start();
|
|
353
|
+
|
|
354
|
+
// Simulate an incoming AMQP 1.0 message
|
|
355
|
+
assert.ok(capturedMessageHandler, 'message handler should be registered');
|
|
356
|
+
capturedMessageHandler({ message: { body: 'incoming message', properties: {} } });
|
|
357
|
+
|
|
358
|
+
// Give async pipeline.run() time to execute
|
|
359
|
+
await new Promise(r => setTimeout(r, 10));
|
|
360
|
+
|
|
361
|
+
assert.equal(processed.length, 1);
|
|
362
|
+
assert.equal(processed[0], 'incoming message');
|
|
363
|
+
|
|
364
|
+
await consumer.stop();
|
|
365
|
+
// Verify: context consumer cleared
|
|
366
|
+
assert.equal(ctx.getConsumer('amqp://localhost:5672/testq?protocol=1.0'), null);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('fromAmqp10 JMS mapping applied when jmsMapping=true', async () => {
|
|
370
|
+
const exchanges = [];
|
|
371
|
+
|
|
372
|
+
const pipeline = { async run(ex) { exchanges.push(ex); } };
|
|
373
|
+
let capturedMessageHandler = null;
|
|
374
|
+
|
|
375
|
+
function mockFactory() {
|
|
376
|
+
return {
|
|
377
|
+
connect() {
|
|
378
|
+
const listeners = {};
|
|
379
|
+
const conn = {
|
|
380
|
+
on(ev, fn) {
|
|
381
|
+
listeners[ev] = fn;
|
|
382
|
+
if (ev === 'message') capturedMessageHandler = fn;
|
|
383
|
+
return conn;
|
|
384
|
+
},
|
|
385
|
+
once(ev, fn) { listeners['__once_' + ev] = fn; return conn; },
|
|
386
|
+
open_receiver() { return {}; },
|
|
387
|
+
close() {
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
const h = listeners['__once_connection_close'] ?? listeners['connection_close'];
|
|
390
|
+
if (h) h();
|
|
391
|
+
}, 0);
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
setTimeout(() => listeners['connection_open']?.(), 0);
|
|
395
|
+
return conn;
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const comp = new AmqpComponent();
|
|
401
|
+
comp.setClientFactory10(mockFactory);
|
|
402
|
+
|
|
403
|
+
const params = new URLSearchParams('protocol=1.0&jms=true');
|
|
404
|
+
const ctx = new CamelContext();
|
|
405
|
+
const ep = comp.createEndpoint('amqp://localhost:5672/orders?protocol=1.0&jms=true', '', params, ctx);
|
|
406
|
+
const consumer = ep.createConsumer(pipeline);
|
|
407
|
+
|
|
408
|
+
await consumer.start();
|
|
409
|
+
capturedMessageHandler({
|
|
410
|
+
message: {
|
|
411
|
+
body: 'order data',
|
|
412
|
+
properties: { message_id: 'ID:xyz', subject: 'OrderEvent' },
|
|
413
|
+
header: {},
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await new Promise(r => setTimeout(r, 10));
|
|
418
|
+
|
|
419
|
+
assert.equal(exchanges.length, 1);
|
|
420
|
+
assert.equal(exchanges[0].in.getHeader('JMSMessageID'), 'ID:xyz');
|
|
421
|
+
assert.equal(exchanges[0].in.getHeader('JMSType'), 'OrderEvent');
|
|
422
|
+
|
|
423
|
+
await consumer.stop();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ── Amqp091Consumer unit test ────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
describe('Amqp091Consumer', () => {
|
|
430
|
+
it('delivers message to pipeline and closes cleanly on stop()', async () => {
|
|
431
|
+
const processed = [];
|
|
432
|
+
const pipeline = { async run(exchange) { processed.push(exchange.in.body); } };
|
|
433
|
+
|
|
434
|
+
let capturedConsumeHandler = null;
|
|
435
|
+
|
|
436
|
+
async function mockFactory() {
|
|
437
|
+
return {
|
|
438
|
+
async createChannel() {
|
|
439
|
+
return {
|
|
440
|
+
async assertQueue() {},
|
|
441
|
+
consume(queue, handler) { capturedConsumeHandler = handler; },
|
|
442
|
+
ack() {},
|
|
443
|
+
async close() {},
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
async close() {},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const comp = new AmqpComponent();
|
|
451
|
+
comp.setClientFactory091(mockFactory);
|
|
452
|
+
|
|
453
|
+
const params = new URLSearchParams('protocol=0-9-1');
|
|
454
|
+
const ctx = new CamelContext();
|
|
455
|
+
const ep = comp.createEndpoint('amqp://localhost:5672/q091?protocol=0-9-1', '', params, ctx);
|
|
456
|
+
const consumer = ep.createConsumer(pipeline);
|
|
457
|
+
|
|
458
|
+
await consumer.start();
|
|
459
|
+
|
|
460
|
+
// Simulate incoming message
|
|
461
|
+
assert.ok(capturedConsumeHandler, 'consume handler should be registered');
|
|
462
|
+
capturedConsumeHandler({
|
|
463
|
+
content: Buffer.from('hello 0-9-1'),
|
|
464
|
+
properties: {},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
await new Promise(r => setTimeout(r, 10));
|
|
468
|
+
|
|
469
|
+
assert.equal(processed.length, 1);
|
|
470
|
+
assert.equal(processed[0], 'hello 0-9-1');
|
|
471
|
+
|
|
472
|
+
await consumer.stop();
|
|
473
|
+
assert.equal(ctx.getConsumer('amqp://localhost:5672/q091?protocol=0-9-1'), null);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ── Integration test (conditional on AMQP_URL) ───────────────────────────────
|
|
478
|
+
|
|
479
|
+
const AMQP_URL = process.env.AMQP_URL;
|
|
480
|
+
const AMQP_PROTOCOL = process.env.AMQP_PROTOCOL ?? '1.0';
|
|
481
|
+
|
|
482
|
+
if (AMQP_URL) {
|
|
483
|
+
describe('AMQP integration (live broker)', () => {
|
|
484
|
+
it('round-trips a message through a live AMQP broker', async () => {
|
|
485
|
+
// Parse URL for component
|
|
486
|
+
const url = new URL(AMQP_URL);
|
|
487
|
+
const host = url.hostname;
|
|
488
|
+
const port = url.port || 5672;
|
|
489
|
+
const queue = 'camel-lite-test-' + Date.now();
|
|
490
|
+
const endpointUri = `amqp://${host}:${port}/${queue}?protocol=${AMQP_PROTOCOL}`;
|
|
491
|
+
|
|
492
|
+
const received = [];
|
|
493
|
+
const context = new CamelContext();
|
|
494
|
+
const amqp = new AmqpComponent();
|
|
495
|
+
context.addComponent('amqp', amqp);
|
|
496
|
+
|
|
497
|
+
class TestRoutes extends RouteBuilder {
|
|
498
|
+
configure(ctx) {
|
|
499
|
+
this.from(endpointUri).process((ex) => { received.push(ex.in.body); });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
context.addRoutes(new TestRoutes());
|
|
504
|
+
await context.start();
|
|
505
|
+
|
|
506
|
+
// Send a message via producer
|
|
507
|
+
const params = new URLSearchParams(`protocol=${AMQP_PROTOCOL}`);
|
|
508
|
+
const endpoint = amqp.createEndpoint(endpointUri, '', params, context);
|
|
509
|
+
const producer = endpoint.createProducer();
|
|
510
|
+
|
|
511
|
+
const ex = new Exchange();
|
|
512
|
+
ex.in.body = 'integration test message';
|
|
513
|
+
await producer.send(ex);
|
|
514
|
+
|
|
515
|
+
// Wait for consumer to receive
|
|
516
|
+
await new Promise(r => setTimeout(r, 500));
|
|
517
|
+
|
|
518
|
+
await context.stop();
|
|
519
|
+
|
|
520
|
+
assert.ok(received.length >= 1, 'should have received at least one message');
|
|
521
|
+
assert.equal(received[0], 'integration test message');
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
} else {
|
|
525
|
+
describe('AMQP integration (skipped — set AMQP_URL to enable)', () => {
|
|
526
|
+
it('skipped', () => { /* no-op */ });
|
|
527
|
+
});
|
|
528
|
+
}
|