@onlineapps/service-wrapper 2.0.0
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/API.md +336 -0
- package/README.md +185 -0
- package/build-docs.js +54 -0
- package/docs/PERFORMANCE.md +453 -0
- package/docs/PROCESS_FLOWS.md +389 -0
- package/jest.config.js +34 -0
- package/jsdoc.json +22 -0
- package/package.json +44 -0
- package/src/ServiceWrapper.js +343 -0
- package/src/index.js +23 -0
- package/test/component/ServiceWrapper.component.test.js +407 -0
- package/test/component/connector-integration.test.js +293 -0
- package/test/integration/orchestrator-integration.test.js +170 -0
- package/test/mocks/connectors.js +304 -0
- package/test/run-tests.js +135 -0
- package/test/setup.js +31 -0
- package/test/unit/ServiceWrapper.test.js +372 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
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.
|
|
7
|
+
*
|
|
8
|
+
* @author OA Drive Team
|
|
9
|
+
* @license MIT
|
|
10
|
+
* @since 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Import connectors
|
|
14
|
+
const MQConnector = require('@onlineapps/conn-infra-mq');
|
|
15
|
+
const RegistryConnector = require('@onlineapps/conn-orch-registry');
|
|
16
|
+
const LoggerConnector = require('@onlineapps/conn-base-logger');
|
|
17
|
+
const OrchestratorConnector = require('@onlineapps/conn-orch-orchestrator');
|
|
18
|
+
const ApiMapperConnector = require('@onlineapps/conn-orch-api-mapper');
|
|
19
|
+
const CookbookConnector = require('@onlineapps/conn-orch-cookbook');
|
|
20
|
+
const CacheConnector = require('@onlineapps/conn-base-cache');
|
|
21
|
+
const ErrorHandlerConnector = require('@onlineapps/conn-infra-error-handler');
|
|
22
|
+
|
|
23
|
+
/**
|
|
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();
|
|
41
|
+
*/
|
|
42
|
+
class ServiceWrapper {
|
|
43
|
+
/**
|
|
44
|
+
* Create a new ServiceWrapper instance
|
|
45
|
+
* @constructor
|
|
46
|
+
* @param {Object} options - Configuration options
|
|
47
|
+
* @param {Object} options.service - Express application instance
|
|
48
|
+
* @param {string} options.serviceName - Name of the service
|
|
49
|
+
* @param {Object} options.openApiSpec - OpenAPI specification
|
|
50
|
+
* @param {Object} [options.config={}] - Infrastructure configuration
|
|
51
|
+
* @param {string} [options.config.rabbitmq] - RabbitMQ connection URL
|
|
52
|
+
* @param {string} [options.config.redis] - Redis connection string
|
|
53
|
+
* @param {string} [options.config.registry] - Registry service URL
|
|
54
|
+
* @param {number} [options.config.port=3000] - Service port
|
|
55
|
+
* @param {number} [options.config.prefetch=10] - MQ prefetch count
|
|
56
|
+
*
|
|
57
|
+
* @throws {Error} If required options are missing
|
|
58
|
+
*/
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
this._validateOptions(options);
|
|
61
|
+
|
|
62
|
+
// Store configuration
|
|
63
|
+
this.service = options.service;
|
|
64
|
+
this.serviceName = options.serviceName;
|
|
65
|
+
this.openApiSpec = options.openApiSpec;
|
|
66
|
+
this.config = options.config || {};
|
|
67
|
+
|
|
68
|
+
// Initialize connectors
|
|
69
|
+
this.logger = LoggerConnector.create(this.serviceName);
|
|
70
|
+
this.mqClient = null;
|
|
71
|
+
this.registryClient = null;
|
|
72
|
+
this.orchestrator = null;
|
|
73
|
+
this.cacheConnector = null;
|
|
74
|
+
|
|
75
|
+
// State
|
|
76
|
+
this.isRunning = false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate constructor options
|
|
81
|
+
* @private
|
|
82
|
+
* @param {Object} options - Options to validate
|
|
83
|
+
* @throws {Error} If required options are missing
|
|
84
|
+
*/
|
|
85
|
+
_validateOptions(options) {
|
|
86
|
+
if (!options.service) {
|
|
87
|
+
throw new Error('Service (Express app) is required');
|
|
88
|
+
}
|
|
89
|
+
if (!options.serviceName) {
|
|
90
|
+
throw new Error('Service name is required');
|
|
91
|
+
}
|
|
92
|
+
if (!options.openApiSpec) {
|
|
93
|
+
throw new Error('OpenAPI specification is required');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Start the service wrapper
|
|
99
|
+
* @async
|
|
100
|
+
* @method start
|
|
101
|
+
* @returns {Promise<void>}
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* await wrapper.start();
|
|
105
|
+
* console.log('Service wrapper started');
|
|
106
|
+
*/
|
|
107
|
+
async start() {
|
|
108
|
+
if (this.isRunning) {
|
|
109
|
+
this.logger.warn('Service wrapper already running');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
this.logger.info(`Starting service wrapper for ${this.serviceName}`);
|
|
115
|
+
|
|
116
|
+
// 1. Initialize infrastructure connectors
|
|
117
|
+
await this._initializeConnectors();
|
|
118
|
+
|
|
119
|
+
// 2. Initialize cache connector if Redis is configured
|
|
120
|
+
if (this.config.redis) {
|
|
121
|
+
this.cacheConnector = new CacheConnector({
|
|
122
|
+
host: this.config.redis,
|
|
123
|
+
namespace: this.serviceName,
|
|
124
|
+
defaultTTL: this.config.cacheTTL || 300
|
|
125
|
+
});
|
|
126
|
+
await this.cacheConnector.connect();
|
|
127
|
+
this.logger.info('Cache connector initialized');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 3. Initialize error handler
|
|
131
|
+
const errorHandler = new ErrorHandlerConnector({
|
|
132
|
+
maxRetries: this.config.maxRetries || 3,
|
|
133
|
+
retryDelay: this.config.retryDelay || 1000,
|
|
134
|
+
logger: this.logger
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 4. Create the orchestrator with all dependencies
|
|
138
|
+
this.orchestrator = OrchestratorConnector.create({
|
|
139
|
+
mqClient: this.mqClient,
|
|
140
|
+
registryClient: this.registryClient,
|
|
141
|
+
apiMapper: ApiMapperConnector.create({
|
|
142
|
+
openApiSpec: this.openApiSpec,
|
|
143
|
+
serviceUrl: `http://localhost:${this.config.port || 3000}`,
|
|
144
|
+
service: this.service,
|
|
145
|
+
directCall: true,
|
|
146
|
+
logger: this.logger
|
|
147
|
+
}),
|
|
148
|
+
cookbook: CookbookConnector,
|
|
149
|
+
cache: this.cacheConnector,
|
|
150
|
+
errorHandler: errorHandler,
|
|
151
|
+
logger: this.logger,
|
|
152
|
+
defaultTimeout: this.config.defaultTimeout || 30000
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 5. Register service with registry
|
|
156
|
+
await this._registerService();
|
|
157
|
+
|
|
158
|
+
// 6. Subscribe to workflow messages
|
|
159
|
+
await this._subscribeToQueues();
|
|
160
|
+
|
|
161
|
+
// 7. Start heartbeat
|
|
162
|
+
this._startHeartbeat();
|
|
163
|
+
|
|
164
|
+
this.isRunning = true;
|
|
165
|
+
this.logger.info(`Service wrapper started successfully for ${this.serviceName}`);
|
|
166
|
+
|
|
167
|
+
} catch (error) {
|
|
168
|
+
this.logger.error('Failed to start service wrapper', { error: error.message });
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Stop the service wrapper
|
|
175
|
+
* @async
|
|
176
|
+
* @method stop
|
|
177
|
+
* @returns {Promise<void>}
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* await wrapper.stop();
|
|
181
|
+
* console.log('Service wrapper stopped');
|
|
182
|
+
*/
|
|
183
|
+
async stop() {
|
|
184
|
+
if (!this.isRunning) {
|
|
185
|
+
this.logger.warn('Service wrapper not running');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
this.logger.info(`Stopping service wrapper for ${this.serviceName}`);
|
|
191
|
+
|
|
192
|
+
// Stop heartbeat
|
|
193
|
+
if (this.heartbeatInterval) {
|
|
194
|
+
clearInterval(this.heartbeatInterval);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Unregister from registry
|
|
198
|
+
if (this.registryClient) {
|
|
199
|
+
await this.registryClient.unregister(this.serviceName);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Disconnect from cache
|
|
203
|
+
if (this.cacheConnector) {
|
|
204
|
+
await this.cacheConnector.disconnect();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Disconnect from MQ
|
|
208
|
+
if (this.mqClient) {
|
|
209
|
+
await this.mqClient.disconnect();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.isRunning = false;
|
|
213
|
+
this.logger.info(`Service wrapper stopped successfully for ${this.serviceName}`);
|
|
214
|
+
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.logger.error('Error stopping service wrapper', { error: error.message });
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Initialize infrastructure connectors
|
|
223
|
+
* @private
|
|
224
|
+
* @async
|
|
225
|
+
* @returns {Promise<void>}
|
|
226
|
+
*/
|
|
227
|
+
async _initializeConnectors() {
|
|
228
|
+
// Initialize MQ client
|
|
229
|
+
this.mqClient = new MQConnector({
|
|
230
|
+
url: this.config.rabbitmq || process.env.RABBITMQ_URL || 'amqp://localhost:5672',
|
|
231
|
+
serviceName: this.serviceName,
|
|
232
|
+
prefetchCount: this.config.prefetch || 10,
|
|
233
|
+
logger: this.logger
|
|
234
|
+
});
|
|
235
|
+
await this.mqClient.connect();
|
|
236
|
+
|
|
237
|
+
// Initialize registry client
|
|
238
|
+
this.registryClient = new RegistryConnector.ServiceRegistryClient({
|
|
239
|
+
registryUrl: this.config.registry || process.env.REGISTRY_URL || 'http://localhost:4000',
|
|
240
|
+
logger: this.logger
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Register service with registry
|
|
246
|
+
* @private
|
|
247
|
+
* @async
|
|
248
|
+
* @returns {Promise<void>}
|
|
249
|
+
*/
|
|
250
|
+
async _registerService() {
|
|
251
|
+
const serviceInfo = {
|
|
252
|
+
name: this.serviceName,
|
|
253
|
+
url: `http://localhost:${this.config.port || 3000}`,
|
|
254
|
+
openapi: this.openApiSpec,
|
|
255
|
+
metadata: {
|
|
256
|
+
version: this.openApiSpec.info?.version || '1.0.0',
|
|
257
|
+
description: this.openApiSpec.info?.description || ''
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
await this.registryClient.register(serviceInfo);
|
|
262
|
+
this.logger.info(`Service registered: ${this.serviceName}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Subscribe to workflow message queues
|
|
267
|
+
* @private
|
|
268
|
+
* @async
|
|
269
|
+
* @returns {Promise<void>}
|
|
270
|
+
*/
|
|
271
|
+
async _subscribeToQueues() {
|
|
272
|
+
const queueName = `${this.serviceName}.workflow`;
|
|
273
|
+
|
|
274
|
+
// Subscribe to workflow messages
|
|
275
|
+
await this.mqClient.consume(
|
|
276
|
+
async (message) => {
|
|
277
|
+
try {
|
|
278
|
+
// Delegate ALL processing to orchestrator
|
|
279
|
+
const result = await this.orchestrator.processWorkflowMessage(
|
|
280
|
+
message,
|
|
281
|
+
this.serviceName
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
this.logger.info('Message processed successfully', {
|
|
285
|
+
workflow_id: message.workflow_id,
|
|
286
|
+
step: message.current_step,
|
|
287
|
+
result
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
} catch (error) {
|
|
291
|
+
this.logger.error('Message processing failed', {
|
|
292
|
+
workflow_id: message.workflow_id,
|
|
293
|
+
step: message.current_step,
|
|
294
|
+
error: error.message
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{ queue: queueName }
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
this.logger.info(`Subscribed to queue: ${queueName}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Start service heartbeat
|
|
306
|
+
* @private
|
|
307
|
+
*/
|
|
308
|
+
_startHeartbeat() {
|
|
309
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
310
|
+
try {
|
|
311
|
+
await this.registryClient.sendHeartbeat(this.serviceName);
|
|
312
|
+
this.logger.debug(`Heartbeat sent for ${this.serviceName}`);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
this.logger.error('Heartbeat failed', { error: error.message });
|
|
315
|
+
}
|
|
316
|
+
}, this.config.heartbeatInterval || 30000);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get wrapper status
|
|
321
|
+
* @method getStatus
|
|
322
|
+
* @returns {Object} Status information
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* const status = wrapper.getStatus();
|
|
326
|
+
* console.log('Wrapper status:', status);
|
|
327
|
+
*/
|
|
328
|
+
getStatus() {
|
|
329
|
+
return {
|
|
330
|
+
serviceName: this.serviceName,
|
|
331
|
+
isRunning: this.isRunning,
|
|
332
|
+
mqConnected: this.mqClient?.isConnected() || false,
|
|
333
|
+
registryConnected: this.registryClient?.isConnected() || false,
|
|
334
|
+
config: {
|
|
335
|
+
port: this.config.port || 3000,
|
|
336
|
+
prefetch: this.config.prefetch || 10,
|
|
337
|
+
heartbeatInterval: this.config.heartbeatInterval || 30000
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = ServiceWrapper;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module @onlineapps/service-wrapper
|
|
5
|
+
* @description Infrastructure orchestration for microservices.
|
|
6
|
+
* Thin layer that delegates all work to specialized connectors.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link https://github.com/onlineapps/oa-drive/tree/main/shared/service-wrapper|GitHub Repository}
|
|
9
|
+
* @author OA Drive Team
|
|
10
|
+
* @license MIT
|
|
11
|
+
* @since 2.0.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const ServiceWrapper = require('./ServiceWrapper');
|
|
15
|
+
|
|
16
|
+
// Note: WorkflowProcessor and ApiCaller functionality has been moved to connectors:
|
|
17
|
+
// - WorkflowProcessor -> @onlineapps/conn-orch-orchestrator
|
|
18
|
+
// - ApiCaller -> @onlineapps/conn-orch-api-mapper
|
|
19
|
+
|
|
20
|
+
module.exports = ServiceWrapper;
|
|
21
|
+
module.exports.ServiceWrapper = ServiceWrapper;
|
|
22
|
+
module.exports.default = ServiceWrapper;
|
|
23
|
+
module.exports.VERSION = '2.0.0';
|