@jsnw/nestjs-rabbitmq 1.0.2 → 1.2.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.
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RabbitmqQueue = exports.RabbitmqExchange = exports.Rabbitmq = void 0;
3
+ exports.RabbitmqSubscriber = exports.RabbitmqQueue = exports.RabbitmqExchange = exports.Rabbitmq = void 0;
4
4
  var rabbitmq_1 = require("./rabbitmq");
5
5
  Object.defineProperty(exports, "Rabbitmq", { enumerable: true, get: function () { return rabbitmq_1.Rabbitmq; } });
6
6
  var rabbitmq_exchange_1 = require("./rabbitmq-exchange");
7
7
  Object.defineProperty(exports, "RabbitmqExchange", { enumerable: true, get: function () { return rabbitmq_exchange_1.RabbitmqExchange; } });
8
8
  var rabbitmq_queue_1 = require("./rabbitmq-queue");
9
9
  Object.defineProperty(exports, "RabbitmqQueue", { enumerable: true, get: function () { return rabbitmq_queue_1.RabbitmqQueue; } });
10
+ var rabbitmq_subscriber_1 = require("./rabbitmq-subscriber");
11
+ Object.defineProperty(exports, "RabbitmqSubscriber", { enumerable: true, get: function () { return rabbitmq_subscriber_1.RabbitmqSubscriber; } });
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RabbitmqSubscriber = void 0;
4
+ const rabbitmq_client_1 = require("rabbitmq-client");
5
+ const common_1 = require("@nestjs/common");
6
+ const rabbitmq_helpers_1 = require("./rabbitmq.helpers");
7
+ class RabbitmqSubscriber {
8
+ id;
9
+ connection;
10
+ params;
11
+ subscriber;
12
+ logger;
13
+ consumer;
14
+ _isActive;
15
+ /**
16
+ * @return {string}
17
+ */
18
+ get consumerTag() {
19
+ return this.consumer.consumerTag;
20
+ }
21
+ /**
22
+ * @return {Consumer["stats"]}
23
+ */
24
+ get stats() {
25
+ return this.consumer.stats;
26
+ }
27
+ /**
28
+ * @return {boolean}
29
+ */
30
+ get isActive() {
31
+ return this._isActive;
32
+ }
33
+ /**
34
+ * @param {string} id
35
+ * @param {Connection} connection
36
+ * @param {RabbitmqSubscribeParams} params
37
+ * @param {RabbitmqSubscriberFunction} subscriber
38
+ */
39
+ constructor(id, connection, params, subscriber) {
40
+ this.id = id;
41
+ this.connection = connection;
42
+ this.params = params;
43
+ this.subscriber = subscriber;
44
+ this.logger = new common_1.Logger(`${this.constructor.name}(id=${id})`);
45
+ this._isActive = !params.autoStart;
46
+ this.consumer = this.connection.createConsumer({
47
+ queue: params.queue.name,
48
+ requeue: !!params.requeue,
49
+ qos: {
50
+ prefetchSize: params.prefetchSize ?? 0,
51
+ prefetchCount: params.prefetchCount ?? 1
52
+ },
53
+ concurrency: params.concurrency ?? 1,
54
+ lazy: !!params.autoStart
55
+ }, this.onMessage);
56
+ }
57
+ /**
58
+ */
59
+ start() {
60
+ if (this._isActive)
61
+ return;
62
+ this._isActive = true;
63
+ this.consumer.start();
64
+ }
65
+ /**
66
+ * @return {Promise<void>}
67
+ */
68
+ stop() {
69
+ if (!this._isActive)
70
+ return Promise.resolve();
71
+ this._isActive = false;
72
+ return this.consumer.close();
73
+ }
74
+ /**
75
+ * @param {AsyncMessage} message
76
+ * @return {Promise<ConsumerStatus>}
77
+ * @private
78
+ */
79
+ onMessage = async (message) => {
80
+ if (!message.body || (typeof message.body !== 'string' && !Buffer.isBuffer(message.body)))
81
+ return rabbitmq_client_1.ConsumerStatus.DROP;
82
+ const messageBody = Buffer.isBuffer(message.body)
83
+ ? message.body.toString('utf-8')
84
+ : typeof message.body === 'string'
85
+ ? message.body
86
+ : null;
87
+ if (!messageBody)
88
+ return rabbitmq_client_1.ConsumerStatus.DROP;
89
+ const isJSON = message.contentType === 'application/json';
90
+ let data;
91
+ try {
92
+ data = isJSON ? JSON.parse(messageBody) : messageBody;
93
+ }
94
+ catch (e) {
95
+ this.logger.error(`Failed to parse message body as JSON`);
96
+ return rabbitmq_client_1.ConsumerStatus.DROP;
97
+ }
98
+ if (!!this.params.validation?.schema) {
99
+ const { data: parsed, error, success } = this.params.validation.schema.safeParse(data);
100
+ if (error || !success)
101
+ return (0, rabbitmq_helpers_1.mapRabbitmqResponseToConsumerStatus)(this.params.validation.onFail ?? 'drop');
102
+ data = parsed;
103
+ }
104
+ try {
105
+ const response = await (typeof this.subscriber === 'function'
106
+ ? this.subscriber(data, message)
107
+ : this.subscriber.instance[this.subscriber.methodName](data, message));
108
+ return (0, rabbitmq_helpers_1.mapRabbitmqResponseToConsumerStatus)(response);
109
+ }
110
+ catch (e) {
111
+ this.logger.error(`Error: ${e.constructor.name}(${e.message ?? ''})`);
112
+ return !!this.params.requeue ? rabbitmq_client_1.ConsumerStatus.REQUEUE : rabbitmq_client_1.ConsumerStatus.DROP;
113
+ }
114
+ };
115
+ }
116
+ exports.RabbitmqSubscriber = RabbitmqSubscriber;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapRabbitmqResponseToConsumerStatus = void 0;
4
+ const rabbitmq_client_1 = require("rabbitmq-client");
5
+ /**
6
+ * @param {RabbitmqResponse | string} response
7
+ * @return {ConsumerStatus}
8
+ */
9
+ const mapRabbitmqResponseToConsumerStatus = (response) => {
10
+ response = response.toLowerCase();
11
+ switch (response) {
12
+ case 'ack': return rabbitmq_client_1.ConsumerStatus.ACK;
13
+ case 'requeue': return rabbitmq_client_1.ConsumerStatus.REQUEUE;
14
+ case 'drop': return rabbitmq_client_1.ConsumerStatus.DROP;
15
+ }
16
+ return rabbitmq_client_1.ConsumerStatus.DROP;
17
+ };
18
+ exports.mapRabbitmqResponseToConsumerStatus = mapRabbitmqResponseToConsumerStatus;
@@ -3,14 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Rabbitmq = void 0;
4
4
  const rabbitmq_client_1 = require("rabbitmq-client");
