@onlineapps/mq-client-core 1.0.32 → 1.0.34

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/mq-client-core",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -35,6 +35,13 @@ class RabbitMQClient extends EventEmitter {
35
35
  this._channel = null; // ConfirmChannel for publish operations
36
36
  this._queueChannel = null; // Regular channel for queue operations (assertQueue, checkQueue)
37
37
  this._consumerChannel = null; // Dedicated channel for consume operations
38
+
39
+ // Track active consumers for re-registration after channel recreation
40
+ this._activeConsumers = new Map(); // queue -> { handler, options, consumerTag }
41
+
42
+ // Auto-reconnect flags
43
+ this._reconnecting = false;
44
+ this._reconnectAttempts = 0;
38
45
  }
39
46
 
40
47
  /**
@@ -94,33 +101,57 @@ class RabbitMQClient extends EventEmitter {
94
101
 
95
102
  // Use ConfirmChannel to enable publisher confirms for publish operations
96
103
  this._channel = await this._connection.createConfirmChannel();
97
- this._channel.on('error', (err) => this.emit('error', err));
98
- this._channel.on('close', () => {
99
- // Emit a channel close error
100
- this.emit('error', new Error('RabbitMQ channel closed unexpectedly'));
104
+ // Enable publisher confirms explicitly
105
+ this._channel.on('error', async (err) => {
106
+ console.error('[RabbitMQClient] [mq-client-core] Publisher channel error:', err.message);
107
+ // Auto-recreate channel if connection is still alive
108
+ await this._ensurePublisherChannel();
109
+ this.emit('error', err);
110
+ });
111
+ this._channel.on('close', async () => {
112
+ console.warn('[RabbitMQClient] [mq-client-core] Publisher channel closed - will auto-recreate on next publish');
113
+ // Mark channel as null - will be recreated on next publish
114
+ this._channel = null;
115
+ // Try to recreate immediately if connection is alive
116
+ await this._ensurePublisherChannel();
117
+ });
118
+ // Set up publisher confirms callback
119
+ this._channel.on('drain', () => {
120
+ console.log('[RabbitMQClient] [mq-client-core] Publisher channel drained');
101
121
  });
102
122
 
103
123
  // Create a separate regular channel for queue operations (assertQueue, checkQueue)
104
124
  // This avoids RPC reply queue issues with ConfirmChannel.assertQueue()
105
125
  this._queueChannel = await this._connection.createChannel();
106
- this._queueChannel.on('error', (err) => {
126
+ this._queueChannel.on('error', async (err) => {
107
127
  // Log but don't emit - queue channel errors are less critical
108
128
  console.warn('[RabbitMQClient] Queue channel error:', err.message);
129
+ // Auto-recreate channel if connection is still alive
130
+ await this._ensureQueueChannel();
109
131
  });
110
- this._queueChannel.on('close', () => {
111
- console.warn('[RabbitMQClient] Queue channel closed');
132
+ this._queueChannel.on('close', async () => {
133
+ console.warn('[RabbitMQClient] Queue channel closed - will auto-recreate on next operation');
134
+ // Mark channel as null - will be recreated on next queue operation
135
+ this._queueChannel = null;
136
+ // Try to recreate immediately if connection is alive
137
+ await this._ensureQueueChannel();
112
138
  });
113
139
 
114
140
  // Create a dedicated channel for consume operations
115
141
  // This prevents channel conflicts between publish (ConfirmChannel) and consume operations
116
142
  this._consumerChannel = await this._connection.createChannel();
117
- this._consumerChannel.on('error', (err) => {
143
+ this._consumerChannel.on('error', async (err) => {
118
144
  console.warn('[RabbitMQClient] Consumer channel error:', err.message);
145
+ // Auto-recreate channel and re-register consumers
146
+ await this._ensureConsumerChannel();
119
147
  this.emit('error', err);
120
148
  });
121
- this._consumerChannel.on('close', () => {
122
- console.warn('[RabbitMQClient] Consumer channel closed');
123
- this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
149
+ this._consumerChannel.on('close', async () => {
150
+ console.warn('[RabbitMQClient] Consumer channel closed - will auto-recreate and re-register consumers');
151
+ // Mark channel as null - will be recreated and consumers re-registered
152
+ this._consumerChannel = null;
153
+ // Try to recreate and re-register consumers immediately if connection is alive
154
+ await this._ensureConsumerChannel();
124
155
  });
125
156
  } catch (err) {
126
157
  // Cleanup partially created resources
@@ -136,11 +167,192 @@ class RabbitMQClient extends EventEmitter {
136
167
  }
137
168
  }
138
169
 
170
+ /**
171
+ * Ensure publisher channel exists and is open
172
+ * Auto-recreates if closed
173
+ * @private
174
+ * @returns {Promise<void>}
175
+ */
176
+ async _ensurePublisherChannel() {
177
+ if (this._channel && !this._channel.closed) {
178
+ return; // Channel is good
179
+ }
180
+
181
+ if (!this._connection || this._connection.closed) {
182
+ throw new Error('Cannot recreate publisher channel: connection is closed');
183
+ }
184
+
185
+ try {
186
+ // Close old channel if exists
187
+ if (this._channel) {
188
+ try {
189
+ await this._channel.close();
190
+ } catch (_) {
191
+ // Ignore errors when closing already-closed channel
192
+ }
193
+ }
194
+
195
+ // Create new ConfirmChannel
196
+ this._channel = await this._connection.createConfirmChannel();
197
+ this._channel.on('error', async (err) => {
198
+ console.error('[RabbitMQClient] [mq-client-core] Publisher channel error:', err.message);
199
+ await this._ensurePublisherChannel();
200
+ this.emit('error', err);
201
+ });
202
+ this._channel.on('close', async () => {
203
+ console.warn('[RabbitMQClient] [mq-client-core] Publisher channel closed - will auto-recreate on next publish');
204
+ this._channel = null;
205
+ await this._ensurePublisherChannel();
206
+ });
207
+ this._channel.on('drain', () => {
208
+ console.log('[RabbitMQClient] [mq-client-core] Publisher channel drained');
209
+ });
210
+
211
+ console.log('[RabbitMQClient] [mq-client-core] ✓ Publisher channel recreated');
212
+ } catch (err) {
213
+ this._channel = null;
214
+ throw new Error(`Failed to recreate publisher channel: ${err.message}`);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Ensure queue channel exists and is open
220
+ * Auto-recreates if closed
221
+ * @private
222
+ * @returns {Promise<void>}
223
+ */
224
+ async _ensureQueueChannel() {
225
+ if (this._queueChannel && !this._queueChannel.closed) {
226
+ return; // Channel is good
227
+ }
228
+
229
+ if (!this._connection || this._connection.closed) {
230
+ throw new Error('Cannot recreate queue channel: connection is closed');
231
+ }
232
+
233
+ try {
234
+ // Close old channel if exists
235
+ if (this._queueChannel) {
236
+ try {
237
+ await this._queueChannel.close();
238
+ } catch (_) {
239
+ // Ignore errors when closing already-closed channel
240
+ }
241
+ }
242
+
243
+ // Create new regular channel
244
+ this._queueChannel = await this._connection.createChannel();
245
+ this._queueChannel.on('error', async (err) => {
246
+ console.warn('[RabbitMQClient] Queue channel error:', err.message);
247
+ await this._ensureQueueChannel();
248
+ });
249
+ this._queueChannel.on('close', async () => {
250
+ console.warn('[RabbitMQClient] Queue channel closed - will auto-recreate on next operation');
251
+ this._queueChannel = null;
252
+ await this._ensureQueueChannel();
253
+ });
254
+
255
+ console.log('[RabbitMQClient] [mq-client-core] ✓ Queue channel recreated');
256
+ } catch (err) {
257
+ this._queueChannel = null;
258
+ throw new Error(`Failed to recreate queue channel: ${err.message}`);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Ensure consumer channel exists and is open
264
+ * Auto-recreates and re-registers all consumers if closed
265
+ * @private
266
+ * @returns {Promise<void>}
267
+ */
268
+ async _ensureConsumerChannel() {
269
+ if (this._consumerChannel && !this._consumerChannel.closed) {
270
+ return; // Channel is good
271
+ }
272
+
273
+ if (!this._connection || this._connection.closed) {
274
+ throw new Error('Cannot recreate consumer channel: connection is closed');
275
+ }
276
+
277
+ try {
278
+ // Close old channel if exists
279
+ if (this._consumerChannel) {
280
+ try {
281
+ await this._consumerChannel.close();
282
+ } catch (_) {
283
+ // Ignore errors when closing already-closed channel
284
+ }
285
+ }
286
+
287
+ // Create new regular channel
288
+ this._consumerChannel = await this._connection.createChannel();
289
+ this._consumerChannel.on('error', async (err) => {
290
+ console.warn('[RabbitMQClient] Consumer channel error:', err.message);
291
+ await this._ensureConsumerChannel();
292
+ this.emit('error', err);
293
+ });
294
+ this._consumerChannel.on('close', async () => {
295
+ console.warn('[RabbitMQClient] Consumer channel closed - will auto-recreate and re-register consumers');
296
+ this._consumerChannel = null;
297
+ await this._ensureConsumerChannel();
298
+ });
299
+
300
+ // Re-register all active consumers
301
+ if (this._activeConsumers.size > 0) {
302
+ console.log(`[RabbitMQClient] [mq-client-core] Re-registering ${this._activeConsumers.size} consumers...`);
303
+ for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
304
+ try {
305
+ // Re-assert queue with correct options before consuming (may have been deleted)
306
+ if (consumerInfo.options.queueOptions) {
307
+ try {
308
+ await this._ensureQueueChannel();
309
+ await this._queueChannel.assertQueue(queue, consumerInfo.options.queueOptions);
310
+ } catch (assertErr) {
311
+ if (assertErr.code === 404) {
312
+ console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} does not exist, skipping consumer re-registration`);
313
+ this._activeConsumers.delete(queue);
314
+ continue;
315
+ }
316
+ // 406 error means queue exists with different args - skip this consumer
317
+ if (assertErr.code === 406) {
318
+ console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} exists with different args, skipping consumer re-registration`);
319
+ this._activeConsumers.delete(queue);
320
+ continue;
321
+ }
322
+ throw assertErr;
323
+ }
324
+ }
325
+
326
+ const consumeResult = await this._consumerChannel.consume(
327
+ queue,
328
+ consumerInfo.handler,
329
+ { noAck: consumerInfo.options.noAck }
330
+ );
331
+ consumerInfo.consumerTag = consumeResult.consumerTag;
332
+ console.log(`[RabbitMQClient] [mq-client-core] ✓ Re-registered consumer for queue: ${queue} (consumerTag: ${consumeResult.consumerTag})`);
333
+ } catch (err) {
334
+ console.error(`[RabbitMQClient] [mq-client-core] Failed to re-register consumer for queue ${queue}:`, err.message);
335
+ // Remove failed consumer from tracking
336
+ this._activeConsumers.delete(queue);
337
+ }
338
+ }
339
+ }
340
+
341
+ console.log('[RabbitMQClient] [mq-client-core] ✓ Consumer channel recreated');
342
+ } catch (err) {
343
+ this._consumerChannel = null;
344
+ throw new Error(`Failed to recreate consumer channel: ${err.message}`);
345
+ }
346
+ }
347
+
139
348
  /**
140
349
  * Disconnects: closes channel and connection.
141
350
  * @returns {Promise<void>}
142
351
  */
143
352
  async disconnect() {
353
+ // Clear active consumers tracking
354
+ this._activeConsumers.clear();
355
+
144
356
  try {
145
357
  if (this._consumerChannel) {
146
358
  await this._consumerChannel.close();
@@ -185,10 +397,11 @@ class RabbitMQClient extends EventEmitter {
185
397
  * @throws {Error} If publish fails or channel is not available.
186
398
  */
187
399
  async publish(queue, buffer, options = {}) {
188
- // Check channel state before publish
189
- if (!this._channel || this._channel.closed) {
190
- throw new Error('Cannot publish: channel is not initialized or closed');
191
- }
400
+ // Ensure publisher channel exists and is open (auto-recreates if closed)
401
+ await this._ensurePublisherChannel();
402
+
403
+ // Log publish attempt for debugging
404
+ console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Attempting to publish to queue "${queue}"`);
192
405
 
193
406
  const exchange = this._config.exchange || '';
194
407
  const routingKey = options.routingKey || queue;
@@ -203,21 +416,8 @@ class RabbitMQClient extends EventEmitter {
203
416
  // If queue doesn't exist (404), we should NOT auto-create it - infrastructure queues must be created explicitly
204
417
  // This prevents creating queues with wrong arguments (no TTL) which causes 406 errors later
205
418
  try {
206
- // Check queueChannel state
207
- if (!this._queueChannel || this._queueChannel.closed) {
208
- // Recreate queueChannel if closed
209
- if (this._connection && !this._connection.closed) {
210
- this._queueChannel = await this._connection.createChannel();
211
- this._queueChannel.on('error', (err) => {
212
- console.warn('[RabbitMQClient] Queue channel error:', err.message);
213
- });
214
- this._queueChannel.on('close', () => {
215
- console.warn('[RabbitMQClient] Queue channel closed');
216
- });
217
- } else {
218
- throw new Error('Cannot publish: connection is closed');
219
- }
220
- }
419
+ // Ensure queue channel exists and is open (auto-recreates if closed)
420
+ await this._ensureQueueChannel();
221
421
  await this._queueChannel.checkQueue(queue);
222
422
  // Queue exists - proceed to publish
223
423
  } catch (checkErr) {
@@ -252,30 +452,56 @@ class RabbitMQClient extends EventEmitter {
252
452
  }
253
453
  }
254
454
  // Publish to queue using ConfirmChannel (for publisher confirms)
255
- // Check channel state again before sendToQueue
256
- if (this._channel.closed) {
257
- throw new Error('Cannot publish: channel closed during operation');
258
- }
259
- this._channel.sendToQueue(queue, buffer, { persistent, headers });
455
+ // Channel is guaranteed to be open (ensured above)
456
+ console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Sending message to queue "${queue}" (size: ${buffer.length} bytes)`);
457
+
458
+ // Use callback-based confirmation - kanály jsou spolehlivé, takže callback vždy dorazí
459
+ const confirmPromise = new Promise((resolve, reject) => {
460
+ // Send message with callback
461
+ this._channel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
462
+ if (err) {
463
+ console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Send callback error for queue "${queue}":`, err.message);
464
+ reject(err);
465
+ } else {
466
+ console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] ✓ Message confirmed for queue "${queue}"`);
467
+ resolve();
468
+ }
469
+ });
470
+ });
471
+
472
+ await confirmPromise;
260
473
  } else {
261
474
  // If exchange is specified, assert exchange and publish to it
262
- if (this._channel.closed) {
263
- throw new Error('Cannot publish: channel closed during operation');
264
- }
475
+ // Channel is guaranteed to be open (ensured above)
265
476
  await this._channel.assertExchange(exchange, 'direct', { durable: this._config.durable });
266
- this._channel.publish(exchange, routingKey, buffer, { persistent, headers });
477
+
478
+ // Use callback-based confirmation - kanály jsou spolehlivé
479
+ const confirmPromise = new Promise((resolve, reject) => {
480
+ this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
481
+ if (err) {
482
+ console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Exchange publish callback error:`, err.message);
483
+ reject(err);
484
+ } else {
485
+ console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] ✓ Exchange publish confirmed`);
486
+ resolve();
487
+ }
488
+ });
489
+ });
490
+
491
+ await confirmPromise;
267
492
  }
