@onlineapps/conn-orch-registry 6.0.3 → 7.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.
@@ -31,7 +31,7 @@ Microservice Registry Backend
31
31
  > The legacy `apiDescriptionRequest` / `sendApiDescription` round-trip has
32
32
  > been removed — the full service specification (including the `operations`
33
33
  > map) travels as part of the `register` message. See
34
- > [operations-registry-contract.md §2](../../../../docs/biz/30-operations/registration-wire.md).
34
+ > [registration-wire.md §2.1](../../../../docs/biz/30-operations/registration-wire.md).
35
35
 
36
36
  ## Usage
37
37
 
@@ -131,7 +131,7 @@ against the Operations Registry contract and either confirms or rejects.
131
131
 
132
132
  **Parameters:**
133
133
  - `serviceInfo.endpoints` - Array of API endpoints
134
- - `serviceInfo.operations` - Operations map (see [operations-registry-contract.md §3](../../../../docs/biz/30-operations/registration-wire.md))
134
+ - `serviceInfo.operations` - Operations map (see [registration-wire.md §3](../../../../docs/biz/30-operations/registration-wire.md))
135
135
  - `serviceInfo.metadata` - Service metadata object
136
136
  - `serviceInfo.health` - Health check endpoint
137
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-registry",
3
- "version": "6.0.3",
3
+ "version": "7.0.0",
4
4
  "license": "MIT",
5
5
  "description": "conn-orch-registry provides the core communication mechanism for microservices in this environment. It enables them to interact with a services_registry to receive and fulfill tasks by submitting heartbeats or their API descriptions.",
6
6
  "keywords": [
@@ -18,7 +18,9 @@
18
18
  "docs/",
19
19
  "examples/",
20
20
  "README.md",
21
- "LICENSE"
21
+ "LICENSE",
22
+ "CHANGELOG.md",
23
+ "API.md"
22
24
  ],
23
25
  "oa": {
24
26
  "category": "orchestration"
@@ -37,7 +39,7 @@
37
39
  },
38
40
  "dependencies": {
39
41
  "@onlineapps/logger-contract": "2.0.0",
40
- "@onlineapps/mq-client-core": "4.2.0",
42
+ "@onlineapps/mq-client-core": "5.0.0",
41
43
  "uuid": "^9.0.1"
42
44
  },
