@onlineapps/conn-orch-registry 3.0.1 → 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.
@@ -22,15 +22,16 @@
22
22
  * @see api/docs/biz/30-operations/registration-wire.md §2 MQ wire format
23
23
  * @see api/docs/biz/30-operations/schema-v3.md
24
24
  *
25
- * @module @onlineapps/connector-registry-client/src/registryClient
25
+ * @module @onlineapps/conn-orch-registry/src/registryClient
26
26
  */
27
27
 
28
28
  const EventEmitter = require('events');
29
29
  const QueueManager = require('./queueManager');
30
30
  const RegistryEventConsumer = require('./registryEventConsumer');
31
31
  const { v4: uuidv4 } = require('uuid');
32
- const queueConfig = require('@onlineapps/mq-client-core/src/config/queueConfig');
32
+ const { queueConfig } = require('@onlineapps/mq-client-core');
33
33
  const DEFAULTS = require('./defaults');
34
+ const { assertLogger } = require('@onlineapps/logger-contract');
34
35
 
35
36
 
36
37
  class ServiceRegistryClient extends EventEmitter {
@@ -39,23 +40,42 @@ class ServiceRegistryClient extends EventEmitter {
39
40
  * @param {string} opts.amqpUrl - AMQP URI for connecting to RabbitMQ
40
41
  * @param {string} opts.serviceName - Name of the service (e.g., 'invoicing')
41
42
  * @param {string} opts.version - Version of the service (e.g., '1.2.0')
42
- * @param {string} [opts.specificationEndpoint='/api/v1/specification'] - Endpoint where API specification is available
43
- * @param {number} [opts.heartbeatInterval=10000] - Heartbeat interval in milliseconds
43
+ * @param {string} opts.specificationEndpoint - Path the `register` and `heartbeat` messages
44
+ * carry. REQUIRED: the wire format declares it required
45
+ * (api/docs/biz/30-operations/registration-wire.md §2.1) and the registry stores what it
46
+ * is sent, so a default here would write a path nobody configured into `registry:services`.
47
+ * @param {number} opts.heartbeatInterval - Milliseconds between heartbeats. REQUIRED: the
48
+ * cadence has ONE owner and it is not a library default (owner confirmation
49
+ * docs/governance/confirmations/biz-health-freshness.md 001 point 3); ServiceWrapper
50
+ * passes `wrapper.registry.heartbeatInterval`, itself fed from BIZ_HEARTBEAT_INTERVAL_MS.
44
51
  * @param {string} [opts.apiQueue='api_services_queuer'] - Queue name for heartbeat and API traffic
45
52
  * @param {string} [opts.registryQueue='registry.register'] - Queue name for registry messages
46
53
  * @param {Object} [opts.validationProof=null] - Validation proof object {hash, data} (injected from outside)
47
54
  * @param {number} [opts.registrationTimeoutMs] - How long register() waits for the registry response
48
55
  * before rejecting. Default comes from ./defaults.js (30000 ms); never read from the environment here.
56
+ * @param {Object} opts.logger - Logger with info/warn/error/debug. Required and validated here:
57
+ * registration, heartbeat and revalidation events are what this client exists to report, and
58
+ * until 2026-09-05 they went to stdout through bare printing while this parameter was accepted
59
+ * and never read. Owner confirmation:
60
+ * docs/governance/confirmations/connector-logger-contract.md 001 + 002.
49
61
  */
50
- constructor({ amqpUrl, serviceName, version, specificationEndpoint = '/api/v1/specification',
51
- heartbeatInterval = 10000, apiQueue = 'api_services_queuer', registryQueue = 'registry.register',
52
- registryUrl = null, redis = null, storageConfig = {}, validationProof = null,
62
+ constructor({ amqpUrl, serviceName, version, specificationEndpoint,
63
+ heartbeatInterval, apiQueue = 'api_services_queuer', registryQueue = 'registry.register',
64
+ redis = null, storageConfig = {}, validationProof = null, logger,
53
65
  registryKeyPrefix = DEFAULTS.registryKeyPrefix,
54
66
  registrationTimeoutMs = DEFAULTS.registrationTimeoutMs }) {
55
67
  super();
56
68
  if (!amqpUrl || !serviceName || !version) {
57
- throw new Error('amqpUrl, serviceName, and version are required');
69
+ throw new Error(
70
+ '[RegistryClient] Missing constructor options - amqpUrl, serviceName, and version are required. '
71
+ + 'Fix: pass all three to new ServiceRegistryClient({ amqpUrl, serviceName, version, logger }).'
72
+ );
58
73
  }
74
+ this.logger = assertLogger(
75
+ 'RegistryClient',
76
+ logger,
77
+ 'registration, heartbeat and revalidation events reach the service log'
78
+ );
59
79
  if (typeof registrationTimeoutMs !== 'number' || !Number.isFinite(registrationTimeoutMs) || registrationTimeoutMs <= 0) {
60
80
  throw new Error(
61
81
  `[RegistryClient] ${serviceName}: Invalid registrationTimeoutMs - expected a positive number of milliseconds, `
@@ -66,13 +86,35 @@ class ServiceRegistryClient extends EventEmitter {
66
86
  this.registrationTimeoutMs = registrationTimeoutMs;
67
87
  this.serviceName = serviceName;
68
88
  this.version = version;
69
- // Spec endpoint is still configurable, but defaults are module-owned and can be overridden via runtime config.
89
+ // Both of these were module defaults until d.278c, and both were statements this
90
+ // package had no standing to make (architecture-principles §3, no fallbacks).
91
+ if (typeof specificationEndpoint !== 'string' || specificationEndpoint === '') {
92
+ throw new Error(
93
+ `[RegistryClient] ${serviceName}: Missing constructor option - specificationEndpoint is required, `
94
+ + `got ${JSON.stringify(specificationEndpoint)}. Expected: the path the register message must carry `
95
+ + '(api/docs/biz/30-operations/registration-wire.md §2.1 declares it required). '
96
+ + 'Fix: pass service.specificationEndpoint from the service configuration; nothing may substitute a path for it.'
97
+ );
98
+ }
99
+ if (heartbeatInterval === undefined) {
100
+ throw new Error(
101
+ `[RegistryClient] ${serviceName}: Missing constructor option - heartbeatInterval is required. `
102
+ + 'Expected: the cadence its one owner decided (docs/governance/confirmations/biz-health-freshness.md 001 '
103
+ + 'point 3), passed as a positive number of milliseconds. '
104
+ + 'Fix: pass wrapper.registry.heartbeatInterval (ServiceWrapper reads it from BIZ_HEARTBEAT_INTERVAL_MS).'
105
+ );
106
+ }
107
+ if (typeof heartbeatInterval !== 'number' || !Number.isFinite(heartbeatInterval) || heartbeatInterval <= 0) {
108
+ throw new Error(
109
+ `[RegistryClient] ${serviceName}: Invalid heartbeatInterval - expected a positive number of milliseconds, `
110
+ + `got ${JSON.stringify(heartbeatInterval)}. Fix: pass the cadence as a number; a string is not parsed here.`
111
+ );
112
+ }
70
113
  this.specificationEndpoint = specificationEndpoint;
71
114
  this.heartbeatInterval = heartbeatInterval;
72
115
  this.apiQueue = apiQueue;
73
116
  this.registryQueue = registryQueue;
74
- this.registryUrl = registryUrl;
75
- this.queueManager = new QueueManager(amqpUrl, serviceName);
117
+ this.queueManager = new QueueManager(amqpUrl, serviceName, this.logger);
76
118
  this.heartbeatTimer = null;
77
119
 
78
120
  // Event consumer (optional, activated via subscribeToChanges)
@@ -93,119 +135,131 @@ class ServiceRegistryClient extends EventEmitter {
93
135
  async init() {
94
136
  await this.queueManager.init();
95
137
 
96
- // Validation proof is now injected via constructor (not loaded here)
138
+ // Validation proof is now injected via constructor (not loaded here).
139
+ // One line either way: a proof is the normal case (info), its absence means the
140
+ // registry falls back to Tier 2 validation, which is a degraded mode (warn).
97
141
  if (this.validationProof) {
98
- console.log(`[RegistryClient] ${this.serviceName}: ✅ Validation proof provided via constructor`);
99
- console.log(`[RegistryClient] ${this.serviceName}: Proof hash: ${this.validationProof.validationProof.substring(0, 32)}...`);
142
+ this.logger.info('[RegistryClient] Validation proof provided via constructor', {
143
+ serviceName: this.serviceName,
144
+ hasValidationProof: true,
145
+ proofHash: this.validationProof.validationProof.substring(0, 32)
146
+ });
100
147
  } else {
101
- console.log(`[RegistryClient] ${this.serviceName}: ⚠️ No validation proof - will use Tier 2 validation`);
148
+ this.logger.warn('[RegistryClient] No validation proof - registry will run Tier 2 validation', {
149
+ serviceName: this.serviceName,
150
+ hasValidationProof: false
151
+ });
102
152
  }
103
153
 
104
- // FÁZE 0.5: Vytvoření service-specific front
105
- const queueCreationStartTime = Date.now();
154
+ // FÁZE 0.5 + 0.6: the response queue and its consumer, in one step.
155
+ //
156
+ // The queue is NOT asserted here any more. `consume()` in the core declares a
157
+ // business queue itself, with the arguments `queueConfig` owns
158
+ // (`transports/rabbitmqClient.js` § _prepareQueueForConsume), so a separate
159
+ // `ensureQueues()` call ahead of it was a second declaration of one queue —
160
+ // agreeing today, and 406 PRECONDITION-FAILED the day the two drift apart
161
+ // (`.claude/rules/change-discipline.md` § One rail per concern).
162
+ const consumerStartTime = Date.now();
106
163
  this.serviceRegistryQueue = `${this.serviceName}.registry`;
107
- console.log(`[FÁZE 0.5] Service Queue Creation - STARTING`);
108
-
109
- try {
110
- // Ensure existence of service-specific queue only (infrastructure queues must already exist)
111
- await this.queueManager.ensureQueues([this.serviceRegistryQueue]);
112
- console.log(`[FÁZE 0.5] Service Queue Creation - PASSED (${Date.now() - queueCreationStartTime}ms)`);
113
- } catch (error) {
114
- console.error(`[FÁZE 0.5] Service Queue Creation - FAILED: ${error.message}`);
115
- throw error;
116
- }
117
164
 
118
- // FÁZE 0.6: Spuštění konzumerů
119
- const consumerStartTime = Date.now();
120
- console.log(`[FÁZE 0.6] Consumer Startup - STARTING`);
121
-
122
- // CRITICAL: Before consume(), we must assertQueue with correct parameters
123
- // amqplib's channel.consume() may internally call assertQueue() WITHOUT parameters
124
- // This causes 406 PRECONDITION-FAILED if queue exists with different arguments
125
- console.log(`[RegistryClient] [CONSUMER] About to consume from ${this.serviceRegistryQueue}`);
126
- console.log(`[RegistryClient] [CONSUMER] ⚠ WARNING: amqplib's channel.consume() may internally call assertQueue() WITHOUT parameters`);
127
- console.log(`[RegistryClient] [CONSUMER] ⚠ WARNING: Queue should already be asserted by ensureQueues() above with correct parameters from queueConfig.js`);
128
-
129
- // Start consuming service registry queue for registry responses and events
130
165
  const CONSUME_TIMEOUT = 5000; // 5 seconds
131
- const consumeRegistryPromise = this.queueManager.channel.consume(
166
+ // `requeueOnError: false` is the budget this consumer has always had, said in
167
+ // the core's word: ONE attempt. The difference is where a failure goes — the
168
+ // hand-written `nack(msg, false, false)` this replaces asked the broker to drop
169
+ // the message, while the policy rejects it into `<service>.dlq` and publishes a
170
+ // `message_dlq` event. The handler reports failure by THROWING; it settles
171
+ // nothing itself (@onlineapps/mq-client-core README § consume() — the delivery
172
+ // contract).
173
+ const consumeRegistryPromise = this.queueManager.consume(
132
174
  this.serviceRegistryQueue,
133
- msg => {
134
- // Handle null message (queue deleted, connection closed, consumer canceled)
135
- if (!msg) {
136
- console.warn(`[RegistryClient] ${this.serviceName}: Received null message from ${this.serviceRegistryQueue} (queue may be deleted or connection closed)`);
137
- return;
138
- }
139
- try {
140
- this._handleRegistryMessage(msg);
141
- } catch (error) {
142
- console.error(`[RegistryClient] ${this.serviceName}: Error in consume callback:`, error);
143
- // Nack message on error
144
- try {
145
- this.queueManager.channel.nack(msg, false, false);
146
- } catch (nackErr) {
147
- console.error(`[RegistryClient] ${this.serviceName}: Failed to nack message:`, nackErr);
148
- }
149
- }
150
- },
151
- { noAck: false }
175
+ msg => this._handleRegistryMessage(msg),
176
+ { requeueOnError: false }
152
177
  );
153
178
  let consumeRegistryTimeout;
154
179
  const consumeRegistryTimeoutPromise = new Promise((_, reject) => {
155
180
  consumeRegistryTimeout = setTimeout(() => {
156
- reject(new Error(`consume() timeout for ${this.serviceRegistryQueue} after ${CONSUME_TIMEOUT}ms`));
181
+ reject(new Error(
182
+ `[RegistryClient] consume() timeout for ${this.serviceRegistryQueue} after ${CONSUME_TIMEOUT}ms - `
183
+ + 'the broker never confirmed the consumer. Fix: check RabbitMQ responsiveness and that the queue exists.'
184
+ ));
157
185
  }, CONSUME_TIMEOUT);
158
186
  });
159
187
 
160
188
  try {
161
189
  await Promise.race([consumeRegistryPromise, consumeRegistryTimeoutPromise]);
162
- console.log(`[FÁZE 0.6] Consumer Startup - PASSED (${Date.now() - consumerStartTime}ms)`);
163
190
  } catch (consumeErr) {
164
- console.error(`[FÁZE 0.6] Consumer Startup - FAILED: ${consumeErr.message}`);
165
- throw new Error(`[RegistryClient] ${this.serviceName}: Failed to start consumer on ${this.serviceRegistryQueue}: ${consumeErr.message}`);
191
+ // Not logged before throwing: the thrown message already names the service,
192
+ // the queue and the cause.
193
+ throw new Error(
194
+ `[RegistryClient] ${this.serviceName}: Failed to start consumer on ${this.serviceRegistryQueue} - `
195
+ + `${consumeErr.message}. Fix: check RabbitMQ health; without this consumer no registry reply is ever read.`,
196
+ { cause: consumeErr }
197
+ );
166
198
  } finally {
167
199
  if (consumeRegistryTimeout) clearTimeout(consumeRegistryTimeout);
168
200
  }
201
+
202
+ // One line for the whole boot of this client: which queues it talks on, and how
203
+ // long the consumer took to attach. Replaces the four FÁZE 0.5/0.6 markers.
204
+ this.logger.info('[RegistryClient] Initialized', {
205
+ serviceName: this.serviceName,
206
+ version: this.version,
207
+ registryQueue: this.registryQueue,
208
+ responseQueue: this.serviceRegistryQueue,
209
+ consumerStartupMs: Date.now() - consumerStartTime
210
+ });
169
211
  }
170
212
 
171
213
  /**
172
214
  * Internal handler for incoming messages from the registry queue.
173
215
  * Handles registration responses and revalidation requests.
216
+ *
217
+ * Settling is NOT this method's business (d.198b-3). A return means "handled"
218
+ * and the core acks once; a THROW means "not handled" and the core applies the
219
+ * delivery policy the consumer was registered with — one attempt, then
220
+ * `<service>.dlq` with a `message_dlq` event. The hand-written
221
+ * `channel.ack` / `channel.nack(msg, false, false)` that used to end this method
222
+ * asked the broker to DROP a message it could not read.
223
+ *
174
224
  * @param {Object} msg - AMQP message
225
+ * @returns {Promise<void>}
226
+ * @throws {Error} If the payload is not readable as a registry message
175
227
  * @private
176
228
  */
177
- _handleRegistryMessage(msg) {
178
- // CRITICAL: Handle null message (queue deleted, connection closed, consumer canceled)
179
- if (!msg) {
180
- console.warn(`[RegistryClient] ${this.serviceName}: Received null message (queue may be deleted or connection closed)`);
181
- return; // Don't try to parse or ack null message
182
- }
183
-
184
- // Log which queue this message came from (if available in msg properties)
229
+ async _handleRegistryMessage(msg) {
230
+ // Which queue this message came from (if available in msg properties)
185
231
  const queueName = msg.fields?.routingKey || msg.fields?.exchange || 'unknown';
186
- console.log(`[RegistryClient] ${this.serviceName}: [MESSAGE] Processing message from queue: ${queueName}`);
187
-
232
+
188
233
  let payload;
189
234
  try {
190
235
  payload = JSON.parse(msg.content.toString());
191
- console.log(`[RegistryClient] ${this.serviceName}: [MESSAGE] Parsed payload type: ${payload.type}, requestId: ${payload.requestId}`);
236
+ // One line per inbound message, after parsing: the "processing from queue"
237
+ // line that used to precede it said nothing this one does not.
238
+ this.logger.debug('[RegistryClient] Registry message received', {
239
+ serviceName: this.serviceName,
240
+ queue: queueName,
241
+ type: payload.type,
242
+ requestId: payload.requestId
243
+ });
192
244
  } catch (err) {
193
- console.error(`[RegistryClient] ${this.serviceName}: [MESSAGE] Failed to parse message:`, err);
194
- this.emit('error', err);
195
- // Only nack if message is not null
196
- if (msg) {
197
- try {
198
- this.queueManager.channel.nack(msg, false, false);
199
- } catch (nackErr) {
200
- console.error(`[RegistryClient] ${this.serviceName}: Failed to nack message:`, nackErr.message);
201
- }
202
- }
203
- return;
245
+ // Not logged before throwing, and NOT emitted as an 'error' event either: an
246
+ // EventEmitter with no 'error' listener RETHROWS what is emitted, which would
247
+ // turn a delivery the core knows how to reject into an uncatchable crash of the
248
+ // service. The rejection below is the one report, and the core turns it into a
249
+ // dead-letter plus a `message_dlq` event (connector-logger-contract 003).
250
+ throw new Error(
251
+ `[RegistryClient] Failed to parse registry message from ${queueName} - ${err.message}. `
252
+ + 'Expected: JSON written by api_services_registry. '
253
+ + 'Fix: the message is rejected into the dead-letter queue unread; read it there '
254
+ + 'through the DLQ dashboard to see what was published.',
255
+ { cause: err }
256
+ );
204
257
  }
205
258
 
206
259
  // Handle revalidation request from registry (infra version change)
207
260
  if (payload.type === 'revalidate') {
208
- console.log(`[RegistryClient] ${this.serviceName}: Received revalidation request`, {
261
+ this.logger.info('[RegistryClient] Revalidation requested by the registry', {
262
+ serviceName: this.serviceName,
209
263
  reason: payload.reason,
210
264
  infraFingerprint: payload.infraFingerprint?.substring(0, 16)
211
265
  });
@@ -215,17 +269,16 @@ class ServiceRegistryClient extends EventEmitter {
215
269
  // Handle registration response from registry
216
270
  // Registry sends 'register.confirmed' after validation
217
271
  if ((payload.type === 'registerResponse' || payload.type === 'register.confirmed') && payload.requestId) {
218
- console.log(`[RegistryClient] ${this.serviceName}: [REGISTRATION] Received ${payload.type} message`, {
272
+ this.logger.info('[RegistryClient] Registration response received', {
273
+ serviceName: this.serviceName,
274
+ type: payload.type,
219
275
  requestId: payload.requestId,
220
- hasPendingRegistrations: !!(this.pendingRegistrations),
276
+ queue: queueName,
221
277
  pendingCount: this.pendingRegistrations ? this.pendingRegistrations.size : 0,
222
- hasMatchingRequest: !!(this.pendingRegistrations && this.pendingRegistrations.has(payload.requestId)),
223
- pendingKeys: this.pendingRegistrations ? Array.from(this.pendingRegistrations.keys()) : [],
224
- queueName
278
+ matched: !!(this.pendingRegistrations && this.pendingRegistrations.has(payload.requestId))
225
279
  });
226
280
 
227
281
  if (this.pendingRegistrations && this.pendingRegistrations.has(payload.requestId)) {
228
- console.log(`[RegistryClient] ${this.serviceName}: [REGISTRATION] Found matching pending registration, extracting resolve function...`);
229
282
  const { resolve } = this.pendingRegistrations.get(payload.requestId);
230
283
 
231
284
  // Clear associated timeout
@@ -235,18 +288,20 @@ class ServiceRegistryClient extends EventEmitter {
235
288
  }
236
289
 
237
290
  this.pendingRegistrations.delete(payload.requestId);
238
- console.log(`[RegistryClient] ${this.serviceName}: [REGISTRATION] ✓ Resolve function extracted, pending registration removed`);
239
-
240
- // CRITICAL: Resolve promise synchronously to ensure it's called before any async operations
241
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Starting resolve process for requestId: ${payload.requestId}`);
242
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Payload keys:`, Object.keys(payload));
243
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Payload.success:`, payload.success);
244
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Payload.validated:`, payload.validated);
245
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Payload.hasCertificate:`, !!payload.certificate);
246
- if (payload.certificate) {
247
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Payload.certificate.id:`, payload.certificate.id);
248
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Payload.certificate.serviceName:`, payload.certificate.serviceName);
249
- }
291
+
292
+ // What the registry actually answered, in one structured record. Nine
293
+ // stdout lines printed these same fields one per line.
294
+ this.logger.debug('[RegistryClient] Registry verdict payload', {
295
+ serviceName: this.serviceName,
296
+ requestId: payload.requestId,
297
+ payloadKeys: Object.keys(payload),
298
+ success: payload.success,
299
+ validated: payload.validated,
300
+ errorCode: payload.errorCode,
301
+ hasCertificate: !!payload.certificate,
302
+ certificateId: payload.certificate?.id,
303
+ certificateServiceName: payload.certificate?.serviceName
304
+ });
250
305
 
251
306
  // Resolve the registration promise with the response
252
307
  const registrationResult = {
@@ -256,50 +311,71 @@ class ServiceRegistryClient extends EventEmitter {
256
311
  version: this.version,
257
312
  registrationId: payload.registrationId,
258
313
  validated: payload.validated || payload.success, // Consider successful registration as validated
259
- certificate: payload.certificate || null // Include certificate from Registry
314
+ certificate: payload.certificate || null, // Include certificate from Registry
315
+ // WHY the registry decided as it did, in a stable machine code the caller
316
+ // can branch on instead of matching the message text. Passed through
317
+ // verbatim - no default, no fallback (architecture-principles §3): the
318
+ // explicit `null` the registry sends on a confirmed registration stays
319
+ // null, and a payload carrying no code at all leaves this undefined.
320
+ // @see api/docs/biz/30-operations/registration-wire.md §8
321
+ errorCode: payload.errorCode,
322
+ // The parked registration of §2.5 — "you have not pre-validated yet" — is
323
+ // not a refusal and carries no errorCode; what it carries is the state and
324
+ // what to run about it. Both were read off the wire and dropped here until
325
+ // d.278c, so a caller saw `success: false` and nothing that explained it.
326
+ // Verbatim, like errorCode: a confirmed registration sends neither key and
327
+ // this leaves both undefined rather than inventing a status.
328
+ // @see api/docs/biz/30-operations/registration-wire.md §2.5
329
+ validationStatus: payload.validationStatus,
330
+ instructions: payload.instructions
260
331
  };
261
-
262
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Created registrationResult:`, JSON.stringify({
263
- success: registrationResult.success,
264
- validated: registrationResult.validated,
265
- hasCertificate: !!registrationResult.certificate,
266
- certificateId: registrationResult.certificate?.id || 'none',
267
- serviceName: registrationResult.serviceName,
268
- version: registrationResult.version
269
- }));
270
-
271
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] About to call resolve() function...`);
272
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] resolve function type:`, typeof resolve);
273
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] resolve function exists:`, !!resolve);
274
-
332
+
275
333
  try {
276
334
  // CRITICAL: Call resolve synchronously - this must happen immediately
277
335
  resolve(registrationResult);
278
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] ✓ resolve() called successfully`);
279
- console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Promise should now be resolved`);
336
+ this.logger.info('[RegistryClient] Registration resolved', {
337
+ serviceName: this.serviceName,
338
+ requestId: payload.requestId,
339
+ success: registrationResult.success,
340
+ validated: registrationResult.validated,
341
+ hasCertificate: !!registrationResult.certificate,
342
+ certificateId: registrationResult.certificate?.id || null
343
+ });
280
344
  } catch (resolveError) {
281
- console.error(`[RegistryClient] ${this.serviceName}: [RESOLVE] ✗ Error calling resolve():`, resolveError);
282
- console.error(`[RegistryClient] ${this.serviceName}: [RESOLVE] Error stack:`, resolveError.stack);
345
+ this.logger.error('[RegistryClient] Resolving the registration promise threw', {
346
+ serviceName: this.serviceName,
347
+ requestId: payload.requestId,
348
+ error: resolveError.message,
349
+ stack: resolveError.stack
350
+ });
283
351
  throw resolveError;
284
352
  }
285
353
  } else {
286
- console.warn(`[RegistryClient] ${this.serviceName}: ⚠️ Received ${payload.type} with requestId ${payload.requestId}, but no matching pending registration found`);
354
+ this.logger.warn('[RegistryClient] Registration response has no matching pending request', {
355
+ serviceName: this.serviceName,
356
+ type: payload.type,
357
+ requestId: payload.requestId
358
+ });
287
359
  }
288
360
  }
289
361
 
290
- // Acknowledge all messages (only if message is not null)
291
- if (msg) {
292
- try {
293
- this.queueManager.channel.ack(msg);
294
- } catch (ackErr) {
295
- console.error(`[RegistryClient] ${this.serviceName}: Failed to ack message:`, ackErr.message);
296
- }
297
- }
362
+ // No ack here: returning IS the ack (d.198b-3). The core settles the delivery
363
+ // once the handler resolves, on the channel that delivered it.
298
364
  }
299
365
 
300
366
  /**
301
- * Ensure an infrastructure queue exists without mutating it.
302
- * Uses a temporary channel so failures don't kill the primary channel.
367
+ * Confirms an infrastructure queue exists, without creating it.
368
+ *
369
+ * A consumer never creates an infrastructure queue: it is brought into being by
370
+ * the infrastructure service that owns it (`api/docs/standards/queue-ownership.md`),
371
+ * with the arguments the central `queueConfig` declares. What this client may do
372
+ * is ask whether it is there, so a publish that would otherwise vanish into the
373
+ * default exchange fails fast and says which queue is missing.
374
+ *
375
+ * The check goes through the core's queue channel (d.198b-3). Until then it opened
376
+ * a raw amqplib channel of its own, precisely because a 404 kills the channel it
377
+ * arrives on — the core owns that recreation now.
378
+ *
303
379
  * @param {string} queueName
304
380
  * @returns {Promise<void>}
305
381
  * @private
@@ -309,7 +385,7 @@ class ServiceRegistryClient extends EventEmitter {
309
385
  return;
310
386
  }
311
387
 
312
- if (!queueConfig?.isInfrastructureQueue(queueName)) {
388
+ if (!queueConfig.isInfrastructureQueue(queueName)) {
313
389
  return;
314
390
  }
315
391
 
@@ -317,62 +393,28 @@ class ServiceRegistryClient extends EventEmitter {
317
393
  return;
318
394
  }
319
395
 
320
- if (!this.queueManager?.conn) {
321
- throw new Error(`[RegistryClient] ${this.serviceName}: MQ connection not ready, cannot verify infrastructure queue ${queueName}. Ensure RabbitMQ is accessible.`);
396
+ if (!this.queueManager.isConnected()) {
397
+ throw new Error(
398
+ `[RegistryClient] ${this.serviceName}: MQ connection not ready - cannot verify infrastructure queue ${queueName}. `
399
+ + 'Fix: complete init() first and check that RabbitMQ is accessible.'
400
+ );
322
401
  }
323
402
 
324
- // Add timeout to prevent hanging
325
- const QUEUE_VERIFY_TIMEOUT = 10000; // 10 seconds
326
- const verifyPromise = (async () => {
327
- let verificationChannel;
328
- let channelTimeout;
329
-
330
- try {
331
- // Wrapped createChannel with timeout
332
- const createChannelPromise = this.queueManager.conn.createChannel();
333
- const createChannelTimeoutPromise = new Promise((_, reject) => {
334
- channelTimeout = setTimeout(() => {
335
- reject(new Error(`createChannel timeout after ${QUEUE_VERIFY_TIMEOUT / 2}ms`));
336
- }, QUEUE_VERIFY_TIMEOUT / 2);
337
- });
338
-
339
- verificationChannel = await Promise.race([createChannelPromise, createChannelTimeoutPromise]);
340
- if (channelTimeout) clearTimeout(channelTimeout);
341
-
342
- await verificationChannel.checkQueue(queueName);
343
- this._verifiedInfraQueues.add(queueName);
344
- } catch (error) {
345
- if (error.code === 404) {
346
- throw new Error(`[RegistryClient] Infrastructure queue '${queueName}' is missing. Ensure the responsible infrastructure service has initialized it.`);
347
- }
348
- throw error;
349
- } finally {
350
- if (channelTimeout) clearTimeout(channelTimeout);
351
- if (verificationChannel) {
352
- try {
353
- await verificationChannel.close();
354
- } catch (closeErr) {
355
- console.warn(`[RegistryClient] ${this.serviceName}: Failed to close verification channel for ${queueName}:`, closeErr.message);
356
- }
357
- }
358
- }
359
- })();
360
-
361
- let verifyTimeout;
362
- const timeoutPromise = new Promise((_, reject) => {
363
- verifyTimeout = setTimeout(() => {
364
- reject(new Error(`[RegistryClient] ${this.serviceName}: Timeout verifying infrastructure queue '${queueName}' after ${QUEUE_VERIFY_TIMEOUT}ms. Queue may not exist or RabbitMQ is unresponsive.`));
365
- }, QUEUE_VERIFY_TIMEOUT);
366
- });
367
-
368
403
  try {
369
- await Promise.race([verifyPromise, timeoutPromise]);
404
+ await this.queueManager.checkQueue(queueName);
405
+ this._verifiedInfraQueues.add(queueName);
370
406
  } catch (error) {
371
- // Clear verification cache on error so we can retry
407
+ // Cleared so the next call asks again: a queue that was missing a second ago
408
+ // may have been created by its owner since.
372
409
  this._verifiedInfraQueues.delete(queueName);
410
+ if (error.code === 404) {
411
+ throw new Error(
412
+ `[RegistryClient] Infrastructure queue '${queueName}' is missing - the broker answered 404 for it. `
413
+ + 'Fix: start the infrastructure service that owns this queue before this service registers.',
414
+ { cause: error }
415
+ );
416
+ }
373
417
  throw error;
374
- } finally {
375
- if (verifyTimeout) clearTimeout(verifyTimeout);
376
418
  }
377
419
  }
378
420
 
@@ -385,17 +427,27 @@ class ServiceRegistryClient extends EventEmitter {
385
427
  * @param {Object} serviceInfo.metadata - Additional service metadata
386
428
  * @param {string} serviceInfo.health - Health check endpoint
387
429
  * @param {Object} serviceInfo.spec - OpenAPI specification
430
+ * @param {(boolean|null)} [serviceInfo.deployable] - Manifest-conformance verdict of the
431
+ * caller's last validation run: `true`, `false`, or `null` when the run never measured
432
+ * it. Emitted only when the caller states one; see the `deployable` block below.
388
433
  * @param {number} serviceInfo.timeout - Timeout for registration response (default: 30000ms)
389
- * @returns {Promise<Object>} Registration result with success status
434
+ * @returns {Promise<{success: boolean, message: string, serviceName: string, version: string,
435
+ * registrationId: (string|undefined), validated: boolean, certificate: (Object|null),
436
+ * errorCode: (string|null|undefined)}>} The registry verdict. `errorCode` is the
437
+ * registry's machine reason for a refusal (`UNDECLARED_SERVICE`,
438
+ * `INVALID_OPERATIONS_SCHEMA`, `VALIDATOR_UNAVAILABLE`, …) — branch on it rather
439
+ * than on `message`, whose wording is not a contract. It is passed through
440
+ * verbatim: `null` on a confirmed registration, `undefined` when the reply
441
+ * carried no code at all.
442
+ * @see api/docs/biz/30-operations/registration-wire.md §8
390
443
  */
391
444
  async register(serviceInfo = {}) {
392
- // DEBUG: Check validation proof status
393
- console.log(`[RegistryClient] ${this.serviceName}: >>> REGISTER CALLED <<<`);
394
- console.log(`[RegistryClient] ${this.serviceName}: this.validationProof =`, this.validationProof);
395
-
396
445
  // Validate input
397
446
  if (serviceInfo.endpoints && !Array.isArray(serviceInfo.endpoints)) {
398
- throw new Error('endpoints must be an array');
447
+ throw new Error(
448
+ '[RegistryClient] Invalid serviceInfo - endpoints must be an array. '
449
+ + 'Fix: pass endpoints as an array of endpoint descriptors, or omit the key entirely.'
450
+ );
399
451
  }
400
452
 
401
453
  // FAIL-FAST: a client that was never initialized, or that has been closed, is not
@@ -403,9 +455,9 @@ class ServiceRegistryClient extends EventEmitter {
403
455
  // channel WITHOUT restarting the response consumer started by init() - so the
404
456
  // registry answered into a queue nobody was reading and register() waited out the
405
457
  // full timeout. No fallbacks (architecture-principles §3), fail fast (§4).
406
- if (!this.queueManager.channel) {
458
+ if (!this.queueManager.isConnected()) {
407
459
  throw new Error(
408
- `[RegistryClient] ${this.serviceName}: Cannot register - the MQ channel is not open `
460
+ `[RegistryClient] ${this.serviceName}: Cannot register - the MQ connection is not open `
409
461
  + '(client not initialized or already closed). Expected: init() completed before register(). '
410
462
  + 'Fix: call init() on a fresh ServiceRegistryClient; a closed client is not reusable.'
411
463
  );
@@ -436,16 +488,31 @@ class ServiceRegistryClient extends EventEmitter {
436
488
  msg.workspaceScoped = serviceInfo.workspaceScoped;
437
489
  }
438
490
 
491
+ // The manifest-conformance verdict, verbatim from the caller: `true`, `false`,
492
+ // or `null` when validation stopped before step 7 measured it. The registry
493
+ // projects it into `infrastructure:health:<service>` beside `status`
494
+ // (owner confirmation `api/docs/governance/confirmations/biz-service-manifest.md`
495
+ // 001 §4), so `null` is a value this message must be able to carry — hence a
496
+ // presence check and not a truthiness one. A caller that sends no verdict
497
+ // (anything other than ServiceWrapper) puts no key on the wire; nothing is
498
+ // substituted for it.
499
+ if ('deployable' in serviceInfo) {
500
+ msg.deployable = serviceInfo.deployable;
501
+ }
502
+
439
503
  // Include validation proof if loaded
440
504
  // Structure: { validationProof: "hash", validationData: { ... } }
441
505
  // See: @onlineapps/service-validator-core/README.md#validation-proof-structure
442
506
  if (this.validationProof) {
443
507
  msg.validationProof = this.validationProof.validationProof;
444
508
  msg.validationData = this.validationProof.validationData;
445
- console.log(`[RegistryClient] ${this.serviceName}: ✅ Including validation proof in registration message`);
446
- console.log(`[RegistryClient] ${this.serviceName}: Proof hash: ${this.validationProof.validationProof.substring(0, 32)}...`);
509
+ // The proof itself is reported on the "Registration message sent" line below;
510
+ // repeating it here would log the same fact twice per registration.
447
511
  } else {
448
- console.log(`[RegistryClient] ${this.serviceName}: ⚠️ NO validation proof available - Registry will perform Tier 2 validation`);
512
+ this.logger.warn('[RegistryClient] Registering without a validation proof - registry will run Tier 2', {
513
+ serviceName: this.serviceName,
514
+ version: this.version
515
+ });
449
516
  }
450
517
 
451
518
  // Create promise to wait for registration response.
@@ -482,44 +549,56 @@ class ServiceRegistryClient extends EventEmitter {
482
549
  // Send registration message to registry
483
550
  // CRITICAL: Use queueConfig.js to get correct parameters (TTL, max-length, etc.)
484
551
  // This prevents 406 PRECONDITION-FAILED errors from TTL mismatches
485
- console.log(`[RegistryClient] [PUBLISH] Preparing to publish to ${this.registryQueue}`);
486
-
487
552
  try {
488
553
  await this._ensureInfrastructureQueue(this.registryQueue);
489
554
  } catch (queueError) {
490
- const errorMsg = `[RegistryClient] ${this.serviceName}: Cannot register - infrastructure queue verification failed: ${queueError.message}`;
491
- console.error(errorMsg);
492
- throw this._abortPendingRegistration(msgId, new Error(errorMsg));
555
+ // Not logged before throwing: the rejection carries this exact message.
556
+ throw this._abortPendingRegistration(msgId, new Error(
557
+ `[RegistryClient] ${this.serviceName}: Cannot register - infrastructure queue verification failed: ${queueError.message}`
558
+ ));
493
559
  }
494
560
 
495
- console.log(`[RegistryClient] ${this.serviceName}: Sending registration message to queue: ${this.registryQueue}`);
496
- console.log(`[RegistryClient] ${this.serviceName}: Message type: ${msg.type}, serviceName: ${msg.serviceName}, version: ${msg.version}`);
497
-
498
- // Send message synchronously (sendToQueue is synchronous, returns boolean).
499
- // The channel existed when register() started; it can still have been closed while
500
- // the infrastructure-queue check awaited above.
501
- if (!this.queueManager.channel || this.queueManager.channel.closed) {
561
+ // The connection was open when register() started; it can still have been lost
562
+ // while the infrastructure-queue check awaited above.
563
+ if (!this.queueManager.isConnected()) {
502
564
  throw this._abortPendingRegistration(msgId, new Error(
503
- `[RegistryClient] ${this.serviceName}: Cannot publish - the MQ channel closed before the registration `
504
- + 'message was sent. Expected: an open channel for the whole register() call. '
565
+ `[RegistryClient] ${this.serviceName}: Cannot publish - the MQ connection closed before the registration `
566
+ + 'message was sent. Expected: an open connection for the whole register() call. '
505
567
  + 'Fix: re-create the client (init() + register()) once the MQ connection is back.'
506
568
  ));
507
569
  }
508
570
 
509
571
  try {
510
- const sent = this.queueManager.channel.sendToQueue(
572
+ // The core's publish resolves or THROWS; there is no falsy return to read
573
+ // (@onlineapps/mq-client-core README § API — "publish() … failure is signalled
574
+ // by a thrown PublishError, never by a falsy return"). Serialisation is its
575
+ // job too, so the message travels as the object it is.
576
+ await this.queueManager.publish(
511
577
  this.registryQueue,
512
- Buffer.from(JSON.stringify(msg)),
578
+ msg,
513
579
  { persistent: true }
514
580
  );
515
- if (!sent) {
516
- throw new Error(`[RegistryClient] ${this.serviceName}: sendToQueue returned false - queue may be full or channel backpressure active.`);
517
- }
518
- console.log(`[RegistryClient] ${this.serviceName}: ✓ Registration message sent, waiting for response...`);
581
+ // The one line per registration attempt: what was published, where, under
582
+ // which request id, and whether it carried a proof. Six stdout lines
583
+ // ("REGISTER CALLED", the proof pair, "Preparing to publish", "Sending
584
+ // registration message", "Message type") said this in pieces.
585
+ this.logger.info('[RegistryClient] Registration message sent', {
586
+ serviceName: this.serviceName,
587
+ version: this.version,
588
+ registryQueue: this.registryQueue,
589
+ responseQueue: this.serviceRegistryQueue,
590
+ requestId: msgId,
591
+ hasValidationProof: !!this.validationProof,
592
+ proofHash: this.validationProof
593
+ ? this.validationProof.validationProof.substring(0, 32)
594
+ : null,
595
+ timeoutMs: timeout
596
+ });
519
597
  } catch (sendError) {
520
- const errorMsg = `[RegistryClient] ${this.serviceName}: Failed to send registration message: ${sendError.message}`;
521
- console.error(errorMsg);
522
- throw this._abortPendingRegistration(msgId, new Error(errorMsg));
598
+ // Not logged before throwing: the rejection carries this exact message.
599
+ throw this._abortPendingRegistration(msgId, new Error(
600
+ `[RegistryClient] ${this.serviceName}: Failed to send registration message: ${sendError.message}`
601
+ ));
523
602
  }
524
603
 
525
604
  this.emit('registerSent', msg);
@@ -529,16 +608,10 @@ class ServiceRegistryClient extends EventEmitter {
529
608
  // which is what a legitimate registry rejection looks like (e.g. missing
530
609
  // validation proof). The caller must be able to tell "the registry said no"
531
610
  // from "the registry never answered" (architecture-principles §3, §4).
532
- console.log(`[RegistryClient] ${this.serviceName}: [AWAIT] Waiting for registration response promise...`);
533
- const response = await responsePromise;
534
- console.log(`[RegistryClient] ${this.serviceName}: [AWAIT] ✓ Promise resolved, response received:`, {
535
- hasResponse: !!response,
536
- responseType: typeof response,
537
- responseKeys: response ? Object.keys(response) : [],
538
- hasSuccess: !!response?.success,
539
- hasCertificate: !!response?.certificate
540
- });
541
- return response;
611
+ // No log around the await: the same event is already reported once, from
612
+ // _handleRegistryMessage ("Registration resolved"), and a timeout or an abort
613
+ // rejects with a message of its own.
614
+ return responsePromise;
542
615
  }
543
616
 
544
617
  /**
@@ -566,7 +639,21 @@ class ServiceRegistryClient extends EventEmitter {
566
639
 
567
640
  /**
568
641
  * Deregisters the service from the registry.
569
- * @returns {Promise<Object>} Deregistration result
642
+ *
643
+ * The registry sends NO reply to a `deregister`
644
+ * (api/docs/biz/30-operations/registration-wire.md §2.3 — `processDeregistration`
645
+ * removes the record and publishes `service.deregistered` to monitoring), so
646
+ * there is no verdict to report and none is invented. Until d.278c this method
647
+ * answered a hardcoded `{ success: true, message: 'Service deregistered
648
+ * successfully' }` whatever happened — a claim about the registry made by the
649
+ * one party that never heard from it.
650
+ *
651
+ * What this call knows, and therefore what it returns: the request it published,
652
+ * where it published it, and that the heartbeat timer is stopped. A publish that
653
+ * fails REJECTS; reaching the return means the broker took the message.
654
+ *
655
+ * @returns {Promise<{requestId: string, serviceName: string, version: string,
656
+ * publishedTo: string, heartbeatStopped: boolean, publishedAt: string}>}
570
657
  */
571
658
  async deregister() {
572
659
  const msg = {
@@ -577,15 +664,18 @@ class ServiceRegistryClient extends EventEmitter {
577
664
  timestamp: new Date().toISOString()
578
665
  };
579
666
 
580
- // Send deregistration message to registry
581
- // CRITICAL: Use queueConfig.js to get correct parameters (TTL, max-length, etc.)
582
- console.log(`[RegistryClient] [PUBLISH] Preparing to publish to ${this.registryQueue} (deregister)`);
583
667
  await this._ensureInfrastructureQueue(this.registryQueue);
584
- this.queueManager.channel.sendToQueue(
668
+ await this.queueManager.publish(
585
669
  this.registryQueue,
586
- Buffer.from(JSON.stringify(msg)),
670
+ msg,
587
671
  { persistent: true }
588
672
  );
673
+ this.logger.info('[RegistryClient] Deregistration request published', {
674
+ serviceName: this.serviceName,
675
+ version: this.version,
676
+ registryQueue: this.registryQueue,
677
+ requestId: msg.id
678
+ });
589
679
 
590
680
  this.emit('deregisterSent', msg);
591
681
 
@@ -593,10 +683,12 @@ class ServiceRegistryClient extends EventEmitter {
593
683
  this.stopHeartbeat();
594
684
 
595
685
  return {
596
- success: true,
597
- message: 'Service deregistered successfully',
686
+ requestId: msg.id,
598
687
  serviceName: this.serviceName,
599
- version: this.version
688
+ version: this.version,
689
+ publishedTo: this.registryQueue,
690
+ heartbeatStopped: this.heartbeatTimer === null,
691
+ publishedAt: msg.timestamp
600
692
  };
601
693
  }
602
694
 
@@ -623,14 +715,19 @@ class ServiceRegistryClient extends EventEmitter {
623
715
  specificationEndpoint: this.specificationEndpoint,
624
716
  timestamp: new Date().toISOString()
625
717
  };
626
- // CRITICAL: Use queueConfig.js to get correct parameters (TTL, max-length, etc.)
627
- console.log(`[RegistryClient] [PUBLISH] Preparing to publish to ${this.registryQueue} (heartbeat)`);
628
718
  await this._ensureInfrastructureQueue(this.registryQueue);
629
- this.queueManager.channel.sendToQueue(
719
+ await this.queueManager.publish(
630
720
  this.registryQueue,
631
- Buffer.from(JSON.stringify(msg)),
721
+ msg,
632
722
  { persistent: true }
633
723
  );
724
+ // debug, not info: this repeats every heartbeatInterval, so at
725
+ // info it would bury every other line this client writes.
726
+ this.logger.debug('[RegistryClient] Heartbeat sent', {
727
+ serviceName: this.serviceName,
728
+ version: this.version,
729
+ registryQueue: this.registryQueue
730
+ });
634
731
  this.emit('heartbeatSent', msg);
635
732
  }
636
733
 
@@ -640,7 +737,10 @@ class ServiceRegistryClient extends EventEmitter {
640
737
  startHeartbeat() {
641
738
  // Send immediately after init
642
739
  this.sendHeartbeat().catch(err => {
643
- console.error(`[RegistryClient] ${this.serviceName}: Initial heartbeat failed: ${err.message}`);
740
+ this.logger.error('[RegistryClient] Initial heartbeat failed', {
741
+ serviceName: this.serviceName,
742
+ error: err.message
743
+ });
644
744
  });
645
745
 
646
746
  // Repeat at the configured interval using a safe recursive timeout to avoid parallel runs
@@ -648,7 +748,10 @@ class ServiceRegistryClient extends EventEmitter {
648
748
  try {
649
749
  await this.sendHeartbeat();
650
750
  } catch (err) {
651
- console.error(`[RegistryClient] ${this.serviceName}: Heartbeat failed: ${err.message}`);
751
+ this.logger.error('[RegistryClient] Heartbeat failed', {
752
+ serviceName: this.serviceName,
753
+ error: err.message
754
+ });
652
755
  } finally {
653
756
  // Schedule next heartbeat only after previous one finished (or failed)
654
757
  if (this.heartbeatTimer !== null) {
@@ -684,7 +787,8 @@ class ServiceRegistryClient extends EventEmitter {
684
787
  queueManager: this.queueManager,
685
788
  serviceName: this.serviceName,
686
789
  redis: this.redis,
687
- storageConfig: this.storageConfig
790
+ storageConfig: this.storageConfig,
791
+ logger: this.logger
688
792
  });
689
793
 
690
794
  // Forward events from consumer
@@ -699,18 +803,6 @@ class ServiceRegistryClient extends EventEmitter {
699
803
  }
700
804
  }
701
805
 
702
- /**
703
- * Get service spec from event consumer (requires subscribeToChanges)
704
- * @param {string} serviceName - Name of the service
705
- * @returns {Promise<Object>} - Service specification
706
- */
707
- async getServiceSpec(serviceName) {
708
- if (!this.eventConsumer) {
709
- throw new Error('Event consumer not initialized. Call subscribeToChanges() first.');
710
- }
711
- return this.eventConsumer.getServiceSpec(serviceName);
712
- }
713
-
714
806
  /**
715
807
  * Get basic service state for service discovery.
716
808
  *
@@ -731,8 +823,16 @@ class ServiceRegistryClient extends EventEmitter {
731
823
  * cookbook-router expects `{ status }`; the registry writes it lowercase
732
824
  * ('active'/'inactive') into the summary, so no normalization happens here.
733
825
  *
826
+ * @typedef {Object} ServiceSummary
827
+ * @property {string} serviceName
828
+ * @property {string} status - lowercase, as the registry writes it ('active'/'inactive')
829
+ * @property {string} [version]
830
+ * @property {string} [lastHeartbeatAt]
831
+ */
832
+
833
+ /**
734
834
  * @param {string} serviceName
735
- * @returns {Promise<{serviceName: string, status: string, version?: string, lastHeartbeatAt?: string} | null>}
835
+ * @returns {Promise<ServiceSummary|null>}
736
836
  */
737
837
  async getService(serviceName) {
738
838
  if (!serviceName || typeof serviceName !== 'string') {
@@ -774,29 +874,6 @@ class ServiceRegistryClient extends EventEmitter {
774
874
  };
775
875
  }
776
876
 
777
- /**
778
- * Check if service is active (requires subscribeToChanges)
779
- * @param {string} serviceName - Name of the service
780
- * @returns {boolean}
781
- */
782
- isServiceActive(serviceName) {
783
- if (!this.eventConsumer) {
784
- throw new Error('Event consumer not initialized. Call subscribeToChanges() first.');
785
- }
786
- return this.eventConsumer.isServiceActive(serviceName);
787
- }
788
-
789
- /**
790
- * Get list of active services (requires subscribeToChanges)
791
- * @returns {Array<string>}
792
- */
793
- getActiveServices() {
794
- if (!this.eventConsumer) {
795
- return [];
796
- }
797
- return this.eventConsumer.getActiveServices();
798
- }
799
-
800
877
  /**
801
878
  * Releases resources: stops heartbeat and closes connection.
802
879
  * @returns {Promise<void>}