@onlineapps/conn-infra-mq 1.1.3 → 1.1.5

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/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # @onlineapps/connector-mq-client
1
+ # @onlineapps/conn-infra-mq
2
2
 
3
- [![Build Status](https://img.shields.io/github/actions/workflow/status/onlineapps/connector-mq-client/nodejs.yml?branch=main)](https://github.com/onlineapps/connector-mq-client/actions)
4
- [![Coverage Status](https://codecov.io/gh/onlineapps/connector-mq-client/branch/main/graph/badge.svg)](https://codecov.io/gh/onlineapps/connector-mq-client)
5
- [![npm version](https://img.shields.io/npm/v/@onlineapps/connector-mq-client)](https://www.npmjs.com/package/@onlineapps/connector-mq-client)
3
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/onlineapps/conn-infra-mq/nodejs.yml?branch=main)](https://github.com/onlineapps/conn-infra-mq/actions)
4
+ [![Coverage Status](https://codecov.io/gh/onlineapps/conn-infra-mq/branch/main/graph/badge.svg)](https://codecov.io/gh/onlineapps/conn-infra-mq)
5
+ [![npm version](https://img.shields.io/npm/v/@onlineapps/conn-infra-mq)](https://www.npmjs.com/package/@onlineapps/conn-infra-mq)
6
6
 
7
7
  > Message queue connector with **layered architecture** for workflow orchestration, RPC, fork-join, and retry patterns. Built on top of RabbitMQ with clean separation of concerns.
8
8
 
@@ -26,9 +26,9 @@
26
26
  ## 📦 Installation
27
27
 
28
28
  ```bash
29
- npm install @onlineapps/connector-mq-client
29
+ npm install @onlineapps/conn-infra-mq
30
30
  # or
31
- yarn add @onlineapps/connector-mq-client
31
+ yarn add @onlineapps/conn-infra-mq
32
32
  ````
33
33
 
34
34
  > Requires Node.js ≥12. For RabbitMQ usage, ensure an accessible AMQP server.
@@ -52,7 +52,7 @@ ConnectorMQClient (main orchestrator)
52
52
  ```js
53
53
  'use strict';
54
54
 
55
- const ConnectorMQClient = require('@onlineapps/connector-mq-client');
55
+ const ConnectorMQClient = require('@onlineapps/conn-infra-mq');
56
56
 
57
57
  (async () => {
58
58
  // 1. Create client with configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-infra-mq",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "A promise-based, broker-agnostic client for sending and receiving messages via RabbitMQ",
5
5
  "main": "src/index.js",
6
6
  "repository": {
@@ -21,9 +21,9 @@
21
21
  "test:integration:required": "REQUIRE_SERVICES=true jest --config=jest.integration.config.js",
22
22
  "test:watch": "jest --watch",
23
23
  "test:coverage": "jest --coverage --coverageReporters=text-lcov html",
24
- "lint": "eslint \"src/**/*.js\" \"test/**/*.js\"",
25
- "prettier:check": "prettier --check \"src/**/*.js\" \"test/**/*.js\"",
26
- "prettier:fix": "prettier --write \"src/**/*.js\" \"test/**/*.js\""
24
+ "lint": "eslint \"src/**/*.js\" \"tests/**/*.js\"",
25
+ "prettier:check": "prettier --check \"src/**/*.js\" \"tests/**/*.js\"",
26
+ "prettier:fix": "prettier --write \"src/**/*.js\" \"tests/**/*.js\""
27
27
  },
28
28
  "keywords": [
29
29
  "connector",
package/src/BaseClient.js CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,269 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * queueConfig.js
5
+ *
6
+ * Central configuration for INFRASTRUCTURE queues only.
7
+ * Business queues ({service}.workflow, {service}.queue, {service}.dlq) are created
8
+ * by individual services via QueueManager.setupServiceQueues().
9
+ *
10
+ * This ensures consistent configuration for infrastructure queues across all services.
11
+ */
12
+
13
+ module.exports = {
14
+ /**
15
+ * Workflow infrastructure queue configurations
16
+ * These are shared infrastructure queues used by all services
17
+ */
18
+ workflow: {
19
+ /**
20
+ * workflow.init - Entry point for all workflows
21
+ * All services listen as competing consumers
22
+ */
23
+ init: {
24
+ durable: true,
25
+ arguments: {
26
+ 'x-message-ttl': 300000, // 5 minutes TTL
27
+ 'x-max-length': 10000 // Max 10k messages
28
+ }
29
+ },
30
+
31
+ /**
32
+ * workflow.completed - Completed workflows
33
+ */
34
+ completed: {
35
+ durable: true,
36
+ arguments: {
37
+ 'x-message-ttl': 300000, // 5 minutes TTL
38
+ 'x-max-length': 10000
39
+ }
40
+ },
41
+
42
+ /**
43
+ * workflow.failed - Failed workflows
44
+ */
45
+ failed: {
46
+ durable: true,
47
+ arguments: {
48
+ 'x-message-ttl': 300000, // 5 minutes TTL
49
+ 'x-max-length': 10000
50
+ }
51
+ },
52
+
53
+ /**
54
+ * workflow.dlq - Dead letter queue for workflows
55
+ */
56
+ dlq: {
57
+ durable: true,
58
+ arguments: {
59
+ // No TTL for DLQ - messages should persist
60
+ 'x-max-length': 50000 // Higher limit for DLQ
61
+ }
62
+ }
63
+ },
64
+
65
+ /**
66
+ * Business queue templates
67
+ * These are templates for service-specific queues ({service}.workflow, {service}.queue, {service}.dlq)
68
+ * Used by QueueManager.setupServiceQueues() to ensure consistent configuration
69
+ */
70
+ business: {
71
+ /**
72
+ * {service}.workflow - Service-specific workflow queue
73
+ * Used for routing workflow steps to specific services
74
+ */
75
+ workflow: {
76
+ durable: true,
77
+ arguments: {
78
+ 'x-message-ttl': 300000, // 5 minutes TTL
79
+ 'x-max-length': 10000, // Max 10k messages
80
+ 'x-dead-letter-exchange': 'dlx',
81
+ 'x-dead-letter-routing-key': '{service}.dlq' // Placeholder, replaced with actual service name
82
+ }
83
+ },
84
+
85
+ /**
86
+ * {service}.queue - Service main processing queue
87
+ * Used for direct service-to-service communication
88
+ */
89
+ queue: {
90
+ durable: true,
91
+ arguments: {
92
+ 'x-message-ttl': 30000, // 30 seconds TTL
93
+ 'x-max-length': 10000, // Max 10k messages
94
+ 'x-dead-letter-exchange': 'dlx',
95
+ 'x-dead-letter-routing-key': '{service}.dlq' // Placeholder, replaced with actual service name
96
+ }
97
+ },
98
+
99
+ /**
100
+ * {service}.dlq - Service dead letter queue
101
+ * Receives failed messages from service.queue and service.workflow
102
+ */
103
+ dlq: {
104
+ durable: true,
105
+ arguments: {
106
+ // No TTL for DLQ - messages should persist
107
+ 'x-max-length': 50000 // Higher limit for DLQ
108
+ }
109
+ }
110
+ },
111
+
112
+ /**
113
+ * Registry infrastructure queue configurations
114
+ */
115
+ registry: {
116
+ /**
117
+ * registry.register - Registration requests
118
+ */
119
+ register: {
120
+ durable: true,
121
+ arguments: {
122
+ 'x-message-ttl': 60000, // 1 minute TTL
123
+ 'x-max-length': 1000
124
+ }
125
+ },
126
+
127
+ /**
128
+ * registry.heartbeats - Service heartbeats
129
+ */
130
+ heartbeats: {
131
+ durable: true,
132
+ arguments: {
133
+ 'x-message-ttl': 120000, // 2 minutes TTL
134
+ 'x-max-length': 5000
135
+ }
136
+ },
137
+
138
+ /**
139
+ * registry.changes - Registry event fanout exchange
140
+ * (This is an exchange, not a queue, but included for reference)
141
+ */
142
+ changes: {
143
+ type: 'exchange',
144
+ durable: true
145
+ }
146
+ },
147
+
148
+ /**
149
+ * Get infrastructure queue configuration by type and name
150
+ * @param {string} type - Queue type: 'workflow', 'registry'
151
+ * @param {string} name - Queue name: 'init', 'completed', 'register', etc.
152
+ * @returns {Object} Queue configuration
153
+ */
154
+ getQueueConfig(type, name) {
155
+ const config = this[type]?.[name];
156
+ if (!config) {
157
+ throw new Error(`Infrastructure queue config not found: ${type}.${name}`);
158
+ }
159
+
160
+ // Deep clone to avoid modifying original
161
+ return JSON.parse(JSON.stringify(config));
162
+ },
163
+
164
+ /**
165
+ * Get workflow infrastructure queue configuration
166
+ * @param {string} queueName - Queue name (e.g., 'workflow.init', 'workflow.completed')
167
+ * @returns {Object} Queue configuration
168
+ */
169
+ getWorkflowQueueConfig(queueName) {
170
+ const parts = queueName.split('.');
171
+ if (parts.length !== 2 || parts[0] !== 'workflow') {
172
+ throw new Error(`Invalid workflow queue name: ${queueName}. Expected format: workflow.{name}`);
173
+ }
174
+ return this.getQueueConfig('workflow', parts[1]);
175
+ },
176
+
177
+ /**
178
+ * Get registry infrastructure queue configuration
179
+ * @param {string} queueName - Queue name (e.g., 'registry.register', 'registry.heartbeats')
180
+ * @returns {Object} Queue configuration
181
+ */
182
+ getRegistryQueueConfig(queueName) {
183
+ const parts = queueName.split('.');
184
+ if (parts.length !== 2 || parts[0] !== 'registry') {
185
+ throw new Error(`Invalid registry queue name: ${queueName}. Expected format: registry.{name}`);
186
+ }
187
+ return this.getQueueConfig('registry', parts[1]);
188
+ },
189
+
190
+ /**
191
+ * Check if queue name is an infrastructure queue
192
+ * @param {string} queueName - Queue name to check
193
+ * @returns {boolean} True if infrastructure queue
194
+ */
195
+ isInfrastructureQueue(queueName) {
196
+ return queueName.startsWith('workflow.') || queueName.startsWith('registry.');
197
+ },
198
+
199
+ /**
200
+ * Check if queue name is a business queue
201
+ * @param {string} queueName - Queue name to check
202
+ * @returns {boolean} True if business queue
203
+ */
204
+ isBusinessQueue(queueName) {
205
+ // Business queues follow pattern: {service}.{type}
206
+ // where type is: workflow, queue, dlq
207
+ const parts = queueName.split('.');
208
+ if (parts.length !== 2) return false;
209
+ return ['workflow', 'queue', 'dlq'].includes(parts[1]);
210
+ },
211
+
212
+ /**
213
+ * Parse business queue name to extract service name and queue type
214
+ * @param {string} queueName - Business queue name (e.g., 'hello-service.workflow')
215
+ * @returns {Object} { serviceName, queueType } or null if not a business queue
216
+ */
217
+ parseBusinessQueue(queueName) {
218
+ if (!this.isBusinessQueue(queueName)) {
219
+ return null;
220
+ }
221
+ const parts = queueName.split('.');
222
+ return {
223
+ serviceName: parts[0],
224
+ queueType: parts[1]
225
+ };
226
+ },
227
+
228
+ /**
229
+ * Get business queue configuration template
230
+ * @param {string} queueType - Queue type: 'workflow', 'queue', 'dlq'
231
+ * @param {string} serviceName - Service name (replaces {service} placeholder)
232
+ * @returns {Object} Queue configuration
233
+ */
234
+ getBusinessQueueConfig(queueType, serviceName) {
235
+ const template = this.business?.[queueType];
236
+ if (!template) {
237
+ throw new Error(`Business queue template not found: ${queueType}`);
238
+ }
239
+
240
+ // Deep clone to avoid modifying original
241
+ const config = JSON.parse(JSON.stringify(template));
242
+
243
+ // Replace {service} placeholder in routing keys
244
+ if (config.arguments && serviceName) {
245
+ const routingKey = config.arguments['x-dead-letter-routing-key'];
246
+ if (routingKey && typeof routingKey === 'string') {
247
+ config.arguments['x-dead-letter-routing-key'] = routingKey.replace('{service}', serviceName);
248
+ }
249
+ }
250
+
251
+ return config;
252
+ },
253
+
254
+ /**
255
+ * Get infrastructure queue configuration by queue name (auto-detect type)
256
+ * @param {string} queueName - Full queue name (e.g., 'workflow.init', 'registry.register')
257
+ * @returns {Object} Queue configuration
258
+ */
259
+ getInfrastructureQueueConfig(queueName) {
260
+ if (queueName.startsWith('workflow.')) {
261
+ return this.getWorkflowQueueConfig(queueName);
262
+ } else if (queueName.startsWith('registry.')) {
263
+ return this.getRegistryQueueConfig(queueName);
264
+ } else {
265
+ throw new Error(`Queue ${queueName} is not an infrastructure queue. Infrastructure queues must start with 'workflow.' or 'registry.'`);
266
+ }
267
+ }
268
+ };
269
+
package/src/index.js CHANGED
File without changes
File without changes
@@ -1,8 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ const queueConfig = require('../config/queueConfig');
4
+
3
5
  /**
4
6
  * QueueManager - Manages queue creation, configuration, and lifecycle
5
7
  * Handles TTL, DLQ, auto-delete, and other queue properties
8
+ *
9
+ * For infrastructure queues (workflow.*, registry.*), uses central configuration.
10
+ * For business queues ({service}.*), uses service-specific configuration.
6
11
  */
7
12
  class QueueManager {
8
13
  constructor(mqClient, config = {}) {
@@ -19,8 +24,11 @@ class QueueManager {
19
24
 
20
25
  /**
21
26
  * Create or ensure queue exists with specific configuration
27
+ * For infrastructure queues (workflow.*, registry.*), uses central configuration.
28
+ * For business queues, uses provided options.
29
+ *
22
30
  * @param {string} queueName - Name of the queue
23
- * @param {Object} options - Queue configuration options
31
+ * @param {Object} options - Queue configuration options (ignored for infrastructure queues)
24
32
  */
25
33
  async ensureQueue(queueName, options = {}) {
26
34
  // Get channel from client's transport
@@ -30,6 +38,57 @@ class QueueManager {
30
38
  }
31
39
  const channel = transport.channel;
32
40
 
41
+ let queueOptions;
42
+
43
+ // Check if this is an infrastructure queue - use central config
44
+ if (queueConfig.isInfrastructureQueue(queueName)) {
45
+ try {
46
+ const infraConfig = queueConfig.getInfrastructureQueueConfig(queueName);
47
+ queueOptions = {
48
+ durable: infraConfig.durable !== false,
49
+ arguments: { ...infraConfig.arguments }
50
+ };
51
+ } catch (error) {
52
+ // If config not found, fall back to provided options
53
+ console.warn(`[QueueManager] Infrastructure queue config not found for ${queueName}, using provided options:`, error.message);
54
+ queueOptions = this._buildQueueOptions(queueName, options);
55
+ }
56
+ } else if (queueConfig.isBusinessQueue(queueName)) {
57
+ // Business queue - use central business queue config
58
+ try {
59
+ const parsed = queueConfig.parseBusinessQueue(queueName);
60
+ if (parsed) {
61
+ const businessConfig = queueConfig.getBusinessQueueConfig(parsed.queueType, parsed.serviceName);
62
+ queueOptions = {
63
+ durable: businessConfig.durable !== false,
64
+ arguments: { ...businessConfig.arguments }
65
+ };
66
+ } else {
67
+ // Fallback to provided options
68
+ queueOptions = this._buildQueueOptions(queueName, options);
69
+ }
70
+ } catch (error) {
71
+ // If config not found, fall back to provided options
72
+ console.warn(`[QueueManager] Business queue config not found for ${queueName}, using provided options:`, error.message);
73
+ queueOptions = this._buildQueueOptions(queueName, options);
74
+ }
75
+ } else {
76
+ // Unknown queue type - use provided options
77
+ queueOptions = this._buildQueueOptions(queueName, options);
78
+ }
79
+
80
+ // Track managed queue
81
+ this.managedQueues.add(queueName);
82
+
83
+ // Assert the queue
84
+ return channel.assertQueue(queueName, queueOptions);
85
+ }
86
+
87
+ /**
88
+ * Build queue options from provided configuration
89
+ * @private
90
+ */
91
+ _buildQueueOptions(queueName, options) {
33
92
  const queueOptions = {
34
93
  durable: options.durable !== false,
35
94
  arguments: {
@@ -63,11 +122,21 @@ class QueueManager {
63
122
  queueOptions.arguments['x-max-priority'] = options.maxPriority;
64
123
  }
65
124
 
66
- // Track managed queue
67
- this.managedQueues.add(queueName);
125
+ return queueOptions;
126
+ }
68
127
 
69
- // Assert the queue
70
- return channel.assertQueue(queueName, queueOptions);
128
+ /**
129
+ * Ensure infrastructure queue exists (workflow.*, registry.*)
130
+ * Uses central configuration from queueConfig
131
+ *
132
+ * @param {string} queueName - Infrastructure queue name (e.g., 'workflow.init', 'registry.register')
133
+ * @returns {Promise<Object>} Queue assertion result
134
+ */
135
+ async ensureInfrastructureQueue(queueName) {
136
+ if (!queueConfig.isInfrastructureQueue(queueName)) {
137
+ throw new Error(`Queue ${queueName} is not an infrastructure queue. Use ensureQueue() for business queues.`);
138
+ }
139
+ return this.ensureQueue(queueName);
71
140
  }
72
141
 
73
142
  /**
File without changes
File without changes
File without changes
@@ -7,6 +7,7 @@
7
7
 
8
8
  const amqp = require('amqplib');
9
9
  const EventEmitter = require('events');
10
+ const queueConfig = require('../config/queueConfig');
10
11
 
11
12
  class RabbitMQClient extends EventEmitter {
12
13
  /**
@@ -121,7 +122,9 @@ class RabbitMQClient extends EventEmitter {
121
122
  try {
122
123
  // Ensure queue exists if publishing directly to queue and using default exchange
123
124
  if (!exchange) {
124
- await this._channel.assertQueue(queue, { durable: this._config.durable });
125
+ // Use central config for infrastructure queues
126
+ const queueOptions = this._getQueueOptions(queue);
127
+ await this._channel.assertQueue(queue, queueOptions);
125
128
  this._channel.sendToQueue(queue, buffer, { persistent, headers, routingKey });
126
129
  } else {
127
130
  // If exchange is specified, assert exchange and publish to it
@@ -157,8 +160,32 @@ class RabbitMQClient extends EventEmitter {
157
160
  // Skip assertQueue for reply queues (they're already created with specific settings)
158
161
  // Reply queues start with 'rpc.reply.' and are created as non-durable
159
162
  if (!queue.startsWith('rpc.reply.')) {
160
- // Ensure queue exists
161
- await this._channel.assertQueue(queue, { durable });
163
+ // Use central config for infrastructure queues
164
+ const queueOptions = this._getQueueOptions(queue, { durable });
165
+
166
+ // Try to assert queue with our config
167
+ // If it fails with 406 (PRECONDITION-FAILED), queue exists with different args - use it as-is
168
+ try {
169
+ await this._channel.assertQueue(queue, queueOptions);
170
+ } catch (assertErr) {
171
+ // If queue exists with different arguments (406), use it as-is
172
+ if (assertErr.code === 406) {
173
+ console.warn(`[RabbitMQClient] Queue ${queue} exists with different arguments, using as-is:`, assertErr.message);
174
+ // Try to assert with minimal options (just durable) to verify queue exists
175
+ try {
176
+ await this._channel.assertQueue(queue, { durable: durable !== false });
177
+ } catch (minErr) {
178
+ // If even minimal assert fails, queue might not exist - log and proceed
179
+ console.warn(`[RabbitMQClient] Could not assert queue ${queue} with minimal options:`, minErr.message);
180
+ }
181
+ } else if (assertErr.code === 404) {
182
+ // Queue doesn't exist - this shouldn't happen with assertQueue, but handle it
183
+ throw assertErr;
184
+ } else {
185
+ // Other error - rethrow
186
+ throw assertErr;
187
+ }
188
+ }
162
189
  }
163
190
  // Set prefetch if provided
164
191
  if (typeof prefetch === 'number') {
@@ -189,6 +216,56 @@ class RabbitMQClient extends EventEmitter {
189
216
  }
190
217
  }
191
218
 
219
+ /**
220
+ * Get queue options - uses central config for infrastructure and business queues
221
+ * @private
222
+ * @param {string} queueName - Queue name
223
+ * @param {Object} [defaultOptions] - Default options if config not found
224
+ * @returns {Object} Queue options
225
+ */
226
+ _getQueueOptions(queueName, defaultOptions = {}) {
227
+ // Check if this is an infrastructure queue - use central config
228
+ if (queueConfig.isInfrastructureQueue(queueName)) {
229
+ try {
230
+ const infraConfig = queueConfig.getInfrastructureQueueConfig(queueName);
231
+ return {
232
+ durable: infraConfig.durable !== false,
233
+ arguments: { ...infraConfig.arguments }
234
+ };
235
+ } catch (error) {
236
+ // If config not found, fall back to default options
237
+ console.warn(`[RabbitMQClient] Infrastructure queue config not found for ${queueName}, using default options:`, error.message);
238
+ return {
239
+ durable: defaultOptions.durable !== false,
240
+ ...defaultOptions
241
+ };
242
+ }
243
+ }
244
+
245
+ // Check if this is a business queue - use central business queue config
246
+ if (queueConfig.isBusinessQueue(queueName)) {
247
+ try {
248
+ const parsed = queueConfig.parseBusinessQueue(queueName);
249
+ if (parsed) {
250
+ const businessConfig = queueConfig.getBusinessQueueConfig(parsed.queueType, parsed.serviceName);
251
+ return {
252
+ durable: businessConfig.durable !== false,
253
+ arguments: { ...businessConfig.arguments }
254
+ };
255
+ }
256
+ } catch (error) {
257
+ // If config not found, fall back to default options
258
+ console.warn(`[RabbitMQClient] Business queue config not found for ${queueName}, using default options:`, error.message);
259
+ }
260
+ }
261
+
262
+ // Unknown queue type - use default options
263
+ return {
264
+ durable: defaultOptions.durable !== false,
265
+ ...defaultOptions
266
+ };
267
+ }
268
+
192
269
  /**
193
270
  * Acknowledges a message.
194
271
  * @param {Object} msg - RabbitMQ message object.
File without changes
File without changes
File without changes
File without changes