@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 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
- AppStartup.logger.log(`Registering RabbitMQ consumer for queue: "${queue2}" \u2192 ${providerClass.name}.${descriptor.methodName}()`);
117642
- try {
117643
- const channel = await RabbitMQConnection.getConsumerChannel(queue2);
117644
- await channel.consume(queue2, async (raw) => {
117645
- if (!raw)
117646
- return;
117647
- const data = (() => {
117648
- try {
117649
- return JSON.parse(raw.content.toString());
117650
- } catch {
117651
- return raw.content.toString();
117652
- }
117653
- })();
117654
- const msg = {
117655
- data,
117656
- raw,
117657
- ack: () => channel.ack(raw),
117658
- nack: (requeue = true) => channel.nack(raw, false, requeue),
117659
- reject: () => channel.reject(raw, false)
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 ${providerClass.name}.${descriptor.methodName}() on queue "${queue2}": ${err.message}`);
117665
- if (!noAck) {
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
- }, { noAck });
117670
- } catch (err) {
117671
- AppStartup.logger.error(`Failed to register consumer for queue "${queue2}": ${err.message}`);
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;
@@ -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
- // ── Direct queue mode ──────────────────────────────────────────────
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
- AppStartup.logger.log(
961
- `Registering RabbitMQ consumer for queue: "${queue}" → ${providerClass.name}.${descriptor.methodName}()`,
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
- try {
965
- const channel = await RabbitMQConnection.getConsumerChannel(queue);
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
- await channel.consume(
968
- queue,
969
- async (raw) => {
970
- if (!raw) return; // consumer cancelled
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
- const data = (() => {
973
- try {
974
- return JSON.parse(raw.content.toString());
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
- const msg: RabbitMessage = {
981
- data,
982
- raw,
983
- ack: () => channel.ack(raw),
984
- nack: (requeue = true) => channel.nack(raw, false, requeue),
985
- reject: () => channel.reject(raw, false),
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 ${providerClass.name}.${descriptor.methodName}() on queue "${queue}": ${err.message}`,
1092
+ `Unhandled error in RabbitMQ handler ${providerName}.${descriptor.methodName}() on queue "${queue}": ${err.message}`,
993
1093
  );
994
- if (!noAck) {
995
- // Nack and requeue by default so the message isn't lost
996
- channel.nack(raw, false, true);
1094
+ if (!handlerNoAck && !settled) {
1095
+ settle(() => channel.nack(raw, false, true));
997
1096
  }
998
1097
  }
999
- },
1000
- { noAck },
1001
- );
1002
- } catch (err: any) {
1003
- AppStartup.logger.error(
1004
- `Failed to register consumer for queue "${queue}": ${err.message}`,
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
  })();
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "types": "./dist/*.d.ts"
14
14
  }
15
15
  },
16
- "version": "0.4.0",
16
+ "version": "0.4.2",
17
17
  "homepage": "https://bunstone.diario.one/",
18
18
  "repository": {
19
19
  "url": "https://github.com/diariodaregiao/bunstone.git",