@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.
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WorkflowRouter - Handles workflow routing between services
5
+ * Manages workflow.init, service routing, and workflow.completed patterns
6
+ */
7
+ class WorkflowRouter {
8
+ constructor(mqClient, config = {}) {
9
+ this.client = mqClient;
10
+ this.config = {
11
+ workflowInitQueue: config.workflowInitQueue || 'workflow.init',
12
+ workflowCompletedQueue: config.workflowCompletedQueue || 'workflow.completed',
13
+ serviceName: config.serviceName || 'unknown',
14
+ ...config
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Publish workflow to init queue (entry point for all workflows)
20
+ * @param {Object} workflow - Workflow message with cookbook
21
+ * @param {Object} options - Additional publish options
22
+ */
23
+ async publishWorkflowInit(workflow, options = {}) {
24
+ return this.client.publish(workflow, {
25
+ queue: this.config.workflowInitQueue,
26
+ ...options
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Publish to specific service workflow queue
32
+ * @param {string} serviceName - Target service name
33
+ * @param {Object} message - Workflow message
34
+ * @param {Object} options - Additional publish options
35
+ */
36
+ async publishToServiceWorkflow(serviceName, message, options = {}) {
37
+ return this.client.publish(message, {
38
+ queue: `${serviceName}.workflow`,
39
+ ...options
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Publish directly to service queue (non-workflow)
45
+ * @param {string} serviceName - Target service name
46
+ * @param {Object} message - Message to send
47
+ * @param {Object} options - Additional publish options
48
+ */
49
+ async publishToService(serviceName, message, options = {}) {
50
+ return this.client.publish(message, {
51
+ queue: `${serviceName}.queue`,
52
+ ...options
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Publish completed workflow result
58
+ * @param {Object} result - Workflow completion result
59
+ * @param {Object} options - Additional publish options
60
+ */
61
+ async publishWorkflowCompleted(result, options = {}) {
62
+ return this.client.publish(result, {
63
+ queue: this.config.workflowCompletedQueue,
64
+ ...options
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Route to next service based on workflow step
70
+ * @param {Object} workflow - Current workflow state
71
+ * @param {string} nextService - Next service to route to
72
+ * @param {Object} stepResult - Result from current step
73
+ */
74
+ async routeToNextService(workflow, nextService, stepResult) {
75
+ const updatedWorkflow = {
76
+ ...workflow,
77
+ current_step: (workflow.current_step || 0) + 1,
78
+ results: [...(workflow.results || []), stepResult],
79
+ last_service: this.config.serviceName,
80
+ routed_at: new Date().toISOString()
81
+ };
82
+
83
+ if (nextService) {
84
+ return this.publishToServiceWorkflow(nextService, updatedWorkflow);
85
+ } else {
86
+ // No next service, workflow is complete
87
+ updatedWorkflow.status = 'completed';
88
+ return this.publishWorkflowCompleted(updatedWorkflow);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Consume from workflow init queue (competing consumers pattern)
94
+ * @param {Function} handler - Message handler function
95
+ * @param {Object} options - Consume options
96
+ */
97
+ async consumeWorkflowInit(handler, options = {}) {
98
+ return this.client.consume(handler, {
99
+ queue: this.config.workflowInitQueue,
100
+ ...options
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Consume from service workflow queue
106
+ * @param {string} serviceName - Service name (defaults to configured)
107
+ * @param {Function} handler - Message handler function
108
+ * @param {Object} options - Consume options
109
+ */
110
+ async consumeServiceWorkflow(serviceName, handler, options = {}) {
111
+ if (typeof serviceName === 'function') {
112
+ // If serviceName is actually the handler
113
+ handler = serviceName;
114
+ serviceName = this.config.serviceName;
115
+ }
116
+
117
+ return this.client.consume(handler, {
118
+ queue: `${serviceName}.workflow`,
119
+ ...options
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Consume completed workflows
125
+ * @param {Function} handler - Message handler function
126
+ * @param {Object} options - Consume options
127
+ */
128
+ async consumeWorkflowCompleted(handler, options = {}) {
129
+ return this.client.consume(handler, {
130
+ queue: this.config.workflowCompletedQueue,
131
+ ...options
132
+ });
133
+ }
134
+ }
135
+
136
+ module.exports = WorkflowRouter;
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * RabbitMQClient: transport implementation for RabbitMQ using amqplib.
5
+ * Implements connect, disconnect, publish, consume, ack, nack, and error propagation.
6
+ */
7
+
8
+ const amqp = require('amqplib');
9
+ const EventEmitter = require('events');
10
+
11
+ class RabbitMQClient extends EventEmitter {
12
+ /**
13
+ * @param {Object} config
14
+ * @param {string} config.host - AMQP URI or hostname (e.g., 'amqp://localhost:5672')
15
+ * @param {string} [config.queue] - Default queue name (optional; can be overridden per call)
16
+ * @param {string} [config.exchange] - Default exchange (default: '')
17
+ * @param {boolean} [config.durable] - Declare queues/exchanges as durable (default: true)
18
+ * @param {number} [config.prefetch] - Default prefetch count for consumers (default: 1)
19
+ * @param {boolean} [config.noAck] - Default auto-acknowledge setting (default: false)
20
+ * @param {Object} [config.retryPolicy] - { retries, initialDelayMs, maxDelayMs, factor } (not implemented here)
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
+ * Connects to RabbitMQ server and creates a confirm channel.
40
+ * @returns {Promise<void>}
41
+ * @throws {Error} If connection or channel creation fails.
42
+ */
43
+ async connect() {
44
+ try {
45
+ this._connection = await amqp.connect(this._config.host);
46
+ this._connection.on('error', (err) => this.emit('error', err));
47
+ this._connection.on('close', () => {
48
+ // Emit a connection close error to notify listeners
49
+ this.emit('error', new Error('RabbitMQ connection closed unexpectedly'));
50
+ });
51
+
52
+ // Use ConfirmChannel to enable publisher confirms
53
+ this._channel = await this._connection.createConfirmChannel();
54
+ this._channel.on('error', (err) => this.emit('error', err));
55
+ this._channel.on('close', () => {
56
+ // Emit a channel close error
57
+ this.emit('error', new Error('RabbitMQ channel closed unexpectedly'));
58
+ });
59
+ } catch (err) {
60
+ // Cleanup partially created resources
61
+ if (this._connection) {
62
+ try {
63
+ await this._connection.close();
64
+ } catch (_) {
65
+ /* ignore */
66
+ }
67
+ this._connection = null;
68
+ }
69
+ throw err;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Disconnects: closes channel and connection.
75
+ * @returns {Promise<void>}
76
+ */
77
+ async disconnect() {
78
+ try {
79
+ if (this._channel) {
80
+ await this._channel.close();
81
+ this._channel = null;
82
+ }
83
+ } catch (err) {
84
+ this.emit('error', err);
85
+ }
86
+ try {
87
+ if (this._connection) {
88
+ await this._connection.close();
89
+ this._connection = null;
90
+ }
91
+ } catch (err) {
92
+ this.emit('error', err);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Publishes a message buffer to the specified queue (or default queue) or exchange.
98
+ * @param {string} queue - Target queue name.
99
+ * @param {Buffer} buffer - Message payload as Buffer.
100
+ * @param {Object} [options] - Overrides: routingKey, persistent, headers.
101
+ * @returns {Promise<void>}
102
+ * @throws {Error} If publish fails or channel is not available.
103
+ */
104
+ async publish(queue, buffer, options = {}) {
105
+ if (!this._channel) {
106
+ throw new Error('Cannot publish: channel is not initialized');
107
+ }
108
+
109
+ const exchange = this._config.exchange || '';
110
+ const routingKey = options.routingKey || queue;
111
+ const persistent = options.persistent !== undefined ? options.persistent : this._config.durable;
112
+ const headers = options.headers || {};
113
+
114
+ try {
115
+ // Ensure queue exists if publishing directly to queue and using default exchange
116
+ if (!exchange) {
117
+ await this._channel.assertQueue(queue, { durable: this._config.durable });
118
+ this._channel.sendToQueue(queue, buffer, { persistent, headers, routingKey });
119
+ } else {
120
+ // If exchange is specified, assert exchange and publish to it
121
+ await this._channel.assertExchange(exchange, 'direct', { durable: this._config.durable });
122
+ this._channel.publish(exchange, routingKey, buffer, { persistent, headers });
123
+ }
124
+ // Wait for confirmation
125
+ await this._channel.waitForConfirms();
126
+ } catch (err) {
127
+ this.emit('error', err);
128
+ throw err;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Starts consuming messages from the specified queue.
134
+ * @param {string} queue - Queue name to consume from.
135
+ * @param {function(Object): Promise<void>} onMessage - Async handler receiving raw msg.
136
+ * @param {Object} [options] - Overrides: prefetch, noAck.
137
+ * @returns {Promise<void>}
138
+ * @throws {Error} If consume setup fails or channel is not available.
139
+ */
140
+ async consume(queue, onMessage, options = {}) {
141
+ if (!this._channel) {
142
+ throw new Error('Cannot consume: channel is not initialized');
143
+ }
144
+
145
+ const durable = this._config.durable;
146
+ const prefetch = options.prefetch !== undefined ? options.prefetch : this._config.prefetch;
147
+ const noAck = options.noAck !== undefined ? options.noAck : this._config.noAck;
148
+
149
+ try {
150
+ // Ensure queue exists
151
+ await this._channel.assertQueue(queue, { durable });
152
+ // Set prefetch if provided
153
+ if (typeof prefetch === 'number') {
154
+ this._channel.prefetch(prefetch);
155
+ }
156
+
157
+ await this._channel.consume(
158
+ queue,
159
+ async (msg) => {
160
+ if (msg === null) {
161
+ return;
162
+ }
163
+ try {
164
+ await onMessage(msg);
165
+ if (!noAck) {
166
+ this._channel.ack(msg);
167
+ }
168
+ } catch (handlerErr) {
169
+ // Negative acknowledge and requeue by default
170
+ this._channel.nack(msg, false, true);
171
+ }
172
+ },
173
+ { noAck }
174
+ );
175
+ } catch (err) {
176
+ this.emit('error', err);
177
+ throw err;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Acknowledges a message.
183
+ * @param {Object} msg - RabbitMQ message object.
184
+ */
185
+ async ack(msg) {
186
+ if (!this._channel) {
187
+ throw new Error('Cannot ack: channel is not initialized');
188
+ }
189
+ try {
190
+ this._channel.ack(msg);
191
+ } catch (err) {
192
+ this.emit('error', err);
193
+ throw err;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Negative-acknowledges a message.
199
+ * @param {Object} msg - RabbitMQ message object.
200
+ * @param {Object} [options] - { requeue: boolean }.
201
+ */
202
+ async nack(msg, options = {}) {
203
+ if (!this._channel) {
204
+ throw new Error('Cannot nack: channel is not initialized');
205
+ }
206
+ const requeue = options.requeue !== undefined ? options.requeue : true;
207
+ try {
208
+ this._channel.nack(msg, false, requeue);
209
+ } catch (err) {
210
+ this.emit('error', err);
211
+ throw err;
212
+ }
213
+ }
214
+ }
215
+
216
+ module.exports = RabbitMQClient;
@@ -0,0 +1,33 @@
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
+ };
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * errorHandler.js
5
+ *
6
+ * Defines all custom error types used by AgentMQClient and RabbitMQClient.
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
+ };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * logger.js
5
+ *
6
+ * Provides a simple abstraction over console or a custom logger.
7
+ * If the user passes a custom logger object with methods { info, warn, error, debug },
8
+ * these are used; otherwise console.* se použije jako fallback.
9
+ */
10
+
11
+ function createLogger(customLogger) {
12
+ const methods = ['info', 'warn', 'error', 'debug'];
13
+ if (
14
+ customLogger &&
15
+ typeof customLogger === 'object' &&
16
+ methods.every((fn) => typeof customLogger[fn] === 'function')
17
+ ) {
18
+ // Wrap custom logger to ensure consistent signature
19
+ return {
20
+ info: (...args) => customLogger.info(...args),
21
+ warn: (...args) => customLogger.warn(...args),
22
+ error: (...args) => customLogger.error(...args),
23
+ debug: (...args) => customLogger.debug(...args),
24
+ };
25
+ }
26
+
27
+ // Fallback to console
28
+ return {
29
+ info: (...args) => console.log('[INFO]', ...args),
30
+ warn: (...args) => console.warn('[WARN]', ...args),
31
+ error: (...args) => console.error('[ERROR]', ...args),
32
+ debug: (...args) => console.debug('[DEBUG]', ...args),
33
+ };
34
+ }
35
+
36
+ module.exports = {
37
+ createLogger,
38
+ };
@@ -0,0 +1,44 @@
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
+ };