@onlineapps/mq-client-core 2.0.1-rc.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.
@@ -3,51 +3,432 @@
3
3
  /**
4
4
  * configSchema.js
5
5
  *
6
- * JSON Schema used by Ajv to validate the user-supplied configuration object.
7
- *
8
- * FLEXIBLE SCHEMA for infrastructure services:
9
- * - Only requires 'type' and 'host'
10
- * - Allows additional properties for flexibility
11
- * - Queue is optional (infrastructure services may not need default queue)
6
+ * JSON Schema used by Ajv (in the `BaseClient` constructor) to validate the
7
+ * configuration object a caller hands this client.
8
+ *
9
+ * WHAT A DECLARATION IS FOR, and what it is not.
10
+ *
11
+ * It is the place a reader looks up what a key means, and the place a validator
12
+ * checks the value against. Until 2026-09-13 it was neither for most of the
13
+ * client: `src/` read 43 keys off `this._config`, this file declared 10, and the
14
+ * remaining 34 reached their mechanism only through `additionalProperties: true`
15
+ * — a door that was labelled "for flexibility" and that admitted `onFatal`
16
+ * (whether a spent reconnect budget ends the process), `recoveryScope` (which
17
+ * kind of service this client is) and `queueCreationCallback` (which, until
18
+ * d.419, declared a queue nobody owned). Undeclared is not merely undocumented:
19
+ * `reconnectEnabled: 'no'` was accepted, and the transport read it as
20
+ * `'no' !== false` → recovery ON, the exact opposite of what the caller wrote
21
+ * (`transports/rabbitmqClient.js`, `_reconnectEnabled`). Declaring the type is
22
+ * what turns that into a refusal at construction (`architecture-principles.md`
23
+ * §3, §4).
24
+ *
25
+ * A NAME THAT IS NOT A KEY IS NOT DECLARED HERE EITHER. `url` (the name
26
+ * `@onlineapps/conn-infra-mq` documents for the broker URL) and `queue` (a
27
+ * "default queue name" nothing has read since the library was extracted) were
28
+ * declared here until 2026-09-14, which made a reader believe both were inputs.
29
+ * Removing them from `properties` is now a REFUSAL and not merely a silence:
30
+ * the schema ends with `additionalProperties: false` since d.291b, so a name no
31
+ * side declares is rejected at construction, named, together with the declared
32
+ * key it is closest to.
33
+ *
34
+ * `url` is refused BY NAME in `BaseClient` (`NOT_A_KEY_OF_THIS_CLIENT`) all the
35
+ * same, and that is not a second rail for the same concern: the schema can say
36
+ * "this is not a key", it cannot say WHICH key the value belongs under, which an
37
+ * Ajv `not` cannot word either. The named list runs first and answers the caller
38
+ * with the rename; everything it does not know falls through to the schema. Same
39
+ * order, and the same reason, as `assertLogger()` running before Ajv.
40
+ *
41
+ * DEFAULTS ARE NOT DECLARED HERE — one concern, one rail. Every default already
42
+ * has an owner, and a second copy here would be free to drift from it:
43
+ * - `config/defaultConfig.js` — every CONSTANT default, composed into the config
44
+ * BEFORE this validation runs, so what it says is what is validated and built;
45
+ * - `../config.js` + `../defaults.js` — the runtime-resolved ones (`host`,
46
+ * `heartbeat`, `serviceName`, `maxReconnectAttempts`, `maxDeliveryAttempts`),
47
+ * explicit → env → module default;
48
+ * - `transports/rabbitmqClient.js` — `reconnectWaitTimeout` alone, because it is
49
+ * DERIVED from two other settings rather than being a constant.
50
+ * So a property here carries a type, a description and where its default comes
51
+ * from — never the default itself. `tests/unit/config-schema-declares-every-key-src-reads.test.js`
52
+ * holds both halves of that: no property carries `default`, and no key read in
53
+ * `src/` is missing from `properties`. Ajv is compiled WITHOUT `useDefaults` since
54
+ * d.292 §7 — the switch had nothing to write and was an invitation to open a
55
+ * second rail of defaults right here.
56
+ *
57
+ * A FUNCTION HAS NO JSON TYPE. Several keys are callbacks (or `null` for "none"),
58
+ * and JSON Schema's seven types are the JSON ones — `function` is not among them.
59
+ * The constraint is therefore written as "none of the JSON types", which admits
60
+ * exactly a function or `null` and rejects a string, number, object or array
61
+ * (`callbackOrNull()` below). Not a formality: a `healthReportCallback` that is a
62
+ * string would be awaited at the moment the client is already unwell, and an
63
+ * `onFatal` that is a string would be called at the moment the reconnect budget
64
+ * is already spent.
65
+ *
66
+ * @see ../../README.md § Configuration
12
67
  */
