@zdavison/matador 3.0.6 → 4.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.
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 +325 -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 +26 -1
  22. package/dist/topology/builder.d.ts.map +1 -1
  23. package/dist/topology/builder.js +72 -0
  24. package/dist/topology/builder.test.js +179 -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 +108 -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,35 @@ 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
+ * @default 'matador'
476
+ * @example
477
+ * TopologyBuilder.create().withNamespace('myapp') // matador.myapp.events
478
+ * TopologyBuilder.create().withNamespace('myapp').withGlobalPrefix('acme') // acme.myapp.events
479
+ * TopologyBuilder.create().withNamespace('myapp').withGlobalPrefix(null) // myapp.events
480
+ */
481
+ withGlobalPrefix(prefix) {
482
+ this.prefix = prefix;
483
+ return this;
484
+ }
485
+ /**
486
+ * Overrides how broker resource names are derived.
487
+ * Use during migrations to keep pre-existing queue/exchange names so a
488
+ * rolling deploy keeps routing through the resources already declared on
489
+ * the broker.
490
+ * @see TopologyNaming
491
+ */
492
+ withNaming(naming) {
493
+ this.naming = naming;
494
+ return this;
495
+ }
418
496
  addQueue(nameOrDefinition, options = {}) {
419
497
  if (isQueueDefinition(nameOrDefinition)) {
420
498
  this.queues.push(nameOrDefinition);
@@ -473,8 +551,10 @@ var TopologyBuilder = class _TopologyBuilder {
473
551
  validate() {
474
552
  return [
475
553
  ...validateNamespace(this.namespace),
554
+ ...validatePrefix(this.prefix),
476
555
  ...validateQueues(this.queues),
477
- ...validateRetry(this.retry)
556
+ ...validateRetry(this.retry),
557
+ ...validateNaming(this.naming)
478
558
  ];
479
559
  }
480
560
  /**
@@ -493,7 +573,9 @@ var TopologyBuilder = class _TopologyBuilder {
493
573
  namespace: this.namespace,
494
574
  queues: [...this.queues],
495
575
  deadLetter: this.deadLetter,
496
- retry: this.retry
576
+ retry: this.retry,
577
+ naming: this.naming,
578
+ prefix: this.prefix
497
579
  };
498
580
  }
499
581
  };
@@ -509,6 +591,20 @@ function validateNamespace(namespace) {
509
591
  }
510
592
  return [];
511
593
  }
594
+ function validatePrefix(prefix) {
595
+ if (prefix == null) {
596
+ return [];
597
+ }
598
+ if (prefix.trim() === "") {
599
+ return ["Prefix must be a non-empty string, or null to disable prefixing"];
600
+ }
601
+ if (!IDENTIFIER_PATTERN.test(prefix)) {
602
+ return [
603
+ "Prefix must start with a letter and contain only alphanumeric characters, underscores, and hyphens"
604
+ ];
605
+ }
606
+ return [];
607
+ }
512
608
  function validateQueueName(queue, seen) {
513
609
  if (!queue.name || queue.name.trim() === "") {
514
610
  return ["Queue name cannot be empty"];
@@ -546,6 +642,27 @@ function validateQueues(queues) {
546
642
  }
547
643
  return issues;
548
644
  }
645
+ function validateNaming(naming) {
646
+ if (!naming) return [];
647
+ const issues = [];
648
+ for (const field of [
649
+ "queue",
650
+ "mainExchange",
651
+ "dlxExchange",
652
+ "delayedExchange"
653
+ ]) {
654
+ const value = naming[field];
655
+ if (value !== void 0 && typeof value !== "function") {
656
+ issues.push(`Naming override "${field}" must be a function`);
657
+ }
658
+ }
659
+ if (naming.dlxExchangeType !== void 0 && naming.dlxExchangeType !== "direct" && naming.dlxExchangeType !== "topic") {
660
+ issues.push(
661
+ `Naming override "dlxExchangeType" must be 'direct' or 'topic'`
662
+ );
663
+ }
664
+ return issues;
665
+ }
549
666
  function validateRetry(retry) {
550
667
  if (!retry.enabled) return [];
551
668
  const issues = [];
@@ -686,14 +803,14 @@ var FanoutEngine = class {
686
803
  transport;
687
804
  schema;
688
805
  hooks;
689
- namespace;
806
+ topology;
690
807
  defaultQueue;
691
808
  enqueuingCount = 0;
692
809
  constructor(config) {
693
810
  this.transport = config.transport;
694
811
  this.schema = config.schema;
695
812
  this.hooks = config.hooks;
696
- this.namespace = config.namespace;
813
+ this.topology = config.topology;
697
814
  this.defaultQueue = config.defaultQueue;
698
815
  }
699
816
  /**
@@ -720,7 +837,7 @@ var FanoutEngine = class {
720
837
  continue;
721
838
  }
722
839
  const targetQueue = subscriber.targetQueue ?? this.defaultQueue;
723
- const qualifiedQueue = getQualifiedQueueName(this.namespace, targetQueue);
840
+ const qualifiedQueue = resolveTargetQueueName(this.topology, targetQueue);
724
841
  const envelope = createEnvelope({
725
842
  eventKey,
726
843
  eventDescription: eventClass.description,
@@ -2060,7 +2177,7 @@ var Matador = class {
2060
2177
  transport: this.transport,
2061
2178
  schema: this.schema,
2062
2179
  hooks: this.hooks,
2063
- namespace: this.topology.namespace,
2180
+ topology: this.topology,
2064
2181
  defaultQueue
2065
2182
  });
2066
2183
  this.shutdownManager = new ShutdownManager(
@@ -2134,13 +2251,14 @@ var Matador = class {
2134
2251
  this.hooks.logger.info(
2135
2252
  `[Matador] \u{1F7E2} Worker subscribing to '${this.consumeFrom.join(",")}'.`
2136
2253
  );
2254
+ } else {
2255
+ this.hooks.logger.info(
2256
+ "[Matador] \u{1F7E1} Worker not subscribing to any queues (consumeFrom is empty)."
2257
+ );
2137
2258
  }
2138
2259
  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);
2260
+ const qualifiedName = resolveTargetQueueName(this.topology, queueName);
2261
+ const queueDef = findQueueDefinition(this.topology, queueName);
2144
2262
  const subscription = await this.transport.subscribe(
2145
2263
  qualifiedName,
2146
2264
  async (envelope, receipt) => {
@@ -2171,12 +2289,18 @@ var Matador = class {
2171
2289
  const data = dataOrOptions;
2172
2290
  const options2 = maybeOptions;
2173
2291
  const event2 = new eventClass2(data);
2174
- return this.fanout.send(eventClass2, event2, options2);
2292
+ const result2 = await this.fanout.send(eventClass2, event2, options2);
2293
+ if (result2.errors.length > 0)
2294
+ throw new SomeSendError(result2.eventKey, result2.errors);
2295
+ return result2;
2175
2296
  }
2176
2297
  const event = eventOrClass;
2177
2298
  const options = dataOrOptions;
2178
2299
  const eventClass = event.constructor;
2179
- return this.fanout.send(eventClass, event, options);
2300
+ const result = await this.fanout.send(eventClass, event, options);
2301
+ if (result.errors.length > 0)
2302
+ throw new SomeSendError(result.eventKey, result.errors);
2303
+ return result;
2180
2304
  }
2181
2305
  /**
2182
2306
  * Gets current handler state.
@@ -2437,7 +2561,12 @@ var LocalTransport = class {
2437
2561
  throw new TransportNotConnectedError(this.name, "applyTopology");
2438
2562
  }
2439
2563
  for (const queueDef of topology.queues) {
2440
- const queueName = `${topology.namespace}.${queueDef.name}`;
2564
+ const queueName = resolveQueueName(
2565
+ topology.namespace,
2566
+ queueDef,
2567
+ topology.naming,
2568
+ topology.prefix
2569
+ );
2441
2570
  if (!this.queues.has(queueName)) {
2442
2571
  this.queues.set(queueName, []);
2443
2572
  }
@@ -2483,7 +2612,12 @@ var LocalTransport = class {
2483
2612
  }
2484
2613
  async deliverToSubscribers(queue, message) {
2485
2614
  const subs = this.subscriptions.get(queue);
2486
- if (!subs) return;
2615
+ if (!subs) {
2616
+ this.logger.warn(
2617
+ `[Matador][LocalTransport] \u{1F7E1} No subscriptions found for queue '${queue}', message will be lost.`
2618
+ );
2619
+ return;
2620
+ }
2487
2621
  for (const sub of subs) {
2488
2622
  if (!sub.active || message.completed) continue;
2489
2623
  const receipt = {
@@ -2773,6 +2907,7 @@ var RabbitMQTransport = class {
2773
2907
  publishChannel = null;
2774
2908
  connectionManager;
2775
2909
  queueChannels = /* @__PURE__ */ new Map();
2910
+ subscriptionIntents = [];
2776
2911
  topology = null;
2777
2912
  codec = new RabbitMQCodec();
2778
2913
  config;
@@ -2786,7 +2921,8 @@ var RabbitMQTransport = class {
2786
2921
  connection: config.connection ?? {},
2787
2922
  quorumQueues: config.quorumQueues ?? true,
2788
2923
  defaultPrefetch: config.defaultPrefetch ?? 10,
2789
- enableDelayedMessages: config.enableDelayedMessages ?? true
2924
+ enableDelayedMessages: config.enableDelayedMessages ?? true,
2925
+ publishTimeoutMs: config.publishTimeoutMs ?? 5e3
2790
2926
  };
2791
2927
  this.connectionManager = new ConnectionManager(
2792
2928
  () => this.doConnect(),
@@ -2809,14 +2945,18 @@ var RabbitMQTransport = class {
2809
2945
  throw new TransportNotConnectedError(this.name, "applyTopology");
2810
2946
  }
2811
2947
  const channel = this.publishChannel;
2812
- const mainExchange = this.getMainExchangeName(topology.namespace);
2948
+ const mainExchange = this.getMainExchangeName(topology);
2813
2949
  await channel.assertExchange(mainExchange, "direct", { durable: true });
2814
- const dlxExchange = this.getDLXExchangeName(topology.namespace);
2950
+ const dlxExchange = this.getDLXExchangeName(topology);
2815
2951
  if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
2816
- await channel.assertExchange(dlxExchange, "direct", { durable: true });
2952
+ await channel.assertExchange(
2953
+ dlxExchange,
2954
+ topology.naming?.dlxExchangeType ?? "direct",
2955
+ { durable: true }
2956
+ );
2817
2957
  }
2818
2958
  if (this.config.enableDelayedMessages) {
2819
- await this.setupDelayedExchange(topology.namespace);
2959
+ await this.setupDelayedExchange(topology);
2820
2960
  }
2821
2961
  for (const queueDef of topology.queues) {
2822
2962
  await this.assertWorkQueue(channel, topology, queueDef);
@@ -2848,14 +2988,13 @@ var RabbitMQTransport = class {
2848
2988
  if (!this.delayedExchangeAvailable) {
2849
2989
  throw new DelayedMessagesNotSupportedError(this.name);
2850
2990
  }
2851
- const delayedExchange = this.getDelayedExchangeName(
2852
- this.topology.namespace
2853
- );
2991
+ const delayedExchange = this.getDelayedExchangeName(this.topology);
2854
2992
  publishOptions.headers = {
2855
2993
  ...publishOptions.headers,
2856
2994
  "x-delay": options.delay
2857
2995
  };
2858
- this.publishChannel.publish(
2996
+ await this.confirmPublish(
2997
+ this.publishChannel,
2859
2998
  delayedExchange,
2860
2999
  queue,
2861
3000
  buffer,
@@ -2870,14 +3009,67 @@ var RabbitMQTransport = class {
2870
3009
  publishOptions.persistent = options.transport.rabbitmq.persistent;
2871
3010
  }
2872
3011
  const routingKey = options?.transport?.rabbitmq?.routingKey ?? queue;
2873
- const exchange = this.getMainExchangeName(this.topology.namespace);
2874
- this.publishChannel.publish(exchange, routingKey, buffer, publishOptions);
3012
+ const exchange = this.getMainExchangeName(this.topology);
3013
+ await this.confirmPublish(
3014
+ this.publishChannel,
3015
+ exchange,
3016
+ routingKey,
3017
+ buffer,
3018
+ publishOptions
3019
+ );
2875
3020
  return this.name;
2876
3021
  }
2877
3022
  async subscribe(queue, handler, options = {}) {
2878
3023
  if (!this.connection || !this.topology) {
2879
3024
  throw new TransportNotConnectedError(this.name, "subscribe");
2880
3025
  }
3026
+ const intent = {
3027
+ queue,
3028
+ handler,
3029
+ options,
3030
+ active: true,
3031
+ currentConsumer: null
3032
+ };
3033
+ this.subscriptionIntents.push(intent);
3034
+ await this.activateIntent(intent);
3035
+ return {
3036
+ unsubscribe: async () => {
3037
+ intent.active = false;
3038
+ const idx = this.subscriptionIntents.indexOf(intent);
3039
+ if (idx !== -1) this.subscriptionIntents.splice(idx, 1);
3040
+ const consumer = intent.currentConsumer;
3041
+ if (consumer) {
3042
+ consumer.active = false;
3043
+ const queueChannel = this.queueChannels.get(queue);
3044
+ if (queueChannel) {
3045
+ try {
3046
+ await queueChannel.channel.cancel(consumer.consumerTag);
3047
+ } catch {
3048
+ }
3049
+ const cIdx = queueChannel.consumers.indexOf(consumer);
3050
+ if (cIdx !== -1) queueChannel.consumers.splice(cIdx, 1);
3051
+ if (queueChannel.consumers.length === 0) {
3052
+ try {
3053
+ await queueChannel.channel.close();
3054
+ } catch {
3055
+ }
3056
+ this.queueChannels.delete(queue);
3057
+ }
3058
+ }
3059
+ intent.currentConsumer = null;
3060
+ }
3061
+ },
3062
+ get isActive() {
3063
+ return intent.active;
3064
+ }
3065
+ };
3066
+ }
3067
+ /**
3068
+ * Wires a single subscription intent to the current connection.
3069
+ * Called once on subscribe() and again after each reconnect.
3070
+ */
3071
+ async activateIntent(intent) {
3072
+ const { queue, handler, options } = intent;
2881
3073
  const queueChannel = await this.getOrCreateQueueChannel(queue, options);
2882
3074
  const { channel } = queueChannel;
2883
3075
  const consumer = {
@@ -2913,33 +3105,10 @@ var RabbitMQTransport = class {
2913
3105
  }
2914
3106
  },
2915
3107
  { noAck: false }
2916
- // Always manually ack
2917
3108
  );
2918
3109
  consumer.consumerTag = consumerTag;
2919
3110
  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
- };
3111
+ intent.currentConsumer = consumer;
2943
3112
  }
2944
3113
  async complete(receipt) {
2945
3114
  const { channel, msg } = receipt.handle;
@@ -2963,7 +3132,7 @@ var RabbitMQTransport = class {
2963
3132
  };
2964
3133
  const encoded = this.codec.encode(dlqEnvelope);
2965
3134
  const buffer = Buffer.from(encoded.body);
2966
- const dlxExchange = this.getDLXExchangeName(this.topology.namespace);
3135
+ const dlxExchange = this.getDLXExchangeName(this.topology);
2967
3136
  const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
2968
3137
  const publishOptions = {
2969
3138
  persistent: true,
@@ -2975,7 +3144,8 @@ var RabbitMQTransport = class {
2975
3144
  "x-matador-dead-letter-reason": reason
2976
3145
  }
2977
3146
  };
2978
- this.publishChannel.publish(
3147
+ await this.confirmPublish(
3148
+ this.publishChannel,
2979
3149
  dlxExchange,
2980
3150
  dlqQueueName,
2981
3151
  buffer,
@@ -2983,6 +3153,41 @@ var RabbitMQTransport = class {
2983
3153
  );
2984
3154
  await this.complete(receipt);
2985
3155
  }
3156
+ /**
3157
+ * Publishes on the confirm channel and resolves once the broker
3158
+ * acknowledges the message, rejecting on broker nack, channel error, or
3159
+ * after `publishTimeoutMs` elapses without a confirm.
3160
+ *
3161
+ * Mirrors Matador v1's promise-wrapped amqplib publish callback to give
3162
+ * callers confirmed (at-least-once) delivery semantics.
3163
+ */
3164
+ confirmPublish(channel, exchange, routingKey, buffer, options) {
3165
+ return new Promise((resolve, reject) => {
3166
+ let timeout = setTimeout(() => {
3167
+ timeout = null;
3168
+ reject(
3169
+ new TransportSendError(
3170
+ routingKey,
3171
+ new Error(
3172
+ `Publish not confirmed by broker within ${this.config.publishTimeoutMs}ms`
3173
+ )
3174
+ )
3175
+ );
3176
+ }, this.config.publishTimeoutMs);
3177
+ channel.publish(exchange, routingKey, buffer, options, (err) => {
3178
+ if (timeout) {
3179
+ clearTimeout(timeout);
3180
+ } else {
3181
+ return;
3182
+ }
3183
+ if (err) {
3184
+ reject(new TransportSendError(routingKey, err));
3185
+ } else {
3186
+ resolve();
3187
+ }
3188
+ });
3189
+ });
3190
+ }
2986
3191
  // Private methods
2987
3192
  /**
2988
3193
  * Gets or creates a dedicated channel for a queue subscription.
@@ -3012,6 +3217,7 @@ var RabbitMQTransport = class {
3012
3217
  return queueChannel;
3013
3218
  }
3014
3219
  async doConnect() {
3220
+ this.queueChannels.clear();
3015
3221
  this.logger.info(
3016
3222
  `[Matador] \u23F3 Connecting to RabbitMQ at '${redactAmqpUrl(this.config.url)}'.`
3017
3223
  );
@@ -3023,19 +3229,28 @@ var RabbitMQTransport = class {
3023
3229
  this.logger.error("[Matador] \u{1F534} RabbitMQ connection error", err);
3024
3230
  });
3025
3231
  connection.on("close", () => {
3232
+ for (const queueChannel of this.queueChannels.values()) {
3233
+ for (const consumer of queueChannel.consumers) {
3234
+ consumer.active = false;
3235
+ }
3236
+ }
3026
3237
  if (this.connectionManager.isConnected()) {
3027
3238
  this.connectionManager.handleConnectionLost(
3028
3239
  new Error("Connection closed unexpectedly")
3029
3240
  );
3030
3241
  }
3031
3242
  });
3032
- this.publishChannel = await connection.createChannel();
3243
+ this.publishChannel = await connection.createConfirmChannel();
3033
3244
  this.publishChannel.on("error", (err) => {
3034
3245
  this.logger.error("[Matador] \u{1F534} RabbitMQ publish channel error", err);
3035
3246
  });
3036
3247
  if (this.topology) {
3037
3248
  await this.applyTopology(this.topology);
3249
+ for (const intent of this.subscriptionIntents) {
3250
+ await this.activateIntent(intent);
3251
+ }
3038
3252
  }
3253
+ this.logger.info("[Matador] \u{1F50C} Connected to RabbitMQ");
3039
3254
  }
3040
3255
  async doDisconnect() {
3041
3256
  for (const queueChannel of this.queueChannels.values()) {
@@ -3072,12 +3287,12 @@ var RabbitMQTransport = class {
3072
3287
  delayedMessages: false
3073
3288
  };
3074
3289
  }
3075
- async setupDelayedExchange(namespace) {
3290
+ async setupDelayedExchange(topology) {
3076
3291
  if (!this.connection) {
3077
3292
  return;
3078
3293
  }
3079
3294
  this.delayedExchangeAvailable = false;
3080
- const delayedExchange = this.getDelayedExchangeName(namespace);
3295
+ const delayedExchange = this.getDelayedExchangeName(topology);
3081
3296
  const connection = this.connection;
3082
3297
  return new Promise((resolve) => {
3083
3298
  let resolved = false;
@@ -3129,7 +3344,7 @@ var RabbitMQTransport = class {
3129
3344
  queueOptions.arguments["x-queue-type"] = "quorum";
3130
3345
  }
3131
3346
  if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
3132
- queueOptions.arguments["x-dead-letter-exchange"] = this.getDLXExchangeName(topology.namespace);
3347
+ queueOptions.arguments["x-dead-letter-exchange"] = this.getDLXExchangeName(topology);
3133
3348
  }
3134
3349
  if (queueDef.priorities) {
3135
3350
  queueOptions.arguments["x-max-priority"] = 10;
@@ -3140,23 +3355,39 @@ var RabbitMQTransport = class {
3140
3355
  return queueOptions;
3141
3356
  }
3142
3357
  async assertWorkQueue(channel, topology, queueDef) {
3143
- const queueName = queueDef.exact ? queueDef.name : `${topology.namespace}.${queueDef.name}`;
3358
+ const queueName = resolveQueueName(
3359
+ topology.namespace,
3360
+ queueDef,
3361
+ topology.naming,
3362
+ topology.prefix
3363
+ );
3144
3364
  const rabbitmqOptions = queueDef.transport?.rabbitmq?.options;
3145
3365
  const queueOptions = rabbitmqOptions ?? this.buildWorkQueueOptions(topology, queueDef);
3146
3366
  await channel.assertQueue(queueName, queueOptions);
3147
- const mainExchange = this.getMainExchangeName(topology.namespace);
3367
+ const mainExchange = this.getMainExchangeName(topology);
3148
3368
  await channel.bindQueue(queueName, mainExchange, queueName);
3149
3369
  if (this.delayedExchangeAvailable) {
3150
- const delayedExchange = this.getDelayedExchangeName(topology.namespace);
3370
+ const delayedExchange = this.getDelayedExchangeName(topology);
3151
3371
  await channel.bindQueue(queueName, delayedExchange, queueName);
3152
3372
  }
3153
3373
  if (topology.retry.enabled && !queueDef.exact) {
3154
- await this.assertRetryQueue(channel, topology, queueName);
3374
+ await this.assertRetryQueue(channel, topology, queueDef);
3155
3375
  }
3156
3376
  }
3157
- async assertRetryQueue(channel, topology, workQueueName) {
3158
- const retryQueueName = `${workQueueName}.retry`;
3159
- const mainExchange = this.getMainExchangeName(topology.namespace);
3377
+ async assertRetryQueue(channel, topology, queueDef) {
3378
+ const workQueueName = resolveQueueName(
3379
+ topology.namespace,
3380
+ queueDef,
3381
+ topology.naming,
3382
+ topology.prefix
3383
+ );
3384
+ const retryQueueName = getRetryQueueName(
3385
+ topology.namespace,
3386
+ queueDef.name,
3387
+ topology.naming,
3388
+ topology.prefix
3389
+ );
3390
+ const mainExchange = this.getMainExchangeName(topology);
3160
3391
  const retryQueueOptions = {
3161
3392
  durable: true,
3162
3393
  arguments: {
@@ -3172,12 +3403,17 @@ var RabbitMQTransport = class {
3172
3403
  await channel.bindQueue(retryQueueName, mainExchange, retryQueueName);
3173
3404
  }
3174
3405
  async assertDeadLetterQueues(channel, topology, dlqType) {
3175
- const dlxExchange = this.getDLXExchangeName(topology.namespace);
3406
+ const dlxExchange = this.getDLXExchangeName(topology);
3176
3407
  const dlConfig = topology.deadLetter[dlqType];
3177
3408
  for (const queueDef of topology.queues) {
3178
3409
  if (queueDef.exact) continue;
3179
- const workQueueName = `${topology.namespace}.${queueDef.name}`;
3180
- const dlqName = `${workQueueName}.${dlqType}`;
3410
+ const dlqName = getDeadLetterQueueName(
3411
+ topology.namespace,
3412
+ queueDef.name,
3413
+ dlqType,
3414
+ topology.naming,
3415
+ topology.prefix
3416
+ );
3181
3417
  const dlqOptions = {
3182
3418
  durable: true,
3183
3419
  arguments: {}
@@ -3185,18 +3421,21 @@ var RabbitMQTransport = class {
3185
3421
  if (dlConfig.maxLength) {
3186
3422
  dlqOptions.arguments["x-max-length"] = dlConfig.maxLength;
3187
3423
  }
3424
+ if (this.config.quorumQueues) {
3425
+ dlqOptions.arguments["x-queue-type"] = "quorum";
3426
+ }
3188
3427
  await channel.assertQueue(dlqName, dlqOptions);
3189
3428
  await channel.bindQueue(dlqName, dlxExchange, dlqName);
3190
3429
  }
3191
3430
  }
3192
- getMainExchangeName(namespace) {
3193
- return `${namespace}.exchange`;
3431
+ getMainExchangeName(topology) {
3432
+ return topology.naming?.mainExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.exchange`);
3194
3433
  }
3195
- getDLXExchangeName(namespace) {
3196
- return `${namespace}.dlx`;
3434
+ getDLXExchangeName(topology) {
3435
+ return topology.naming?.dlxExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.dlx`);
3197
3436
  }
3198
- getDelayedExchangeName(namespace) {
3199
- return `${namespace}.delayed`;
3437
+ getDelayedExchangeName(topology) {
3438
+ return topology.naming?.delayedExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.delayed`);
3200
3439
  }
3201
3440
  getAttemptNumber(msg) {
3202
3441
  const headerValue = msg.properties.headers?.["x-matador-attempts"];
@@ -3275,6 +3514,7 @@ exports.SchemaError = SchemaError;
3275
3514
  exports.SchemaRegistry = SchemaRegistry;
3276
3515
  exports.ShutdownInProgressError = ShutdownInProgressError;
3277
3516
  exports.ShutdownManager = ShutdownManager;
3517
+ exports.SomeSendError = SomeSendError;
3278
3518
  exports.StandardRetryPolicy = StandardRetryPolicy;
3279
3519
  exports.SubscriberIsStubError = SubscriberIsStubError;
3280
3520
  exports.SubscriberNotRegisteredError = SubscriberNotRegisteredError;
@@ -3284,6 +3524,8 @@ exports.TopologyValidationError = TopologyValidationError;
3284
3524
  exports.TransportClosedError = TransportClosedError;
3285
3525
  exports.TransportNotConnectedError = TransportNotConnectedError;
3286
3526
  exports.TransportSendError = TransportSendError;
3527
+ exports.UnknownQueueReferenceError = UnknownQueueReferenceError;
3528
+ exports.applyPrefix = applyPrefix;
3287
3529
  exports.assertEvent = assertEvent;
3288
3530
  exports.bind = bind;
3289
3531
  exports.consoleLogger = consoleLogger;
@@ -3294,6 +3536,7 @@ exports.createSubscriberStub = createSubscriberStub;
3294
3536
  exports.defaultConnectionConfig = defaultConnectionConfig;
3295
3537
  exports.defaultRetryConfig = defaultRetryConfig;
3296
3538
  exports.defaultShutdownConfig = defaultShutdownConfig;
3539
+ exports.findQueueDefinition = findQueueDefinition;
3297
3540
  exports.getDeadLetterQueueName = getDeadLetterQueueName;
3298
3541
  exports.getQualifiedQueueName = getQualifiedQueueName;
3299
3542
  exports.getRetryQueueName = getRetryQueueName;
@@ -3316,7 +3559,9 @@ exports.isSubscriber = isSubscriber;
3316
3559
  exports.isSubscriberNotRegisteredError = isSubscriberNotRegisteredError;
3317
3560
  exports.isSubscriberStub = isSubscriberStub;
3318
3561
  exports.isTransportNotConnectedError = isTransportNotConnectedError;
3562
+ exports.isUnknownQueueReferenceError = isUnknownQueueReferenceError;
3319
3563
  exports.resolveQueueName = resolveQueueName;
3564
+ exports.resolveTargetQueueName = resolveTargetQueueName;
3320
3565
  exports.supportsDelayedMessages = supportsDelayedMessages;
3321
3566
  exports.supportsDeliveryMode = supportsDeliveryMode;
3322
3567
  exports.validResult = validResult;