@onlineapps/conn-infra-mq 1.1.19 → 1.1.21

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
@@ -4,16 +4,16 @@
4
4
  [![Coverage Status](https://codecov.io/gh/onlineapps/conn-infra-mq/branch/main/graph/badge.svg)](https://codecov.io/gh/onlineapps/conn-infra-mq)
5
5
  [![npm version](https://img.shields.io/npm/v/@onlineapps/conn-infra-mq)](https://www.npmjs.com/package/@onlineapps/conn-infra-mq)
6
6
 
7
- > Message queue connector with **layered architecture** for workflow orchestration, RPC, fork-join, and retry patterns. Built on top of RabbitMQ with clean separation of concerns.
7
+ > Message queue connector with **layered architecture** for workflow orchestration, fork-join, and retry patterns. Built on top of RabbitMQ with clean separation of concerns. **Asynchronous workflow pattern only** - synchronous RPC patterns are not supported and not aligned with our architecture philosophy.
8
8
 
9
9
  ---
10
10
 
11
11
  ## 🚀 Features
12
12
 
13
- - **Layered Architecture**: Clean separation into specialized layers (WorkflowRouter, QueueManager, ForkJoinHandler, RPCHandler, RetryHandler)
13
+ - **Layered Architecture**: Clean separation into specialized layers (WorkflowRouter, QueueManager, ForkJoinHandler, RetryHandler)
14
14
  - **Workflow Orchestration**: Decentralized workflow routing without central orchestrator
15
15
  - **Fork-Join Pattern**: Parallel processing with result aggregation and built-in join strategies
16
- - **RPC Support**: Request-response communication with correlation IDs and timeouts
16
+ - **Asynchronous First**: All communication is asynchronous (fire-and-forget), no synchronous blocking patterns
17
17
  - **Automatic Retry**: Exponential backoff, dead letter queue management, configurable retry policies
18
18
  - **Queue Management**: TTL, DLQ, auto-delete, temporary queues, exchange bindings
19
19
  - **Promise-based API**: All operations return promises for clean async/await usage
@@ -43,7 +43,6 @@ ConnectorMQClient (main orchestrator - for business services only)
43
43
  ├── WorkflowRouter (workflow orchestration)
44
44
  ├── QueueManager (queue lifecycle management)
45
45
  ├── ForkJoinHandler (parallel processing)
46
- ├── RPCHandler (request-response patterns)
47
46
  └── RetryHandler (error recovery & DLQ)
48
47
  ```
49
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-infra-mq",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
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": {
@@ -4,7 +4,6 @@ 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');
7
- const RPCHandler = require('./layers/RPCHandler');
8
7
  const RetryHandler = require('./layers/RetryHandler');
9
8
 
10
9
  /**
@@ -25,8 +24,7 @@ const RetryHandler = require('./layers/RetryHandler');
25
24
  * @example <caption>With Workflow</caption>
26
25
  * const mqClient = new ConnectorMQClient({
27
26
  * url: 'amqp://localhost:5672',
28
- * serviceName: 'invoice-service',
29
- * enableRPC: true
27
+ * serviceName: 'invoice-service'
30
28
  * });
31
29
  */
32
30
  class ConnectorMQClient extends BaseClient {
@@ -37,7 +35,6 @@ class ConnectorMQClient extends BaseClient {
37
35
  * @param {Object} [config={}] - Configuration options
38
36
  * @param {string} config.url - RabbitMQ connection URL
39
37
  * @param {string} [config.serviceName='unknown'] - Service name
40
- * @param {boolean} [config.enableRPC=true] - Enable RPC handler
41
38
  * @param {number} [config.prefetchCount=10] - Message prefetch count
42
39
  * @param {Object} [config.retry] - Retry configuration
43
40
  *
@@ -56,7 +53,6 @@ class ConnectorMQClient extends BaseClient {
56
53
  this.queues = new QueueManager(this, config);
57
54
  this.retry = new RetryHandler(this, config);
58
55
  this.forkJoin = new ForkJoinHandler(this, this.queues, config);
59
- this.rpc = new RPCHandler(this, this.queues, config);
60
56
 
61
57
  // Service identification
62
58
  this.serviceName = config.serviceName || 'unknown';
@@ -86,11 +82,6 @@ class ConnectorMQClient extends BaseClient {
86
82
  // Base connection
87
83
  await super.connect();
88
84
 
89
- // Initialize RPC handler if needed
90
- if (this._config.enableRPC !== false) {
91
- await this.rpc.initialize();
92
- }
93
-
94
85
  // Setup service queues if service name provided
95
86
  const shouldAutoSetup = this._config.autoSetupServiceQueues !== false;
96
87
 
@@ -126,7 +117,6 @@ class ConnectorMQClient extends BaseClient {
126
117
  if (this._initialized) {
127
118
  // Cleanup layers
128
119
  await this.forkJoin.cleanupAll();
129
- await this.rpc.cleanup();
130
120
  await this.queues.cleanup();
131
121
  }
132
122
 
@@ -291,38 +281,6 @@ class ConnectorMQClient extends BaseClient {
291
281
  return this.forkJoin.join(queueName, joinStrategy, options);
292
282
  }
293
283
 
294
- // ============================================
295
- // RPC Operations (via RPCHandler)
296
- // ============================================
297
-
298
- /**
299
- * Make RPC call
300
- */
301
- async rpcCall(targetQueue, request, options) {
302
- return this.rpc.call(targetQueue, request, options);
303
- }
304
-
305
- /**
306
- * Setup RPC server
307
- */
308
- async rpcServe(queue, handler, options) {
309
- return this.rpc.serve(queue, handler, options);
310
- }
311
-
312
- /**
313
- * Make multiple RPC calls
314
- */
315
- async rpcCallMany(requests) {
316
- return this.rpc.callMany(requests);
317
- }
318
-
319
- /**
320
- * RPC with retry
321
- */
322
- async rpcCallWithRetry(targetQueue, request, options) {
323
- return this.rpc.callWithRetry(targetQueue, request, options);
324
- }
325
-
326
284
  // ============================================
327
285
  // Retry Operations (via RetryHandler)
328
286
  // ============================================
@@ -374,8 +332,6 @@ class ConnectorMQClient extends BaseClient {
374
332
  return this.publishWorkflowInit(message, options);
375
333
  } else if (options.service) {
376
334
  return this.publishToService(options.service, message, options);
377
- } else if (options.rpc) {
378
- return this.rpcCall(options.rpc, message, options);
379
335
  } else {
380
336
  return this.publish(message, options);
381
337
  }
@@ -416,12 +372,10 @@ class ConnectorMQClient extends BaseClient {
416
372
  workflow: true,
417
373
  queues: true,
418
374
  retry: true,
419
- forkJoin: true,
420
- rpc: this.rpc.getPendingCount() >= 0
375
+ forkJoin: true
421
376
  },
422
377
  stats: {
423
378
  retry: this.retry.getStats(),
424
- pendingRPC: this.rpc.getPendingCount(),
425
379
  managedQueues: this.queues.getManagedQueues().length
426
380
  }
427
381
  };
package/src/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  /**
4
4
  * @module @onlineapps/conn-infra-mq
5
- * @description RabbitMQ connector with workflow routing, fork-join, RPC, and retry patterns for OA Drive.
6
- * Provides layered architecture for complex messaging patterns.
5
+ * @description RabbitMQ connector with workflow routing, fork-join, and retry patterns for OA Drive.
6
+ * Provides layered architecture for asynchronous messaging patterns.
7
7
  *
8
8
  * @see {@link https://github.com/onlineapps/oa-drive/tree/main/shared/connector/conn-infra-mq|GitHub Repository}
9
9
  * @author OA Drive Team
@@ -18,7 +18,6 @@ const BaseClient = require('@onlineapps/mq-client-core');
18
18
  const WorkflowRouter = require('./layers/WorkflowRouter');
19
19
  const QueueManager = require('./layers/QueueManager');
20
20
  const ForkJoinHandler = require('./layers/ForkJoinHandler');
21
- const RPCHandler = require('./layers/RPCHandler');
22
21
  const RetryHandler = require('./layers/RetryHandler');
23
22
 
24
23
  // Default export - the main client
@@ -33,7 +32,6 @@ module.exports.layers = {
33
32
  WorkflowRouter,
34
33
  QueueManager,
35
34
  ForkJoinHandler,
36
- RPCHandler,
37
35
  RetryHandler
38
36
  };
39
37
 
@@ -103,17 +103,40 @@ class QueueManager {
103
103
  throw new Error('Channel closed during checkQueue - cannot verify queue');
104
104
  }
105
105
 
106
- // If queue doesn't exist (404), we'll let it be created lazily on first publish
107
- // This avoids RPC reply queue issues with assertQueue()
106
+ // If queue doesn't exist (404), create it using assertQueue with proper error handling
107
+ // Business queues MUST be created with specific arguments (TTL, DLQ, max-length) from queueConfig
108
+ // Lazy creation via sendToQueue() is NOT suitable - it creates queue with default arguments
108
109
  if (checkErr.code === 404) {
109
- // Queue doesn't exist - will be created on first sendToQueue()
110
- // Return a mock queue info to indicate queue will be created lazily
111
- console.info(`[QueueManager] Queue ${queueName} doesn't exist - will be created on first publish`);
112
- return {
113
- queue: queueName,
114
- messageCount: 0,
115
- consumerCount: 0
116
- };
110
+ // CRITICAL: Business queues MUST be created explicitly with correct arguments
111
+ // Use assertQueue() on regular channel (queueChannel) - this should work without RPC issues
112
+ // If it fails, it's a real problem that needs to be fixed, not worked around
113
+ try {
114
+ if (!channel || channel.closed) {
115
+ throw new Error('Channel closed - cannot create queue');
116
+ }
117
+
118
+ // For business queues: use assertQueue with queueOptions from queueConfig
119
+ // This ensures correct TTL, DLQ, max-length arguments
120
+ return await channel.assertQueue(queueName, queueOptions);
121
+ } catch (assertErr) {
122
+ // If channel closed during assertQueue, it's a real error
123
+ if (!channel || channel.closed) {
124
+ throw new Error(`Channel closed during assertQueue for ${queueName} - check channel management`);
125
+ }
126
+
127
+ // If 406 PRECONDITION-FAILED, queue exists with different args
128
+ if (assertErr.code === 406) {
129
+ console.warn(`[QueueManager] Queue ${queueName} exists with different arguments:`, assertErr.message);
130
+ // Try to get queue info anyway
131
+ if (!channel || channel.closed) {
132
+ throw new Error('Channel closed - cannot check existing queue');
133
+ }
134
+ return await channel.checkQueue(queueName);
135
+ }
136
+
137
+ // Other errors - rethrow
138
+ throw assertErr;
139
+ }
117
140
  } else {
118
141
  // Other error (including 406) - queue exists with different args
119
142
  // Log warning and return queue info without asserting
@@ -222,23 +222,24 @@ class RabbitMQClient extends EventEmitter {
222
222
  throw checkErr;
223
223
  }
224
224
  } else {
225
- // Business queue - may need to be created, but use unified config
225
+ // Business queue - should already be created by setupServiceQueues() BEFORE consume is called
226
226
  // Use queueChannel (regular channel) for queue operations to avoid RPC reply queue issues
227
- const queueOptions = this._getQueueOptions(queue, { durable });
228
-
229
- // Try to assert queue with our config
230
- // If it fails with 406 (PRECONDITION-FAILED), queue exists with different args - use it as-is
231
- // IMPORTANT: Don't try to re-assert after 406, as it will close the channel
227
+ // Only check if it exists - don't try to create it here (that's setupServiceQueues() responsibility)
232
228
  try {
233
- await this._queueChannel.assertQueue(queue, queueOptions);
234
- } catch (assertErr) {
235
- // If queue exists with different arguments (406), use it as-is without re-asserting
236
- if (assertErr.code === 406) {
237
- console.warn(`[RabbitMQClient] Queue ${queue} exists with different arguments, using as-is (skipping assert to avoid channel close):`, assertErr.message);
238
- // Don't try to re-assert - just proceed to consume
229
+ await this._queueChannel.checkQueue(queue);
230
+ // Queue exists - proceed to consume
231
+ } catch (checkErr) {
232
+ if (checkErr.code === 404) {
233
+ // Queue doesn't exist - this is an error, queue should have been created by setupServiceQueues()
234
+ throw new Error(`Business queue '${queue}' not found. Queue should be created by setupServiceQueues() before consuming.`);
235
+ }
236
+ // Other error (including 406) - queue exists with different args, use it as-is
237
+ if (checkErr.code === 406) {
238
+ console.warn(`[RabbitMQClient] Queue ${queue} exists with different arguments, using as-is:`, checkErr.message);
239
+ // Proceed to consume - queue exists, just with different args
239
240
  } else {
240
241
  // Other error - rethrow
241
- throw assertErr;
242
+ throw checkErr;
242
243
  }
243
244
  }
244
245
  }
@@ -1,324 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * RPCHandler - Manages RPC (Remote Procedure Call) patterns
5
- * Handles request-response communication with correlation IDs and timeouts
6
- */
7
- class RPCHandler {
8
- constructor(mqClient, queueManager, config = {}) {
9
- this.client = mqClient;
10
- this.queueManager = queueManager;
11
- this.config = {
12
- defaultTimeout: config.defaultTimeout || 5000,
13
- replyQueuePrefix: config.replyQueuePrefix || 'rpc.reply',
14
- ...config
15
- };
16
- this.pendingRequests = new Map();
17
- this.replyQueue = null;
18
- this.replyConsumer = null;
19
- }
20
-
21
- /**
22
- * Initialize RPC handler (create reply queue)
23
- */
24
- async initialize() {
25
- if (this.replyQueue) {
26
- return this.replyQueue;
27
- }
28
-
29
- // Create exclusive reply queue
30
- this.replyQueue = await this.queueManager.createTemporaryQueue(
31
- this.config.replyQueuePrefix,
32
- {
33
- exclusive: true,
34
- autoDelete: true,
35
- expires: 3600000 // 1 hour
36
- }
37
- );
38
-
39
- // Start consuming replies
40
- this.replyConsumer = await this.client.consume(
41
- this.replyQueue,
42
- (message, rawMsg) => this.handleReply(message, rawMsg),
43
- {
44
- noAck: true
45
- }
46
- );
47
-
48
- return this.replyQueue;
49
- }
50
-
51
- /**
52
- * Send RPC request and wait for response
53
- * @param {string} targetQueue - Target service queue
54
- * @param {Object} request - Request message
55
- * @param {Object} options - RPC options
56
- */
57
- async call(targetQueue, request, options = {}) {
58
- // Ensure reply queue exists
59
- await this.initialize();
60
-
61
- const correlationId = this.generateCorrelationId();
62
- const timeout = options.timeout || this.config.defaultTimeout;
63
-
64
- // Create promise for response
65
- const responsePromise = new Promise((resolve, reject) => {
66
- // Setup timeout
67
- const timeoutHandle = setTimeout(() => {
68
- this.pendingRequests.delete(correlationId);
69
- reject(new Error(`RPC request timeout after ${timeout}ms`));
70
- }, timeout);
71
-
72
- // Store pending request
73
- this.pendingRequests.set(correlationId, {
74
- resolve,
75
- reject,
76
- timeoutHandle,
77
- startTime: Date.now(),
78
- targetQueue
79
- });
80
- });
81
-
82
- // Send request with correlation ID and reply queue
83
- await this.client.publish(request, {
84
- queue: targetQueue,
85
- correlationId,
86
- replyTo: this.replyQueue,
87
- expiration: timeout.toString(),
88
- ...options
89
- });
90
-
91
- return responsePromise;
92
- }
93
-
94
- /**
95
- * Setup service as RPC server
96
- * @param {string} queue - Queue to listen on
97
- * @param {Function} handler - Request handler function
98
- * @param {Object} options - Server options
99
- */
100
- async serve(queue, handler, options = {}) {
101
- return this.client.consume(async (request, rawMsg) => {
102
- const { correlationId, replyTo } = rawMsg.properties;
103
-
104
- if (!replyTo) {
105
- // Not an RPC request, handle normally
106
- if (options.allowNonRPC) {
107
- await handler(request, rawMsg);
108
- }
109
- await this.client.ack(rawMsg);
110
- return;
111
- }
112
-
113
- try {
114
- // Process request
115
- const response = await handler(request, rawMsg);
116
-
117
- // Send response
118
- if (replyTo && correlationId) {
119
- await this.client.publish(response, {
120
- queue: replyTo,
121
- correlationId
122
- });
123
- }
124
-
125
- await this.client.ack(rawMsg);
126
- } catch (error) {
127
- // Send error response
128
- if (replyTo && correlationId) {
129
- await this.client.publish(
130
- {
131
- error: {
132
- message: error.message,
133
- code: error.code || 'RPC_ERROR',
134
- timestamp: Date.now()
135
- }
136
- },
137
- {
138
- queue: replyTo,
139
- correlationId
140
- }
141
- );
142
- }
143
-
144
- // Reject or acknowledge based on configuration
145
- if (options.rejectOnError) {
146
- await this.client.nack(rawMsg, false);
147
- } else {
148
- await this.client.ack(rawMsg);
149
- }
150
- }
151
- }, {
152
- queue,
153
- ...options
154
- });
155
- }
156
-
157
- /**
158
- * Handle RPC reply
159
- * @private
160
- */
161
- handleReply(message, rawMsg) {
162
- const { correlationId } = rawMsg.properties;
163
-
164
- if (!correlationId || !this.pendingRequests.has(correlationId)) {
165
- // Unknown or expired request
166
- return;
167
- }
168
-
169
- const pending = this.pendingRequests.get(correlationId);
170
- this.pendingRequests.delete(correlationId);
171
-
172
- // Clear timeout
173
- clearTimeout(pending.timeoutHandle);
174
-
175
- // Calculate round-trip time
176
- const rtt = Date.now() - pending.startTime;
177
-
178
- // Check for error response
179
- if (message && message.error) {
180
- const error = new Error(message.error.message || 'RPC error');
181
- error.code = message.error.code;
182
- error.rtt = rtt;
183
- pending.reject(error);
184
- } else {
185
- // Add metadata to response
186
- if (typeof message === 'object' && message !== null) {
187
- message._rpcMetadata = {
188
- rtt,
189
- correlationId,
190
- targetQueue: pending.targetQueue
191
- };
192
- }
193
- pending.resolve(message);
194
- }
195
- }
196
-
197
- /**
198
- * Call multiple RPC requests in parallel
199
- * @param {Array} requests - Array of { queue, request, options } objects
200
- */
201
- async callMany(requests) {
202
- const promises = requests.map(req =>
203
- this.call(req.queue, req.request, req.options)
204
- .then(response => ({ success: true, response, queue: req.queue }))
205
- .catch(error => ({ success: false, error, queue: req.queue }))
206
- );
207
-
208
- return Promise.all(promises);
209
- }
210
-
211
- /**
212
- * Call with retry logic
213
- * @param {string} targetQueue - Target service queue
214
- * @param {Object} request - Request message
215
- * @param {Object} options - RPC options with retry configuration
216
- */
217
- async callWithRetry(targetQueue, request, options = {}) {
218
- const maxRetries = options.maxRetries || 3;
219
- const retryDelay = options.retryDelay || 1000;
220
- let lastError;
221
-
222
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
223
- try {
224
- return await this.call(targetQueue, request, options);
225
- } catch (error) {
226
- lastError = error;
227
-
228
- if (attempt < maxRetries) {
229
- // Wait before retry with exponential backoff
230
- const delay = retryDelay * Math.pow(2, attempt);
231
- await new Promise(resolve => setTimeout(resolve, delay));
232
- }
233
- }
234
- }
235
-
236
- throw lastError;
237
- }
238
-
239
- /**
240
- * Broadcast request to multiple queues and collect responses
241
- * @param {Array} queues - Target queues
242
- * @param {Object} request - Request message
243
- * @param {Object} options - Broadcast options
244
- */
245
- async broadcast(queues, request, options = {}) {
246
- const timeout = options.timeout || this.config.defaultTimeout;
247
- const waitForAll = options.waitForAll !== false;
248
-
249
- const results = [];
250
- const promises = [];
251
-
252
- for (const queue of queues) {
253
- const promise = this.call(queue, request, { timeout })
254
- .then(response => {
255
- results.push({ queue, response, success: true });
256
- return { queue, response, success: true };
257
- })
258
- .catch(error => {
259
- const result = { queue, error: error.message, success: false };
260
- if (!waitForAll) {
261
- results.push(result);
262
- }
263
- return result;
264
- });
265
-
266
- promises.push(promise);
267
- }
268
-
269
- if (waitForAll) {
270
- const allResults = await Promise.all(promises);
271
- return allResults;
272
- } else {
273
- // Race for first successful response
274
- await Promise.race(promises.filter(p => p.then(r => r.success)));
275
- return results;
276
- }
277
- }
278
-
279
- /**
280
- * Generate unique correlation ID
281
- * @private
282
- */
283
- generateCorrelationId() {
284
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
285
- }
286
-
287
- /**
288
- * Get pending requests count
289
- */
290
- getPendingCount() {
291
- return this.pendingRequests.size;
292
- }
293
-
294
- /**
295
- * Clear all pending requests
296
- */
297
- clearPending() {
298
- for (const pending of this.pendingRequests.values()) {
299
- clearTimeout(pending.timeoutHandle);
300
- pending.reject(new Error('RPC handler shutdown'));
301
- }
302
- this.pendingRequests.clear();
303
- }
304
-
305
- /**
306
- * Cleanup RPC handler
307
- */
308
- async cleanup() {
309
- this.clearPending();
310
-
311
- if (this.replyQueue) {
312
- try {
313
- await this.queueManager.deleteQueue(this.replyQueue);
314
- } catch (error) {
315
- // Queue might already be deleted
316
- }
317
- this.replyQueue = null;
318
- }
319
-
320
- this.replyConsumer = null;
321
- }
322
- }
323
-
324
- module.exports = RPCHandler;