@onlineapps/conn-orch-registry 3.0.0 → 4.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.
@@ -5,10 +5,11 @@
5
5
  * Implements opt-in subscription to registry.changes exchange.
6
6
  * Maintains local service index and enables lazy loading of specs.
7
7
  *
8
- * @module @onlineapps/connector-registry-client/src/registryEventConsumer
8
+ * @module @onlineapps/conn-orch-registry/src/registryEventConsumer
9
9
  */
10
10
 
11
11
  const EventEmitter = require('events');
12
+ const { assertLogger } = require('@onlineapps/logger-contract');
12
13
 
13
14
  // The storage connector arrives by INJECTION. There used to be an implicit
14
15
  // default here — `require('@onlineapps/connector-storage')` inside a try/catch
@@ -27,12 +28,19 @@ class RegistryEventConsumer extends EventEmitter {
27
28
  * @param {Object} [opts.redis] - Optional Redis client for index persistence
28
29
  * @param {Object} [opts.storageConfig] - Configuration for StorageConnector
29
30
  * @param {Object} [opts.StorageConnector] - Injectable StorageConnector class for testing
31
+ * @param {Object} opts.logger - Logger with info/warn/error/debug; required, validated here
32
+ * (docs/governance/confirmations/connector-logger-contract.md 001)
30
33
  * @param {boolean} [opts.cacheEnabled=true] - Enable local spec caching
31
34
  * @param {number} [opts.maxCacheSize=50] - Maximum cached specs
32
35
  */
33
- constructor({ queueManager, serviceName, redis = null, storageConfig = {}, StorageConnector = null, cacheEnabled = true, maxCacheSize = 50 }) {
36
+ constructor({ queueManager, serviceName, redis = null, storageConfig = {}, StorageConnector = null, logger, cacheEnabled = true, maxCacheSize = 50 }) {
34
37
  super();
35
38
 
39
+ this.logger = assertLogger(
40
+ 'RegistryEventConsumer',
41
+ logger,
42
+ 'registry index changes are readable in the service log rather than on stdout'
43
+ );
36
44
  this.queueManager = queueManager;
37
45
  this.serviceName = serviceName;
38
46
  this.redis = redis;
@@ -92,7 +100,10 @@ class RegistryEventConsumer extends EventEmitter {
92
100
  await this.storage.initialize();
93
101
  this.emit('storageReady');
94
102
  } else {
95
- console.warn('Storage connector not configured - spec downloading disabled (index-only mode)');
103
+ this.logger.warn('[RegistryEventConsumer] Index-only mode - spec downloading disabled', {
104
+ serviceName: this.serviceName,
105
+ reason: 'no storage connector configured'
106
+ });
96
107
  }
97
108
  }
98
109
 
@@ -102,49 +113,44 @@ class RegistryEventConsumer extends EventEmitter {
102
113
  */
103
114
  async subscribeToChanges() {
104
115
  try {
105
- const channel = this.queueManager.channel;
106
-
107
116
  // Assert exchange exists (fanout type for broadcasting)
108
- await channel.assertExchange(this.exchangeName, 'fanout', {
117
+ await this.queueManager.assertExchange(this.exchangeName, 'fanout', {
109
118
  durable: true
110
119
  });
111
120
 
112
- // Create service-specific queue for registry events
113
- // Try to assert queue with arguments, fall back to simple assert if conflict
114
- try {
115
- await channel.assertQueue(this.eventsQueueName, {
116
- durable: true,
117
- arguments: {
118
- 'x-message-ttl': 60000, // Messages expire after 1 minute
119
- 'x-max-length': 1000 // Keep max 1000 messages
120
- }
121
- });
122
- } catch (error) {
123
- if (error.code === 406) {
124
- // Queue exists with different arguments, use it as-is
125
- await channel.assertQueue(this.eventsQueueName, {
126
- durable: true
127
- });
128
- } else {
129
- throw error;
130
- }
131
- }
121
+ // The events queue is a BUSINESS queue of this service, and its declaration —
122
+ // durable, 1 min TTL, 1000 messages, dead-letter route to `<service>.dlq` —
123
+ // belongs to `queueConfig` in the core (`business['registry.events']`), not to
124
+ // an options object written here. The 406 branch this replaces was a fallback
125
+ // in the literal sense architecture-principles §3 forbids: it answered "the
126
+ // queue exists with different arguments" by using it anyway, which is how a
127
+ // queue keeps whatever arguments whoever declared it first happened to pass.
128
+ // It is asserted here, ahead of the binding, because a binding needs the queue
129
+ // to exist; `consume()` below asserts the same declaration from the same owner.
130
+ await this.queueManager.ensureQueues([this.eventsQueueName]);
132
131
 
133
132
  // Bind queue to exchange (receive all registry events)
134
- await channel.bindQueue(this.eventsQueueName, this.exchangeName, '');
133
+ await this.queueManager.bindQueue(this.eventsQueueName, this.exchangeName, '');
135
134
 
136
135
  // Load initial snapshot from Redis if available
137
136
  await this.loadIndexFromCache();
138
137
 
139
- // Start consuming events
140
- await channel.consume(
138
+ // Start consuming events on the core's delivery rail: one attempt, and a
139
+ // failure is rejected into `<service>.dlq` instead of being dropped
140
+ // (d.198b-3; @onlineapps/mq-client-core README § consume() — the delivery
141
+ // contract).
142
+ await this.queueManager.consume(
141
143
  this.eventsQueueName,
142
144
  msg => this._handleRegistryEvent(msg),
143
- { noAck: false }
145
+ { requeueOnError: false }
144
146
  );
145
147
 
146
148
  this.emit('subscribed', { queue: this.eventsQueueName, exchange: this.exchangeName });
147
- console.log(`[${this.serviceName}] Subscribed to registry changes on ${this.eventsQueueName}`);
149
+ this.logger.info('[RegistryEventConsumer] Subscribed to registry changes', {
150
+ serviceName: this.serviceName,
151
+ queue: this.eventsQueueName,
152
+ exchange: this.exchangeName
153
+ });
148
154
 
149
155
  // Request initial snapshot
150
156
  await this.requestSnapshot();
@@ -181,24 +187,32 @@ class RegistryEventConsumer extends EventEmitter {
181
187
  break;
182
188
 
183
189
  default:
184
- console.log(`[${this.serviceName}] Unknown event type: ${event.type}`);
190
+ this.logger.warn('[RegistryEventConsumer] Unknown registry event type', {
191
+ serviceName: this.serviceName,
192
+ eventType: event.type
193
+ });
185
194
  }
186
195
 
187
196
  // Persist index after update
188
197
  await this.persistIndex();
189
198
 
190
- // Acknowledge message
191
- this.queueManager.channel.ack(msg);
199
+ // No ack here: returning IS the ack (d.198b-3). The core settles the delivery
200
+ // on the channel that delivered it, once this handler resolves.
192
201
 
193
202
  // Emit event for service to react
194
203
  this.emit('registryEvent', event);
195
204
 
196
205
  } catch (error) {
197
- console.error(`[${this.serviceName}] Failed to handle registry event:`, error);
198
- this.emit('error', error);
199
-
200
- // Reject message without requeue
201
- this.queueManager.channel.nack(msg, false, false);
206
+ // Not emitted as an 'error' event: an EventEmitter with no 'error' listener
207
+ // rethrows what is emitted, so a delivery the core knows how to reject would
208
+ // become an uncatchable crash. The rejection is the one report.
209
+ //
210
+ // Rethrown, not nacked by hand: the consumer was registered with a
211
+ // one-attempt budget, so the core rejects this delivery into
212
+ // `<service>.dlq` and publishes a `message_dlq` event for it. The
213
+ // `nack(msg, false, false)` this replaces asked the broker to DROP it, and
214
+ // the queue declared nowhere to put it anyway.
215
+ throw error;
202
216
  }
203
217
  }
204
218
 
@@ -219,7 +233,12 @@ class RegistryEventConsumer extends EventEmitter {
219
233
  });
220
234
 
221
235
  this.emit('serviceUpdated', { service, fingerprint, version });
222
- console.log(`[${this.serviceName}] Service ${service} updated with fingerprint ${fingerprint}`);
236
+ this.logger.info('[RegistryEventConsumer] Service spec published', {
237
+ serviceName: this.serviceName,
238
+ service,
239
+ fingerprint,
240
+ version
241
+ });
223
242
  }
224
243
 
225
244
  /**
@@ -234,7 +253,11 @@ class RegistryEventConsumer extends EventEmitter {
234
253
  entry.updatedAt = event.timestamp;
235
254
 
236
255
  this.emit('statusChanged', { service, status });
237
- console.log(`[${this.serviceName}] Service ${service} status changed to ${status}`);
256
+ this.logger.info('[RegistryEventConsumer] Service status changed', {
257
+ serviceName: this.serviceName,
258
+ service,
259
+ status
260
+ });
238
261
  }
239
262
  }
240
263
 
@@ -259,7 +282,10 @@ class RegistryEventConsumer extends EventEmitter {
259
282
  }
260
283
 
261
284
  this.emit('snapshotReceived', { count: services.length });
262
- console.log(`[${this.serviceName}] Received snapshot with ${services.length} services`);
285
+ this.logger.info('[RegistryEventConsumer] Registry snapshot applied', {
286
+ serviceName: this.serviceName,
287
+ count: services.length
288
+ });
263
289
  }
264
290
 
265
291
  /**
@@ -281,7 +307,10 @@ class RegistryEventConsumer extends EventEmitter {
281
307
  fingerprintsToRemove.forEach(fp => this.specCache.delete(fp));
282
308
 
283
309
  this.emit('serviceRemoved', { service });
284
- console.log(`[${this.serviceName}] Service ${service} removed from index`);
310
+ this.logger.info('[RegistryEventConsumer] Service removed from index', {
311
+ serviceName: this.serviceName,
312
+ service
313
+ });
285
314
  }
286
315
  }
287
316
 
@@ -296,10 +325,13 @@ class RegistryEventConsumer extends EventEmitter {
296
325
  timestamp: new Date().toISOString()
297
326
  };
298
327
 
299
- await this.queueManager.channel.publish(
328
+ // Published to the fanout exchange, never to a queue: the first argument names
329
+ // the target for the log line, and `routingKey: ''` is a VALUE a fanout ignores
330
+ // — the same shape the core's own monitoring publisher uses.
331
+ await this.queueManager.publish(
300
332
  this.exchangeName,
301
- '',
302
- Buffer.from(JSON.stringify(request))
333
+ request,
334
+ { exchange: this.exchangeName, routingKey: '' }
303
335
  );
304
336
  }
305
337
 
@@ -312,11 +344,19 @@ class RegistryEventConsumer extends EventEmitter {
312
344
  // Get entry from index
313
345
  const entry = this.serviceIndex.get(serviceName);
314
346
  if (!entry) {
315
- throw new Error(`Service ${serviceName} not found in index`);
347
+ throw new Error(
348
+ `[RegistryEventConsumer] Service ${serviceName} not found in index - `
349
+ + 'the registry has published no entry under that name. '
350
+ + 'Fix: wait for the service to register, or check the name against getActiveServices().'
351
+ );
316
352
  }
317
353
 
318
354
  if (entry.status !== 'ACTIVE') {
319
- throw new Error(`Service ${serviceName} is not active (status: ${entry.status})`);
355
+ throw new Error(
356
+ `[RegistryEventConsumer] Service ${serviceName} is not active (status: ${entry.status}) - `
357
+ + 'only an ACTIVE entry exposes a specification. '
358
+ + 'Fix: wait for the service to finish registering, or read the index entry with getServiceInfo().'
359
+ );
320
360
  }
321
361
 
322
362
  const { fingerprint, bucket, path } = entry;
@@ -340,7 +380,13 @@ class RegistryEventConsumer extends EventEmitter {
340
380
  }
341
381
 
342
382
  // Download from MinIO with verification
343
- console.log(`[${this.serviceName}] Downloading spec for ${serviceName} from ${bucket}/${path}`);
383
+ this.logger.debug('[RegistryEventConsumer] Downloading spec', {
384
+ consumer: this.serviceName,
385
+ service: serviceName,
386
+ bucket,
387
+ path,
388
+ fingerprint
389
+ });
344
390
 
345
391
  const content = await this.storage.downloadWithVerification(
346
392
  bucket,
@@ -402,7 +448,10 @@ class RegistryEventConsumer extends EventEmitter {
402
448
  3600 // Expire after 1 hour
403
449
  );
404
450
  } catch (error) {
405
- console.error(`[${this.serviceName}] Failed to persist index:`, error);
451
+ this.logger.error('[RegistryEventConsumer] Failed to persist index', {
452
+ serviceName: this.serviceName,
453
+ error: error.message
454
+ });
406
455
  }
407
456
  }
408
457
 
@@ -419,10 +468,16 @@ class RegistryEventConsumer extends EventEmitter {
419
468
  this.serviceIndex = new Map(entries);
420
469
 
421
470
  this.emit('indexLoaded', { count: this.serviceIndex.size });
422
- console.log(`[${this.serviceName}] Loaded ${this.serviceIndex.size} services from cache`);
471
+ this.logger.info('[RegistryEventConsumer] Service index loaded from cache', {
472
+ serviceName: this.serviceName,
473
+ count: this.serviceIndex.size
474
+ });
423
475
  }
424
476
  } catch (error) {
425
- console.error(`[${this.serviceName}] Failed to load index from cache:`, error);
477
+ this.logger.error('[RegistryEventConsumer] Failed to load index from cache', {
478
+ serviceName: this.serviceName,
479
+ error: error.message
480
+ });
426
481
  }
427
482
  }
428
483