13
68
 
69
+ /**
70
+ * A callback key: a function, or `null` meaning "none".
71
+ *
72
+ * Written as six single-type alternatives rather than one type array because
73
+ * Ajv's strict mode warns about a union type inside `not` (`allowUnionTypes`),
74
+ * and a console warning on every client construction is noise in every service's
75
+ * boot log.
76
+ *
77
+ * @param {string} description - What the callback is called with, and by whom.
78
+ * @returns {Object} JSON Schema fragment.
79
+ */
80
+ const callbackOrNull = (description) => ({
81
+ not: {
82
+ anyOf: [
83
+ { type: 'boolean' }, { type: 'object' }, { type: 'array' },
84
+ { type: 'number' }, { type: 'string' }, { type: 'integer' },
85
+ ],
86
+ },
87
+ description,
88
+ });
89
+
14
90
  module.exports = {
15
91
  type: 'object',
16
92
  properties: {
93
+ // ---------------------------------------------------------------------
94
+ // Connection shape. Defaults: config/defaultConfig.js (merged before this
95
+ // validation runs); `host` is resolved by ../config.js and never defaulted.
96
+ // ---------------------------------------------------------------------
17
97
  type: {
18
98
  type: 'string',
19
99
  enum: ['rabbitmq'],
100
+ description:
101
+ "Transport to build. The only implemented one is 'rabbitmq' "
102
+ + '(transports/transportFactory.js).',
20
103
  },
21
104
  host: {
22
105
  type: 'string',
23
106
  minLength: 1,
24
- },
25
- // Queue is optional for infrastructure services
26
- queue: {
27
- type: 'string',
107
+ description:
108
+ 'Broker URL (it carries the account password, and only the redacted form reaches '
109
+ + 'the log — utils/redactCredentials.js). Explicit value, else ENV RABBITMQ_URL; '
110
+ + 'never defaulted, missing is fatal (../config.js).',
28
111
  },
29
112
  exchange: {
30
113
  type: 'string',
114
+ description:
115
+ "Default exchange for publish(); empty string means the default direct exchange. "
116
+ + 'Per-call `options.exchange` wins.',
31
117
  },
32
118
  durable: {
33
119
  type: 'boolean',
120
+ description:
121
+ 'Declare queues/exchanges as durable, and publish persistently unless a call '
122
+ + 'overrides it with `options.persistent`.',
34
123
  },
35
124
  prefetch: {
36
125
  type: 'integer',
37
126
  minimum: 0,
127
+ description: 'Default prefetch count for consumers; 0 means unlimited.',
38
128
  },
39
129
  noAck: {
40
130
  type: 'boolean',
131
+ description:
132
+ 'Default auto-acknowledge setting for consumers. `true` means the broker considers '
133
+ + 'a message delivered the moment it is sent, so a handler failure loses it.',
41
134
  },
135
+ heartbeat: {
136
+ type: 'integer',
137
+ minimum: 0,
138
+ description:
139
+ 'AMQP heartbeat in SECONDS, handed to amqplib at connect. Explicit value, else ENV '
140
+ + 'RABBITMQ_HEARTBEAT, else the module default (../defaults.js heartbeatSeconds).',
141
+ },
142
+ serviceName: {
143
+ type: 'string',
144
+ minLength: 1,
145
+ description:
146
+ 'Name this client publishes under and names its queues with. Explicit value, else '
147
+ + 'ENV SERVICE_NAME, else the module default (../defaults.js serviceName).',
148
+ },
149
+ connectionName: {
150
+ type: 'string',
151
+ minLength: 1,
152
+ description:
153
+ 'Name shown against this connection in the broker UI. Built by the BaseClient '
154
+ + 'constructor as `<serviceName>:<pid>` when the caller passes none.',
155
+ },
156
+ clientName: {
157
+ type: 'string',
158
+ minLength: 1,
159
+ description:
160
+ 'Fallback for `connectionName`, read by the transport when neither the caller nor '
161
+ + 'the BaseClient constructor set one.',
162
+ },
163
+ // REQUIRED, and not by this schema: presence and completeness are checked by
164
+ // `assertLogger()` from `@onlineapps/logger-contract` at the top of the
165
+ // BaseClient constructor, which names the four methods and the fix. Ajv would
166
+ // only be able to say "must have required property 'logger'", and two rails
167
+ // enforcing one contract is what confirmation 004 closed. The entry stays here
168
+ // because the schema is where a reader looks up what a key means.
42
169
  logger: {
43
- oneOf: [
44
- { type: 'null' },
45
- { type: 'object' }
46
- ],
47
- description: 'Custom logger with methods: info, warn, error, debug (can be null)',
170
+ type: 'object',
171
+ description:
172
+ 'Logger with methods: info, warn, error, debug — required, validated by '
173
+ + '@onlineapps/logger-contract in the BaseClient constructor',
174
+ },
175
+
176
+ // ---------------------------------------------------------------------
177
+ // Connection-level recovery. Read by the transport constructor; the defaults
178
+ // live there (and in ../defaults.js for the budget).
179
+ // ---------------------------------------------------------------------
180
+ maxReconnectAttempts: {
181
+ type: 'integer',
182
+ minimum: 0,
183
+ description:
184
+ 'How many times connection-level recovery retries before the connection is declared '
185
+ + 'permanently lost and `onFatal` fires. Explicit value, else ENV '
186
+ + 'RABBITMQ_MAX_RECONNECT_ATTEMPTS, else the module default (../defaults.js).',
187
+ },
188
+ maxDeliveryAttempts: {
189
+ type: 'integer',
190
+ minimum: 1,
191
+ description:
192
+ 'How many times a consumer\'s handler may run for ONE message before it is rejected '
193
+ + 'into `<svc>.dlq`, for every consumer of this client. Explicit value, else ENV '
194
+ + 'RABBITMQ_MAX_DELIVERY_ATTEMPTS, else the module default (../defaults.js); the '
195
+ + 'narrowest override is per call, `consume(queue, handler, { maxAttempts })`. '
196
+ + 'Below 1 would mean the handler never runs, which is not consuming at all.',
197
+ },
198
+ maxReconnectCycles: {
199
+ type: 'integer',
200
+ minimum: 1,
201
+ description:
202
+ 'How many recovery CYCLES the client spends before the connection is declared '
203
+ + 'permanently lost. One cycle is the whole maxReconnectAttempts budget; the first '
204
+ + 'is started by the death of the connection, every further one lazily by the next '
205
+ + 'use (publish/consume/performHealthCheck). Explicit value, else ENV '
206
+ + 'RABBITMQ_MAX_RECONNECT_CYCLES, else the module default (../defaults.js). Below 1 '
207
+ + 'would mean no recovery at all, which is what reconnectEnabled: false says.',
208
+ },
209
+ reconnectEnabled: {
210
+ type: 'boolean',
211
+ description:
212
+ 'Whether a lost connection is recovered at all. Anything other than `false` means '
213
+ + 'enabled, which is why the type matters: a string was read as "enabled".',
214
+ },
215
+ reconnectBaseDelay: {
216
+ type: 'integer',
217
+ minimum: 0,
218
+ description: 'First backoff step between reconnect attempts, in milliseconds.',
219
+ },
220
+ reconnectMaxDelay: {
221
+ type: 'integer',
222
+ minimum: 0,
223
+ description: 'Ceiling for the reconnect backoff, in milliseconds.',
224
+ },
225
+ reconnectWaitTimeout: {
226
+ type: 'integer',
227
+ minimum: 0,
228
+ description:
229
+ 'How long an operation waits for recovery to finish before giving up, in '
230
+ + 'milliseconds. Defaulted by the transport from the budget × the max delay.',
231
+ },
232
+ onFatal: callbackOrNull(
233
+ 'Function (error) => void, or null. Called ONCE when the reconnect budget is spent '
234
+ + 'and this client will not try again. The library never ends the process — that '
235
+ + 'decision belongs to the owner of the lifecycle. Whether the value is callable is '
236
+ + 'checked by the transport constructor, which names the key and the fix '
237
+ + '(transports/rabbitmqClient.js, "Invalid config value for \\"onFatal\\""); this '
238
+ + 'entry declares the key, it does not open a second rail for that check.'
239
+ ),
240
+
241
+ // ---------------------------------------------------------------------
242
+ // Publish retry + confirmation. Read by the transport constructor.
243
+ // ---------------------------------------------------------------------
244
+ publishRetryEnabled: {
245
+ type: 'boolean',
246
+ description:
247
+ 'Whether a failed publish is retried. Anything other than `false` means enabled.',
248
+ },
249
+ publishMaxRetries: {
250
+ type: 'integer',
251
+ minimum: 0,
252
+ description: 'How many publish attempts one message gets before the publish fails.',
253
+ },
254
+ publishRetryBaseDelay: {
255
+ type: 'integer',
256
+ minimum: 0,
257
+ description: 'First backoff step between publish attempts, in milliseconds.',
258
+ },
259
+ publishRetryMaxDelay: {
260
+ type: 'integer',
261
+ minimum: 0,
262
+ description: 'Ceiling for the publish backoff, in milliseconds.',
263
+ },
264
+ publishRetryBackoffMultiplier: {
265
+ type: 'number',
266
+ exclusiveMinimum: 0,
267
+ description: 'Factor each publish backoff step is multiplied by (2 = exponential).',
268
+ },
269
+ brokerAnswerTimeout: {
270
+ type: 'integer',
271
+ minimum: 0,
272
+ description:
273
+ 'How long this client waits for an ANSWER from the broker, in milliseconds: the confirm of '
274
+ + 'one publish attempt, and the close-ok of each handle disconnect() closes. One number, two '
275
+ + 'consumers — it was named publishConfirmationTimeout until d.299, after the teardown had '
276
+ + 'been spending it since d.283.',
277
+ },
278
+ connectTimeout: {
279
+ type: 'integer',
280
+ minimum: 1,
281
+ description:
282
+ 'How long the connect handshake may take before the attempt is abandoned, in milliseconds. '
283
+ + 'Applies to the first connect and to every reconnect.',
284
+ },
285
+ publishConfirmWatchdogDelay: {
286
+ type: 'integer',
287
+ minimum: 0,
288
+ description:
289
+ 'How long after sendToQueue() one publish attempt waits before checking whether the missing '
290
+ + 'confirm is a dead channel rather than a slow broker, in milliseconds.',
291
+ },
292
+
293
+ // ---------------------------------------------------------------------
294
+ // Publish buffer (layers/PublishLayer.js → buffer/MessageBuffer.js).
295
+ // ---------------------------------------------------------------------
296
+ publishBufferMaxSize: {
297
+ type: 'integer',
298
+ minimum: 0,
299
+ description: 'How many messages the in-memory publish buffer holds while disconnected.',
300
+ },
301
+ publishBufferTtlMs: {
302
+ type: 'integer',
303
+ minimum: 0,
304
+ description: 'How long a buffered message stays publishable, in milliseconds.',
305
+ },
306
+ // NO `persistentBufferEnabled` / `persistentRedisClient`. They declared a
307
+ // persistent publish buffer this package never had: `buffer/RedisBuffer.js`
308
+ // stored and returned nothing, so `true` dropped the critical messages it
309
+ // claimed to protect. Both names are refused BY NAME in `BaseClient`
310
+ // (`NOT_A_KEY_OF_THIS_CLIENT`), for the reason two paragraphs up — the
311
+ // schema can only say the name is not a key, while the named list can say
312
+ // the mechanism itself is gone and what stands in its place (d.343).
313
+
314
+ // ---------------------------------------------------------------------
315
+ // Channel thrashing detection. Read by the transport constructor.
316
+ // ---------------------------------------------------------------------
317
+ thrashingThreshold: {
318
+ type: 'integer',
319
+ minimum: 0,
320
+ description: 'How many closes of one channel type within the window count as thrashing.',
321
+ },
322
+ thrashingWindowMs: {
323
+ type: 'integer',
324
+ minimum: 0,
325
+ description: 'Width of the window the thrashing threshold is counted over, in milliseconds.',
326
+ },
327
+ thrashingAlertCallback: callbackOrNull(
328
+ 'Function (type, count, windowMs) => void, or null. Called once per channel type when '
329
+ + 'the close rate crosses thrashingThreshold.'
330
+ ),
331
+
332
+ // ---------------------------------------------------------------------
333
+ // Prefetch utilization monitoring. Read by the transport constructor.
334
+ // ---------------------------------------------------------------------
335
+ prefetchUtilizationThreshold: {
336
+ type: 'number',
337
+ exclusiveMinimum: 0,
338
+ maximum: 1,
339
+ description:
340
+ 'Fraction of the prefetch window in flight (0.8 = 80%) above which the alert '
341
+ + 'callback is called for that queue.',
342
+ },
343
+ prefetchCheckInterval: {
344
+ type: 'integer',
345
+ minimum: 0,
346
+ description: 'How often prefetch utilization is sampled, in milliseconds.',
347
+ },
348
+ prefetchAlertCallback: callbackOrNull(
349
+ 'Function (queue, utilization, inFlight, prefetchCount) => void, or null. Called when '
350
+ + 'a queue crosses prefetchUtilizationThreshold.'
351
+ ),
352
+
353
+ // ---------------------------------------------------------------------
354
+ // Health monitoring. The periodic loop is OFF for business services — see
355
+ // `@onlineapps/service-wrapper` `_initializeMQ()` for why.
356
+ // ---------------------------------------------------------------------
357
+ healthCheckEnabled: {
358
+ type: 'boolean',
359
+ description:
360
+ 'Whether the transport runs its periodic health check. Anything other than `false` '
361
+ + 'means enabled.',
362
+ },
363
+ healthCheckInterval: {
364
+ type: 'integer',
365
+ minimum: 0,
366
+ description: 'Period of that health check, in milliseconds. Read only when it is enabled.',
367
+ },
368
+ healthReportCallback: callbackOrNull(
369
+ 'Async function (health) => void, or null. Called with every health result — it only '
370
+ + 'reports, it decides nothing.'
371
+ ),
372
+ healthCriticalCallback: callbackOrNull(
373
+ 'Async function (health) => void, or null. Called when a health result is critical.'
374
+ ),
375
+ criticalHealthShutdown: {
376
+ type: 'boolean',
377
+ description:
378
+ 'Whether a sustained critical health result makes the transport emit `health:shutdown`. '
379
+ + 'Anything other than `false` means enabled. It only arms the event; ending the '
380
+ + 'process remains the owner\'s decision.',
381
+ },
382
+ criticalHealthShutdownDelay: {
383
+ type: 'integer',
384
+ minimum: 0,
385
+ description:
386
+ 'How long health must stay critical before that event is emitted, in milliseconds.',
387
+ },
388
+
389
+ // ---------------------------------------------------------------------
390
+ // Which kind of service this client belongs to. `queueCreationFilter` and
391
+ // `queueCreationCallback` stood here until d.419: they were the two keys by
392
+ // which a client declared a queue nobody owns, and a queue now comes into
393
+ // being from a declaration or not at all
394
+ // (`docs/governance/confirmations/mq-consumer-contract.md` 006). A config
395
+ // still carrying either is refused by name at construction — the schema is
396
+ // CLOSED, which is what makes the removal visible to the caller instead of
397
+ // silently ignored.
398
+ // ---------------------------------------------------------------------
399
+ recoveryScope: {
400
+ type: 'string',
401
+ enum: ['infrastructure', 'business'],
402
+ description:
403
+ 'Which kind of service this client belongs to. It decides nothing about queue '
404
+ + 'creation: no client declares a queue (workers/RecoveryWorker.js).',
48
405
  },
49
406
  },
50
407
  required: ['type', 'host'], // Only type and host required
51
- additionalProperties: true, // Allow additional properties for flexibility
52
- };
53
408
 
