@onlineapps/mq-client-core 1.0.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/README.md +89 -0
- package/package.json +32 -0
- package/src/BaseClient.js +230 -0
- package/src/config/configSchema.js +50 -0
- package/src/config/defaultConfig.js +38 -0
- package/src/index.js +30 -0
- package/src/transports/rabbitmqClient.js +245 -0
- package/src/transports/transportFactory.js +34 -0
- package/src/utils/errorHandler.js +121 -0
- package/src/utils/serializer.js +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @onlineapps/mq-client-core
|
|
2
|
+
|
|
3
|
+
Core MQ client library for RabbitMQ - shared by infrastructure services and connectors.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This is the **core library** extracted from `@onlineapps/conn-infra-mq` to provide basic MQ functionality for infrastructure services (gateway, registry, validator) without business-specific features.
|
|
8
|
+
|
|
9
|
+
**Architecture Principle**: Connectors are exclusively for business services. If a connector contains functionality that should also serve infrastructure services, it must be extracted into a shared library.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @onlineapps/mq-client-core
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### For Infrastructure Services
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
const BaseClient = require('@onlineapps/mq-client-core');
|
|
23
|
+
|
|
24
|
+
const mqClient = new BaseClient({
|
|
25
|
+
type: 'rabbitmq',
|
|
26
|
+
host: 'amqp://localhost:5672',
|
|
27
|
+
queue: 'workflow.init' // Optional default queue
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await mqClient.connect();
|
|
31
|
+
await mqClient.publish('workflow.init', { workflowId: '123', data: '...' });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Configuration
|
|
35
|
+
|
|
36
|
+
**Flexible Schema** - Only `type` and `host` are required:
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
{
|
|
40
|
+
type: 'rabbitmq', // Required
|
|
41
|
+
host: 'amqp://...', // Required
|
|
42
|
+
queue: 'optional', // Optional default queue
|
|
43
|
+
exchange: '', // Optional exchange
|
|
44
|
+
durable: true, // Optional (default: true)
|
|
45
|
+
prefetch: 1, // Optional (default: 1)
|
|
46
|
+
noAck: false, // Optional (default: false)
|
|
47
|
+
logger: null // Optional custom logger
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
### BaseClient
|
|
54
|
+
|
|
55
|
+
- `connect(options?)` - Connect to RabbitMQ
|
|
56
|
+
- `disconnect()` - Disconnect from RabbitMQ
|
|
57
|
+
- `publish(queue, message, options?)` - Publish message to queue
|
|
58
|
+
- `consume(queue, handler, options?)` - Consume messages from queue
|
|
59
|
+
- `ack(msg)` - Acknowledge message
|
|
60
|
+
- `nack(msg, options?)` - Negative acknowledge message
|
|
61
|
+
- `isConnected()` - Check connection status
|
|
62
|
+
- `onError(callback)` - Register error handler
|
|
63
|
+
|
|
64
|
+
## Architecture
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
mq-client-core (this library)
|
|
68
|
+
├── BaseClient - Core AMQP operations
|
|
69
|
+
├── RabbitMQClient - Transport implementation
|
|
70
|
+
└── Basic publish/consume functionality
|
|
71
|
+
|
|
72
|
+
conn-infra-mq (connector for business services)
|
|
73
|
+
├── Uses mq-client-core internally
|
|
74
|
+
├── ConnectorMQClient - Orchestrator with layers
|
|
75
|
+
├── WorkflowRouter - Business workflow routing
|
|
76
|
+
└── Additional business-specific features
|
|
77
|
+
|
|
78
|
+
Infrastructure services (gateway, registry, validator)
|
|
79
|
+
└── Use mq-client-core directly (not the connector)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Related Packages
|
|
83
|
+
|
|
84
|
+
- `@onlineapps/conn-infra-mq` - Full connector with business-specific features (uses this library internally)
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
|
89
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onlineapps/mq-client-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"test:unit": "jest --testPathPattern=tests/unit",
|
|
9
|
+
"test:component": "jest --testPathPattern=tests/component",
|
|
10
|
+
"test:integration": "jest --testPathPattern=tests/integration"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"rabbitmq",
|
|
14
|
+
"amqp",
|
|
15
|
+
"message-queue",
|
|
16
|
+
"mq"
|
|
17
|
+
],
|
|
18
|
+
"author": "OnlineApps",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"amqplib": "^0.10.3",
|
|
22
|
+
"ajv": "^8.12.0",
|
|
23
|
+
"lodash.merge": "^4.6.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"jest": "^29.7.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BaseClient: a promise-based, broker-agnostic client for RabbitMQ
|
|
5
|
+
* Core MQ client for infrastructure services and as base for connectors.
|
|
6
|
+
* Uses transportFactory to select the appropriate transport implementation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const Ajv = require('ajv');
|
|
10
|
+
const merge = require('lodash.merge');
|
|
11
|
+
|
|
12
|
+
const configSchema = require('./config/configSchema');
|
|
13
|
+
const defaultConfig = require('./config/defaultConfig');
|
|
14
|
+
const transportFactory = require('./transports/transportFactory');
|
|
15
|
+
const serializer = require('./utils/serializer');
|
|
16
|
+
const {
|
|
17
|
+
ConnectionError,
|
|
18
|
+
PublishError,
|
|
19
|
+
ConsumeError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
SerializationError,
|
|
22
|
+
} = require('./utils/errorHandler');
|
|
23
|
+
|
|
24
|
+
class BaseClient {
|
|
25
|
+
/**
|
|
26
|
+
* @param {Object} config - User-supplied configuration.
|
|
27
|
+
* @throws {ValidationError} If required fields are missing or invalid.
|
|
28
|
+
*/
|
|
29
|
+
constructor(config) {
|
|
30
|
+
const ajv = new Ajv({ allErrors: true, useDefaults: true });
|
|
31
|
+
const validate = ajv.compile(configSchema);
|
|
32
|
+
|
|
33
|
+
// Merge user config with defaults
|
|
34
|
+
this._config = merge({}, defaultConfig, config || {});
|
|
35
|
+
|
|
36
|
+
// Validate merged config
|
|
37
|
+
const valid = validate(this._config);
|
|
38
|
+
if (!valid) {
|
|
39
|
+
const details = validate.errors.map((err) => ({
|
|
40
|
+
path: err.instancePath,
|
|
41
|
+
message: err.message,
|
|
42
|
+
}));
|
|
43
|
+
throw new ValidationError('Invalid configuration', details);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this._transport = null;
|
|
47
|
+
this._connected = false;
|
|
48
|
+
this._errorHandlers = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Connects to the message broker using merged configuration.
|
|
53
|
+
* @param {Object} [options] - Optional overrides for host, queue, etc.
|
|
54
|
+
* @returns {Promise<void>}
|
|
55
|
+
* @throws {ConnectionError} If connecting fails.
|
|
56
|
+
*/
|
|
57
|
+
async connect(options = {}) {
|
|
58
|
+
if (this._connected) return;
|
|
59
|
+
|
|
60
|
+
// Merge overrides into existing config
|
|
61
|
+
this._config = merge({}, this._config, options);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Instantiate appropriate transport: RabbitMQClient
|
|
65
|
+
this._transport = transportFactory.create(this._config);
|
|
66
|
+
|
|
67
|
+
// Register internal error propagation
|
|
68
|
+
this._transport.on('error', (err) => this._handleError(err));
|
|
69
|
+
|
|
70
|
+
await this._transport.connect(this._config);
|
|
71
|
+
this._connected = true;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw new ConnectionError('Failed to connect to broker', err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Disconnects from the message broker.
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
* @throws {Error} If disconnecting fails unexpectedly.
|
|
81
|
+
*/
|
|
82
|
+
async disconnect() {
|
|
83
|
+
if (!this._connected || !this._transport) return;
|
|
84
|
+
try {
|
|
85
|
+
await this._transport.disconnect();
|
|
86
|
+
this._connected = false;
|
|
87
|
+
this._transport = null;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
throw new Error(`Error during disconnect: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Publishes a message to the specified queue.
|
|
95
|
+
* @param {string} queue - Target queue name.
|
|
96
|
+
* @param {Object|Buffer|string} message - Payload to send.
|
|
97
|
+
* @param {Object} [options] - RabbitMQ-specific overrides (routingKey, persistent, headers, queueOptions).
|
|
98
|
+
* @returns {Promise<void>}
|
|
99
|
+
* @throws {ConnectionError} If not connected.
|
|
100
|
+
* @throws {PublishError} If publish fails.
|
|
101
|
+
*/
|
|
102
|
+
async publish(queue, message, options = {}) {
|
|
103
|
+
if (!this._connected || !this._transport) {
|
|
104
|
+
throw new ConnectionError('Cannot publish: client is not connected');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let buffer;
|
|
108
|
+
try {
|
|
109
|
+
if (Buffer.isBuffer(message)) {
|
|
110
|
+
buffer = message;
|
|
111
|
+
} else if (typeof message === 'string') {
|
|
112
|
+
buffer = Buffer.from(message, 'utf8');
|
|
113
|
+
} else {
|
|
114
|
+
const json = serializer.serialize(message);
|
|
115
|
+
buffer = Buffer.from(json, 'utf8');
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new SerializationError('Failed to serialize message', message, err);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await this._transport.publish(queue, buffer, options);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
throw new PublishError(`Failed to publish to queue "${queue}"`, queue, err);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Begins consuming messages from the specified queue.
|
|
130
|
+
* @param {string} queue - Name of the queue to consume from.
|
|
131
|
+
* @param {function(Object): Promise<void>} messageHandler - Async function to process each message.
|
|
132
|
+
* @param {Object} [options] - RabbitMQ-specific overrides (prefetch, noAck, queueOptions).
|
|
133
|
+
* @returns {Promise<void>}
|
|
134
|
+
* @throws {ConnectionError} If not connected.
|
|
135
|
+
* @throws {ConsumeError} If consumer setup fails.
|
|
136
|
+
*/
|
|
137
|
+
async consume(queue, messageHandler, options = {}) {
|
|
138
|
+
if (!this._connected || !this._transport) {
|
|
139
|
+
throw new ConnectionError('Cannot consume: client is not connected');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Apply prefetch and noAck overrides if provided
|
|
143
|
+
const { prefetch, noAck } = options;
|
|
144
|
+
const consumeOptions = {};
|
|
145
|
+
if (typeof prefetch === 'number') consumeOptions.prefetch = prefetch;
|
|
146
|
+
if (typeof noAck === 'boolean') consumeOptions.noAck = noAck;
|
|
147
|
+
if (options.queueOptions) consumeOptions.queueOptions = options.queueOptions;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await this._transport.consume(
|
|
151
|
+
queue,
|
|
152
|
+
async (msg) => {
|
|
153
|
+
try {
|
|
154
|
+
await messageHandler(msg);
|
|
155
|
+
if (consumeOptions.noAck === false) {
|
|
156
|
+
await this.ack(msg);
|
|
157
|
+
}
|
|
158
|
+
} catch (handlerErr) {
|
|
159
|
+
// On handler error, nack with requeue: true
|
|
160
|
+
await this.nack(msg, { requeue: true });
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
consumeOptions
|
|
164
|
+
);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
throw new ConsumeError(`Failed to start consumer for queue "${queue}"`, queue, err);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Acknowledges a RabbitMQ message.
|
|
172
|
+
* @param {Object} msg - RabbitMQ message object.
|
|
173
|
+
* @returns {Promise<void>}
|
|
174
|
+
*/
|
|
175
|
+
async ack(msg) {
|
|
176
|
+
if (!this._connected || !this._transport) {
|
|
177
|
+
throw new ConnectionError('Cannot ack: client is not connected');
|
|
178
|
+
}
|
|
179
|
+
return this._transport.ack(msg);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Negative-acknowledges a RabbitMQ message.
|
|
184
|
+
* @param {Object} msg - RabbitMQ message object.
|
|
185
|
+
* @param {Object} [options] - Options such as { requeue: boolean }.
|
|
186
|
+
* @returns {Promise<void>}
|
|
187
|
+
*/
|
|
188
|
+
async nack(msg, options = {}) {
|
|
189
|
+
if (!this._connected || !this._transport) {
|
|
190
|
+
throw new ConnectionError('Cannot nack: client is not connected');
|
|
191
|
+
}
|
|
192
|
+
return this._transport.nack(msg, options);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Registers a global error handler. Internal or transport-level errors will be forwarded here.
|
|
197
|
+
* @param {function(Error): void} callback
|
|
198
|
+
*/
|
|
199
|
+
onError(callback) {
|
|
200
|
+
if (typeof callback === 'function') {
|
|
201
|
+
this._errorHandlers.push(callback);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Internal helper to invoke all registered error handlers.
|
|
207
|
+
* @param {Error} error
|
|
208
|
+
* @private
|
|
209
|
+
*/
|
|
210
|
+
_handleError(error) {
|
|
211
|
+
this._errorHandlers.forEach((cb) => {
|
|
212
|
+
try {
|
|
213
|
+
cb(error);
|
|
214
|
+
} catch (_) {
|
|
215
|
+
// Ignore errors in user-provided error handlers
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if client is connected
|
|
222
|
+
* @returns {boolean} Connection status
|
|
223
|
+
*/
|
|
224
|
+
isConnected() {
|
|
225
|
+
return this._connected === true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = BaseClient;
|
|
230
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* configSchema.js
|
|
5
|
+
*
|
|
6
|
+
* JSON Schema used by Ajv to validate the user-supplied configuration object.
|
|
7
|
+
*
|
|
8
|
+
* FLEXIBLE SCHEMA for infrastructure services:
|
|
9
|
+
* - Only requires 'type' and 'host'
|
|
10
|
+
* - Allows additional properties for flexibility
|
|
11
|
+
* - Queue is optional (infrastructure services may not need default queue)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
type: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
enum: ['rabbitmq'],
|
|
20
|
+
},
|
|
21
|
+
host: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
minLength: 1,
|
|
24
|
+
},
|
|
25
|
+
// Queue is optional for infrastructure services
|
|
26
|
+
queue: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
},
|
|
29
|
+
exchange: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
},
|
|
32
|
+
durable: {
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
},
|
|
35
|
+
prefetch: {
|
|
36
|
+
type: 'integer',
|
|
37
|
+
minimum: 0,
|
|
38
|
+
},
|
|
39
|
+
noAck: {
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
},
|
|
42
|
+
logger: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
description: 'Custom logger with methods: info, warn, error, debug',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ['type', 'host'], // Only type and host required
|
|
48
|
+
additionalProperties: true, // Allow additional properties for flexibility
|
|
49
|
+
};
|
|
50
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* defaultConfig.js
|
|
5
|
+
*
|
|
6
|
+
* Provides default configuration values for MQ Client Core.
|
|
7
|
+
* Users can override any of these by passing a custom config object
|
|
8
|
+
* or by supplying overrides to connect().
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
// Transport type: currently only 'rabbitmq' is fully supported.
|
|
13
|
+
type: 'rabbitmq',
|
|
14
|
+
|
|
15
|
+
// RabbitMQ connection URI or hostname (e.g., 'amqp://localhost:5672').
|
|
16
|
+
host: 'amqp://localhost:5672',
|
|
17
|
+
|
|
18
|
+
// Default queue name; can be overridden per call to publish/consume.
|
|
19
|
+
// Optional for infrastructure services (they may not need a default queue).
|
|
20
|
+
queue: '',
|
|
21
|
+
|
|
22
|
+
// Default exchange name (empty string → default direct exchange).
|
|
23
|
+
exchange: '',
|
|
24
|
+
|
|
25
|
+
// Declare queues/exchanges as durable by default.
|
|
26
|
+
durable: true,
|
|
27
|
+
|
|
28
|
+
// Default prefetch count for consumers.
|
|
29
|
+
prefetch: 1,
|
|
30
|
+
|
|
31
|
+
// Default auto-acknowledge setting for consumers.
|
|
32
|
+
noAck: false,
|
|
33
|
+
|
|
34
|
+
// Custom logger object (if not provided, console.* will be used).
|
|
35
|
+
// Expected interface: { info(), warn(), error(), debug() }.
|
|
36
|
+
logger: null,
|
|
37
|
+
};
|
|
38
|
+
|
package/src/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @onlineapps/mq-client-core
|
|
5
|
+
*
|
|
6
|
+
* Core MQ client library for RabbitMQ - shared by infrastructure services and connectors.
|
|
7
|
+
* Provides basic publish/consume functionality without business-specific features.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BaseClient = require('./BaseClient');
|
|
11
|
+
const RabbitMQClient = require('./transports/rabbitmqClient');
|
|
12
|
+
const {
|
|
13
|
+
ValidationError,
|
|
14
|
+
ConnectionError,
|
|
15
|
+
PublishError,
|
|
16
|
+
ConsumeError,
|
|
17
|
+
SerializationError,
|
|
18
|
+
} = require('./utils/errorHandler');
|
|
19
|
+
|
|
20
|
+
module.exports = BaseClient;
|
|
21
|
+
module.exports.BaseClient = BaseClient;
|
|
22
|
+
module.exports.RabbitMQClient = RabbitMQClient;
|
|
23
|
+
module.exports.errors = {
|
|
24
|
+
ValidationError,
|
|
25
|
+
ConnectionError,
|
|
26
|
+
PublishError,
|
|
27
|
+
ConsumeError,
|
|
28
|
+
SerializationError,
|
|
29
|
+
};
|
|
30
|
+
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RabbitMQClient: transport implementation for RabbitMQ using amqplib.
|
|
5
|
+
* Simplified version for infrastructure services - no queueConfig dependency.
|
|
6
|
+
* Implements connect, disconnect, publish, consume, ack, nack, and error propagation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const amqp = require('amqplib');
|
|
10
|
+
const EventEmitter = require('events');
|
|
11
|
+
|
|
12
|
+
class RabbitMQClient extends EventEmitter {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} config
|
|
15
|
+
* @param {string} config.host - AMQP URI or hostname (e.g., 'amqp://localhost:5672')
|
|
16
|
+
* @param {string} [config.queue] - Default queue name (optional; can be overridden per call)
|
|
17
|
+
* @param {string} [config.exchange] - Default exchange (default: '')
|
|
18
|
+
* @param {boolean} [config.durable] - Declare queues/exchanges as durable (default: true)
|
|
19
|
+
* @param {number} [config.prefetch] - Default prefetch count for consumers (default: 1)
|
|
20
|
+
* @param {boolean} [config.noAck] - Default auto-acknowledge setting (default: false)
|
|
21
|
+
*/
|
|
22
|
+
constructor(config) {
|
|
23
|
+
super();
|
|
24
|
+
this._config = Object.assign(
|
|
25
|
+
{
|
|
26
|
+
exchange: '',
|
|
27
|
+
durable: true,
|
|
28
|
+
prefetch: 1,
|
|
29
|
+
noAck: false,
|
|
30
|
+
},
|
|
31
|
+
config
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
this._connection = null;
|
|
35
|
+
this._channel = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Getter for channel - provides compatibility with QueueManager
|
|
40
|
+
*/
|
|
41
|
+
get channel() {
|
|
42
|
+
return this._channel;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Connects to RabbitMQ server and creates a confirm channel.
|
|
47
|
+
* @returns {Promise<void>}
|
|
48
|
+
* @throws {Error} If connection or channel creation fails.
|
|
49
|
+
*/
|
|
50
|
+
async connect() {
|
|
51
|
+
try {
|
|
52
|
+
this._connection = await amqp.connect(this._config.host);
|
|
53
|
+
this._connection.on('error', (err) => this.emit('error', err));
|
|
54
|
+
this._connection.on('close', () => {
|
|
55
|
+
// Emit a connection close error to notify listeners
|
|
56
|
+
this.emit('error', new Error('RabbitMQ connection closed unexpectedly'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Use ConfirmChannel to enable publisher confirms
|
|
60
|
+
this._channel = await this._connection.createConfirmChannel();
|
|
61
|
+
this._channel.on('error', (err) => this.emit('error', err));
|
|
62
|
+
this._channel.on('close', () => {
|
|
63
|
+
// Emit a channel close error
|
|
64
|
+
this.emit('error', new Error('RabbitMQ channel closed unexpectedly'));
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Cleanup partially created resources
|
|
68
|
+
if (this._connection) {
|
|
69
|
+
try {
|
|
70
|
+
await this._connection.close();
|
|
71
|
+
} catch (_) {
|
|
72
|
+
/* ignore */
|
|
73
|
+
}
|
|
74
|
+
this._connection = null;
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Disconnects: closes channel and connection.
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
async disconnect() {
|
|
85
|
+
try {
|
|
86
|
+
if (this._channel) {
|
|
87
|
+
await this._channel.close();
|
|
88
|
+
this._channel = null;
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
this.emit('error', err);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
if (this._connection) {
|
|
95
|
+
await this._connection.close();
|
|
96
|
+
this._connection = null;
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
this.emit('error', err);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Publishes a message buffer to the specified queue (or default queue) or exchange.
|
|
105
|
+
* @param {string} queue - Target queue name.
|
|
106
|
+
* @param {Buffer} buffer - Message payload as Buffer.
|
|
107
|
+
* @param {Object} [options] - Overrides: routingKey, persistent, headers.
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
* @throws {Error} If publish fails or channel is not available.
|
|
110
|
+
*/
|
|
111
|
+
async publish(queue, buffer, options = {}) {
|
|
112
|
+
if (!this._channel) {
|
|
113
|
+
throw new Error('Cannot publish: channel is not initialized');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const exchange = this._config.exchange || '';
|
|
117
|
+
const routingKey = options.routingKey || queue;
|
|
118
|
+
const persistent = options.persistent !== undefined ? options.persistent : this._config.durable;
|
|
119
|
+
const headers = options.headers || {};
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Ensure queue exists if publishing directly to queue and using default exchange
|
|
123
|
+
if (!exchange) {
|
|
124
|
+
// Simple queue assertion - infrastructure services should ensure queues exist
|
|
125
|
+
// If queue doesn't exist, assertQueue will create it with default options
|
|
126
|
+
const queueOptions = options.queueOptions || { durable: this._config.durable };
|
|
127
|
+
await this._channel.assertQueue(queue, queueOptions);
|
|
128
|
+
this._channel.sendToQueue(queue, buffer, { persistent, headers, routingKey });
|
|
129
|
+
} else {
|
|
130
|
+
// If exchange is specified, assert exchange and publish to it
|
|
131
|
+
await this._channel.assertExchange(exchange, 'direct', { durable: this._config.durable });
|
|
132
|
+
this._channel.publish(exchange, routingKey, buffer, { persistent, headers });
|
|
133
|
+
}
|
|
134
|
+
// Wait for confirmation
|
|
135
|
+
await this._channel.waitForConfirms();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
this.emit('error', err);
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Starts consuming messages from the specified queue.
|
|
144
|
+
* @param {string} queue - Queue name to consume from.
|
|
145
|
+
* @param {function(Object): Promise<void>} onMessage - Async handler receiving raw msg.
|
|
146
|
+
* @param {Object} [options] - Overrides: prefetch, noAck, queueOptions.
|
|
147
|
+
* @returns {Promise<void>}
|
|
148
|
+
* @throws {Error} If consume setup fails or channel is not available.
|
|
149
|
+
*/
|
|
150
|
+
async consume(queue, onMessage, options = {}) {
|
|
151
|
+
if (!this._channel) {
|
|
152
|
+
throw new Error('Cannot consume: channel is not initialized');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const durable = options.durable !== undefined ? options.durable : this._config.durable;
|
|
156
|
+
const prefetch = options.prefetch !== undefined ? options.prefetch : this._config.prefetch;
|
|
157
|
+
const noAck = options.noAck !== undefined ? options.noAck : this._config.noAck;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// Skip assertQueue for reply queues (they're already created with specific settings)
|
|
161
|
+
// Reply queues start with 'rpc.reply.' and are created as non-durable
|
|
162
|
+
if (!queue.startsWith('rpc.reply.')) {
|
|
163
|
+
// Simple queue assertion - infrastructure services should ensure queues exist
|
|
164
|
+
// If queue doesn't exist, assertQueue will create it with default options
|
|
165
|
+
// If it exists with different args (406), log warning and proceed
|
|
166
|
+
const queueOptions = options.queueOptions || { durable };
|
|
167
|
+
try {
|
|
168
|
+
await this._channel.assertQueue(queue, queueOptions);
|
|
169
|
+
} catch (assertErr) {
|
|
170
|
+
// If queue exists with different arguments (406), use it as-is
|
|
171
|
+
if (assertErr.code === 406) {
|
|
172
|
+
console.warn(`[RabbitMQClient] Queue ${queue} exists with different arguments, using as-is:`, assertErr.message);
|
|
173
|
+
// Don't try to re-assert - just proceed to consume
|
|
174
|
+
} else {
|
|
175
|
+
// Other error - rethrow
|
|
176
|
+
throw assertErr;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Set prefetch if provided
|
|
181
|
+
if (typeof prefetch === 'number') {
|
|
182
|
+
this._channel.prefetch(prefetch);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await this._channel.consume(
|
|
186
|
+
queue,
|
|
187
|
+
async (msg) => {
|
|
188
|
+
if (msg === null) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
await onMessage(msg);
|
|
193
|
+
if (!noAck) {
|
|
194
|
+
this._channel.ack(msg);
|
|
195
|
+
}
|
|
196
|
+
} catch (handlerErr) {
|
|
197
|
+
// Negative acknowledge and requeue by default
|
|
198
|
+
this._channel.nack(msg, false, true);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{ noAck }
|
|
202
|
+
);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
this.emit('error', err);
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Acknowledges a message.
|
|
211
|
+
* @param {Object} msg - RabbitMQ message object.
|
|
212
|
+
*/
|
|
213
|
+
async ack(msg) {
|
|
214
|
+
if (!this._channel) {
|
|
215
|
+
throw new Error('Cannot ack: channel is not initialized');
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
this._channel.ack(msg);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
this.emit('error', err);
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Negative-acknowledges a message.
|
|
227
|
+
* @param {Object} msg - RabbitMQ message object.
|
|
228
|
+
* @param {Object} [options] - { requeue: boolean }.
|
|
229
|
+
*/
|
|
230
|
+
async nack(msg, options = {}) {
|
|
231
|
+
if (!this._channel) {
|
|
232
|
+
throw new Error('Cannot nack: channel is not initialized');
|
|
233
|
+
}
|
|
234
|
+
const requeue = options.requeue !== undefined ? options.requeue : true;
|
|
235
|
+
try {
|
|
236
|
+
this._channel.nack(msg, false, requeue);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
this.emit('error', err);
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = RabbitMQClient;
|
|
245
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* transportFactory: selects and instantiates the appropriate transport
|
|
5
|
+
* based on configuration. Currently supports RabbitMQ.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const RabbitMQClient = require('./rabbitmqClient');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Factory method: returns a transport instance based on config.type.
|
|
12
|
+
* @param {Object} config
|
|
13
|
+
* @param {'rabbitmq'} config.type - Transport type ('rabbitmq' for now)
|
|
14
|
+
* @returns {Object} Instance of transport (RabbitMQClient, etc.)
|
|
15
|
+
* @throws {Error} If the configured type is unsupported or missing
|
|
16
|
+
*/
|
|
17
|
+
function create(config) {
|
|
18
|
+
if (!config || !config.type) {
|
|
19
|
+
throw new Error('Transport type is required in configuration');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
switch (config.type.toLowerCase()) {
|
|
23
|
+
case 'rabbitmq':
|
|
24
|
+
return new RabbitMQClient(config);
|
|
25
|
+
|
|
26
|
+
default:
|
|
27
|
+
throw new Error(`Unsupported transport type: ${config.type}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
create,
|
|
33
|
+
};
|
|
34
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* errorHandler.js
|
|
5
|
+
*
|
|
6
|
+
* Defines all custom error types used by MQ client core.
|
|
7
|
+
* Each extends the built-in Error class and carries additional contextual fields.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ValidationError
|
|
12
|
+
* Thrown when user-supplied configuration does not match schema.
|
|
13
|
+
* - message: human-readable description
|
|
14
|
+
* - details: array of { path, message } describing each invalid field
|
|
15
|
+
*/
|
|
16
|
+
class ValidationError extends Error {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} message
|
|
19
|
+
* @param {Array<{path: string, message: string}>} details
|
|
20
|
+
*/
|
|
21
|
+
constructor(message, details) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'ValidationError';
|
|
24
|
+
this.details = Array.isArray(details) ? details : [];
|
|
25
|
+
Error.captureStackTrace(this, ValidationError);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ConnectionError
|
|
31
|
+
* Thrown when client cannot connect to broker.
|
|
32
|
+
* - message: description of what went wrong
|
|
33
|
+
* - cause: original error object
|
|
34
|
+
*/
|
|
35
|
+
class ConnectionError extends Error {
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} message
|
|
38
|
+
* @param {Error} cause
|
|
39
|
+
*/
|
|
40
|
+
constructor(message, cause) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'ConnectionError';
|
|
43
|
+
this.cause = cause;
|
|
44
|
+
Error.captureStackTrace(this, ConnectionError);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* PublishError
|
|
50
|
+
* Thrown when publishing a message fails.
|
|
51
|
+
* - message: description
|
|
52
|
+
* - queue: target queue name
|
|
53
|
+
* - cause: original error
|
|
54
|
+
*/
|
|
55
|
+
class PublishError extends Error {
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} message
|
|
58
|
+
* @param {string} queue
|
|
59
|
+
* @param {Error} cause
|
|
60
|
+
*/
|
|
61
|
+
constructor(message, queue, cause) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = 'PublishError';
|
|
64
|
+
this.queue = queue;
|
|
65
|
+
this.cause = cause;
|
|
66
|
+
Error.captureStackTrace(this, PublishError);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* ConsumeError
|
|
72
|
+
* Thrown when setting up a consumer fails.
|
|
73
|
+
* - message: description
|
|
74
|
+
* - queue: queue name associated with the consumer
|
|
75
|
+
* - cause: original error
|
|
76
|
+
*/
|
|
77
|
+
class ConsumeError extends Error {
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} message
|
|
80
|
+
* @param {string} queue
|
|
81
|
+
* @param {Error} cause
|
|
82
|
+
*/
|
|
83
|
+
constructor(message, queue, cause) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = 'ConsumeError';
|
|
86
|
+
this.queue = queue;
|
|
87
|
+
this.cause = cause;
|
|
88
|
+
Error.captureStackTrace(this, ConsumeError);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* SerializationError
|
|
94
|
+
* Thrown when JSON serialization or deserialization fails.
|
|
95
|
+
* - message: description
|
|
96
|
+
* - payload: original object or buffer that caused the error
|
|
97
|
+
* - cause: native exception (e.g., TypeError for circular references)
|
|
98
|
+
*/
|
|
99
|
+
class SerializationError extends Error {
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} message
|
|
102
|
+
* @param {*} payload
|
|
103
|
+
* @param {Error} cause
|
|
104
|
+
*/
|
|
105
|
+
constructor(message, payload, cause) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = 'SerializationError';
|
|
108
|
+
this.payload = payload;
|
|
109
|
+
this.cause = cause;
|
|
110
|
+
Error.captureStackTrace(this, SerializationError);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
ValidationError,
|
|
116
|
+
ConnectionError,
|
|
117
|
+
PublishError,
|
|
118
|
+
ConsumeError,
|
|
119
|
+
SerializationError,
|
|
120
|
+
};
|
|
121
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* serializer.js
|
|
5
|
+
*
|
|
6
|
+
* Contains helper functions for serializing and deserializing payloads.
|
|
7
|
+
* On failure, throws a SerializationError (defined in errorHandler.js).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { SerializationError } = require('./errorHandler');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Serializes a JavaScript object to JSON string.
|
|
14
|
+
* @param {Object} obj
|
|
15
|
+
* @returns {string}
|
|
16
|
+
* @throws {SerializationError} If JSON.stringify fails (e.g., circular reference).
|
|
17
|
+
*/
|
|
18
|
+
function serialize(obj) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(obj);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
throw new SerializationError('Failed to serialize object', obj, err);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Deserializes a Buffer or string into a JavaScript object via JSON.parse.
|
|
28
|
+
* @param {Buffer|string} buffer
|
|
29
|
+
* @returns {Object}
|
|
30
|
+
* @throws {SerializationError} If JSON.parse fails or buffer cannot be converted.
|
|
31
|
+
*/
|
|
32
|
+
function deserialize(buffer) {
|
|
33
|
+
try {
|
|
34
|
+
const str = Buffer.isBuffer(buffer) ? buffer.toString('utf8') : buffer;
|
|
35
|
+
return JSON.parse(str);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new SerializationError('Failed to deserialize payload', buffer, err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
serialize,
|
|
43
|
+
deserialize,
|
|
44
|
+
};
|
|
45
|
+
|