@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/LICENSE +22 -0
- package/README.md +151 -0
- package/docs/REGISTRY_CLIENT_GUIDE.md +240 -0
- package/examples/basicUsage.js +85 -0
- package/examples/event-consumer-example.js +108 -0
- package/package.json +66 -0
- package/src/config.js +52 -0
- package/src/events.js +28 -0
- package/src/index.js +37 -0
- package/src/queueManager.js +88 -0
- package/src/registryClient.js +397 -0
- package/src/registryEventConsumer.js +422 -0
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 };
|