@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 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
+