@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.
- 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 +325 -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 +26 -1
- package/dist/topology/builder.d.ts.map +1 -1
- package/dist/topology/builder.js +72 -0
- package/dist/topology/builder.test.js +179 -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 +108 -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,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
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
2140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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
|
|
2948
|
+
const mainExchange = this.getMainExchangeName(topology);
|
|
2813
2949
|
await channel.assertExchange(mainExchange, "direct", { durable: true });
|
|
2814
|
-
const dlxExchange = this.getDLXExchangeName(topology
|
|
2950
|
+
const dlxExchange = this.getDLXExchangeName(topology);
|
|
2815
2951
|
if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
|
|
2816
|
-
await channel.assertExchange(
|
|
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
|
|
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.
|
|
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
|
|
2874
|
-
this.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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(
|
|
3290
|
+
async setupDelayedExchange(topology) {
|
|
3076
3291
|
if (!this.connection) {
|
|
3077
3292
|
return;
|
|
3078
3293
|
}
|
|
3079
3294
|
this.delayedExchangeAvailable = false;
|
|
3080
|
-
const delayedExchange = this.getDelayedExchangeName(
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
3374
|
+
await this.assertRetryQueue(channel, topology, queueDef);
|
|
3155
3375
|
}
|
|
3156
3376
|
}
|
|
3157
|
-
async assertRetryQueue(channel, topology,
|
|
3158
|
-
const
|
|
3159
|
-
|
|
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
|
|
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
|
|
3180
|
-
|
|
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(
|
|
3193
|
-
return `${namespace}.exchange
|
|
3431
|
+
getMainExchangeName(topology) {
|
|
3432
|
+
return topology.naming?.mainExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.exchange`);
|
|
3194
3433
|
}
|
|
3195
|
-
getDLXExchangeName(
|
|
3196
|
-
return `${namespace}.dlx
|
|
3434
|
+
getDLXExchangeName(topology) {
|
|
3435
|
+
return topology.naming?.dlxExchange?.(topology.namespace) ?? applyPrefix(topology.prefix, `${topology.namespace}.dlx`);
|
|
3197
3436
|
}
|
|
3198
|
-
getDelayedExchangeName(
|
|
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;
|