@grupodiariodaregiao/bunstone 0.3.11 → 0.4.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/index.js CHANGED
@@ -99813,8 +99813,8 @@ class HttpException extends Error {
99813
99813
  response;
99814
99814
  constructor(response, status) {
99815
99815
  const responseObj = typeof response === "string" ? { message: response } : response;
99816
- const errorMessage = typeof response === "string" ? response : typeof response === "object" && response !== null && ("message" in response) ? String(response.message) : "Error";
99817
- super(errorMessage);
99816
+ const message = typeof response === "string" ? response : response.message ?? JSON.stringify(response);
99817
+ super(message);
99818
99818
  this.status = status;
99819
99819
  this.response = responseObj;
99820
99820
  Object.setPrototypeOf(this, HttpException.prototype);
@@ -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
  }
@@ -117125,7 +117139,10 @@ class AppStartup {
117125
117139
  errors: errors5
117126
117140
  };
117127
117141
  }
117128
- return error48;
117142
+ set2.status = 500;
117143
+ return {
117144
+ message: error48 instanceof Error ? error48.message : "Internal Server Error"
117145
+ };
117129
117146
  });
117130
117147
  if (options?.cors) {
117131
117148
  AppStartup.elysia.use(cors(options.cors));
@@ -117576,7 +117593,51 @@ if (document.readyState === 'loading') {
117576
117593
  for (const [providerClass, descriptors] of providersRabbitMQ.entries()) {
117577
117594
  const instance = injectables?.get(providerClass) ?? new providerClass;
117578
117595
  for (const descriptor of descriptors) {
117579
- const { queue: queue2, noAck = false } = descriptor.options;
117596
+ const {
117597
+ queue: queue2,
117598
+ exchange,
117599
+ routingKey,
117600
+ noAck = false
117601
+ } = descriptor.options;
117602
+ if (exchange && routingKey) {
117603
+ AppStartup.logger.log(`Registering RabbitMQ consumer for exchange: "${exchange}" routingKey: "${routingKey}" \u2192 ${providerClass.name}.${descriptor.methodName}()`);
117604
+ try {
117605
+ const { channel, queueName } = await RabbitMQConnection.createRoutingKeyConsumerChannel(exchange, routingKey);
117606
+ await channel.consume(queueName, async (raw) => {
117607
+ if (!raw)
117608
+ return;
117609
+ const data = (() => {
117610
+ try {
117611
+ return JSON.parse(raw.content.toString());
117612
+ } catch {
117613
+ return raw.content.toString();
117614
+ }
117615
+ })();
117616
+ const msg = {
117617
+ data,
117618
+ raw,
117619
+ ack: () => channel.ack(raw),
117620
+ nack: (requeue = true) => channel.nack(raw, false, requeue),
117621
+ reject: () => channel.reject(raw, false)
117622
+ };
117623
+ try {
117624
+ await instance[descriptor.methodName](msg);
117625
+ } catch (err) {
117626
+ AppStartup.logger.error(`Unhandled error in RabbitMQ handler ${providerClass.name}.${descriptor.methodName}() on exchange "${exchange}" routingKey "${routingKey}": ${err.message}`);
117627
+ if (!noAck) {
117628
+ channel.nack(raw, false, true);
117629
+ }
117630
+ }
117631
+ }, { noAck });
117632
+ } catch (err) {
117633
+ AppStartup.logger.error(`Failed to register consumer for exchange "${exchange}" routingKey "${routingKey}": ${err.message}`);
117634
+ }
117635
+ continue;
117636
+ }
117637
+ if (!queue2) {
117638
+ AppStartup.logger.warn(`@RabbitSubscribe on ${providerClass.name}.${descriptor.methodName}() has neither 'queue' nor 'exchange'+'routingKey' \u2013 skipping.`);
117639
+ continue;
117640
+ }
117580
117641
  AppStartup.logger.log(`Registering RabbitMQ consumer for queue: "${queue2}" \u2192 ${providerClass.name}.${descriptor.methodName}()`);
117581
117642
  try {
117582
117643
  const channel = await RabbitMQConnection.getConsumerChannel(queue2);
@@ -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: string;
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.
@@ -142,7 +142,11 @@ export class AppStartup {
142
142
  };
143
143
  }
144
144
 
145
- return error;
145
+ set.status = 500;
146
+ return {
147
+ message:
148
+ error instanceof Error ? error.message : "Internal Server Error",
149
+ };
146
150
  });
147
151
 
148
152
  if (options?.cors) {
@@ -882,7 +886,76 @@ if (document.readyState === 'loading') {
882
886
  const instance = injectables?.get(providerClass) ?? new providerClass();
883
887
 
884
888
  for (const descriptor of descriptors) {
885
- const { queue, noAck = false } = descriptor.options;
889
+ const {
890
+ queue,
891
+ exchange,
892
+ routingKey,
893
+ noAck = false,
894
+ } = descriptor.options;
895
+
896
+ // ── Routing-key mode: exchange + routingKey ─────────────────────────
897
+ if (exchange && routingKey) {
898
+ AppStartup.logger.log(
899
+ `Registering RabbitMQ consumer for exchange: "${exchange}" routingKey: "${routingKey}" → ${providerClass.name}.${descriptor.methodName}()`,
900
+ );
901
+
902
+ try {
903
+ const { channel, queueName } =
904
+ await RabbitMQConnection.createRoutingKeyConsumerChannel(
905
+ exchange,
906
+ routingKey,
907
+ );
908
+
909
+ await channel.consume(
910
+ queueName,
911
+ async (raw) => {
912
+ if (!raw) return;
913
+
914
+ const data = (() => {
915
+ try {
916
+ return JSON.parse(raw.content.toString());
917
+ } catch {
918
+ return raw.content.toString();
919
+ }
920
+ })();
921
+
922
+ const msg: RabbitMessage = {
923
+ data,
924
+ raw,
925
+ ack: () => channel.ack(raw),
926
+ nack: (requeue = true) => channel.nack(raw, false, requeue),
927
+ reject: () => channel.reject(raw, false),
928
+ };
929
+
930
+ try {
931
+ await instance[descriptor.methodName](msg);
932
+ } catch (err: any) {
933
+ AppStartup.logger.error(
934
+ `Unhandled error in RabbitMQ handler ${providerClass.name}.${descriptor.methodName}() on exchange "${exchange}" routingKey "${routingKey}": ${err.message}`,
935
+ );
936
+ if (!noAck) {
937
+ channel.nack(raw, false, true);
938
+ }
939
+ }
940
+ },
941
+ { noAck },
942
+ );
943
+ } catch (err: any) {
944
+ AppStartup.logger.error(
945
+ `Failed to register consumer for exchange "${exchange}" routingKey "${routingKey}": ${err.message}`,
946
+ );
947
+ }
948
+
949
+ continue;
950
+ }
951
+
952
+ // ── Direct queue mode ──────────────────────────────────────────────
953
+ if (!queue) {
954
+ AppStartup.logger.warn(
955
+ `@RabbitSubscribe on ${providerClass.name}.${descriptor.methodName}() has neither 'queue' nor 'exchange'+'routingKey' – skipping.`,
956
+ );
957
+ continue;
958
+ }
886
959
 
887
960
  AppStartup.logger.log(
888
961
  `Registering RabbitMQ consumer for queue: "${queue}" → ${providerClass.name}.${descriptor.methodName}()`,
@@ -10,17 +10,12 @@ export class HttpException extends Error {
10
10
  ) {
11
11
  const responseObj =
12
12
  typeof response === "string" ? { message: response } : response;
13
- // Extract a simple message string for the Error base class
14
- // to avoid JSON.stringify issues when error is serialized
15
- const errorMessage =
13
+ const message =
16
14
  typeof response === "string"
17
15
  ? response
18
- : typeof response === "object" &&
19
- response !== null &&
20
- "message" in response
21
- ? String(response.message)
22
- : "Error";
23
- super(errorMessage);
16
+ : ((response as { message?: string }).message ??
17
+ JSON.stringify(response));
18
+ super(message);
24
19
  this.response = responseObj;
25
20
  Object.setPrototypeOf(this, HttpException.prototype);
26
21
  }
@@ -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: string;
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.
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "types": "./dist/*.d.ts"
14
14
  }
15
15
  },
16
- "version": "0.3.11",
16
+ "version": "0.4.0",
17
17
  "homepage": "https://bunstone.diario.one/",
18
18
  "repository": {
19
19
  "url": "https://github.com/diariodaregiao/bunstone.git",