@relaybus/rabbitmq 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { RelayBusError, NonRetryableError, assertEventType, systemClock, ConfigurationError, toErrorInfo, computeRetryDelay, isRetryableError, createRetryPolicy, toMilliseconds } from '@relaybus/core';
2
- import { createCloudEventReaders, mediaTypeOf, isJsonObject, createJsonEnvelopeFormat, schedule, writeStructuredCloudEvent, assertPositiveInteger } from '@relaybus/core/internal';
1
+ import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError, systemClock, toErrorInfo, computeRetryDelay, isRetryableError, createRetryPolicy } from '@relaybus/core';
2
+ import { createCloudEventReaders, DecodedEventReader, mediaTypeOf, isJsonObject, observeRejection, createJsonEnvelopeFormat, SubscribingTransport, writeStructuredCloudEvent, schedule, NonEmptyText, PositiveInteger, Milliseconds } from '@relaybus/core/internal';
3
3
  export { fromHeader, fromPayloadField } from '@relaybus/core/internal';
4
4
 
5
5
  // src/errors.ts
@@ -31,6 +31,99 @@ var RabbitMqUnencodableEventError = class extends NonRetryableError {
31
31
  name = "RabbitMqUnencodableEventError";
32
32
  code = "RELAYBUS_RABBITMQ_UNENCODABLE_EVENT";
33
33
  };
