@grupodiariodaregiao/bunstone 0.3.12 → 0.4.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/index.js +112 -26
- package/dist/lib/app-startup.d.ts +8 -0
- package/dist/lib/rabbitmq/interfaces/rabbitmq-message.interface.d.ts +39 -1
- package/dist/lib/rabbitmq/rabbitmq-connection.d.ts +14 -0
- package/lib/app-startup.ts +160 -34
- package/lib/rabbitmq/interfaces/rabbitmq-message.interface.ts +39 -1
- package/lib/rabbitmq/rabbitmq-connection.ts +36 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -116733,6 +116733,20 @@ class RabbitMQConnection {
|
|
|
116733
116733
|
RabbitMQConnection.consumerChannels.set(queue2, channel);
|
|
116734
116734
|
return channel;
|
|
116735
116735
|
}
|
|
116736
|
+
static async createRoutingKeyConsumerChannel(exchange, routingKey) {
|
|
116737
|
+
const connection2 = await RabbitMQConnection.getConnection();
|
|
116738
|
+
const channel = await connection2.createChannel();
|
|
116739
|
+
const prefetch = RabbitMQConnection.options?.prefetch ?? 10;
|
|
116740
|
+
await channel.prefetch(prefetch);
|
|
116741
|
+
const { queue: queueName } = await channel.assertQueue("", {
|
|
116742
|
+
exclusive: true,
|
|
116743
|
+
autoDelete: true,
|
|
116744
|
+
durable: false
|
|
116745
|
+
});
|
|
116746
|
+
await channel.bindQueue(queueName, exchange, routingKey);
|
|
116747
|
+
logger2.log(`Routing-key consumer queue "${queueName}" bound to exchange "${exchange}" with key "${routingKey}"`);
|
|
116748
|
+
return { channel, queueName };
|
|
116749
|
+
}
|
|
116736
116750
|
static async initialise() {
|
|
116737
116751
|
await RabbitMQConnection.getConnection();
|
|
116738
116752
|
}
|
|
@@ -117576,42 +117590,114 @@ if (document.readyState === 'loading') {
|
|
|
117576
117590
|
AppStartup.logger.error(`RabbitMQ initialisation failed: ${err.message}`);
|
|
117577
117591
|
return;
|
|
117578
117592
|
}
|
|
117593
|
+
const queueMap = new Map;
|
|
117579
117594
|
for (const [providerClass, descriptors] of providersRabbitMQ.entries()) {
|
|
117580
117595
|
const instance = injectables?.get(providerClass) ?? new providerClass;
|
|
117581
117596
|
for (const descriptor of descriptors) {
|
|
117582
|
-
const {
|
|
117583
|
-
|
|
117584
|
-
|
|
117585
|
-
|
|
117586
|
-
|
|
117587
|
-
|
|
117588
|
-
|
|
117589
|
-
|
|
117597
|
+
const {
|
|
117598
|
+
queue: queue2,
|
|
117599
|
+
exchange,
|
|
117600
|
+
routingKey,
|
|
117601
|
+
noAck = false
|
|
117602
|
+
} = descriptor.options;
|
|
117603
|
+
if (exchange && routingKey) {
|
|
117604
|
+
AppStartup.logger.log(`Registering RabbitMQ consumer for exchange: "${exchange}" routingKey: "${routingKey}" \u2192 ${providerClass.name}.${descriptor.methodName}()`);
|
|
117605
|
+
try {
|
|
117606
|
+
const { channel, queueName } = await RabbitMQConnection.createRoutingKeyConsumerChannel(exchange, routingKey);
|
|
117607
|
+
await channel.consume(queueName, async (raw) => {
|
|
117608
|
+
if (!raw)
|
|
117609
|
+
return;
|
|
117610
|
+
const data = (() => {
|
|
117611
|
+
try {
|
|
117612
|
+
return JSON.parse(raw.content.toString());
|
|
117613
|
+
} catch {
|
|
117614
|
+
return raw.content.toString();
|
|
117615
|
+
}
|
|
117616
|
+
})();
|
|
117617
|
+
const msg = {
|
|
117618
|
+
data,
|
|
117619
|
+
raw,
|
|
117620
|
+
ack: () => channel.ack(raw),
|
|
117621
|
+
nack: (requeue = true) => channel.nack(raw, false, requeue),
|
|
117622
|
+
reject: () => channel.reject(raw, false)
|
|
117623
|
+
};
|
|
117590
117624
|
try {
|
|
117591
|
-
|
|
117592
|
-
} catch {
|
|
117593
|
-
|
|
117625
|
+
await instance[descriptor.methodName](msg);
|
|
117626
|
+
} catch (err) {
|
|
117627
|
+
AppStartup.logger.error(`Unhandled error in RabbitMQ handler ${providerClass.name}.${descriptor.methodName}() on exchange "${exchange}" routingKey "${routingKey}": ${err.message}`);
|
|
117628
|
+
if (!noAck) {
|
|
117629
|
+
channel.nack(raw, false, true);
|
|
117630
|
+
}
|
|
117594
117631
|
}
|
|
117595
|
-
})
|
|
117596
|
-
|
|
117597
|
-
|
|
117598
|
-
|
|
117599
|
-
|
|
117600
|
-
|
|
117601
|
-
|
|
117602
|
-
|
|
117632
|
+
}, { noAck });
|
|
117633
|
+
} catch (err) {
|
|
117634
|
+
AppStartup.logger.error(`Failed to register consumer for exchange "${exchange}" routingKey "${routingKey}": ${err.message}`);
|
|
117635
|
+
}
|
|
117636
|
+
continue;
|
|
117637
|
+
}
|
|
117638
|
+
if (!queue2) {
|
|
117639
|
+
AppStartup.logger.warn(`@RabbitSubscribe on ${providerClass.name}.${descriptor.methodName}() has neither 'queue' nor 'exchange'+'routingKey' \u2013 skipping.`);
|
|
117640
|
+
continue;
|
|
117641
|
+
}
|
|
117642
|
+
if (!queueMap.has(queue2))
|
|
117643
|
+
queueMap.set(queue2, []);
|
|
117644
|
+
const queueHandlers = queueMap.get(queue2);
|
|
117645
|
+
queueHandlers.push({
|
|
117646
|
+
instance,
|
|
117647
|
+
descriptor,
|
|
117648
|
+
noAck,
|
|
117649
|
+
providerName: providerClass.name
|
|
117650
|
+
});
|
|
117651
|
+
}
|
|
117652
|
+
}
|
|
117653
|
+
for (const [queue2, handlers] of queueMap.entries()) {
|
|
117654
|
+
const noAck = handlers.every((h3) => h3.noAck);
|
|
117655
|
+
const handlerList = handlers.map((h3) => `${h3.providerName}.${h3.descriptor.methodName}()`).join(", ");
|
|
117656
|
+
AppStartup.logger.log(`Registering RabbitMQ consumer for queue: "${queue2}" \u2192 [${handlerList}]`);
|
|
117657
|
+
try {
|
|
117658
|
+
const channel = await RabbitMQConnection.getConsumerChannel(queue2);
|
|
117659
|
+
await channel.consume(queue2, async (raw) => {
|
|
117660
|
+
if (!raw)
|
|
117661
|
+
return;
|
|
117662
|
+
const data = (() => {
|
|
117663
|
+
try {
|
|
117664
|
+
return JSON.parse(raw.content.toString());
|
|
117665
|
+
} catch {
|
|
117666
|
+
return raw.content.toString();
|
|
117667
|
+
}
|
|
117668
|
+
})();
|
|
117669
|
+
let settled = false;
|
|
117670
|
+
const settle = (fn3) => {
|
|
117671
|
+
if (!settled) {
|
|
117672
|
+
settled = true;
|
|
117673
|
+
fn3();
|
|
117674
|
+
}
|
|
117675
|
+
};
|
|
117676
|
+
const msg = {
|
|
117677
|
+
data,
|
|
117678
|
+
raw,
|
|
117679
|
+
ack: () => settle(() => channel.ack(raw)),
|
|
117680
|
+
nack: (requeue = true) => settle(() => channel.nack(raw, false, requeue)),
|
|
117681
|
+
reject: () => settle(() => channel.reject(raw, false))
|
|
117682
|
+
};
|
|
117683
|
+
for (const {
|
|
117684
|
+
instance,
|
|
117685
|
+
descriptor,
|
|
117686
|
+
noAck: handlerNoAck,
|
|
117687
|
+
providerName
|
|
117688
|
+
} of handlers) {
|
|
117603
117689
|
try {
|
|
117604
117690
|
await instance[descriptor.methodName](msg);
|
|
117605
117691
|
} catch (err) {
|
|
117606
|
-
AppStartup.logger.error(`Unhandled error in RabbitMQ handler ${
|
|
117607
|
-
if (!
|
|
117608
|
-
channel.nack(raw, false, true);
|
|
117692
|
+
AppStartup.logger.error(`Unhandled error in RabbitMQ handler ${providerName}.${descriptor.methodName}() on queue "${queue2}": ${err.message}`);
|
|
117693
|
+
if (!handlerNoAck && !settled) {
|
|
117694
|
+
settle(() => channel.nack(raw, false, true));
|
|
117609
117695
|
}
|
|
117610
117696
|
}
|
|
117611
|
-
}
|
|
117612
|
-
}
|
|
117613
|
-
|
|
117614
|
-
}
|
|
117697
|
+
}
|
|
117698
|
+
}, { noAck });
|
|
117699
|
+
} catch (err) {
|
|
117700
|
+
AppStartup.logger.error(`Failed to register consumer for queue "${queue2}": ${err.message}`);
|
|
117615
117701
|
}
|
|
117616
117702
|
}
|
|
117617
117703
|
})();
|
|
@@ -80,6 +80,14 @@ export declare class AppStartup {
|
|
|
80
80
|
/**
|
|
81
81
|
* Sets up RabbitMQ consumers for every provider that has `@RabbitSubscribe`
|
|
82
82
|
* methods registered in the given module.
|
|
83
|
+
*
|
|
84
|
+
* Queue mode (fan-out): all handlers subscribed to the same named queue share a
|
|
85
|
+
* single AMQP consumer. Every message is delivered to ALL handlers in declaration
|
|
86
|
+
* order. A "settle guard" ensures ack/nack/reject is called only once per message
|
|
87
|
+
* regardless of how many handlers invoke it.
|
|
88
|
+
*
|
|
89
|
+
* Routing-key mode: each handler gets its own exclusive auto-delete queue bound
|
|
90
|
+
* to the exchange, so the broker itself handles fan-out.
|
|
83
91
|
*/
|
|
84
92
|
private static registerRabbitMQConsumers;
|
|
85
93
|
private static registerCqrsHandlers;
|
|
@@ -120,13 +120,51 @@ export interface RabbitPublishOptions {
|
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
122
122
|
* Options for the `@RabbitSubscribe` method decorator.
|
|
123
|
+
*
|
|
124
|
+
* Two usage modes:
|
|
125
|
+
*
|
|
126
|
+
* 1. **Direct queue** – set `queue` to consume from a named queue.
|
|
127
|
+
* 2. **Routing key** – set `exchange` + `routingKey` to subscribe to a topic/direct
|
|
128
|
+
* exchange with a specific routing key pattern. The lib creates an exclusive
|
|
129
|
+
* auto-delete queue per handler, so **every** handler bound to the same routing
|
|
130
|
+
* key receives its own copy of the message (fan-out per key).
|
|
131
|
+
*
|
|
132
|
+
* @example Queue mode
|
|
133
|
+
* ```typescript
|
|
134
|
+
* @RabbitSubscribe({ queue: 'orders.created' })
|
|
135
|
+
* async handle(msg: RabbitMessage<Order>) { msg.ack(); }
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* @example Routing key mode
|
|
139
|
+
* ```typescript
|
|
140
|
+
* @RabbitSubscribe({ exchange: 'events', routingKey: 'article.published' })
|
|
141
|
+
* async onPublished(msg: RabbitMessage<Article>) { msg.ack(); }
|
|
142
|
+
* ```
|
|
123
143
|
*/
|
|
124
144
|
export interface RabbitSubscribeOptions {
|
|
125
145
|
/**
|
|
126
146
|
* Queue to consume messages from.
|
|
127
147
|
* The queue must be declared either via `RabbitMQModule.register({ queues: [...] })` or by the broker.
|
|
148
|
+
* Mutually exclusive with `exchange` + `routingKey`.
|
|
128
149
|
*/
|
|
129
|
-
queue
|
|
150
|
+
queue?: string;
|
|
151
|
+
/**
|
|
152
|
+
* Exchange name to bind to. Must be used together with `routingKey`.
|
|
153
|
+
* The lib automatically creates an exclusive auto-delete queue for each handler
|
|
154
|
+
* and binds it to this exchange, so all handlers for the same routing key
|
|
155
|
+
* receive a copy of every published message.
|
|
156
|
+
*/
|
|
157
|
+
exchange?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Routing key to subscribe to. Supports wildcards for topic exchanges:
|
|
160
|
+
* - `*` matches exactly one word
|
|
161
|
+
* - `#` matches zero or more words
|
|
162
|
+
*
|
|
163
|
+
* Examples: `article.published`, `article.*`, `article.#`
|
|
164
|
+
*
|
|
165
|
+
* Must be used together with `exchange`.
|
|
166
|
+
*/
|
|
167
|
+
routingKey?: string;
|
|
130
168
|
/**
|
|
131
169
|
* When `true`, messages are automatically acknowledged as soon as they are delivered.
|
|
132
170
|
* When `false` (default), you must call `msg.ack()` / `msg.nack()` manually.
|
|
@@ -27,6 +27,20 @@ export declare class RabbitMQConnection {
|
|
|
27
27
|
* Each queue gets its own channel so that prefetch applies per-queue.
|
|
28
28
|
*/
|
|
29
29
|
static getConsumerChannel(queue: string): Promise<Channel>;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a **new** channel with an exclusive, auto-delete server-named queue
|
|
32
|
+
* bound to `exchange` with `routingKey`.
|
|
33
|
+
*
|
|
34
|
+
* Because each call creates an independent queue, every handler that subscribes
|
|
35
|
+
* to the same exchange + routing key receives its own copy of the message
|
|
36
|
+
* (fan-out behaviour per routing key).
|
|
37
|
+
*
|
|
38
|
+
* The channel and queue are **not** cached – each call returns a fresh pair.
|
|
39
|
+
*/
|
|
40
|
+
static createRoutingKeyConsumerChannel(exchange: string, routingKey: string): Promise<{
|
|
41
|
+
channel: Channel;
|
|
42
|
+
queueName: string;
|
|
43
|
+
}>;
|
|
30
44
|
/**
|
|
31
45
|
* Initialises the connection, asserts all configured exchanges and queues,
|
|
32
46
|
* then resolves. Safe to call multiple times – returns the same promise.
|
package/lib/app-startup.ts
CHANGED
|
@@ -857,6 +857,14 @@ if (document.readyState === 'loading') {
|
|
|
857
857
|
/**
|
|
858
858
|
* Sets up RabbitMQ consumers for every provider that has `@RabbitSubscribe`
|
|
859
859
|
* methods registered in the given module.
|
|
860
|
+
*
|
|
861
|
+
* Queue mode (fan-out): all handlers subscribed to the same named queue share a
|
|
862
|
+
* single AMQP consumer. Every message is delivered to ALL handlers in declaration
|
|
863
|
+
* order. A "settle guard" ensures ack/nack/reject is called only once per message
|
|
864
|
+
* regardless of how many handlers invoke it.
|
|
865
|
+
*
|
|
866
|
+
* Routing-key mode: each handler gets its own exclusive auto-delete queue bound
|
|
867
|
+
* to the exchange, so the broker itself handles fan-out.
|
|
860
868
|
*/
|
|
861
869
|
private static registerRabbitMQConsumers(module: any): void {
|
|
862
870
|
const providersRabbitMQ: Map<any, RabbitMQMethodDescriptor[]> | undefined =
|
|
@@ -871,6 +879,13 @@ if (document.readyState === 'loading') {
|
|
|
871
879
|
module,
|
|
872
880
|
);
|
|
873
881
|
|
|
882
|
+
type QueueHandler = {
|
|
883
|
+
instance: any;
|
|
884
|
+
descriptor: RabbitMQMethodDescriptor;
|
|
885
|
+
noAck: boolean;
|
|
886
|
+
providerName: string;
|
|
887
|
+
};
|
|
888
|
+
|
|
874
889
|
// Fire-and-forget – connect asynchronously so startup is never blocked
|
|
875
890
|
(async () => {
|
|
876
891
|
try {
|
|
@@ -882,59 +897,170 @@ if (document.readyState === 'loading') {
|
|
|
882
897
|
return;
|
|
883
898
|
}
|
|
884
899
|
|
|
900
|
+
// ── Step 1: separate routing-key handlers from named-queue handlers ──
|
|
901
|
+
// Named-queue handlers are grouped by queue name so that a single AMQP
|
|
902
|
+
// consumer is created per queue and every message is fanned-out to all
|
|
903
|
+
// registered handlers in-process.
|
|
904
|
+
const queueMap = new Map<string, QueueHandler[]>();
|
|
905
|
+
|
|
885
906
|
for (const [providerClass, descriptors] of providersRabbitMQ.entries()) {
|
|
886
907
|
const instance = injectables?.get(providerClass) ?? new providerClass();
|
|
887
908
|
|
|
888
909
|
for (const descriptor of descriptors) {
|
|
889
|
-
const {
|
|
910
|
+
const {
|
|
911
|
+
queue,
|
|
912
|
+
exchange,
|
|
913
|
+
routingKey,
|
|
914
|
+
noAck = false,
|
|
915
|
+
} = descriptor.options;
|
|
916
|
+
|
|
917
|
+
// ── Routing-key mode: exchange + routingKey ─────────────────────
|
|
918
|
+
if (exchange && routingKey) {
|
|
919
|
+
AppStartup.logger.log(
|
|
920
|
+
`Registering RabbitMQ consumer for exchange: "${exchange}" routingKey: "${routingKey}" → ${providerClass.name}.${descriptor.methodName}()`,
|
|
921
|
+
);
|
|
890
922
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
923
|
+
try {
|
|
924
|
+
const { channel, queueName } =
|
|
925
|
+
await RabbitMQConnection.createRoutingKeyConsumerChannel(
|
|
926
|
+
exchange,
|
|
927
|
+
routingKey,
|
|
928
|
+
);
|
|
894
929
|
|
|
895
|
-
|
|
896
|
-
|
|
930
|
+
await channel.consume(
|
|
931
|
+
queueName,
|
|
932
|
+
async (raw) => {
|
|
933
|
+
if (!raw) return;
|
|
897
934
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
935
|
+
const data = (() => {
|
|
936
|
+
try {
|
|
937
|
+
return JSON.parse(raw.content.toString());
|
|
938
|
+
} catch {
|
|
939
|
+
return raw.content.toString();
|
|
940
|
+
}
|
|
941
|
+
})();
|
|
942
|
+
|
|
943
|
+
const msg: RabbitMessage = {
|
|
944
|
+
data,
|
|
945
|
+
raw,
|
|
946
|
+
ack: () => channel.ack(raw),
|
|
947
|
+
nack: (requeue = true) => channel.nack(raw, false, requeue),
|
|
948
|
+
reject: () => channel.reject(raw, false),
|
|
949
|
+
};
|
|
902
950
|
|
|
903
|
-
const data = (() => {
|
|
904
951
|
try {
|
|
905
|
-
|
|
906
|
-
} catch {
|
|
907
|
-
|
|
952
|
+
await instance[descriptor.methodName](msg);
|
|
953
|
+
} catch (err: any) {
|
|
954
|
+
AppStartup.logger.error(
|
|
955
|
+
`Unhandled error in RabbitMQ handler ${providerClass.name}.${descriptor.methodName}() on exchange "${exchange}" routingKey "${routingKey}": ${err.message}`,
|
|
956
|
+
);
|
|
957
|
+
if (!noAck) {
|
|
958
|
+
channel.nack(raw, false, true);
|
|
959
|
+
}
|
|
908
960
|
}
|
|
909
|
-
}
|
|
961
|
+
},
|
|
962
|
+
{ noAck },
|
|
963
|
+
);
|
|
964
|
+
} catch (err: any) {
|
|
965
|
+
AppStartup.logger.error(
|
|
966
|
+
`Failed to register consumer for exchange "${exchange}" routingKey "${routingKey}": ${err.message}`,
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// ── Queue mode: collect and group by queue name ─────────────────
|
|
974
|
+
if (!queue) {
|
|
975
|
+
AppStartup.logger.warn(
|
|
976
|
+
`@RabbitSubscribe on ${providerClass.name}.${descriptor.methodName}() has neither 'queue' nor 'exchange'+'routingKey' – skipping.`,
|
|
977
|
+
);
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (!queueMap.has(queue)) queueMap.set(queue, []);
|
|
982
|
+
const queueHandlers = queueMap.get(queue) as QueueHandler[];
|
|
983
|
+
queueHandlers.push({
|
|
984
|
+
instance,
|
|
985
|
+
descriptor,
|
|
986
|
+
noAck,
|
|
987
|
+
providerName: providerClass.name,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
910
991
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
nack: (requeue = true) => channel.nack(raw, false, requeue),
|
|
916
|
-
reject: () => channel.reject(raw, false),
|
|
917
|
-
};
|
|
992
|
+
// ── Step 2: one AMQP consumer per unique queue, fan-out in-process ──
|
|
993
|
+
for (const [queue, handlers] of queueMap.entries()) {
|
|
994
|
+
// Use noAck only when every handler opts in; otherwise manual ack.
|
|
995
|
+
const noAck = handlers.every((h) => h.noAck);
|
|
918
996
|
|
|
997
|
+
const handlerList = handlers
|
|
998
|
+
.map((h) => `${h.providerName}.${h.descriptor.methodName}()`)
|
|
999
|
+
.join(", ");
|
|
1000
|
+
|
|
1001
|
+
AppStartup.logger.log(
|
|
1002
|
+
`Registering RabbitMQ consumer for queue: "${queue}" → [${handlerList}]`,
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
try {
|
|
1006
|
+
const channel = await RabbitMQConnection.getConsumerChannel(queue);
|
|
1007
|
+
|
|
1008
|
+
await channel.consume(
|
|
1009
|
+
queue,
|
|
1010
|
+
async (raw) => {
|
|
1011
|
+
if (!raw) return; // consumer cancelled
|
|
1012
|
+
|
|
1013
|
+
const data = (() => {
|
|
1014
|
+
try {
|
|
1015
|
+
return JSON.parse(raw.content.toString());
|
|
1016
|
+
} catch {
|
|
1017
|
+
return raw.content.toString();
|
|
1018
|
+
}
|
|
1019
|
+
})();
|
|
1020
|
+
|
|
1021
|
+
// Settle guard: ack/nack/reject may only be called once per
|
|
1022
|
+
// delivery tag regardless of how many handlers invoke it.
|
|
1023
|
+
let settled = false;
|
|
1024
|
+
const settle = (fn: () => void) => {
|
|
1025
|
+
if (!settled) {
|
|
1026
|
+
settled = true;
|
|
1027
|
+
fn();
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const msg: RabbitMessage = {
|
|
1032
|
+
data,
|
|
1033
|
+
raw,
|
|
1034
|
+
ack: () => settle(() => channel.ack(raw)),
|
|
1035
|
+
nack: (requeue = true) =>
|
|
1036
|
+
settle(() => channel.nack(raw, false, requeue)),
|
|
1037
|
+
reject: () => settle(() => channel.reject(raw, false)),
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
for (const {
|
|
1041
|
+
instance,
|
|
1042
|
+
descriptor,
|
|
1043
|
+
noAck: handlerNoAck,
|
|
1044
|
+
providerName,
|
|
1045
|
+
} of handlers) {
|
|
919
1046
|
try {
|
|
920
1047
|
await instance[descriptor.methodName](msg);
|
|
921
1048
|
} catch (err: any) {
|
|
922
1049
|
AppStartup.logger.error(
|
|
923
|
-
`Unhandled error in RabbitMQ handler ${
|
|
1050
|
+
`Unhandled error in RabbitMQ handler ${providerName}.${descriptor.methodName}() on queue "${queue}": ${err.message}`,
|
|
924
1051
|
);
|
|
925
|
-
if (!
|
|
926
|
-
|
|
927
|
-
channel.nack(raw, false, true);
|
|
1052
|
+
if (!handlerNoAck && !settled) {
|
|
1053
|
+
settle(() => channel.nack(raw, false, true));
|
|
928
1054
|
}
|
|
929
1055
|
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
{ noAck },
|
|
1059
|
+
);
|
|
1060
|
+
} catch (err: any) {
|
|
1061
|
+
AppStartup.logger.error(
|
|
1062
|
+
`Failed to register consumer for queue "${queue}": ${err.message}`,
|
|
1063
|
+
);
|
|
938
1064
|
}
|
|
939
1065
|
}
|
|
940
1066
|
})();
|
|
@@ -134,13 +134,51 @@ export interface RabbitPublishOptions {
|
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
136
|
* Options for the `@RabbitSubscribe` method decorator.
|
|
137
|
+
*
|
|
138
|
+
* Two usage modes:
|
|
139
|
+
*
|
|
140
|
+
* 1. **Direct queue** – set `queue` to consume from a named queue.
|
|
141
|
+
* 2. **Routing key** – set `exchange` + `routingKey` to subscribe to a topic/direct
|
|
142
|
+
* exchange with a specific routing key pattern. The lib creates an exclusive
|
|
143
|
+
* auto-delete queue per handler, so **every** handler bound to the same routing
|
|
144
|
+
* key receives its own copy of the message (fan-out per key).
|
|
145
|
+
*
|
|
146
|
+
* @example Queue mode
|
|
147
|
+
* ```typescript
|
|
148
|
+
* @RabbitSubscribe({ queue: 'orders.created' })
|
|
149
|
+
* async handle(msg: RabbitMessage<Order>) { msg.ack(); }
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example Routing key mode
|
|
153
|
+
* ```typescript
|
|
154
|
+
* @RabbitSubscribe({ exchange: 'events', routingKey: 'article.published' })
|
|
155
|
+
* async onPublished(msg: RabbitMessage<Article>) { msg.ack(); }
|
|
156
|
+
* ```
|
|
137
157
|
*/
|
|
138
158
|
export interface RabbitSubscribeOptions {
|
|
139
159
|
/**
|
|
140
160
|
* Queue to consume messages from.
|
|
141
161
|
* The queue must be declared either via `RabbitMQModule.register({ queues: [...] })` or by the broker.
|
|
162
|
+
* Mutually exclusive with `exchange` + `routingKey`.
|
|
142
163
|
*/
|
|
143
|
-
queue
|
|
164
|
+
queue?: string;
|
|
165
|
+
/**
|
|
166
|
+
* Exchange name to bind to. Must be used together with `routingKey`.
|
|
167
|
+
* The lib automatically creates an exclusive auto-delete queue for each handler
|
|
168
|
+
* and binds it to this exchange, so all handlers for the same routing key
|
|
169
|
+
* receive a copy of every published message.
|
|
170
|
+
*/
|
|
171
|
+
exchange?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Routing key to subscribe to. Supports wildcards for topic exchanges:
|
|
174
|
+
* - `*` matches exactly one word
|
|
175
|
+
* - `#` matches zero or more words
|
|
176
|
+
*
|
|
177
|
+
* Examples: `article.published`, `article.*`, `article.#`
|
|
178
|
+
*
|
|
179
|
+
* Must be used together with `exchange`.
|
|
180
|
+
*/
|
|
181
|
+
routingKey?: string;
|
|
144
182
|
/**
|
|
145
183
|
* When `true`, messages are automatically acknowledged as soon as they are delivered.
|
|
146
184
|
* When `false` (default), you must call `msg.ack()` / `msg.nack()` manually.
|
|
@@ -70,6 +70,42 @@ export class RabbitMQConnection {
|
|
|
70
70
|
return channel;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Creates a **new** channel with an exclusive, auto-delete server-named queue
|
|
75
|
+
* bound to `exchange` with `routingKey`.
|
|
76
|
+
*
|
|
77
|
+
* Because each call creates an independent queue, every handler that subscribes
|
|
78
|
+
* to the same exchange + routing key receives its own copy of the message
|
|
79
|
+
* (fan-out behaviour per routing key).
|
|
80
|
+
*
|
|
81
|
+
* The channel and queue are **not** cached – each call returns a fresh pair.
|
|
82
|
+
*/
|
|
83
|
+
static async createRoutingKeyConsumerChannel(
|
|
84
|
+
exchange: string,
|
|
85
|
+
routingKey: string,
|
|
86
|
+
): Promise<{ channel: Channel; queueName: string }> {
|
|
87
|
+
const connection = await RabbitMQConnection.getConnection();
|
|
88
|
+
const channel = await connection.createChannel();
|
|
89
|
+
|
|
90
|
+
const prefetch = RabbitMQConnection.options?.prefetch ?? 10;
|
|
91
|
+
await channel.prefetch(prefetch);
|
|
92
|
+
|
|
93
|
+
// Server-generated name, exclusive so only this consumer uses it,
|
|
94
|
+
// autoDelete so it disappears when the consumer disconnects.
|
|
95
|
+
const { queue: queueName } = await channel.assertQueue("", {
|
|
96
|
+
exclusive: true,
|
|
97
|
+
autoDelete: true,
|
|
98
|
+
durable: false,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await channel.bindQueue(queueName, exchange, routingKey);
|
|
102
|
+
logger.log(
|
|
103
|
+
`Routing-key consumer queue "${queueName}" bound to exchange "${exchange}" with key "${routingKey}"`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return { channel, queueName };
|
|
107
|
+
}
|
|
108
|
+
|
|
73
109
|
/**
|
|
74
110
|
* Initialises the connection, asserts all configured exchanges and queues,
|
|
75
111
|
* then resolves. Safe to call multiple times – returns the same promise.
|