@onlineapps/conn-infra-mq 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ONLINE APPS s.r.o.; info@onlineapps.cz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the β€œSoftware”), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED β€œAS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # @onlineapps/connector-mq-client
2
+
3
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/onlineapps/connector-mq-client/nodejs.yml?branch=main)](https://github.com/onlineapps/connector-mq-client/actions)
4
+ [![Coverage Status](https://codecov.io/gh/onlineapps/connector-mq-client/branch/main/graph/badge.svg)](https://codecov.io/gh/onlineapps/connector-mq-client)
5
+ [![npm version](https://img.shields.io/npm/v/@onlineapps/connector-mq-client)](https://www.npmjs.com/package/@onlineapps/connector-mq-client)
6
+
7
+ > Message queue connector with **layered architecture** for workflow orchestration, RPC, fork-join, and retry patterns. Built on top of RabbitMQ with clean separation of concerns.
8
+
9
+ ---
10
+
11
+ ## πŸš€ Features
12
+
13
+ - **Layered Architecture**: Clean separation into specialized layers (WorkflowRouter, QueueManager, ForkJoinHandler, RPCHandler, RetryHandler)
14
+ - **Workflow Orchestration**: Decentralized workflow routing without central orchestrator
15
+ - **Fork-Join Pattern**: Parallel processing with result aggregation and built-in join strategies
16
+ - **RPC Support**: Request-response communication with correlation IDs and timeouts
17
+ - **Automatic Retry**: Exponential backoff, dead letter queue management, configurable retry policies
18
+ - **Queue Management**: TTL, DLQ, auto-delete, temporary queues, exchange bindings
19
+ - **Promise-based API**: All operations return promises for clean async/await usage
20
+ - **Built-in Serialization**: JSON with custom error handling
21
+ - **Config Validation**: Strict schema validation via Ajv
22
+ - **Extensible Transport**: Clear separation between core logic and transport layer
23
+
24
+ ---
25
+
26
+ ## πŸ“¦ Installation
27
+
28
+ ```bash
29
+ npm install @onlineapps/connector-mq-client
30
+ # or
31
+ yarn add @onlineapps/connector-mq-client
32
+ ````
33
+
34
+ > Requires Node.js β‰₯12. For RabbitMQ usage, ensure an accessible AMQP server.
35
+
36
+ ---
37
+
38
+ ## πŸ—οΈ Architecture
39
+
40
+ ```
41
+ ConnectorMQClient (main orchestrator)
42
+ β”œβ”€β”€ BaseClient (core AMQP operations)
43
+ β”œβ”€β”€ WorkflowRouter (workflow orchestration)
44
+ β”œβ”€β”€ QueueManager (queue lifecycle management)
45
+ β”œβ”€β”€ ForkJoinHandler (parallel processing)
46
+ β”œβ”€β”€ RPCHandler (request-response patterns)
47
+ └── RetryHandler (error recovery & DLQ)
48
+ ```
49
+
50
+ ## πŸ”§ Quick Start
51
+
52
+ ```js
53
+ 'use strict';
54
+
55
+ const ConnectorMQClient = require('@onlineapps/connector-mq-client');
56
+
57
+ (async () => {
58
+ // 1. Create client with configuration
59
+ const client = new ConnectorMQClient({
60
+ host: 'amqp://localhost:5672',
61
+ serviceName: 'my-service',
62
+ queue: 'default-queue',
63
+ durable: true,
64
+ prefetch: 5, // Default prefetch count for consumers
65
+ noAck: false, // Default auto-acknowledge = false
66
+ retryPolicy: { // Optional reconnection policy (not enforced in v1.0.0)
67
+ retries: 5,
68
+ initialDelayMs: 1000,
69
+ maxDelayMs: 30000,
70
+ factor: 2
71
+ }
72
+ });
73
+
74
+ // 2. Register a global error handler
75
+ client.onError(err => {
76
+ console.error('[AgentMQClient] Error:', err);
77
+ });
78
+
79
+ // 3. Connect to RabbitMQ
80
+ try {
81
+ await client.connect();
82
+ console.log('Connected to broker');
83
+ } catch (err) {
84
+ console.error('Connection failed:', err);
85
+ process.exit(1);
86
+ }
87
+
88
+ // 4. Publish a sample message
89
+ const samplePayload = { taskId: 'abc123', action: 'processData', timestamp: Date.now() };
90
+ try {
91
+ await client.publish('job_queue', samplePayload, {
92
+ persistent: true,
93
+ headers: { origin: 'quickStart' }
94
+ });
95
+ console.log('Message published:', samplePayload);
96
+ } catch (err) {
97
+ console.error('Publish error:', err);
98
+ }
99
+
100
+ // 5. Consume messages
101
+ try {
102
+ await client.consume(
103
+ 'job_queue',
104
+ async (msg) => {
105
+ const data = JSON.parse(msg.content.toString('utf8'));
106
+ console.log('Received:', data);
107
+ // Process message...
108
+ await client.ack(msg);
109
+ },
110
+ { prefetch: 5, noAck: false }
111
+ );
112
+ console.log('Consuming from "job_queue"...');
113
+ } catch (err) {
114
+ console.error('Consume error:', err);
115
+ }
116
+
117
+ // 6. Graceful shutdown on SIGINT
118
+ process.on('SIGINT', async () => {
119
+ console.log('Shutting down...');
120
+ try {
121
+ await client.disconnect();
122
+ console.log('Disconnected, exiting.');
123
+ process.exit(0);
124
+ } catch (discErr) {
125
+ console.error('Error during disconnect:', discErr);
126
+ process.exit(1);
127
+ }
128
+ });
129
+ })();
130
+ ```
131
+
132
+ ---
133
+
134
+ ## πŸ“„ Configuration
135
+
136
+ Configuration can be provided to the `AgentMQClient` constructor or as overrides to `connect()`. Below is a summary of supported fields (see `docs/api.md` for full details):
137
+
138
+ | Field | Type | Description | Default |
139
+ | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
140
+ | `type` | `string` | Transport type: `'rabbitmq'` | `'rabbitmq'` |
141
+ | `host` | `string` | Connection URI or hostname. For RabbitMQ: e.g. `'amqp://user:pass@localhost:5672'`. | *Required* |
142
+ | `queue` | `string` | Default queue name for publish/consume if not overridden per call. | `''` |
143
+ | `exchange` | `string` | Default exchange name. Empty string uses the default direct exchange. | `''` |
144
+ | `durable` | `boolean` | Declare queues/exchanges as durable. | `true` |
145
+ | `prefetch` | `integer` | Default prefetch count for consumers. | `1` |
146
+ | `noAck` | `boolean` | Default auto-acknowledge setting for consumers. If `true`, messages will be auto-acked. | `false` |
147
+ | `logger` | `object` | Custom logger with methods: `info()`, `warn()`, `error()`, `debug()`. If omitted, `console` is used. | `null` |
148
+ | `retryPolicy` | `object` | Reconnection policy with properties:<br>β€’ `retries` (number)<br>β€’ `initialDelayMs` (ms)<br>β€’ `maxDelayMs` (ms)<br>β€’ `factor` (multiplier). Not enforced in v1.0.0. | `{ retries: 5, initialDelayMs: 1000, maxDelayMs: 30000, factor: 2 }` |
149
+
150
+ ---
151
+
152
+ ## πŸ› οΈ API Reference
153
+
154
+ For full class and method documentation, including parameter descriptions, return values, and error details, see [docs/api.md](https://github.com/onlineapps/agent-mq-client/blob/main/docs/api.md).
155
+
156
+ ---
157
+
158
+ ## βœ… Testing
159
+
160
+ ```bash
161
+ npm test # All tests
162
+ npm run test:unit # Unit tests only
163
+ npm run test:component # Component tests
164
+ npm run test:integration # Integration tests
165
+ ```
166
+
167
+ ### Test Coverage Status
168
+ - **Overall Coverage**: 24.52% (improving after refactoring)
169
+ - **Passing Tests**: 75/104 (72%)
170
+ - **Test Suites**: 10/14 passing
171
+ - **Well Tested**: Config, Transports, Error handling (100%)
172
+ - **Needs Testing**: New layers (1-5%)
173
+
174
+ See [Test Report](docs/TEST_REPORT.md) for detailed coverage analysis.
175
+
176
+ ---
177
+
178
+ ## 🎨 Coding Standards
179
+
180
+ * **Linting**: ESLint (`eslint:recommended` + Prettier).
181
+ * **Formatting**: Prettier β€” check with `npm run prettier:check`, fix with `npm run prettier:fix`.
182
+ * **Testing**: Jest, aiming for β‰₯90% coverage.
183
+
184
+ ---
185
+
186
+ ## 🎯 Refactoring Benefits
187
+
188
+ ### βœ… What Was Achieved
189
+ 1. **Clean Layered Architecture** - Separated into specialized layers
190
+ 2. **Removed Technical Debt** - Replaced MQWrapper with cleaner design
191
+ 3. **Improved Extensibility** - Easy to add new patterns
192
+ 4. **Better Developer Experience** - Cleaner API and documentation
193
+
194
+ ### πŸ“Š Quality Improvements
195
+ - **Separation of Concerns** - Each layer has single responsibility
196
+ - **Modular Design** - Use individual layers independently
197
+ - **Testability** - Each layer can be tested in isolation
198
+ - **Maintainability** - Easier to understand and modify
199
+ - **Backwards Compatibility** - MQWrapper alias maintained
200
+
201
+ ---
202
+
203
+ ## 🀝 Contributing
204
+
205
+ Contributions welcome! Please see [CONTRIBUTING.md](https://github.com/onlineapps/agent-mq-client/blob/main/CONTRIBUTING.md) for guidelines:
206
+
207
+ 1. Fork the repo.
208
+ 2. Create a feature branch: `git checkout -b feature/your-feature`.
209
+ 3. Run tests locally and ensure linting passes.
210
+ 4. Commit your changes and push to your branch.
211
+ 5. Open a Pull Request against `main`.
212
+
213
+ ---
214
+
215
+ ## πŸ“œ License
216
+
217
+ This project is licensed under the MIT License. See [LICENSE](https://github.com/onlineapps/agent-mq-client/blob/main/LICENSE) for details.
218
+
219
+ ## πŸ“š Documentation
220
+
221
+ - [Complete Connectors Documentation](../../../docs/modules/connector.md)
222
+ - [Testing Standards](../../../docs/modules/connector.md#testing-standards)
223
+
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@onlineapps/conn-infra-mq",
3
+ "version": "1.1.0",
4
+ "description": "A promise-based, broker-agnostic client for sending and receiving messages via RabbitMQ",
5
+ "main": "src/index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/onlineapps/connector-mq-client.git"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "README.md",
13
+ "LICENSE",
14
+ "CHANGELOG.md"
15
+ ],
16
+ "scripts": {
17
+ "test": "jest --coverage",
18
+ "test:unit": "jest --testPathPattern=unit --coverage",
19
+ "test:component": "jest --testPathPattern=component --coverage",
20
+ "test:integration": "jest --config=jest.integration.config.js",
21
+ "test:integration:required": "REQUIRE_SERVICES=true jest --config=jest.integration.config.js",
22
+ "test:watch": "jest --watch",
23
+ "test:coverage": "jest --coverage --coverageReporters=text-lcov html",
24
+ "lint": "eslint \"src/**/*.js\" \"test/**/*.js\"",
25
+ "prettier:check": "prettier --check \"src/**/*.js\" \"test/**/*.js\"",
26
+ "prettier:fix": "prettier --write \"src/**/*.js\" \"test/**/*.js\""
27
+ },
28
+ "keywords": [
29
+ "connector",
30
+ "mq",
31
+ "rabbitmq",
32
+ "kafka",
33
+ "message-queue",
34
+ "microservice",
35
+ "client",
36
+ "async"
37
+ ],
38
+ "author": "OnlineApps Team <dev@onlineapps.io>",
39
+ "license": "MIT",
40
+ "bugs": {
41
+ "url": "https://github.com/onlineapps/connector-mq-client/issues"
42
+ },
43
+ "homepage": "https://github.com/onlineapps/connector-mq-client#readme",
44
+ "dependencies": {
45
+ "ajv": "^8.11.0",
46
+ "amqplib": "^0.10.3",
47
+ "lodash.merge": "^4.6.2"
48
+ },
49
+ "devDependencies": {
50
+ "@semantic-release/changelog": "^6.0.3",
51
+ "@semantic-release/commit-analyzer": "^11.1.0",
52
+ "@semantic-release/git": "^10.0.1",
53
+ "@semantic-release/github": "^9.2.6",
54
+ "@semantic-release/npm": "^11.0.3",
55
+ "@semantic-release/release-notes-generator": "^12.1.0",
56
+ "dotenv": "^17.2.2",
57
+ "eslint": "^8.30.0",
58
+ "eslint-config-prettier": "^8.5.0",
59
+ "jest": "^29.5.0",
60
+ "prettier": "^2.8.0",
61
+ "sinon": "^15.0.0",
62
+ "uuid": "^13.0.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=12"
66
+ },
67
+ "eslintConfig": {
68
+ "env": {
69
+ "node": true,
70
+ "jest": true,
71
+ "es2021": true
72
+ },
73
+ "extends": [
74
+ "eslint:recommended",
75
+ "prettier"
76
+ ],
77
+ "parserOptions": {
78
+ "ecmaVersion": 12,
79
+ "sourceType": "script"
80
+ },
81
+ "rules": {
82
+ "no-console": "off",
83
+ "strict": [
84
+ "error",
85
+ "global"
86
+ ]
87
+ }
88
+ },
89
+ "prettier": {
90
+ "printWidth": 100,
91
+ "singleQuote": true,
92
+ "trailingComma": "es5",
93
+ "tabWidth": 2,
94
+ "endOfLine": "lf"
95
+ }
96
+ }
@@ -0,0 +1,219 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ConnectorMQClient: a promise-based, broker-agnostic client for RabbitMQ
5
+ * Uses transportFactory to select the appropriate transport implementation.
6
+ */
7
+
8
+ const Ajv = require('ajv');
9
+ const merge = require('lodash.merge');
10
+
11
+ const configSchema = require('./config/configSchema');
12
+ const defaultConfig = require('./config/defaultConfig');
13
+ const transportFactory = require('./transports/transportFactory');
14
+ const serializer = require('./utils/serializer');
15
+ const {
16
+ ConnectionError,
17
+ PublishError,
18
+ ConsumeError,
19
+ ValidationError,
20
+ SerializationError,
21
+ } = require('./utils/errorHandler');
22
+
23
+ class ConnectorMQClient {
24
+ /**
25
+ * @param {Object} config - User-supplied configuration.
26
+ * @throws {ValidationError} If required fields are missing or invalid.
27
+ */
28
+ constructor(config) {
29
+ const ajv = new Ajv({ allErrors: true, useDefaults: true });
30
+ const validate = ajv.compile(configSchema);
31
+
32
+ // Merge user config with defaults
33
+ this._config = merge({}, defaultConfig, config || {});
34
+
35
+ // Validate merged config
36
+ const valid = validate(this._config);
37
+ if (!valid) {
38
+ const details = validate.errors.map((err) => ({
39
+ path: err.instancePath,
40
+ message: err.message,
41
+ }));
42
+ throw new ValidationError('Invalid configuration', details);
43
+ }
44
+
45
+ this._transport = null;
46
+ this._connected = false;
47
+ this._errorHandlers = [];
48
+ }
49
+
50
+ /**
51
+ * Connects to the message broker using merged configuration.
52
+ * @param {Object} [options] - Optional overrides for host, queue, etc.
53
+ * @returns {Promise<void>}
54
+ * @throws {ConnectionError} If connecting fails.
55
+ */
56
+ async connect(options = {}) {
57
+ if (this._connected) return;
58
+
59
+ // Merge overrides into existing config
60
+ this._config = merge({}, this._config, options);
61
+
62
+ try {
63
+ // Instantiate appropriate transport: RabbitMQClient
64
+ this._transport = transportFactory.create(this._config);
65
+
66
+ // Register internal error propagation
67
+ this._transport.on('error', (err) => this._handleError(err));
68
+
69
+ await this._transport.connect(this._config);
70
+ this._connected = true;
71
+ } catch (err) {
72
+ throw new ConnectionError('Failed to connect to broker', err);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Disconnects from the message broker.
78
+ * @returns {Promise<void>}
79
+ * @throws {Error} If disconnecting fails unexpectedly.
80
+ */
81
+ async disconnect() {
82
+ if (!this._connected || !this._transport) return;
83
+ try {
84
+ await this._transport.disconnect();
85
+ this._connected = false;
86
+ this._transport = null;
87
+ } catch (err) {
88
+ throw new Error(`Error during disconnect: ${err.message}`);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Publishes a message to the specified queue.
94
+ * @param {string} queue - Target queue name.
95
+ * @param {Object|Buffer|string} message - Payload to send.
96
+ * @param {Object} [options] - RabbitMQ-specific overrides (routingKey, persistent, headers).
97
+ * @returns {Promise<void>}
98
+ * @throws {ConnectionError} If not connected.
99
+ * @throws {PublishError} If publish fails.
100
+ */
101
+ async publish(queue, message, options = {}) {
102
+ if (!this._connected || !this._transport) {
103
+ throw new ConnectionError('Cannot publish: client is not connected');
104
+ }
105
+
106
+ let buffer;
107
+ try {
108
+ if (Buffer.isBuffer(message)) {
109
+ buffer = message;
110
+ } else if (typeof message === 'string') {
111
+ buffer = Buffer.from(message, 'utf8');
112
+ } else {
113
+ const json = serializer.serialize(message);
114
+ buffer = Buffer.from(json, 'utf8');
115
+ }
116
+ } catch (err) {
117
+ throw new SerializationError('Failed to serialize message', message, err);
118
+ }
119
+
120
+ try {
121
+ await this._transport.publish(queue, buffer, options);
122
+ } catch (err) {
123
+ throw new PublishError(`Failed to publish to queue "${queue}"`, queue, err);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Begins consuming messages from the specified queue.
129
+ * @param {string} queue - Name of the queue to consume from.
130
+ * @param {function(Object): Promise<void>} messageHandler - Async function to process each message.
131
+ * @param {Object} [options] - RabbitMQ-specific overrides (prefetch, noAck).
132
+ * @returns {Promise<void>}
133
+ * @throws {ConnectionError} If not connected.
134
+ * @throws {ConsumeError} If consumer setup fails.
135
+ */
136
+ async consume(queue, messageHandler, options = {}) {
137
+ if (!this._connected || !this._transport) {
138
+ throw new ConnectionError('Cannot consume: client is not connected');
139
+ }
140
+
141
+ // Apply prefetch and noAck overrides if provided
142
+ const { prefetch, noAck } = options;
143
+ const consumeOptions = {};
144
+ if (typeof prefetch === 'number') consumeOptions.prefetch = prefetch;
145
+ if (typeof noAck === 'boolean') consumeOptions.noAck = noAck;
146
+
147
+ try {
148
+ await this._transport.consume(
149
+ queue,
150
+ async (msg) => {
151
+ try {
152
+ await messageHandler(msg);
153
+ if (consumeOptions.noAck === false) {
154
+ await this.ack(msg);
155
+ }
156
+ } catch (handlerErr) {
157
+ // On handler error, nack with requeue: true
158
+ await this.nack(msg, { requeue: true });
159
+ }
160
+ },
161
+ consumeOptions
162
+ );
163
+ } catch (err) {
164
+ throw new ConsumeError(`Failed to start consumer for queue "${queue}"`, queue, err);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Acknowledges a RabbitMQ message.
170
+ * @param {Object} msg - RabbitMQ message object.
171
+ * @returns {Promise<void>}
172
+ */
173
+ async ack(msg) {
174
+ if (!this._connected || !this._transport) {
175
+ throw new ConnectionError('Cannot ack: client is not connected');
176
+ }
177
+ return this._transport.ack(msg);
178
+ }
179
+
180
+ /**
181
+ * Negative-acknowledges a RabbitMQ message.
182
+ * @param {Object} msg - RabbitMQ message object.
183
+ * @param {Object} [options] - Options such as { requeue: boolean }.
184
+ * @returns {Promise<void>}
185
+ */
186
+ async nack(msg, options = {}) {
187
+ if (!this._connected || !this._transport) {
188
+ throw new ConnectionError('Cannot nack: client is not connected');
189
+ }
190
+ return this._transport.nack(msg, options);
191
+ }
192
+
193
+ /**
194
+ * Registers a global error handler. Internal or transport-level errors will be forwarded here.
195
+ * @param {function(Error): void} callback
196
+ */
197
+ onError(callback) {
198
+ if (typeof callback === 'function') {
199
+ this._errorHandlers.push(callback);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Internal helper to invoke all registered error handlers.
205
+ * @param {Error} error
206
+ * @private
207
+ */
208
+ _handleError(error) {
209
+ this._errorHandlers.forEach((cb) => {
210
+ try {
211
+ cb(error);
212
+ } catch (_) {
213
+ // Ignore errors in user-provided error handlers
214
+ }
215
+ });
216
+ }
217
+ }
218
+
219
+ module.exports = ConnectorMQClient;