@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.
@@ -0,0 +1,389 @@
1
+ # Service Wrapper - Process Flows
2
+
3
+ ## Complete Message Processing Flow
4
+
5
+ ### 1. Message Arrival
6
+ ```javascript
7
+ // Message structure from RabbitMQ
8
+ {
9
+ properties: {
10
+ headers: {
11
+ workflowId: 'wf-550e8400-e29b',
12
+ spanId: 'span-7d3f-4b2a',
13
+ parentSpanId: 'span-6c2e-3a1b',
14
+ userId: 'user-456',
15
+ tenantId: 'tenant-789',
16
+ priority: 'normal'
17
+ }
18
+ },
19
+ content: Buffer.from(JSON.stringify({
20
+ cookbook: { /* workflow definition */ },
21
+ current_step: 2,
22
+ context: { /* accumulated results */ }
23
+ }))
24
+ }
25
+ ```
26
+
27
+ ### 2. Context Extraction
28
+ ```javascript
29
+ // Extract correlation context from headers
30
+ const context = {
31
+ workflowId: msg.properties.headers.workflowId,
32
+ spanId: msg.properties.headers.spanId,
33
+ parentSpanId: msg.properties.headers.parentSpanId,
34
+ userId: msg.properties.headers.userId
35
+ };
36
+
37
+ // Set context in logger for all subsequent operations
38
+ logger.setContext(context);
39
+ logger.info('message.received', { queue: 'hello-service.workflow' });
40
+ ```
41
+
42
+ ### 3. Cookbook Processing
43
+ ```javascript
44
+ // Parse message body
45
+ const body = JSON.parse(msg.content.toString());
46
+ const cookbook = body.cookbook;
47
+ const currentStep = cookbook.steps[body.current_step];
48
+
49
+ // Check if this service should handle this step
50
+ if (currentStep.service !== this.serviceName) {
51
+ // Route to correct service
52
+ const targetQueue = `${currentStep.service}.workflow`;
53
+ await mqClient.publish(targetQueue, msg);
54
+ return;
55
+ }
56
+ ```
57
+
58
+ ### 4. Cache Check
59
+ ```javascript
60
+ // Generate cache key from operation and parameters
61
+ const cacheKey = generateCacheKey(currentStep);
62
+ // Format: "cache:api:hello-service:sayGoodDay:hash(John)"
63
+
64
+ // Check cache
65
+ const startTime = Date.now();
66
+ const cached = await cache.get(cacheKey);
67
+
68
+ if (cached) {
69
+ logger.info('cache.hit', {
70
+ key: cacheKey,
71
+ latency: Date.now() - startTime
72
+ });
73
+
74
+ // Skip API call, use cached result
75
+ return processNextStep(cached);
76
+ }
77
+
78
+ logger.info('cache.miss', { key: cacheKey });
79
+ ```
80
+
81
+ ### 5. API Call Mapping
82
+ ```javascript
83
+ // Map cookbook operation to HTTP endpoint
84
+ const endpoint = mapOperationToEndpoint(currentStep.operation);
85
+ // Example: "sayGoodDay" → GET /good-day
86
+
87
+ // Transform parameters
88
+ const httpParams = transformParameters(currentStep.params);
89
+ // Example: {name: "John"} → ?name=John
90
+
91
+ // Make HTTP call to service
92
+ const apiStart = Date.now();
93
+ const response = await axios({
94
+ method: endpoint.method,
95
+ url: `${this.serviceUrl}${endpoint.path}`,
96
+ params: httpParams
97
+ });
98
+
99
+ logger.info('api.call.completed', {
100
+ operation: currentStep.operation,
101
+ latency: Date.now() - apiStart,
102
+ status: response.status
103
+ });
104
+ ```
105
+
106
+ ### 6. Cache Storage
107
+ ```javascript
108
+ // Store successful response in cache
109
+ if (response.status === 200) {
110
+ await cache.set(cacheKey, response.data, {
111
+ ttl: this.config.cache.ttl || 300 // 5 minutes default
112
+ });
113
+
114
+ logger.info('cache.stored', {
115
+ key: cacheKey,
116
+ ttl: 300
117
+ });
118
+ }
119
+ ```
120
+
121
+ ### 7. Context Update
122
+ ```javascript
123
+ // Add result to workflow context
124
+ body.context.results = body.context.results || {};
125
+ body.context.results[currentStep.id] = response.data;
126
+
127
+ // Update step index
128
+ body.current_step++;
129
+
130
+ // Add trace information
131
+ body.trace = body.trace || [];
132
+ body.trace.push({
133
+ step: body.current_step - 1,
134
+ service: this.serviceName,
135
+ timestamp: new Date().toISOString(),
136
+ duration: Date.now() - startTime
137
+ });
138
+ ```
139
+
140
+ ### 8. Next Step Routing
141
+ ```javascript
142
+ // Determine next action
143
+ if (body.current_step >= cookbook.steps.length) {
144
+ // Workflow complete
145
+ await mqClient.publish('workflow.completed', {
146
+ headers: msg.properties.headers,
147
+ body: body
148
+ });
149
+
150
+ logger.info('workflow.completed', {
151
+ workflowId: context.workflowId,
152
+ totalSteps: cookbook.steps.length
153
+ });
154
+ } else {
155
+ // Route to next service
156
+ const nextStep = cookbook.steps[body.current_step];
157
+ const nextQueue = `${nextStep.service}.workflow`;
158
+
159
+ // Create new span for next step
160
+ const newHeaders = {
161
+ ...msg.properties.headers,
162
+ parentSpanId: context.spanId,
163
+ spanId: generateSpanId()
164
+ };
165
+
166
+ await mqClient.publish(nextQueue, {
167
+ headers: newHeaders,
168
+ body: body
169
+ });
170
+
171
+ logger.info('message.routed', {
172
+ nextQueue,
173
+ nextService: nextStep.service
174
+ });
175
+ }
176
+ ```
177
+
178
+ ## Fork-Join Flow
179
+
180
+ ### 1. Fork Detection
181
+ ```javascript
182
+ if (currentStep.type === 'fork_join') {
183
+ const branches = currentStep.branches;
184
+ const accumulatorQueue = `fork.${context.workflowId}.${currentStep.id}`;
185
+
186
+ // Create accumulator metadata
187
+ const metadata = {
188
+ expectedResults: branches.length,
189
+ receivedResults: 0,
190
+ results: {}
191
+ };
192
+
193
+ await cache.set(`fork:${accumulatorQueue}`, metadata, { ttl: 300 });
194
+ ```
195
+
196
+ ### 2. Branch Distribution
197
+ ```javascript
198
+ // Send to each branch
199
+ for (const branch of branches) {
200
+ const branchMessage = {
201
+ ...body,
202
+ current_step: 0,
203
+ cookbook: branch,
204
+ fork_metadata: {
205
+ accumulatorQueue,
206
+ branchId: branch.id
207
+ }
208
+ };
209
+
210
+ const firstService = branch.steps[0].service;
211
+ await mqClient.publish(`${firstService}.workflow`, {
212
+ headers: {
213
+ ...headers,
214
+ parentSpanId: context.spanId,
215
+ spanId: generateSpanId()
216
+ },
217
+ body: branchMessage
218
+ });
219
+ }
220
+ }
221
+ ```
222
+
223
+ ### 3. Result Collection
224
+ ```javascript
225
+ // When branch completes, it sends to accumulator
226
+ if (body.fork_metadata) {
227
+ const { accumulatorQueue, branchId } = body.fork_metadata;
228
+
229
+ // Update accumulator
230
+ const metadata = await cache.get(`fork:${accumulatorQueue}`);
231
+ metadata.results[branchId] = body.context.results;
232
+ metadata.receivedResults++;
233
+
234
+ if (metadata.receivedResults === metadata.expectedResults) {
235
+ // All branches complete - join results
236
+ const joinedResults = joinStrategy(metadata.results);
237
+
238
+ // Continue main workflow
239
+ const mainMessage = {
240
+ ...originalBody,
241
+ context: {
242
+ ...originalBody.context,
243
+ results: {
244
+ ...originalBody.context.results,
245
+ [currentStep.id]: joinedResults
246
+ }
247
+ },
248
+ current_step: originalBody.current_step + 1
249
+ };
250
+
251
+ // Route to next step in main workflow
252
+ routeToNext(mainMessage);
253
+ } else {
254
+ // Save updated metadata and wait for more results
255
+ await cache.set(`fork:${accumulatorQueue}`, metadata);
256
+ }
257
+ }
258
+ ```
259
+
260
+ ## Error Handling Flows
261
+
262
+ ### 1. Transient Error (Retry)
263
+ ```javascript
264
+ try {
265
+ const response = await axios.get(url);
266
+ } catch (error) {
267
+ if (isTransientError(error)) {
268
+ // Network timeout, 503, etc.
269
+ message.retry_count = (message.retry_count || 0) + 1;
270
+
271
+ if (message.retry_count <= 3) {
272
+ // Retry with exponential backoff
273
+ const delay = Math.pow(2, message.retry_count) * 1000;
274
+
275
+ setTimeout(() => {
276
+ mqClient.publish(currentQueue, message);
277
+ }, delay);
278
+
279
+ logger.warn('message.retry', {
280
+ attempt: message.retry_count,
281
+ delay,
282
+ error: error.message
283
+ });
284
+ } else {
285
+ // Max retries exceeded - send to DLQ
286
+ await mqClient.publish(`${currentQueue}.dlq`, message);
287
+ logger.error('message.dlq', {
288
+ reason: 'max_retries',
289
+ attempts: message.retry_count
290
+ });
291
+ }
292
+ }
293
+ }
294
+ ```
295
+
296
+ ### 2. Business Error (Continue)
297
+ ```javascript
298
+ if (error.response && error.response.status === 400) {
299
+ // Business validation error - include in workflow
300
+ body.context.results[currentStep.id] = {
301
+ error: true,
302
+ message: error.response.data.message,
303
+ code: 'VALIDATION_ERROR'
304
+ };
305
+
306
+ // Continue to next step
307
+ body.current_step++;
308
+ routeToNext(body);
309
+
310
+ logger.info('business.error', {
311
+ step: currentStep.id,
312
+ error: error.response.data.message
313
+ });
314
+ }
315
+ ```
316
+
317
+ ### 3. Fatal Error (Stop)
318
+ ```javascript
319
+ if (error.code === 'SERVICE_NOT_FOUND') {
320
+ // Service doesn't exist - can't continue
321
+ await mqClient.publish('workflow.failed', {
322
+ headers: context,
323
+ body: {
324
+ ...body,
325
+ error: {
326
+ step: currentStep.id,
327
+ service: currentStep.service,
328
+ message: 'Service not found in registry'
329
+ }
330
+ }
331
+ });
332
+
333
+ logger.error('workflow.failed', {
334
+ workflowId: context.workflowId,
335
+ reason: 'service_not_found',
336
+ service: currentStep.service
337
+ });
338
+ }
339
+ ```
340
+
341
+ ## Performance Tracking
342
+
343
+ ### Metrics Collection
344
+ ```javascript
345
+ class PerformanceTracker {
346
+ constructor() {
347
+ this.metrics = {
348
+ queue_wait: 0,
349
+ cache_check: 0,
350
+ api_call: 0,
351
+ cache_store: 0,
352
+ routing: 0,
353
+ total: 0
354
+ };
355
+ }
356
+
357
+ async trackOperation(name, fn) {
358
+ const start = Date.now();
359
+ try {
360
+ const result = await fn();
361
+ this.metrics[name] = Date.now() - start;
362
+ return result;
363
+ } catch (error) {
364
+ this.metrics[name] = Date.now() - start;
365
+ throw error;
366
+ }
367
+ }
368
+
369
+ getMetrics() {
370
+ this.metrics.total = Object.values(this.metrics)
371
+ .reduce((sum, val) => sum + val, 0);
372
+ return this.metrics;
373
+ }
374
+ }
375
+
376
+ // Usage in message processing
377
+ const tracker = new PerformanceTracker();
378
+
379
+ const cached = await tracker.trackOperation('cache_check',
380
+ () => cache.get(cacheKey));
381
+
382
+ const response = await tracker.trackOperation('api_call',
383
+ () => axios.get(url));
384
+
385
+ logger.info('performance.metrics', tracker.getMetrics());
386
+ ```
387
+
388
+ ---
389
+ *For configuration and setup, see main [Service Wrapper documentation](../README.md)*
package/jest.config.js ADDED
@@ -0,0 +1,34 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ coverageDirectory: 'coverage',
4
+ collectCoverageFrom: [
5
+ 'src/**/*.js',
6
+ '!src/**/*.test.js',
7
+ '!src/**/index.js'
8
+ ],
9
+ testMatch: [
10
+ '**/test/**/*.test.js'
11
+ ],
12
+ testPathIgnorePatterns: [
13
+ '/node_modules/'
14
+ ],
15
+ coverageThresholds: {
16
+ global: {
17
+ branches: 80,
18
+ functions: 80,
19
+ lines: 80,
20
+ statements: 80
21
+ }
22
+ },
23
+ moduleNameMapper: {
24
+ // Map connector imports to mocks in unit tests
25
+ '@onlineapps/conn-infra-mq': '<rootDir>/test/mocks/connectors.js',
26
+ '@onlineapps/conn-orch-registry': '<rootDir>/test/mocks/connectors.js',
27
+ '@onlineapps/conn-base-logger': '<rootDir>/test/mocks/connectors.js',
28
+ '@onlineapps/conn-orch-orchestrator': '<rootDir>/test/mocks/connectors.js',
29
+ '@onlineapps/conn-orch-api-mapper': '<rootDir>/test/mocks/connectors.js',
30
+ '@onlineapps/conn-orch-cookbook': '<rootDir>/test/mocks/connectors.js'
31
+ },
32
+ setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
33
+ verbose: true
34
+ };
package/jsdoc.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "source": {
3
+ "include": ["src/"],
4
+ "includePattern": ".js$",
5
+ "excludePattern": "(node_modules/|docs/|test/)"
6
+ },
7
+ "plugins": ["plugins/markdown"],
8
+ "templates": {
9
+ "cleverLinks": false,
10
+ "monospaceLinks": false,
11
+ "default": {
12
+ "outputSourceFiles": true,
13
+ "includeDate": false
14
+ }
15
+ },
16
+ "opts": {
17
+ "destination": "docs/html",
18
+ "recurse": true,
19
+ "readme": "README.md",
20
+ "package": "package.json"
21
+ }
22
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@onlineapps/service-wrapper",
3
+ "version": "2.0.0",
4
+ "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "test:unit": "jest test/unit",
9
+ "test:component": "jest test/component",
10
+ "test:integration": "jest test/integration",
11
+ "test:coverage": "jest --coverage",
12
+ "test:mocked": "node test/run-tests.js",
13
+ "docs": "jsdoc2md --files src/**/*.js > API.md",
14
+ "docs:html": "jsdoc -c jsdoc.json -d docs/html"
15
+ },
16
+ "keywords": [
17
+ "microservices",
18
+ "workflow",
19
+ "infrastructure",
20
+ "wrapper",
21
+ "thin-layer",
22
+ "orchestration"
23
+ ],
24
+ "author": "OA Drive Team",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@onlineapps/conn-base-logger": "^1.0.0",
28
+ "@onlineapps/conn-infra-mq": "^1.1.0",
29
+ "@onlineapps/conn-orch-registry": "^1.1.4",
30
+ "@onlineapps/conn-orch-cookbook": "^2.0.0",
31
+ "@onlineapps/conn-orch-orchestrator": "^1.0.0",
32
+ "@onlineapps/conn-orch-api-mapper": "^1.0.0",
33
+ "@onlineapps/conn-base-cache": "^1.0.0",
34
+ "@onlineapps/conn-infra-error-handler": "^1.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "jest": "^29.5.0",
38
+ "jsdoc": "^4.0.2",
39
+ "jsdoc-to-markdown": "^8.0.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=14.0.0"
43
+ }
44
+ }