268
- // Wait for confirmation (with timeout to prevent hanging)
269
- const confirmPromise = this._channel.waitForConfirms();
270
- const timeoutPromise = new Promise((_, reject) => {
271
- setTimeout(() => reject(new Error('Publisher confirm timeout after 5 seconds')), 5000);
272
- });
273
- await Promise.race([confirmPromise, timeoutPromise]);
274
493
  } catch (err) {
275
- // If channel was closed, mark it for recreation
494
+ // If channel was closed, try to recreate and retry once
276
495
  if (err.message && (err.message.includes('Channel closed') || err.message.includes('channel is closed') || this._channel?.closed)) {
277
- console.warn('[RabbitMQClient] [mq-client-core] [PUBLISH] Channel closed during publish, will need to reconnect');
278
- this._channel = null;
496
+ console.warn('[RabbitMQClient] [mq-client-core] [PUBLISH] Channel closed during publish, recreating and retrying...');
497
+ try {
498
+ await this._ensurePublisherChannel();
499
+ // Retry publish once
500
+ return await this.publish(queue, buffer, options);
501
+ } catch (retryErr) {
502
+ console.error('[RabbitMQClient] [mq-client-core] [PUBLISH] Retry failed:', retryErr.message);
503
+ throw retryErr;
504
+ }
279
505
  }
280
506
  this.emit('error', err);
281
507
  throw err;
@@ -291,41 +517,8 @@ class RabbitMQClient extends EventEmitter {
291
517
  * @throws {Error} If consume setup fails or channel is not available.
292
518
  */
293
519
  async consume(queue, onMessage, options = {}) {
294
- // Use dedicated consumer channel instead of ConfirmChannel
295
- // ConfirmChannel is optimized for publish operations, not consume
296
- if (!this._consumerChannel) {
297
- // Recreate consumer channel if closed
298
- if (this._connection && !this._connection.closed) {
299
- this._consumerChannel = await this._connection.createChannel();
300
- this._consumerChannel.on('error', (err) => {
301
- console.warn('[RabbitMQClient] Consumer channel error:', err.message);
302
- this.emit('error', err);
303
- });
304
- this._consumerChannel.on('close', () => {
305
- console.warn('[RabbitMQClient] Consumer channel closed');
306
- this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
307
- });
308
- } else {
309
- throw new Error('Cannot consume: consumer channel is not initialized and connection is closed');
310
- }
311
- }
312
-
313
- if (this._consumerChannel.closed) {
314
- // Recreate consumer channel if closed
315
- if (this._connection && !this._connection.closed) {
316
- this._consumerChannel = await this._connection.createChannel();
317
- this._consumerChannel.on('error', (err) => {
318
- console.warn('[RabbitMQClient] Consumer channel error:', err.message);
319
- this.emit('error', err);
320
- });
321
- this._consumerChannel.on('close', () => {
322
- console.warn('[RabbitMQClient] Consumer channel closed');
323
- this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
324
- });
325
- } else {
326
- throw new Error('Cannot consume: consumer channel is closed and connection is closed');
327
- }
328
- }
520
+ // Ensure consumer channel exists and is open (auto-recreates if closed and re-registers consumers)
521
+ await this._ensureConsumerChannel();
329
522
 
330
523
  const durable = options.durable !== undefined ? options.durable : this._config.durable;
331
524
  const prefetch = options.prefetch !== undefined ? options.prefetch : this._config.prefetch;
@@ -435,25 +628,36 @@ class RabbitMQClient extends EventEmitter {
435
628
 
436
629
  // Use dedicated consumer channel for consume operations
437
630
  // This prevents conflicts with ConfirmChannel used for publish operations
631
+ const messageHandler = async (msg) => {
632
+ if (msg === null) {
633
+ return;
634
+ }
635
+ try {
636
+ await onMessage(msg);
637
+ if (!noAck) {
638
+ this._consumerChannel.ack(msg);
639
+ }
640
+ } catch (handlerErr) {
641
+ // Negative acknowledge and requeue by default
642
+ this._consumerChannel.nack(msg, false, true);
643
+ }
644
+ };
645
+
438
646
  const consumeResult = await this._consumerChannel.consume(
439
647
  queue,
440
- async (msg) => {
441
- if (msg === null) {
442
- return;
443
- }
444
- try {
445
- await onMessage(msg);
446
- if (!noAck) {
447
- this._consumerChannel.ack(msg);
448
- }
449
- } catch (handlerErr) {
450
- // Negative acknowledge and requeue by default
451
- this._consumerChannel.nack(msg, false, true);
452
- }
453
- },
648
+ messageHandler,
454
649
  { noAck }
455
650
  );
456
651
 
652
+ // Track consumer for auto re-registration if channel closes
653
+ this._activeConsumers.set(queue, {
654
+ handler: messageHandler,
655
+ options: { noAck, prefetch, durable, queueOptions },
656
+ consumerTag: consumeResult.consumerTag
657
+ });
658
+
659
+ console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Consumer registered for queue "${queue}" (consumerTag: ${consumeResult.consumerTag})`);
660
+
457
661
  // Return consumer tag for cancellation
458
662
  return consumeResult.consumerTag;
459
663
  } catch (err) {