@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,312 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ForkJoinHandler - Manages fork-join parallel processing patterns
5
+ * Handles accumulator queues and result collection from parallel branches
6
+ */
7
+ class ForkJoinHandler {
8
+ constructor(mqClient, queueManager, config = {}) {
9
+ this.client = mqClient;
10
+ this.queueManager = queueManager;
11
+ this.config = {
12
+ defaultTimeout: config.defaultTimeout || 30000,
13
+ accumulatorPrefix: config.accumulatorPrefix || 'fork',
14
+ ...config
15
+ };
16
+ this.activeAccumulators = new Map();
17
+ }
18
+
19
+ /**
20
+ * Create accumulator queue for collecting fork-join results
21
+ * @param {string} workflowId - Workflow identifier
22
+ * @param {string} stepId - Step identifier
23
+ * @param {number} expectedCount - Number of expected results
24
+ */
25
+ async createAccumulator(workflowId, stepId, expectedCount) {
26
+ const queueName = `${this.config.accumulatorPrefix}.${workflowId}.${stepId}.accumulator`;
27
+
28
+ // Create temporary queue for accumulation
29
+ await this.queueManager.ensureQueue(queueName, {
30
+ durable: false,
31
+ autoDelete: true,
32
+ expires: this.config.defaultTimeout * 2, // Double the timeout for safety
33
+ exclusive: false
34
+ });
35
+
36
+ // Track accumulator
37
+ this.activeAccumulators.set(queueName, {
38
+ workflowId,
39
+ stepId,
40
+ expectedCount,
41
+ collectedCount: 0,
42
+ results: [],
43
+ createdAt: Date.now()
44
+ });
45
+
46
+ return queueName;
47
+ }
48
+
49
+ /**
50
+ * Fork - distribute work to multiple queues
51
+ * @param {Array} branches - Array of { queue, message } objects
52
+ * @param {Object} context - Common context for all branches
53
+ */
54
+ async fork(branches, context = {}) {
55
+ const forkId = `fork-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
56
+ const promises = [];
57
+
58
+ for (let i = 0; i < branches.length; i++) {
59
+ const branch = branches[i];
60
+ const message = {
61
+ ...branch.message,
62
+ _fork: {
63
+ id: forkId,
64
+ branch: i,
65
+ total: branches.length,
66
+ context
67
+ }
68
+ };
69
+
70
+ promises.push(
71
+ this.client.publish(message, {
72
+ queue: branch.queue,
73
+ ...branch.options
74
+ })
75
+ );
76
+ }
77
+
78
+ await Promise.all(promises);
79
+ return forkId;
80
+ }
81
+
82
+ /**
83
+ * Collect results from accumulator queue
84
+ * @param {string} queueName - Accumulator queue name
85
+ * @param {Function} onComplete - Callback when all results collected
86
+ * @param {Object} options - Collection options
87
+ */
88
+ async collectResults(queueName, onComplete, options = {}) {
89
+ const accumulator = this.activeAccumulators.get(queueName);
90
+ if (!accumulator) {
91
+ throw new Error(`Accumulator ${queueName} not found`);
92
+ }
93
+
94
+ const timeout = options.timeout || this.config.defaultTimeout;
95
+ const timeoutHandle = setTimeout(() => {
96
+ this.handleTimeout(queueName, onComplete);
97
+ }, timeout);
98
+
99
+ // Consume with prefetch 1 for atomic result collection
100
+ await this.client.consume(async (result, rawMsg) => {
101
+ accumulator.results.push(result);
102
+ accumulator.collectedCount++;
103
+
104
+ // Acknowledge immediately
105
+ await this.client.ack(rawMsg);
106
+
107
+ // Check if all results collected
108
+ if (accumulator.collectedCount >= accumulator.expectedCount) {
109
+ clearTimeout(timeoutHandle);
110
+ await this.handleComplete(queueName, onComplete);
111
+ }
112
+ }, {
113
+ queue: queueName,
114
+ prefetch: 1,
115
+ noAck: false
116
+ });
117
+
118
+ return {
119
+ queueName,
120
+ expectedCount: accumulator.expectedCount
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Join - wait for all results and combine them
126
+ * @param {string} queueName - Accumulator queue name
127
+ * @param {Function} joinStrategy - Function to combine results
128
+ * @param {Object} options - Join options
129
+ */
130
+ async join(queueName, joinStrategy, options = {}) {
131
+ return new Promise((resolve, reject) => {
132
+ const timeout = options.timeout || this.config.defaultTimeout;
133
+
134
+ this.collectResults(queueName, async (error, results) => {
135
+ if (error) {
136
+ reject(error);
137
+ return;
138
+ }
139
+
140
+ try {
141
+ const joined = await joinStrategy(results);
142
+ resolve(joined);
143
+ } catch (err) {
144
+ reject(err);
145
+ }
146
+ }, { timeout });
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Fork-join convenience method
152
+ * @param {Array} branches - Branches to fork to
153
+ * @param {Function} joinStrategy - How to combine results
154
+ * @param {Object} options - Fork-join options
155
+ */
156
+ async forkJoin(branches, joinStrategy, options = {}) {
157
+ const workflowId = options.workflowId || `wf-${Date.now()}`;
158
+ const stepId = options.stepId || 'step-1';
159
+
160
+ // Create accumulator
161
+ const accumulatorQueue = await this.createAccumulator(
162
+ workflowId,
163
+ stepId,
164
+ branches.length
165
+ );
166
+
167
+ // Modify branches to send results to accumulator
168
+ const modifiedBranches = branches.map(branch => ({
169
+ ...branch,
170
+ message: {
171
+ ...branch.message,
172
+ _replyTo: accumulatorQueue
173
+ }
174
+ }));
175
+
176
+ // Fork work
177
+ const forkId = await this.fork(modifiedBranches);
178
+
179
+ // Join results
180
+ const result = await this.join(accumulatorQueue, joinStrategy, options);
181
+
182
+ // Cleanup
183
+ await this.cleanupAccumulator(accumulatorQueue);
184
+
185
+ return {
186
+ forkId,
187
+ result,
188
+ branchCount: branches.length
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Handle timeout for accumulator
194
+ * @private
195
+ */
196
+ handleTimeout(queueName, callback) {
197
+ const accumulator = this.activeAccumulators.get(queueName);
198
+ if (accumulator) {
199
+ const error = new Error(
200
+ `Timeout waiting for fork-join results. Received ${accumulator.collectedCount}/${accumulator.expectedCount}`
201
+ );
202
+ error.partialResults = accumulator.results;
203
+ callback(error, null);
204
+ this.cleanupAccumulator(queueName).catch(() => {});
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Handle completion of result collection
210
+ * @private
211
+ */
212
+ async handleComplete(queueName, callback) {
213
+ const accumulator = this.activeAccumulators.get(queueName);
214
+ if (accumulator) {
215
+ callback(null, accumulator.results);
216
+ await this.cleanupAccumulator(queueName);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Cleanup accumulator queue and tracking
222
+ * @param {string} queueName - Accumulator queue name
223
+ */
224
+ async cleanupAccumulator(queueName) {
225
+ this.activeAccumulators.delete(queueName);
226
+
227
+ try {
228
+ await this.queueManager.deleteQueue(queueName, { ifEmpty: true });
229
+ } catch (error) {
230
+ // Queue might already be deleted or have messages
231
+ console.warn(`Failed to delete accumulator queue ${queueName}:`, error.message);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Built-in join strategies
237
+ */
238
+ static JoinStrategies = {
239
+ /**
240
+ * Merge all results into single object
241
+ */
242
+ merge: (results) => {
243
+ return results.reduce((acc, result) => ({ ...acc, ...result }), {});
244
+ },
245
+
246
+ /**
247
+ * Concatenate array results
248
+ */
249
+ concat: (results) => {
250
+ return results.reduce((acc, result) => {
251
+ if (Array.isArray(result)) {
252
+ return acc.concat(result);
253
+ }
254
+ acc.push(result);
255
+ return acc;
256
+ }, []);
257
+ },
258
+
259
+ /**
260
+ * Return first result
261
+ */
262
+ first: (results) => results[0],
263
+
264
+ /**
265
+ * Return last result
266
+ */
267
+ last: (results) => results[results.length - 1],
268
+
269
+ /**
270
+ * Return all results as array
271
+ */
272
+ all: (results) => results,
273
+
274
+ /**
275
+ * Sum numeric results
276
+ */
277
+ sum: (results) => {
278
+ return results.reduce((acc, result) => {
279
+ if (typeof result === 'number') {
280
+ return acc + result;
281
+ }
282
+ if (result && typeof result.value === 'number') {
283
+ return acc + result.value;
284
+ }
285
+ return acc;
286
+ }, 0);
287
+ }
288
+ };
289
+
290
+ /**
291
+ * Get active accumulators
292
+ */
293
+ getActiveAccumulators() {
294
+ return Array.from(this.activeAccumulators.entries()).map(([queue, data]) => ({
295
+ queue,
296
+ ...data
297
+ }));
298
+ }
299
+
300
+ /**
301
+ * Cleanup all active accumulators
302
+ */
303
+ async cleanupAll() {
304
+ const promises = [];
305
+ for (const queueName of this.activeAccumulators.keys()) {
306
+ promises.push(this.cleanupAccumulator(queueName));
307
+ }
308
+ await Promise.all(promises);
309
+ }
310
+ }
311
+
312
+ module.exports = ForkJoinHandler;
@@ -0,0 +1,263 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * QueueManager - Manages queue creation, configuration, and lifecycle
5
+ * Handles TTL, DLQ, auto-delete, and other queue properties
6
+ */
7
+ class QueueManager {
8
+ constructor(mqClient, config = {}) {
9
+ this.client = mqClient;
10
+ this.config = {
11
+ defaultTTL: config.defaultTTL || 30000,
12
+ dlxExchange: config.dlxExchange || 'dlx',
13
+ autoDeleteTimeout: config.autoDeleteTimeout || 300000, // 5 minutes
14
+ maxRetries: config.maxRetries || 3,
15
+ ...config
16
+ };
17
+ this.managedQueues = new Set();
18
+ }
19
+
20
+ /**
21
+ * Create or ensure queue exists with specific configuration
22
+ * @param {string} queueName - Name of the queue
23
+ * @param {Object} options - Queue configuration options
24
+ */
25
+ async ensureQueue(queueName, options = {}) {
26
+ // Get channel from client's transport
27
+ const transport = this.client._transport;
28
+ if (!transport || !transport.channel) {
29
+ throw new Error('MQ client not connected');
30
+ }
31
+ const channel = transport.channel;
32
+
33
+ const queueOptions = {
34
+ durable: options.durable !== false,
35
+ arguments: {
36
+ ...options.arguments
37
+ }
38
+ };
39
+
40
+ // Add TTL if specified
41
+ if (options.ttl !== null && options.ttl !== undefined) {
42
+ queueOptions.arguments['x-message-ttl'] = options.ttl || this.config.defaultTTL;
43
+ }
44
+
45
+ // Add DLQ configuration
46
+ if (options.dlq !== false) {
47
+ queueOptions.arguments['x-dead-letter-exchange'] = options.dlxExchange || this.config.dlxExchange;
48
+ queueOptions.arguments['x-dead-letter-routing-key'] = options.dlqKey || `${queueName}.dlq`;
49
+ }
50
+
51
+ // Add auto-delete expiry
52
+ if (options.autoDelete === true) {
53
+ queueOptions.arguments['x-expires'] = options.expires || this.config.autoDeleteTimeout;
54
+ }
55
+
56
+ // Add max length if specified
57
+ if (options.maxLength) {
58
+ queueOptions.arguments['x-max-length'] = options.maxLength;
59
+ }
60
+
61
+ // Add priority support
62
+ if (options.maxPriority) {
63
+ queueOptions.arguments['x-max-priority'] = options.maxPriority;
64
+ }
65
+
66
+ // Track managed queue
67
+ this.managedQueues.add(queueName);
68
+
69
+ // Assert the queue
70
+ return channel.assertQueue(queueName, queueOptions);
71
+ }
72
+
73
+ /**
74
+ * Setup service queues (main + DLQ + optional workflow queue)
75
+ * @param {string} serviceName - Name of the service
76
+ * @param {Object} options - Configuration options
77
+ */
78
+ async setupServiceQueues(serviceName, options = {}) {
79
+ const transport = this.client._transport;
80
+ if (!transport || !transport.channel) {
81
+ throw new Error('MQ client not connected');
82
+ }
83
+ const channel = transport.channel;
84
+
85
+ const queues = {};
86
+
87
+ // Create main processing queue
88
+ await this.ensureQueue(`${serviceName}.queue`, {
89
+ ttl: options.ttl || this.config.defaultTTL,
90
+ dlq: true,
91
+ durable: true,
92
+ maxRetries: this.config.maxRetries
93
+ });
94
+ queues.main = `${serviceName}.queue`;
95
+
96
+ // Create dead letter queue
97
+ await this.ensureQueue(`${serviceName}.dlq`, {
98
+ ttl: null, // No TTL for DLQ
99
+ dlq: false,
100
+ durable: true,
101
+ autoDelete: false
102
+ });
103
+ queues.dlq = `${serviceName}.dlq`;
104
+
105
+ // Create workflow queue if requested
106
+ if (options.includeWorkflow !== false) {
107
+ await this.ensureQueue(`${serviceName}.workflow`, {
108
+ ttl: options.workflowTTL || this.config.defaultTTL,
109
+ dlq: true,
110
+ durable: true
111
+ });
112
+ queues.workflow = `${serviceName}.workflow`;
113
+ }
114
+
115
+ // Setup DLQ exchange and bindings
116
+ await channel.assertExchange(this.config.dlxExchange, 'direct', {
117
+ durable: true
118
+ });
119
+
120
+ // Bind DLQ
121
+ await channel.bindQueue(
122
+ queues.dlq,
123
+ this.config.dlxExchange,
124
+ `${serviceName}.queue.dlq`
125
+ );
126
+
127
+ if (queues.workflow) {
128
+ await channel.bindQueue(
129
+ queues.dlq,
130
+ this.config.dlxExchange,
131
+ `${serviceName}.workflow.dlq`
132
+ );
133
+ }
134
+
135
+ return queues;
136
+ }
137
+
138
+ /**
139
+ * Create temporary queue for short-lived operations
140
+ * @param {string} prefix - Queue name prefix
141
+ * @param {Object} options - Queue options
142
+ */
143
+ async createTemporaryQueue(prefix = 'temp', options = {}) {
144
+ const queueName = `${prefix}.${Date.now()}.${Math.random().toString(36).substr(2, 9)}`;
145
+
146
+ await this.ensureQueue(queueName, {
147
+ durable: false,
148
+ autoDelete: true,
149
+ expires: options.expires || 60000, // 1 minute default
150
+ exclusive: options.exclusive || false,
151
+ ...options
152
+ });
153
+
154
+ return queueName;
155
+ }
156
+
157
+ /**
158
+ * Delete a queue
159
+ * @param {string} queueName - Name of the queue to delete
160
+ * @param {Object} options - Delete options
161
+ */
162
+ async deleteQueue(queueName, options = {}) {
163
+ const transport = this.client._transport;
164
+ if (!transport || !transport.channel) {
165
+ throw new Error('MQ client not connected');
166
+ }
167
+ const channel = transport.channel;
168
+
169
+ this.managedQueues.delete(queueName);
170
+ return channel.deleteQueue(queueName, options);
171
+ }
172
+
173
+ /**
174
+ * Purge all messages from a queue
175
+ * @param {string} queueName - Name of the queue to purge
176
+ */
177
+ async purgeQueue(queueName) {
178
+ const transport = this.client._transport;
179
+ if (!transport || !transport.channel) {
180
+ throw new Error('MQ client not connected');
181
+ }
182
+ const channel = transport.channel;
183
+
184
+ return channel.purgeQueue(queueName);
185
+ }
186
+
187
+ /**
188
+ * Get queue statistics
189
+ * @param {string} queueName - Name of the queue
190
+ */
191
+ async getQueueStats(queueName) {
192
+ const transport = this.client._transport;
193
+ if (!transport || !transport.channel) {
194
+ throw new Error('MQ client not connected');
195
+ }
196
+ const channel = transport.channel;
197
+
198
+ const result = await channel.checkQueue(queueName);
199
+ return {
200
+ name: result.queue,
201
+ messageCount: result.messageCount,
202
+ consumerCount: result.consumerCount
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Setup exchange
208
+ * @param {string} exchangeName - Name of the exchange
209
+ * @param {string} type - Exchange type (direct, topic, fanout, headers)
210
+ * @param {Object} options - Exchange options
211
+ */
212
+ async setupExchange(exchangeName, type = 'direct', options = {}) {
213
+ const transport = this.client._transport;
214
+ if (!transport || !transport.channel) {
215
+ throw new Error('MQ client not connected');
216
+ }
217
+ const channel = transport.channel;
218
+
219
+ return channel.assertExchange(exchangeName, type, {
220
+ durable: options.durable !== false,
221
+ ...options
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Bind queue to exchange
227
+ * @param {string} queue - Queue name
228
+ * @param {string} exchange - Exchange name
229
+ * @param {string} routingKey - Routing key for binding
230
+ */
231
+ async bindQueue(queue, exchange, routingKey = '') {
232
+ const transport = this.client._transport;
233
+ if (!transport || !transport.channel) {
234
+ throw new Error('MQ client not connected');
235
+ }
236
+ const channel = transport.channel;
237
+
238
+ return channel.bindQueue(queue, exchange, routingKey);
239
+ }
240
+
241
+ /**
242
+ * Cleanup all managed queues
243
+ */
244
+ async cleanup() {
245
+ const promises = [];
246
+ for (const queueName of this.managedQueues) {
247
+ if (queueName.startsWith('temp.')) {
248
+ promises.push(this.deleteQueue(queueName).catch(() => {}));
249
+ }
250
+ }
251
+ await Promise.all(promises);
252
+ this.managedQueues.clear();
253
+ }
254
+
255
+ /**
256
+ * Get list of managed queues
257
+ */
258
+ getManagedQueues() {
259
+ return Array.from(this.managedQueues);
260
+ }
261
+ }
262
+
263
+ module.exports = QueueManager;