@onlineapps/mq-client-core 2.0.1 → 3.0.0

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.
@@ -4,7 +4,11 @@
4
4
  * Provides resilient, fail-safe publishing to monitoring queues (monitoring.workflow, monitoring.services).
5
5
  *
6
6
  * Principles:
7
- * - Fail-safe: Never throws errors, always logs failures
7
+ * - Fail-safe about PUBLISHING: a broker that is down or a queue that does not
8
+ * exist yet returns `false` and is reported, never thrown. The single
9
+ * exception is a missing or incomplete `logger`, which is a programming error
10
+ * in the caller: without it the "always logs failures" half of this promise
11
+ * cannot hold, so the call refuses instead of dropping events in silence.
8
12
  * - Resilient: Handles queue unavailability gracefully (queue doesn't exist yet, RabbitMQ down)
9
13
  * - Unified: Single API for all monitoring publish operations across all services
10
14
  * - Encapsulated: All monitoring publish logic in one place, no duplication
@@ -15,13 +19,30 @@
15
19
  */
16
20
 
17
21
  const { PublishError, ConnectionError } = require('./utils/errorHandler');
22
+ const { assertLogger } = require('@onlineapps/logger-contract');
18
23
  const { QueueNotFoundError, classifyPublishError } = require('./utils/publishErrors');
19
24
 
20
25
  /**
21
26
  * Check if error indicates queue doesn't exist or RabbitMQ is unavailable
27
+ *
28
+ * The queue is named because the last question it asks —
29
+ * `classifyPublishError()` — builds a `QueueNotFoundError`, and that error carries
30
+ * a queue name it must be told (d.435); the caller has had it all along.
31
+ *
32
+ * @param {Error} error - The error a publish to `queueName` produced.
33
+ * @param {string} queueName - The queue that publish was aimed at.
22
34
  * @private
23
35
  */
24
- function isQueueUnavailableError(error) {
36
+ function isQueueUnavailableError(error, queueName) {
37
+ if (typeof queueName !== 'string' || queueName === '') {
38
+ throw new Error(
39
+ '[monitoring-publish] isQueueUnavailableError requires the queue name - '
40
+ + 'Expected: the caller names the queue whose publish failed, so the classification it asks '
41
+ + 'for carries a real name. Fix: pass it as the second argument, e.g. '
42
+ + 'isQueueUnavailableError(error, queueName).'
43
+ );
44
+ }
45
+
25
46
  if (!error) return false;
26
47
 
27
48
  const errorMessage = error.message || String(error);
@@ -50,7 +71,7 @@ function isQueueUnavailableError(error) {
50
71
  }
51
72
 
52
73
  // Classify publish errors
53
- const classified = classifyPublishError(error);
74
+ const classified = classifyPublishError(error, queueName);
54
75
  if (classified instanceof QueueNotFoundError) {
55
76
  return true;
56
77
  }
@@ -64,91 +85,97 @@ function isQueueUnavailableError(error) {
64
85
  * @param {Object} mqClient - BaseClient instance (must be connected)
65
86
  * @param {string} queueName - Queue name ('monitoring.workflow' or 'monitoring.services')
66
87
  * @param {Object} message - Message to publish (must include event_type)
67
- * @param {Object} [logger] - Logger instance (optional, for logging)
88
+ * @param {Object} logger - Logger with info/warn/error/debug (REQUIRED)
68
89
  * @param {Object} [context] - Additional context for logging (optional)
69
90
  * @returns {Promise<boolean>} - true if published successfully, false otherwise (never throws)
91
+ * @throws {Error} If the logger is missing or incomplete — the ONLY throw here.
70
92
  */
71
- async function publishToMonitoringResilient(mqClient, queueName, message, logger = null, context = {}) {
93
+ async function publishToMonitoringResilient(mqClient, queueName, message, logger, context = {}) {
94
+ // BREAKING (2026-09-07): the logger is required and validated up front. This
95
+ // function is fail-safe by design — every failure path below returns `false`
96
+ // and says why through the logger — so a missing logger turned it into a
97
+ // silent no-op: the caller got `false` and nobody ever learned that monitoring
98
+ // events were being dropped. Owner confirmation
99
+ // `docs/governance/confirmations/connector-logger-contract.md` 001/002.
100
+ assertLogger(
101
+ 'publishToMonitoringResilient',
102
+ logger,
103
+ 'every skipped or failed monitoring publish is reported instead of silently returning false',
104
+ 'pass the logger your service already built as the fourth argument'
105
+ );
106
+
72
107
  // Validate inputs
73
108
  if (!mqClient) {
74
- if (logger) {
75
- logger.warn('Monitoring publish skipped: mqClient not provided', context);
76
- }
109
+ logger.warn('Monitoring publish skipped: mqClient not provided', context);
77
110
  return false;
78
111
  }
79
112
 
80
113
  if (!mqClient.isConnected || typeof mqClient.isConnected !== 'function' || !mqClient.isConnected()) {
81
- if (logger) {
82
- logger.warn(`Monitoring publish skipped: mqClient not connected (queue: ${queueName})`, context);
83
- }
114
+ logger.warn(`Monitoring publish skipped: mqClient not connected (queue: ${queueName})`, context);
84
115
  return false;
85
116
  }
86
117
 
87
118
  if (!message || !message.event_type) {
88
- if (logger) {
89
- logger.warn(`Monitoring publish skipped: message missing event_type (queue: ${queueName})`, {
90
- ...context,
91
- hasMessage: !!message
92
- });
93
- }
119
+ logger.warn(`Monitoring publish skipped: message missing event_type (queue: ${queueName})`, {
120
+ ...context,
121
+ hasMessage: !!message
122
+ });
94
123
  return false;
95
124
  }
96
125
 
97
126
  // Validate queue name
98
127
  if (queueName !== 'monitoring.workflow' && queueName !== 'monitoring.services') {
99
- if (logger) {
100
- logger.warn(`Monitoring publish skipped: invalid queue name (expected monitoring.workflow or monitoring.services, got: ${queueName})`, context);
101
- }
128
+ logger.warn(`Monitoring publish skipped: invalid queue name (expected monitoring.workflow or monitoring.services, got: ${queueName})`, context);
102
129
  return false;
103
130
  }
104
131
 
105
132
  try {
106
133
  const WORKFLOW_FANOUT = 'monitoring.workflow.fanout';
134
+ // The exchange is NAMED, not declared: it is a fanout owned by the monitoring
135
+ // consumer, which asserts it from the declaration in `config/queueConfig.js`
136
+ // ('monitoring' → 'workflow.fanout'). `exchangeType` stood here for the
137
+ // per-publish `assertExchange()` the publish path made until d.342, and left
138
+ // with it — a key nothing reads is not kept lying around
139
+ // (`change-discipline.md` § Removing something removes its declaration).
107
140
  const publishOptions = queueName === 'monitoring.workflow'
108
- ? { exchange: WORKFLOW_FANOUT, exchangeType: 'fanout', routingKey: '' }
141
+ ? { exchange: WORKFLOW_FANOUT, routingKey: '' }
109
142
  : {};
110
143
  await mqClient.publish(queueName, message, publishOptions);
111
-
112
- if (logger && logger.debug) {
113
- logger.debug(`Published to ${queueName}`, {
114
- ...context,
115
- event_type: message.event_type,
116
- workflow_id: message.workflow_id,
117
- service_name: message.service_name
118
- });
119
- }
120
-
144
+
145
+ logger.debug(`Published to ${queueName}`, {
146
+ ...context,
147
+ event_type: message.event_type,
148
+ workflow_id: message.workflow_id,
149
+ service_name: message.service_name
150
+ });
151
+
121
152
  return true;
122
153
  } catch (error) {
123
154
  // Check if this is a queue unavailable error
124
- if (isQueueUnavailableError(error)) {
155
+ if (isQueueUnavailableError(error, queueName)) {
125
156
  // Queue doesn't exist yet or RabbitMQ is unavailable - log but don't fail
126
- if (logger && logger.warn) {
127
- logger.warn(`Monitoring queue ${queueName} not available (queue may not exist yet or RabbitMQ unavailable)`, {
128
- ...context,
129
- error: error.message,
130
- errorCode: error.code,
131
- event_type: message.event_type,
132
- workflow_id: message.workflow_id,
133
- service_name: message.service_name
134
- });
135
- }
136
- return false;
137
- }
138
-
139
- // Other errors - log as warning but don't fail
140
- if (logger && logger.warn) {
141
- logger.warn(`Failed to publish to monitoring queue ${queueName}`, {
157
+ logger.warn(`Monitoring queue ${queueName} not available (queue may not exist yet or RabbitMQ unavailable)`, {
142
158
  ...context,
143
159
  error: error.message,
144
160
  errorCode: error.code,
145
- errorName: error.name,
146
161
  event_type: message.event_type,
147
162
  workflow_id: message.workflow_id,
148
163
  service_name: message.service_name
149
164
  });
165
+ return false;
150
166
  }
151
-
167
+
168
+ // Other errors - log as warning but don't fail
169
+ logger.warn(`Failed to publish to monitoring queue ${queueName}`, {
170
+ ...context,
171
+ error: error.message,
172
+ errorCode: error.code,
173
+ errorName: error.name,
174
+ event_type: message.event_type,
175
+ workflow_id: message.workflow_id,
176
+ service_name: message.service_name
177
+ });
178
+
152
179
  return false;
153
180
  }
154
181
  }
@@ -158,7 +185,7 @@ async function publishToMonitoringResilient(mqClient, queueName, message, logger
158
185
  *
159
186
  * @param {Object} mqClient - BaseClient instance (must be connected)
160
187
  * @param {Object} message - Message with event_type ('completed' | 'failed' | 'progress'), workflow_id, etc.
161
- * @param {Object} [logger] - Logger instance (optional)
188
+ * @param {Object} logger - Logger with info/warn/error/debug (REQUIRED)
162
189
  * @param {Object} [context] - Additional context for logging (optional)
163
190
  * @returns {Promise<boolean>} - true if published successfully, false otherwise (never throws)
164
191
  *
@@ -172,7 +199,7 @@ async function publishToMonitoringResilient(mqClient, queueName, message, logger
172
199
  * timestamp: new Date().toISOString()
173
200
  * }, logger);
174
201
  */
175
- async function publishToMonitoringWorkflow(mqClient, message, logger = null, context = {}) {
202
+ async function publishToMonitoringWorkflow(mqClient, message, logger, context = {}) {
176
203
  return publishToMonitoringResilient(mqClient, 'monitoring.workflow', message, logger, context);
177
204
  }
178
205
 
@@ -181,7 +208,7 @@ async function publishToMonitoringWorkflow(mqClient, message, logger = null, con
181
208
  *
182
209
  * @param {Object} mqClient - BaseClient instance (must be connected)
183
210
  * @param {Object} message - Message with event_type ('service.registered' | 'service.validation.completed' | etc.), service_name, etc.
184
- * @param {Object} [logger] - Logger instance (optional)
211
+ * @param {Object} logger - Logger with info/warn/error/debug (REQUIRED)
185
212
  * @param {Object} [context] - Additional context for logging (optional)
186
213
  * @returns {Promise<boolean>} - true if published successfully, false otherwise (never throws)
187
214
  *
@@ -194,7 +221,7 @@ async function publishToMonitoringWorkflow(mqClient, message, logger = null, con
194
221
  * timestamp: new Date().toISOString()
195
222
  * }, logger);
196
223
  */
197
- async function publishToMonitoringServices(mqClient, message, logger = null, context = {}) {
224
+ async function publishToMonitoringServices(mqClient, message, logger, context = {}) {
198
225
  return publishToMonitoringResilient(mqClient, 'monitoring.services', message, logger, context);
199
226
  }
200
227