409
+ // CLOSED. A key nobody declares is refused at construction, naming the key and the
410
+ // declared key it is closest to (`../BaseClient.js`, `invalidConfigurationMessage()`).
411
+ //
412
+ // It stood open until d.291b for one measured reason, and the reason was answered
413
+ // rather than waived: `@onlineapps/conn-infra-mq`'s `ConnectorMQClient` is a SUBCLASS
414
+ // of `BaseClient` — it hands its own config straight to `super()` and then reads keys
415
+ // of its own off the same `this._config` (`autoSetupServiceQueues`, `defaultTTL`,
416
+ // `workflowInitQueue`, `workflowCompletedQueue`). Closing the door with those
417
+ // undeclared would have refused the construction of every business service at boot, and
418
+ // a gate lands together with compliance, never ahead of it (`automation-gates.md` §3).
419
+ // The compliance half is d.291 (that package declares its four keys in its own schema);
420
+ // the ONE place it hands them to this one is the second constructor argument,
421
+ // `super(config, extraProperties)`. The subclass never edits this file and this file
422
+ // never learns the subclass's keys, so one type and one description keep one owner
423
+ // (`change-discipline.md` § One rail per concern).
424
+ //
425
+ // What the flip COSTS, and who pays it, measured 2026-09-14 over every construction of
426
+ // this client in `api/shared`, `api/infra`, `api_biz` and `fe_adminui`: four callers
427
+ // still write `queue`, a name this package has read nowhere since it was extracted
428
+ // (§ A NAME THAT IS NOT A KEY above). They drop it in the same 8.x cascade that pins
429
+ // this version — `api/infra/api_gateway/index.js`,
430
+ // `api/infra/api_delivery_dispatcher/src/services/DeliveryDispatcher.js`,
431
+ // `api/infra/api_meta_reader/src/index.js` and
432
+ // `@onlineapps/service-wrapper` `_initializeMQ()`.
433
+ additionalProperties: false,
434
+ };
@@ -3,25 +3,57 @@
3
3
  /**
4
4
  * defaultConfig.js
5
5
  *
6
- * Provides default configuration values for MQ Client Core.
7
- * Users can override any of these by passing a custom config object
8
- * or by supplying overrides to connect().
6
+ * THE owner of every constant default this client applies. A caller overrides any
7
+ * of them by passing the key to the constructor or to `connect()`.
9
8
  *
10
- * NOTE:
11
- * - Infrastructure topology (RabbitMQ URL) is NOT defaulted here.
12
- * - BaseClient resolves host via @onlineapps/runtime-config (fail-fast when missing).
9
+ * WHY THEY ARE ALL HERE, and not next to the code that reads them. This object is
10
+ * composed into the configuration BEFORE the schema validation runs
11
+ * (`BaseClient`, `config/composeConfig.js`), so what it says is what the client is
12
+ * validated with and built from — there is exactly one value, and a reader looks
13
+ * it up in one place. Until 2026-09-14 the transport constructor carried its own
14
+ * literal for each of its twenty-two behaviour knobs, written as
15
+ * `this._config.<key> || <literal>`. That is two rails for one concern
16
+ * (`change-discipline.md`), and the `||` was worse than the duplication: it means
17
+ * "if falsy", not "if absent", and the schema declares `minimum: 0` for these
18
+ * knobs. A caller who wrote `reconnectBaseDelay: 0` ("retry immediately"),
19
+ * `publishMaxRetries: 0` ("do not retry") or `thrashingThreshold: 0` ("alert on
20
+ * the first close") had the value replaced by its opposite, without a word.
21
+ * Measured on 2026-09-14: twelve knobs swallowed a written `0`.
22
+ *
23
+ * `??` was not the answer either. It fixes the falsy half and leaves the value
24
+ * written in two places, which is the half that rots.
25
+ *
26
+ * NOT EVERY DEFAULT IS A CONSTANT, and the ones that are not do not belong here:
27
+ * - `host`, `heartbeat`, `serviceName`, `maxReconnectAttempts`,
28
+ * `maxDeliveryAttempts` are runtime-resolved (explicit → env → module
29
+ * default) and owned by `../config.js` + `../defaults.js`;
30
+ * - `reconnectWaitTimeout` is DERIVED from `maxReconnectAttempts` and
31
+ * `reconnectMaxDelay` (`transports/rabbitmqClient.js`). A constant copy of it
32
+ * here would be free to disagree with the two values it is computed from;
33
+ * - `connectionName` is composed from the resolved service name and the pid.
34
+ *
35
+ * @see ./configSchema.js — what each key means and what type it must carry.
13
36
  */
