@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.
- package/dist/core/fanout.d.ts +3 -2
- package/dist/core/fanout.d.ts.map +1 -1
- package/dist/core/fanout.js +4 -4
- package/dist/core/fanout.test.js +32 -24
- package/dist/core/matador.d.ts.map +1 -1
- package/dist/core/matador.js +16 -7
- package/dist/core/matador.test.js +32 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +2 -2
- package/dist/errors/matador-errors.d.ts +28 -0
- package/dist/errors/matador-errors.d.ts.map +1 -1
- package/dist/errors/matador-errors.js +41 -0
- package/dist/index.cjs +327 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/topology/builder.d.ts +28 -1
- package/dist/topology/builder.d.ts.map +1 -1
- package/dist/topology/builder.js +74 -0
- package/dist/topology/builder.test.js +188 -1
- package/dist/topology/index.d.ts +2 -2
- package/dist/topology/index.d.ts.map +1 -1
- package/dist/topology/index.js +1 -1
- package/dist/topology/types.d.ts +120 -6
- package/dist/topology/types.d.ts.map +1 -1
- package/dist/topology/types.js +50 -10
- package/dist/transport/local/local-transport.d.ts.map +1 -1
- package/dist/transport/local/local-transport.js +5 -2
- package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.d.ts +2 -0
- package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.d.ts.map +1 -0
- package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.js +110 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +24 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.js +145 -68
- package/dist/transport/rabbitmq/rabbitmq-transport.test.js +130 -0
- 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
|
|
364
|
-
return `${
|
|
389
|
+
function applyPrefix(prefix, name) {
|
|
390
|
+
return prefix == null ? name : `${prefix}.${name}`;
|
|
365
391
|
}
|
|
366
|
-
function
|
|
367
|
-
return `${namespace}.${queueName}
|
|
392
|
+
function getQualifiedQueueName(namespace, queueName, naming, prefix) {
|
|
393
|
+
return naming?.queue?.(namespace, queueName) ?? applyPrefix(prefix, `${namespace}.${queueName}`);
|
|
368
394
|
}
|
|
369
|
-
function
|
|
370
|
-
return `${namespace}.${
|
|
395
|
+
function getDeadLetterQueueName(namespace, queueName, dlqType, naming, prefix) {
|
|
396
|
+
return `${getQualifiedQueueName(namespace, queueName, naming, prefix)}.${dlqType}`;
|
|
371
397
|
}
|
|
372
|
-
function
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
2140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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
|
|
2950
|
+
const mainExchange = this.getMainExchangeName(topology);
|
|
2813
2951
|
await channel.assertExchange(mainExchange, "direct", { durable: true });
|
|
2814
|
-
const dlxExchange = this.getDLXExchangeName(topology
|
|
2952
|
+
const dlxExchange = this.getDLXExchangeName(topology);
|
|
2815
2953
|
if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
|
|
2816
|
-
await channel.assertExchange(
|
|
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
|
|
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.
|
|
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
|
|
2874
|
-
this.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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(
|
|
3292
|
+
async setupDelayedExchange(topology) {
|
|
3076
3293
|
if (!this.connection) {
|
|
3077
3294
|
return;
|
|
3078
3295
|
}
|
|
3079
3296
|
this.delayedExchangeAvailable = false;
|
|
3080
|
-
const delayedExchange = this.getDelayedExchangeName(
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
3376
|
+
await this.assertRetryQueue(channel, topology, queueDef);
|
|
3155
3377
|
}
|
|
3156
3378
|
}
|
|
3157
|
-
async assertRetryQueue(channel, topology,
|
|
3158
|
-
const
|
|
3159
|
-
|
|
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
|
|
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
|
|
3180
|
-
|
|
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(
|
|
3193
|
-
return `${namespace}.exchange
|
|
3433
|
+
getMainExchangeName(topology) {
|
|
3434
|
+
return topology.naming?.mainExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.exchange`);
|
|
3194
3435
|
}
|
|
3195
|
-
getDLXExchangeName(
|
|
3196
|
-
return `${namespace}.dlx
|
|
3436
|
+
getDLXExchangeName(topology) {
|
|
3437
|
+
return topology.naming?.dlxExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.dlx`);
|
|
3197
3438
|
}
|
|
3198
|
-
getDelayedExchangeName(
|
|
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;
|