@zdavison/matador 3.0.8 → 4.0.1

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.
Files changed (40) hide show
  1. package/dist/core/fanout.d.ts +3 -2
  2. package/dist/core/fanout.d.ts.map +1 -1
  3. package/dist/core/fanout.js +4 -4
  4. package/dist/core/fanout.test.js +32 -24
  5. package/dist/core/matador.d.ts.map +1 -1
  6. package/dist/core/matador.js +16 -7
  7. package/dist/core/matador.test.js +32 -1
  8. package/dist/errors/index.d.ts +1 -1
  9. package/dist/errors/index.d.ts.map +1 -1
  10. package/dist/errors/index.js +2 -2
  11. package/dist/errors/matador-errors.d.ts +28 -0
  12. package/dist/errors/matador-errors.d.ts.map +1 -1
  13. package/dist/errors/matador-errors.js +41 -0
  14. package/dist/index.cjs +327 -80
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +3 -3
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/topology/builder.d.ts +28 -1
  22. package/dist/topology/builder.d.ts.map +1 -1
  23. package/dist/topology/builder.js +74 -0
  24. package/dist/topology/builder.test.js +188 -1
  25. package/dist/topology/index.d.ts +2 -2
  26. package/dist/topology/index.d.ts.map +1 -1
  27. package/dist/topology/index.js +1 -1
  28. package/dist/topology/types.d.ts +120 -6
  29. package/dist/topology/types.d.ts.map +1 -1
  30. package/dist/topology/types.js +50 -10
  31. package/dist/transport/local/local-transport.d.ts.map +1 -1
  32. package/dist/transport/local/local-transport.js +5 -2
  33. package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.d.ts +2 -0
  34. package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.d.ts.map +1 -0
  35. package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.js +110 -0
  36. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +24 -0
  37. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
  38. package/dist/transport/rabbitmq/rabbitmq-transport.js +145 -68
  39. package/dist/transport/rabbitmq/rabbitmq-transport.test.js +130 -0
  40. package/package.json +6 -2
package/dist/index.cjs CHANGED
@@ -179,6 +179,19 @@ var TransportSendError = class extends MatadorError {
179
179
  };
180
180
  }
181
181
  };
