@onlineapps/conn-infra-mq 1.1.10 → 1.1.12

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 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.10",
3
+ "version": "1.1.12",
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"
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const BaseClient = require('./BaseClient');
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');
@@ -72,6 +72,6 @@ module.exports = {
72
72
  additionalProperties: false,
73
73
  },
74
74
  },
75
- required: ['type', 'host', 'queue'],
75
+ required: ['type', 'host', 'queue', 'serviceName'],
76
76
  additionalProperties: false,
77
77
  };
package/src/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  const ConnectorMQClient = require('./ConnectorMQClient');
15
- const BaseClient = require('./BaseClient');
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,23 @@ class QueueManager {
80
80
  // Track managed queue
81
81
  this.managedQueues.add(queueName);
82
82
 
83
- // Assert the queue
84
- return channel.assertQueue(queueName, queueOptions);
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
+ try {
86
+ await channel.checkQueue(queueName);
87
+ // Queue exists - return queue info
88
+ return channel.checkQueue(queueName);
89
+ } catch (checkErr) {
90
+ // If queue doesn't exist (404), create it with provided options
91
+ if (checkErr.code === 404) {
92
+ return channel.assertQueue(queueName, queueOptions);
93
+ } else {
94
+ // Other error (including 406) - queue exists with different args
95
+ // Log warning and return queue info without asserting
96
+ console.warn(`[QueueManager] Queue ${queueName} exists with different arguments, using as-is:`, checkErr.message);
97
+ return channel.checkQueue(queueName);
98
+ }
99
+ }
85
100
  }
86
101
 
87
102
  /**
@@ -154,30 +169,60 @@ class QueueManager {
154
169
  const queues = {};
155
170
 
156
171
  // Create main processing queue
157
- await this.ensureQueue(`${serviceName}.queue`, {
158
- ttl: options.ttl || this.config.defaultTTL,
159
- dlq: true,
160
- durable: true,
161
- maxRetries: this.config.maxRetries
162
- });
172
+ // Handle PRECONDITION-FAILED gracefully - queue may exist with different args
173
+ try {
174
+ await this.ensureQueue(`${serviceName}.queue`, {
175
+ ttl: options.ttl || this.config.defaultTTL,
176
+ dlq: true,
177
+ durable: true,
178
+ maxRetries: this.config.maxRetries
179
+ });
180
+ } catch (error) {
181
+ if (error.code === 406) {
182
+ // Queue exists with different arguments - use it as-is
183
+ console.warn(`[QueueManager] Queue ${serviceName}.queue exists with different arguments, using as-is`);
184
+ // Verify queue exists
185
+ await channel.checkQueue(`${serviceName}.queue`);
186
+ } else {
187
+ throw error;
188
+ }
189
+ }
163
190
  queues.main = `${serviceName}.queue`;
164
191
 
165
192
  // Create dead letter queue
166
- await this.ensureQueue(`${serviceName}.dlq`, {
167
- ttl: null, // No TTL for DLQ
168
- dlq: false,
169
- durable: true,
170
- autoDelete: false
171
- });
193
+ try {
194
+ await this.ensureQueue(`${serviceName}.dlq`, {
195
+ ttl: null, // No TTL for DLQ
196
+ dlq: false,
197
+ durable: true,
198
+ autoDelete: false
199
+ });
200
+ } catch (error) {
201
+ if (error.code === 406) {
202
+ console.warn(`[QueueManager] Queue ${serviceName}.dlq exists with different arguments, using as-is`);
203
+ await channel.checkQueue(`${serviceName}.dlq`);
204
+ } else {
205
+ throw error;
206
+ }
207
+ }
172
208
  queues.dlq = `${serviceName}.dlq`;
173
209
 
174
210
  // Create workflow queue if requested
175
211
  if (options.includeWorkflow !== false) {
176
- await this.ensureQueue(`${serviceName}.workflow`, {
177
- ttl: options.workflowTTL || this.config.defaultTTL,
178
- dlq: true,
179
- durable: true
180
- });
212
+ try {
213
+ await this.ensureQueue(`${serviceName}.workflow`, {
214
+ ttl: options.workflowTTL || this.config.defaultTTL,
215
+ dlq: true,
216
+ durable: true
217
+ });
218
+ } catch (error) {
219
+ if (error.code === 406) {
220
+ console.warn(`[QueueManager] Queue ${serviceName}.workflow exists with different arguments, using as-is`);
221
+ await channel.checkQueue(`${serviceName}.workflow`);
222
+ } else {
223
+ throw error;
224
+ }
225
+ }
181
226
  queues.workflow = `${serviceName}.workflow`;
182
227
  }
183
228
 
@@ -21,10 +21,8 @@ class WorkflowRouter {
21
21
  * @param {Object} options - Additional publish options
22
22
  */
23
23
  async publishWorkflowInit(workflow, options = {}) {
24
- return this.client.publish(workflow, {
25
- queue: this.config.workflowInitQueue,
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
- return this.client.publish(message, {
38
- queue: `${serviceName}.workflow`,
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
- return this.client.publish(message, {
51
- queue: `${serviceName}.queue`,
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
- return this.client.publish(result, {
63
- queue: this.config.workflowCompletedQueue,
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
- return this.client.consume(handler, {
99
- queue: this.config.workflowInitQueue,
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
- return this.client.consume(handler, {
118
- queue: `${serviceName}.workflow`,
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
- return this.client.consume(handler, {
130
- queue: this.config.workflowCompletedQueue,
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;