@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,446 @@
1
+ 'use strict';
2
+
3
+ const BaseClient = require('./BaseClient');
4
+ const WorkflowRouter = require('./layers/WorkflowRouter');
5
+ const QueueManager = require('./layers/QueueManager');
6
+ const ForkJoinHandler = require('./layers/ForkJoinHandler');
7
+ const RPCHandler = require('./layers/RPCHandler');
8
+ const RetryHandler = require('./layers/RetryHandler');
9
+
10
+ /**
11
+ * Main MQ client orchestrating all messaging operations
12
+ *
13
+ * @class ConnectorMQClient
14
+ * @extends BaseClient
15
+ *
16
+ * @example <caption>Basic Usage</caption>
17
+ * const mqClient = new ConnectorMQClient({
18
+ * url: 'amqp://localhost:5672',
19
+ * serviceName: 'invoice-service'
20
+ * });
21
+ *
22
+ * await mqClient.connect();
23
+ * await mqClient.publish('invoice.created', { id: 123 });
24
+ *
25
+ * @example <caption>With Workflow</caption>
26
+ * const mqClient = new ConnectorMQClient({
27
+ * url: 'amqp://localhost:5672',
28
+ * serviceName: 'invoice-service',
29
+ * enableRPC: true
30
+ * });
31
+ */
32
+ class ConnectorMQClient extends BaseClient {
33
+ /**
34
+ * Creates a new ConnectorMQClient instance
35
+ *
36
+ * @constructor
37
+ * @param {Object} [config={}] - Configuration options
38
+ * @param {string} config.url - RabbitMQ connection URL
39
+ * @param {string} [config.serviceName='unknown'] - Service name
40
+ * @param {boolean} [config.enableRPC=true] - Enable RPC handler
41
+ * @param {number} [config.prefetchCount=10] - Message prefetch count
42
+ * @param {Object} [config.retry] - Retry configuration
43
+ *
44
+ * @example
45
+ * const mqClient = new ConnectorMQClient({
46
+ * url: 'amqp://user:pass@localhost:5672',
47
+ * serviceName: 'my-service',
48
+ * prefetchCount: 20
49
+ * });
50
+ */
51
+ constructor(config = {}) {
52
+ super(config);
53
+
54
+ // Initialize layers
55
+ this.workflow = new WorkflowRouter(this, config);
56
+ this.queues = new QueueManager(this, config);
57
+ this.retry = new RetryHandler(this, config);
58
+ this.forkJoin = new ForkJoinHandler(this, this.queues, config);
59
+ this.rpc = new RPCHandler(this, this.queues, config);
60
+
61
+ // Service identification
62
+ this.serviceName = config.serviceName || 'unknown';
63
+
64
+ // Track initialization state
65
+ this._initialized = false;
66
+ }
67
+
68
+ /**
69
+ * Connect to RabbitMQ and initialize layers
70
+ *
71
+ * @async
72
+ * @method connect
73
+ * @returns {Promise<boolean>} Connection success
74
+ *
75
+ * @throws {Error} If connection fails
76
+ *
77
+ * @example
78
+ * try {
79
+ * await mqClient.connect();
80
+ * console.log('Connected to RabbitMQ');
81
+ * } catch (error) {
82
+ * console.error('Connection failed:', error);
83
+ * }
84
+ */
85
+ async connect() {
86
+ // Base connection
87
+ await super.connect();
88
+
89
+ // Initialize RPC handler if needed
90
+ if (this._config.enableRPC !== false) {
91
+ await this.rpc.initialize();
92
+ }
93
+
94
+ // Setup service queues if service name provided
95
+ if (this.serviceName !== 'unknown') {
96
+ try {
97
+ await this.queues.setupServiceQueues(this.serviceName);
98
+ } catch (error) {
99
+ console.warn(`Failed to setup service queues: ${error.message}`);
100
+ }
101
+ }
102
+
103
+ this._initialized = true;
104
+ return true;
105
+ }
106
+
107
+ /**
108
+ * Disconnect from RabbitMQ with cleanup
109
+ *
110
+ * @async
111
+ * @method disconnect
112
+ * @returns {Promise<void>}
113
+ *
114
+ * @example
115
+ * await mqClient.disconnect();
116
+ */
117
+ async disconnect() {
118
+ if (this._initialized) {
119
+ // Cleanup layers
120
+ await this.forkJoin.cleanupAll();
121
+ await this.rpc.cleanup();
122
+ await this.queues.cleanup();
123
+ }
124
+
125
+ // Base disconnect
126
+ await super.disconnect();
127
+
128
+ this._initialized = false;
129
+ }
130
+
131
+ // ============================================
132
+ // Workflow Operations (via WorkflowRouter)
133
+ // ============================================
134
+
135
+ /**
136
+ * Publish workflow to initialization queue
137
+ *
138
+ * @async
139
+ * @method publishWorkflowInit
140
+ * @param {Object} workflow - Workflow definition
141
+ * @param {Object} [options] - Publishing options
142
+ * @returns {Promise<boolean>} Publish success
143
+ *
144
+ * @example
145
+ * await mqClient.publishWorkflowInit({
146
+ * cookbook: cookbookDef,
147
+ * context: { invoiceId: 123 }
148
+ * });
149
+ */
150
+ async publishWorkflowInit(workflow, options) {
151
+ return this.workflow.publishWorkflowInit(workflow, options);
152
+ }
153
+
154
+ /**
155
+ * Publish message to service workflow queue
156
+ *
157
+ * @async
158
+ * @method publishToServiceWorkflow
159
+ * @param {string} serviceName - Target service name
160
+ * @param {Object} message - Message to send
161
+ * @param {Object} [options] - Publishing options
162
+ * @returns {Promise<boolean>} Publish success
163
+ *
164
+ * @example
165
+ * await mqClient.publishToServiceWorkflow(
166
+ * 'invoice-service',
167
+ * { action: 'process', data: {...} }
168
+ * );
169
+ */
170
+ async publishToServiceWorkflow(serviceName, message, options) {
171
+ return this.workflow.publishToServiceWorkflow(serviceName, message, options);
172
+ }
173
+
174
+ /**
175
+ * Publish message directly to service queue
176
+ *
177
+ * @async
178
+ * @method publishToService
179
+ * @param {string} serviceName - Target service name
180
+ * @param {Object} message - Message to send
181
+ * @param {Object} [options] - Publishing options
182
+ * @returns {Promise<boolean>} Publish success
183
+ *
184
+ * @example
185
+ * await mqClient.publishToService(
186
+ * 'email-service',
187
+ * { template: 'invoice', data: {...} }
188
+ * );
189
+ */
190
+ async publishToService(serviceName, message, options) {
191
+ return this.workflow.publishToService(serviceName, message, options);
192
+ }
193
+
194
+ /**
195
+ * Publish workflow completed
196
+ */
197
+ async publishWorkflowCompleted(result, options) {
198
+ return this.workflow.publishWorkflowCompleted(result, options);
199
+ }
200
+
201
+ /**
202
+ * Route to next service in workflow
203
+ */
204
+ async routeToNextService(workflow, nextService, stepResult) {
205
+ return this.workflow.routeToNextService(workflow, nextService, stepResult);
206
+ }
207
+
208
+ /**
209
+ * Consume from workflow init queue
210
+ */
211
+ async consumeWorkflowInit(handler, options) {
212
+ return this.workflow.consumeWorkflowInit(handler, options);
213
+ }
214
+
215
+ /**
216
+ * Consume from service workflow queue
217
+ */
218
+ async consumeServiceWorkflow(serviceName, handler, options) {
219
+ return this.workflow.consumeServiceWorkflow(serviceName, handler, options);
220
+ }
221
+
222
+ // ============================================
223
+ // Queue Management (via QueueManager)
224
+ // ============================================
225
+
226
+ /**
227
+ * Ensure queue exists with configuration
228
+ */
229
+ async ensureQueue(queueName, options) {
230
+ return this.queues.ensureQueue(queueName, options);
231
+ }
232
+
233
+ /**
234
+ * Setup service queues
235
+ */
236
+ async setupServiceQueues(serviceName, options) {
237
+ return this.queues.setupServiceQueues(serviceName || this.serviceName, options);
238
+ }
239
+
240
+ /**
241
+ * Create temporary queue
242
+ */
243
+ async createTemporaryQueue(prefix, options) {
244
+ return this.queues.createTemporaryQueue(prefix, options);
245
+ }
246
+
247
+ /**
248
+ * Get queue statistics
249
+ */
250
+ async getQueueStats(queueName) {
251
+ return this.queues.getQueueStats(queueName);
252
+ }
253
+
254
+ // ============================================
255
+ // Fork-Join Operations (via ForkJoinHandler)
256
+ // ============================================
257
+
258
+ /**
259
+ * Execute fork-join pattern
260
+ */
261
+ async forkJoin(branches, joinStrategy, options) {
262
+ return this.forkJoin.forkJoin(branches, joinStrategy, options);
263
+ }
264
+
265
+ /**
266
+ * Fork work to multiple queues
267
+ */
268
+ async fork(branches, context) {
269
+ return this.forkJoin.fork(branches, context);
270
+ }
271
+
272
+ /**
273
+ * Create accumulator for collecting results
274
+ */
275
+ async createAccumulator(workflowId, stepId, expectedCount) {
276
+ return this.forkJoin.createAccumulator(workflowId, stepId, expectedCount);
277
+ }
278
+
279
+ /**
280
+ * Join results with strategy
281
+ */
282
+ async join(queueName, joinStrategy, options) {
283
+ return this.forkJoin.join(queueName, joinStrategy, options);
284
+ }
285
+
286
+ // ============================================
287
+ // RPC Operations (via RPCHandler)
288
+ // ============================================
289
+
290
+ /**
291
+ * Make RPC call
292
+ */
293
+ async rpcCall(targetQueue, request, options) {
294
+ return this.rpc.call(targetQueue, request, options);
295
+ }
296
+
297
+ /**
298
+ * Setup RPC server
299
+ */
300
+ async rpcServe(queue, handler, options) {
301
+ return this.rpc.serve(queue, handler, options);
302
+ }
303
+
304
+ /**
305
+ * Make multiple RPC calls
306
+ */
307
+ async rpcCallMany(requests) {
308
+ return this.rpc.callMany(requests);
309
+ }
310
+
311
+ /**
312
+ * RPC with retry
313
+ */
314
+ async rpcCallWithRetry(targetQueue, request, options) {
315
+ return this.rpc.callWithRetry(targetQueue, request, options);
316
+ }
317
+
318
+ // ============================================
319
+ // Retry Operations (via RetryHandler)
320
+ // ============================================
321
+
322
+ /**
323
+ * Consume with automatic retry
324
+ */
325
+ async consumeWithRetry(queue, handler, options) {
326
+ return this.retry.consumeWithRetry(queue, handler, options);
327
+ }
328
+
329
+ /**
330
+ * Process message with retry logic
331
+ */
332
+ async processWithRetry(handler, message, rawMsg, options) {
333
+ return this.retry.processWithRetry(handler, message, rawMsg, options);
334
+ }
335
+
336
+ /**
337
+ * Send message to DLQ
338
+ */
339
+ async publishToDLQ(originalQueue, message, error, options) {
340
+ return this.retry.sendToDLQ(message, originalQueue, error, options);
341
+ }
342
+
343
+ /**
344
+ * Process DLQ messages
345
+ */
346
+ async processDLQ(dlqName, handler, options) {
347
+ return this.retry.processDLQ(dlqName, handler, options);
348
+ }
349
+
350
+ /**
351
+ * Get retry statistics
352
+ */
353
+ getRetryStats() {
354
+ return this.retry.getStats();
355
+ }
356
+
357
+ // ============================================
358
+ // Convenience Methods
359
+ // ============================================
360
+
361
+ /**
362
+ * Publish with routing (decides between workflow/service/direct)
363
+ */
364
+ async publishWithRouting(message, options = {}) {
365
+ if (options.workflow) {
366
+ return this.publishWorkflowInit(message, options);
367
+ } else if (options.service) {
368
+ return this.publishToService(options.service, message, options);
369
+ } else if (options.rpc) {
370
+ return this.rpcCall(options.rpc, message, options);
371
+ } else {
372
+ return this.publish(message, options);
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Smart consume (with retry and error handling)
378
+ */
379
+ async smartConsume(queue, handler, options = {}) {
380
+ const wrappedHandler = async (message, rawMsg) => {
381
+ // Add context
382
+ message._context = {
383
+ queue,
384
+ service: this.serviceName,
385
+ receivedAt: Date.now()
386
+ };
387
+
388
+ // Process with retry if enabled
389
+ if (options.retry !== false) {
390
+ return this.processWithRetry(handler, message, rawMsg, options);
391
+ } else {
392
+ return handler(message, rawMsg);
393
+ }
394
+ };
395
+
396
+ return this.consume(wrappedHandler, { queue, ...options });
397
+ }
398
+
399
+ /**
400
+ * Get overall health status
401
+ */
402
+ async getHealth() {
403
+ const health = {
404
+ connected: this.isConnected(),
405
+ initialized: this._initialized,
406
+ service: this.serviceName,
407
+ layers: {
408
+ workflow: true,
409
+ queues: true,
410
+ retry: true,
411
+ forkJoin: true,
412
+ rpc: this.rpc.getPendingCount() >= 0
413
+ },
414
+ stats: {
415
+ retry: this.retry.getStats(),
416
+ pendingRPC: this.rpc.getPendingCount(),
417
+ managedQueues: this.queues.getManagedQueues().length
418
+ }
419
+ };
420
+
421
+ // Try to get queue stats if connected
422
+ if (this.isConnected() && this.serviceName !== 'unknown') {
423
+ try {
424
+ health.queueStats = await this.getQueueStats(`${this.serviceName}.queue`);
425
+ } catch (error) {
426
+ health.queueStats = { error: error.message };
427
+ }
428
+ }
429
+
430
+ return health;
431
+ }
432
+
433
+ /**
434
+ * Static factory method for quick setup
435
+ */
436
+ static async create(config) {
437
+ const client = new ConnectorMQClient(config);
438
+ await client.connect();
439
+ return client;
440
+ }
441
+ }
442
+
443
+ // Export join strategies for convenience
444
+ ConnectorMQClient.JoinStrategies = ForkJoinHandler.JoinStrategies;
445
+
446
+ module.exports = ConnectorMQClient;
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * configSchema.js
5
+ *
6
+ * JSON Schema used by Ajv to validate the user-supplied configuration object.
7
+ * Ensures required fields are present and correctly typed. Any unsupported
8
+ * or misspelled field will trigger a ValidationError.
9
+ */
10
+
11
+ module.exports = {
12
+ type: 'object',
13
+ properties: {
14
+ type: {
15
+ type: 'string',
16
+ enum: ['rabbitmq'],
17
+ },
18
+ host: {
19
+ type: 'string',
20
+ minLength: 1,
21
+ },
22
+ // For RabbitMQ: queue is required.
23
+ queue: {
24
+ type: 'string',
25
+ minLength: 1,
26
+ },
27
+ exchange: {
28
+ type: 'string',
29
+ },
30
+ durable: {
31
+ type: 'boolean',
32
+ },
33
+ prefetch: {
34
+ type: 'integer',
35
+ minimum: 0,
36
+ },
37
+ noAck: {
38
+ type: 'boolean',
39
+ },
40
+ logger: {
41
+ type: 'object',
42
+ description: 'Custom logger with methods: info, warn, error, debug',
43
+ },
44
+ retryPolicy: {
45
+ type: 'object',
46
+ properties: {
47
+ retries: {
48
+ type: 'integer',
49
+ minimum: 0,
50
+ },
51
+ initialDelayMs: {
52
+ type: 'integer',
53
+ minimum: 0,
54
+ },
55
+ maxDelayMs: {
56
+ type: 'integer',
57
+ minimum: 0,
58
+ },
59
+ factor: {
60
+ type: 'number',
61
+ minimum: 1,
62
+ },
63
+ },
64
+ required: ['retries', 'initialDelayMs', 'maxDelayMs', 'factor'],
65
+ additionalProperties: false,
66
+ },
67
+ },
68
+ required: ['type', 'host', 'queue'],
69
+ additionalProperties: false,
70
+ };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * defaultConfig.js
5
+ *
6
+ * Provides default configuration values for AgentMQClient.
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
+ queue: '',
20
+
21
+ // Default exchange name (empty string → default direct exchange).
22
+ exchange: '',
23
+
24
+ // Declare queues/exchanges as durable by default.
25
+ durable: true,
26
+
27
+ // Default prefetch count for consumers.
28
+ prefetch: 1,
29
+
30
+ // Default auto-acknowledge setting for consumers.
31
+ noAck: false,
32
+
33
+ // Custom logger object (if not provided, console.* will be used).
34
+ // Expected interface: { info(), warn(), error(), debug() }.
35
+ logger: null,
36
+
37
+ // Retry policy for reconnect attempts (not fully implemented in current version).
38
+ // - retries: maximum number of reconnection attempts
39
+ // - initialDelayMs: starting backoff delay
40
+ // - maxDelayMs: maximum backoff delay
41
+ // - factor: exponential backoff multiplier
42
+ retryPolicy: {
43
+ retries: 5,
44
+ initialDelayMs: 1000,
45
+ maxDelayMs: 30000,
46
+ factor: 2,
47
+ },
48
+ };
package/src/index.js ADDED
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module @onlineapps/conn-infra-mq
5
+ * @description RabbitMQ connector with workflow routing, fork-join, RPC, and retry patterns for OA Drive.
6
+ * Provides layered architecture for complex messaging patterns.
7
+ *
8
+ * @see {@link https://github.com/onlineapps/oa-drive/tree/main/shared/connector/conn-infra-mq|GitHub Repository}
9
+ * @author OA Drive Team
10
+ * @license MIT
11
+ * @since 1.0.0
12
+ */
13
+
14
+ const ConnectorMQClient = require('./ConnectorMQClient');
15
+ const BaseClient = require('./BaseClient');
16
+
17
+ // Layers - exported for advanced usage
18
+ const WorkflowRouter = require('./layers/WorkflowRouter');
19
+ const QueueManager = require('./layers/QueueManager');
20
+ const ForkJoinHandler = require('./layers/ForkJoinHandler');
21
+ const RPCHandler = require('./layers/RPCHandler');
22
+ const RetryHandler = require('./layers/RetryHandler');
23
+
24
+ // Default export - the main client
25
+ module.exports = ConnectorMQClient;
26
+
27
+ // Named exports
28
+ module.exports.ConnectorMQClient = ConnectorMQClient;
29
+ module.exports.BaseClient = BaseClient;
30
+
31
+ // Export layers for advanced usage
32
+ module.exports.layers = {
33
+ WorkflowRouter,
34
+ QueueManager,
35
+ ForkJoinHandler,
36
+ RPCHandler,
37
+ RetryHandler
38
+ };
39
+
40
+ // Backwards compatibility - MQWrapper points to ConnectorMQClient
41
+ module.exports.MQWrapper = ConnectorMQClient;
42
+
43
+ // Export join strategies
44
+ module.exports.JoinStrategies = ForkJoinHandler.JoinStrategies;
45
+
46
+ /**
47
+ * Factory function to create MQ client instance
48
+ *
49
+ * @function create
50
+ * @param {Object} config - Configuration object
51
+ * @returns {ConnectorMQClient} New MQ client instance
52
+ *
53
+ * @example
54
+ * const mqClient = create({
55
+ * url: 'amqp://localhost:5672',
56
+ * serviceName: 'invoice-service'
57
+ * });
58
+ */
59
+ module.exports.create = ConnectorMQClient.create;
60
+
61
+ /**
62
+ * Current version
63
+ * @constant {string}
64
+ */
65
+ module.exports.VERSION = '1.0.0';