182
+ var SomeSendError = class extends MatadorError {
183
+ constructor(eventKey, errors) {
184
+ const summary = errors.map((e) => `${e.subscriberName} (${e.queue}): ${e.error.message}`).join("; ");
185
+ super(
186
+ `send("${eventKey}") failed for ${errors.length} subscriber(s): ${summary}`
187
+ );
188
+ this.eventKey = eventKey;
189
+ this.errors = errors;
190
+ }
191
+ eventKey;
192
+ errors;
193
+ description = "One or more subscriber messages could not be published during send(). ACTION: Inspect the `errors` array for per-failure details. Each entry contains the subscriber name, target queue, and underlying error.";
194
+ };
182
195
  var DelayedMessagesNotSupportedError = class extends MatadorError {
183
196
  constructor(transportName) {
184
197
  super(
@@ -249,6 +262,16 @@ var LocalTransportCannotProcessStubError = class extends MatadorError {
249
262
  subscriberName;
250
263
  description = "The LocalTransport cannot process events for SubscriberStubs. ACTION: SubscriberStubs represent remote implementations that only RabbitMQ can route. If using LocalTransport for testing, provide mock implementations instead of stubs. For production fallback scenarios, be aware that stub-targeted events will be dropped.";
251
264
  };
265
+ var UnknownQueueReferenceError = class extends MatadorError {
266
+ constructor(queueName) {
267
+ super(
268
+ `Queue reference "${queueName}" is not declared in the topology. Add it via .addQueue(name), or .addQueue({ name, exact: true }) for a foreign queue.`
269
+ );
270
+ this.queueName = queueName;
271
+ }
272
+ queueName;
273
+ description = "A queue was referenced that is not declared in the topology. Matador only routes to queues it knows about; an unknown reference would otherwise be silently namespace-qualified and published to a queue nobody consumes, losing the message. ACTION: Declare the queue on the topology before referencing it. For a Matador-owned queue use `.addQueue(name)`; for a foreign queue owned by another service use `.addQueue({ name, exact: true })` so it is routed to verbatim.";
274
+ };
252
275
  var QueueNotFoundError = class extends MatadorError {
253
276
  constructor(queueName) {
254
277
  super(
@@ -318,6 +341,9 @@ function isEventNotRegisteredError(error) {
318
341
  function isSubscriberNotRegisteredError(error) {
319
342
  return error instanceof SubscriberNotRegisteredError;
320
343
  }
344
+ function isUnknownQueueReferenceError(error) {
345
+ return error instanceof UnknownQueueReferenceError;
346
+ }
321
347
  function isMessageMaybePoisonedError(error) {
322
348
  return error instanceof MessageMaybePoisonedError;
323
349
  }
@@ -360,20 +386,41 @@ function isCheckpointStoreError(error) {
360
386
  }
361
387
 
362
388
  // src/topology/types.ts
363
- function getQualifiedQueueName(namespace, queueName) {
364
- return `${namespace}.${queueName}`;
389
+ function applyPrefix(prefix, name) {
390
+ return prefix == null ? name : `${prefix}.${name}`;
365
391
  }
366
- function getDeadLetterQueueName(namespace, queueName, dlqType) {
367
- return `${namespace}.${queueName}.${dlqType}`;
392
+ function getQualifiedQueueName(namespace, queueName, naming, prefix) {
393
+ return naming?.queue?.(namespace, queueName) ?? applyPrefix(prefix, `${namespace}.${queueName}`);
368
394
  }
369
- function getRetryQueueName(namespace, queueName) {
370
- return `${namespace}.${queueName}.retry`;
395
+ function getDeadLetterQueueName(namespace, queueName, dlqType, naming, prefix) {
396
+ return `${getQualifiedQueueName(namespace, queueName, naming, prefix)}.${dlqType}`;
371
397
  }
372
- function resolveQueueName(namespace, queueDef) {
398
+ function getRetryQueueName(namespace, queueName, naming, prefix) {
399
+ return `${getQualifiedQueueName(namespace, queueName, naming, prefix)}.retry`;
400
+ }
401
+ function resolveQueueName(namespace, queueDef, naming, prefix) {
373
402
  if (queueDef.exact) {
374
403
  return queueDef.name;
375
404
  }
376
- return `${namespace}.${queueDef.name}`;
405
+ return getQualifiedQueueName(namespace, queueDef.name, naming, prefix);
406
+ }
407
+ function findQueueDefinition(topology, queueName) {
408
+ return topology.queues.find((q) => q.name === queueName);
409
+ }
410
+ function resolveTargetQueueName(topology, queueName) {
411
+ const def = findQueueDefinition(topology, queueName);
412
+ if (def === void 0) {
413
+ throw new UnknownQueueReferenceError(queueName);
414
+ }
415
+ if (def.exact) {
416
+ return def.name;
417
+ }
418
+ return getQualifiedQueueName(
419
+ topology.namespace,
420
+ queueName,
421
+ topology.naming,
422
+ topology.prefix
423
+ );
377
424
  }
378
425
 
379
426
  // src/topology/builder.ts
@@ -408,6 +455,8 @@ var TopologyBuilder = class _TopologyBuilder {
408
455
  maxDelayMs: 3e5
409
456
  // 5 minutes
410
457
  };
458
+ naming;
459
+ prefix = "matador";
411
460
  /**
412
461
  * Sets the namespace prefix for all queues.
413
462
  */
@@ -415,6 +464,37 @@ var TopologyBuilder = class _TopologyBuilder {
415
464
  this.namespace = namespace;
416
465
  return this;
417
466
  }
467
+ /**
468
+ * Sets the prefix prepended to every default-derived broker resource name
469
+ * (e.g. `matador.{namespace}.{queue}`), so Matador-managed queues and
470
+ * exchanges are identifiable. Pass `null` to disable prefixing.
471
+ *
472
+ * Only applies to default names — a {@link withNaming} override fully owns
473
+ * its output and is never prefixed.
474
+ *
475
+ * Pass `null` or `undefined` to disable prefixing.
476
+ *
477
+ * @default 'matador'
478
+ * @example
479
+ * TopologyBuilder.create().withNamespace('myapp') // matador.myapp.events
480
+ * TopologyBuilder.create().withNamespace('myapp').withGlobalPrefix('acme') // acme.myapp.events
481
+ * TopologyBuilder.create().withNamespace('myapp').withGlobalPrefix(null) // myapp.events
482
+ */
483
+ withGlobalPrefix(prefix) {
484
+ this.prefix = prefix;
485
+ return this;
486
+ }
487
+ /**
488
+ * Overrides how broker resource names are derived.
489
+ * Use during migrations to keep pre-existing queue/exchange names so a
490
+ * rolling deploy keeps routing through the resources already declared on
491
+ * the broker.
492
+ * @see TopologyNaming
493
+ */
494
+ withNaming(naming) {
495
+ this.naming = naming;
496
+ return this;
497
+ }
418
498
  addQueue(nameOrDefinition, options = {}) {
419
499
  if (isQueueDefinition(nameOrDefinition)) {
420
500
  this.queues.push(nameOrDefinition);
@@ -473,8 +553,10 @@ var TopologyBuilder = class _TopologyBuilder {
473
553
  validate() {
474
554
  return [
475
555
  ...validateNamespace(this.namespace),
556
+ ...validatePrefix(this.prefix),
476
557
  ...validateQueues(this.queues),
477
- ...validateRetry(this.retry)
558
+ ...validateRetry(this.retry),
559
+ ...validateNaming(this.naming)
478
560
  ];
479
561
  }
480
562
  /**
@@ -493,7 +575,9 @@ var TopologyBuilder = class _TopologyBuilder {
493
575
  namespace: this.namespace,
494
576
  queues: [...this.queues],
495
577
  deadLetter: this.deadLetter,
496
- retry: this.retry
578
+ retry: this.retry,
579
+ naming: this.naming,
580
+ prefix: this.prefix
497
581
  };
498
582
  }
499
583
  };
@@ -509,6 +593,20 @@ function validateNamespace(namespace) {
509
593
  }
510
594
  return [];
511
595
  }
596
+ function validatePrefix(prefix) {
597
+ if (prefix == null) {
598
+ return [];
599
+ }
600
+ if (prefix.trim() === "") {
601
+ return ["Prefix must be a non-empty string, or null to disable prefixing"];
602
+ }
603
+ if (!IDENTIFIER_PATTERN.test(prefix)) {
604
+ return [
605
+ "Prefix must start with a letter and contain only alphanumeric characters, underscores, and hyphens"
606
+ ];
607
+ }
608
+ return [];
609
+ }
512
610
  function validateQueueName(queue, seen) {
513
611
  if (!queue.name || queue.name.trim() === "") {
514
612
  return ["Queue name cannot be empty"];
@@ -546,6 +644,27 @@ function validateQueues(queues) {
546
644
  }
547
645
  return issues;
548
646
  }
647
+ function validateNaming(naming) {
648
+ if (!naming) return [];
649
+ const issues = [];
650
+ for (const field of [
651
+ "queue",
652
+ "mainExchange",
653
+ "dlxExchange",
654
+ "delayedExchange"
655
+ ]) {
656
+ const value = naming[field];
657
+ if (value !== void 0 && typeof value !== "function") {
658
+ issues.push(`Naming override "${field}" must be a function`);
659
+ }
660
+ }
661
+ if (naming.dlxExchangeType !== void 0 && naming.dlxExchangeType !== "direct" && naming.dlxExchangeType !== "topic") {
662
+ issues.push(
663
+ `Naming override "dlxExchangeType" must be 'direct' or 'topic'`
664
+ );
665
+ }
666
+ return issues;
667
+ }
549
668
  function validateRetry(retry) {
550
669
  if (!retry.enabled) return [];
551
670
  const issues = [];
@@ -686,14 +805,14 @@ var FanoutEngine = class {
686
805
  transport;
687
806
  schema;
688
807
  hooks;
689
- namespace;
808
+ topology;
690
809
  defaultQueue;
691
810
  enqueuingCount = 0;
692
811
  constructor(config) {
693
812
  this.transport = config.transport;
694
813
  this.schema = config.schema;
695
814
  this.hooks = config.hooks;
696
- this.namespace = config.namespace;
815
+ this.topology = config.topology;
697
816
  this.defaultQueue = config.defaultQueue;
698
817
  }
699
818
  /**
@@ -720,7 +839,7 @@ var FanoutEngine = class {
720
839
  continue;
721
840
  }
722
841
  const targetQueue = subscriber.targetQueue ?? this.defaultQueue;
723
- const qualifiedQueue = getQualifiedQueueName(this.namespace, targetQueue);
842
+ const qualifiedQueue = resolveTargetQueueName(this.topology, targetQueue);
724
843
  const envelope = createEnvelope({
725
844
  eventKey,
726
845
  eventDescription: eventClass.description,
@@ -2060,7 +2179,7 @@ var Matador = class {
2060
2179
  transport: this.transport,
2061
2180
  schema: this.schema,
2062
2181
  hooks: this.hooks,
2063
- namespace: this.topology.namespace,
2182
+ topology: this.topology,
2064
2183
  defaultQueue
2065
2184
  });
2066
2185
  this.shutdownManager = new ShutdownManager(
@@ -2134,13 +2253,14 @@ var Matador = class {
2134
2253
  this.hooks.logger.info(
2135
2254
  `[Matador] \u{1F7E2} Worker subscribing to '${this.consumeFrom.join(",")}'.`
2136
2255
  );
2256
+ } else {
2257
+ this.hooks.logger.info(
2258
+ "[Matador] \u{1F7E1} Worker not subscribing to any queues (consumeFrom is empty)."
2259
+ );
2137
2260
  }
2138
2261
  for (const queueName of this.consumeFrom) {
2139
- const qualifiedName = getQualifiedQueueName(
2140
- this.topology.namespace,
2141
- queueName
2142
- );
2143
- const queueDef = this.topology.queues.find((q) => q.name === queueName);
2262
+ const qualifiedName = resolveTargetQueueName(this.topology, queueName);
2263
+ const queueDef = findQueueDefinition(this.topology, queueName);
2144
2264
  const subscription = await this.transport.subscribe(
2145
2265
  qualifiedName,
2146
2266
  async (envelope, receipt) => {
@@ -2171,12 +2291,18 @@ var Matador = class {
2171
2291
  const data = dataOrOptions;
2172
2292
  const options2 = maybeOptions;
2173
2293
  const event2 = new eventClass2(data);
2174
- return this.fanout.send(eventClass2, event2, options2);
2294
+ const result2 = await this.fanout.send(eventClass2, event2, options2);
2295
+ if (result2.errors.length > 0)
2296
+ throw new SomeSendError(result2.eventKey, result2.errors);
2297
+ return result2;
2175
2298
  }
2176
2299
  const event = eventOrClass;
2177
2300
  const options = dataOrOptions;
2178
2301
  const eventClass = event.constructor;
2179
- return this.fanout.send(eventClass, event, options);
2302
+ const result = await this.fanout.send(eventClass, event, options);
2303
+ if (result.errors.length > 0)
2304
+ throw new SomeSendError(result.eventKey, result.errors);
2305
+ return result;
2180
2306
  }
2181
2307
  /**
2182
2308
  * Gets current handler state.
@@ -2437,7 +2563,12 @@ var LocalTransport = class {
2437
2563
  throw new TransportNotConnectedError(this.name, "applyTopology");
2438
2564
  }
2439
2565
  for (const queueDef of topology.queues) {
2440
- const queueName = `${topology.namespace}.${queueDef.name}`;
2566
+ const queueName = resolveQueueName(
2567
+ topology.namespace,
2568
+ queueDef,
2569
+ topology.naming,
2570
+ topology.prefix
2571
+ );
2441
2572
  if (!this.queues.has(queueName)) {
2442
2573
  this.queues.set(queueName, []);
2443
2574
  }
@@ -2483,7 +2614,12 @@ var LocalTransport = class {
2483
2614
  }
2484
2615
  async deliverToSubscribers(queue, message) {
2485
2616
  const subs = this.subscriptions.get(queue);
2486
- if (!subs) return;
2617
+ if (!subs) {
2618
+ this.logger.warn(
2619
+ `[Matador][LocalTransport] \u{1F7E1} No subscriptions found for queue '${queue}', message will be lost.`
2620
+ );
2621
+ return;
2622
+ }
2487
2623
  for (const sub of subs) {
2488
2624
  if (!sub.active || message.completed) continue;
2489
2625
  const receipt = {
@@ -2773,6 +2909,7 @@ var RabbitMQTransport = class {
2773
2909
  publishChannel = null;
2774
2910
  connectionManager;
2775
2911
  queueChannels = /* @__PURE__ */ new Map();
2912
+ subscriptionIntents = [];
2776
2913
  topology = null;
2777
2914
  codec = new RabbitMQCodec();
2778
2915
  config;
@@ -2786,7 +2923,8 @@ var RabbitMQTransport = class {
2786
2923
  connection: config.connection ?? {},
2787
2924
  quorumQueues: config.quorumQueues ?? true,
2788
2925
  defaultPrefetch: config.defaultPrefetch ?? 10,
2789
- enableDelayedMessages: config.enableDelayedMessages ?? true
2926
+ enableDelayedMessages: config.enableDelayedMessages ?? true,
2927
+ publishTimeoutMs: config.publishTimeoutMs ?? 5e3
2790
2928
  };
2791
2929
  this.connectionManager = new ConnectionManager(
2792
2930
  () => this.doConnect(),
@@ -2809,14 +2947,18 @@ var RabbitMQTransport = class {
2809
2947
  throw new TransportNotConnectedError(this.name, "applyTopology");
2810
2948
  }
2811
2949
  const channel = this.publishChannel;
2812
- const mainExchange = this.getMainExchangeName(topology.namespace);
2950
+ const mainExchange = this.getMainExchangeName(topology);
2813
2951
  await channel.assertExchange(mainExchange, "direct", { durable: true });
2814
- const dlxExchange = this.getDLXExchangeName(topology.namespace);
2952
+ const dlxExchange = this.getDLXExchangeName(topology);
2815
2953
  if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
2816
- await channel.assertExchange(dlxExchange, "direct", { durable: true });
2954
+ await channel.assertExchange(
2955
+ dlxExchange,
2956
+ topology.naming?.dlxExchangeType ?? "direct",
2957
+ { durable: true }
2958
+ );
2817
2959
  }
2818
2960
  if (this.config.enableDelayedMessages) {
2819
- await this.setupDelayedExchange(topology.namespace);
2961
+ await this.setupDelayedExchange(topology);
2820
2962
  }
2821
2963
  for (const queueDef of topology.queues) {
2822
2964
  await this.assertWorkQueue(channel, topology, queueDef);
@@ -2848,14 +2990,13 @@ var RabbitMQTransport = class {
2848
2990
  if (!this.delayedExchangeAvailable) {
2849
2991
  throw new DelayedMessagesNotSupportedError(this.name);
2850
2992
  }
2851
- const delayedExchange = this.getDelayedExchangeName(
2852
- this.topology.namespace
2853
- );
2993
+ const delayedExchange = this.getDelayedExchangeName(this.topology);
2854
2994
  publishOptions.headers = {
2855
2995
  ...publishOptions.headers,
2856
2996
  "x-delay": options.delay
2857
2997
  };
2858
- this.publishChannel.publish(
2998
+ await this.confirmPublish(
2999
+ this.publishChannel,
2859
3000
  delayedExchange,
2860
3001
  queue,
2861
3002
  buffer,
@@ -2870,14 +3011,67 @@ var RabbitMQTransport = class {
2870
3011
  publishOptions.persistent = options.transport.rabbitmq.persistent;
2871
3012
  }
2872
3013
  const routingKey = options?.transport?.rabbitmq?.routingKey ?? queue;
2873
- const exchange = this.getMainExchangeName(this.topology.namespace);
2874
- this.publishChannel.publish(exchange, routingKey, buffer, publishOptions);
3014
+ const exchange = this.getMainExchangeName(this.topology);
3015
+ await this.confirmPublish(
3016
+ this.publishChannel,
3017
+ exchange,
3018
+ routingKey,
3019
+ buffer,
3020
+ publishOptions
3021
+ );
2875
3022
  return this.name;
2876
3023
  }
2877
3024
  async subscribe(queue, handler, options = {}) {
2878
3025
  if (!this.connection || !this.topology) {
2879
3026
  throw new TransportNotConnectedError(this.name, "subscribe");
2880
3027
  }
3028
+ const intent = {
3029
+ queue,
3030
+ handler,
3031
+ options,
3032
+ active: true,
3033
+ currentConsumer: null
3034
+ };
3035
+ this.subscriptionIntents.push(intent);
3036
+ await this.activateIntent(intent);
3037
+ return {
3038
+ unsubscribe: async () => {
3039
+ intent.active = false;
3040
+ const idx = this.subscriptionIntents.indexOf(intent);
3041
+ if (idx !== -1) this.subscriptionIntents.splice(idx, 1);
3042
+ const consumer = intent.currentConsumer;
3043
+ if (consumer) {
3044
+ consumer.active = false;
3045
+ const queueChannel = this.queueChannels.get(queue);
3046
+ if (queueChannel) {
3047
+ try {
3048
+ await queueChannel.channel.cancel(consumer.consumerTag);
3049
+ } catch {
3050
+ }
3051
+ const cIdx = queueChannel.consumers.indexOf(consumer);
3052
+ if (cIdx !== -1) queueChannel.consumers.splice(cIdx, 1);
3053
+ if (queueChannel.consumers.length === 0) {
3054
+ try {
3055
+ await queueChannel.channel.close();
3056
+ } catch {
3057
+ }
3058
+ this.queueChannels.delete(queue);
3059
+ }
3060
+ }
3061
+ intent.currentConsumer = null;
3062
+ }
3063
+ },
3064
+ get isActive() {
3065
+ return intent.active;
3066
+ }
3067
+ };
3068
+ }
3069
+ /**
3070
+ * Wires a single subscription intent to the current connection.
3071
+ * Called once on subscribe() and again after each reconnect.
3072
+ */
3073
+ async activateIntent(intent) {
3074
+ const { queue, handler, options } = intent;
2881
3075
  const queueChannel = await this.getOrCreateQueueChannel(queue, options);
2882
3076
  const { channel } = queueChannel;
2883
3077
  const consumer = {
@@ -2913,33 +3107,10 @@ var RabbitMQTransport = class {
2913
3107
  }
2914
3108
  },
2915
3109
  { noAck: false }
2916
- // Always manually ack
2917
3110
  );
2918
3111
  consumer.consumerTag = consumerTag;
2919
3112
  queueChannel.consumers.push(consumer);
2920
- return {
2921
- unsubscribe: async () => {
2922
- consumer.active = false;
2923
- try {
2924
- await channel.cancel(consumerTag);
2925
- } catch {
2926
- }
2927
- const idx = queueChannel.consumers.indexOf(consumer);
2928
- if (idx !== -1) {
2929
- queueChannel.consumers.splice(idx, 1);
2930
- }
2931
- if (queueChannel.consumers.length === 0) {
2932
- try {
2933
- await channel.close();
2934
- } catch {
2935
- }
2936
- this.queueChannels.delete(queue);
2937
- }
2938
- },
2939
- get isActive() {
2940
- return consumer.active;
2941
- }
2942
- };
3113
+ intent.currentConsumer = consumer;
2943
3114
  }
2944
3115
  async complete(receipt) {
2945
3116
  const { channel, msg } = receipt.handle;
@@ -2963,7 +3134,7 @@ var RabbitMQTransport = class {
2963
3134
  };
2964
3135
  const encoded = this.codec.encode(dlqEnvelope);
2965
3136
  const buffer = Buffer.from(encoded.body);
2966
- const dlxExchange = this.getDLXExchangeName(this.topology.namespace);
3137
+ const dlxExchange = this.getDLXExchangeName(this.topology);
2967
3138
  const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
2968
3139
  const publishOptions = {
2969
3140
  persistent: true,
@@ -2975,7 +3146,8 @@ var RabbitMQTransport = class {
2975
3146
  "x-matador-dead-letter-reason": reason
2976
3147
  }
2977
3148
  };
2978
- this.publishChannel.publish(
3149
+ await this.confirmPublish(
3150
+ this.publishChannel,
2979
3151
  dlxExchange,
2980
3152
  dlqQueueName,
2981
3153
  buffer,
@@ -2983,6 +3155,41 @@ var RabbitMQTransport = class {
2983
3155
  );
2984
3156
  await this.complete(receipt);
2985
3157
  }
3158
+ /**
3159
+ * Publishes on the confirm channel and resolves once the broker
3160
+ * acknowledges the message, rejecting on broker nack, channel error, or
3161
+ * after `publishTimeoutMs` elapses without a confirm.
3162
+ *
3163
+ * Mirrors Matador v1's promise-wrapped amqplib publish callback to give
3164
+ * callers confirmed (at-least-once) delivery semantics.
3165
+ */
3166
+ confirmPublish(channel, exchange, routingKey, buffer, options) {
3167
+ return new Promise((resolve, reject) => {
3168
+ let timeout = setTimeout(() => {
3169
+ timeout = null;
3170
+ reject(
3171
+ new TransportSendError(
3172
+ routingKey,
3173
+ new Error(
3174
+ `Publish not confirmed by broker within ${this.config.publishTimeoutMs}ms`
3175
+ )
3176
+ )
3177
+ );
3178
+ }, this.config.publishTimeoutMs);
3179
+ channel.publish(exchange, routingKey, buffer, options, (err) => {
3180
+ if (timeout) {
3181
+ clearTimeout(timeout);
3182
+ } else {
3183
+ return;
3184
+ }
3185
+ if (err) {
3186
+ reject(new TransportSendError(routingKey, err));
3187
+ } else {
3188
+ resolve();
3189
+ }
3190
+ });
3191
+ });
3192
+ }
2986
3193
  // Private methods
2987
3194
  /**
2988
3195
  * Gets or creates a dedicated channel for a queue subscription.
@@ -3012,6 +3219,7 @@ var RabbitMQTransport = class {
3012
3219
  return queueChannel;
3013
3220
  }
3014
3221
  async doConnect() {
3222
+ this.queueChannels.clear();
3015
3223
  this.logger.info(
3016
3224
  `[Matador] \u23F3 Connecting to RabbitMQ at '${redactAmqpUrl(this.config.url)}'.`
3017
3225
  );
@@ -3023,19 +3231,28 @@ var RabbitMQTransport = class {
3023
3231
  this.logger.error("[Matador] \u{1F534} RabbitMQ connection error", err);
3024
3232
  });
3025
3233
  connection.on("close", () => {
3234
+ for (const queueChannel of this.queueChannels.values()) {
3235
+ for (const consumer of queueChannel.consumers) {
3236
+ consumer.active = false;
3237
+ }
3238
+ }
3026
3239
  if (this.connectionManager.isConnected()) {
3027
3240
  this.connectionManager.handleConnectionLost(
3028
3241
  new Error("Connection closed unexpectedly")
3029
3242
  );
3030
3243
  }
3031
3244
  });
3032
- this.publishChannel = await connection.createChannel();
3245
+ this.publishChannel = await connection.createConfirmChannel();
3033
3246
  this.publishChannel.on("error", (err) => {
3034
3247
  this.logger.error("[Matador] \u{1F534} RabbitMQ publish channel error", err);
3035
3248
  });
3036
3249
  if (this.topology) {
3037
3250
  await this.applyTopology(this.topology);
3251
+ for (const intent of this.subscriptionIntents) {
3252
+ await this.activateIntent(intent);
3253
+ }
3038
3254
  }
3255
+ this.logger.info("[Matador] \u{1F50C} Connected to RabbitMQ");
3039
3256
  }
3040
3257
  async doDisconnect() {
3041
3258
  for (const queueChannel of this.queueChannels.values()) {
@@ -3072,12 +3289,12 @@ var RabbitMQTransport = class {
3072
3289
  delayedMessages: false
3073
3290
  };
3074
3291
  }
3075
- async setupDelayedExchange(namespace) {
3292
+ async setupDelayedExchange(topology) {
3076
3293
  if (!this.connection) {
3077
3294
  return;
3078
3295
  }
3079
3296
  this.delayedExchangeAvailable = false;
3080
- const delayedExchange = this.getDelayedExchangeName(namespace);
3297
+ const delayedExchange = this.getDelayedExchangeName(topology);
3081
3298
  const connection = this.connection;
3082
3299
  return new Promise((resolve) => {
3083
3300
  let resolved = false;
@@ -3129,7 +3346,7 @@ var RabbitMQTransport = class {
3129
3346
  queueOptions.arguments["x-queue-type"] = "quorum";
3130
3347
  }
3131
3348
  if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
3132
- queueOptions.arguments["x-dead-letter-exchange"] = this.getDLXExchangeName(topology.namespace);
3349
+ queueOptions.arguments["x-dead-letter-exchange"] = this.getDLXExchangeName(topology);
3133
3350
  }
3134
3351
  if (queueDef.priorities) {
3135
3352
  queueOptions.arguments["x-max-priority"] = 10;
@@ -3140,23 +3357,39 @@ var RabbitMQTransport = class {
3140
3357
  return queueOptions;
3141
3358
  }
3142
3359
  async assertWorkQueue(channel, topology, queueDef) {
3143
- const queueName = queueDef.exact ? queueDef.name : `${topology.namespace}.${queueDef.name}`;
3360
+ const queueName = resolveQueueName(
3361
+ topology.namespace,
3362
+ queueDef,
3363
+ topology.naming,
3364
+ topology.prefix
3365
+ );
3144
3366
  const rabbitmqOptions = queueDef.transport?.rabbitmq?.options;
3145
3367
  const queueOptions = rabbitmqOptions ?? this.buildWorkQueueOptions(topology, queueDef);
3146
3368
  await channel.assertQueue(queueName, queueOptions);
3147
- const mainExchange = this.getMainExchangeName(topology.namespace);
3369
+ const mainExchange = this.getMainExchangeName(topology);
3148
3370
  await channel.bindQueue(queueName, mainExchange, queueName);
3149
3371
  if (this.delayedExchangeAvailable) {
3150
- const delayedExchange = this.getDelayedExchangeName(topology.namespace);
3372
+ const delayedExchange = this.getDelayedExchangeName(topology);
3151
3373
  await channel.bindQueue(queueName, delayedExchange, queueName);
3152
3374
  }
3153
3375
  if (topology.retry.enabled && !queueDef.exact) {
3154
- await this.assertRetryQueue(channel, topology, queueName);
3376
+ await this.assertRetryQueue(channel, topology, queueDef);
3155
3377
  }
3156
3378
  }
3157
- async assertRetryQueue(channel, topology, workQueueName) {
3158
- const retryQueueName = `${workQueueName}.retry`;
3159
- const mainExchange = this.getMainExchangeName(topology.namespace);
3379
+ async assertRetryQueue(channel, topology, queueDef) {
3380
+ const workQueueName = resolveQueueName(
3381
+ topology.namespace,
3382
+ queueDef,
3383
+ topology.naming,
3384
+ topology.prefix
3385
+ );
3386
+ const retryQueueName = getRetryQueueName(
3387
+ topology.namespace,
3388
+ queueDef.name,
3389
+ topology.naming,
3390
+ topology.prefix
3391
+ );
3392
+ const mainExchange = this.getMainExchangeName(topology);
3160
3393
  const retryQueueOptions = {
3161
3394
  durable: true,
3162
3395
  arguments: {
@@ -3172,12 +3405,17 @@ var RabbitMQTransport = class {
3172
3405
  await channel.bindQueue(retryQueueName, mainExchange, retryQueueName);
3173
3406
  }
3174
3407
  async assertDeadLetterQueues(channel, topology, dlqType) {
3175
- const dlxExchange = this.getDLXExchangeName(topology.namespace);
3408
+ const dlxExchange = this.getDLXExchangeName(topology);
3176
3409
  const dlConfig = topology.deadLetter[dlqType];
3177
3410
  for (const queueDef of topology.queues) {
3178
3411
  if (queueDef.exact) continue;
3179
- const workQueueName = `${topology.namespace}.${queueDef.name}`;
3180
- const dlqName = `${workQueueName}.${dlqType}`;
3412
+ const dlqName = getDeadLetterQueueName(
3413
+ topology.namespace,
3414
+ queueDef.name,
3415
+ dlqType,
3416
+ topology.naming,
3417
+ topology.prefix
3418
+ );
3181
3419
  const dlqOptions = {
3182
3420
  durable: true,
3183
3421
  arguments: {}
@@ -3185,18 +3423,21 @@ var RabbitMQTransport = class {
3185
3423
  if (dlConfig.maxLength) {
3186
3424
  dlqOptions.arguments["x-max-length"] = dlConfig.maxLength;
3187
3425
  }
3426
+ if (this.config.quorumQueues) {
3427
+ dlqOptions.arguments["x-queue-type"] = "quorum";
3428
+ }
3188
3429
  await channel.assertQueue(dlqName, dlqOptions);
3189
3430
  await channel.bindQueue(dlqName, dlxExchange, dlqName);
3190
3431
  }
3191
3432
  }
3192
- getMainExchangeName(namespace) {
3193
- return `${namespace}.exchange`;
3433
+ getMainExchangeName(topology) {
3434
+ return topology.naming?.mainExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.exchange`);
3194
3435
  }
3195
- getDLXExchangeName(namespace) {
3196
- return `${namespace}.dlx`;
3436
+ getDLXExchangeName(topology) {
3437
+ return topology.naming?.dlxExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.dlx`);
3197
3438
  }
3198
- getDelayedExchangeName(namespace) {
3199
- return `${namespace}.delayed`;
3439
+ getDelayedExchangeName(topology) {
3440
+ return topology.naming?.delayedExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.delayed`);
3200
3441
  }
3201
3442
  getAttemptNumber(msg) {
3202
3443
  const headerValue = msg.properties.headers?.["x-matador-attempts"];
@@ -3275,6 +3516,7 @@ exports.SchemaError = SchemaError;
3275
3516
  exports.SchemaRegistry = SchemaRegistry;
3276
3517
  exports.ShutdownInProgressError = ShutdownInProgressError;
3277
3518
  exports.ShutdownManager = ShutdownManager;
3519
+ exports.SomeSendError = SomeSendError;
3278
3520
  exports.StandardRetryPolicy = StandardRetryPolicy;
3279
3521
  exports.SubscriberIsStubError = SubscriberIsStubError;
3280
3522
  exports.SubscriberNotRegisteredError = SubscriberNotRegisteredError;
@@ -3284,6 +3526,8 @@ exports.TopologyValidationError = TopologyValidationError;
3284
3526
  exports.TransportClosedError = TransportClosedError;
3285
3527
  exports.TransportNotConnectedError = TransportNotConnectedError;
3286
3528
  exports.TransportSendError = TransportSendError;
3529
+ exports.UnknownQueueReferenceError = UnknownQueueReferenceError;
3530
+ exports.applyPrefix = applyPrefix;
3287
3531
  exports.assertEvent = assertEvent;
3288
3532
  exports.bind = bind;
3289
3533
  exports.consoleLogger = consoleLogger;
@@ -3294,6 +3538,7 @@ exports.createSubscriberStub = createSubscriberStub;
3294
3538
  exports.defaultConnectionConfig = defaultConnectionConfig;
3295
3539
  exports.defaultRetryConfig = defaultRetryConfig;
3296
3540
  exports.defaultShutdownConfig = defaultShutdownConfig;
3541
+ exports.findQueueDefinition = findQueueDefinition;
3297
3542
  exports.getDeadLetterQueueName = getDeadLetterQueueName;
3298
3543
  exports.getQualifiedQueueName = getQualifiedQueueName;
3299
3544
  exports.getRetryQueueName = getRetryQueueName;
@@ -3316,7 +3561,9 @@ exports.isSubscriber = isSubscriber;
3316
3561
  exports.isSubscriberNotRegisteredError = isSubscriberNotRegisteredError;
3317
3562
  exports.isSubscriberStub = isSubscriberStub;
3318
3563
  exports.isTransportNotConnectedError = isTransportNotConnectedError;
3564
+ exports.isUnknownQueueReferenceError = isUnknownQueueReferenceError;
3319
3565
  exports.resolveQueueName = resolveQueueName;
3566
+ exports.resolveTargetQueueName = resolveTargetQueueName;
3320
3567
  exports.supportsDelayedMessages = supportsDelayedMessages;
3321
3568
  exports.supportsDeliveryMode = supportsDeliveryMode;
3322
3569
  exports.validResult = validResult;