@onlineapps/conn-infra-mq 1.1.20 → 1.1.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-infra-mq",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
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": {
@@ -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
@@ -192,6 +215,7 @@ class QueueManager {
192
215
 
193
216
  /**
194
217
  * Setup service queues (main + DLQ + optional workflow queue)
218
+ * CRITICAL: All queues MUST be created together - if one fails, all fail
195
219
  * @param {string} serviceName - Name of the service
196
220
  * @param {Object} options - Configuration options
197
221
  */
@@ -200,67 +224,110 @@ class QueueManager {
200
224
  if (!transport || !transport.channel) {
201
225
  throw new Error('MQ client not connected');
202
226
  }
203
- // Use queueChannel for queue operations to avoid RPC reply queue issues
204
- const channel = transport.queueChannel || transport.channel;
205
-
206
- const queues = {};
207
-
208
- // Create main processing queue
209
- // Handle PRECONDITION-FAILED gracefully - queue may exist with different args
210
- try {
211
- await this.ensureQueue(`${serviceName}.queue`, {
212
- ttl: options.ttl || this.config.defaultTTL,
213
- dlq: true,
214
- durable: true,
215
- maxRetries: this.config.maxRetries
216
- });
217
- } catch (error) {
218
- if (error.code === 406) {
219
- // Queue exists with different arguments - use it as-is
220
- console.warn(`[QueueManager] Queue ${serviceName}.queue exists with different arguments, using as-is`);
221
- // Verify queue exists
222
- await channel.checkQueue(`${serviceName}.queue`);
223
- } else {
224
- throw error;
225
- }
227
+
228
+ // CRITICAL: Use queueChannel for queue operations
229
+ // If channel is closed, we cannot continue - this is a critical error
230
+ let channel = transport.queueChannel || transport.channel;
231
+
232
+ if (!channel || channel.closed) {
233
+ throw new Error('Queue channel is not available or closed - cannot create business queues');
226
234
  }
227
- queues.main = `${serviceName}.queue`;
228
235
 
229
- // Create dead letter queue
230
- try {
231
- await this.ensureQueue(`${serviceName}.dlq`, {
232
- ttl: null, // No TTL for DLQ
233
- dlq: false,
234
- durable: true,
235
- autoDelete: false
236
- });
237
- } catch (error) {
238
- if (error.code === 406) {
239
- console.warn(`[QueueManager] Queue ${serviceName}.dlq exists with different arguments, using as-is`);
240
- await channel.checkQueue(`${serviceName}.dlq`);
241
- } else {
242
- throw error;
243
- }
236
+ const queues = {};
237
+ const queueNames = [];
238
+
239
+ // Collect all queue names that need to be created
240
+ queueNames.push({ name: `${serviceName}.queue`, type: 'main' });
241
+ queueNames.push({ name: `${serviceName}.dlq`, type: 'dlq' });
242
+ if (options.includeWorkflow !== false) {
243
+ queueNames.push({ name: `${serviceName}.workflow`, type: 'workflow' });
244
244
  }
245
- queues.dlq = `${serviceName}.dlq`;
246
245
 
247
- // Create workflow queue if requested
248
- if (options.includeWorkflow !== false) {
249
- try {
250
- await this.ensureQueue(`${serviceName}.workflow`, {
246
+ // CRITICAL: Create ALL queues using assertQueue directly (no checkQueue first to avoid channel closure)
247
+ // This ensures all queues are created atomically
248
+ for (const queueInfo of queueNames) {
249
+ const queueName = queueInfo.name;
250
+ let queueOptions;
251
+
252
+ if (queueInfo.type === 'main') {
253
+ queueOptions = this._buildQueueOptions(queueName, {
254
+ ttl: options.ttl || this.config.defaultTTL,
255
+ dlq: true,
256
+ durable: true,
257
+ maxRetries: this.config.maxRetries
258
+ });
259
+ } else if (queueInfo.type === 'dlq') {
260
+ queueOptions = this._buildQueueOptions(queueName, {
261
+ ttl: null,
262
+ dlq: false,
263
+ durable: true,
264
+ autoDelete: false
265
+ });
266
+ } else if (queueInfo.type === 'workflow') {
267
+ queueOptions = this._buildQueueOptions(queueName, {
251
268
  ttl: options.workflowTTL || this.config.defaultTTL,
252
269
  dlq: true,
253
270
  durable: true
254
271
  });
255
- } catch (error) {
256
- if (error.code === 406) {
257
- console.warn(`[QueueManager] Queue ${serviceName}.workflow exists with different arguments, using as-is`);
258
- await channel.checkQueue(`${serviceName}.workflow`);
272
+ }
273
+
274
+ // Use business queue config from queueConfig if available
275
+ if (queueConfig.isBusinessQueue(queueName)) {
276
+ try {
277
+ const parsed = queueConfig.parseBusinessQueue(queueName);
278
+ if (parsed) {
279
+ const businessConfig = queueConfig.getBusinessQueueConfig(parsed.queueType, parsed.serviceName);
280
+ queueOptions = {
281
+ durable: businessConfig.durable !== false,
282
+ arguments: { ...businessConfig.arguments }
283
+ };
284
+ // Replace placeholder {service} with actual service name
285
+ if (queueOptions.arguments['x-dead-letter-routing-key']) {
286
+ queueOptions.arguments['x-dead-letter-routing-key'] =
287
+ queueOptions.arguments['x-dead-letter-routing-key'].replace('{service}', serviceName);
288
+ }
289
+ }
290
+ } catch (error) {
291
+ // If config not found, use built options
292
+ console.warn(`[QueueManager] Business queue config not found for ${queueName}, using provided options:`, error.message);
293
+ }
294
+ }
295
+
296
+ // CRITICAL: Use assertQueue directly - if queue exists with different args (406), that's OK
297
+ // We'll handle it gracefully, but channel should NOT close
298
+ try {
299
+ // Check if channel is still open before each operation
300
+ if (!channel || channel.closed) {
301
+ throw new Error(`Channel closed before creating ${queueName} - cannot continue`);
302
+ }
303
+
304
+ await channel.assertQueue(queueName, queueOptions);
305
+ queues[queueInfo.type] = queueName;
306
+ console.info(`[QueueManager] ✓ Created business queue: ${queueName}`);
307
+ } catch (assertErr) {
308
+ // If channel closed, this is a critical error
309
+ if (!channel || channel.closed) {
310
+ throw new Error(`Channel closed during assertQueue for ${queueName} - cannot create remaining queues`);
311
+ }
312
+
313
+ // If 406 PRECONDITION-FAILED, queue exists with different args - use it as-is
314
+ if (assertErr.code === 406) {
315
+ console.warn(`[QueueManager] Queue ${queueName} exists with different arguments, using as-is:`, assertErr.message);
316
+ // Verify queue exists
317
+ if (!channel || channel.closed) {
318
+ throw new Error(`Channel closed after 406 error for ${queueName}`);
319
+ }
320
+ try {
321
+ await channel.checkQueue(queueName);
322
+ queues[queueInfo.type] = queueName;
323
+ } catch (checkErr) {
324
+ throw new Error(`Failed to verify existing queue ${queueName}: ${checkErr.message}`);
325
+ }
259
326
  } else {
260
- throw error;
327
+ // Other error - critical, cannot continue
328
+ throw new Error(`Failed to create business queue ${queueName}: ${assertErr.message}`);
261
329
  }
262
330
  }
263
- queues.workflow = `${serviceName}.workflow`;
264
331
  }
265
332
 
266
333
  // Setup DLQ exchange and bindings
@@ -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
  }