@onlineapps/conn-infra-mq 1.1.36 → 1.1.38

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.36",
3
+ "version": "1.1.38",
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": {
@@ -249,29 +249,64 @@ class QueueManager {
249
249
  for (const queueInfo of queueNames) {
250
250
  // CRITICAL: Check if channel is closed before each queue operation (may have been closed by previous 406)
251
251
  // Channel closes SYNCHRONOUSLY on 406, so we must check before each assertQueue
252
- // amqplib's channel.closed may be undefined, so we test by trying a no-op operation
252
+ // We check channel state directly - if it's null or closed, recreate it
253
253
  if (!channel) {
254
254
  console.warn(`[QueueManager] DEBUG: Channel is null before ${queueInfo.name}, recreating...`);
255
255
  channel = await transport._connection.createChannel();
256
+ channel._createdAt = new Date().toISOString();
257
+ channel._closeReason = null;
258
+ channel._lastOperation = `Recreated (was null) before ${queueInfo.name}`;
259
+
260
+ channel.on('error', (err) => {
261
+ console.error('[RabbitMQClient] Recreated queue channel error:', err.message);
262
+ channel._closeReason = `Error: ${err.message} (code: ${err.code})`;
263
+ });
264
+ channel.on('close', () => {
265
+ console.error('[RabbitMQClient] Recreated queue channel closed');
266
+ console.error('[RabbitMQClient] Close reason:', channel._closeReason || 'Unknown');
267
+ });
268
+
256
269
  transport.queueChannel = channel;
257
270
  console.log(`[QueueManager] DEBUG: Channel recreated (was null) before ${queueInfo.name}`);
258
271
  } else {
259
- // Test if channel is usable by trying a no-op (checkQueue on a known queue)
260
- // This will throw if channel is closed
272
+ // Check if channel is closed - amqplib may not set closed property immediately
273
+ // We check by trying to access channel properties - if it throws, channel is closed
261
274
  let channelUsable = false;
262
275
  try {
263
- // Try to check a queue that definitely exists (workflow.init) - this tests channel usability
264
- // If channel is closed, this will throw immediately
265
- await channel.checkQueue('workflow.init');
276
+ // Check if channel has closed property set to true
277
+ if (channel.closed === true) {
278
+ console.warn(`[QueueManager] DEBUG: Channel.closed === true before ${queueInfo.name}, recreating...`);
279
+ throw new Error('Channel closed property is true');
280
+ }
281
+
282
+ // Channel exists and closed is not true - assume it's usable
283
+ // We don't test with checkQueue() because that can also close channel on 404
266
284
  channelUsable = true;
267
- console.log(`[QueueManager] DEBUG: Channel is usable before ${queueInfo.name}`);
285
+ console.log(`[QueueManager] DEBUG: Channel appears usable before ${queueInfo.name} (closed=${channel.closed})`);
268
286
  } catch (testErr) {
269
287
  // Channel is closed or unusable - recreate it
270
- console.warn(`[QueueManager] DEBUG: Channel test failed before ${queueInfo.name} (${testErr.message}), recreating...`);
288
+ console.warn(`[QueueManager] DEBUG: Channel check failed before ${queueInfo.name} (${testErr.message}), recreating...`);
289
+ console.warn(`[QueueManager] DEBUG: Channel close reason: ${channel._closeReason || 'N/A'}`);
290
+ console.warn(`[QueueManager] DEBUG: Channel last operation: ${channel._lastOperation || 'N/A'}`);
291
+
271
292
  try {
272
- channel = await transport._connection.createChannel();
293
+ const newChannel = await transport._connection.createChannel();
294
+ newChannel._createdAt = new Date().toISOString();
295
+ newChannel._closeReason = null;
296
+ newChannel._lastOperation = `Recreated (check failed) before ${queueInfo.name}`;
297
+
298
+ newChannel.on('error', (err) => {
299
+ console.error('[RabbitMQClient] Recreated queue channel error:', err.message);
300
+ newChannel._closeReason = `Error: ${err.message} (code: ${err.code})`;
301
+ });
302
+ newChannel.on('close', () => {
303
+ console.error('[RabbitMQClient] Recreated queue channel closed');
304
+ console.error('[RabbitMQClient] Close reason:', newChannel._closeReason || 'Unknown');
305
+ });
306
+
307
+ channel = newChannel;
273
308
  transport.queueChannel = channel;
274
- console.log(`[QueueManager] DEBUG: Channel recreated (test failed) before ${queueInfo.name}`);
309
+ console.log(`[QueueManager] DEBUG: Channel recreated (check failed) before ${queueInfo.name}`);
275
310
  channelUsable = true;
276
311
  } catch (recreateErr) {
277
312
  throw new Error(`Failed to recreate channel before ${queueInfo.name}: ${recreateErr.message}`);
@@ -330,63 +365,94 @@ class QueueManager {
330
365
  }
331
366
  }
332
367
 
333
- // CRITICAL: Use assertQueue directly - if queue exists with different args (406), that's OK
334
- // BUT: 406 PRECONDITION-FAILED closes the channel in amqplib!
335
- // Solution: Catch 406, recreate channel immediately, and continue
368
+ // CRITICAL: Use the SAME pattern as ensureQueue() - checkQueue() FIRST, then assertQueue() only if 404
369
+ // This prevents channel closure on 406 PRECONDITION-FAILED
370
+ // Pattern: checkQueue() -> if 404, assertQueue() -> if 406, use existing queue (channel stays open)
336
371
  let queueCreated = false;
372
+
373
+ // Track operation on channel for debugging
374
+ if (channel && channel._lastOperation !== undefined) {
375
+ channel._lastOperation = `About to checkQueue ${queueName}`;
376
+ }
377
+
337
378
  try {
338
- console.log(`[QueueManager] DEBUG: About to assertQueue ${queueName} with options:`, JSON.stringify(queueOptions, null, 2));
379
+ console.log(`[QueueManager] DEBUG: About to checkQueue ${queueName} (same pattern as ensureQueue)`);
380
+ console.log(`[QueueManager] DEBUG: Channel state: exists=${!!channel}, closed=${channel ? (channel.closed === true ? 'true' : channel.closed === false ? 'false' : 'undefined') : 'N/A'}`);
339
381
 
340
- await channel.assertQueue(queueName, queueOptions);
382
+ // CRITICAL: Use checkQueue() FIRST (same as ensureQueue() pattern)
383
+ // This prevents channel closure on 406 - if queue exists with different args, checkQueue() returns 406 but channel stays open
384
+ await channel.checkQueue(queueName);
341
385
 
342
- console.log(`[QueueManager] DEBUG: assertQueue succeeded for ${queueName}`);
386
+ // Queue exists - use it
387
+ console.log(`[QueueManager] DEBUG: checkQueue succeeded - queue ${queueName} exists`);
388
+ if (channel && channel._lastOperation !== undefined) {
389
+ channel._lastOperation = `checkQueue ${queueName} succeeded`;
390
+ }
343
391
  queues[queueInfo.type] = queueName;
344
392
  queueCreated = true;
345
- console.info(`[QueueManager] ✓ Created business queue: ${queueName}`);
346
- } catch (assertErr) {
347
- console.error(`[QueueManager] DEBUG: assertQueue failed for ${queueName}:`, assertErr.message);
348
- console.error(`[QueueManager] DEBUG: Error code: ${assertErr.code}`);
393
+ console.info(`[QueueManager] ✓ Business queue exists: ${queueName}`);
394
+ } catch (checkErr) {
395
+ console.log(`[QueueManager] DEBUG: checkQueue failed for ${queueName}:`, checkErr.message);
396
+ console.log(`[QueueManager] DEBUG: Error code: ${checkErr.code}`);
397
+
398
+ // Check if channel is still open
399
+ if (!channel || channel.closed) {
400
+ throw new Error('Channel closed during checkQueue - cannot verify queue');
401
+ }
349
402
 
350
- // If 406 PRECONDITION-FAILED, queue exists with different args - channel is closed, recreate it IMMEDIATELY
351
- // CRITICAL: amqplib closes channel on 406, so we MUST recreate it to continue
352
- if (assertErr.code === 406) {
353
- console.warn(`[QueueManager] Queue ${queueName} exists with different arguments (406), channel will be closed. Recreating channel...`);
403
+ // If queue doesn't exist (404), create it using assertQueue
404
+ if (checkErr.code === 404) {
405
+ console.log(`[QueueManager] DEBUG: Queue ${queueName} doesn't exist (404), creating with assertQueue...`);
354
406
 
355
- // Recreate channel IMMEDIATELY - 406 closes it synchronously
356
- try {
357
- // Wait a tiny bit for channel close event to propagate
358
- await new Promise(resolve => setImmediate(resolve));
359
- channel = await transport._connection.createChannel();
360
- transport.queueChannel = channel;
361
- console.log(`[QueueManager] DEBUG: Channel recreated after 406 for ${queueName}`);
362
- } catch (recreateErr) {
363
- throw new Error(`Failed to recreate channel after 406 for ${queueName}: ${recreateErr.message}`);
407
+ if (channel && channel._lastOperation !== undefined) {
408
+ channel._lastOperation = `About to assertQueue ${queueName} (queue doesn't exist)`;
364
409
  }
365
410
 
366
- // Queue exists (just with different args) - accept it
367
- queues[queueInfo.type] = queueName;
368
- queueCreated = true;
369
- } else {
370
- // Other error - check if it's a channel closed error
371
- if (assertErr.message && assertErr.message.includes('Channel closed')) {
372
- console.error(`[QueueManager] DEBUG: Channel was closed during assertQueue for ${queueName}, recreating...`);
373
- // Try to recreate channel
374
- try {
375
- await new Promise(resolve => setImmediate(resolve));
376
- channel = await transport._connection.createChannel();
377
- transport.queueChannel = channel;
378
- console.log(`[QueueManager] DEBUG: Channel recreated after channel closed error for ${queueName}`);
379
- } catch (recreateErr) {
380
- throw new Error(`Channel closed during assertQueue for ${queueName} and failed to recreate: ${recreateErr.message}`);
411
+ try {
412
+ await channel.assertQueue(queueName, queueOptions);
413
+ console.log(`[QueueManager] DEBUG: assertQueue succeeded for ${queueName}`);
414
+ if (channel && channel._lastOperation !== undefined) {
415
+ channel._lastOperation = `assertQueue ${queueName} succeeded`;
416
+ }
417
+ queues[queueInfo.type] = queueName;
418
+ queueCreated = true;
419
+ console.info(`[QueueManager] ✓ Created business queue: ${queueName}`);
420
+ } catch (assertErr) {
421
+ // If channel closed during assertQueue, it's a real error
422
+ if (!channel || channel.closed) {
423
+ throw new Error(`Channel closed during assertQueue for ${queueName} - check channel management`);
381
424
  }
382
- // Retry assertQueue with new channel (but only if it's not 406)
383
- if (assertErr.code !== 406) {
384
- throw new Error(`Failed to create business queue ${queueName}: ${assertErr.message}`);
425
+
426
+ // If 406 PRECONDITION-FAILED, queue exists with different args (race condition?)
427
+ if (assertErr.code === 406) {
428
+ console.warn(`[QueueManager] Queue ${queueName} exists with different arguments (406 from assertQueue):`, assertErr.message);
429
+ // Channel should still be open - try checkQueue to verify
430
+ if (!channel || channel.closed) {
431
+ throw new Error('Channel closed - cannot check existing queue');
432
+ }
433
+ try {
434
+ await channel.checkQueue(queueName);
435
+ queues[queueInfo.type] = queueName;
436
+ queueCreated = true;
437
+ console.info(`[QueueManager] ✓ Business queue exists (verified): ${queueName}`);
438
+ } catch (verifyErr) {
439
+ throw new Error(`Failed to verify existing queue ${queueName}: ${verifyErr.message}`);
440
+ }
441
+ } else {
442
+ // Other errors - rethrow
443
+ throw assertErr;
385
444
  }
386
- } else {
387
- // Other error - critical, cannot continue
388
- throw new Error(`Failed to create business queue ${queueName}: ${assertErr.message}`);
389
445
  }
446
+ } else if (checkErr.code === 406) {
447
+ // Queue exists with different args (406 from checkQueue) - use it as-is
448
+ // CRITICAL: checkQueue() on 406 does NOT close channel (unlike assertQueue() on 406)
449
+ console.warn(`[QueueManager] Queue ${queueName} exists with different arguments (406 from checkQueue), using as-is:`, checkErr.message);
450
+ queues[queueInfo.type] = queueName;
451
+ queueCreated = true;
452
+ console.info(`[QueueManager] ✓ Business queue exists (different args): ${queueName}`);
453
+ } else {
454
+ // Other error - rethrow
455
+ throw checkErr;
390
456
  }
391
457
  }
392
458
 
@@ -78,13 +78,26 @@ class RabbitMQClient extends EventEmitter {
78
78
  // Create a separate regular channel for queue operations (assertQueue, checkQueue)
79
79
  // This avoids RPC reply queue issues with ConfirmChannel.assertQueue()
80
80
  this._queueChannel = await this._connection.createChannel();
81
+
82
+ // CRITICAL: Track channel state and close reason
83
+ this._queueChannel._createdAt = new Date().toISOString();
84
+ this._queueChannel._closeReason = null;
85
+ this._queueChannel._lastOperation = null;
86
+
81
87
  this._queueChannel.on('error', (err) => {
82
88
  // Log but don't emit - queue channel errors are less critical
83
89
  console.error('[RabbitMQClient] Queue channel error:', err.message);
90
+ console.error('[RabbitMQClient] Error code:', err.code);
91
+ console.error('[RabbitMQClient] Channel created at:', this._queueChannel._createdAt);
92
+ console.error('[RabbitMQClient] Last operation:', this._queueChannel._lastOperation);
84
93
  console.error('[RabbitMQClient] Stack trace:', new Error().stack);
94
+ this._queueChannel._closeReason = `Error: ${err.message} (code: ${err.code})`;
85
95
  });
86
96
  this._queueChannel.on('close', () => {
87
97
  console.error('[RabbitMQClient] Queue channel closed');
98
+ console.error('[RabbitMQClient] Channel created at:', this._queueChannel._createdAt);
99
+ console.error('[RabbitMQClient] Close reason:', this._queueChannel._closeReason || 'Unknown');
100
+ console.error('[RabbitMQClient] Last operation:', this._queueChannel._lastOperation);
88
101
  console.error('[RabbitMQClient] Stack trace at close:', new Error().stack);
89
102
  });
90
103
  } catch (err) {