@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.
- package/CHANGELOG.md +153 -0
- package/README.md +491 -8
- package/package.json +12 -8
- package/src/BaseClient.js +953 -80
- package/src/buffer/InMemoryBuffer.js +50 -9
- package/src/buffer/MessageBuffer.js +20 -52
- package/src/config/composeConfig.js +60 -0
- package/src/config/configSchema.js +398 -17
- package/src/config/defaultConfig.js +169 -15
- package/src/config/deliveryPolicy.js +165 -0
- package/src/config/queueConfig.js +738 -64
- package/src/config.js +29 -0
- package/src/defaults.js +43 -0
- package/src/index.js +91 -2
- package/src/layers/PublishLayer.js +83 -37
- package/src/monitoring/PublishMonitor.js +11 -4
- package/src/monitoring-publish.js +81 -54
- package/src/transports/rabbitmqClient.js +2698 -738
- package/src/transports/transportFactory.js +9 -2
- package/src/utils/errorHandler.js +83 -4
- package/src/utils/nearestKey.js +101 -0
- package/src/utils/publishErrors.js +95 -10
- package/src/utils/redactCredentials.js +106 -0
- package/src/utils/serializer.js +12 -2
- package/src/workers/RecoveryWorker.js +58 -81
- package/src/buffer/RedisBuffer.js +0 -57
|
@@ -3,51 +3,432 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* configSchema.js
|
|
5
5
|
*
|
|
6
|
-
* JSON Schema used by Ajv to validate the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
//
|
|
20
|
-
//
|
|
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
|
-
//
|
|
23
|
-
//
|
|
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
|
-
//
|
|
39
|
-
//
|
|
40
|
-
logger
|
|
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
|
+
};
|