@onlineapps/mq-client-core 5.1.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.
@@ -2,10 +2,11 @@
2
2
 
3
3
  const {
4
4
  TransientPublishError,
5
+ RecoveryEndedPublishError,
5
6
  PermanentPublishError,
6
7
  QueueNotFoundError,
7
8
  } = require('../utils/publishErrors');
8
- const { ValidationError, PublishError } = require('../utils/errorHandler');
9
+ const { ValidationError, PublishError, carryVerdict } = require('../utils/errorHandler');
9
10
  const MessageBuffer = require('../buffer/MessageBuffer');
10
11
  const { assertLogger } = require('@onlineapps/logger-contract');
11
12
 
@@ -23,6 +24,8 @@ class PublishLayer {
23
24
  * @param {Object} options.client - Instance RabbitMQClient
24
25
  * @param {Object} options.logger - Logger s info/warn/error/debug (povinný)
25
26
  * @param {Object} [options.bufferConfig] - Konfigurace pro MessageBuffer
27
+ * @param {{setTimeout: Function, clearTimeout: Function}} options.timers - The clock the
28
+ * buffer expires its entries on (required, d.1275; the transport passes its own timer rail).
26
29
  * @param {boolean} [options.retryEnabled=true] - Zapnout retry
27
30
  * @param {number} [options.maxRetries=3] - Max počet pokusů
28
31
  * @param {number} [options.retryBaseDelay=100] - Base delay v ms
@@ -114,7 +117,11 @@ class PublishLayer {
114
117
  this._buffer = new MessageBuffer({
115
118
  inMemory: options.bufferConfig?.inMemory,
116
119
  logger: this._logger,
120
+ timers: options.timers,
117
121
  });
122
+ // Read by the one sentence that tells a caller how long the buffer holds its message
123
+ // (`_holdOnFailure()`); `InMemoryBuffer` has already refused a missing value above.
124
+ this._bufferTtlMs = options.bufferConfig.inMemory.ttlMs;
118
125
  }
119
126
 
120
127
  /**
@@ -312,6 +319,21 @@ class PublishLayer {
312
319
  throw said(err);
313
320
  }
314
321
 
322
+ // The attempt could not get its send channel because the connection stayed down
323
+ // (B1, d.1300): the recovery it waited for ended in standby or fatal, or none may
324
+ // start. Retrying NOW is pointless — the next use of the client starts the next
325
+ // cycle — so the message is held at once, on the same rail as the retry below that
326
+ // waited for a reconnect and saw it fail. Asked BEFORE the generic transient branch,
327
+ // because the class is a transient one (the message is held and replayed).
328
+ if (err instanceof RecoveryEndedPublishError) {
329
+ throw said(await this._failedAfterRecovery({
330
+ queue, buffer, options, priority, attempt, transient: err, recoveryError: err.cause || err, sentAny,
331
+ headline: '[PublishLayer] Publish failed: the recovery this publish waited for ended without a connection '
332
+ + `(attempt ${attempt} for queue "${queue}") - ${err.message} `
333
+ + 'Expected: a live connection before the send. '
334
+ }));
335
+ }
336
+
315
337
  // Transient errors - check if we should retry or buffer
316
338
  if (err instanceof TransientPublishError) {
317
339
  // If we have more attempts, retry
@@ -325,23 +347,11 @@ class PublishLayer {
325
347
  continue; // Retry immediately after reconnect
326
348
  } catch (reconnectErr) {
327
349
  // Reconnection failed - buffer (unless the caller said not to) and fail
328
- const held = await this._holdOnFailure(queue, buffer, options, priority, err, sentAny);
329
- this._client.emit('publish:failed', {
330
- queue,
331
- attempt,
332
- error: `Reconnection failed: ${reconnectErr.message}`,
333
- retryable: false,
334
- reconnectFailed: true
335
- });
336
- const failure = new PublishError(
337
- `[PublishLayer] Publish failed: reconnection failed after ${attempt} attempts for queue "${queue}" - `
350
+ throw said(await this._failedAfterRecovery({
351
+ queue, buffer, options, priority, attempt, transient: err, recoveryError: reconnectErr, sentAny,
352
+ headline: `[PublishLayer] Publish failed: reconnection failed after ${attempt} attempts for queue "${queue}" - `
338
353
  + 'Expected: the transport to re-establish the connection before the retry. '
339
- + `Fix: read error.cause for the reconnect reason; ${held.sentence}`,
340
- queue,
341
- reconnectErr
342
- );
343
- failure.buffered = held.buffered;
344
- throw said(failure);
354
+ }));
345
355
  }
346
356
  }
347
357
 
@@ -415,6 +425,39 @@ class PublishLayer {
415
425
  }
416
426
  }
417
427
 
428
+ /**
429
+ * The one ending for "the recovery this publish depended on did not arrive": hold the
430
+ * message (unless the caller said not to), report the failure, and build the error the
431
+ * caller gets — with `buffered` written on it and the verdict of a fatal ending carried on
432
+ * top (`carryVerdict()`, d.1297/d.1300).
433
+ *
434
+ * Two callers, one body (d.1300): the retry that waited for a reconnect in flight and saw
435
+ * it fail, and the attempt whose send channel could not be had because the connection
436
+ * stayed down (`RecoveryEndedPublishError`). Until d.1300 only the first held the message;
437
+ * the second threw the wait's ending raw, so two near-identical situations ended two ways
438
+ * (INFRA-DOCS ID254 point 1).
439
+ *
440
+ * @private
441
+ * @returns {Promise<PublishError>} the failure to throw; the caller writes `sent` on it
442
+ */
443
+ async _failedAfterRecovery({ queue, buffer, options, priority, attempt, transient, recoveryError, sentAny, headline }) {
444
+ const held = await this._holdOnFailure(queue, buffer, options, priority, transient, sentAny);
445
+ this._client.emit('publish:failed', {
446
+ queue,
447
+ attempt,
448
+ error: `Reconnection failed: ${recoveryError.message}`,
449
+ retryable: false,
450
+ reconnectFailed: true
451
+ });
452
+ const failure = new PublishError(
453
+ `${headline}Fix: read error.cause for the reconnect reason; ${held.sentence}`,
454
+ queue,
455
+ recoveryError
456
+ );
457
+ failure.buffered = held.buffered;
458
+ return carryVerdict(failure, recoveryError);
459
+ }
460
+
418
461
  /**
419
462
  * Handle publish error - buffer transient errors
420
463
  * @private
@@ -459,8 +502,42 @@ class PublishLayer {
459
502
  + 'the caller holds the only copy (error.buffered === false).'
460
503
  };
461
504
  }
505
+ // Asked AFTER the caller's explicit choice above (`architecture-principles.md` §8: the
506
+ // option the caller passed is what its sentence names), and before any hold.
507
+ // A client that will never reconnect on its own cannot replay anything: the two flushes
508
+ // are `reconnected` and `unblocked`, and neither comes after the connection is declared
509
+ // lost, after `disconnect()`, or with `reconnectEnabled: false`. Holding there is a loss
510
+ // with a delay (reported LOST at TTL), not a safety — so nothing is held, and the error
511
+ // says so (d.1301, the lead's decision narrowing B1 of d.1300). The transport writes the
512
+ // fact (`replayable`) where it knows the state; this layer only reads it.
513
+ if (err instanceof RecoveryEndedPublishError && err.replayable === false) {
514
+ this._logger.warn(
515
+ `[PublishLayer] Message for queue '${queue}' NOT buffered: this client will not reconnect, so no replay could come - ${err.message}`
516
+ );
517
+ return {
518
+ buffered: false,
519
+ sentence: 'the message is NOT buffered: this client will not reconnect on its own (the connection is '
520
+ + 'permanently lost, the caller closed it, or reconnectEnabled is false), so no reconnect could replay '
521
+ + 'it — the caller holds the only copy (error.buffered === false).'
522
+ };
523
+ }
462
524
  if (await this._bufferMessage(queue, buffer, options, priority, err, sent)) {
463
- return { buffered: true, sentence: 'the message is buffered, so it is not lost.' };
525
+ // What the buffer WILL do, and nothing more (d.1275). It used to say "the message is
526
+ // buffered, so it is not lost" — while the only two flushes are the reconnect and
527
+ // `unblocked`, so an outage of the publisher channel alone ended with the message
528
+ // expiring (INFRA 531, W224). Whether a channel recovery should flush too is an open
529
+ // decision (a sent entry re-sent after it may be a duplicate); until it is made, the
530
+ // sentence states the gap instead of papering over it.
531
+ return {
532
+ buffered: true,
533
+ sentence: `the message is buffered for up to ${this._bufferTtlMs} ms (publishBufferTtlMs) and `
534
+ + 'replayed only after a reconnect or after the broker unblocks the connection — a recovered '
535
+ + 'publisher channel alone does not replay it; if neither comes within that time it expires and '
536
+ + (sent
537
+ ? 'is reported as an expired message whose frame had been sent (the broker may hold a copy) '
538
+ : 'is reported as LOST ')
539
+ + '(an error line naming the queue or exchange), and nothing replays it then.'
540
+ };
464
541
  }
465
542
  return {
466
543
  buffered: false,