@onlineapps/mq-client-core 1.0.80 → 1.0.82

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.80",
3
+ "version": "1.0.82",
4
4
  "description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -249,6 +249,7 @@ module.exports = {
249
249
  monitoring: {
250
250
  /**
251
251
  * monitoring.workflow - Unified workflow lifecycle monitoring
252
+ * Bound to monitoring.workflow.fanout exchange (fanout).
252
253
  * Delivery Dispatcher publishes 'completed'/'failed' events after processing workflow.completed/workflow.failed
253
254
  * Business services publish 'progress' events during workflow step processing
254
255
  * Message format includes event_type: 'completed' | 'failed' | 'progress'
@@ -277,6 +278,73 @@ module.exports = {
277
278
  'x-message-ttl': 600000, // 10 minutes TTL (longer for service tracking)
278
279
  'x-max-length': 20000
279
280
  }
281
+ },
282
+
283
+ /**
284
+ * monitoring.workflow.fanout - Fanout exchange for workflow events
285
+ * Publishers send to this exchange; monitoring.workflow + delivery.workflow.events both receive copies.
286
+ */
287
+ 'workflow.fanout': {
288
+ type: 'exchange',
289
+ exchangeType: 'fanout',
290
+ durable: true
291
+ }
292
+ },
293
+
294
+ /**
295
+ * Delivery endpoint event queue configurations
296
+ * These are queues consumed by api_delivery_endpoint for real-time WS push.
297
+ */
298
+ deliveryEvents: {
299
+ /**
300
+ * delivery.workflow.events - Workflow progress events for WS clients
301
+ * Bound to monitoring.workflow.fanout exchange.
302
+ */
303
+ 'workflow.events': {
304
+ durable: true,
305
+ arguments: {
306
+ 'x-message-ttl': 60000, // 60s TTL - real-time events, stale ones are useless
307
+ 'x-max-length': 10000
308
+ }
309
+ },
310
+
311
+ /**
312
+ * delivery.health.events - Infrastructure health events for WS clients
313
+ * Bound to infrastructure.health.events exchange.
314
+ */
315
+ 'health.events': {
316
+ durable: true,
317
+ arguments: {
318
+ 'x-message-ttl': 60000,
319
+ 'x-max-length': 5000
320
+ }
321
+ },
322
+
323
+ /**
324
+ * delivery.alert.events - Alert events for WS clients
325
+ * Bound to alert.events.fanout exchange.
326
+ */
327
+ 'alert.events': {
328
+ durable: true,
329
+ arguments: {
330
+ 'x-message-ttl': 300000, // 5 minutes - alerts are more important
331
+ 'x-max-length': 5000
332
+ }
333
+ }
334
+ },
335
+
336
+ /**
337
+ * Alert exchange configuration
338
+ */
339
+ alerts: {
340
+ /**
341
+ * alert.events.fanout - Fanout exchange for alert events
342
+ * Monitoring watchdogs publish alerts here; delivery.alert.events consumes copies.
343
+ */
344
+ 'events.fanout': {
345
+ type: 'exchange',
346
+ exchangeType: 'fanout',
347
+ durable: true
280
348
  }
281
349
  },
282
350
 
@@ -103,7 +103,11 @@ async function publishToMonitoringResilient(mqClient, queueName, message, logger
103
103
  }
104
104
 
