@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.
- package/README.md +66 -39
- package/docs/ARCHITECTURE_DECISION.md +200 -0
- package/docs/FINAL_ARCHITECTURE.md +271 -0
- package/docs/HANDLER_VS_HTTP_COMPARISON.md +269 -0
- package/docs/INSTALLATION_GUIDE.md +353 -0
- package/docs/OPERATIONS_SCHEMA.md +43 -1
- package/docs/WRAPPER_ARCHITECTURE.md +218 -0
- package/package.json +3 -3
- package/src/ServiceWrapper.js +432 -273
package/src/ServiceWrapper.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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}
|
|
48
|
-
* @param {
|
|
49
|
-
* @param {
|
|
50
|
-
* @param {Object} options.
|
|
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.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
|
|
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.
|
|
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.
|
|
91
|
-
throw new Error('
|
|
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.
|
|
94
|
-
throw new Error('
|
|
70
|
+
if (!options.config) {
|
|
71
|
+
throw new Error('Configuration is required');
|
|
95
72
|
}
|
|
96
|
-
if (!options.
|
|
97
|
-
throw new Error('
|
|
73
|
+
if (!options.operations) {
|
|
74
|
+
throw new Error('Operations schema is required');
|
|
98
75
|
}
|
|
99
76
|
}
|
|
100
77
|
|
|
101
78
|
/**
|
|
102
|
-
*
|
|
103
|
-
* @
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
198
|
-
if (
|
|
199
|
-
|
|
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.
|
|
120
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
121
|
+
console.log(`Initializing ServiceWrapper for ${serviceName}`);
|
|
205
122
|
|
|
206
|
-
//
|
|
207
|
-
if (this.
|
|
208
|
-
|
|
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
|
-
//
|
|
212
|
-
if (this.
|
|
213
|
-
await this.
|
|
128
|
+
// 2. Initialize MQ connection
|
|
129
|
+
if (this.config.wrapper?.mq?.enabled !== false) {
|
|
130
|
+
await this._initializeMQ();
|
|
214
131
|
}
|
|
215
132
|
|
|
216
|
-
//
|
|
217
|
-
if (this.
|
|
218
|
-
await this.
|
|
133
|
+
// 3. Initialize service registry
|
|
134
|
+
if (this.config.wrapper?.registry?.enabled !== false) {
|
|
135
|
+
await this._initializeRegistry();
|
|
219
136
|
}
|
|
220
137
|
|
|
221
|
-
//
|
|
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.
|
|
150
|
+
await this._initializeOrchestrator();
|
|
151
|
+
await this._subscribeToQueues();
|
|
224
152
|
}
|
|
225
153
|
|
|
226
|
-
this.
|
|
227
|
-
|
|
154
|
+
this.isInitialized = true;
|
|
155
|
+
console.log(`ServiceWrapper initialized successfully for ${serviceName}`);
|
|
228
156
|
|
|
229
157
|
} catch (error) {
|
|
230
|
-
|
|
158
|
+
console.error('Failed to initialize ServiceWrapper:', error);
|
|
231
159
|
throw error;
|
|
232
160
|
}
|
|
233
161
|
}
|
|
234
162
|
|
|
235
163
|
/**
|
|
236
|
-
* Initialize
|
|
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
|
|
242
|
-
|
|
243
|
-
|
|
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:
|
|
248
|
-
queue: `${
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
*
|
|
226
|
+
* Initialize service registry
|
|
273
227
|
* @private
|
|
274
|
-
* @async
|
|
275
|
-
* @returns {Promise<void>}
|
|
276
228
|
*/
|
|
277
|
-
async
|
|
278
|
-
const
|
|
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:
|
|
281
|
-
url:
|
|
282
|
-
|
|
249
|
+
name: serviceName,
|
|
250
|
+
url: `http://localhost:${servicePort}`,
|
|
251
|
+
operations: this.operations?.operations || {},
|
|
283
252
|
metadata: {
|
|
284
|
-
version: this.
|
|
285
|
-
description: this.
|
|
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
|
-
|
|
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
|
-
*
|
|
273
|
+
* Initialize cache connector
|
|
295
274
|
* @private
|
|
296
|
-
* @async
|
|
297
|
-
* @returns {Promise<void>}
|
|
298
275
|
*/
|
|
299
|
-
async
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
285
|
+
this.cacheConnector = new CacheConnector({
|
|
286
|
+
host: cacheUrl,
|
|
287
|
+
namespace: serviceName,
|
|
288
|
+
defaultTTL: this.config.wrapper?.cache?.ttl || 300
|
|
289
|
+
});
|
|
341
290
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
result
|
|
346
|
-
});
|
|
291
|
+
await this.cacheConnector.connect();
|
|
292
|
+
console.log('Cache connector initialized');
|
|
293
|
+
}
|
|
347
294
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error(`Error processing message from ${queueName}:`, error);
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
359
439
|
}
|
|
360
440
|
|
|
361
441
|
/**
|
|
362
|
-
*
|
|
442
|
+
* Execute operation by calling HTTP endpoint
|
|
363
443
|
* @private
|
|
364
444
|
*/
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
}
|