poly-bus-rabbitmq 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/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # poly-bus-rabbitmq
2
+
3
+ The RabbitMQ transport for PolyBus.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 18+
8
+ - npm 9+
9
+ - RabbitMQ server (for runtime/integration scenarios)
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install poly-bus-rabbitmq poly-bus@0.3.11
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### Setting Up Development Environment
20
+
21
+ ```bash
22
+ # Navigate to the typescript directory
23
+ cd src/typescript
24
+
25
+ # Install dependencies
26
+ npm install
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```ts
32
+ import { PolyBusBuilder } from "poly-bus";
33
+ import { RabbitMqConfig } from "poly-bus-rabbitmq";
34
+
35
+ const builder = new PolyBusBuilder("my-service");
36
+
37
+ const rabbitMq = new RabbitMqConfig();
38
+ rabbitMq.connectionString = "amqp://guest:guest@localhost:5672";
39
+ rabbitMq.queueName = "my-service";
40
+
41
+ builder.withTransport(async (b, bus) => rabbitMq.create(b, bus));
42
+ ```
43
+
44
+ ## Behavior
45
+
46
+ The transport mirrors the .NET implementation in this repository:
47
+
48
+ - Command, event, and direct exchanges.
49
+ - Dead-letter queue setup.
50
+ - Delayed delivery queues using binary routing segments and TTL dead-letter chaining.
51
+ - Routing key format:
52
+ - Publish: `MessageType.Endpoint.Name.Major.Minor.Patch`
53
+ - Subscribe: `MessageType.Endpoint.Name.Major.#`
54
+
55
+ ## Exports
56
+
57
+ - `RabbitMqConfig`
58
+ - `RabbitMqTransport`
59
+
60
+ ## Development
61
+
62
+ ```bash
63
+ cd src/typescript
64
+ npm install
65
+ npm run build
66
+ npm test
67
+ ```
68
+
69
+ ## Building The Project
70
+
71
+ ```bash
72
+ cd src/typescript
73
+ npm run build
74
+ ```
75
+
76
+ ## Running Tests
77
+
78
+ ```bash
79
+ # Run all tests once
80
+ npm test
81
+
82
+ # Run a specific test file
83
+ npx jest src/__tests__/transport/rabbitmq/rabbitmq-transport.test.ts
84
+
85
+ # Run tests matching a test name pattern
86
+ npx jest -t "formats publish routing keys"
87
+
88
+ # Run RabbitMQ integration tests (creates and deletes its own RabbitMQ virtual host)
89
+ # Optional: set RABBITMQ_MANAGEMENT_URL if management API is not on http://localhost:15672
90
+ RABBITMQ_CONNECTION_STRING=amqp://guest:guest@localhost:5672 \
91
+ npx jest src/__tests__/transport/rabbitmq/rabbitmq-transport.integration.test.ts --runInBand
92
+ ```
93
+
94
+ ## Development Workflow
95
+
96
+ ### Code Quality
97
+
98
+ Current package scripts include:
99
+
100
+ - `npm run build` for TypeScript compilation
101
+ - `npm test` for Jest test execution
102
+
103
+ If you want stricter checks, common additions are:
104
+
105
+ - `npm run typecheck` using `tsc --noEmit`
106
+ - linting with ESLint
107
+ - formatting with Prettier
108
+
109
+ ## Configuration
110
+
111
+ ### RabbitMqConfig
112
+
113
+ Common options:
114
+
115
+ - `connectionString` (required): AMQP connection URI.
116
+ - `queueName`: endpoint queue name.
117
+ - `deadLetterQueueName`: dead-letter queue endpoint.
118
+ - `commandExchangeName`, `eventExchangeName`, `directExchangeName`: exchange naming overrides.
119
+ - `delayDelivery`: delayed message exchange prefix.
120
+ - `networkRecoveryIntervalMs`: reconnect interval.
121
+
122
+ ### Routing Behavior
123
+
124
+ Routing keys mirror the .NET implementation:
125
+
126
+ - Publish format: `MessageType.Endpoint.Name.Major.Minor.Patch`
127
+ - Subscribe format: `MessageType.Endpoint.Name.Major.#`
128
+
129
+ ## Dependencies
130
+
131
+ ### Runtime Dependencies
132
+
133
+ - `amqplib`
134
+ - `poly-bus@0.3.11`
135
+
136
+ ### Development Dependencies
137
+
138
+ - `typescript`
139
+ - `jest`
140
+ - `@types/node`
141
+ - `@types/amqplib`
142
+ - `rimraf`
143
+
144
+ ## Common Commands
145
+
146
+ ```bash
147
+ cd src/typescript
148
+
149
+ # Install, build, and test
150
+ npm install
151
+ npm run build
152
+ npm test
153
+
154
+ # Clean output
155
+ npm run clean
156
+
157
+ # Inspect generated package files
158
+ npm pack --dry-run
159
+ ```
160
+
161
+ ## Troubleshooting
162
+
163
+ ### Environment Issues
164
+
165
+ 1. Verify Node.js version:
166
+
167
+ ```bash
168
+ node --version
169
+ npm --version
170
+ ```
171
+
172
+ 2. Ensure RabbitMQ is reachable via your connection string.
173
+
174
+ ### Build Issues
175
+
176
+ 1. Clean and rebuild:
177
+
178
+ ```bash
179
+ npm run clean
180
+ npm run build
181
+ ```
182
+
183
+ 2. If type declaration or module resolution errors appear, delete `node_modules` and reinstall:
184
+
185
+ ```bash
186
+ rm -rf node_modules package-lock.json
187
+ npm install
188
+ ```
189
+
190
+ ### Test Issues
191
+
192
+ 1. Run one file directly to isolate failures:
193
+
194
+ ```bash
195
+ npx jest src/__tests__/transport/rabbitmq/rabbitmq-transport.test.ts
196
+ ```
197
+
198
+ 2. Use test name filtering to narrow down behavioral regressions.
199
+
200
+ ## Contributing
201
+
202
+ 1. Run `npm run build` and `npm test` before committing.
203
+ 2. Add tests for new transport behavior.
204
+ 3. Keep behavior aligned with the .NET and Python transport implementations.
205
+ 4. Update README examples and configuration docs when APIs change.
206
+
207
+ ## License
208
+
209
+ See the repository LICENSE file for licensing details.
@@ -0,0 +1,60 @@
1
+ export declare enum MessageType {
2
+ Event = 0,
3
+ Command = 1
4
+ }
5
+ export declare const Headers: {
6
+ readonly MessageType: "MessageType";
7
+ };
8
+ export declare class MessageInfo {
9
+ readonly type: MessageType;
10
+ readonly endpoint: string;
11
+ readonly name: string;
12
+ readonly major: number;
13
+ readonly minor: number;
14
+ readonly patch: number;
15
+ constructor(type: MessageType, endpoint: string, name: string, major: number, minor: number, patch: number);
16
+ static getAttributeFromHeader(_header: string): MessageInfo | null;
17
+ }
18
+ export declare class IncomingMessage {
19
+ headers: Map<string, string>;
20
+ constructor(_bus: IPolyBus, _body: string, _messageInfo: MessageInfo);
21
+ }
22
+ export interface OutgoingMessage {
23
+ messageInfo: MessageInfo;
24
+ headers: Map<string, string>;
25
+ endpoint?: string;
26
+ body: string;
27
+ deliverAt?: Date;
28
+ }
29
+ export interface Transaction {
30
+ outgoingMessages: OutgoingMessage[];
31
+ }
32
+ export interface IncomingTransaction {
33
+ commit(): Promise<void>;
34
+ }
35
+ export interface OutgoingTransaction extends Transaction {
36
+ }
37
+ export interface IPolyBus {
38
+ name: string;
39
+ properties: Map<string, object>;
40
+ transport: unknown;
41
+ incomingPipeline: unknown[];
42
+ outgoingPipeline: unknown[];
43
+ messages: {
44
+ getHeaderByMessageInfo(messageInfo: MessageInfo): string;
45
+ getTypeByMessageInfo(messageInfo: MessageInfo): new () => unknown;
46
+ getMessageInfo(type: new () => unknown): MessageInfo;
47
+ add(type: new () => unknown): MessageInfo;
48
+ };
49
+ createIncomingTransaction(message: IncomingMessage): Promise<IncomingTransaction>;
50
+ createOutgoingTransaction(): Promise<OutgoingTransaction>;
51
+ send(transaction: Transaction): Promise<void>;
52
+ start(): Promise<void>;
53
+ stop(): Promise<void>;
54
+ }
55
+ export interface ITransport {
56
+ start(): Promise<void>;
57
+ stop(): Promise<void>;
58
+ subscribe(messageInfo: MessageInfo): Promise<void>;
59
+ handle(transaction: Transaction): Promise<void>;
60
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IncomingMessage = exports.MessageInfo = exports.Headers = exports.MessageType = void 0;
4
+ var MessageType;
5
+ (function (MessageType) {
6
+ MessageType[MessageType["Event"] = 0] = "Event";
7
+ MessageType[MessageType["Command"] = 1] = "Command";
8
+ })(MessageType || (exports.MessageType = MessageType = {}));
9
+ exports.Headers = {
10
+ MessageType: "MessageType"
11
+ };
12
+ class MessageInfo {
13
+ constructor(type, endpoint, name, major, minor, patch) {
14
+ this.type = type;
15
+ this.endpoint = endpoint;
16
+ this.name = name;
17
+ this.major = major;
18
+ this.minor = minor;
19
+ this.patch = patch;
20
+ }
21
+ static getAttributeFromHeader(_header) {
22
+ return null;
23
+ }
24
+ }
25
+ exports.MessageInfo = MessageInfo;
26
+ class IncomingMessage {
27
+ constructor(_bus, _body, _messageInfo) {
28
+ this.headers = new Map();
29
+ }
30
+ }
31
+ exports.IncomingMessage = IncomingMessage;
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_crypto_1 = require("node:crypto");
7
+ const globals_1 = require("@jest/globals");
8
+ const amqplib_1 = __importDefault(require("amqplib"));
9
+ const poly_bus_1 = require("poly-bus");
10
+ const rabbitmq_config_1 = require("../../../transport/rabbitmq/rabbitmq-config");
11
+ const rabbitmq_transport_1 = require("../../../transport/rabbitmq/rabbitmq-transport");
12
+ const baseConnectionString = process.env.RABBITMQ_CONNECTION_STRING ?? 'amqp://guest:guest@localhost:5672';
13
+ function encodePathSegment(value) {
14
+ return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
15
+ }
16
+ function getManagementBaseUrl(connectionString) {
17
+ if (process.env.RABBITMQ_MANAGEMENT_URL) {
18
+ return process.env.RABBITMQ_MANAGEMENT_URL;
19
+ }
20
+ const connection = new URL(connectionString);
21
+ const protocol = connection.protocol === 'amqps:' ? 'https:' : 'http:';
22
+ const port = connection.port === '5671' ? '15671' : '15672';
23
+ return `${protocol}//${connection.hostname}:${port}`;
24
+ }
25
+ async function callManagementApi(managementBaseUrl, username, password, path, method, body) {
26
+ const authToken = Buffer.from(`${username}:${password}`, 'utf8').toString('base64');
27
+ const response = await fetch(`${managementBaseUrl}${path}`, {
28
+ method,
29
+ headers: {
30
+ Authorization: `Basic ${authToken}`,
31
+ 'Content-Type': 'application/json'
32
+ },
33
+ body: body ? JSON.stringify(body) : undefined
34
+ });
35
+ if (!response.ok) {
36
+ const responseText = await response.text();
37
+ throw new Error(`RabbitMQ management API ${method} ${path} failed with ${response.status}: ${responseText}`);
38
+ }
39
+ }
40
+ async function createTestVirtualHost(connectionString, vhost) {
41
+ const connection = new URL(connectionString);
42
+ const username = decodeURIComponent(connection.username || 'guest');
43
+ const password = decodeURIComponent(connection.password || 'guest');
44
+ const managementBaseUrl = getManagementBaseUrl(connectionString);
45
+ const encodedVhost = encodePathSegment(vhost);
46
+ const encodedUser = encodePathSegment(username);
47
+ await callManagementApi(managementBaseUrl, username, password, `/api/vhosts/${encodedVhost}`, 'PUT');
48
+ await callManagementApi(managementBaseUrl, username, password, `/api/permissions/${encodedVhost}/${encodedUser}`, 'PUT', {
49
+ configure: '.*',
50
+ write: '.*',
51
+ read: '.*'
52
+ });
53
+ connection.pathname = `/${vhost}`;
54
+ connection.search = '';
55
+ connection.hash = '';
56
+ return connection.toString();
57
+ }
58
+ async function deleteTestVirtualHost(connectionString, vhost) {
59
+ const connection = new URL(connectionString);
60
+ const username = decodeURIComponent(connection.username || 'guest');
61
+ const password = decodeURIComponent(connection.password || 'guest');
62
+ const managementBaseUrl = getManagementBaseUrl(connectionString);
63
+ const encodedVhost = encodePathSegment(vhost);
64
+ await callManagementApi(managementBaseUrl, username, password, `/api/vhosts/${encodedVhost}`, 'DELETE');
65
+ }
66
+ function createHeader(messageInfo) {
67
+ return `${messageInfo.type}.${messageInfo.endpoint}.${messageInfo.name}.${messageInfo.major}.${messageInfo.minor}.${messageInfo.patch}`;
68
+ }
69
+ function createBusStub(name, onIncomingMessage) {
70
+ return {
71
+ name,
72
+ properties: new Map(),
73
+ transport: {},
74
+ incomingPipeline: [],
75
+ outgoingPipeline: [],
76
+ messages: {
77
+ getHeaderByMessageInfo: (messageInfo) => createHeader(messageInfo),
78
+ getTypeByMessageInfo: () => class TestMessage {
79
+ },
80
+ getMessageInfo: () => new poly_bus_1.MessageInfo(poly_bus_1.MessageType.Command, name, 'created', 1, 0, 0),
81
+ add: () => new poly_bus_1.MessageInfo(poly_bus_1.MessageType.Command, name, 'created', 1, 0, 0)
82
+ },
83
+ createIncomingTransaction: async (message) => {
84
+ onIncomingMessage(message);
85
+ return {
86
+ commit: async () => undefined
87
+ };
88
+ },
89
+ createOutgoingTransaction: async () => ({
90
+ outgoingMessages: []
91
+ }),
92
+ send: async (_transaction) => undefined,
93
+ start: async () => undefined,
94
+ stop: async () => undefined
95
+ };
96
+ }
97
+ function withTimeout(promise, timeoutMs = 20000) {
98
+ return new Promise((resolve, reject) => {
99
+ const timeout = setTimeout(() => {
100
+ reject(new Error(`Timed out after ${timeoutMs}ms waiting for RabbitMQ message`));
101
+ }, timeoutMs);
102
+ promise
103
+ .then((value) => {
104
+ clearTimeout(timeout);
105
+ resolve(value);
106
+ })
107
+ .catch((error) => {
108
+ clearTimeout(timeout);
109
+ reject(error);
110
+ });
111
+ });
112
+ }
113
+ (0, globals_1.describe)('RabbitMqTransport integration', () => {
114
+ let connectionString;
115
+ let testVirtualHostName;
116
+ let transport;
117
+ let queueName;
118
+ let receivedResolve;
119
+ let receivedReject;
120
+ let receivedMessagePromise;
121
+ let parserSpy;
122
+ let publisherConnection;
123
+ let publisherChannel;
124
+ (0, globals_1.beforeAll)(async () => {
125
+ testVirtualHostName = `ts-rabbitmq-integration-${(0, node_crypto_1.randomUUID)()}`;
126
+ connectionString = await createTestVirtualHost(baseConnectionString, testVirtualHostName);
127
+ });
128
+ (0, globals_1.afterAll)(async () => {
129
+ await deleteTestVirtualHost(baseConnectionString, testVirtualHostName);
130
+ });
131
+ (0, globals_1.beforeEach)(() => {
132
+ queueName = `ts-rabbitmq-integration-${(0, node_crypto_1.randomUUID)()}`;
133
+ receivedMessagePromise = new Promise((resolve, reject) => {
134
+ receivedResolve = resolve;
135
+ receivedReject = reject;
136
+ });
137
+ const bus = createBusStub(queueName, (message) => {
138
+ receivedResolve?.(message);
139
+ });
140
+ const config = new rabbitmq_config_1.RabbitMqConfig();
141
+ config.connectionString = connectionString;
142
+ config.queueName = queueName;
143
+ config.deadLetterQueueName = `${queueName}.dead.letters`;
144
+ config.log = {
145
+ info: globals_1.jest.fn(),
146
+ error: (message, ...meta) => {
147
+ receivedReject?.(new Error([message, ...meta.map((entry) => String(entry))].join(' ')));
148
+ }
149
+ };
150
+ parserSpy = globals_1.jest.spyOn(poly_bus_1.MessageInfo, 'getAttributeFromHeader').mockImplementation((header) => {
151
+ const parts = header.split('.');
152
+ if (parts.length !== 6) {
153
+ return null;
154
+ }
155
+ const [typeText, endpoint, name, major, minor, patch] = parts;
156
+ const parsedType = Number.parseInt(typeText, 10);
157
+ if (Number.isNaN(parsedType)) {
158
+ return null;
159
+ }
160
+ return new poly_bus_1.MessageInfo(parsedType, endpoint, name, Number.parseInt(major, 10), Number.parseInt(minor, 10), Number.parseInt(patch, 10));
161
+ });
162
+ transport = new rabbitmq_transport_1.RabbitMqTransport(config, bus);
163
+ publisherConnection = null;
164
+ publisherChannel = null;
165
+ });
166
+ (0, globals_1.afterEach)(async () => {
167
+ parserSpy.mockRestore();
168
+ if (publisherChannel) {
169
+ try {
170
+ await publisherChannel.close();
171
+ }
172
+ catch {
173
+ // Ignore shutdown failures in cleanup.
174
+ }
175
+ publisherChannel = null;
176
+ }
177
+ if (publisherConnection) {
178
+ try {
179
+ await publisherConnection.close();
180
+ }
181
+ catch {
182
+ // Ignore shutdown failures in cleanup.
183
+ }
184
+ publisherConnection = null;
185
+ }
186
+ await transport.stop();
187
+ });
188
+ (0, globals_1.it)('creates a real RabbitMQ channel and connection on start', async () => {
189
+ await transport.start();
190
+ (0, globals_1.expect)(transport.connection).not.toBeNull();
191
+ (0, globals_1.expect)(transport.channel).not.toBeNull();
192
+ });
193
+ (0, globals_1.it)('publishes and receives command messages through RabbitMQ', async () => {
194
+ await transport.start();
195
+ const messageInfo = new poly_bus_1.MessageInfo(poly_bus_1.MessageType.Command, queueName, 'created', 1, 0, 0);
196
+ const transaction = {
197
+ outgoingMessages: [
198
+ {
199
+ messageInfo,
200
+ headers: new Map(),
201
+ endpoint: undefined,
202
+ body: JSON.stringify({ id: 'integration-command' }),
203
+ deliverAt: undefined
204
+ }
205
+ ]
206
+ };
207
+ await transport.handle(transaction);
208
+ const incoming = await withTimeout(receivedMessagePromise);
209
+ (0, globals_1.expect)(incoming).toBeInstanceOf(poly_bus_1.IncomingMessage);
210
+ (0, globals_1.expect)(incoming.headers.get(poly_bus_1.Headers.MessageType)).toBe(createHeader(messageInfo));
211
+ });
212
+ (0, globals_1.it)('receives subscribed events published by a RabbitMQ producer', async () => {
213
+ await transport.start();
214
+ const eventInfo = new poly_bus_1.MessageInfo(poly_bus_1.MessageType.Event, 'billing', 'paid', 1, 0, 0);
215
+ await transport.subscribe(eventInfo);
216
+ publisherConnection = await amqplib_1.default.connect(connectionString);
217
+ publisherChannel = await publisherConnection.createChannel();
218
+ await publisherChannel.assertExchange(transport.config.eventExchangeName, 'topic', { durable: true });
219
+ const routingKey = transport.getRoutingKey(false, eventInfo);
220
+ const options = {
221
+ headers: {
222
+ [poly_bus_1.Headers.MessageType]: createHeader(eventInfo)
223
+ }
224
+ };
225
+ publisherChannel.publish(transport.config.eventExchangeName, routingKey, Buffer.from(JSON.stringify({ id: 'integration-event' }), 'utf8'), options);
226
+ const incoming = await withTimeout(receivedMessagePromise);
227
+ (0, globals_1.expect)(incoming).toBeInstanceOf(poly_bus_1.IncomingMessage);
228
+ (0, globals_1.expect)(incoming.headers.get(poly_bus_1.Headers.MessageType)).toBe(createHeader(eventInfo));
229
+ });
230
+ });