5
5
  const common_1 = require("@nestjs/common");
6
+ const rabbitmq_subscriber_1 = require("./rabbitmq-subscriber");
6
7
  class Rabbitmq {
7
8
  name;
8
9
  logger;
9
10
  connection;
10
11
  exchanges = new Map();
11
12
  queues = new Map();
12
- consumers = new Set;
13
- consumersById = new Map();
13
+ subscribers = new Map;
14
14
  publisher = null;
15
15
  /**
16
16
  * @param {string | RabbitmqConstructorParams} arg1
@@ -101,96 +101,81 @@ class Rabbitmq {
101
101
  }
102
102
  }
103
103
  }
104
+ /**
105
+ * @param {RabbitmqQueue | string} queue
106
+ * @return {Promise<RabbitmqQueueStats | null>}
107
+ */
108
+ async queueStats(queue) {
109
+ try {
110
+ const { messageCount, consumerCount } = await this.connection.queueDeclare({
111
+ queue: typeof queue === 'string' ? queue : queue.name,
112
+ passive: true
113
+ });
114
+ return { messages: messageCount, consumers: consumerCount };
115
+ }
116
+ catch (e) {
117
+ return null;
118
+ }
119
+ }
120
+ /**
121
+ * @param {RabbitmqQueue} queue
122
+ * @return {Promise<number>}
123
+ */
124
+ async purgeQueue(queue) {
125
+ const { messageCount } = await this.connection.queuePurge({
126
+ queue: queue.name
127
+ });
128
+ return messageCount;
129
+ }
104
130
  /**
105
131
  * @param {RabbitmqSubscribeParams} params
106
- * @param {RabbitmqSubscriber} subscriber
107
- * @returns {Promise<void>}
132
+ * @param {RabbitmqSubscriberFunction} subscriber
133
+ * @return {Promise<RabbitmqSubscriber>}
108
134
  */
109
135
  async subscribe(params, subscriber) {
136
+ const id = params.id ?? (typeof subscriber === 'function'
137
+ ? Math.random().toString()
138
+ : subscriber.instance.constructor.name + '.' + subscriber.methodName);
110
139
  await this.declareQueue(params.queue);
111
- const consumer = this.connection.createConsumer({
112
- queue: params.queue.name,
113
- requeue: !!params.requeue,
114
- qos: {
115
- prefetchSize: params.prefetchSize ?? 0,
116
- prefetchCount: params.prefetchCount ?? 1
117
- },
118
- concurrency: params.concurrency ?? 1,
119
- lazy: !!params.autoStart
120
- }, this.internalConsumer.bind(this, params, subscriber));
121
- this.consumers.add(consumer);
122
- if (params.id)
123
- this.consumersById.set(params.id, { isStarted: !!params.autoStart, consumer: consumer });
140
+ if (this.subscribers.has(id))
141
+ throw new Error(`Seems like a subscriber (id: ${id}) already registered`);
142
+ const rmqSubscriber = new rabbitmq_subscriber_1.RabbitmqSubscriber(id, this.connection, params, subscriber);
143
+ this.subscribers.set(id, rmqSubscriber);
144
+ return rmqSubscriber;
145
+ }
146
+ /**
147
+ * @param {string} id
148
+ * @return {RabbitmqSubscriber | null}
149
+ */
150
+ getSubscriberById(id) {
151
+ return this.subscribers.get(id) ?? null;
124
152
  }
125
153
  /**
126
154
  * @param {string} id
127
155
  * @returns {void}
128
156
  */
129
157
  startSubscriber(id) {
130
- const consumer = this.consumersById.get(id);
131
- if (!consumer)
158
+ const subscriber = this.subscribers.get(id);
159
+ if (!subscriber)
132
160
  throw new Error(`Subscriber ${id} not found for connection ${this.name}`);
133
- if (consumer.isStarted)
134
- return;
135
- consumer.isStarted = true;
136
- consumer.consumer.start();
161
+ subscriber.start();
137
162
  }
138
163
  /**
139
- * @returns {void}
164
+ * @param {string} id
165
+ * @return {Promise<void>}
140
166
  */
141
- startPendingSubscribers() {
142
- for (const consumer of this.consumersById.values()) {
143
- if (consumer.isStarted)
144
- continue;
145
- consumer.isStarted = true;
146
- consumer.consumer.start();
147
- }
167
+ async stopSubscriber(id) {
168
+ const subscriber = this.subscribers.get(id);
169
+ if (!subscriber)
170
+ throw new Error(`Subscriber ${id} not found for connection ${this.name}`);
171
+ await subscriber.stop();
148
172
  }
149
173
  /**
150
- * @param {RabbitmqSubscribeParams} params
151
- * @param {RabbitmqSubscriber} subscriber
152
- * @param {AsyncMessage} message
153
- * @returns {Promise<ConsumerStatus>}
154
- * @private
174
+ * @returns {void}
155
175
  */
156
- async internalConsumer(params, subscriber, message) {
157
- if (!message.body || (typeof message.body !== 'string' && !Buffer.isBuffer(message.body)))
158
- return rabbitmq_client_1.ConsumerStatus.DROP;
159
- const messageBody = Buffer.isBuffer(message.body)
160
- ? message.body.toString('utf-8')
161
- : typeof message.body === 'string'
162
- ? message.body
163
- : null;
164
- if (!messageBody)
165
- return rabbitmq_client_1.ConsumerStatus.DROP;
166
- const isJSON = message.contentType === 'application/json';
167
- let data;
168
- try {
169
- data = isJSON ? JSON.parse(messageBody) : messageBody;
170
- }
171
- catch (e) {
172
- this.logger.error(`Failed to parse message body as JSON`);
173
- return rabbitmq_client_1.ConsumerStatus.DROP;
174
- }
175
- if (!!params.validation?.schema) {
176
- const { data: parsed, error, success } = params.validation.schema.safeParse(data);
177
- if (error || !success)
178
- return this.mapRabbitmqResponseToConsumerStatus(params.validation.onFail ?? 'drop');
179
- data = parsed;
180
- }
181
- const subscriberName = typeof subscriber === 'function'
182
- ? subscriber.name ?? 'anonymous function'
183
- : `${subscriber.instance.constructor.name}.${subscriber.methodName}`;
184
- try {
185
- const response = await (typeof subscriber === 'function'
186
- ? subscriber(data, message)
187
- : subscriber.instance[subscriber.methodName](data, message));
188
- return this.mapRabbitmqResponseToConsumerStatus(response);
189
- }
190
- catch (e) {
191
- this.logger.error(`Error in subscriber ${subscriberName} (connection: ${this.name}). Error: ${e.constructor.name}(${e.message ?? ''})`);
192
- return !!params.requeue ? rabbitmq_client_1.ConsumerStatus.REQUEUE : rabbitmq_client_1.ConsumerStatus.DROP;
193
- }
176
+ startPendingSubscribers() {
177
+ for (const subscriber of this.subscribers.values())
178
+ subscriber.start();
194
179
  }
195
180
  /**
196
181
  * @param {RabbitmqPublishOptions} options
@@ -207,27 +192,13 @@ class Rabbitmq {
207
192
  expiration: options.ttlMs?.toString() ?? undefined
208
193
  }, options.type === 'json' ? JSON.stringify(options.message) : options.message);
209
194
  }
210
- /**
211
- * @param {RabbitmqResponse | string} response
212
- * @return {ConsumerStatus}
213
- * @protected
214
- */
215
- mapRabbitmqResponseToConsumerStatus(response) {
216
- response = response.toLowerCase();
217
- switch (response) {
218
- case 'ack': return rabbitmq_client_1.ConsumerStatus.ACK;
219
- case 'requeue': return rabbitmq_client_1.ConsumerStatus.REQUEUE;
220
- case 'drop': return rabbitmq_client_1.ConsumerStatus.DROP;
221
- }
222
- return rabbitmq_client_1.ConsumerStatus.DROP;
223
- }
224
195
  /**
225
196
  * @return {Promise<void>}
226
197
  */
227
198
  async close() {
228
199
  const promises = [];
229
- for (const consumer of this.consumers.values())
230
- promises.push(consumer.close());
200
+ for (const subscriber of this.subscribers.values())
201
+ promises.push(subscriber.stop());
231
202
  promises.push(this.connection.close());
232
203
  await Promise.allSettled(promises);
233
204
  }
@@ -1,3 +1,4 @@
1
- export { Rabbitmq, type RabbitmqConstructorParams, type RabbitmqResponse, type RabbitmqSubscribeParams, type RabbitmqMessageValidation, type RabbitmqSubscriber, type RabbitmqPublishOptions } from './rabbitmq';
1
+ export { Rabbitmq, type RabbitmqConstructorParams, type RabbitmqResponse, type RabbitmqSubscribeParams, type RabbitmqMessageValidation, type RabbitmqSubscriberFunction, type RabbitmqPublishOptions, type RabbitmqQueueStats } from './rabbitmq';
2
2
  export { RabbitmqExchange, type RabbitmqExchangeType, type RabbitmqExchangeDeclaration } from './rabbitmq-exchange';
3
3
  export { RabbitmqQueue, type RabbitmqQueueArguments, type RabbitmqQueueBinding, type RabbitmqQueueDeclaration } from './rabbitmq-queue';
4
+ export { RabbitmqSubscriber } from './rabbitmq-subscriber';
@@ -0,0 +1,43 @@
1
+ import { type AsyncMessage, Connection, Consumer, ConsumerStatus } from 'rabbitmq-client';
2
+ import type { RabbitmqSubscribeParams, RabbitmqSubscriberFunction } from './rabbitmq';
3
+ export declare class RabbitmqSubscriber {
4
+ readonly id: string;
5
+ private readonly connection;
6
+ private readonly params;
7
+ private readonly subscriber;
8
+ private readonly logger;
9
+ private readonly consumer;
10
+ private _isActive;
11
+ /**
12
+ * @return {string}
13
+ */
14
+ get consumerTag(): string;
15
+ /**
16
+ * @return {Consumer["stats"]}
17
+ */
18
+ get stats(): Consumer['stats'];
19
+ /**
20
+ * @return {boolean}
21
+ */
22
+ get isActive(): boolean;
23
+ /**
24
+ * @param {string} id
25
+ * @param {Connection} connection
26
+ * @param {RabbitmqSubscribeParams} params
27
+ * @param {RabbitmqSubscriberFunction} subscriber
28
+ */
29
+ constructor(id: string, connection: Connection, params: RabbitmqSubscribeParams, subscriber: RabbitmqSubscriberFunction);
30
+ /**
31
+ */
32
+ start(): void;
33
+ /**
34
+ * @return {Promise<void>}
35
+ */
36
+ stop(): Promise<void>;
37
+ /**
38
+ * @param {AsyncMessage} message
39
+ * @return {Promise<ConsumerStatus>}
40
+ * @private
41
+ */
42
+ protected onMessage: (message: AsyncMessage) => Promise<ConsumerStatus>;
43
+ }
@@ -1,9 +1,10 @@
1
1
  import { z } from 'zod';
2
- import { type AsyncMessage, Connection, type Consumer, ConsumerStatus, Publisher } from 'rabbitmq-client';
2
+ import { type AsyncMessage, Connection, Publisher } from 'rabbitmq-client';
3
3
  import { RabbitmqExchange } from './rabbitmq-exchange';
4
4
  import { RabbitmqQueue } from './rabbitmq-queue';
5
5
  import { Logger } from '@nestjs/common';
6
6
  import { Jsonifiable } from 'type-fest';
7
+ import { RabbitmqSubscriber } from './rabbitmq-subscriber';
7
8
  export type RabbitmqConstructorParams = {
8
9
  name: string;
9
10
  /**
@@ -18,6 +19,10 @@ export type RabbitmqConstructorParams = {
18
19
  connectionTimeout?: number;
19
20
  };
20
21
  export type RabbitmqResponse = 'ack' | 'drop' | 'requeue';
22
+ export type RabbitmqQueueStats = {
23
+ messages: number;
24
+ consumers: number;
25
+ };
21
26
  export type RabbitmqMessageValidation = {
22
27
  schema: z.ZodTypeAny;
23
28
  onFail?: RabbitmqResponse;
@@ -36,7 +41,7 @@ export type RabbitmqSubscribeParams = {
36
41
  id?: never;
37
42
  autoStart?: never;
38
43
  });
39
- export type RabbitmqSubscriber = {
44
+ export type RabbitmqSubscriberFunction = {
40
45
  instance: any;
41
46
  methodName: string;
42
47
  } | ((data: any, message: AsyncMessage) => RabbitmqResponse | Promise<RabbitmqResponse>);
@@ -58,11 +63,7 @@ export declare class Rabbitmq {
58
63
  protected readonly connection: Connection;
59
64
  protected readonly exchanges: Map<string, RabbitmqExchange>;
60
65
  protected readonly queues: Map<string, RabbitmqQueue>;
61
- protected readonly consumers: Set<Consumer>;
62
- protected readonly consumersById: Map<string, {
63
- isStarted: boolean;
64
- consumer: Consumer;
65
- }>;
66
+ protected readonly subscribers: Map<string, RabbitmqSubscriber>;
66
67
  protected publisher: Publisher | null;
67
68
  constructor(name: string, connection: Connection);
68
69
  constructor(params: RabbitmqConstructorParams);
@@ -82,40 +83,46 @@ export declare class Rabbitmq {
82
83
  * @protected
83
84
  */
84
85
  protected setupQueueBindings(queue: RabbitmqQueue): Promise<void>;
86
+ /**
87
+ * @param {RabbitmqQueue | string} queue
88
+ * @return {Promise<RabbitmqQueueStats | null>}
89
+ */
90
+ queueStats(queue: RabbitmqQueue | string): Promise<RabbitmqQueueStats | null>;
91
+ /**
92
+ * @param {RabbitmqQueue} queue
93
+ * @return {Promise<number>}
94
+ */
95
+ purgeQueue(queue: RabbitmqQueue): Promise<number>;
85
96
  /**
86
97
  * @param {RabbitmqSubscribeParams} params
87
- * @param {RabbitmqSubscriber} subscriber
88
- * @returns {Promise<void>}
98
+ * @param {RabbitmqSubscriberFunction} subscriber
99
+ * @return {Promise<RabbitmqSubscriber>}
100
+ */
101
+ subscribe(params: RabbitmqSubscribeParams, subscriber: RabbitmqSubscriberFunction): Promise<RabbitmqSubscriber>;
102
+ /**
103
+ * @param {string} id
104
+ * @return {RabbitmqSubscriber | null}
89
105
  */
90
- subscribe(params: RabbitmqSubscribeParams, subscriber: RabbitmqSubscriber): Promise<void>;
106
+ getSubscriberById(id: string): RabbitmqSubscriber | null;
91
107
  /**
92
108
  * @param {string} id
93
109
  * @returns {void}
94
110
  */
95
111
  startSubscriber(id: string): void;
96
112
  /**
97
- * @returns {void}
113
+ * @param {string} id
114
+ * @return {Promise<void>}
98
115
  */
99
- startPendingSubscribers(): void;
116
+ stopSubscriber(id: string): Promise<void>;
100
117
  /**
101
- * @param {RabbitmqSubscribeParams} params
102
- * @param {RabbitmqSubscriber} subscriber
103
- * @param {AsyncMessage} message
104
- * @returns {Promise<ConsumerStatus>}
105
- * @private
118
+ * @returns {void}
106
119
  */
107
- private internalConsumer;
120
+ startPendingSubscribers(): void;
108
121
  /**
109
122
  * @param {RabbitmqPublishOptions} options
110
123
  * @return {Promise<void>}
111
124
  */
112
125
  publish(options: RabbitmqPublishOptions): Promise<void>;
113
- /**
114
- * @param {RabbitmqResponse | string} response
115
- * @return {ConsumerStatus}
116
- * @protected
117
- */
118
- protected mapRabbitmqResponseToConsumerStatus(response: RabbitmqResponse | string): ConsumerStatus;
119
126
  /**
120
127
  * @return {Promise<void>}
121
128
  */
@@ -0,0 +1,7 @@
1
+ import { ConsumerStatus } from 'rabbitmq-client';
2
+ import { type RabbitmqResponse } from './rabbitmq';
3
+ /**
4
+ * @param {RabbitmqResponse | string} response
5
+ * @return {ConsumerStatus}
6
+ */
7
+ export declare const mapRabbitmqResponseToConsumerStatus: (response: RabbitmqResponse | string) => ConsumerStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsnw/nestjs-rabbitmq",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "NestJS module for RabbitMQ integration with decorator-based consumers, message validation, and dead-letter exchange support",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",