@onlineapps/service-wrapper 2.0.8 → 2.0.10

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.
@@ -1,13 +1,15 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * @module @onlineapps/service-wrapper
5
- * @description Thin orchestration layer for microservices that handles all infrastructure concerns.
6
- * Delegates all actual work to specialized connectors.
4
+ * Service Wrapper - Collection of connectors for business services
7
5
  *
8
- * @author OA Drive Team
9
- * @license MIT
10
- * @since 2.0.0
6
+ * Provides infrastructure components in single-process architecture:
7
+ * - MQ integration (RabbitMQ)
8
+ * - Service discovery (Registry)
9
+ * - Monitoring and metrics
10
+ * - Health checks
11
+ *
12
+ * Uses HTTP pattern exclusively - no direct handler calls
11
13
  */
12
14
 
13
15
  // Import connectors
@@ -21,377 +23,534 @@ const CacheConnector = require('@onlineapps/conn-base-cache');
21
23
  const ErrorHandlerConnector = require('@onlineapps/conn-infra-error-handler');
22
24
 
23
25
  /**
24
- * @class ServiceWrapper
25
- * @description Thin wrapper that orchestrates all infrastructure concerns for a microservice.
26
- * ALL actual functionality is delegated to specialized connectors.
27
- *
28
- * @example
29
- * const wrapper = new ServiceWrapper({
30
- * service: expressApp,
31
- * serviceName: 'hello-service',
32
- * openApiSpec: require('./openapi.json'),
33
- * config: {
34
- * rabbitmq: process.env.RABBITMQ_URL,
35
- * redis: process.env.REDIS_HOST,
36
- * registry: process.env.REGISTRY_URL
37
- * }
38
- * });
39
- *
40
- * await wrapper.start();
26
+ * ServiceWrapper class
27
+ * Collection of connectors that enhance business service with infrastructure
41
28
  */
