@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 ADDED
@@ -0,0 +1,66 @@
1
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ }
@@ -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
+ }