@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 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.11",
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"
@@ -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,45 @@ 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
+ // 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
- 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;