@onlineapps/service-wrapper 2.0.7 → 2.0.9

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,19 +1,21 @@
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
14
16
  const MQConnector = require('@onlineapps/conn-infra-mq');
15
17
  const RegistryConnector = require('@onlineapps/conn-orch-registry');
16
- const LoggerConnector = require('@onlineapps/conn-base-logger');
18
+ const MonitoringConnector = require('@onlineapps/conn-base-monitoring');
17
19
  const OrchestratorConnector = require('@onlineapps/conn-orch-orchestrator');
18
20
  const ApiMapperConnector = require('@onlineapps/conn-orch-api-mapper');
19
21
  const CookbookConnector = require('@onlineapps/conn-orch-cookbook');
@@ -21,373 +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 = new LoggerConnector({ serviceName: this.serviceName });
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
73
48
  this.mqClient = null;
74
49
  this.registryClient = null;
75
50
  this.orchestrator = null;
76
51
  this.cacheConnector = null;
52
+ this.monitoring = null;
53
+ this.logger = null;
77
54
 
78
55
  // State
79
- this.isRunning = false;
56
+ this.isInitialized = false;
80
57
  }
81
58
 
82
59
  /**
83
60
  * Validate constructor options
84
61
  * @private
85
- * @param {Object} options - Options to validate
86
- * @throws {Error} If required options are missing
87
62
  */
88
63
  _validateOptions(options) {
89
- if (!options.service && !options.serviceUrl) {
90
- 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');
91
69
  }
92
- if (!options.serviceName) {
93
- throw new Error('Service name is required');
70
+ if (!options.config) {
71
+ throw new Error('Configuration is required');
94
72
  }
95
- if (!options.openApiSpec) {
96
- throw new Error('OpenAPI specification is required');
73
+ if (!options.operations) {
74
+ throw new Error('Operations schema is required');
97
75
  }
98
76
  }
99
77
 
100
78
  /**
101
- * Start the service wrapper
102
- * @async
103
- * @method start
104
- * @returns {Promise<void>}
105
- *
106
- * @example
107
- * await wrapper.start();
108
- * console.log('Service wrapper started');
79
+ * Process and validate configuration
80
+ * @private
109
81
  */
110
- async start() {
111
- if (this.isRunning) {
112
- this.logger.warn('Service wrapper already running');
113
- return;
114
- }
115
-
116
- try {
117
- this.logger.info(`Starting service wrapper for ${this.serviceName}`);
118
-
119
- // 1. Initialize infrastructure connectors
120
- await this._initializeConnectors();
121
-
122
- // 2. Initialize cache connector if Redis is configured
123
- if (this.config.redis) {
124
- this.cacheConnector = new CacheConnector({
125
- host: this.config.redis,
126
- namespace: this.serviceName,
127
- defaultTTL: this.config.cacheTTL || 300
128
- });
129
- await this.cacheConnector.connect();
130
- 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 || '';
131
89
  }
90
+ return value;
91
+ };
132
92
 
133
- // 3. Initialize error handler
134
- const errorHandler = new ErrorHandlerConnector({
135
- maxRetries: this.config.maxRetries || 3,
136
- retryDelay: this.config.retryDelay || 1000,
137
- logger: this.logger
138
- });
139
-
140
- // 4. Create the orchestrator with all dependencies
141
- // Determine if we should use direct calls or HTTP
142
- const useDirectCall = this.config.directCall !== undefined
143
- ? this.config.directCall
144
- : (this.service && !this.serviceUrl); // Auto-detect based on what was provided
145
-
146
- const serviceUrl = this.serviceUrl || `http://localhost:${this.config.port || 3000}`;
147
-
148
- this.orchestrator = OrchestratorConnector.create({
149
- mqClient: this.mqClient,
150
- registryClient: this.registryClient,
151
- apiMapper: ApiMapperConnector.create({
152
- openApiSpec: this.openApiSpec,
153
- serviceUrl: serviceUrl,
154
- service: this.service,
155
- directCall: useDirectCall,
156
- logger: this.logger
157
- }),
158
- cookbook: CookbookConnector,
159
- cache: this.cacheConnector,
160
- errorHandler: errorHandler,
161
- logger: this.logger,
162
- defaultTimeout: this.config.defaultTimeout || 30000
163
- });
164
-
165
- // 5. Register service with registry
166
- await this._registerService();
167
-
168
- // 6. Subscribe to workflow messages
169
- await this._subscribeToQueues();
170
-
171
- // 7. Start heartbeat
172
- this._startHeartbeat();
173
-
174
- this.isRunning = true;
175
- 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
+ };
176
105
 
177
- } catch (error) {
178
- this.logger.error('Failed to start service wrapper', { error: error.message });
179
- throw error;
180
- }
106
+ return processObject(config);
181
107
  }
