@onlineapps/conn-orch-registry 1.1.4

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/src/config.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * config.js
3
+ *
4
+ * Loads and validates configuration variables via environment variables.
5
+ * Uses dotenv to load the .env file and Joi for validation.
6
+ *
7
+ * @module @onlineapps/connector-registry-client/src/config
8
+ */
9
+
10
+ // Load .env (if it exists)
11
+ require('dotenv').config();
12
+
13
+ const Joi = require('joi');
14
+
15
+ // Define schema for environment variables
16
+ const envSchema = Joi.object({
17
+ AMQP_URL: Joi.string().uri().required()
18
+ .description('AMQP URI for connecting to RabbitMQ'),
19
+
20
+ SERVICE_NAME: Joi.string().required()
21
+ .description('Name of the service to register'),
22
+
23
+ SERVICE_VERSION: Joi.string().required()
24
+ .description('Service version in SemVer format'),
25
+
26
+ HEARTBEAT_INTERVAL: Joi.number().integer().min(1000).default(10000)
27
+ .description('Interval for sending heartbeat messages in ms'),
28
+
29
+ API_QUEUE: Joi.string().default('api_services_queuer')
30
+ .description('Name of the queue for heartbeat and API requests'),
31
+
32
+ REGISTRY_QUEUE: Joi.string().default('registry.register')
33
+ .description('Name of the queue for registry messages')
34
+ })
35
+ .unknown() // allow additional variables
36
+ .required();
37
+
38
+ // Validate variables
39
+ const { error, value: envVars } = envSchema.validate(process.env);
40
+ if (error) {
41
+ throw new Error(`Config validation error: ${error.message}`);
42
+ }
43
+
44
+ // Export configuration
45
+ module.exports = {
46
+ amqpUrl: envVars.AMQP_URL,
47
+ serviceName: envVars.SERVICE_NAME,
48
+ version: envVars.SERVICE_VERSION,
49
+ heartbeatInterval: envVars.HEARTBEAT_INTERVAL,
50
+ apiQueue: envVars.API_QUEUE,
51
+ registryQueue: envVars.REGISTRY_QUEUE
52
+ };
package/src/events.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * events.js
3
+ *
4
+ * List of events used in the connector-registry-client module.
5
+ * Acts as a central place for constants to avoid typos
6
+ * when emitting and listening for events.
7
+ *
8
+ * @module @onlineapps/connector-registry-client/src/events
9
+ */
10
+
11
+ /**
12
+ * Named EventEmitter events for ServiceRegistryClient.
13
+ */
14
+ const EVENTS = {
15
+ /** Emitted after a successful heartbeat is sent. Payload: { id, type, serviceName, version, timestamp } */
16
+ HEARTBEAT_SENT: 'heartbeatSent',
17
+
18
+ /** Emitted when the registryOffice requests the API description. Payload: { id, type, serviceName, version, timestamp } */
19
+ API_DESCRIPTION_REQUEST: 'apiDescriptionRequest',
20
+
21
+ /** Emitted after sending the API description. Payload: { id, type, serviceName, version, description, timestamp } */
22
+ API_DESCRIPTION_SENT: 'apiDescriptionSent',
23
+
24
+ /** Emitted on internal errors. Payload: Error instance or error description. */
25
+ ERROR: 'error'
26
+ };
27
+
28
+ module.exports = EVENTS;
package/src/index.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @module @onlineapps/conn-orch-registry
3
+ * @description Service registry connector for dynamic service discovery, health monitoring,
4
+ * and OpenAPI specification management in OA Drive microservices architecture.
5
+ *
6
+ * @see {@link https://github.com/onlineapps/oa-drive/tree/main/shared/connector/conn-orch-registry|GitHub Repository}
7
+ * @author OA Drive Team
8
+ * @license MIT
9
+ * @since 1.0.0
10
+ */
11
+
12
+ // Core client
13
+ const { ServiceRegistryClient } = require('./registryClient');
14
+ // Events for use with EventEmitter
15
+ const EVENTS = require('./events');
16
+ // Event consumer for registry changes
17
+ const RegistryEventConsumer = require('./registryEventConsumer');
18
+
19
+ module.exports = {
20
+ /**
21
+ * Class for registering a service and sending heartbeats to the central registry.
22
+ * @type {Function}
23
+ */
24
+ ServiceRegistryClient,
25
+
26
+ /**
27
+ * Class for consuming registry change events (opt-in).
28
+ * @type {Function}
29
+ */
30
+ RegistryEventConsumer,
31
+
32
+ /**
33
+ * Constant containing the list of events emitted by ServiceRegistryClient.
34
+ * @type {Object}
35
+ */
36
+ EVENTS
37
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * queueManager.js
3
+ *
4
+ * Responsible for connecting to RabbitMQ and asserting/confirming the existence of queues
5
+ * used by the microservice connector.
6
+ *
7
+ * Overview:
8
+ * - Queues managed by the microservice: 'workflow' and '<serviceName>.registry'
9
+ * - On startup: assertQueue for all required queues
10
+ * - Provides access to the channel for further operations (send, consume)
11
+ *
12
+ * Usage:
13
+ * const qm = new QueueManager(amqpUrl, serviceName);
14
+ * await qm.init();
15
+ * await qm.ensureQueues();
16
+ * const { channel } = qm;
17
+ *
18
+ * @module @onlineapps/connector-registry-client/src/queueManager
19
+ */
20
+
21
+ const amqp = require('amqplib');
22
+
23
+ /**
24
+ * Queue manager for the microservice connector.
25
+ */
26
+ class QueueManager {
27
+ /**
28
+ * @param {string} amqpUrl - RabbitMQ server URL (AMQP URI)
29
+ * @param {string} serviceName - Name of the microservice (e.g. 'invoicing')
30
+ */
31
+ constructor(amqpUrl, serviceName) {
32
+ if (!amqpUrl) throw new Error('amqpUrl is required');
33
+ if (!serviceName) throw new Error('serviceName is required');
34
+ this.amqpUrl = amqpUrl;
35
+ this.serviceName = serviceName;
36
+ this.conn = null;
37
+ this.channel = null;
38
+ }
39
+
40
+ /**
41
+ * Initializes the connection and channel to RabbitMQ.
42
+ * @returns {Promise<void>}
43
+ */
44
+ async init() {
45
+ this.conn = await amqp.connect(this.amqpUrl);
46
+ this.channel = await this.conn.createChannel();
47
+ }
48
+
49
+ /**
50
+ * Ensures all required queues exist. Creates them if they don't.
51
+ * @param {Array<string>} [additionalQueues=[]] - Any additional custom queues
52
+ * @returns {Promise<void>}
53
+ */
54
+ async ensureQueues(additionalQueues = []) {
55
+ if (!this.channel) {
56
+ throw new Error('Channel is not initialized. Call init() first.');
57
+ }
58
+
59
+ // Default queues for the registry client
60
+ const baseQueues = [
61
+ 'workflow'
62
+ // Note: ${serviceName}.registry queue is created by RegistryEventConsumer when needed
63
+ ];
64
+
65
+ const queuesToCreate = baseQueues.concat(additionalQueues);
66
+
67
+ for (const q of queuesToCreate) {
68
+ await this.channel.assertQueue(q, { durable: true });
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Closes the channel and connection.
74
+ * @returns {Promise<void>}
75
+ */
76
+ async close() {
77
+ if (this.channel) {
78
+ await this.channel.close();
79
+ this.channel = null;
80
+ }
81
+ if (this.conn) {
82
+ await this.conn.close();
83
+ this.conn = null;
84
+ }
85
+ }
86
+ }
87
+
88
+ module.exports = QueueManager;
@@ -0,0 +1,397 @@
1
+ /**
2
+ * registryClient.js
3
+ *
4
+ * ServiceRegistryClient for communication between a microservice (via Agent) and the central registry.
5
+ * Sends heartbeat messages, listens for API description requests, and sends API descriptions.
6
+ * Uses QueueManager to manage AMQP queues.
7
+ * Emits events through EventEmitter.
8
+ *
9
+ * Events (see src/events.js):
10
+ * - 'heartbeatSent'
11
+ * - 'apiDescriptionRequest'
12
+ * - 'apiDescriptionSent'
13
+ * - 'error'
14
+ *
15
+ * @module @onlineapps/connector-registry-client/src/registryClient
16
+ */
17
+
18
+ const EventEmitter = require('events');
19
+ const QueueManager = require('./queueManager');
20
+ const RegistryEventConsumer = require('./registryEventConsumer');
21
+ const { v4: uuidv4 } = require('uuid');
22
+
23
+ class ServiceRegistryClient extends EventEmitter {
24
+ /**
25
+ * @param {Object} opts
26
+ * @param {string} opts.amqpUrl - AMQP URI for connecting to RabbitMQ
27
+ * @param {string} opts.serviceName - Name of the service (e.g., 'invoicing')
28
+ * @param {string} opts.version - Version of the service (e.g., '1.2.0')
29
+ * @param {string} [opts.specificationEndpoint='/api/v1/specification'] - Endpoint where API specification is available
30
+ * @param {number} [opts.heartbeatInterval=10000] - Heartbeat interval in milliseconds
31
+ * @param {string} [opts.apiQueue='api_services_queuer'] - Queue name for heartbeat and API traffic
32
+ * @param {string} [opts.registryQueue='registry_office'] - Queue name for registry messages
33
+ */
34
+ constructor({ amqpUrl, serviceName, version, specificationEndpoint = '/api/v1/specification',
35
+ heartbeatInterval = 10000, apiQueue = 'api_services_queuer', registryQueue = 'registry_office',
36
+ redis = null, storageConfig = {} }) {
37
+ super();
38
+ if (!amqpUrl || !serviceName || !version) {
39
+ throw new Error('amqpUrl, serviceName, and version are required');
40
+ }
41
+ this.serviceName = serviceName;
42
+ this.version = version;
43
+ this.specificationEndpoint = specificationEndpoint;
44
+ this.heartbeatInterval = heartbeatInterval;
45
+ this.apiQueue = apiQueue;
46
+ this.registryQueue = registryQueue;
47
+ this.queueManager = new QueueManager(amqpUrl, serviceName);
48
+ this.heartbeatTimer = null;
49
+
50
+ // Event consumer (optional, activated via subscribeToChanges)
51
+ this.eventConsumer = null;
52
+ this.redis = redis;
53
+ this.storageConfig = storageConfig;
54
+ }
55
+
56
+ /**
57
+ * Initializes the connection, ensures queues exist, and starts consuming the service response queue.
58
+ * @returns {Promise<void>}
59
+ */
60
+ async init() {
61
+ await this.queueManager.init();
62
+
63
+ // Create service-specific response queue
64
+ this.serviceResponseQueue = `${this.serviceName}.responses`;
65
+
66
+ // Ensure existence of API, registry and service response queues
67
+ await this.queueManager.ensureQueues([this.apiQueue, this.registryQueue, this.serviceResponseQueue]);
68
+
69
+ // Start consuming service response queue for registry responses
70
+ await this.queueManager.channel.consume(
71
+ this.serviceResponseQueue,
72
+ msg => this._handleRegistryMessage(msg),
73
+ { noAck: false }
74
+ );
75
+ }
76
+
77
+ /**
78
+ * Internal handler for incoming messages from the registry queue.
79
+ * Emits 'apiDescriptionRequest' when appropriate.
80
+ * Handles registration responses.
81
+ * @param {Object} msg - AMQP message
82
+ * @private
83
+ */
84
+ _handleRegistryMessage(msg) {
85
+ let payload;
86
+ try {
87
+ payload = JSON.parse(msg.content.toString());
88
+ } catch (err) {
89
+ this.emit('error', err);
90
+ return this.queueManager.channel.nack(msg, false, false);
91
+ }
92
+
93
+ // Handle API description request from registry
94
+ if (payload.type === 'apiDescriptionRequest' &&
95
+ payload.serviceName === this.serviceName &&
96
+ payload.version === this.version) {
97
+ // Emit API description request event
98
+ this.emit('apiDescriptionRequest', payload);
99
+ }
100
+
101
+ // Handle registration response from registry
102
+ if (payload.type === 'registerResponse' && payload.requestId) {
103
+ if (this.pendingRegistrations && this.pendingRegistrations.has(payload.requestId)) {
104
+ const { resolve } = this.pendingRegistrations.get(payload.requestId);
105
+ this.pendingRegistrations.delete(payload.requestId);
106
+
107
+ // Resolve the registration promise with the response
108
+ resolve({
109
+ success: payload.success || false,
110
+ message: payload.message || 'Registration processed',
111
+ serviceName: this.serviceName,
112
+ version: this.version,
113
+ registrationId: payload.registrationId,
114
+ validated: payload.validated || false
115
+ });
116
+ }
117
+ }
118
+
119
+ // Acknowledge all messages
120
+ this.queueManager.channel.ack(msg);
121
+ }
122
+
123
+ /**
124
+ * Registers the service with the registry.
125
+ * Registry will validate if the service is properly tested and valid.
126
+ * Upon successful validation, the service will be activated and can start heartbeats.
127
+ * @param {Object} serviceInfo - Service registration information
128
+ * @param {Array} serviceInfo.endpoints - API endpoints provided by the service
129
+ * @param {Object} serviceInfo.metadata - Additional service metadata
130
+ * @param {string} serviceInfo.health - Health check endpoint
131
+ * @param {Object} serviceInfo.spec - OpenAPI specification
132
+ * @param {number} serviceInfo.timeout - Timeout for registration response (default: 30000ms)
133
+ * @returns {Promise<Object>} Registration result with success status
134
+ */
135
+ async register(serviceInfo = {}) {
136
+ // Validate input
137
+ if (serviceInfo.endpoints && !Array.isArray(serviceInfo.endpoints)) {
138
+ throw new Error('endpoints must be an array');
139
+ }
140
+
141
+ const msgId = uuidv4();
142
+ const msg = {
143
+ id: msgId,
144
+ type: 'register',
145
+ serviceName: this.serviceName,
146
+ version: this.version,
147
+ specificationEndpoint: this.specificationEndpoint,
148
+ endpoints: serviceInfo.endpoints || [],
149
+ metadata: serviceInfo.metadata || {},
150
+ health: serviceInfo.health || '/health',
151
+ spec: serviceInfo.spec || null,
152
+ validationToken: serviceInfo.token || serviceInfo.validationToken,
153
+ tokenSecret: serviceInfo.secret || serviceInfo.tokenSecret,
154
+ responseQueue: this.serviceResponseQueue, // Queue for registry to send response
155
+ timestamp: new Date().toISOString()
156
+ };
157
+
158
+ // Create promise to wait for registration response
159
+ const timeout = serviceInfo.timeout || 30000; // 30 seconds default
160
+ const responsePromise = new Promise((resolve, reject) => {
161
+ // Store resolver for this registration request
162
+ this.pendingRegistrations = this.pendingRegistrations || new Map();
163
+ this.pendingRegistrations.set(msgId, { resolve, reject });
164
+
165
+ // Set timeout for registration response
166
+ setTimeout(() => {
167
+ if (this.pendingRegistrations.has(msgId)) {
168
+ this.pendingRegistrations.delete(msgId);
169
+ reject(new Error(`Registration timeout after ${timeout}ms - no response from registry`));
170
+ }
171
+ }, timeout);
172
+ });
173
+
174
+ // Send registration message to registry
175
+ await this.queueManager.channel.assertQueue(this.registryQueue, { durable: true });
176
+ this.queueManager.channel.sendToQueue(
177
+ this.registryQueue,
178
+ Buffer.from(JSON.stringify(msg)),
179
+ { persistent: true }
180
+ );
181
+
182
+ this.emit('registerSent', msg);
183
+
184
+ // Wait for registration response from registry
185
+ try {
186
+ const response = await responsePromise;
187
+ return response;
188
+ } catch (error) {
189
+ return {
190
+ success: false,
191
+ message: error.message,
192
+ serviceName: this.serviceName,
193
+ version: this.version
194
+ };
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Deregisters the service from the registry.
200
+ * @returns {Promise<Object>} Deregistration result
201
+ */
202
+ async deregister() {
203
+ const msg = {
204
+ id: uuidv4(),
205
+ type: 'deregister',
206
+ serviceName: this.serviceName,
207
+ version: this.version,
208
+ timestamp: new Date().toISOString()
209
+ };
210
+
211
+ // Send deregistration message to registry
212
+ await this.queueManager.channel.assertQueue(this.registryQueue, { durable: true });
213
+ this.queueManager.channel.sendToQueue(
214
+ this.registryQueue,
215
+ Buffer.from(JSON.stringify(msg)),
216
+ { persistent: true }
217
+ );
218
+
219
+ this.emit('deregisterSent', msg);
220
+
221
+ // Stop heartbeat if running
222
+ this.stopHeartbeat();
223
+
224
+ return {
225
+ success: true,
226
+ message: 'Service deregistered successfully',
227
+ serviceName: this.serviceName,
228
+ version: this.version
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Sends a heartbeat message to the API queue.
234
+ * Should be called only after successful registration.
235
+ * Emits 'heartbeatSent'.
236
+ * @returns {Promise<void>}
237
+ */
238
+ async sendHeartbeat() {
239
+ const msg = {
240
+ id: uuidv4(),
241
+ type: 'heartbeat',
242
+ serviceName: this.serviceName,
243
+ version: this.version,
244
+ specificationEndpoint: this.specificationEndpoint,
245
+ timestamp: new Date().toISOString()
246
+ };
247
+ await this.queueManager.channel.assertQueue(this.registryQueue, { durable: true });
248
+ this.queueManager.channel.sendToQueue(
249
+ this.registryQueue,
250
+ Buffer.from(JSON.stringify(msg)),
251
+ { persistent: true }
252
+ );
253
+ this.emit('heartbeatSent', msg);
254
+ }
255
+
256
+ /**
257
+ * Starts periodic heartbeat messages.
258
+ */
259
+ startHeartbeat() {
260
+ // Send immediately after init
261
+ this.sendHeartbeat();
262
+ // Repeat at the configured interval
263
+ this.heartbeatTimer = setInterval(
264
+ () => this.sendHeartbeat(),
265
+ this.heartbeatInterval
266
+ );
267
+ }
268
+
269
+ /**
270
+ * Stops periodic heartbeat messages.
271
+ */
272
+ stopHeartbeat() {
273
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
274
+ }
275
+
276
+ /**
277
+ * Sends an API description message to the registry queue.
278
+ * Emits 'apiDescriptionSent'.
279
+ * @param {Object} apiDescription - JSON object describing the API
280
+ * @returns {Promise<void>}
281
+ */
282
+ async sendApiDescription(apiDescription) {
283
+ const msg = {
284
+ id: uuidv4(),
285
+ type: 'apiDescription',
286
+ serviceName: this.serviceName,
287
+ version: this.version,
288
+ description: apiDescription,
289
+ timestamp: new Date().toISOString()
290
+ };
291
+ await this.queueManager.channel.assertQueue(this.registryQueue, { durable: true });
292
+ this.queueManager.channel.sendToQueue(
293
+ this.registryQueue,
294
+ Buffer.from(JSON.stringify(msg)),
295
+ { persistent: true }
296
+ );
297
+ this.emit('apiDescriptionSent', msg);
298
+ }
299
+
300
+ /**
301
+ * Alias for sendApiDescription - sends specification to registry
302
+ * @param {Object} spec - API specification (optional, will fetch if not provided)
303
+ * @returns {Promise<void>}
304
+ */
305
+ async sendSpecification(spec) {
306
+ // If no spec provided, try to fetch it
307
+ if (!spec) {
308
+ try {
309
+ const response = await fetch(`http://localhost:${process.env.PORT || 3000}${this.specificationEndpoint}`);
310
+ if (response.ok) {
311
+ spec = await response.json();
312
+ }
313
+ } catch (error) {
314
+ console.error('Failed to fetch specification:', error);
315
+ return;
316
+ }
317
+ }
318
+
319
+ // Use existing method
320
+ return this.sendApiDescription(spec);
321
+ }
322
+
323
+ /**
324
+ * Subscribe to registry change events (opt-in)
325
+ * Creates event consumer and starts listening to registry.changes exchange
326
+ * @returns {Promise<void>}
327
+ */
328
+ async subscribeToChanges() {
329
+ if (!this.eventConsumer) {
330
+ this.eventConsumer = new RegistryEventConsumer({
331
+ queueManager: this.queueManager,
332
+ serviceName: this.serviceName,
333
+ redis: this.redis,
334
+ storageConfig: this.storageConfig
335
+ });
336
+
337
+ // Forward events from consumer
338
+ this.eventConsumer.on('serviceUpdated', data => this.emit('serviceUpdated', data));
339
+ this.eventConsumer.on('statusChanged', data => this.emit('statusChanged', data));
340
+ this.eventConsumer.on('snapshotReceived', data => this.emit('snapshotReceived', data));
341
+ this.eventConsumer.on('error', err => this.emit('error', err));
342
+
343
+ // Initialize storage and subscribe
344
+ await this.eventConsumer.initStorage();
345
+ await this.eventConsumer.subscribeToChanges();
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Get service spec from event consumer (requires subscribeToChanges)
351
+ * @param {string} serviceName - Name of the service
352
+ * @returns {Promise<Object>} - Service specification
353
+ */
354
+ async getServiceSpec(serviceName) {
355
+ if (!this.eventConsumer) {
356
+ throw new Error('Event consumer not initialized. Call subscribeToChanges() first.');
357
+ }
358
+ return this.eventConsumer.getServiceSpec(serviceName);
359
+ }
360
+
361
+ /**
362
+ * Check if service is active (requires subscribeToChanges)
363
+ * @param {string} serviceName - Name of the service
364
+ * @returns {boolean}
365
+ */
366
+ isServiceActive(serviceName) {
367
+ if (!this.eventConsumer) {
368
+ throw new Error('Event consumer not initialized. Call subscribeToChanges() first.');
369
+ }
370
+ return this.eventConsumer.isServiceActive(serviceName);
371
+ }
372
+
373
+ /**
374
+ * Get list of active services (requires subscribeToChanges)
375
+ * @returns {Array<string>}
376
+ */
377
+ getActiveServices() {
378
+ if (!this.eventConsumer) {
379
+ return [];
380
+ }
381
+ return this.eventConsumer.getActiveServices();
382
+ }
383
+
384
+ /**
385
+ * Releases resources: stops heartbeat and closes connection.
386
+ * @returns {Promise<void>}
387
+ */
388
+ async close() {
389
+ this.stopHeartbeat();
390
+ if (this.eventConsumer) {
391
+ this.eventConsumer.clearCache();
392
+ }
393
+ await this.queueManager.close();
394
+ }
395
+ }
396
+
397
+ module.exports = { ServiceRegistryClient };