@onlineapps/conn-infra-mq 1.1.11 → 1.1.13
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 +20 -1
- package/package.json +2 -1
- package/src/ConnectorMQClient.js +1 -1
- package/src/config/configSchema.js +1 -1
- package/src/index.js +1 -1
- package/src/layers/QueueManager.js +39 -2
- package/src/layers/WorkflowRouter.js +14 -28
- package/src/BaseClient.js +0 -219
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ yarn add @onlineapps/conn-infra-mq
|
|
|
38
38
|
## 🏗️ Architecture
|
|
39
39
|
|
|
40
40
|
```
|
|
41
|
-
ConnectorMQClient (main orchestrator)
|
|
41
|
+
ConnectorMQClient (main orchestrator - for business services only)
|
|
42
42
|
├── BaseClient (core AMQP operations)
|
|
43
43
|
├── WorkflowRouter (workflow orchestration)
|
|
44
44
|
├── QueueManager (queue lifecycle management)
|
|
@@ -47,6 +47,25 @@ ConnectorMQClient (main orchestrator)
|
|
|
47
47
|
└── RetryHandler (error recovery & DLQ)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
### WorkflowRouter - How It Works
|
|
51
|
+
|
|
52
|
+
**Purpose**: Handles workflow routing between services in a decentralized architecture.
|
|
53
|
+
|
|
54
|
+
**Key Methods**:
|
|
55
|
+
- `publishWorkflowInit(workflow, options)` - Publishes workflow to `workflow.init` queue (entry point)
|
|
56
|
+
- `publishToServiceWorkflow(serviceName, message, options)` - Routes to specific service's workflow queue
|
|
57
|
+
- `publishWorkflowCompleted(result, options)` - Publishes completed workflow to `workflow.completed` queue
|
|
58
|
+
- `consumeWorkflowInit(handler, options)` - Consumes from `workflow.init` (competing consumers pattern)
|
|
59
|
+
- `consumeServiceWorkflow(serviceName, handler, options)` - Consumes from service-specific workflow queue
|
|
60
|
+
|
|
61
|
+
**How It Works**:
|
|
62
|
+
1. **Gateway** publishes to `workflow.init` via `publishWorkflowInit()`
|
|
63
|
+
2. **Business services** (competing consumers) consume from `workflow.init` via `consumeWorkflowInit()`
|
|
64
|
+
3. **Services** route to next service via `publishToServiceWorkflow()`
|
|
65
|
+
4. **Final service** publishes completion via `publishWorkflowCompleted()`
|
|
66
|
+
|
|
67
|
+
**Note**: WorkflowRouter is part of `conn-infra-mq` connector (for business services). Infrastructure services (gateway) should use the underlying MQ client library directly, not the connector.
|
|
68
|
+
|
|
50
69
|
## 🔧 Quick Start
|
|
51
70
|
|
|
52
71
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-infra-mq",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.13",
|
|
4
4
|
"description": "A promise-based, broker-agnostic client for sending and receiving messages via RabbitMQ",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/onlineapps/connector-mq-client#readme",
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"@onlineapps/mq-client-core": "^1.0.0",
|
|
45
46
|
"ajv": "^8.11.0",
|
|
46
47
|
"amqplib": "^0.10.3",
|
|
47
48
|
"lodash.merge": "^4.6.2"
|
package/src/ConnectorMQClient.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const BaseClient = require('
|
|
3
|
+
const BaseClient = require('@onlineapps/mq-client-core');
|
|
4
4
|
const WorkflowRouter = require('./layers/WorkflowRouter');
|
|
5
5
|
const QueueManager = require('./layers/QueueManager');
|
|
6
6
|
const ForkJoinHandler = require('./layers/ForkJoinHandler');
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const ConnectorMQClient = require('./ConnectorMQClient');
|
|
15
|
-
const BaseClient = require('
|
|
15
|
+
const BaseClient = require('@onlineapps/mq-client-core');
|
|
16
16
|
|
|
17
17
|
// Layers - exported for advanced usage
|
|
18
18
|
const WorkflowRouter = require('./layers/WorkflowRouter');
|
|
@@ -80,8 +80,45 @@ class QueueManager {
|
|
|
80
80
|
// Track managed queue
|
|
81
81
|
this.managedQueues.add(queueName);
|
|
82
82
|
|
|
83
|
-
//
|
|
84
|
-
|
|
83
|
+
// Use checkQueue first to avoid 406 PRECONDITION-FAILED closing the channel
|
|
84
|
+
// If queue doesn't exist (404), then assertQueue to create it
|
|
85
|
+
// IMPORTANT: Check if channel is still open before operations
|
|
86
|
+
if (!channel || channel.closed) {
|
|
87
|
+
throw new Error('Channel is closed - cannot ensure queue');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const queueInfo = await channel.checkQueue(queueName);
|
|
92
|
+
// Queue exists - return queue info
|
|
93
|
+
return queueInfo;
|
|
94
|
+
} catch (checkErr) {
|
|
95
|
+
// Check if channel is still open before assertQueue
|
|
96
|
+
if (!channel || channel.closed) {
|
|
97
|
+
throw new Error('Channel closed during checkQueue - cannot create queue');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If queue doesn't exist (404), create it with provided options
|
|
101
|
+
if (checkErr.code === 404) {
|
|
102
|
+
try {
|
|
103
|
+
return await channel.assertQueue(queueName, queueOptions);
|
|
104
|
+
} catch (assertErr) {
|
|
105
|
+
// If channel closed during assertQueue, throw descriptive error
|
|
106
|
+
if (!channel || channel.closed) {
|
|
107
|
+
throw new Error(`Channel closed during assertQueue for ${queueName} - likely due to RPC reply queue issue`);
|
|
108
|
+
}
|
|
109
|
+
throw assertErr;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Other error (including 406) - queue exists with different args
|
|
113
|
+
// Log warning and return queue info without asserting
|
|
114
|
+
console.warn(`[QueueManager] Queue ${queueName} exists with different arguments, using as-is:`, checkErr.message);
|
|
115
|
+
// Check channel again before checkQueue
|
|
116
|
+
if (!channel || channel.closed) {
|
|
117
|
+
throw new Error('Channel closed - cannot check queue');
|
|
118
|
+
}
|
|
119
|
+
return channel.checkQueue(queueName);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
85
122
|
}
|
|
86
123
|
|
|
87
124
|
/**
|
|
@@ -21,10 +21,8 @@ class WorkflowRouter {
|
|
|
21
21
|
* @param {Object} options - Additional publish options
|
|
22
22
|
*/
|
|
23
23
|
async publishWorkflowInit(workflow, options = {}) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
...options
|
|
27
|
-
});
|
|
24
|
+
// BaseClient.publish expects: publish(queue, message, options)
|
|
25
|
+
return this.client.publish(this.config.workflowInitQueue, workflow, options);
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
/**
|
|
@@ -34,10 +32,8 @@ class WorkflowRouter {
|
|
|
34
32
|
* @param {Object} options - Additional publish options
|
|
35
33
|
*/
|
|
36
34
|
async publishToServiceWorkflow(serviceName, message, options = {}) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
...options
|
|
40
|
-
});
|
|
35
|
+
// BaseClient.publish expects: publish(queue, message, options)
|
|
36
|
+
return this.client.publish(`${serviceName}.workflow`, message, options);
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
/**
|
|
@@ -47,10 +43,8 @@ class WorkflowRouter {
|
|
|
47
43
|
* @param {Object} options - Additional publish options
|
|
48
44
|
*/
|
|
49
45
|
async publishToService(serviceName, message, options = {}) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
...options
|
|
53
|
-
});
|
|
46
|
+
// BaseClient.publish expects: publish(queue, message, options)
|
|
47
|
+
return this.client.publish(`${serviceName}.queue`, message, options);
|
|
54
48
|
}
|
|
55
49
|
|
|
56
50
|
/**
|
|
@@ -59,10 +53,8 @@ class WorkflowRouter {
|
|
|
59
53
|
* @param {Object} options - Additional publish options
|
|
60
54
|
*/
|
|
61
55
|
async publishWorkflowCompleted(result, options = {}) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
...options
|
|
65
|
-
});
|
|
56
|
+
// BaseClient.publish expects: publish(queue, message, options)
|
|
57
|
+
return this.client.publish(this.config.workflowCompletedQueue, result, options);
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
/**
|
|
@@ -95,10 +87,8 @@ class WorkflowRouter {
|
|
|
95
87
|
* @param {Object} options - Consume options
|
|
96
88
|
*/
|
|
97
89
|
async consumeWorkflowInit(handler, options = {}) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
...options
|
|
101
|
-
});
|
|
90
|
+
// BaseClient.consume expects: consume(queue, handler, options)
|
|
91
|
+
return this.client.consume(this.config.workflowInitQueue, handler, options);
|
|
102
92
|
}
|
|
103
93
|
|
|
104
94
|
/**
|
|
@@ -114,10 +104,8 @@ class WorkflowRouter {
|
|
|
114
104
|
serviceName = this.config.serviceName;
|
|
115
105
|
}
|
|
116
106
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
...options
|
|
120
|
-
});
|
|
107
|
+
// BaseClient.consume expects: consume(queue, handler, options)
|
|
108
|
+
return this.client.consume(`${serviceName}.workflow`, handler, options);
|
|
121
109
|
}
|
|
122
110
|
|
|
123
111
|
/**
|
|
@@ -126,10 +114,8 @@ class WorkflowRouter {
|
|
|
126
114
|
* @param {Object} options - Consume options
|
|
127
115
|
*/
|
|
128
116
|
async consumeWorkflowCompleted(handler, options = {}) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
...options
|
|
132
|
-
});
|
|
117
|
+
// BaseClient.consume expects: consume(queue, handler, options)
|
|
118
|
+
return this.client.consume(this.config.workflowCompletedQueue, handler, options);
|
|
133
119
|
}
|
|
134
120
|
}
|
|
135
121
|
|
package/src/BaseClient.js
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ConnectorMQClient: a promise-based, broker-agnostic client for RabbitMQ
|
|
5
|
-
* Uses transportFactory to select the appropriate transport implementation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const Ajv = require('ajv');
|
|
9
|
-
const merge = require('lodash.merge');
|
|
10
|
-
|
|
11
|
-
const configSchema = require('./config/configSchema');
|
|
12
|
-
const defaultConfig = require('./config/defaultConfig');
|
|
13
|
-
const transportFactory = require('./transports/transportFactory');
|
|
14
|
-
const serializer = require('./utils/serializer');
|
|
15
|
-
const {
|
|
16
|
-
ConnectionError,
|
|
17
|
-
PublishError,
|
|
18
|
-
ConsumeError,
|
|
19
|
-
ValidationError,
|
|
20
|
-
SerializationError,
|
|
21
|
-
} = require('./utils/errorHandler');
|
|
22
|
-
|
|
23
|
-
class ConnectorMQClient {
|
|
24
|
-
/**
|
|
25
|
-
* @param {Object} config - User-supplied configuration.
|
|
26
|
-
* @throws {ValidationError} If required fields are missing or invalid.
|
|
27
|
-
*/
|
|
28
|
-
constructor(config) {
|
|
29
|
-
const ajv = new Ajv({ allErrors: true, useDefaults: true });
|
|
30
|
-
const validate = ajv.compile(configSchema);
|
|
31
|
-
|
|
32
|
-
// Merge user config with defaults
|
|
33
|
-
this._config = merge({}, defaultConfig, config || {});
|
|
34
|
-
|
|
35
|
-
// Validate merged config
|
|
36
|
-
const valid = validate(this._config);
|
|
37
|
-
if (!valid) {
|
|
38
|
-
const details = validate.errors.map((err) => ({
|
|
39
|
-
path: err.instancePath,
|
|
40
|
-
message: err.message,
|
|
41
|
-
}));
|
|
42
|
-
throw new ValidationError('Invalid configuration', details);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
this._transport = null;
|
|
46
|
-
this._connected = false;
|
|
47
|
-
this._errorHandlers = [];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Connects to the message broker using merged configuration.
|
|
52
|
-
* @param {Object} [options] - Optional overrides for host, queue, etc.
|
|
53
|
-
* @returns {Promise<void>}
|
|
54
|
-
* @throws {ConnectionError} If connecting fails.
|
|
55
|
-
*/
|
|
56
|
-
async connect(options = {}) {
|
|
57
|
-
if (this._connected) return;
|
|
58
|
-
|
|
59
|
-
// Merge overrides into existing config
|
|
60
|
-
this._config = merge({}, this._config, options);
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
// Instantiate appropriate transport: RabbitMQClient
|
|
64
|
-
this._transport = transportFactory.create(this._config);
|
|
65
|
-
|
|
66
|
-
// Register internal error propagation
|
|
67
|
-
this._transport.on('error', (err) => this._handleError(err));
|
|
68
|
-
|
|
69
|
-
await this._transport.connect(this._config);
|
|
70
|
-
this._connected = true;
|
|
71
|
-
} catch (err) {
|
|
72
|
-
throw new ConnectionError('Failed to connect to broker', err);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Disconnects from the message broker.
|
|
78
|
-
* @returns {Promise<void>}
|
|
79
|
-
* @throws {Error} If disconnecting fails unexpectedly.
|
|
80
|
-
*/
|
|
81
|
-
async disconnect() {
|
|
82
|
-
if (!this._connected || !this._transport) return;
|
|
83
|
-
try {
|
|
84
|
-
await this._transport.disconnect();
|
|
85
|
-
this._connected = false;
|
|
86
|
-
this._transport = null;
|
|
87
|
-
} catch (err) {
|
|
88
|
-
throw new Error(`Error during disconnect: ${err.message}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Publishes a message to the specified queue.
|
|
94
|
-
* @param {string} queue - Target queue name.
|
|
95
|
-
* @param {Object|Buffer|string} message - Payload to send.
|
|
96
|
-
* @param {Object} [options] - RabbitMQ-specific overrides (routingKey, persistent, headers).
|
|
97
|
-
* @returns {Promise<void>}
|
|
98
|
-
* @throws {ConnectionError} If not connected.
|
|
99
|
-
* @throws {PublishError} If publish fails.
|
|
100
|
-
*/
|
|
101
|
-
async publish(queue, message, options = {}) {
|
|
102
|
-
if (!this._connected || !this._transport) {
|
|
103
|
-
throw new ConnectionError('Cannot publish: client is not connected');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let buffer;
|
|
107
|
-
try {
|
|
108
|
-
if (Buffer.isBuffer(message)) {
|
|
109
|
-
buffer = message;
|
|
110
|
-
} else if (typeof message === 'string') {
|
|
111
|
-
buffer = Buffer.from(message, 'utf8');
|
|
112
|
-
} else {
|
|
113
|
-
const json = serializer.serialize(message);
|
|
114
|
-
buffer = Buffer.from(json, 'utf8');
|
|
115
|
-
}
|
|
116
|
-
} catch (err) {
|
|
117
|
-
throw new SerializationError('Failed to serialize message', message, err);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
await this._transport.publish(queue, buffer, options);
|
|
122
|
-
} catch (err) {
|
|
123
|
-
throw new PublishError(`Failed to publish to queue "${queue}"`, queue, err);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Begins consuming messages from the specified queue.
|
|
129
|
-
* @param {string} queue - Name of the queue to consume from.
|
|
130
|
-
* @param {function(Object): Promise<void>} messageHandler - Async function to process each message.
|
|
131
|
-
* @param {Object} [options] - RabbitMQ-specific overrides (prefetch, noAck).
|
|
132
|
-
* @returns {Promise<void>}
|
|
133
|
-
* @throws {ConnectionError} If not connected.
|
|
134
|
-
* @throws {ConsumeError} If consumer setup fails.
|
|
135
|
-
*/
|
|
136
|
-
async consume(queue, messageHandler, options = {}) {
|
|
137
|
-
if (!this._connected || !this._transport) {
|
|
138
|
-
throw new ConnectionError('Cannot consume: client is not connected');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Apply prefetch and noAck overrides if provided
|
|
142
|
-
const { prefetch, noAck } = options;
|
|
143
|
-
const consumeOptions = {};
|
|
144
|
-
if (typeof prefetch === 'number') consumeOptions.prefetch = prefetch;
|
|
145
|
-
if (typeof noAck === 'boolean') consumeOptions.noAck = noAck;
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
await this._transport.consume(
|
|
149
|
-
queue,
|
|
150
|
-
async (msg) => {
|
|
151
|
-
try {
|
|
152
|
-
await messageHandler(msg);
|
|
153
|
-
if (consumeOptions.noAck === false) {
|
|
154
|
-
await this.ack(msg);
|
|
155
|
-
}
|
|
156
|
-
} catch (handlerErr) {
|
|
157
|
-
// On handler error, nack with requeue: true
|
|
158
|
-
await this.nack(msg, { requeue: true });
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
consumeOptions
|
|
162
|
-
);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
throw new ConsumeError(`Failed to start consumer for queue "${queue}"`, queue, err);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Acknowledges a RabbitMQ message.
|
|
170
|
-
* @param {Object} msg - RabbitMQ message object.
|
|
171
|
-
* @returns {Promise<void>}
|
|
172
|
-
*/
|
|
173
|
-
async ack(msg) {
|
|
174
|
-
if (!this._connected || !this._transport) {
|
|
175
|
-
throw new ConnectionError('Cannot ack: client is not connected');
|
|
176
|
-
}
|
|
177
|
-
return this._transport.ack(msg);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Negative-acknowledges a RabbitMQ message.
|
|
182
|
-
* @param {Object} msg - RabbitMQ message object.
|
|
183
|
-
* @param {Object} [options] - Options such as { requeue: boolean }.
|
|
184
|
-
* @returns {Promise<void>}
|
|
185
|
-
*/
|
|
186
|
-
async nack(msg, options = {}) {
|
|
187
|
-
if (!this._connected || !this._transport) {
|
|
188
|
-
throw new ConnectionError('Cannot nack: client is not connected');
|
|
189
|
-
}
|
|
190
|
-
return this._transport.nack(msg, options);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Registers a global error handler. Internal or transport-level errors will be forwarded here.
|
|
195
|
-
* @param {function(Error): void} callback
|
|
196
|
-
*/
|
|
197
|
-
onError(callback) {
|
|
198
|
-
if (typeof callback === 'function') {
|
|
199
|
-
this._errorHandlers.push(callback);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Internal helper to invoke all registered error handlers.
|
|
205
|
-
* @param {Error} error
|
|
206
|
-
* @private
|
|
207
|
-
*/
|
|
208
|
-
_handleError(error) {
|
|
209
|
-
this._errorHandlers.forEach((cb) => {
|
|
210
|
-
try {
|
|
211
|
-
cb(error);
|
|
212
|
-
} catch (_) {
|
|
213
|
-
// Ignore errors in user-provided error handlers
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
module.exports = ConnectorMQClient;
|