@grupodiariodaregiao/bunstone 0.4.0 → 0.4.2
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 +92 -27
- package/dist/lib/app-startup.d.ts +8 -0
- package/lib/app-startup.ts +143 -37
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -117583,6 +117583,33 @@ if (document.readyState === 'loading') {
|
|
|
117583
117583
|
return;
|
|
117584
117584
|
}
|
|
117585
117585
|
const injectables = Reflect.getMetadata("dip:injectables", module);
|
|
117586
|
+
function matchRoutingKey(pattern, routingKey) {
|
|
117587
|
+
if (pattern === routingKey)
|
|
117588
|
+
return true;
|
|
117589
|
+
if (!pattern.includes("*") && !pattern.includes("#"))
|
|
117590
|
+
return false;
|
|
117591
|
+
const pp = pattern.split(".");
|
|
117592
|
+
const kp = routingKey.split(".");
|
|
117593
|
+
function go2(pi3, ki3) {
|
|
117594
|
+
if (pi3 === pp.length && ki3 === kp.length)
|
|
117595
|
+
return true;
|
|
117596
|
+
if (pi3 === pp.length)
|
|
117597
|
+
return false;
|
|
117598
|
+
if (pp[pi3] === "#") {
|
|
117599
|
+
for (let j4 = ki3;j4 <= kp.length; j4++) {
|
|
117600
|
+
if (go2(pi3 + 1, j4))
|
|
117601
|
+
return true;
|
|
117602
|
+
}
|
|
117603
|
+
return false;
|
|
117604
|
+
}
|
|
117605
|
+
if (ki3 === kp.length)
|
|
117606
|
+
return false;
|
|
117607
|
+
if (pp[pi3] === "*" || pp[pi3] === kp[ki3])
|
|
117608
|
+
return go2(pi3 + 1, ki3 + 1);
|
|
117609
|
+
return false;
|
|
117610
|
+
}
|
|
117611
|
+
return go2(0, 0);
|
|
117612
|
+
}
|
|
117586
117613
|
(async () => {
|
|
117587
117614
|
try {
|
|
117588
117615
|
await RabbitMQConnection.initialise();
|
|
@@ -117590,6 +117617,7 @@ if (document.readyState === 'loading') {
|
|
|
117590
117617
|
AppStartup.logger.error(`RabbitMQ initialisation failed: ${err.message}`);
|
|
117591
117618
|
return;
|
|
117592
117619
|
}
|
|
117620
|
+
const queueMap = new Map;
|
|
117593
117621
|
for (const [providerClass, descriptors] of providersRabbitMQ.entries()) {
|
|
117594
117622
|
const instance = injectables?.get(providerClass) ?? new providerClass;
|
|
117595
117623
|
for (const descriptor of descriptors) {
|
|
@@ -117638,38 +117666,75 @@ if (document.readyState === 'loading') {
|
|
|
117638
117666
|
AppStartup.logger.warn(`@RabbitSubscribe on ${providerClass.name}.${descriptor.methodName}() has neither 'queue' nor 'exchange'+'routingKey' \u2013 skipping.`);
|
|
117639
117667
|
continue;
|
|
117640
117668
|
}
|
|
117641
|
-
|
|
117642
|
-
|
|
117643
|
-
|
|
117644
|
-
|
|
117645
|
-
|
|
117646
|
-
|
|
117647
|
-
|
|
117648
|
-
|
|
117649
|
-
|
|
117650
|
-
|
|
117651
|
-
|
|
117652
|
-
|
|
117653
|
-
|
|
117654
|
-
|
|
117655
|
-
|
|
117656
|
-
|
|
117657
|
-
|
|
117658
|
-
|
|
117659
|
-
|
|
117660
|
-
|
|
117669
|
+
if (!queueMap.has(queue2))
|
|
117670
|
+
queueMap.set(queue2, []);
|
|
117671
|
+
const queueHandlers = queueMap.get(queue2);
|
|
117672
|
+
queueHandlers.push({
|
|
117673
|
+
instance,
|
|
117674
|
+
descriptor,
|
|
117675
|
+
noAck,
|
|
117676
|
+
providerName: providerClass.name
|
|
117677
|
+
});
|
|
117678
|
+
}
|
|
117679
|
+
}
|
|
117680
|
+
for (const [queue2, handlers] of queueMap.entries()) {
|
|
117681
|
+
const noAck = handlers.every((h3) => h3.noAck);
|
|
117682
|
+
const handlerList = handlers.map((h3) => {
|
|
117683
|
+
const rk = h3.descriptor.options.routingKey;
|
|
117684
|
+
return `${h3.providerName}.${h3.descriptor.methodName}()${rk ? ` [${rk}]` : ""}`;
|
|
117685
|
+
}).join(", ");
|
|
117686
|
+
AppStartup.logger.log(`Registering RabbitMQ consumer for queue: "${queue2}" \u2192 [${handlerList}]`);
|
|
117687
|
+
try {
|
|
117688
|
+
const channel = await RabbitMQConnection.getConsumerChannel(queue2);
|
|
117689
|
+
await channel.consume(queue2, async (raw) => {
|
|
117690
|
+
if (!raw)
|
|
117691
|
+
return;
|
|
117692
|
+
const data = (() => {
|
|
117693
|
+
try {
|
|
117694
|
+
return JSON.parse(raw.content.toString());
|
|
117695
|
+
} catch {
|
|
117696
|
+
return raw.content.toString();
|
|
117697
|
+
}
|
|
117698
|
+
})();
|
|
117699
|
+
let settled = false;
|
|
117700
|
+
const settle = (fn3) => {
|
|
117701
|
+
if (!settled) {
|
|
117702
|
+
settled = true;
|
|
117703
|
+
fn3();
|
|
117704
|
+
}
|
|
117705
|
+
};
|
|
117706
|
+
const msg = {
|
|
117707
|
+
data,
|
|
117708
|
+
raw,
|
|
117709
|
+
ack: () => settle(() => channel.ack(raw)),
|
|
117710
|
+
nack: (requeue = true) => settle(() => channel.nack(raw, false, requeue)),
|
|
117711
|
+
reject: () => settle(() => channel.reject(raw, false))
|
|
117712
|
+
};
|
|
117713
|
+
for (const {
|
|
117714
|
+
instance,
|
|
117715
|
+
descriptor,
|
|
117716
|
+
noAck: handlerNoAck,
|
|
117717
|
+
providerName
|
|
117718
|
+
} of handlers) {
|
|
117719
|
+
const handlerRoutingKey = descriptor.options.routingKey;
|
|
117720
|
+
if (handlerRoutingKey && !matchRoutingKey(handlerRoutingKey, raw.fields.routingKey)) {
|
|
117721
|
+
continue;
|
|
117722
|
+
}
|
|
117661
117723
|
try {
|
|
117662
117724
|
await instance[descriptor.methodName](msg);
|
|
117663
117725
|
} catch (err) {
|
|
117664
|
-
AppStartup.logger.error(`Unhandled error in RabbitMQ handler ${
|
|
117665
|
-
if (!
|
|
117666
|
-
channel.nack(raw, false, true);
|
|
117726
|
+
AppStartup.logger.error(`Unhandled error in RabbitMQ handler ${providerName}.${descriptor.methodName}() on queue "${queue2}": ${err.message}`);
|
|
117727
|
+
if (!handlerNoAck && !settled) {
|
|
117728
|
+
settle(() => channel.nack(raw, false, true));
|
|
117667
117729
|
}
|
|
117668
117730
|
}
|
|
117669
|
-
}
|
|
117670
|
-
|
|
117671
|
-
|
|
117672
|
-
|
|
117731
|
+
}
|
|
117732
|
+
if (!noAck && !settled) {
|
|
117733
|
+
settle(() => channel.ack(raw));
|
|
117734
|
+
}
|
|
117735
|
+
}, { noAck });
|
|
117736
|
+
} catch (err) {
|
|
117737
|
+
AppStartup.logger.error(`Failed to register consumer for queue "${queue2}": ${err.message}`);
|
|
117673
117738
|
}
|
|
117674
117739
|
}
|
|
117675
117740
|
})();
|
|
@@ -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;
|
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,41 @@ if (document.readyState === 'loading') {
|
|
|
871
879
|
module,
|
|
872
880
|
);
|
|
873
881
|
|
|
882
|
+
/**
|
|
883
|
+
* Matches a RabbitMQ topic routing key pattern against a concrete routing key.
|
|
884
|
+
* Supports `*` (exactly one word) and `#` (zero or more words).
|
|
885
|
+
*/
|
|
886
|
+
function matchRoutingKey(pattern: string, routingKey: string): boolean {
|
|
887
|
+
if (pattern === routingKey) return true;
|
|
888
|
+
if (!pattern.includes("*") && !pattern.includes("#")) return false;
|
|
889
|
+
|
|
890
|
+
const pp = pattern.split(".");
|
|
891
|
+
const kp = routingKey.split(".");
|
|
892
|
+
|
|
893
|
+
function go(pi: number, ki: number): boolean {
|
|
894
|
+
if (pi === pp.length && ki === kp.length) return true;
|
|
895
|
+
if (pi === pp.length) return false;
|
|
896
|
+
if (pp[pi] === "#") {
|
|
897
|
+
for (let j = ki; j <= kp.length; j++) {
|
|
898
|
+
if (go(pi + 1, j)) return true;
|
|
899
|
+
}
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
if (ki === kp.length) return false;
|
|
903
|
+
if (pp[pi] === "*" || pp[pi] === kp[ki]) return go(pi + 1, ki + 1);
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return go(0, 0);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
type QueueHandler = {
|
|
911
|
+
instance: any;
|
|
912
|
+
descriptor: RabbitMQMethodDescriptor;
|
|
913
|
+
noAck: boolean;
|
|
914
|
+
providerName: string;
|
|
915
|
+
};
|
|
916
|
+
|
|
874
917
|
// Fire-and-forget – connect asynchronously so startup is never blocked
|
|
875
918
|
(async () => {
|
|
876
919
|
try {
|
|
@@ -882,6 +925,12 @@ if (document.readyState === 'loading') {
|
|
|
882
925
|
return;
|
|
883
926
|
}
|
|
884
927
|
|
|
928
|
+
// ── Step 1: separate routing-key handlers from named-queue handlers ──
|
|
929
|
+
// Named-queue handlers are grouped by queue name so that a single AMQP
|
|
930
|
+
// consumer is created per queue and every message is fanned-out to all
|
|
931
|
+
// registered handlers in-process.
|
|
932
|
+
const queueMap = new Map<string, QueueHandler[]>();
|
|
933
|
+
|
|
885
934
|
for (const [providerClass, descriptors] of providersRabbitMQ.entries()) {
|
|
886
935
|
const instance = injectables?.get(providerClass) ?? new providerClass();
|
|
887
936
|
|
|
@@ -893,7 +942,7 @@ if (document.readyState === 'loading') {
|
|
|
893
942
|
noAck = false,
|
|
894
943
|
} = descriptor.options;
|
|
895
944
|
|
|
896
|
-
// ── Routing-key mode: exchange + routingKey
|
|
945
|
+
// ── Routing-key mode: exchange + routingKey ─────────────────────
|
|
897
946
|
if (exchange && routingKey) {
|
|
898
947
|
AppStartup.logger.log(
|
|
899
948
|
`Registering RabbitMQ consumer for exchange: "${exchange}" routingKey: "${routingKey}" → ${providerClass.name}.${descriptor.methodName}()`,
|
|
@@ -949,7 +998,7 @@ if (document.readyState === 'loading') {
|
|
|
949
998
|
continue;
|
|
950
999
|
}
|
|
951
1000
|
|
|
952
|
-
// ──
|
|
1001
|
+
// ── Queue mode: collect and group by queue name ─────────────────
|
|
953
1002
|
if (!queue) {
|
|
954
1003
|
AppStartup.logger.warn(
|
|
955
1004
|
`@RabbitSubscribe on ${providerClass.name}.${descriptor.methodName}() has neither 'queue' nor 'exchange'+'routingKey' – skipping.`,
|
|
@@ -957,53 +1006,110 @@ if (document.readyState === 'loading') {
|
|
|
957
1006
|
continue;
|
|
958
1007
|
}
|
|
959
1008
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1009
|
+
if (!queueMap.has(queue)) queueMap.set(queue, []);
|
|
1010
|
+
const queueHandlers = queueMap.get(queue) as QueueHandler[];
|
|
1011
|
+
queueHandlers.push({
|
|
1012
|
+
instance,
|
|
1013
|
+
descriptor,
|
|
1014
|
+
noAck,
|
|
1015
|
+
providerName: providerClass.name,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
963
1019
|
|
|
964
|
-
|
|
965
|
-
|
|
1020
|
+
// ── Step 2: one AMQP consumer per unique queue, fan-out in-process ──
|
|
1021
|
+
for (const [queue, handlers] of queueMap.entries()) {
|
|
1022
|
+
// Use noAck only when every handler opts in; otherwise manual ack.
|
|
1023
|
+
const noAck = handlers.every((h) => h.noAck);
|
|
966
1024
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1025
|
+
const handlerList = handlers
|
|
1026
|
+
.map((h) => {
|
|
1027
|
+
const rk = h.descriptor.options.routingKey;
|
|
1028
|
+
return `${h.providerName}.${h.descriptor.methodName}()${rk ? ` [${rk}]` : ""}`;
|
|
1029
|
+
})
|
|
1030
|
+
.join(", ");
|
|
971
1031
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
} catch {
|
|
976
|
-
return raw.content.toString();
|
|
977
|
-
}
|
|
978
|
-
})();
|
|
1032
|
+
AppStartup.logger.log(
|
|
1033
|
+
`Registering RabbitMQ consumer for queue: "${queue}" → [${handlerList}]`,
|
|
1034
|
+
);
|
|
979
1035
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1036
|
+
try {
|
|
1037
|
+
const channel = await RabbitMQConnection.getConsumerChannel(queue);
|
|
1038
|
+
|
|
1039
|
+
await channel.consume(
|
|
1040
|
+
queue,
|
|
1041
|
+
async (raw) => {
|
|
1042
|
+
if (!raw) return; // consumer cancelled
|
|
1043
|
+
|
|
1044
|
+
const data = (() => {
|
|
1045
|
+
try {
|
|
1046
|
+
return JSON.parse(raw.content.toString());
|
|
1047
|
+
} catch {
|
|
1048
|
+
return raw.content.toString();
|
|
1049
|
+
}
|
|
1050
|
+
})();
|
|
1051
|
+
|
|
1052
|
+
// Settle guard: ack/nack/reject may only be called once per
|
|
1053
|
+
// delivery tag regardless of how many handlers invoke it.
|
|
1054
|
+
let settled = false;
|
|
1055
|
+
const settle = (fn: () => void) => {
|
|
1056
|
+
if (!settled) {
|
|
1057
|
+
settled = true;
|
|
1058
|
+
fn();
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const msg: RabbitMessage = {
|
|
1063
|
+
data,
|
|
1064
|
+
raw,
|
|
1065
|
+
ack: () => settle(() => channel.ack(raw)),
|
|
1066
|
+
nack: (requeue = true) =>
|
|
1067
|
+
settle(() => channel.nack(raw, false, requeue)),
|
|
1068
|
+
reject: () => settle(() => channel.reject(raw, false)),
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
for (const {
|
|
1072
|
+
instance,
|
|
1073
|
+
descriptor,
|
|
1074
|
+
noAck: handlerNoAck,
|
|
1075
|
+
providerName,
|
|
1076
|
+
} of handlers) {
|
|
1077
|
+
// ── Routing-key filter ─────────────────────────────────────────────
|
|
1078
|
+
// When { queue, routingKey } is set without exchange, only dispatch
|
|
1079
|
+
// if the message's routing key matches the declared pattern.
|
|
1080
|
+
const handlerRoutingKey = descriptor.options.routingKey;
|
|
1081
|
+
if (
|
|
1082
|
+
handlerRoutingKey &&
|
|
1083
|
+
!matchRoutingKey(handlerRoutingKey, raw.fields.routingKey)
|
|
1084
|
+
) {
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
987
1087
|
|
|
988
1088
|
try {
|
|
989
1089
|
await instance[descriptor.methodName](msg);
|
|
990
1090
|
} catch (err: any) {
|
|
991
1091
|
AppStartup.logger.error(
|
|
992
|
-
`Unhandled error in RabbitMQ handler ${
|
|
1092
|
+
`Unhandled error in RabbitMQ handler ${providerName}.${descriptor.methodName}() on queue "${queue}": ${err.message}`,
|
|
993
1093
|
);
|
|
994
|
-
if (!
|
|
995
|
-
|
|
996
|
-
channel.nack(raw, false, true);
|
|
1094
|
+
if (!handlerNoAck && !settled) {
|
|
1095
|
+
settle(() => channel.nack(raw, false, true));
|
|
997
1096
|
}
|
|
998
1097
|
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ── Auto-ack if no handler consumed the message ────────────────
|
|
1101
|
+
// All handlers were filtered out by routingKey – ack silently
|
|
1102
|
+
// to prevent the message from piling up as unacked.
|
|
1103
|
+
if (!noAck && !settled) {
|
|
1104
|
+
settle(() => channel.ack(raw));
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
{ noAck },
|
|
1108
|
+
);
|
|
1109
|
+
} catch (err: any) {
|
|
1110
|
+
AppStartup.logger.error(
|
|
1111
|
+
`Failed to register consumer for queue "${queue}": ${err.message}`,
|
|
1112
|
+
);
|
|
1007
1113
|
}
|
|
1008
1114
|
}
|
|
1009
1115
|
})();
|