182
108
 
183
109
  /**
184
- * Stop the service wrapper
110
+ * Initialize all wrapper components
185
111
  * @async
186
- * @method stop
187
- * @returns {Promise<void>}
188
- *
189
- * @example
190
- * await wrapper.stop();
191
- * console.log('Service wrapper stopped');
192
112
  */
193
- async stop() {
194
- if (!this.isRunning) {
195
- this.logger.warn('Service wrapper not running');
113
+ async initialize() {
114
+ if (this.isInitialized) {
115
+ console.warn('ServiceWrapper already initialized');
196
116
  return;
197
117
  }
198
118
 
199
119
  try {
200
- 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}`);
201
122
 
202
- // Stop heartbeat
203
- if (this.heartbeatInterval) {
204
- clearInterval(this.heartbeatInterval);
123
+ // 1. Initialize monitoring first (needed for logging)
124
+ if (this.config.wrapper?.monitoring?.enabled !== false) {
125
+ await this._initializeMonitoring();
205
126
  }
206
127
 
207
- // Unregister from registry
208
- if (this.registryClient) {
209
- await this.registryClient.unregister(this.serviceName);
128
+ // 2. Initialize MQ connection
129
+ if (this.config.wrapper?.mq?.enabled !== false) {
130
+ await this._initializeMQ();
210
131
  }
211
132
 
212
- // Disconnect from cache
213
- if (this.cacheConnector) {
214
- await this.cacheConnector.disconnect();
133
+ // 3. Initialize service registry
134
+ if (this.config.wrapper?.registry?.enabled !== false) {
135
+ await this._initializeRegistry();
215
136
  }
216
137
 
217
- // 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
218
149
  if (this.mqClient) {
219
- await this.mqClient.disconnect();
150
+ await this._initializeOrchestrator();
151
+ await this._subscribeToQueues();
220
152
  }
221
153
 
222
- this.isRunning = false;
223
- this.logger.info(`Service wrapper stopped successfully for ${this.serviceName}`);
154
+ this.isInitialized = true;
155
+ console.log(`ServiceWrapper initialized successfully for ${serviceName}`);
224
156
 
225
157
  } catch (error) {
226
- this.logger.error('Error stopping service wrapper', { error: error.message });
158
+ console.error('Failed to initialize ServiceWrapper:', error);
227
159
  throw error;
228
160
  }
229
161
  }
230
162
 
231
163
  /**
232
- * 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
233
201
  * @private
234
- * @async
235
- * @returns {Promise<void>}
236
202
  */
237
- async _initializeConnectors() {
238
- // Initialize MQ client with correct config format
239
- 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';
240
210
 
241
211
  this.mqClient = new MQConnector({
242
212
  type: 'rabbitmq',
243
- host: rabbitUrl,
244
- queue: `${this.serviceName}.workflow`,
245
- prefetch: this.config.prefetch || 10,
213
+ host: mqUrl,
214
+ queue: `${serviceName}.workflow`,
215
+ prefetch: this.config.wrapper?.mq?.prefetch || 10,
246
216
  durable: true,
247
217
  noAck: false,
248
- logger: {
249
- info: (...args) => this.logger.info(...args),
250
- warn: (...args) => this.logger.warn(...args),
251
- error: (...args) => this.logger.error(...args),
252
- debug: (...args) => this.logger.debug(...args)
253
- }
218
+ logger: this.logger || console
254
219
  });
255
- await this.mqClient.connect();
256
220
 
257
- // Initialize registry client with required parameters
258
- this.registryClient = new RegistryConnector.ServiceRegistryClient({
259
- amqpUrl: rabbitUrl,
260
- serviceName: this.serviceName,
261
- version: this.openApiSpec?.info?.version || '1.0.0',
262
- registryUrl: this.config.registry || process.env.REGISTRY_URL || 'http://localhost:4000',
263
- logger: this.logger
264
- });
221
+ await this.mqClient.connect();
222
+ console.log('MQ connector initialized');
265
223
  }
266
224
 
267
225
  /**
268
- * Register service with registry
226
+ * Initialize service registry
269
227
  * @private
270
- * @async
271
- * @returns {Promise<void>}
272
228
  */
273
- async _registerService() {
274
- 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
275
248
  const serviceInfo = {
276
- name: this.serviceName,
277
- url: serviceUrl,
278
- openapi: this.openApiSpec,
249
+ name: serviceName,
250
+ url: `http://localhost:${servicePort}`,
251
+ operations: this.operations?.operations || {},
279
252
  metadata: {
280
- version: this.openApiSpec.info?.version || '1.0.0',
281
- description: this.openApiSpec.info?.description || ''
253
+ version: this.config.service?.version || '1.0.0',
254
+ description: this.config.service?.description || ''
282
255
  }
283
256
  };
284
257
 
285
258
  await this.registryClient.register(serviceInfo);
286
- 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);
287
270
  }