34
+ var NACK_MESSAGE = "message nacked";
35
+ var ConfirmedPublishing = class {
36
+ #transportName;
37
+ #publishTimeoutMs;
38
+ constructor(transportName, publishTimeoutMs) {
39
+ this.#transportName = transportName;
40
+ this.#publishTimeoutMs = publishTimeoutMs;
41
+ }
42
+ async publish(publisher, input) {
43
+ const { exchange, routingKey, content, options, mandatory } = input;
44
+ const eventId = options.messageId ?? "unknown";
45
+ if (publisher === void 0) {
46
+ throw new RabbitMqUnavailableError(
47
+ `[RabbitMqTransport](publish) Not connected to the broker | transport=${this.#transportName} destination=${routingKey}`
48
+ );
49
+ }
50
+ const returnKey = unroutableKey(options.messageId, routingKey);
51
+ const target = { eventId, destination: routingKey };
52
+ await new Promise((resolve, reject) => {
53
+ const cancelTimeout = this.#startTimeout(target, reject);
54
+ const pending = { ...target, publisher, returnKey, cancelTimeout, resolve, reject };
55
+ try {
56
+ publisher.channel.publish(
57
+ exchange,
58
+ routingKey,
59
+ content,
60
+ { ...options, mandatory },
61
+ (error) => this.#confirmed(pending, error)
62
+ );
63
+ } catch (error) {
64
+ cancelTimeout();
65
+ reject(this.#toPublishError(error, target));
66
+ }
67
+ });
68
+ }
69
+ #startTimeout({ eventId, destination }, reject) {
70
+ return schedule(systemClock, {
71
+ run: () => {
72
+ reject(
73
+ new RabbitMqUnavailableError(
74
+ `[RabbitMqTransport](publish) No broker confirm within ${this.#publishTimeoutMs}ms | transport=${this.#transportName} eventId=${eventId} destination=${destination}`
75
+ )
76
+ );
77
+ },
78
+ delayMs: this.#publishTimeoutMs
79
+ });
80
+ }
81
+ /** The broker's confirm (or nack) of one message arrived. */
82
+ #confirmed(pending, error) {
83
+ pending.cancelTimeout();
84
+ const returned = takeUnroutable(pending.publisher.unroutable, pending.returnKey);
85
+ if (error) {
86
+ pending.reject(this.#toPublishError(error, pending));
87
+ } else if (returned) {
88
+ pending.reject(
89
+ new UnroutableMessageError(
90
+ `[RabbitMqTransport](publish) No queue is bound to the routing key | transport=${this.#transportName} eventId=${pending.eventId} destination=${pending.destination}`
91
+ )
92
+ );
93
+ } else {
94
+ pending.resolve();
95
+ }
96
+ }
97
+ #toPublishError(error, { eventId, destination }) {
98
+ const { message } = toErrorInfo(error);
99
+ const detail = `transport=${this.#transportName} eventId=${eventId} destination=${destination} error=${message}`;
100
+ return message === NACK_MESSAGE ? new RabbitMqPublishRejectedError(
101
+ `[RabbitMqTransport](publish) Broker nacked the message | ${detail}`,
102
+ { cause: error }
103
+ ) : new RabbitMqUnavailableError(`[RabbitMqTransport](publish) Publish failed | ${detail}`, {
104
+ cause: error
105
+ });
106
+ }
107
+ };
108
+ function unroutableKey(messageId, routingKey) {
109
+ return `${String(messageId)}
110
+ ${routingKey}`;
111
+ }
112
+ function recordUnroutable(unroutable, key) {
113
+ unroutable.set(key, (unroutable.get(key) ?? 0) + 1);
114
+ }
115
+ function takeUnroutable(unroutable, key) {
116
+ const count = unroutable.get(key);
117
+ if (count === void 0) {
118
+ return false;
119
+ }
120
+ if (count === 1) {
121
+ unroutable.delete(key);
122
+ } else {
123
+ unroutable.set(key, count - 1);
124
+ }
125
+ return true;
126
+ }
34
127
  function abortableSleep(milliseconds, signal) {
35
128
  if (signal.aborted) {
36
129
  return Promise.resolve();
@@ -277,35 +370,41 @@ function visitObject(value, {
277
370
  }
278
371
  ancestors.add(value);
279
372
  if (Array.isArray(value)) {
280
- for (let index = 0; index < value.length; index += 1) {
281
- if (!(index in value)) {
282
- throw refuse(eventId, {
283
- path: `${path}[${index}]`,
284
- reason: "is an array hole, which JSON turns into null"
285
- });
286
- }
287
- visit(value[index], { path: `${path}[${index}]`, ancestors, eventId });
288
- }
373
+ visitArray(value, { path, ancestors, eventId });
289
374
  } else {
290
- const prototype = Object.getPrototypeOf(value);
291
- if (prototype !== Object.prototype && prototype !== null) {
375
+ visitPlainObject(value, { path, ancestors, eventId });
376
+ }
377
+ ancestors.delete(value);
378
+ }
379
+ function visitArray(value, { path, ancestors, eventId }) {
380
+ for (let index = 0; index < value.length; index += 1) {
381
+ if (!(index in value)) {
292
382
  throw refuse(eventId, {
293
- path,
294
- reason: "is not a plain object (class instance, Date, Map or Set)"
295
- });
296
- }
297
- if (Object.getOwnPropertySymbols(value).length > 0) {
298
- throw refuse(eventId, { path, reason: "has a symbol key, which JSON drops" });
299
- }
300
- for (const key of Object.keys(value)) {
301
- visit(value[key], {
302
- path: `${path}.${key}`,
303
- ancestors,
304
- eventId
383
+ path: `${path}[${index}]`,
384
+ reason: "is an array hole, which JSON turns into null"
305
385
  });
306
386
  }
387
+ visit(value[index], { path: `${path}[${index}]`, ancestors, eventId });
388
+ }
389
+ }
390
+ function visitPlainObject(value, { path, ancestors, eventId }) {
391
+ const prototype = Object.getPrototypeOf(value);
392
+ if (prototype !== Object.prototype && prototype !== null) {
393
+ throw refuse(eventId, {
394
+ path,
395
+ reason: "is not a plain object (class instance, Date, Map or Set)"
396
+ });
397
+ }
398
+ if (Object.getOwnPropertySymbols(value).length > 0) {
399
+ throw refuse(eventId, { path, reason: "has a symbol key, which JSON drops" });
400
+ }
401
+ for (const key of Object.keys(value)) {
402
+ visit(value[key], {
403
+ path: `${path}.${key}`,
404
+ ancestors,
405
+ eventId
406
+ });
307
407
  }
308
- ancestors.delete(value);
309
408
  }
310
409
  function refuse(eventId, { path, reason }) {
311
410
  return new UnserializablePayloadError(
@@ -334,6 +433,22 @@ function encodeEnvelope(envelope) {
334
433
  [`${ATTRIBUTE_PREFIX}time`]: envelope.occurredAt.toISOString(),
335
434
  [`${ATTRIBUTE_PREFIX}datacontenttype`]: JSON_CONTENT_TYPE
336
435
  };
436
+ addOptionalAttributes(headers, envelope);
437
+ return {
438
+ content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
439
+ options: {
440
+ contentType: JSON_CONTENT_TYPE,
441
+ persistent: true,
442
+ messageId: envelope.id,
443
+ timestamp: Math.floor(envelope.occurredAt.getTime() / MILLISECONDS_PER_SECOND),
444
+ ...mirror("type", envelope.type),
445
+ ...mirror("appId", envelope.source),
446
+ ...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
447
+ headers
448
+ }
449
+ };
450
+ }
451
+ function addOptionalAttributes(headers, envelope) {
337
452
  if (envelope.key !== null) {
338
453
  headers[`${ATTRIBUTE_PREFIX}subject`] = envelope.key;
339
454
  }
@@ -350,19 +465,6 @@ function encodeEnvelope(envelope) {
350
465
  );
351
466
  headers[`${EVENT_HEADER_PREFIX}${name}`] = value;
352
467
  }
353
- return {
354
- content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
355
- options: {
356
- contentType: JSON_CONTENT_TYPE,
357
- persistent: true,
358
- messageId: envelope.id,
359
- timestamp: Math.floor(envelope.occurredAt.getTime() / MILLISECONDS_PER_SECOND),
360
- ...mirror("type", envelope.type),
361
- ...mirror("appId", envelope.source),
362
- ...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
363
- headers
364
- }
365
- };
366
468
  }
367
469
  function encodeStructuredEnvelope(envelope) {
368
470
  assertJsonPayload(envelope.payload, envelope.id);
@@ -547,18 +649,13 @@ function plainJsonDecoder(options) {
547
649
  if (properties.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(properties.contentType) === STRUCTURED_MEDIA_TYPE) {
548
650
  return decodeCloudEvent(message);
549
651
  }
550
- const plain = {
551
- routingKey: message.fields.routingKey,
552
- properties,
553
- headers: readForeignHeaders(properties),
554
- payload: parseJsonBody(message.content)
555
- };
556
- const type = resolveText(options.type, { message: plain, name: "type" });
652
+ const plain = plainMessageOf(message);
653
+ const type = RABBITMQ_EVENTS.text(options.type, { plain, name: "type" });
557
654
  if (type === void 0) {
558
655
  throw new MalformedMessageError("Message has no event type: the type option found none");
559
656
  }
560
657
  assertValidType(type, "The event type");
561
- const id = resolveText(options.id, { message: plain, name: "id" }) ?? nonEmptyText(properties.messageId);
658
+ const id = RABBITMQ_EVENTS.text(options.id, { plain, name: "id" }) ?? nonEmptyText(properties.messageId);
562
659
  if (id === void 0) {
563
660
  throw new MalformedMessageError(
564
661
  "Message has no id: set the messageId property on the producer or configure the id option"
@@ -567,8 +664,8 @@ function plainJsonDecoder(options) {
567
664
  return {
568
665
  id,
569
666
  type,
570
- source: resolveText(options.source, { message: plain, name: "source" }) ?? defaultSource(message),
571
- key: resolveText(options.key, { message: plain, name: "key" }) ?? null,
667
+ source: RABBITMQ_EVENTS.text(options.source, { plain, name: "source" }) ?? defaultSource(message),
668
+ key: RABBITMQ_EVENTS.text(options.key, { plain, name: "key" }) ?? null,
572
669
  payload: plain.payload,
573
670
  headers: plain.headers,
574
671
  correlationId: null,
@@ -577,27 +674,35 @@ function plainJsonDecoder(options) {
577
674
  };
578
675
  };
579
676
  }
580
- function completeDecodedEvent(decoded, message) {
581
- if (typeof decoded !== "object" || decoded === null) {
582
- throw new MalformedMessageError("The decoder must return an event object synchronously");
583
- }
584
- if ("then" in decoded) {
585
- observeRejection(decoded);
586
- throw new MalformedMessageError("The decoder must return an event object synchronously");
587
- }
588
- const event = decoded;
677
+ function plainMessageOf(message) {
678
+ const { properties } = message;
589
679
  return {
590
- id: event.id,
591
- type: event.type,
592
- source: event.source ?? defaultSource(message),
593
- key: event.key ?? null,
594
- payload: event.payload,
595
- headers: event.headers ?? {},
596
- correlationId: event.correlationId ?? null,
597
- causationId: event.causationId ?? null,
598
- occurredAt: event.occurredAt ?? propertyTimeOrNow(message.properties)
680
+ routingKey: message.fields.routingKey,
681
+ properties,
682
+ headers: readForeignHeaders(properties),
683
+ payload: parseJsonBody(message.content)
599
684
  };
600
685
  }
686
+ var RabbitMqDecodedEvents = class extends DecodedEventReader {
687
+ invalid(reason) {
688
+ return new MalformedMessageError(reason);
689
+ }
690
+ defaultSource(message) {
691
+ return defaultSource(message);
692
+ }
693
+ /** None: AMQP messages carry no ordering key. */
694
+ defaultKey() {
695
+ return null;
696
+ }
697
+ /** The `timestamp` property, else the time of receipt. */
698
+ defaultTime(message) {
699
+ return propertyTimeOrNow(message.properties);
700
+ }
701
+ };
702
+ var RABBITMQ_EVENTS = new RabbitMqDecodedEvents();
703
+ function completeDecodedEvent(decoded, message) {
704
+ return RABBITMQ_EVENTS.complete(decoded, message);
705
+ }
601
706
  function parseJsonBody(content) {
602
707
  let text2;
603
708
  try {
@@ -642,19 +747,6 @@ function readForeignHeaders(properties) {
642
747
  )
643
748
  );
644
749
  }
645
- function resolveText(option, { message, name }) {
646
- const value = typeof option === "function" ? option(message) : option;
647
- if (value === void 0 || value === null) {
648
- return void 0;
649
- }
650
- if (typeof value !== "string") {
651
- observeRejection(value);
652
- throw new MalformedMessageError(
653
- `The ${name} option returned ${typeof value}, expected a string`
654
- );
655
- }
656
- return value.length === 0 ? void 0 : value;
657
- }
658
750
  function resolveOccurredAt(option, message) {
659
751
  const value = option?.(message);
660
752
  if (value === void 0) {
@@ -666,11 +758,6 @@ function resolveOccurredAt(option, message) {
666
758
  }
667
759
  return value;
668
760
  }
669
- function observeRejection(value) {
670
- if (typeof value === "object" && value !== null && "then" in value) {
671
- Promise.resolve(value).catch(() => void 0);
672
- }
673
- }
674
761
 
675
762
  // src/codecs.ts
676
763
  var cloudEventsBinary = Object.freeze({
@@ -722,6 +809,12 @@ function requireMessageId(message) {
722
809
  }
723
810
  return id;
724
811
  }
812
+ function decodeRabbitMqMessage(codec, message) {
813
+ if (codec === cloudEventsBinary) {
814
+ return decodeEnvelope(message);
815
+ }
816
+ return completeDecodedEvent(codec.decode(message), message);
817
+ }
725
818
  var REDELIVERIES_HEADER = "x-relaybus-redeliveries";
726
819
  var FIRST_FAILURE_HEADER = "x-relaybus-first-failure-at";
727
820
  var DEAD_LETTER_REASON_HEADER = "x-relaybus-dead-letter-reason";
@@ -841,7 +934,7 @@ function resolveSettings(options) {
841
934
  };
842
935
  }
843
936
  function resolveBasics(options) {
844
- const exchange = nonEmpty(options.exchange ?? "relaybus", "exchange");
937
+ const exchange = NonEmptyText.of(options.exchange ?? "relaybus", "exchange");
845
938
  const failOnConnectError = options.failOnConnectError ?? true;
846
939
  if (!failOnConnectError && options.reconnect === false) {
847
940
  throw new ConfigurationError(
@@ -849,7 +942,7 @@ function resolveBasics(options) {
849
942
  );
850
943
  }
851
944
  const ordered = options.ordered ?? false;
852
- const prefetch = assertPositiveInteger(
945
+ const prefetch = PositiveInteger.of(
853
946
  options.prefetch ?? (ordered ? 1 : DEFAULT_PREFETCH),
854
947
  "prefetch"
855
948
  );
@@ -861,19 +954,19 @@ function resolveBasics(options) {
861
954
  }
862
955
  function resolveTopology(options, { exchange, prefetch }) {
863
956
  return {
864
- name: nonEmpty(options.name ?? "rabbitmq", "name"),
957
+ name: NonEmptyText.of(options.name ?? "rabbitmq", "name"),
865
958
  connect: resolveConnect(options),
866
959
  exchange,
867
960
  prefetch,
868
- queuePrefix: nonEmpty(options.queuePrefix ?? "relaybus", "queuePrefix"),
869
- consumerGroup: optionalNonEmpty(options.consumerGroup, "consumerGroup"),
870
- queue: optionalNonEmpty(options.queue, "queue"),
871
- deadLetterExchange: nonEmpty(
961
+ queuePrefix: NonEmptyText.of(options.queuePrefix ?? "relaybus", "queuePrefix"),
962
+ consumerGroup: NonEmptyText.optional(options.consumerGroup, "consumerGroup"),
963
+ queue: NonEmptyText.optional(options.queue, "queue"),
964
+ deadLetterExchange: NonEmptyText.of(
872
965
  options.deadLetterExchange ?? `${exchange}.dead`,
873
966
  "deadLetterExchange"
874
967
  ),
875
- deadLetterQueue: optionalNonEmpty(options.deadLetterQueue, "deadLetterQueue"),
876
- deliveryLimit: assertPositiveInteger(
968
+ deadLetterQueue: NonEmptyText.optional(options.deadLetterQueue, "deadLetterQueue"),
969
+ deliveryLimit: PositiveInteger.of(
877
970
  options.deliveryLimit ?? DEFAULT_DELIVERY_LIMIT,
878
971
  "deliveryLimit"
879
972
  )
@@ -884,7 +977,7 @@ function resolveRetries(options, { ordered, redelivery }) {
884
977
  handlerRetry: createRetryPolicy(options.handlerRetry, DEFAULT_HANDLER_RETRY),
885
978
  redelivery,
886
979
  retryLadder: ordered ? [] : buildRetryLadder(redelivery),
887
- redeliveryMaxAgeMs: positiveDuration(
980
+ redeliveryMaxAgeMs: Milliseconds.positive(
888
981
  options.redelivery?.maxAge ?? DEFAULT_REDELIVERY_MAX_AGE,
889
982
  "redelivery.maxAge"
890
983
  ),
@@ -896,8 +989,8 @@ function resolveRuntime(options, { failOnConnectError }) {
896
989
  reconnect: resolveReconnect(options.reconnect),
897
990
  failOnConnectError,
898
991
  allowUnroutable: options.allowUnroutable ?? false,
899
- publishTimeoutMs: positiveDuration(options.publishTimeout ?? "30s", "publishTimeout"),
900
- shutdownTimeoutMs: toMilliseconds(options.shutdownTimeout ?? "30s", "shutdownTimeout"),
992
+ publishTimeoutMs: Milliseconds.positive(options.publishTimeout ?? "30s", "publishTimeout"),
993
+ shutdownTimeoutMs: Milliseconds.of(options.shutdownTimeout ?? "30s", "shutdownTimeout"),
901
994
  ...resolveCodec(options),
902
995
  logger: options.logger ?? console,
903
996
  random: options.random ?? Math.random
@@ -927,7 +1020,7 @@ function resolveCodec({
927
1020
  }
928
1021
  return {
929
1022
  encode: chosen.encode,
930
- decode: (message) => completeDecodedEvent(chosen.decode(message), message)
1023
+ decode: (message) => decodeRabbitMqMessage(chosen, message)
931
1024
  };
932
1025
  }
933
1026
  function resolveConnect(options) {
@@ -950,22 +1043,6 @@ function resolveReconnect(reconnect) {
950
1043
  }
951
1044
  return createRetryPolicy({ ...reconnect, attempts: 1 }, DEFAULT_RECONNECT);
952
1045
  }
953
- function nonEmpty(value, name) {
954
- if (value.length === 0) {
955
- throw new ConfigurationError(`${name} must not be empty`);
956
- }
957
- return value;
958
- }
959
- function optionalNonEmpty(value, name) {
960
- return value === void 0 ? void 0 : nonEmpty(value, name);
961
- }
962
- function positiveDuration(value, name) {
963
- const milliseconds = toMilliseconds(value, name);
964
- if (milliseconds === 0) {
965
- throw new ConfigurationError(`${name} must be greater than zero`);
966
- }
967
- return milliseconds;
968
- }
969
1046
 
970
1047
  // src/topology.ts
971
1048
  function retryQueueName(queue, delayMs) {
@@ -1308,20 +1385,22 @@ var SerialQueue = class {
1308
1385
  };
1309
1386
 
1310
1387
  // src/rabbitmq-transport.ts
1311
- var NACK_MESSAGE = "message nacked";
1312
- var RabbitMqTransport = class {
1388
+ var RabbitMqTransport = class extends SubscribingTransport {
1313
1389
  name;
1314
1390
  system = "rabbitmq";
1315
1391
  #settings;
1316
1392
  #supervisor;
1393
+ #publishing;
1317
1394
  /** Serializes topology work so a subscription never races a reconnect. */
1318
1395
  #topologyWork = new SerialQueue();
1319
1396
  #live;
1320
1397
  #subscription;
1321
1398
  #closing;
1322
1399
  constructor(options) {
1400
+ super();
1323
1401
  this.#settings = resolveSettings(options);
1324
1402
  this.name = this.#settings.name;
1403
+ this.#publishing = new ConfirmedPublishing(this.name, this.#settings.publishTimeoutMs);
1325
1404
  this.#supervisor = new ConnectionSupervisor({
1326
1405
  connect: this.#settings.connect,
1327
1406
  reconnect: this.#settings.reconnect,
@@ -1363,7 +1442,7 @@ var RabbitMqTransport = class {
1363
1442
  async publish(envelope, destination) {
1364
1443
  assertShortString("Destination (routing key)", destination);
1365
1444
  const { content, options } = this.#settings.encode(envelope);
1366
- await this.#publishConfirmed({
1445
+ await this.#publishing.publish(this.#live?.publisher, {
1367
1446
  exchange: this.#settings.exchange,
1368
1447
  routingKey: destination,
1369
1448
  content,
@@ -1371,63 +1450,16 @@ var RabbitMqTransport = class {
1371
1450
  mandatory: !this.#settings.allowUnroutable
1372
1451
  });
1373
1452
  }
1374
- async #publishConfirmed(input) {
1375
- const { exchange, routingKey, content, options, mandatory } = input;
1376
- const publisher = this.#live?.publisher;
1377
- const eventId = options.messageId ?? "unknown";
1378
- if (publisher === void 0) {
1379
- throw new RabbitMqUnavailableError(
1380
- `[RabbitMqTransport](publish) Not connected to the broker | transport=${this.name} destination=${routingKey}`
1381
- );
1382
- }
1383
- const returnKey = unroutableKey(options.messageId, routingKey);
1384
- await new Promise((resolve, reject) => {
1385
- const cancelTimeout = schedule(systemClock, {
1386
- run: () => {
1387
- reject(
1388
- new RabbitMqUnavailableError(
1389
- `[RabbitMqTransport](publish) No broker confirm within ${this.#settings.publishTimeoutMs}ms | transport=${this.name} eventId=${eventId} destination=${routingKey}`
1390
- )
1391
- );
1392
- },
1393
- delayMs: this.#settings.publishTimeoutMs
1394
- });
1395
- const onConfirm = (error) => {
1396
- cancelTimeout();
1397
- const returned = takeUnroutable(publisher.unroutable, returnKey);
1398
- if (error) {
1399
- reject(this.#toPublishError(error, { eventId, destination: routingKey }));
1400
- } else if (returned) {
1401
- reject(
1402
- new UnroutableMessageError(
1403
- `[RabbitMqTransport](publish) No queue is bound to the routing key | transport=${this.name} eventId=${eventId} destination=${routingKey}`
1404
- )
1405
- );
1406
- } else {
1407
- resolve();
1408
- }
1409
- };
1410
- try {
1411
- publisher.channel.publish(
1412
- exchange,
1413
- routingKey,
1414
- content,
1415
- { ...options, mandatory },
1416
- onConfirm
1417
- );
1418
- } catch (error) {
1419
- cancelTimeout();
1420
- reject(this.#toPublishError(error, { eventId, destination: routingKey }));
1421
- }
1422
- });
1423
- }
1424
1453
  /**
1425
1454
  * Binds one durable quorum queue to `destinations` and delivers its messages
1426
1455
  * to `onMessage`. Call it once per transport; give each subscription its own
1427
1456
  * transport instance, with its own `name` and `consumerGroup`.
1428
1457
  */
1429
- // eslint-disable-next-line relaybus/max-two-params -- the Transport.subscribe signature
1430
- async subscribe(destinations, onMessage, hooks) {
1458
+ async openSubscription({
1459
+ destinations,
1460
+ onMessage,
1461
+ hooks
1462
+ }) {
1431
1463
  if (this.#subscription !== void 0) {
1432
1464
  throw new ConfigurationError(
1433
1465
  `Transport '${this.name}' already has a subscription; create another transport for another consumer group`
@@ -1438,34 +1470,8 @@ var RabbitMqTransport = class {
1438
1470
  }
1439
1471
  const queue = resolveQueueName(this.#settings);
1440
1472
  const subscription = {
1441
- topology: {
1442
- exchange: this.#settings.exchange,
1443
- queue,
1444
- deadLetterExchange: this.#settings.deadLetterExchange,
1445
- deadLetterQueue: this.#settings.deadLetterQueue ?? `${queue}.dead`,
1446
- deliveryLimit: this.#settings.deliveryLimit,
1447
- ordered: this.#settings.ordered,
1448
- retryDelaysMs: this.#settings.retryLadder,
1449
- destinations
1450
- },
1451
- consumer: new QueueConsumer({
1452
- queue,
1453
- prefetch: this.#settings.prefetch,
1454
- handlerRetry: this.#settings.handlerRetry,
1455
- redelivery: this.#settings.redelivery,
1456
- retryLadder: this.#settings.retryLadder,
1457
- redeliveryMaxAgeMs: this.#settings.redeliveryMaxAgeMs,
1458
- ordered: this.#settings.ordered,
1459
- deadLetterExchange: this.#settings.deadLetterExchange,
1460
- republish: ({ exchange, routingKey }, { content, options }) => this.#publishConfirmed({ exchange, routingKey, content, options, mandatory: true }),
1461
- shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
1462
- onMessage,
1463
- ...hooks === void 0 ? {} : { onInvalidMessage: hooks.onInvalidMessage },
1464
- decode: this.#settings.decode,
1465
- onConsumerLost: (reason) => this.#supervisor.recover(reason),
1466
- logger: this.#settings.logger,
1467
- random: this.#settings.random
1468
- })
1473
+ topology: this.#topologyOf(queue, destinations),
1474
+ consumer: this.#createConsumer(queue, { onMessage, hooks })
1469
1475
  };
1470
1476
  await this.#topologyWork.run(async () => {
1471
1477
  this.#subscription = subscription;
@@ -1474,6 +1480,44 @@ var RabbitMqTransport = class {
1474
1480
  }
1475
1481
  });
1476
1482
  }
1483
+ #topologyOf(queue, destinations) {
1484
+ return {
1485
+ exchange: this.#settings.exchange,
1486
+ queue,
1487
+ deadLetterExchange: this.#settings.deadLetterExchange,
1488
+ deadLetterQueue: this.#settings.deadLetterQueue ?? `${queue}.dead`,
1489
+ deliveryLimit: this.#settings.deliveryLimit,
1490
+ ordered: this.#settings.ordered,
1491
+ retryDelaysMs: this.#settings.retryLadder,
1492
+ destinations
1493
+ };
1494
+ }
1495
+ #createConsumer(queue, { onMessage, hooks }) {
1496
+ return new QueueConsumer({
1497
+ queue,
1498
+ prefetch: this.#settings.prefetch,
1499
+ handlerRetry: this.#settings.handlerRetry,
1500
+ redelivery: this.#settings.redelivery,
1501
+ retryLadder: this.#settings.retryLadder,
1502
+ redeliveryMaxAgeMs: this.#settings.redeliveryMaxAgeMs,
1503
+ ordered: this.#settings.ordered,
1504
+ deadLetterExchange: this.#settings.deadLetterExchange,
1505
+ republish: ({ exchange, routingKey }, { content, options }) => this.#publishing.publish(this.#live?.publisher, {
1506
+ exchange,
1507
+ routingKey,
1508
+ content,
1509
+ options,
1510
+ mandatory: true
1511
+ }),
1512
+ shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
1513
+ onMessage,
1514
+ ...hooks === void 0 ? {} : { onInvalidMessage: hooks.onInvalidMessage },
1515
+ decode: this.#settings.decode,
1516
+ onConsumerLost: (reason) => this.#supervisor.recover(reason),
1517
+ logger: this.#settings.logger,
1518
+ random: this.#settings.random
1519
+ });
1520
+ }
1477
1521
  /**
1478
1522
  * Stops consuming, waits up to `shutdownTimeout` for running handlers
1479
1523
  * (messages still being retried go back to the queue), then closes the
@@ -1543,37 +1587,8 @@ var RabbitMqTransport = class {
1543
1587
  this.#live = void 0;
1544
1588
  this.#subscription?.consumer.detach();
1545
1589
  }
1546
- #toPublishError(error, { eventId, destination }) {
1547
- const { message } = toErrorInfo(error);
1548
- const detail = `transport=${this.name} eventId=${eventId} destination=${destination} error=${message}`;
1549
- return message === NACK_MESSAGE ? new RabbitMqPublishRejectedError(
1550
- `[RabbitMqTransport](publish) Broker nacked the message | ${detail}`,
1551
- { cause: error }
1552
- ) : new RabbitMqUnavailableError(`[RabbitMqTransport](publish) Publish failed | ${detail}`, {
1553
- cause: error
1554
- });
1555
- }
1556
1590
  };
1557
- function unroutableKey(messageId, routingKey) {
1558
- return `${String(messageId)}
1559
- ${routingKey}`;
1560
- }
1561
- function recordUnroutable(unroutable, key) {
1562
- unroutable.set(key, (unroutable.get(key) ?? 0) + 1);
1563
- }
1564
- function takeUnroutable(unroutable, key) {
1565
- const count = unroutable.get(key);
1566
- if (count === void 0) {
1567
- return false;
1568
- }
1569
- if (count === 1) {
1570
- unroutable.delete(key);
1571
- } else {
1572
- unroutable.set(key, count - 1);
1573
- }
1574
- return true;
1575
- }
1576
1591
 
1577
- export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, RabbitMqUnencodableEventError, UnroutableMessageError, UnserializablePayloadError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, jsonEnvelope, plainJsonDecoder };
1592
+ export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, RabbitMqUnencodableEventError, UnroutableMessageError, UnserializablePayloadError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, decodeRabbitMqMessage, jsonEnvelope, plainJsonDecoder };
1578
1593
  //# sourceMappingURL=index.js.map
1579
1594
  //# sourceMappingURL=index.js.map