43
45
  "devDependencies": {
@@ -68,6 +68,38 @@ const RETIRED_SERVICE_INFO_KEYS = Object.freeze({
68
68
  + 'destructure it, so deployability is read from that CI job and never from a registration message'
69
69
  });
70
70
 
71
+ /**
72
+ * The keys `register()` ACCEPTS — the whole input contract of the call (d.702).
73
+ *
74
+ * It is the caller's half of `api/docs/biz/30-operations/registration-wire.md`
75
+ * §2.1: the five register-message fields a CALLER supplies, plus `timeout`, which
76
+ * is this call's own response budget and reaches no message. The rest of §2.1
77
+ * (`id`, `type`, `serviceName`, `version`, `specificationEndpoint`,
78
+ * `responseQueue`, `timestamp`, `validationProof`, `validationData`) is client
79
+ * state bound in the constructor, never caller input — a caller cannot set it and
80
+ * this table must not invite one to try.
81
+ *
82
+ * Any other key is refused BY NAME, for the same reason the table above exists:
83
+ * until d.702 `register()` refused six retired keys and dropped everything else in
84
+ * silence, so `helth`, `current_step` or `name` bought a confirmed registration
85
+ * whose message never carried the value. A key silently dropped leaves the caller
86
+ * believing something travelled, and a typo in an optional key is exactly the
87
+ * shape nobody notices (`architecture-principles.md` §3 no fallbacks, §4
88
+ * fail-fast).
89
+ *
90
+ * `RETIRED_SERVICE_INFO_KEYS` is the subset that can still say WHY the key went;
91
+ * ONE membership check reads both tables, so there is no second list to drift
92
+ * (`.claude/rules/change-discipline.md` § One rail per concern).
93
+ */
94
+ const ACCEPTED_SERVICE_INFO_KEYS = Object.freeze({
95
+ endpoints: 'the `endpoints` array of the register message (§2.1); `[]` for every v3 service',
96
+ operations: 'the complete `operations` map the registry validates (§3)',
97
+ metadata: 'the `{ version, description }` object stored beside the spec (§2.1)',
98
+ health: 'the legacy health path of the register message; liveness travels by heartbeat (§2.2)',
99
+ workspaceScoped: 'the boolean emitted only when the service declares it (§2.1)',
100
+ timeout: 'how long THIS call waits for the registry verdict; it reaches no message'
101
+ });
102
+
71
103
 
72
104
  class ServiceRegistryClient extends EventEmitter {
73
105
  /**
@@ -95,7 +127,7 @@ class ServiceRegistryClient extends EventEmitter {
95
127
  * docs/governance/confirmations/connector-logger-contract.md 001 + 002.
96
128
  */
97
129
  constructor({ amqpUrl, serviceName, version, specificationEndpoint,
98
- heartbeatInterval, apiQueue = 'api_services_queuer', registryQueue = 'registry.register',
130
+ heartbeatInterval, instanceId, apiQueue = 'api_services_queuer', registryQueue = 'registry.register',
99
131
  redis = null, storageConfig = {}, validationProof = null, logger,
100
132
  registryKeyPrefix = DEFAULTS.registryKeyPrefix,
101
133
  registrationTimeoutMs = DEFAULTS.registrationTimeoutMs }) {
@@ -118,6 +150,32 @@ class ServiceRegistryClient extends EventEmitter {
118
150
  + `(module default ${DEFAULTS.registrationTimeoutMs}).`
119
151
  );
120
152
  }
153
+ // WHICH RUNNING COPY. The service NAME says which service; until d.817 nothing said
154
+ // which copy, so a short-lived foreign instance deregistered the live one and three
155
+ // services then beat into `unknown` for nine hours (INFRA batch 398, from production
156
+ // logs; owner decision `registry-instance-identity` 001).
157
+ //
158
+ // INJECTED, never made here (§1 DI, §3 no fallback): the wrapper makes ONE value per
159
+ // process at boot and hands the same one to `monitoring-core` as `serviceInstanceId`,
160
+ // so a second birthplace here would give one running copy two identities. Rule 11 —
161
+ // no default, no `|| uuidv4()`, no transition period.
162
+ if (typeof instanceId !== 'string' || instanceId === '') {
163
+ throw new Error(
164
+ `[RegistryClient] ${serviceName}: Missing instanceId - got ${JSON.stringify(instanceId)}. `
165
+ + 'Expected: a non-empty string naming THIS running copy of the service, the same value for the '
166
+ + 'whole process; the service name says which service, this says which copy. '
167
+ + 'Fix: pass the instance id the wrapper made once at boot (SERVICE_INSTANCE_ID when the '
168
+ + 'deployment gives one, otherwise a UUID v4) - this client never makes one, because the same '
169
+ + 'value also reaches monitoring-core as serviceInstanceId.'
170
+ );
171
+ }
172
+ this.instanceId = instanceId;
173
+ // Where the registry answers THIS copy's registration (conf
174
+ // `registry-instance-identity` 001 (c), d.817c) — carried as `responseQueue`.
175
+ // The shared `<service>.registry` (`serviceRegistryQueue`, set in `init()`)
176
+ // carries the registry's own prompts to every copy.
177
+ this.instanceResponseQueue = `${serviceName}.registry.${instanceId}`;
178
+
121
179
  this.registrationTimeoutMs = registrationTimeoutMs;
122
180
  this.serviceName = serviceName;
123
181
  this.version = version;
@@ -195,8 +253,42 @@ class ServiceRegistryClient extends EventEmitter {
195
253
  // agreeing today, and 406 PRECONDITION-FAILED the day the two drift apart
196
254
  // (`.claude/rules/change-discipline.md` § One rail per concern).
197
255
  const consumerStartTime = Date.now();
256
+ // TWO queues, two jobs (conf `registry-instance-identity` 001 (c), d.817c):
257
+ // - `<service>.registry`, shared by every copy — the registry's own prompts
258
+ // (`revalidate`; `api_services_registry` `revalidationRequester.js` sends to
259
+ // `<serviceName>.registry`);
260
+ // - `<service>.registry.<instance_id>` — the answers to THIS copy's registration,
261
+ // named in `register`'s `responseQueue`, so a live copy can no longer eat the
262
+ // confirmation meant for another.
263
+ // Both have a dead-letter route in `queueConfig` (the per-instance template landed
264
+ // first, d.817b — the order conf 002 made binding), so `consume()` accepts both.
198
265
  this.serviceRegistryQueue = `${this.serviceName}.registry`;
199
266
 
267
+ for (const queue of [this.serviceRegistryQueue, this.instanceResponseQueue]) {
268
+ await this._startRegistryConsumer(queue);
269
+ }
270
+
271
+ // One line for the whole boot of this client: which queues it talks on, and how
272
+ // long the consumers took to attach. Replaces the four FÁZE 0.5/0.6 markers.
273
+ this.logger.info('[RegistryClient] Initialized', {
274
+ serviceName: this.serviceName,
275
+ version: this.version,
276
+ registryQueue: this.registryQueue,
277
+ promptQueue: this.serviceRegistryQueue,
278
+ responseQueue: this.instanceResponseQueue,
279
+ consumerStartupMs: Date.now() - consumerStartTime
280
+ });
281
+ }
282
+
283
+ /**
284
+ * Attach this client's handler to one registry queue, within a bounded wait.
285
+ *
286
+ * @param {string} queue - `<service>.registry` or `<service>.registry.<instance_id>`
287
+ * @returns {Promise<void>}
288
+ * @throws {Error} naming the queue, carrying the source's classification
289
+ * @private
290
+ */
291
+ async _startRegistryConsumer(queue) {
200
292
  const CONSUME_TIMEOUT = 5000; // 5 seconds
201
293
  // `requeueOnError: false` is the budget this consumer has always had, said in
202
294
  // the core's word: ONE attempt. The difference is where a failure goes — the
@@ -206,7 +298,7 @@ class ServiceRegistryClient extends EventEmitter {
206
298
  // nothing itself (@onlineapps/mq-client-core README § consume() — the delivery
207
299
  // contract).
208
300
  const consumeRegistryPromise = this.queueManager.consume(
209
- this.serviceRegistryQueue,
301
+ queue,
210
302
  msg => this._handleRegistryMessage(msg),
211
303
  { requeueOnError: false }
212
304
  );
@@ -214,7 +306,7 @@ class ServiceRegistryClient extends EventEmitter {
214
306
  const consumeRegistryTimeoutPromise = new Promise((_, reject) => {
215
307
  consumeRegistryTimeout = setTimeout(() => {
216
308
  reject(new Error(
217
- `[RegistryClient] consume() timeout for ${this.serviceRegistryQueue} after ${CONSUME_TIMEOUT}ms - `
309
+ `[RegistryClient] consume() timeout for ${queue} after ${CONSUME_TIMEOUT}ms - `
218
310
  + 'the broker never confirmed the consumer. Fix: check RabbitMQ responsiveness and that the queue exists.'
219
311
  ));
220
312
  }, CONSUME_TIMEOUT);
@@ -234,7 +326,7 @@ class ServiceRegistryClient extends EventEmitter {
234
326
  // { cause })` carries no own property, so the marker used to die here.
235
327
  // Nothing is classified HERE — what the source left unmarked stays unmarked.
236
328
  const refusal = new Error(
237
- `[RegistryClient] ${this.serviceName}: Failed to start consumer on ${this.serviceRegistryQueue} - `
329
+ `[RegistryClient] ${this.serviceName}: Failed to start consumer on ${queue} - `
238
330
  + `${consumeErr.message}. Fix: check RabbitMQ health; without this consumer no registry reply is ever read.`,
239
331
  { cause: consumeErr }
240
332
  );
@@ -244,16 +336,6 @@ class ServiceRegistryClient extends EventEmitter {
244
336
  } finally {
245
337
  if (consumeRegistryTimeout) clearTimeout(consumeRegistryTimeout);
246
338
  }
247
-
248
- // One line for the whole boot of this client: which queues it talks on, and how
249
- // long the consumer took to attach. Replaces the four FÁZE 0.5/0.6 markers.
250
- this.logger.info('[RegistryClient] Initialized', {
251
- serviceName: this.serviceName,
252
- version: this.version,
253
- registryQueue: this.registryQueue,
254
- responseQueue: this.serviceRegistryQueue,
255
- consumerStartupMs: Date.now() - consumerStartTime
256
- });
257
339
  }
258
340
 
259
341
  /**
@@ -468,11 +550,17 @@ class ServiceRegistryClient extends EventEmitter {
468
550
  * Registers the service with the registry.
469
551
  * Registry will validate if the service is properly tested and valid.
470
552
  * Upon successful validation, the service will be activated and can start heartbeats.
471
- * @param {Object} serviceInfo - Service registration information
472
- * @param {Array} serviceInfo.endpoints - API endpoints provided by the service
473
- * @param {Object} serviceInfo.metadata - Additional service metadata
474
- * @param {string} serviceInfo.health - Health check endpoint
475
- * @param {number} serviceInfo.timeout - Timeout for registration response (default: 30000ms)
553
+ * @param {Object} serviceInfo - Service registration information. It accepts EXACTLY the keys
554
+ * below (`ACCEPTED_SERVICE_INFO_KEYS`) — any other key is refused by name, and a retired one
555
+ * (`RETIRED_SERVICE_INFO_KEYS`) with the reason it went. Nothing is dropped in silence: a key
556
+ * this call ignored would leave the caller believing it travelled.
557
+ * @param {Array} [serviceInfo.endpoints] - API endpoints provided by the service; `[]` for v3 services
558
+ * @param {Object} [serviceInfo.operations] - The complete `operations` map the registry validates
559
+ * @param {Object} [serviceInfo.metadata] - `{ version, description }` stored beside the spec
560
+ * @param {string} [serviceInfo.health] - Legacy health path; liveness travels by heartbeat
561
+ * @param {boolean} [serviceInfo.workspaceScoped] - Emitted only when the service declares it
562
+ * @param {number} [serviceInfo.timeout] - How long THIS call waits for the registry verdict
563
+ * (default: the constructor's registrationTimeoutMs). It reaches no message.
476
564
  * @returns {Promise<{success: boolean, message: string, serviceName: string, version: string,
477
565
  * registrationId: (string|undefined), validated: boolean, certificate: (Object|null),
478
566
  * errorCode: (string|null|undefined)}>} The registry verdict. `errorCode` is the
@@ -492,9 +580,12 @@ class ServiceRegistryClient extends EventEmitter {
492
580
  );
493
581
  }
494
582
 
495
- // A retired key is refused BY NAME, before anything is published: dropping it
496
- // in silence would leave the caller believing it travelled. The value is never
497
- // echoed - two of these keys carried a secret.
583
+ // ONE membership check over the two tables above, before anything is published.
584
+ // A key the contract does not name is refused BY NAME: dropping it in silence
585
+ // would leave the caller believing it travelled. A retired key keeps its own
586
+ // reason - it is the subset that can still say what replaced it. Neither
587
+ // refusal echoes the value: two of the retired keys carried a secret, and an
588
+ // unknown key can carry anything.
498
589
  for (const key of Object.keys(serviceInfo)) {
499
590
  if (Object.prototype.hasOwnProperty.call(RETIRED_SERVICE_INFO_KEYS, key)) {
500
591
  throw new Error(
@@ -503,6 +594,15 @@ class ServiceRegistryClient extends EventEmitter {
503
594
  + `Fix: delete "${key}" from the object passed to register().`
504
595
  );
505
596
  }
597
+ if (!Object.prototype.hasOwnProperty.call(ACCEPTED_SERVICE_INFO_KEYS, key)) {
598
+ throw new Error(
599
+ `[RegistryClient] ${this.serviceName}: serviceInfo key "${key}" is not part of the registration `
600
+ + `contract - Expected one of: ${Object.keys(ACCEPTED_SERVICE_INFO_KEYS).join(', ')} `
601
+ + '(api/docs/biz/30-operations/registration-wire.md §2.1). '
602
+ + `Fix: delete "${key}" from the object passed to register(), or send the value under the key `
603
+ + 'the contract names for it.'
604
+ );
605
+ }
506
606
  }
507
607
 
508
608
  // FAIL-FAST: a client that was never initialized, or that has been closed, is not
@@ -519,9 +619,32 @@ class ServiceRegistryClient extends EventEmitter {
519
619
  }
520
620
 
521
621
  const msgId = uuidv4();
622
+
623
+ // FAIL-FAST: one request id, one pending registration. `pendingRegistrations` and
624
+ // `pendingTimeouts` are keyed by this id, and until d.760b a second call under an id
625
+ // that was still pending simply set() over both - which dropped the earlier timer's
626
+ // handle. Nothing could reach it afterwards: the registry's answer clears by id
627
+ // (_handleRegistryMessage), and close() walks the table, so both found only the newer
628
+ // handle and the older timer ran to its full 30 s. Cancelling the earlier registration
629
+ // here instead would be a fallback (§3): its caller is still awaiting an answer that
630
+ // belongs to it, and this class cannot know which of the two calls is the mistake.
631
+ if (this.pendingRegistrations && this.pendingRegistrations.has(msgId)) {
632
+ throw new Error(
633
+ `[RegistryClient] ${this.serviceName}: Duplicate request id "${msgId}" - a registration with this id `
634
+ + 'is still pending. Expected: one register() in flight per request id; it is settled by the registry '
635
+ + `answer, by the ${this.registrationTimeoutMs}ms timeout or by close(). `
636
+ + 'Fix: await the register() already in flight before starting another one, and - if the ids repeat '
637
+ + 'because uuid.v4 is stubbed - give each registration its own id.'
638
+ );
639
+ }
640
+
522
641
  // @see api/docs/biz/30-operations/registration-wire.md §2 MQ wire format
523
642
  const msg = {
524
643
  id: msgId,
644
+ // WHICH REQUEST (`id`, per call) and WHICH COPY (`instance_id`, per process) are
645
+ // two identities with two lifetimes — d.817, owner `registry-instance-identity` 001.
646
+ // snake_case is the one spelling the wire has (`workflow-message-aliases` 001).
647
+ instance_id: this.instanceId,
525
648
  type: 'register',
526
649
  serviceName: this.serviceName,
527
650
  version: this.version,
@@ -530,7 +653,7 @@ class ServiceRegistryClient extends EventEmitter {
530
653
  operations: serviceInfo.operations || {},
531
654
  metadata: serviceInfo.metadata || {},
532
655
  health: serviceInfo.health || '/health',
533
- responseQueue: this.serviceRegistryQueue, // Queue for registry to send response
656
+ responseQueue: this.instanceResponseQueue, // THIS copy's answer queue (conf registry-instance-identity 001 (c))
534
657
  timestamp: new Date().toISOString()
535
658
  };
536
659
 
@@ -572,7 +695,7 @@ class ServiceRegistryClient extends EventEmitter {
572
695
  reject(new Error(
573
696
  `[RegistryClient] ${this.serviceName}: Registration timed out - no response from the registry `
574
697
  + `within ${timeout}ms. Expected: api_services_registry consumes '${this.registryQueue}' and answers `
575
- + `on '${this.serviceRegistryQueue}'. Fix: start api_services_registry (docker compose up -d `
698
+ + `on '${this.instanceResponseQueue}'. Fix: start api_services_registry (docker compose up -d `
576
699
  + 'api_services_registry) or raise registrationTimeoutMs.'
577
700
  ));
578
701
  }
@@ -627,7 +750,7 @@ class ServiceRegistryClient extends EventEmitter {
627
750
  serviceName: this.serviceName,
628
751
  version: this.version,
629
752
  registryQueue: this.registryQueue,
630
- responseQueue: this.serviceRegistryQueue,
753
+ responseQueue: this.instanceResponseQueue,
631
754
  requestId: msgId,
632
755
  hasValidationProof: !!this.validationProof,
633
756
  proofHash: this.validationProof
@@ -701,6 +824,9 @@ class ServiceRegistryClient extends EventEmitter {
701
824
  id: uuidv4(),
702
825
  type: 'deregister',
703
826
  serviceName: this.serviceName,
827
+ // The reason this key exists at all: the registry deletes only on a match, so a
828
+ // short-lived foreign copy can no longer deregister the live one (d.817).
829
+ instance_id: this.instanceId,
704
830
  version: this.version,
705
831
  timestamp: new Date().toISOString()
706
832
  };
@@ -752,6 +878,9 @@ class ServiceRegistryClient extends EventEmitter {
752
878
  id: uuidv4(),
753
879
  type: 'heartbeat',
754
880
  serviceName: this.serviceName,
881
+ // The beat says which copy is alive, so the registry can keep its availability
882
+ // episodes per instance instead of per name (d.817, INFRA half).
883
+ instance_id: this.instanceId,
755
884
  version: this.version,
756
885
  specificationEndpoint: this.specificationEndpoint,
757
886
  timestamp: new Date().toISOString()
@@ -801,6 +930,24 @@ class ServiceRegistryClient extends EventEmitter {
801
930
  * budget is spent. The loop is already stopped when it is called.
802
931
  */
803
932
  startHeartbeat(options) {
933
+ // FAIL-FAST: one client, one loop. Checked before the options, because the state
934
+ // of the receiver is the problem here whatever the arguments say. Until d.760c the
935
+ // line at the end of this method wrote the new handle over `heartbeatTimer` and the
936
+ // earlier loop lost the only reference to itself: it re-arms while that field is
937
+ // not null, and the field now belonged to the newer loop, so `stopHeartbeat()`
938
+ // cancelled one loop and the other went on beating for a service that had stopped.
939
+ // Replacing the running loop silently would be a fallback (§3) - the caller that
940
+ // started it is not asking for it to be discarded, and this class cannot know which
941
+ // of the two callers is the mistake.
942
+ if (this.heartbeatTimer) {
943
+ throw new Error(
944
+ `[RegistryClient] ${this.serviceName}: A heartbeat loop is already running - starting a second one would `
945
+ + 'overwrite the timer handle of the first, which would then beat on with nothing able to stop it. '
946
+ + 'Expected: one heartbeat loop per client; stop it with stopHeartbeat() first. '
947
+ + 'Fix: call client.stopHeartbeat() before starting a new loop, or keep the loop that is already running.'
948
+ );
949
+ }
950
+
804
951
  if (options === null || typeof options !== 'object' || Array.isArray(options)) {
805
952
  throw new Error(
806
953
  `[RegistryClient] ${this.serviceName}: Missing heartbeat options - startHeartbeat({ maxFailures, onFatal }) `
@@ -886,12 +1033,21 @@ class ServiceRegistryClient extends EventEmitter {
886
1033
 
887
1034
  /**
888
1035
  * Stops periodic heartbeat messages.
1036
+ *
1037
+ * One scheduler, one cancel: `startHeartbeat()` arms the loop with a recursive
1038
+ * `setTimeout` and nothing in this class has created an interval since that
1039
+ * rewrite (d.503), so `clearTimeout` is the whole job. A `clearInterval` on the
1040
+ * same handle stood beside it until d.752, under a comment offering to cancel
1041
+ * "whether it's from setTimeout or setInterval" — a choice the class does not
1042
+ * have, and one that told every reader the loop might be an interval.
1043
+ *
1044
+ * Setting `heartbeatTimer` to null is the second half and not a formality: the
1045
+ * loop reads it before arming the next beat, so a beat already in flight sees
1046
+ * the stop and schedules nothing.
889
1047
  */
890
1048
  stopHeartbeat() {
891
1049
  if (this.heartbeatTimer) {
892
- // Clear timeout regardless of whether it's from setTimeout or setInterval
893
1050
  clearTimeout(this.heartbeatTimer);
894
- clearInterval(this.heartbeatTimer);
895
1051
  this.heartbeatTimer = null;
896
1052
  }
897
1053
  }
@@ -10,6 +10,7 @@
10
10
 
11
11
  const EventEmitter = require('events');
12
12
  const { assertLogger } = require('@onlineapps/logger-contract');
13
+ const { queueConfig } = require('@onlineapps/mq-client-core');
13
14
 
14
15
  // The storage connector arrives by INJECTION. There used to be an implicit
15
16
  // default here — `require('@onlineapps/connector-storage')` inside a try/catch
@@ -88,8 +89,19 @@ class RegistryEventConsumer extends EventEmitter {
88
89
  // Reusing it would cause 406 PRECONDITION_FAILED when different queue args (TTL/max-length) are applied.
89
90
  this.eventsQueueName = `${serviceName}.registry.events`;
90
91
 
91
- // Exchange name for registry events
92
- this.exchangeName = 'registry.changes';
92
+ // Exchange name for registry events - from the library that declares the whole
93
+ // registry section of the topology, never spelled here. Until d.809 this was the
94
+ // one wire name of this package written twice: a rename in `queueConfig` would
95
+ // have left this consumer subscribed to an exchange nobody publishes to, with
96
+ // nothing red anywhere (`change-discipline.md` § One rail per concern).
97
+ //
98
+ // Only the NAME. The pinned 4.0.1 declares `registry.changes` as
99
+ // `{ type: 'exchange', durable: true }` with no `exchangeType`; the tree has
100
+ // `exchangeType: 'fanout'` (d.712) and is unreleased, so reading the type from the
101
+ // library today would read `undefined` and defaulting it here would be a fallback
102
+ // (§3). The `'fanout'` in subscribeToChanges() therefore stays a literal until
103
+ // mq-client-core is released.
104
+ this.exchangeName = queueConfig.queueName('registry', 'changes');
93
105
  }
94
106
 
95
107
  /**