amqp-suite 0.1.0 β†’ 0.1.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/README.md CHANGED
@@ -1,114 +1,274 @@
1
1
  # amqp-suite
2
2
 
3
- [![NPM version](https://img.shields.io/npm/v/amqp-suite)](https://www.npmjs.com/package/amqp-suite)
4
- [![NPM downloads](https://img.shields.io/npm/dw/amqp-suite.svg)](https://www.npmjs.com/package/amqp-suite)
5
- [![MIT license](https://img.shields.io/badge/License-MIT-bridhtgreen)](https://opensource.org/licenses/MIT)
3
+ [![NPM version](https://img.shields.io/npm/v/amqp-suite?color=1447e6&style=flat-square)](https://www.npmjs.com/package/amqp-suite)
4
+ [![MIT license](https://img.shields.io/badge/License-MIT-bridhtgreen?style=flat-square)](https://opensource.org/licenses/MIT)
5
+ [![NPM downloads](https://img.shields.io/npm/dw/amqp-suite?color=bridhtgreen&style=flat-square)](https://www.npmjs.com/package/amqp-suite)
6
6
  [![stars](https://img.shields.io/github/stars/iamcarlosdaniel/amqp-suite)](https://github.com/iamcarlosdaniel/amqp-suite)
7
7
 
8
- ![](docs/assets/repository_banner.png)
8
+ ![](https://raw.githubusercontent.com/iamcarlosdaniel/amqp-suite/main/docs/assets/repository_banner.png)
9
9
 
10
10
  `amqp-suite` is a simple and efficient AMQP (Advanced Message Queuing Protocol) client wrapper for Node.js that handles connection management, message publishing, and consuming messages from queues with a topic exchange. This package abstracts complex connection handling and simplifies AMQP usage in applications by providing easy-to-use methods for connecting, publishing, consuming, and gracefully shutting down the connection.
11
11
 
12
- ## Features
12
+ ## πŸ”₯ Features
13
13
 
14
- - Automatic Reconnection: Built-in retry logic for connection failures and drops.
15
- - Simplified Pub/Sub: Designed for 'topic' exchanges to allow flexible routing.
16
- - Structured Messaging: Automatic JSON serialization and deserialization.
17
- - Error Handling: Graceful handling of malformed messages and channel crashes.
18
- - Flow Control: Integrated prefetch support to prevent consumer saturation.
14
+ - **Automatic Reconnection:** Built-in retry logic for connection failures and drops.
15
+ - **Simplified Pub/Sub:** Designed for 'topic' exchanges to allow flexible routing.
16
+ - **Structured Messaging:** Automatic JSON serialization and deserialization.
17
+ - **Error Handling:** Graceful handling of malformed messages and channel crashes.
18
+ - **Flow Control:** Integrated prefetch support to prevent consumer saturation.
19
19
 
20
20
  ## Installation
21
21
 
22
+ ### Package manager
23
+
24
+ Using npm:
25
+
22
26
  ```bash
23
27
  npm install amqp-suite
24
28
  ```
25
29
 
30
+ Using yarn:
31
+
32
+ ```bash
33
+ yarn add amqp-suite
34
+ ```
35
+
36
+ Using pnpm:
37
+
38
+ ```bash
39
+ pnpm add amqp-suite
40
+ ```
41
+
42
+ Using bun:
43
+
44
+ ```bash
45
+ bun add amqp-suit
46
+ ```
47
+
48
+ Once the package is installed, you can import the library using ES Modules:
49
+
50
+ ```javascript
51
+ import { AmqpClient } from "amqp-suite";
52
+ ```
53
+
26
54
  ## Quick Start
27
55
 
28
56
  ### 1. Initialize and Connect
29
57
 
30
- Create an instance of the `AmqpClient` and establish a connection to the RabbitMQ broker.
58
+ Create an instance of `AmqpClient` and establish a connection to your RabbitMQ broker. This prepares the client to publish and consume messages.
31
59
 
32
60
  ```javascript
33
61
  import { AmqpClient } from "amqp-suite";
34
62
 
35
- const amqpClient = new AmqpClient("amqp://localhost", "my_exchange");
63
+ const amqpClient = new AmqpClient("amqp://localhost", "example-exchange");
36
64
 
37
- // Connect with optional retry config (retries, delay in ms)
38
- await amqpClient.connect(5, 2000);
65
+ await amqpClient.connect();
39
66
  ```
40
67
 
41
68
  ### 2. Publish Messages
42
69
 
43
- The `publish` method ensures that the message is stringified and sent as a persistent buffer.
70
+ The `publish` method automatically stringifies your message and sends it as a persistent buffer, ensuring it won’t be lost if the broker restarts.
44
71
 
45
72
  ```javascript
46
- const payload = {
47
- id: 123,
48
- event: "user_created",
49
- timestamp: new Date(),
50
- };
51
-
52
- await amqpClient.publish("user.events.create", payload);
73
+ await amqpClient.publish(
74
+ "example.events.hello_world", // Routing Key
75
+ {
76
+ message: "Hello World!",
77
+ },
78
+ {} // Options
79
+ );
53
80
  ```
54
81
 
55
82
  ### 3. Consume Messages
56
83
 
57
- The `consume` method automatically asserts queues, binds them to the exchange, and handles acknowledgments (`ack`/`nack`).
84
+ The `consume` method automatically creates queues, binds them to the exchange, and handles acknowledgments (`ack`/`nack`). You only need to provide the queue name and the function that will process incoming messages.
58
85
 
59
86
  ```javascript
60
87
  await amqpClient.consume(
61
- "user_service_queue",
62
- async (content, msg) => {
63
- console.log("Received data:", content);
64
- // Business logic here...
88
+ "example-queue", // Queue
89
+ (msg) => {
90
+ console.log("Received message:", msg);
91
+ },
92
+ {}, // Options
93
+ "example.events.hello_world" // Binding Key
94
+ );
95
+ ```
96
+
97
+ ### Example Overview
98
+
99
+ This diagram illustrates how a message is sent from the publisher, routed through the topic exchange, enqueued in the queue, and finally consumed by the consumer.
100
+
101
+ ![](https://raw.githubusercontent.com/iamcarlosdaniel/amqp-suite/main/docs/assets/example-architecture-diagram.svg)
102
+
103
+ Here’s a full example that connects, publishes, consumes messages, and finally closes the connection.
104
+
105
+ ```javascript
106
+ import { AmqpClient } from "amqp-suite";
107
+
108
+ const amqpClient = new AmqpClient("amqp://localhost", "example-exchange");
109
+
110
+ await amqpClient.connect();
111
+
112
+ await amqpClient.publish(
113
+ "example.events.hello_world", // Routing Key
114
+ {
115
+ message: "Hello World!",
116
+ },
117
+ {} // Options
118
+ );
119
+
120
+ await amqpClient.consume(
121
+ "example-queue", // Queue
122
+ (msg) => {
123
+ console.log("Received message:", msg);
65
124
  },
66
- { prefetch: 10 }, // Optional: defaults to 10
67
- "user.events.*" // Binding key (Topic pattern)
125
+ {}, // Options
126
+ "example.events.hello_world" // Binding Key
68
127
  );
128
+
129
+ await amqpClient.close();
69
130
  ```
70
131
 
132
+ > **Note:** You can check the full example in [examples/hello-world](https://github.com/iamcarlosdaniel/amqp-suite/tree/main/examples/hello-world).
133
+
71
134
  ## API Reference
72
135
 
73
136
  ### `new AmqpClient(amqpUrl, exchange)`
74
137
 
75
- - **amqpUrl**: The connection string (e.g., `amqp://user:pass@localhost:5672`).
76
- - **exchange**: The name of the topic exchange to use.
138
+ Creates a new instance of the AMQP client.
139
+
140
+ The client uses a **durable topic exchange** to enable flexible message routing using routing patterns.
141
+
142
+ #### Parameters
143
+
144
+ - **`amqpUrl`** (`string`)
145
+ The AMQP connection URL.
146
+ Example: `amqp://user:pass@localhost:5672`
147
+
148
+ - **`exchange`** (`string`)
149
+ The name of the **topic exchange** used for publishing and consuming messages.
150
+ The exchange is asserted as `durable`.
151
+
152
+ ---
77
153
 
78
154
  ### `.connect(retries = 5, delay = 5000)`
79
155
 
80
- Establishes the connection and creates the channel. If the connection drops, it will automatically attempt to reconnect.
156
+ Establishes a connection to the AMQP broker and creates a channel.
157
+ If the connection is lost unexpectedly, the client will automatically attempt to reconnect.
158
+
159
+ #### Parameters
160
+
161
+ - **`retries`** (`number`, optional)
162
+ Maximum number of reconnection attempts during the initial connection.
163
+ Default: `5`
164
+
165
+ - **`delay`** (`number`, optional)
166
+ Delay in milliseconds between reconnection attempts.
167
+ Default: `5000`
81
168
 
82
- - **retries** (optional): Number of reconnection attempts (default: `5`).
83
- - **delay** (optional): Time interval (in milliseconds) between retries (default: `5000`).
169
+ #### Behavior
170
+
171
+ - Prevents multiple simultaneous connection attempts.
172
+ - Automatically reconnects if the connection is closed by the broker.
173
+ - Reconnection attempts triggered after a connection drop do **not** reuse the original retry counter.
174
+
175
+ #### Returns
176
+
177
+ - `Promise<void>`
178
+
179
+ ---
84
180
 
85
181
  ### `.publish(routingKey, message, options = {})`
86
182
 
87
- Publishes a message to the configured exchange with the given routing key.
183
+ Publishes a message to the configured topic exchange using the specified routing key.
184
+
185
+ Messages are automatically serialized to JSON and published as **persistent** by default.
186
+
187
+ #### Parameters
188
+
189
+ - **`routingKey`** (`string`)
190
+ The routing key used to route the message.
191
+ Example: `user.events.create`
192
+
193
+ - **`message`** (`object`)
194
+ The message payload. It will be automatically serialized to JSON.
195
+
196
+ - **`options`** (`object`, optional)
197
+ Additional publish options supported by `amqplib`.
198
+ These options are merged with `{ persistent: true }`.
199
+
200
+ #### Behavior
88
201
 
89
- - **routingKey**: The routing key used to route the message (e.g., `'user.events.create'`).
90
- - **message**: The message payload, which will be stringified into JSON.
91
- - **options** (optional): Additional publish options from `amqplib`.
202
+ - If the channel is not initialized, the client will attempt to connect automatically.
203
+ - If the broker’s write buffer is full, the message may be temporarily buffered locally.
204
+
205
+ #### Returns
206
+
207
+ - `Promise<void>`
208
+
209
+ ---
92
210
 
93
211
  ### `.consume(queue, onMessage, options = {}, bindingKey = "#")`
94
212
 
95
- Consumes messages from the specified queue. The callback `onMessage` is executed when a message is received.
213
+ Consumes messages from the specified queue and binds it to the exchange using the provided routing pattern.
214
+
215
+ The `onMessage` callback is executed for each received message.
216
+
217
+ #### Parameters
96
218
 
97
- - **queue**: The name of the queue to consume messages from.
98
- - **onMessage**: An async callback function that will be called with the message content and the original message.
219
+ - **`queue`** (`string`)
220
+ The name of the queue to consume messages from.
221
+ The queue is asserted as `durable`.
222
+
223
+ - **`onMessage`** (`function`)
224
+ An asynchronous callback executed when a message is received.
99
225
 
100
226
  ```javascript
101
227
  async (content, rawMessage) => {
102
- /* logic */
228
+ // message handling logic
103
229
  };
104
230
  ```
105
231
 
106
- - **options** (optional): Additional options like `prefetch` (default is 10).
107
- - **bindingKey** (optional): The routing pattern to bind the queue (default is `#`).
232
+ - `content`: Parsed JSON message payload.
233
+ - `rawMessage`: The original `ConsumeMessage` from `amqplib`.
234
+
235
+ - **`options`** (`object`, optional)
236
+ Consumer configuration options.
237
+
238
+ - **`prefetch`** (`number`):
239
+ Limits the number of unacknowledged messages.
240
+ Default: `10`
241
+
242
+ - **`bindingKey`** (`string`, optional)
243
+ The routing pattern used to bind the queue to the exchange.
244
+ Default: `#` (matches all routing keys).
245
+
246
+ #### Behavior
247
+
248
+ - Messages are acknowledged (`ack`) automatically after successful processing.
249
+ - If an error is thrown while processing a message:
250
+
251
+ - The message is negatively acknowledged (`nack`)
252
+ - The message is **not requeued**, preventing infinite retry loops for malformed messages.
253
+
254
+ #### Returns
255
+
256
+ - `Promise<void>`
257
+
258
+ ---
108
259
 
109
260
  ### `.close()`
110
261
 
111
- Gracefully closes the channel and the connection.
262
+ Gracefully closes the AMQP channel and connection.
263
+
264
+ #### Behavior
265
+
266
+ - Prevents automatic reconnection during shutdown.
267
+ - Ensures resources are released cleanly.
268
+
269
+ #### Returns
270
+
271
+ - `Promise<void>`
112
272
 
113
273
  ## License
114
274
 
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "amqp-suite",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A simple wrapper for AMQP 0-9-1 messaging.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Carlos Daniel Menchaca Arauz",
8
8
  "email": "contact@iamcarlosdaniel.com",
9
- "url": "http://iamcarlosdaniel.com"
9
+ "url": "https://iamcarlosdaniel.com"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/iamcarlosdaniel/amqp-suite.git"
14
+ },
15
+ "homepage": "https://iamcarlosdaniel.com/projects/amqp-suite",
16
+ "bugs": {
17
+ "url": "https://github.com/iamcarlosdaniel/amqp-suite/issues/new"
10
18
  },
11
19
  "type": "module",
12
20
  "main": "src/index.js",
@@ -27,10 +35,10 @@
27
35
  "rabbitmq"
28
36
  ],
29
37
  "dependencies": {
30
- "amqplib": "^0.10.9"
38
+ "amqplib": "0.10.9"
31
39
  },
32
40
  "devDependencies": {
33
- "@vitest/ui": "^4.0.16",
34
- "vitest": "^4.0.16"
41
+ "@vitest/ui": "4.0.16",
42
+ "vitest": "4.0.16"
35
43
  }
36
44
  }
@@ -1,4 +1,4 @@
1
- declare module "amqp-wrapper" {
1
+ declare module "amqp-suite" {
2
2
  export class AmqpClient {
3
3
  constructor(amqpUrl: string, exchange: string);
4
4
  connect(retries?: number, delay?: number): Promise<void>;
@@ -23,6 +23,8 @@ class AmqpClient {
23
23
  this.channel = null;
24
24
  /** @type {boolean} State flag to prevent multiple simultaneous connection attempts. */
25
25
  this.isConnecting = false;
26
+ /** @type {boolean} State flag to indicate if the client is closing. */
27
+ this.isClosing = false;
26
28
  }
27
29
 
28
30
  /**
@@ -51,11 +53,14 @@ class AmqpClient {
51
53
  });
52
54
 
53
55
  this.connection.on("close", () => {
54
- this.isConnecting = false;
55
56
  this.connection = null;
56
57
  this.channel = null;
57
- console.warn(`AMQP connection lost. Retrying in ${delay}ms...`);
58
- setTimeout(() => this.connect(), delay);
58
+
59
+ if (!this.isClosing) {
60
+ this.isConnecting = false;
61
+ console.warn(`AMQP connection lost. Retrying in ${delay}ms...`);
62
+ setTimeout(() => this.connect(), delay);
63
+ }
59
64
  });
60
65
 
61
66
  console.log("AMQP: Connection established successfully");
@@ -171,10 +176,12 @@ class AmqpClient {
171
176
  */
172
177
  async close() {
173
178
  try {
179
+ this.isClosing = true;
174
180
  await this.channel?.close();
175
181
  await this.connection?.close();
176
182
  console.log("AMQP: Connection closed cleanly.");
177
183
  } catch (err) {
184
+ this.isClosing = false;
178
185
  console.error("AMQP: Error during shutdown:", err);
179
186
  }
180
187
  }
package/src/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { AmqpClient } from "./amqp-client";
1
+ import { AmqpClient } from "./amqp-client.js";
2
2
 
3
3
  export { AmqpClient };
@@ -1,33 +0,0 @@
1
- # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
- # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
-
4
- name: Node.js Package
5
-
6
- on:
7
- release:
8
- types: [created]
9
-
10
- jobs:
11
- build:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
- - uses: actions/setup-node@v4
16
- with:
17
- node-version: 20
18
- - run: npm ci
19
- - run: npm run test:run
20
-
21
- publish-npm:
22
- needs: build
23
- runs-on: ubuntu-latest
24
- steps:
25
- - uses: actions/checkout@v4
26
- - uses: actions/setup-node@v4
27
- with:
28
- node-version: 20
29
- registry-url: https://registry.npmjs.org/
30
- - run: npm ci
31
- - run: npm publish
32
- env:
33
- NODE_AUTH_TOKEN: ${{secrets.npm_token}}
Binary file
@@ -1,172 +0,0 @@
1
- /**
2
- * @fileoverview Unit tests for the AMQP class using Vitest and amqplib mocks.
3
- */
4
-
5
- import { describe, it, expect, vi, beforeEach } from "vitest";
6
- import amqp from "amqplib";
7
- import { AmqpClient } from "amqp-suite";
8
-
9
- vi.mock("amqplib");
10
-
11
- /**
12
- * Test suite for AMQP integration and message handling.
13
- */
14
- describe("AMQP Class", () => {
15
- /** @type {AmqpClient} */
16
- let amqpInstance;
17
-
18
- /** @type {string} */
19
- const mockUrl = "amqp://localhost";
20
-
21
- /** @type {string} */
22
- const mockExchange = "test_exchange";
23
-
24
- /**
25
- * Mocked AMQP Channel object with Vitest spy functions.
26
- * @type {Object}
27
- */
28
- const mockChannel = {
29
- assertExchange: vi.fn().mockResolvedValue(undefined),
30
- assertQueue: vi.fn().mockResolvedValue({}),
31
- bindQueue: vi.fn().mockResolvedValue(undefined),
32
- prefetch: vi.fn().mockResolvedValue(undefined),
33
- publish: vi.fn().mockReturnValue(true),
34
- consume: vi.fn().mockResolvedValue({ consumerTag: "abc" }),
35
- ack: vi.fn(),
36
- nack: vi.fn(),
37
- close: vi.fn().mockResolvedValue(undefined),
38
- };
39
-
40
- /**
41
- * Mocked AMQP Connection object.
42
- * @type {Object}
43
- */
44
- const mockConnection = {
45
- createChannel: vi.fn().mockResolvedValue(mockChannel),
46
- on: vi.fn(),
47
- close: vi.fn().mockResolvedValue(undefined),
48
- };
49
-
50
- /**
51
- * Setup hook to reset mocks and re-initialize the AMQP instance before each test.
52
- */
53
- beforeEach(() => {
54
- vi.clearAllMocks();
55
- amqpInstance = new AmqpClient(mockUrl, mockExchange);
56
- amqp.connect.mockResolvedValue(mockConnection);
57
- });
58
-
59
- /**
60
- * Test case: Constructor property assignment.
61
- */
62
- it("should correctly initialize values in the constructor", () => {
63
- expect(amqpInstance.amqpUrl).toBe(mockUrl);
64
- expect(amqpInstance.exchange).toBe(mockExchange);
65
- });
66
-
67
- /**
68
- * Test case: Connection workflow and exchange assertion.
69
- */
70
- it("should connect and create a channel successfully", async () => {
71
- await amqpInstance.connect();
72
-
73
- expect(amqp.connect).toHaveBeenCalledWith(mockUrl);
74
- expect(mockConnection.createChannel).toHaveBeenCalled();
75
- expect(mockChannel.assertExchange).toHaveBeenCalledWith(
76
- mockExchange,
77
- "topic",
78
- { durable: true }
79
- );
80
- expect(amqpInstance.connection).not.toBeNull();
81
- });
82
-
83
- /**
84
- * Test case: Message publishing logic and buffer conversion.
85
- */
86
- it("should publish a message correctly", async () => {
87
- await amqpInstance.connect();
88
- const routingKey = "user.created";
89
- const message = { id: 1, name: "Test" };
90
-
91
- await amqpInstance.publish(routingKey, message);
92
-
93
- expect(mockChannel.publish).toHaveBeenCalledWith(
94
- mockExchange,
95
- routingKey,
96
- expect.any(Buffer),
97
- expect.objectContaining({ persistent: true })
98
- );
99
- });
100
-
101
- /**
102
- * Test case: Consumer setup and successful message processing.
103
- */
104
- it("should setup consumer and acknowledge messages on success", async () => {
105
- await amqpInstance.connect();
106
- const queueName = "test_queue";
107
- const bindingKey = "test.key";
108
- const mockPayload = { data: "hello world" };
109
- const mockOnMessage = vi.fn().mockResolvedValue(undefined);
110
-
111
- await amqpInstance.consume(
112
- queueName,
113
- mockOnMessage,
114
- { prefetch: 5 },
115
- bindingKey
116
- );
117
-
118
- expect(mockChannel.assertQueue).toHaveBeenCalledWith(
119
- queueName,
120
- expect.objectContaining({ durable: true })
121
- );
122
- expect(mockChannel.bindQueue).toHaveBeenCalledWith(
123
- queueName,
124
- mockExchange,
125
- bindingKey
126
- );
127
- expect(mockChannel.prefetch).toHaveBeenCalledWith(5);
128
-
129
- const consumerCallback = mockChannel.consume.mock.calls[0][1];
130
- const fakeMsg = {
131
- content: Buffer.from(JSON.stringify(mockPayload)),
132
- };
133
-
134
- await consumerCallback(fakeMsg);
135
-
136
- expect(mockOnMessage).toHaveBeenCalledWith(mockPayload, fakeMsg);
137
- expect(mockChannel.ack).toHaveBeenCalledWith(fakeMsg);
138
- });
139
-
140
- /**
141
- * Test case: Consumer error handling (nack).
142
- */
143
- it("should nack messages if the processing fails", async () => {
144
- await amqpInstance.connect();
145
- const mockOnMessage = vi
146
- .fn()
147
- .mockRejectedValue(new Error("Processing failed"));
148
-
149
- await amqpInstance.consume("error_queue", mockOnMessage);
150
-
151
- const consumerCallback = mockChannel.consume.mock.calls[0][1];
152
- const fakeMsg = {
153
- content: Buffer.from(JSON.stringify({ some: "data" })),
154
- };
155
-
156
- await consumerCallback(fakeMsg);
157
-
158
- expect(mockChannel.nack).toHaveBeenCalledWith(fakeMsg, false, false);
159
- expect(mockChannel.ack).not.toHaveBeenCalled();
160
- });
161
-
162
- /**
163
- * Test case: Graceful shutdown of channel and connection.
164
- */
165
- it("should close the connection cleanly", async () => {
166
- await amqpInstance.connect();
167
- await amqpInstance.close();
168
-
169
- expect(mockChannel.close).toHaveBeenCalled();
170
- expect(mockConnection.close).toHaveBeenCalled();
171
- });
172
- });
package/vitest.config.js DELETED
@@ -1,8 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: true, // Permite usar 'describe', 'it', 'expect' sin importarlos
6
- environment: "node",
7
- },
8
- });