288
271
 
289
272
  /**
290
- * Subscribe to workflow message queues
273
+ * Initialize cache connector
291
274
  * @private
292
- * @async
293
- * @returns {Promise<void>}
294
275
  */
295
- async _subscribeToQueues() {
296
- const queueName = `${this.serviceName}.workflow`;
297
-
298
- // Subscribe to workflow messages
299
- await this.mqClient.consume(
300
- queueName,
301
- async (rawMessage) => {
302
- try {
303
- // Parse message content if it's a buffer (AMQP message)
304
- let message;
305
- if (rawMessage && rawMessage.content) {
306
- // This is an AMQP message with content buffer
307
- const messageContent = rawMessage.content.toString();
308
- try {
309
- message = JSON.parse(messageContent);
310
- } catch (parseError) {
311
- this.logger.error('Failed to parse message content', {
312
- error: parseError.message,
313
- content: messageContent
314
- });
315
- throw parseError;
316
- }
317
- } else {
318
- // Already parsed or direct message
319
- message = rawMessage;
320
- }
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
+ }
321
282
 
322
- // DEBUG: Log the actual message structure
323
- this.logger.info('DEBUG: Received message structure', {
324
- workflow_id: message.workflow_id,
325
- has_cookbook: !!message.cookbook,
326
- cookbook_name: message.cookbook?.name,
327
- cookbook_steps: message.cookbook?.steps?.length,
328
- first_step: message.cookbook?.steps?.[0],
329
- current_step: message.current_step
330
- });
283
+ const serviceName = this.config.service?.name || 'unnamed-service';
331
284
 
332
- // Delegate ALL processing to orchestrator
333
- const result = await this.orchestrator.processWorkflowMessage(
334
- message,
335
- this.serviceName
336
- );
285
+ this.cacheConnector = new CacheConnector({
286
+ host: cacheUrl,
287
+ namespace: serviceName,
288
+ defaultTTL: this.config.wrapper?.cache?.ttl || 300
289
+ });
337
290
 
338
- this.logger.info('Message processed successfully', {
339
- workflow_id: message.workflow_id,
340
- step: message.current_step,
341
- result
342
- });
291
+ await this.cacheConnector.connect();
292
+ console.log('Cache connector initialized');
293
+ }
343
294
 
344
- } catch (error) {
345
- this.logger.error('Message processing failed', {
346
- workflow_id: message?.workflow_id || 'unknown',
347
- step: message?.current_step || 'unknown',
348
- error: error.message
349
- });
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'
350
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);
351
322
  }
352
- );
353
323
 
354
- this.logger.info(`Subscribed to queue: ${queueName}`);
324
+ res.json(health);
325
+ });
326
+
327
+ console.log(`Health check endpoint registered at ${healthEndpoint}`);
355
328
  }
356
329
 
357
330
  /**
358
- * Start service heartbeat
331
+ * Initialize orchestrator for workflow processing
359
332
  * @private
360
333
  */
361
- _startHeartbeat() {
362
- this.heartbeatInterval = setInterval(async () => {
363
- try {
364
- await this.registryClient.sendHeartbeat(this.serviceName);
365
- this.logger.debug(`Heartbeat sent for ${this.serviceName}`);
366
- } catch (error) {
367
- this.logger.error('Heartbeat failed', { error: error.message });
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;
368
404
  }
369
- }, this.config.heartbeatInterval || 30000);
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)}`);
433
+ }
434
+
435
+ } catch (error) {
436
+ console.error(`Error processing message from ${queueName}:`, error);
437
+ throw error;
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Execute operation by calling HTTP endpoint
443
+ * @private
444
+ */
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();
531
+ }
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
+ }
370
540
  }
371
541
 
372
542
  /**
373
543
  * Get wrapper status
374
- * @method getStatus
375
- * @returns {Object} Status information
376
- *
377
- * @example
378
- * const status = wrapper.getStatus();
379
- * console.log('Wrapper status:', status);
380
544
  */
381
545
  getStatus() {
382
546
  return {
383
- serviceName: this.serviceName,
384
- isRunning: this.isRunning,
385
- mqConnected: this.mqClient?.isConnected() || false,
386
- registryConnected: this.registryClient?.isConnected() || false,
387
- config: {
388
- port: this.config.port || 3000,
389
- prefetch: this.config.prefetch || 10,
390
- 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
391
554
  }
392
555
  };
393
556
  }