14
37
 
15
38
  module.exports = {
39
+ // ---------------------------------------------------------------------
40
+ // Connection shape.
41
+ // ---------------------------------------------------------------------
42
+
16
43
  // Transport type: currently only 'rabbitmq' is fully supported.
17
44
  type: 'rabbitmq',
18
45
 
19
- // RabbitMQ connection URI or hostname (resolved at runtime; required by schema).
20
- // Provided via explicit config.host or ENV RABBITMQ_URL.
46
+ // NO `host` default. Infrastructure topology is never defaulted: it comes from
47
+ // the explicit config or from ENV RABBITMQ_URL, and its absence is fatal
48
+ // (`../config.js`, fail-fast).
21
49
 
22
- // Default queue name; can be overridden per call to publish/consume.
23
- // Optional for infrastructure services (they may not need a default queue).
24
- queue: '',
50
+ // NO `queue` default. The client has no default queue: every `publish()` and
51
+ // `consume()` names the queue in the call, and the platform's queue names have
52
+ // one owner in `config/queueConfig.js`. `queue: ''` sat here from the library's
53
+ // extraction (`7a47b2fa`) until 2026-09-14 and no line of `src/` ever read the
54
+ // key back — an ambient default that could only ever redirect a
55
+ // publish away from the name the caller wrote (`queue-ownership.md`,
56
+ // `architecture-principles.md` §8).
25
57
 
26
58
  // Default exchange name (empty string → default direct exchange).
27
59
  exchange: '',
@@ -35,8 +67,130 @@ module.exports = {
35
67
  // Default auto-acknowledge setting for consumers.
36
68
  noAck: false,
37
69
 
38
- // Custom logger object (if not provided, console.* will be used).
39
- // Expected interface: { info(), warn(), error(), debug() }.
40
- logger: null,
41
- };
70
+ // NO `logger` default. The logger is required and has no stand-in: the library
71
+ // writes through the one the caller injects, or it refuses to be built
72
+ // (`assertLogger`, confirmation connector-logger-contract 001/003). A `null`
73
+ // default sat here until 2026-09-07, with a comment promising that the global
74
+ // console would stand in — exactly the ambient channel that decision removed.
75
+
76
+ // ---------------------------------------------------------------------
77
+ // Connection-level recovery. The BUDGET (`maxReconnectAttempts`) is runtime
78
+ // resolved in `../config.js`; these are the shape of the backoff it spends.
79
+ // ---------------------------------------------------------------------
80
+
81
+ // Is recovery armed at all? A client that says `false` stays down when the
82
+ // connection dies, and its owner decides what to do about it.
83
+ reconnectEnabled: true,
84
+
85
+ // First wait before a recovery attempt, in ms; it grows exponentially up to
86
+ // `reconnectMaxDelay`.
87
+ reconnectBaseDelay: 1000,
88
+
89
+ // Ceiling of that growth, in ms.
90
+ reconnectMaxDelay: 30000,
91
+
92
+ // Injected by the owner of this client (service/wrapper), called when the
93
+ // recovery budget is spent. `null` means nobody is listening: the LIBRARY never
94
+ // ends the process — that decision belongs to whoever owns the lifecycle.
95
+ onFatal: null,
96
+
97
+ // ---------------------------------------------------------------------
98
+ // Publisher retry. One published message, several attempts.
99
+ // ---------------------------------------------------------------------
100
+
101
+ publishRetryEnabled: true,
102
+
103
+ // How many attempts one publish gets before it is buffered or reported.
104
+ publishMaxRetries: 3,
105
+
106
+ // First wait between two attempts, in ms, multiplied by
107
+ // `publishRetryBackoffMultiplier` on each further attempt, up to
108
+ // `publishRetryMaxDelay`.
109
+ publishRetryBaseDelay: 100,
110
+ publishRetryMaxDelay: 5000,
111
+ publishRetryBackoffMultiplier: 2,
112
+
113
+ // How long this client waits for an ANSWER from the broker, in ms: the confirm
114
+ // of one publish attempt, and the `close-ok` of each handle `disconnect()`
115
+ // closes — one number for two consumers, which is why it carries the name of
116
+ // neither. It was `publishConfirmationTimeout` until d.299, a name that was
117
+ // true of the publish path and false of the teardown that had used it since
118
+ // d.283.
119
+ brokerAnswerTimeout: 5000,
120
+
121
+ // How long the connect handshake may take before the attempt is abandoned, in
122
+ // ms. Written out twice as the literal `10000` — once in `connect()`, once in
123
+ // the recovery path — until d.299, together with two messages that said
124
+ // "after 10 seconds" whatever the number had been.
125
+ connectTimeout: 10000,
42
126
 
127
+ // How long after `sendToQueue()` one publish attempt waits before looking to
128
+ // see whether the missing confirm is a DEAD CHANNEL rather than a slow broker,
129
+ // in ms. The literal `1000` in the publish path until d.299.
130
+ publishConfirmWatchdogDelay: 1000,
131
+
132
+ // ---------------------------------------------------------------------
133
+ // Publish buffer: where a message goes while the broker is unreachable.
134
+ // ---------------------------------------------------------------------
135
+
136
+ // Messages held in memory, and how long each may wait there (ms).
137
+ publishBufferMaxSize: 100,
138
+ publishBufferTtlMs: 300000,
139
+
140
+ // NO `persistentBufferEnabled` / `persistentRedisClient`. There is ONE buffer,
141
+ // the in-memory one above. The pair declared a Redis buffer whose `add()` and
142
+ // `flush()` stored and returned nothing, so switching it on dropped the
143
+ // critical messages it was supposed to protect; both names are now refused by
144
+ // name in `BaseClient` rather than silently ignored (d.343).
145
+
146
+ // ---------------------------------------------------------------------
147
+ // Channel thrashing detection: a channel that keeps closing says something is
148
+ // wrong with what runs on it.
149
+ // ---------------------------------------------------------------------
150
+
151
+ // Closes per window that count as thrashing, and the window in ms.
152
+ thrashingThreshold: 5,
153
+ thrashingWindowMs: 60000,
154
+
155
+ // `(type, count, windowMs) => void`, injected by the owner; `null` = nobody.
156
+ thrashingAlertCallback: null,
157
+
158
+ // ---------------------------------------------------------------------
159
+ // Prefetch monitoring: how full a consumer's window runs.
160
+ // ---------------------------------------------------------------------
161
+
162
+ // Utilisation (0..1) at which the alert fires, and how often it is measured (ms).
163
+ prefetchUtilizationThreshold: 0.8,
164
+ prefetchCheckInterval: 10000,
165
+
166
+ // `(queue, utilization, inFlight, prefetchCount) => void`; `null` = nobody.
167
+ prefetchAlertCallback: null,
168
+
169
+ // ---------------------------------------------------------------------
170
+ // Health monitoring.
171
+ // ---------------------------------------------------------------------
172
+
173
+ healthCheckEnabled: true,
174
+
175
+ // Period of the periodic health check, in ms.
176
+ healthCheckInterval: 30000,
177
+
178
+ // `(health) => Promise<void>` — where a health report goes, and where a
179
+ // CRITICAL one goes. Injected; `null` = nobody.
180
+ healthReportCallback: null,
181
+ healthCriticalCallback: null,
182
+
183
+ // Does critical health end this client, and how long it stays critical first
184
+ // (ms)? The library only reports; the shutdown it asks for is the owner's
185
+ // `onFatal` path.
186
+ criticalHealthShutdown: true,
187
+ criticalHealthShutdownDelay: 60000,
188
+
189
+ // ---------------------------------------------------------------------
190
+ // Which kind of service this client belongs to. It decides nothing about queue
191
+ // creation: this client declares no queue at all, and the two callbacks that
192
+ // used to let it — `queueCreationFilter` and `queueCreationCallback` — left
193
+ // with that door (d.419, conf mq-consumer-contract 006).
194
+ // ---------------------------------------------------------------------
195
+ recoveryScope: 'infrastructure',
196
+ };