@onlineapps/mq-client-core 5.0.0 → 6.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.
@@ -186,7 +186,32 @@ class SerializationError extends Error {
186
186
  }
187
187
  }
188
188
 
189
+ /**
190
+ * Carry a verdict that is ALREADY written onto the error a caller is about to receive.
191
+ *
192
+ * The verdict of a lost connection is written in one place (`RabbitMQClient._failFatally()`,
193
+ * `_markBrokerRefusal()`: `code: 'MQ_CONNECTION_FATAL'`, `reason`). Every receiver reads it ON
194
+ * the error it is handed — `BaseClient._consumerRefusal()`, `@onlineapps/service-wrapper`
195
+ * FÁZE 0.3/0.7 — never down a `cause` chain (a reader walking the chain would be a fallback
196
+ * over a buried marker, `architecture-principles.md` §3). So every layer that builds an error
197
+ * AROUND a verdict copies `code` and `reason` onto it with this one function (d.1297 consume,
198
+ * d.1300 publish); an error that is not a fatal verdict (a spent cycle carries no code, by
199
+ * contract) leaves the target untouched.
200
+ *
201
+ * @param {Error} error - the error being built; `cause` already set by its builder
202
+ * @param {Error|null|undefined} verdict - what the recovery produced
203
+ * @returns {Error} the same error, with the verdict readable on top when there is one
204
+ */
205
+ function carryVerdict(error, verdict) {
206
+ if (verdict && verdict.code === 'MQ_CONNECTION_FATAL') {
207
+ error.code = verdict.code;
208
+ error.reason = verdict.reason;
209
+ }
210
+ return error;
211
+ }
212
+
189
213
  module.exports = {
214
+ carryVerdict,
190
215
  ValidationError,
191
216
  ConnectionError,
192
217
  CONNECTION_CLOSED_UNEXPECTEDLY,
@@ -22,6 +22,54 @@ class TransientPublishError extends Error {
22
22
  }
23
23
  }
24
24
 
25
+ /**
26
+ * The send channel could not be had because the connection stayed down: the recovery this
27
+ * attempt waited for ended without a connection (a spent cycle — standby; the last cycle or
28
+ * a broker refusal — fatal; a wait that completed into a connection already dead again), or
29
+ * no recovery may start (the client is permanently lost, the caller closed it, recovery is
30
+ * disabled by configuration).
31
+ *
32
+ * A TRANSIENT publish error, by the owner's decision (B1, 2026-10-06, d.1300): the message is
33
+ * held in the buffer like every other transiently failed message and replayed after the next
34
+ * `reconnected` / `unblocked` — one rail with the retry that waited for a reconnect and saw it
35
+ * fail (`PublishLayer`). What sets it apart from the other transient errors, and why it is its
36
+ * own class: retrying the attempt NOW is pointless and harmful — the next use of the client is
37
+ * what starts the next recovery cycle, so `PublishLayer` holds the message at once instead of
38
+ * spending its retry budget (and recovery cycles) on it.
39
+ *
40
+ * `cause` is the error that ended the wait when there was one (the fatal verdict is the very
41
+ * object `onFatal` received), and when that verdict is fatal its `code`/`reason` are carried on
42
+ * this error too (`errorHandler.carryVerdict()`), so a reader finds them where every reader
43
+ * looks. A spent cycle carries no code, by contract.
44
+ */
45
+ class RecoveryEndedPublishError extends TransientPublishError {
46
+ /**
47
+ * @param {string} message
48
+ * @param {Error|undefined} cause - what ended the recovery, when an object exists for it
49
+ * @param {{replayable: boolean}} verdict - `replayable`: whether a `reconnected` can still
50
+ * come on this client without the caller's action — true after a spent cycle (the next
51
+ * use starts the next one), false once the client is permanently lost, closed by
52
+ * `disconnect()`, or has `reconnectEnabled: false`. It decides whether `PublishLayer`
53
+ * holds the message (d.1301): a hold only makes sense where a reconnect can replay it;
54
+ * anywhere else it would sit until its TTL and be reported LOST, which is a loss with a
55
+ * delay, not a safety. Required: a transient error that cannot say whether a replay can
56
+ * come must not be held on a guess.
57
+ */
58
+ constructor(message, cause, verdict) {
59
+ super(message, cause);
60
+ this.name = 'RecoveryEndedPublishError';
61
+ if (!verdict || typeof verdict.replayable !== 'boolean') {
62
+ throw new Error(
63
+ '[RecoveryEndedPublishError] Missing required field - replayable (boolean) is required. '
64
+ + 'Expected: true when a reconnected can still come on this client, false when it never will '
65
+ + '(permanently lost, disconnect(), reconnectEnabled: false). '
66
+ + 'Fix: pass { replayable } — RabbitMQClient._recoveryEndedForPublish() derives it from the client state.'
67
+ );
68
+ }
69
+ this.replayable = verdict.replayable;
70
+ }
71
+ }
72
+
25
73
  class PermanentPublishError extends Error {
26
74
  /**
27
75
  * @param {string} message - Human readable message
@@ -468,6 +516,7 @@ module.exports = {
468
516
  missingQueueDefinitionMessage,
469
517
  refusedSendMessage,
470
518
  TransientPublishError,
519
+ RecoveryEndedPublishError,
471
520
  PermanentPublishError,
472
521
  QueueNotFoundError,
473
522
  classifyPublishError,