42
29
  class ServiceWrapper {
43
30
  /**
44
31
  * Create a new ServiceWrapper instance
45
- * @constructor
46
32
  * @param {Object} options - Configuration options
47
- * @param {Object} [options.service] - Express application instance (for direct calls)
48
- * @param {string} [options.serviceUrl] - HTTP URL of the service (for HTTP calls)
49
- * @param {string} options.serviceName - Name of the service
50
- * @param {Object} options.openApiSpec - OpenAPI specification
51
- * @param {Object} [options.config={}] - Infrastructure configuration
52
- * @param {string} [options.config.rabbitmq] - RabbitMQ connection URL
53
- * @param {string} [options.config.redis] - Redis connection string
54
- * @param {string} [options.config.registry] - Registry service URL
55
- * @param {number} [options.config.port=3000] - Service port (deprecated, use serviceUrl)
56
- * @param {number} [options.config.prefetch=10] - MQ prefetch count
57
- * @param {boolean} [options.config.directCall] - Use direct Express calls (default: auto-detect)
58
- *
59
- * @throws {Error} If required options are missing
33
+ * @param {Object} options.app - Express application instance
34
+ * @param {Object} options.server - HTTP server instance
35
+ * @param {Object} options.config - Service and wrapper configuration
36
+ * @param {Object} options.operations - Operations schema
60
37
  */
61
38
  constructor(options = {}) {
62
39
  this._validateOptions(options);
63
40
 
64
41
  // Store configuration
65
- this.service = options.service;
66
- this.serviceUrl = options.serviceUrl;
67
- this.serviceName = options.serviceName;
68
- this.openApiSpec = options.openApiSpec;
69
- this.config = options.config || {};
70
-
71
- // Initialize connectors
72
- this.logger = MonitoringConnector; // Singleton, will be initialized later
73
- this.monitoring = MonitoringConnector; // Also expose as monitoring for clarity
42
+ this.app = options.app;
43
+ this.server = options.server;
44
+ this.config = this._processConfig(options.config);
45
+ this.operations = options.operations;
46
+
47
+ // Initialize connector placeholders
74
48
  this.mqClient = null;
75
49
  this.registryClient = null;
76
50
  this.orchestrator = null;
77
51
  this.cacheConnector = null;
52
+ this.monitoring = null;
53
+ this.logger = null;
78
54
 
79
55
  // State
80
- this.isRunning = false;
56
+ this.isInitialized = false;
81
57
  }
82
58
 
83
59
  /**
84
60
  * Validate constructor options
85
61
  * @private
86
- * @param {Object} options - Options to validate
87
- * @throws {Error} If required options are missing
88
62
  */
89
63
  _validateOptions(options) {
90
- if (!options.service && !options.serviceUrl) {
91
- throw new Error('Either service (Express app) or serviceUrl is required');
64
+ if (!options.app) {
65
+ throw new Error('Express app instance is required');
66
+ }
67
+ if (!options.server) {
68
+ throw new Error('HTTP server instance is required');
92
69
  }
93
- if (!options.serviceName) {
94
- throw new Error('Service name is required');
70
+ if (!options.config) {
71
+ throw new Error('Configuration is required');
95
72
  }
96
- if (!options.openApiSpec) {
97
- throw new Error('OpenAPI specification is required');
73
+ if (!options.operations) {
74
+ throw new Error('Operations schema is required');
98
75
  }
99
76
  }
100
77
 
101
78
  /**
102
- * Start the service wrapper
103
- * @async
104
- * @method start
105
- * @returns {Promise<void>}
106
- *
107
- * @example
108
- * await wrapper.start();
109
- * console.log('Service wrapper started');
79
+ * Process and validate configuration
80
+ * @private
110
81
  */
111
- async start() {
112
- if (this.isRunning) {
113
- console.warn('Service wrapper already running');
114
- return;
115
- }
116
-
117
- try {
118
- // Initialize monitoring first
119
- await this.monitoring.init({ serviceName: this.serviceName });
120
-
121
- this.logger.info(`Starting service wrapper for ${this.serviceName}`);
122
-
123
- // 1. Initialize infrastructure connectors
124
- await this._initializeConnectors();
125
-
126
- // 2. Initialize cache connector if Redis is configured
127
- if (this.config.redis) {
128
- this.cacheConnector = new CacheConnector({
129
- host: this.config.redis,
130
- namespace: this.serviceName,
131
- defaultTTL: this.config.cacheTTL || 300
132
- });
133
- await this.cacheConnector.connect();
134
- this.logger.info('Cache connector initialized');
82
+ _processConfig(config) {
83
+ // Replace environment variables in config
84
+ const processValue = (value) => {
85
+ if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
86
+ const envVar = value.slice(2, -1);
87
+ const [varName, defaultValue] = envVar.split(':');
88
+ return process.env[varName] || defaultValue || '';
135
89
  }
90
+ return value;
91
+ };
136
92
 
137
- // 3. Initialize error handler
138
- const errorHandler = new ErrorHandlerConnector({
139
- maxRetries: this.config.maxRetries || 3,
140
- retryDelay: this.config.retryDelay || 1000,
141
- logger: this.logger
142
- });
143
-
144
- // 4. Create the orchestrator with all dependencies
145
- // Determine if we should use direct calls or HTTP
146
- const useDirectCall = this.config.directCall !== undefined
147
- ? this.config.directCall
148
- : (this.service && !this.serviceUrl); // Auto-detect based on what was provided
149
-
150
- const serviceUrl = this.serviceUrl || `http://localhost:${this.config.port || 3000}`;
151
-
152
- this.orchestrator = OrchestratorConnector.create({
153
- mqClient: this.mqClient,
154
- registryClient: this.registryClient,
155
- apiMapper: ApiMapperConnector.create({
156
- openApiSpec: this.openApiSpec,
157
- serviceUrl: serviceUrl,
158
- service: this.service,
159
- directCall: useDirectCall,
160
- logger: this.logger
161
- }),
162
- cookbook: CookbookConnector,
163
- cache: this.cacheConnector,
164
- errorHandler: errorHandler,
165
- logger: this.logger,
166
- defaultTimeout: this.config.defaultTimeout || 30000
167
- });
168
-
169
- // 5. Register service with registry
170
- await this._registerService();
171
-
172
- // 6. Subscribe to workflow messages
173
- await this._subscribeToQueues();
174
-
175
- // 7. Start heartbeat
176
- this._startHeartbeat();
177
-
178
- this.isRunning = true;
179
- this.logger.info(`Service wrapper started successfully for ${this.serviceName}`);
93
+ // Recursively process config object
94
+ const processObject = (obj) => {
95
+ const result = {};
96
+ for (const [key, value] of Object.entries(obj)) {
97
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
98
+ result[key] = processObject(value);
99
+ } else {
100
+ result[key] = processValue(value);
101
+ }
102
+ }
103
+ return result;
104
+ };
180
105
 
181
- } catch (error) {
182
- this.logger.error('Failed to start service wrapper', { error: error.message });
183
- throw error;
184
- }
106
+ return processObject(config);
185
107
  }
186
108
 
187
109
  /**
188
- * Stop the service wrapper
110
+ * Initialize all wrapper components
189
111
  * @async
190
- * @method stop
191
- * @returns {Promise<void>}
192
- *
193
- * @example
194
- * await wrapper.stop();
195
- * console.log('Service wrapper stopped');
196
112
  */
197
- async stop() {
198
- if (!this.isRunning) {
199
- this.logger.warn('Service wrapper not running');
113
+ async initialize() {
114
+ if (this.isInitialized) {
115
+ console.warn('ServiceWrapper already initialized');
200
116
  return;
201
117
  }
202
118
 
203
119
  try {
204
- this.logger.info(`Stopping service wrapper for ${this.serviceName}`);
120
+ const serviceName = this.config.service?.name || 'unnamed-service';
121
+ console.log(`Initializing ServiceWrapper for ${serviceName}`);
205
122
 
206
- // Stop heartbeat
207
- if (this.heartbeatInterval) {
208
- clearInterval(this.heartbeatInterval);
123
+ // 1. Initialize monitoring first (needed for logging)
124
+ if (this.config.wrapper?.monitoring?.enabled !== false) {
125
+ await this._initializeMonitoring();
209
126
  }
210
127
 
211
- // Unregister from registry
212
- if (this.registryClient) {
213
- await this.registryClient.unregister(this.serviceName);
128
+ // 2. Initialize MQ connection
129
+ if (this.config.wrapper?.mq?.enabled !== false) {
130
+ await this._initializeMQ();
214
131
  }
215
132
 
216
- // Disconnect from cache
217
- if (this.cacheConnector) {
218
- await this.cacheConnector.disconnect();
133
+ // 3. Initialize service registry
134
+ if (this.config.wrapper?.registry?.enabled !== false) {
135
+ await this._initializeRegistry();
219
136
  }
220
137
 
221
- // Disconnect from MQ
138
+ // 4. Initialize cache if configured
139
+ if (this.config.wrapper?.cache?.enabled === true) {
140
+ await this._initializeCache();
141
+ }
142
+
143
+ // 5. Setup health checks
144
+ if (this.config.wrapper?.health?.enabled !== false) {
145
+ this._setupHealthChecks();
146
+ }
147
+
148
+ // 6. Initialize orchestrator for workflow processing
222
149
  if (this.mqClient) {
223
- await this.mqClient.disconnect();
150
+ await this._initializeOrchestrator();
151
+ await this._subscribeToQueues();
224
152
  }
225
153
 
226
- this.isRunning = false;
227
- this.logger.info(`Service wrapper stopped successfully for ${this.serviceName}`);
154
+ this.isInitialized = true;
155
+ console.log(`ServiceWrapper initialized successfully for ${serviceName}`);
228
156
 
229
157
  } catch (error) {
230
- this.logger.error('Error stopping service wrapper', { error: error.message });
158
+ console.error('Failed to initialize ServiceWrapper:', error);
231
159
  throw error;
232
160
  }
233
161
  }
234
162
 
235
163
  /**
236
- * Initialize infrastructure connectors
164
+ * Initialize monitoring
165
+ * @private
166
+ */
167
+ async _initializeMonitoring() {
168
+ const serviceName = this.config.service?.name || 'unnamed-service';
169
+
170
+ this.monitoring = MonitoringConnector;
171
+ await this.monitoring.init({
172
+ serviceName,
173
+ ...this.config.wrapper?.monitoring
174
+ });
175
+
176
+ this.logger = this.monitoring;
177
+ console.log('Monitoring connector initialized');
178
+
179
+ // Add monitoring middleware to Express app
180
+ if (this.app) {
181
+ this.app.use((req, res, next) => {
182
+ const start = Date.now();
183
+
184
+ res.on('finish', () => {
185
+ const duration = Date.now() - start;
186
+ this.monitoring.info('Request processed', {
187
+ method: req.method,
188
+ path: req.path,
189
+ statusCode: res.statusCode,
190
+ duration
191
+ });
192
+ });
193
+
194
+ next();
195
+ });
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Initialize MQ connection
237
201
  * @private
238
- * @async
239
- * @returns {Promise<void>}
240
202
  */
241
- async _initializeConnectors() {
242
- // Initialize MQ client with correct config format
243
- const rabbitUrl = this.config.rabbitmq || process.env.RABBITMQ_URL || 'amqp://localhost:5672';
203
+ async _initializeMQ() {
204
+ const mqUrl = this.config.wrapper?.mq?.url;
205
+ if (!mqUrl) {
206
+ throw new Error('MQ URL is required when MQ is enabled. Set wrapper.mq.url in config');
207
+ }
208
+
209
+ const serviceName = this.config.service?.name || 'unnamed-service';
244
210
 
245
211
  this.mqClient = new MQConnector({
246
212
  type: 'rabbitmq',
247
- host: rabbitUrl,
248
- queue: `${this.serviceName}.workflow`,
249
- prefetch: this.config.prefetch || 10,
213
+ host: mqUrl,
214
+ queue: `${serviceName}.workflow`,
215
+ prefetch: this.config.wrapper?.mq?.prefetch || 10,
250
216
  durable: true,
251
217
  noAck: false,
252
- logger: {
253
- info: (...args) => this.logger.info(...args),
254
- warn: (...args) => this.logger.warn(...args),
255
- error: (...args) => this.logger.error(...args),
256
- debug: (...args) => this.logger.debug(...args)
257
- }
218
+ logger: this.logger || console
258
219
  });
259
- await this.mqClient.connect();
260
220
 
261
- // Initialize registry client with required parameters
262
- this.registryClient = new RegistryConnector.ServiceRegistryClient({
263
- amqpUrl: rabbitUrl,
264
- serviceName: this.serviceName,
265
- version: this.openApiSpec?.info?.version || '1.0.0',
266
- registryUrl: this.config.registry || process.env.REGISTRY_URL || 'http://localhost:4000',
267
- logger: this.logger
268
- });
221
+ await this.mqClient.connect();
222
+ console.log('MQ connector initialized');
269
223
  }
270
224
 
271
225
  /**
272
- * Register service with registry
226
+ * Initialize service registry
273
227
  * @private
274
- * @async
275
- * @returns {Promise<void>}
276
228
  */
277
- async _registerService() {
278
- const serviceUrl = this.serviceUrl || `http://localhost:${this.config.port || 3000}`;
229
+ async _initializeRegistry() {
230
+ const registryUrl = this.config.wrapper?.registry?.url;
231
+ if (!registryUrl) {
232
+ throw new Error('Registry URL is required when registry is enabled. Set wrapper.registry.url in config');
233
+ }
234
+
235
+ const serviceName = this.config.service?.name || 'unnamed-service';
236
+ const servicePort = this.config.service?.port || process.env.PORT || 3000;
237
+ const mqUrl = this.config.wrapper?.mq?.url || '';
238
+
239
+ this.registryClient = new RegistryConnector.ServiceRegistryClient({
240
+ amqpUrl: mqUrl,
241
+ serviceName: serviceName,
242
+ version: this.config.service?.version || '1.0.0',
243
+ registryUrl: registryUrl,
244
+ logger: this.logger || console
245
+ });
246
+
247
+ // Register service
279
248
  const serviceInfo = {
280
- name: this.serviceName,
281
- url: serviceUrl,
282
- openapi: this.openApiSpec,
249
+ name: serviceName,
250
+ url: `http://localhost:${servicePort}`,
251
+ operations: this.operations?.operations || {},
283
252
  metadata: {
284
- version: this.openApiSpec.info?.version || '1.0.0',
285
- description: this.openApiSpec.info?.description || ''
253
+ version: this.config.service?.version || '1.0.0',
254
+ description: this.config.service?.description || ''
286
255
  }
287
256
  };
288
257
 
289
258
  await this.registryClient.register(serviceInfo);
290
- this.logger.info(`Service registered: ${this.serviceName}`);
259
+ console.log(`Service registered: ${serviceName}`);
260
+
261
+ // Start heartbeat
262
+ const heartbeatInterval = this.config.wrapper?.registry?.heartbeatInterval || 30000;
263
+ this.heartbeatTimer = setInterval(async () => {
264
+ try {
265
+ await this.registryClient.sendHeartbeat(serviceName);
266
+ } catch (error) {
267
+ console.error('Heartbeat failed:', error.message);
268
+ }
269
+ }, heartbeatInterval);
291
270
  }
292
271
 
293
272
  /**
294
- * Subscribe to workflow message queues
273
+ * Initialize cache connector
295
274
  * @private
296
- * @async
297
- * @returns {Promise<void>}
298
275
  */
299
- async _subscribeToQueues() {
300
- const queueName = `${this.serviceName}.workflow`;
301
-
302
- // Subscribe to workflow messages
303
- await this.mqClient.consume(
304
- queueName,
305
- async (rawMessage) => {
306
- try {
307
- // Parse message content if it's a buffer (AMQP message)
308
- let message;
309
- if (rawMessage && rawMessage.content) {
310
- // This is an AMQP message with content buffer
311
- const messageContent = rawMessage.content.toString();
312
- try {
313
- message = JSON.parse(messageContent);
314
- } catch (parseError) {
315
- this.logger.error('Failed to parse message content', {
316
- error: parseError.message,
317
- content: messageContent
318
- });
319
- throw parseError;
320
- }
321
- } else {
322
- // Already parsed or direct message
323
- message = rawMessage;
324
- }
276
+ async _initializeCache() {
277
+ const cacheUrl = this.config.wrapper?.cache?.url;
278
+ if (!cacheUrl) {
279
+ console.warn('Cache enabled but no URL provided. Skipping cache initialization');
280
+ return;
281
+ }
325
282
 
326
- // DEBUG: Log the actual message structure
327
- this.logger.info('DEBUG: Received message structure', {
328
- workflow_id: message.workflow_id,
329
- has_cookbook: !!message.cookbook,
330
- cookbook_name: message.cookbook?.name,
331
- cookbook_steps: message.cookbook?.steps?.length,
332
- first_step: message.cookbook?.steps?.[0],
333
- current_step: message.current_step
334
- });
283
+ const serviceName = this.config.service?.name || 'unnamed-service';
335
284
 
336
- // Delegate ALL processing to orchestrator
337
- const result = await this.orchestrator.processWorkflowMessage(
338
- message,
339
- this.serviceName
340
- );
285
+ this.cacheConnector = new CacheConnector({
286
+ host: cacheUrl,
287
+ namespace: serviceName,
288
+ defaultTTL: this.config.wrapper?.cache?.ttl || 300
289
+ });
341
290
 
342
- this.logger.info('Message processed successfully', {
343
- workflow_id: message.workflow_id,
344
- step: message.current_step,
345
- result
346
- });
291
+ await this.cacheConnector.connect();
292
+ console.log('Cache connector initialized');
293
+ }
347
294
 
348
- } catch (error) {
349
- this.logger.error('Message processing failed', {
350
- workflow_id: message?.workflow_id || 'unknown',
351
- step: message?.current_step || 'unknown',
352
- error: error.message
353
- });
295
+ /**
296
+ * Setup health check endpoint
297
+ * @private
298
+ */
299
+ _setupHealthChecks() {
300
+ const healthEndpoint = this.config.wrapper?.health?.endpoint || '/health';
301
+
302
+ this.app.get(healthEndpoint, (req, res) => {
303
+ const health = {
304
+ status: 'healthy',
305
+ service: this.config.service?.name || 'unnamed-service',
306
+ timestamp: new Date().toISOString(),
307
+ components: {
308
+ http: 'healthy',
309
+ mq: this.mqClient?.isConnected() ? 'healthy' : 'unhealthy',
310
+ registry: this.registryClient ? 'healthy' : 'disabled',
311
+ cache: this.cacheConnector ? 'healthy' : 'disabled'
354
312
  }
313
+ };
314
+
315
+ // Determine overall status
316
+ const statuses = Object.values(health.components);
317
+ if (statuses.includes('unhealthy')) {
318
+ health.status = 'unhealthy';
319
+ res.status(503);
320
+ } else {
321
+ res.status(200);
322
+ }
323
+
324
+ res.json(health);
325
+ });
326
+
327
+ console.log(`Health check endpoint registered at ${healthEndpoint}`);
328
+ }
329
+
330
+ /**
331
+ * Initialize orchestrator for workflow processing
332
+ * @private
333
+ */
334
+ async _initializeOrchestrator() {
335
+ const servicePort = this.config.service?.port || process.env.PORT || 3000;
336
+ const serviceUrl = `http://localhost:${servicePort}`;
337
+
338
+ // Create error handler
339
+ const errorHandler = new ErrorHandlerConnector({
340
+ maxRetries: this.config.wrapper?.errorHandling?.maxRetries || 3,
341
+ retryDelay: this.config.wrapper?.errorHandling?.retryDelay || 1000,
342
+ logger: this.logger || console
343
+ });
344
+
345
+ // Create API mapper for HTTP calls (never direct calls)
346
+ const apiMapper = ApiMapperConnector.create({
347
+ openApiSpec: this.operations, // Using operations.json instead of OpenAPI
348
+ serviceUrl: serviceUrl,
349
+ directCall: false, // ALWAYS use HTTP pattern
350
+ logger: this.logger || console
351
+ });
352
+
353
+ // Create orchestrator
354
+ this.orchestrator = OrchestratorConnector.create({
355
+ mqClient: this.mqClient,
356
+ registryClient: this.registryClient,
357
+ apiMapper: apiMapper,
358
+ cookbook: CookbookConnector,
359
+ cache: this.cacheConnector,
360
+ errorHandler: errorHandler,
361
+ logger: this.logger || console,
362
+ defaultTimeout: this.config.wrapper?.timeout || 30000
363
+ });
364
+
365
+ console.log('Orchestrator initialized');
366
+ }
367
+
368
+ /**
369
+ * Subscribe to workflow queues
370
+ * @private
371
+ */
372
+ async _subscribeToQueues() {
373
+ const serviceName = this.config.service?.name || 'unnamed-service';
374
+
375
+ // Subscribe to workflow.init queue (for all services)
376
+ await this.mqClient.consume('workflow.init', async (message) => {
377
+ await this._processWorkflowMessage(message, 'workflow.init');
378
+ });
379
+
380
+ // Subscribe to service-specific queue
381
+ const serviceQueue = `${serviceName}.workflow`;
382
+ await this.mqClient.consume(serviceQueue, async (message) => {
383
+ await this._processWorkflowMessage(message, serviceQueue);
384
+ });
385
+
386
+ console.log(`Subscribed to queues: workflow.init, ${serviceQueue}`);
387
+ }
388
+
389
+ /**
390
+ * Process workflow message
391
+ * @private
392
+ */
393
+ async _processWorkflowMessage(rawMessage, queueName) {
394
+ try {
395
+ // Parse message
396
+ let message;
397
+ if (rawMessage && rawMessage.content) {
398
+ // AMQP message with content buffer
399
+ const messageContent = rawMessage.content.toString();
400
+ message = JSON.parse(messageContent);
401
+ } else {
402
+ // Already parsed message
403
+ message = rawMessage;
404
+ }
405
+
406
+ console.log(`Processing message from ${queueName}:`, {
407
+ workflow_id: message.workflow_id,
408
+ step: message.step?.operation || message.operation
409
+ });
410
+
411
+ // Check if this message is for our service
412
+ const serviceName = this.config.service?.name || 'unnamed-service';
413
+ if (message.step?.service && message.step.service !== serviceName) {
414
+ console.log(`Message not for this service (target: ${message.step.service})`);
415
+ return;
416
+ }
417
+
418
+ // Process based on message type
419
+ if (message.operation && this.operations?.operations?.[message.operation]) {
420
+ // Direct operation call
421
+ const result = await this._executeOperation(message.operation, message.input || {});
422
+ return result;
423
+ } else if (message.step?.operation && this.operations?.operations?.[message.step.operation]) {
424
+ // Workflow step
425
+ const result = await this._executeOperation(message.step.operation, message.input || {});
426
+ return result;
427
+ } else if (this.orchestrator) {
428
+ // Delegate to orchestrator for complex workflow processing
429
+ const result = await this.orchestrator.processWorkflowMessage(message, serviceName);
430
+ return result;
431
+ } else {
432
+ throw new Error(`Unknown message format or operation: ${JSON.stringify(message)}`);
355
433
  }
356
- );
357
434
 
358
- this.logger.info(`Subscribed to queue: ${queueName}`);
435
+ } catch (error) {
436
+ console.error(`Error processing message from ${queueName}:`, error);
437
+ throw error;
438
+ }
359
439
  }
360
440
 
361
441
  /**
362
- * Start service heartbeat
442
+ * Execute operation by calling HTTP endpoint
363
443
  * @private
364
444
  */
365
- _startHeartbeat() {
366
- this.heartbeatInterval = setInterval(async () => {
367
- try {
368
- await this.registryClient.sendHeartbeat(this.serviceName);
369
- this.logger.debug(`Heartbeat sent for ${this.serviceName}`);
370
- } catch (error) {
371
- this.logger.error('Heartbeat failed', { error: error.message });
445
+ async _executeOperation(operationName, input) {
446
+ const operation = this.operations?.operations?.[operationName];
447
+ if (!operation) {
448
+ throw new Error(`Unknown operation: ${operationName}`);
449
+ }
450
+
451
+ const servicePort = this.config.service?.port || process.env.PORT || 3000;
452
+ const url = `http://localhost:${servicePort}${operation.endpoint}`;
453
+ const method = operation.method || 'POST';
454
+
455
+ console.log(`Executing operation ${operationName} via ${method} ${url}`);
456
+
457
+ // Make HTTP request using fetch (Node.js 18+) or http module
458
+ const http = require('http');
459
+
460
+ return new Promise((resolve, reject) => {
461
+ const postData = JSON.stringify(input);
462
+
463
+ const options = {
464
+ hostname: 'localhost',
465
+ port: servicePort,
466
+ path: operation.endpoint,
467
+ method: method,
468
+ headers: {
469
+ 'Content-Type': 'application/json',
470
+ 'Content-Length': Buffer.byteLength(postData)
471
+ }
472
+ };
473
+
474
+ const req = http.request(options, (res) => {
475
+ let data = '';
476
+
477
+ res.on('data', (chunk) => {
478
+ data += chunk;
479
+ });
480
+
481
+ res.on('end', () => {
482
+ try {
483
+ const result = JSON.parse(data);
484
+ if (res.statusCode >= 200 && res.statusCode < 300) {
485
+ resolve(result);
486
+ } else {
487
+ reject(new Error(`Operation failed: ${result.error || data}`));
488
+ }
489
+ } catch (error) {
490
+ reject(new Error(`Invalid response: ${data}`));
491
+ }
492
+ });
493
+ });
494
+
495
+ req.on('error', (error) => {
496
+ reject(error);
497
+ });
498
+
499
+ req.write(postData);
500
+ req.end();
501
+ });
502
+ }
503
+
504
+ /**
505
+ * Shutdown wrapper and cleanup resources
506
+ * @async
507
+ */
508
+ async shutdown() {
509
+ const serviceName = this.config.service?.name || 'unnamed-service';
510
+ console.log(`Shutting down ServiceWrapper for ${serviceName}`);
511
+
512
+ try {
513
+ // Stop heartbeat
514
+ if (this.heartbeatTimer) {
515
+ clearInterval(this.heartbeatTimer);
516
+ }
517
+
518
+ // Unregister from registry
519
+ if (this.registryClient) {
520
+ await this.registryClient.unregister(serviceName);
521
+ }
522
+
523
+ // Disconnect from cache
524
+ if (this.cacheConnector) {
525
+ await this.cacheConnector.disconnect();
526
+ }
527
+
528
+ // Disconnect from MQ
529
+ if (this.mqClient) {
530
+ await this.mqClient.disconnect();
372
531
  }
373
- }, this.config.heartbeatInterval || 30000);
532
+
533
+ this.isInitialized = false;
534
+ console.log('ServiceWrapper shutdown complete');
535
+
536
+ } catch (error) {
537
+ console.error('Error during shutdown:', error);
538
+ throw error;
539
+ }
374
540
  }
375
541
 
376
542
  /**
377
543
  * Get wrapper status
378
- * @method getStatus
379
- * @returns {Object} Status information
380
- *
381
- * @example
382
- * const status = wrapper.getStatus();
383
- * console.log('Wrapper status:', status);
384
544
  */
385
545
  getStatus() {
386
546
  return {
387
- serviceName: this.serviceName,
388
- isRunning: this.isRunning,
389
- mqConnected: this.mqClient?.isConnected() || false,
390
- registryConnected: this.registryClient?.isConnected() || false,
391
- config: {
392
- port: this.config.port || 3000,
393
- prefetch: this.config.prefetch || 10,
394
- heartbeatInterval: this.config.heartbeatInterval || 30000
547
+ initialized: this.isInitialized,
548
+ service: this.config.service?.name || 'unnamed-service',
549
+ components: {
550
+ mq: this.mqClient?.isConnected() || false,
551
+ registry: !!this.registryClient,
552
+ cache: !!this.cacheConnector,
553
+ monitoring: !!this.monitoring
395
554
  }
396
555
  };
397
556
  }