@grupodiariodaregiao/bunstone 0.4.0 → 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 CHANGED
@@ -117590,6 +117590,7 @@ if (document.readyState === 'loading') {
117590
117590
  AppStartup.logger.error(`RabbitMQ initialisation failed: ${err.message}`);
117591
117591
  return;
117592
117592
  }
117593
+ const queueMap = new Map;
117593
117594
  for (const [providerClass, descriptors] of providersRabbitMQ.entries()) {
117594
117595
  const instance = injectables?.get(providerClass) ?? new providerClass;
117595
117596
  for (const descriptor of descriptors) {
@@ -117638,38 +117639,65 @@ if (document.readyState === 'loading') {
117638
117639
  AppStartup.logger.warn(`@RabbitSubscribe on ${providerClass.name}.${descriptor.methodName}() has neither 'queue' nor 'exchange'+'routingKey' \u2013 skipping.`);
117639
117640
  continue;
117640
117641
  }
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
- };
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) {
117661
117689
  try {
117662
117690
  await instance[descriptor.methodName](msg);
117663
117691
  } 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);
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));
117667
117695
  }
117668
117696
  }
117669
- }, { noAck });
117670
- } catch (err) {
117671
- AppStartup.logger.error(`Failed to register consumer for queue "${queue2}": ${err.message}`);
117672
- }
117697
+ }
117698
+ }, { noAck });
117699
+ } catch (err) {
117700
+ AppStartup.logger.error(`Failed to register consumer for queue "${queue2}": ${err.message}`);
117673
117701
  }
117674
117702
  }
117675
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;
@@ -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,6 +897,12 @@ 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
 
@@ -893,7 +914,7 @@ if (document.readyState === 'loading') {
893
914
  noAck = false,
894
915
  } = descriptor.options;
895
916
 
896
- // ── Routing-key mode: exchange + routingKey ─────────────────────────
917
+ // ── Routing-key mode: exchange + routingKey ─────────────────────
897
918
  if (exchange && routingKey) {
898
919
  AppStartup.logger.log(
899
920
  `Registering RabbitMQ consumer for exchange: "${exchange}" routingKey: "${routingKey}" → ${providerClass.name}.${descriptor.methodName}()`,
@@ -949,7 +970,7 @@ if (document.readyState === 'loading') {
949
970
  continue;
950
971
  }
951
972
 
952
- // ── Direct queue mode ──────────────────────────────────────────────
973
+ // ── Queue mode: collect and group by queue name ─────────────────
953
974
  if (!queue) {
954
975
  AppStartup.logger.warn(
955
976
  `@RabbitSubscribe on ${providerClass.name}.${descriptor.methodName}() has neither 'queue' nor 'exchange'+'routingKey' – skipping.`,
@@ -957,53 +978,89 @@ if (document.readyState === 'loading') {
957
978
  continue;
958
979
  }
959
980
 
960
- AppStartup.logger.log(
961
- `Registering RabbitMQ consumer for queue: "${queue}" → ${providerClass.name}.${descriptor.methodName}()`,
962
- );
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
+ }
963
991
 
964
- try {
965
- const channel = await RabbitMQConnection.getConsumerChannel(queue);
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);
966
996
 
967
- await channel.consume(
968
- queue,
969
- async (raw) => {
970
- if (!raw) return; // consumer cancelled
997
+ const handlerList = handlers
998
+ .map((h) => `${h.providerName}.${h.descriptor.methodName}()`)
999
+ .join(", ");
971
1000
 
972
- const data = (() => {
973
- try {
974
- return JSON.parse(raw.content.toString());
975
- } catch {
976
- return raw.content.toString();
977
- }
978
- })();
1001
+ AppStartup.logger.log(
1002
+ `Registering RabbitMQ consumer for queue: "${queue}" → [${handlerList}]`,
1003
+ );
979
1004
 
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
- };
1005
+ try {
1006
+ const channel = await RabbitMQConnection.getConsumerChannel(queue);
987
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) {
988
1046
  try {
989
1047
  await instance[descriptor.methodName](msg);
990
1048
  } catch (err: any) {
991
1049
  AppStartup.logger.error(
992
- `Unhandled error in RabbitMQ handler ${providerClass.name}.${descriptor.methodName}() on queue "${queue}": ${err.message}`,
1050
+ `Unhandled error in RabbitMQ handler ${providerName}.${descriptor.methodName}() on queue "${queue}": ${err.message}`,
993
1051
  );
994
- if (!noAck) {
995
- // Nack and requeue by default so the message isn't lost
996
- channel.nack(raw, false, true);
1052
+ if (!handlerNoAck && !settled) {
1053
+ settle(() => channel.nack(raw, false, true));
997
1054
  }
998
1055
  }
999
- },
1000
- { noAck },
1001
- );
1002
- } catch (err: any) {
1003
- AppStartup.logger.error(
1004
- `Failed to register consumer for queue "${queue}": ${err.message}`,
1005
- );
1006
- }
1056
+ }
1057
+ },
1058
+ { noAck },
1059
+ );
1060
+ } catch (err: any) {
1061
+ AppStartup.logger.error(
1062
+ `Failed to register consumer for queue "${queue}": ${err.message}`,
1063
+ );
1007
1064
  }
1008
1065
  }
1009
1066
  })();
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.1",
17
17
  "homepage": "https://bunstone.diario.one/",
18
18
  "repository": {
19
19
  "url": "https://github.com/diariodaregiao/bunstone.git",