105
105
  try {
106
- await mqClient.publish(queueName, message);
106
+ const WORKFLOW_FANOUT = 'monitoring.workflow.fanout';
107
+ const publishOptions = queueName === 'monitoring.workflow'
108
+ ? { exchange: WORKFLOW_FANOUT, exchangeType: 'fanout', routingKey: '' }
109
+ : {};
110
+ await mqClient.publish(queueName, message, publishOptions);
107
111
 
108
112
  if (logger && logger.debug) {
109
113
  logger.debug(`Published to ${queueName}`, {
@@ -1258,7 +1258,8 @@ class RabbitMQClient extends EventEmitter {
1258
1258
  // Track operation for debugging
1259
1259
  this._trackChannelOperation(this._channel, `publish to ${queue}`);
1260
1260
 
1261
- const exchange = this._config.exchange || '';
1261
+ const exchange = options.exchange || this._config.exchange || '';
1262
+ const exchangeType = options.exchangeType || 'direct';
1262
1263
  const routingKey = options.routingKey || queue;
1263
1264
  const persistent = options.persistent !== undefined ? options.persistent : this._config.durable;
1264
1265
  const headers = options.headers || {};
@@ -1473,7 +1474,7 @@ class RabbitMQClient extends EventEmitter {
1473
1474
  } else {
1474
1475
  // If exchange is specified, assert exchange and publish to it
1475
1476
  // Channel is guaranteed to be open (ensured above)
1476
- await this._channel.assertExchange(exchange, 'direct', { durable: this._config.durable });
1477
+ await this._channel.assertExchange(exchange, exchangeType, { durable: this._config.durable });
1477
1478
 
1478
1479
  // Use callback-based confirmation - kanály jsou spolehlivé
1479
1480
  const confirmPromise = new Promise((resolve, reject) => {
@@ -1715,25 +1716,23 @@ class RabbitMQClient extends EventEmitter {
1715
1716
  try {
1716
1717
  await onMessage(msg);
1717
1718
  // Acknowledge message after successful processing
1718
- if (!noAck) {
1719
- // Only ack on the original channel instance; if it's gone/closed, do not attempt ack.
1719
+ if (!noAck && !msg._mqProcessed) {
1720
1720
  if (!channelForMsg || channelForMsg.closed) {
1721
1721
  console.warn('[RabbitMQClient] [mq-client-core] [CONSUMER] Cannot ack - consumer channel is closed/recreated (message will be requeued by broker)');
1722
1722
  return;
1723
1723
  }
1724
1724
  try {
1725
1725
  channelForMsg.ack(msg);
1726
+ msg._mqProcessed = true;
1726
1727
  } catch (ackErr) {
1727
- // If the channel was closed/recreated during processing, delivery tag becomes invalid.
1728
- // Do NOT send further acks that would close the channel with 406.
1729
1728
  const m = ackErr && ackErr.message ? ackErr.message : '';
1730
1729
  if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
1730
+ msg._mqProcessed = true;
1731
1731
  console.warn('[RabbitMQClient] [mq-client-core] [CONSUMER] Ack failed due to invalid delivery tag / closed channel (message delivery will be handled by broker)', { error: m });
1732
1732
  return;
1733
1733
  }
1734
1734
  throw ackErr;
1735
1735
  }
1736
- // Structured log: message acknowledged
1737
1736
  this._log.output(wfId, 'MSG_ACKED', {
1738
1737
  handler: 'consume',
1739
1738
  function: 'RabbitMQClient.consume',
@@ -1741,7 +1740,6 @@ class RabbitMQClient extends EventEmitter {
1741
1740
  output: { status: 'acknowledged', queue }
1742
1741
  });
1743
1742
 
1744
- // Track ack (decrement in-flight)
1745
1743
  if (tracking) {
1746
1744
  tracking.inFlight = Math.max(0, tracking.inFlight - 1);
1747
1745
  tracking.lastCheck = Date.now();
@@ -1749,16 +1747,16 @@ class RabbitMQClient extends EventEmitter {
1749
1747
  }
1750
1748
  } catch (handlerErr) {
1751
1749
  // Negative acknowledge and requeue by default
1752
- // Check if channel is still valid before nacking
1753
- if (channelForMsg && !channelForMsg.closed) {
1750
+ // Skip if already acked/nacked by the handler (prevents double ack → 406)
1751
+ if (!msg._mqProcessed && channelForMsg && !channelForMsg.closed) {
1754
1752
  try {
1755
1753
  channelForMsg.nack(msg, false, true);
1754
+ msg._mqProcessed = true;
1756
1755
  } catch (nackErr) {
1757
- // Channel may have closed during nack - ignore
1756
+ msg._mqProcessed = true;
1758
1757
  console.warn(`[RabbitMQClient] [mq-client-core] Failed to nack message (channel may be closed): ${nackErr.message}`);
1759
1758
  }
1760
1759
  }
1761
- // Track nack (decrement in-flight)
1762
1760
  if (tracking) {
1763
1761
  tracking.inFlight = Math.max(0, tracking.inFlight - 1);
1764
1762
  tracking.lastCheck = Date.now();
@@ -2218,14 +2216,19 @@ class RabbitMQClient extends EventEmitter {
2218
2216
  * @param {Object} msg - RabbitMQ message object.
2219
2217
  */
2220
2218
  async ack(msg) {
2219
+ if (msg._mqProcessed) {
2220
+ return; // Already acked/nacked — idempotent
2221
+ }
2221
2222
  if (!this._consumerChannel) {
2222
2223
  throw new Error('Cannot ack: consumer channel is not initialized');
2223
2224
  }
2224
2225
  try {
2225
2226
  this._consumerChannel.ack(msg);
2227
+ msg._mqProcessed = true;
2226
2228
  } catch (err) {
2227
2229
  const m = err && err.message ? err.message : '';
2228
2230
  if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
2231
+ msg._mqProcessed = true;
2229
2232
  console.warn('[RabbitMQClient] [mq-client-core] Cannot ack - consumer channel is closed/recreated (delivery tag invalid)', { error: m });
2230
2233
  return;
2231
2234
  }
@@ -2240,15 +2243,20 @@ class RabbitMQClient extends EventEmitter {
2240
2243
  * @param {Object} [options] - { requeue: boolean }.
2241
2244
  */
2242
2245
  async nack(msg, options = {}) {
2246
+ if (msg._mqProcessed) {
2247
+ return; // Already acked/nacked — idempotent
2248
+ }
2243
2249
  if (!this._consumerChannel) {
2244
2250
  throw new Error('Cannot nack: consumer channel is not initialized');
2245
2251
  }
2246
2252
  const requeue = options.requeue !== undefined ? options.requeue : true;
2247
2253
  try {
2248
2254
  this._consumerChannel.nack(msg, false, requeue);
2255
+ msg._mqProcessed = true;
2249
2256
  } catch (err) {
2250
2257
  const m = err && err.message ? err.message : '';
2251
2258
  if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
2259
+ msg._mqProcessed = true;
2252
2260
  console.warn('[RabbitMQClient] [mq-client-core] Cannot nack - consumer channel is closed/recreated (delivery tag invalid)', { error: m });
2253
2261
  return;
2254
2262
  }