@onlineapps/cookbook-router 1.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/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # @onlineapps/cookbook-router
2
+
3
+ Message routing for cookbook workflows - handles service discovery and queue routing.
4
+
5
+ ## Overview
6
+
7
+ This package provides routing capabilities for cookbook workflows in a distributed microservices environment. It handles service discovery, queue management, and retry logic for reliable message delivery.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @onlineapps/cookbook-router
13
+ ```
14
+
15
+ ## Features
16
+
17
+ - **Service discovery** - Integration with service registry
18
+ - **Queue management** - RabbitMQ operations and DLQ handling
19
+ - **Workflow routing** - Route between services based on cookbook steps
20
+ - **Retry logic** - Exponential backoff with jitter
21
+ - **Health checking** - Service availability monitoring
22
+
23
+ ## Usage
24
+
25
+ ### Basic routing setup
26
+
27
+ ```javascript
28
+ const { CookbookRouter } = require('@onlineapps/cookbook-router');
29
+ const MQClient = require('@onlineapps/connector-mq-client');
30
+ const RegistryClient = require('@onlineapps/connector-registry-client');
31
+
32
+ const mqClient = new MQClient(mqConfig);
33
+ const registryClient = new RegistryClient(registryConfig);
34
+
35
+ const router = new CookbookRouter(mqClient, registryClient, {
36
+ defaultQueue: 'workflow.init',
37
+ completedQueue: 'workflow.completed',
38
+ maxRetries: 3
39
+ });
40
+ ```
41
+
42
+ ### Route workflow to first service
43
+
44
+ ```javascript
45
+ const cookbook = {
46
+ id: 'my-workflow',
47
+ steps: [
48
+ { id: 'step1', type: 'task', service: 'user-service', operation: 'getUser' },
49
+ { id: 'step2', type: 'task', service: 'email-service', operation: 'sendEmail' }
50
+ ]
51
+ };
52
+
53
+ const context = {
54
+ workflow_id: 'wf_123',
55
+ api_input: { userId: '456' }
56
+ };
57
+
58
+ await router.routeWorkflow(cookbook, context);
59
+ ```
60
+
61
+ ### Route to next service
62
+
63
+ ```javascript
64
+ // After completing step1, route to next service
65
+ await router.routeToNextService(cookbook, updatedContext, 'step1');
66
+ ```
67
+
68
+ ### Handle failures
69
+
70
+ ```javascript
71
+ try {
72
+ await router.routeWorkflow(cookbook, context);
73
+ } catch (error) {
74
+ // Route to dead letter queue
75
+ await router.routeToDLQ(cookbook, context, error, 'user-service');
76
+ }
77
+ ```
78
+
79
+ ## Service Discovery
80
+
81
+ ```javascript
82
+ const { ServiceDiscovery } = require('@onlineapps/cookbook-router');
83
+
84
+ const discovery = new ServiceDiscovery(registryClient);
85
+
86
+ // Check service availability
87
+ const exists = await discovery.serviceExists('user-service');
88
+
89
+ // Get service details
90
+ const service = await discovery.getService('user-service');
91
+
92
+ // Find services by capability
93
+ const services = await discovery.findServicesByCapability('email');
94
+ ```
95
+
96
+ ## Queue Management
97
+
98
+ ```javascript
99
+ const { QueueManager } = require('@onlineapps/cookbook-router');
100
+
101
+ const queueManager = new QueueManager(mqClient);
102
+
103
+ // Publish message
104
+ await queueManager.publish('user.queue', { action: 'process' });
105
+
106
+ // Subscribe to queue
107
+ await queueManager.subscribe('user.queue', async (message) => {
108
+ console.log('Received:', message);
109
+ });
110
+
111
+ // Setup dead letter queue
112
+ await queueManager.setupDLQ('user.queue', 'user.dlq');
113
+ ```
114
+
115
+ ## Retry Handling
116
+
117
+ ```javascript
118
+ const { RetryHandler } = require('@onlineapps/cookbook-router');
119
+
120
+ const retryHandler = new RetryHandler({
121
+ maxAttempts: 3,
122
+ initialDelay: 1000,
123
+ backoffFactor: 2
124
+ });
125
+
126
+ // Execute with retry
127
+ const result = await retryHandler.execute(async () => {
128
+ return await unreliableOperation();
129
+ });
130
+
131
+ // Wrap function with retry
132
+ const reliableOperation = retryHandler.wrap(unreliableOperation);
133
+ await reliableOperation();
134
+ ```
135
+
136
+ ## Configuration Options
137
+
138
+ ### CookbookRouter Options
139
+
140
+ ```javascript
141
+ {
142
+ defaultQueue: 'workflow.init', // Default entry queue
143
+ completedQueue: 'workflow.completed', // Completion queue
144
+ dlqSuffix: '.dlq', // Dead letter queue suffix
145
+ maxRetries: 3, // Max retry attempts
146
+ retryDelay: 2000, // Base retry delay (ms)
147
+ logger: console // Logger instance
148
+ }
149
+ ```
150
+
151
+ ### ServiceDiscovery Options
152
+
153
+ ```javascript
154
+ {
155
+ cacheTTL: 60000, // Cache TTL in ms
156
+ logger: console // Logger instance
157
+ }
158
+ ```
159
+
160
+ ### QueueManager Options
161
+
162
+ ```javascript
163
+ {
164
+ defaultTTL: 30000, // Message TTL
165
+ persistent: true, // Persistent messages
166
+ logger: console // Logger instance
167
+ }
168
+ ```
169
+
170
+ ### RetryHandler Options
171
+
172
+ ```javascript
173
+ {
174
+ maxAttempts: 3, // Max retry attempts
175
+ initialDelay: 1000, // Initial delay (ms)
176
+ maxDelay: 30000, // Max delay cap (ms)
177
+ backoffFactor: 2, // Exponential factor
178
+ jitter: true, // Add random jitter
179
+ logger: console // Logger instance
180
+ }
181
+ ```
182
+
183
+ ## Queue Naming Conventions
184
+
185
+ - `{service}.workflow` - Workflow messages
186
+ - `{service}.queue` - Direct messages
187
+ - `{service}.dlq` - Dead letter queue
188
+ - `workflow.init` - Entry point
189
+ - `workflow.completed` - Completed workflows
190
+
191
+ ## Error Handling
192
+
193
+ The router distinguishes between retryable and non-retryable errors:
194
+
195
+ **Retryable:**
196
+ - Network timeouts
197
+ - Connection refused
198
+ - Service temporarily unavailable
199
+
200
+ **Non-retryable:**
201
+ - Validation errors
202
+ - Authentication failures
203
+ - Not found errors
204
+ - Bad requests
205
+
206
+ ## Related Packages
207
+
208
+ - `@onlineapps/cookbook-core` - Core parsing and validation
209
+ - `@onlineapps/cookbook-executor` - Workflow execution engine
210
+ - `@onlineapps/cookbook-transformer` - OpenAPI mapping
211
+ - `@onlineapps/connector-cookbook` - Full-featured wrapper
212
+
213
+ ## License
214
+
215
+ PROPRIETARY - All rights reserved
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@onlineapps/cookbook-router",
3
+ "version": "1.0.0",
4
+ "description": "Message routing for cookbook workflows - handles service discovery and queue routing",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "test:watch": "jest --watch",
9
+ "test:coverage": "jest --coverage",
10
+ "docs": "jsdoc2md --files src/**/*.js > API.md"
11
+ },
12
+ "keywords": [
13
+ "cookbook",
14
+ "routing",
15
+ "queue",
16
+ "workflow",
17
+ "messaging"
18
+ ],
19
+ "author": "OnlineApps",
20
+ "license": "PROPRIETARY",
21
+ "dependencies": {
22
+ "@onlineapps/cookbook-core": "^1.0.0",
23
+ "@onlineapps/conn-infra-mq": "^1.1.0",
24
+ "@onlineapps/conn-orch-registry": "^1.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "jest": "^29.7.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "files": [
33
+ "src"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module @onlineapps/cookbook-router
5
+ *
6
+ * Message routing library for cookbook workflows.
7
+ * Handles service discovery, queue routing, and retry logic.
8
+ */
9
+
10
+ const CookbookRouter = require('./router');
11
+ const ServiceDiscovery = require('./serviceDiscovery');
12
+ const QueueManager = require('./queueManager');
13
+ const RetryHandler = require('./retryHandler');
14
+
15
+ module.exports = {
16
+ // Main router class
17
+ CookbookRouter,
18
+
19
+ // Supporting classes
20
+ ServiceDiscovery,
21
+ QueueManager,
22
+ RetryHandler,
23
+
24
+ // Factory function
25
+ createRouter: (mqClient, registryClient, options = {}) => {
26
+ return new CookbookRouter(mqClient, registryClient, options);
27
+ },
28
+
29
+ // Utility exports
30
+ VERSION: '1.0.0'
31
+ };
@@ -0,0 +1,187 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * QueueManager - Queue operations and management
5
+ */
6
+
7
+ class QueueManager {
8
+ constructor(mqClient, options = {}) {
9
+ this.mqClient = mqClient;
10
+ this.options = {
11
+ ensureQueues: options.ensureQueues !== false,
12
+ defaultOptions: {
13
+ durable: true,
14
+ persistent: true,
15
+ ...options.defaultOptions
16
+ },
17
+ logger: options.logger || console,
18
+ ...options
19
+ };
20
+
21
+ this.ensuredQueues = new Set();
22
+ }
23
+
24
+ /**
25
+ * Publish message to queue
26
+ * @param {string} queueName - Target queue name
27
+ * @param {Object} message - Message to publish
28
+ * @param {Object} options - Publishing options
29
+ * @returns {Promise<boolean>}
30
+ */
31
+ async publish(queueName, message, options = {}) {
32
+ const { logger } = this.options;
33
+
34
+ try {
35
+ // Ensure queue exists if enabled
36
+ if (this.options.ensureQueues) {
37
+ await this.ensureQueue(queueName);
38
+ }
39
+
40
+ // Add timestamp to message
41
+ const messageWithTimestamp = {
42
+ ...message,
43
+ timestamp: new Date().toISOString()
44
+ };
45
+
46
+ // Merge publishing options
47
+ const publishOptions = {
48
+ ...this.options.defaultOptions,
49
+ ...options
50
+ };
51
+
52
+ logger.debug(`Publishing to ${queueName}`);
53
+
54
+ // Handle connection errors with retry
55
+ try {
56
+ await this.mqClient.publish(queueName, messageWithTimestamp, publishOptions);
57
+ return true;
58
+ } catch (error) {
59
+ // If connection lost, retry once
60
+ if (error.message.includes('Connection lost')) {
61
+ await this.mqClient.publish(queueName, messageWithTimestamp, publishOptions);
62
+ return true;
63
+ }
64
+ throw error;
65
+ }
66
+
67
+ } catch (error) {
68
+ logger.error(`Failed to publish to ${queueName}:`, error);
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Ensure queue exists
75
+ * @param {string} queueName - Queue name
76
+ * @param {Object} options - Queue options
77
+ * @returns {Promise<boolean>}
78
+ */
79
+ async ensureQueue(queueName, options = {}) {
80
+ // Check if already ensured
81
+ if (this.ensuredQueues.has(queueName)) {
82
+ return true;
83
+ }
84
+
85
+ const queueOptions = {
86
+ ...this.options.defaultOptions,
87
+ ...options
88
+ };
89
+
90
+ try {
91
+ await this.mqClient.assertQueue(queueName, queueOptions);
92
+ this.ensuredQueues.add(queueName);
93
+ return true;
94
+ } catch (error) {
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Setup consumer for queue
101
+ * @param {string} queueName - Queue to consume from
102
+ * @param {Function} handler - Message handler
103
+ * @param {Object} options - Consumer options
104
+ * @returns {Promise<void>}
105
+ */
106
+ async consume(queueName, handler, options = {}) {
107
+ const { logger } = this.options;
108
+
109
+ // Ensure queue exists
110
+ if (this.options.ensureQueues) {
111
+ await this.ensureQueue(queueName);
112
+ }
113
+
114
+ const consumerOptions = {
115
+ noAck: false,
116
+ ...options
117
+ };
118
+
119
+ // Wrap handler with error handling
120
+ const wrappedHandler = async (message) => {
121
+ try {
122
+ // Parse message content
123
+ const content = JSON.parse(message.content.toString());
124
+
125
+ // Call user handler
126
+ await handler(content, message);
127
+
128
+ // Acknowledge message
129
+ this.mqClient.ack(message);
130
+ } catch (error) {
131
+ logger.error(`Error processing message from ${queueName}:`, error);
132
+ // Reject message without requeue
133
+ this.mqClient.nack(message, false, false);
134
+ }
135
+ };
136
+
137
+ await this.mqClient.consume(queueName, wrappedHandler, consumerOptions);
138
+ }
139
+
140
+ /**
141
+ * Get queue information
142
+ * @param {string} queueName - Queue name
143
+ * @returns {Promise<Object>}
144
+ */
145
+ async getQueueInfo(queueName) {
146
+ return await this.mqClient.checkQueue(queueName);
147
+ }
148
+
149
+ /**
150
+ * Purge queue (remove all messages)
151
+ * @param {string} queueName - Queue name
152
+ * @returns {Promise<Object>}
153
+ */
154
+ async purgeQueue(queueName) {
155
+ const { logger } = this.options;
156
+ const result = await this.mqClient.purgeQueue(queueName);
157
+ logger.warn(`Purged ${result.messageCount} messages from ${queueName}`);
158
+ return result;
159
+ }
160
+
161
+ /**
162
+ * Delete queue
163
+ * @param {string} queueName - Queue name
164
+ * @returns {Promise<boolean>}
165
+ */
166
+ async deleteQueue(queueName) {
167
+ const { logger } = this.options;
168
+ const result = await this.mqClient.deleteQueue(queueName);
169
+
170
+ // Remove from ensured queues cache
171
+ this.ensuredQueues.delete(queueName);
172
+
173
+ logger.warn(`Deleted queue ${queueName}`);
174
+ return result;
175
+ }
176
+
177
+ /**
178
+ * Reset connection and clear cache
179
+ */
180
+ resetConnection() {
181
+ const { logger } = this.options;
182
+ this.ensuredQueues.clear();
183
+ logger.info('Connection reset, cleared queue cache');
184
+ }
185
+ }
186
+
187
+ module.exports = QueueManager;
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * RetryHandler - Handles retry logic with exponential backoff
5
+ */
6
+
7
+ class RetryHandler {
8
+ constructor(options = {}) {
9
+ this.options = {
10
+ maxAttempts: options.maxAttempts || 3,
11
+ baseDelay: options.baseDelay || 1000,
12
+ maxDelay: options.maxDelay || 30000,
13
+ backoffMultiplier: options.backoffMultiplier || 2,
14
+ logger: options.logger || console,
15
+ ...options
16
+ };
17
+
18
+ this.attempts = new Map();
19
+ }
20
+
21
+ /**
22
+ * Check if retry should be attempted
23
+ * @param {string} identifier - Unique identifier for the retry
24
+ * @param {Object} options - Override options
25
+ * @returns {boolean}
26
+ */
27
+ shouldRetry(identifier, options = {}) {
28
+ const { logger } = this.options;
29
+ const maxAttempts = options.maxAttempts || this.options.maxAttempts;
30
+
31
+ // Check for non-retryable error
32
+ if (options.error && options.error.retryable === false) {
33
+ return false;
34
+ }
35
+
36
+ // Get current attempt count
37
+ const record = this.attempts.get(identifier);
38
+ const currentAttempts = record ? record.count : 0;
39
+
40
+ const shouldRetry = currentAttempts < maxAttempts;
41
+
42
+ logger.debug(`Retry check for ${identifier}: attempts=${currentAttempts}, max=${maxAttempts}, shouldRetry=${shouldRetry}`);
43
+
44
+ return shouldRetry;
45
+ }
46
+
47
+ /**
48
+ * Get retry delay with exponential backoff
49
+ * @param {string} identifier - Unique identifier
50
+ * @param {Object} options - Override options
51
+ * @returns {number} Delay in milliseconds
52
+ */
53
+ getRetryDelay(identifier, options = {}) {
54
+ const baseDelay = options.baseDelay || this.options.baseDelay;
55
+ const maxDelay = this.options.maxDelay;
56
+ const backoffMultiplier = this.options.backoffMultiplier;
57
+ const jitter = options.jitter || false;
58
+
59
+ const record = this.attempts.get(identifier);
60
+ const attemptNumber = record ? record.count : 1;
61
+
62
+ // Calculate exponential backoff delay
63
+ let delay = baseDelay * Math.pow(backoffMultiplier, attemptNumber - 1);
64
+
65
+ // Cap at max delay
66
+ delay = Math.min(delay, maxDelay);
67
+
68
+ // Add jitter if requested
69
+ if (jitter) {
70
+ const jitterAmount = delay * 0.1 * Math.random();
71
+ delay = delay + jitterAmount;
72
+ }
73
+
74
+ return Math.floor(delay);
75
+ }
76
+
77
+ /**
78
+ * Record an attempt
79
+ * @param {string} identifier - Unique identifier
80
+ * @param {Object} context - Additional context
81
+ */
82
+ recordAttempt(identifier, context = {}) {
83
+ const { logger } = this.options;
84
+ const now = Date.now();
85
+
86
+ let record = this.attempts.get(identifier);
87
+ if (!record) {
88
+ record = { count: 0, firstAttempt: now, lastAttempt: now };
89
+ }
90
+
91
+ record.count += 1;
92
+ record.lastAttempt = now;
93
+
94
+ if (context.error) {
95
+ record.lastError = context.error;
96
+ }
97
+
98
+ this.attempts.set(identifier, record);
99
+
100
+ logger.debug(`Recording attempt ${record.count} for ${identifier}`);
101
+ }
102
+
103
+ /**
104
+ * Reset retry state for identifier
105
+ * @param {string} identifier - Unique identifier
106
+ */
107
+ reset(identifier) {
108
+ const { logger } = this.options;
109
+ this.attempts.delete(identifier);
110
+ logger.debug(`Reset retry state for ${identifier}`);
111
+ }
112
+
113
+ /**
114
+ * Reset all retry states
115
+ */
116
+ resetAll() {
117
+ const { logger } = this.options;
118
+ this.attempts.clear();
119
+ logger.info('Reset all retry states');
120
+ }
121
+
122
+ /**
123
+ * Get attempt count for identifier
124
+ * @param {string} identifier - Unique identifier
125
+ * @returns {number}
126
+ */
127
+ getAttemptCount(identifier) {
128
+ const record = this.attempts.get(identifier);
129
+ return record ? record.count : 0;
130
+ }
131
+
132
+ /**
133
+ * Execute function with retry logic
134
+ * @param {string} identifier - Unique identifier
135
+ * @param {Function} fn - Function to execute
136
+ * @param {Object} options - Override options
137
+ * @returns {Promise<*>}
138
+ */
139
+ async executeWithRetry(identifier, fn, options = {}) {
140
+ const { logger } = this.options;
141
+ const maxAttempts = options.maxAttempts || this.options.maxAttempts;
142
+
143
+ let lastError;
144
+ let attempts = 0;
145
+
146
+ while (attempts < maxAttempts) {
147
+ try {
148
+ const result = await fn();
149
+ return result;
150
+ } catch (error) {
151
+ attempts++;
152
+ lastError = error;
153
+ this.recordAttempt(identifier, { error });
154
+
155
+ if (attempts < maxAttempts) {
156
+ const delay = this.getRetryDelay(identifier, options);
157
+ logger.warn(`Retry attempt ${attempts + 1} for ${identifier} in ${delay}ms`);
158
+ await new Promise(resolve => setTimeout(resolve, delay));
159
+ }
160
+ }
161
+ }
162
+
163
+ throw lastError;
164
+ }
165
+
166
+ /**
167
+ * Cleanup old attempts
168
+ * @param {number} maxAge - Maximum age in milliseconds
169
+ */
170
+ cleanup(maxAge = 3600000) { // Default 1 hour
171
+ const { logger } = this.options;
172
+ const now = Date.now();
173
+ let cleaned = 0;
174
+
175
+ for (const [identifier, record] of this.attempts.entries()) {
176
+ if (now - record.lastAttempt > maxAge) {
177
+ this.attempts.delete(identifier);
178
+ cleaned++;
179
+ }
180
+ }
181
+
182
+ logger.debug(`Cleaned up ${cleaned} old retry records`);
183
+ }
184
+ }
185
+
186
+ module.exports = RetryHandler;
package/src/router.js ADDED
@@ -0,0 +1,207 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CookbookRouter - Main routing class for workflow messages
5
+ */
6
+
7
+ const ServiceDiscovery = require('./serviceDiscovery');
8
+ const QueueManager = require('./queueManager');
9
+ const RetryHandler = require('./retryHandler');
10
+
11
+ class CookbookRouter {
12
+ constructor(mqClient, registryClient, options = {}) {
13
+ this.mqClient = mqClient;
14
+ this.registryClient = registryClient;
15
+ this.options = {
16
+ defaultQueue: 'workflow.init',
17
+ completedQueue: 'workflow.completed',
18
+ dlqSuffix: '.dlq',
19
+ maxRetries: 3,
20
+ retryDelay: 2000,
21
+ logger: console,
22
+ ...options
23
+ };
24
+
25
+ this.serviceDiscovery = new ServiceDiscovery(registryClient, options);
26
+ this.queueManager = new QueueManager(mqClient, options);
27
+ this.retryHandler = new RetryHandler(options);
28
+ }
29
+
30
+ /**
31
+ * Route workflow to the first service
32
+ * @param {Object} cookbook - Cookbook definition
33
+ * @param {Object} context - Workflow context
34
+ * @returns {Promise<void>}
35
+ */
36
+ async routeWorkflow(cookbook, context) {
37
+ const { logger } = this.options;
38
+
39
+ // Find first step
40
+ const firstStep = cookbook.steps?.[0];
41
+ if (!firstStep) {
42
+ throw new Error('No steps defined in cookbook');
43
+ }
44
+
45
+ // Determine target service
46
+ const targetService = await this.determineTargetService(firstStep);
47
+
48
+ // Build workflow message
49
+ const message = this.buildWorkflowMessage(cookbook, context, firstStep, targetService);
50
+
51
+ // Send to service queue
52
+ const queueName = `${targetService}.workflow`;
53
+ logger.info(`Routing workflow to ${queueName}`);
54
+
55
+ await this.queueManager.publish(queueName, message);
56
+ }
57
+
58
+ /**
59
+ * Route to next service in workflow
60
+ * @param {Object} cookbook - Cookbook definition
61
+ * @param {Object} context - Current context
62
+ * @param {string} currentStepId - Current step ID
63
+ * @returns {Promise<void>}
64
+ */
65
+ async routeToNextService(cookbook, context, currentStepId) {
66
+ const { logger } = this.options;
67
+
68
+ // Find current step index
69
+ const currentIndex = cookbook.steps.findIndex(s => s.id === currentStepId);
70
+ if (currentIndex === -1) {
71
+ throw new Error(`Step not found: ${currentStepId}`);
72
+ }
73
+
74
+ // Check if there's a next step
75
+ const nextStep = cookbook.steps[currentIndex + 1];
76
+ if (!nextStep) {
77
+ // Workflow completed
78
+ logger.info('Workflow completed, routing to completed queue');
79
+ return this.routeToCompleted(cookbook, context);
80
+ }
81
+
82
+ // Determine target service for next step
83
+ const targetService = await this.determineTargetService(nextStep);
84
+
85
+ // Build message for next step
86
+ const message = this.buildWorkflowMessage(cookbook, context, nextStep, targetService);
87
+
88
+ // Send to next service
89
+ const queueName = `${targetService}.workflow`;
90
+ logger.info(`Routing to next service: ${queueName}`);
91
+
92
+ await this.queueManager.publish(queueName, message);
93
+ }
94
+
95
+ /**
96
+ * Route completed workflow
97
+ * @param {Object} cookbook - Cookbook definition
98
+ * @param {Object} context - Final context
99
+ * @returns {Promise<void>}
100
+ */
101
+ async routeToCompleted(cookbook, context) {
102
+ const message = {
103
+ workflow_id: context.workflow_id,
104
+ cookbook_id: cookbook.id,
105
+ status: 'completed',
106
+ result: context.result || {},
107
+ trace: context.trace || [],
108
+ completed_at: new Date().toISOString()
109
+ };
110
+
111
+ await this.queueManager.publish(this.options.completedQueue, message);
112
+ }
113
+
114
+ /**
115
+ * Route failed workflow to DLQ
116
+ * @param {Object} cookbook - Cookbook definition
117
+ * @param {Object} context - Context at failure
118
+ * @param {Error} error - Error that caused failure
119
+ * @param {string} service - Service that failed
120
+ * @returns {Promise<void>}
121
+ */
122
+ async routeToDLQ(cookbook, context, error, service) {
123
+ const { logger } = this.options;
124
+
125
+ const dlqName = `${service}${this.options.dlqSuffix}`;
126
+ logger.error(`Routing to DLQ: ${dlqName}`, error);
127
+
128
+ const message = {
129
+ workflow_id: context.workflow_id,
130
+ cookbook_id: cookbook.id,
131
+ service,
132
+ error: {
133
+ message: error.message,
134
+ stack: error.stack,
135
+ code: error.code
136
+ },
137
+ context,
138
+ failed_at: new Date().toISOString()
139
+ };
140
+
141
+ await this.queueManager.publish(dlqName, message);
142
+ }
143
+
144
+ /**
145
+ * Determine target service for a step
146
+ * @private
147
+ */
148
+ async determineTargetService(step) {
149
+ if (step.type === 'task' && step.service) {
150
+ // Verify service is available
151
+ const isAvailable = await this.serviceDiscovery.isServiceAvailable(step.service);
152
+ if (!isAvailable) {
153
+ throw new Error(`Service not available: ${step.service}`);
154
+ }
155
+ return step.service;
156
+ }
157
+
158
+ // Control flow steps go to orchestrator
159
+ if (['foreach', 'switch', 'fork_join'].includes(step.type)) {
160
+ return 'workflow.orchestrator';
161
+ }
162
+
163
+ throw new Error(`Unknown step type: ${step.type}`);
164
+ }
165
+
166
+ /**
167
+ * Build workflow message
168
+ * @private
169
+ */
170
+ buildWorkflowMessage(cookbook, context, step, targetService) {
171
+ return {
172
+ workflow_id: context.workflow_id,
173
+ cookbook,
174
+ context,
175
+ step,
176
+ target_service: targetService,
177
+ timestamp: new Date().toISOString()
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Handle retry logic for steps
183
+ * @param {Object} step - Step to retry
184
+ * @param {Object} context - Current context
185
+ * @returns {Promise<boolean>}
186
+ */
187
+ async handleRetry(step, context) {
188
+ const stepId = step.id;
189
+ const attempts = context.attempts?.[stepId] || 0;
190
+
191
+ // Check if should retry
192
+ const shouldRetry = this.retryHandler.shouldRetry(stepId, { attempts });
193
+
194
+ if (shouldRetry) {
195
+ // Record attempt
196
+ this.retryHandler.recordAttempt(stepId);
197
+
198
+ // Apply delay
199
+ const delay = this.retryHandler.getRetryDelay(stepId);
200
+ await new Promise(resolve => setTimeout(resolve, delay));
201
+ }
202
+
203
+ return shouldRetry;
204
+ }
205
+ }
206
+
207
+ module.exports = CookbookRouter;
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ServiceDiscovery - Service discovery and health checking
5
+ */
6
+
7
+ class ServiceDiscovery {
8
+ constructor(registryClient, options = {}) {
9
+ this.registryClient = registryClient;
10
+ this.options = {
11
+ cacheEnabled: options.cacheEnabled !== false,
12
+ cacheTTL: options.cacheTTL || 300000, // 5 minutes default
13
+ logger: options.logger || console,
14
+ ...options
15
+ };
16
+
17
+ this.cache = new Map();
18
+ }
19
+
20
+ /**
21
+ * Check if a service is available
22
+ * @param {string} serviceName - Service name
23
+ * @returns {Promise<boolean>}
24
+ */
25
+ async isServiceAvailable(serviceName) {
26
+ try {
27
+ // Check cache first if enabled
28
+ if (this.options.cacheEnabled) {
29
+ const cached = this.getCached(serviceName);
30
+ if (cached !== null) {
31
+ return cached.status === 'active';
32
+ }
33
+ }
34
+
35
+ const service = await this.registryClient.getService(serviceName);
36
+
37
+ if (service && this.options.cacheEnabled) {
38
+ this.setCached(serviceName, service);
39
+ }
40
+
41
+ return service && service.status === 'active';
42
+ } catch (error) {
43
+ // Handle specific error codes
44
+ if (error.code === 'ECONNREFUSED') {
45
+ this.options.logger.error('Registry connection failed', { serviceName, error });
46
+ } else {
47
+ this.options.logger.error(`Service discovery failed for ${serviceName}:`, error);
48
+ }
49
+ return false;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Get service information
55
+ * @param {string} serviceName - Service name
56
+ * @returns {Promise<Object>}
57
+ */
58
+ async getServiceInfo(serviceName) {
59
+ this.options.logger.debug(`Getting service info for ${serviceName}`);
60
+
61
+ try {
62
+ // Check cache first if enabled
63
+ if (this.options.cacheEnabled) {
64
+ const cached = this.getCached(serviceName);
65
+ if (cached !== null) {
66
+ return cached;
67
+ }
68
+ }
69
+
70
+ const service = await this.registryClient.getService(serviceName);
71
+
72
+ if (!service) {
73
+ throw new Error(`Service not found: ${serviceName}`);
74
+ }
75
+
76
+ if (this.options.cacheEnabled) {
77
+ this.setCached(serviceName, service);
78
+ }
79
+
80
+ return service;
81
+ } catch (error) {
82
+ this.options.logger.error(`Failed to get service info for ${serviceName}:`, error);
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * List available services
89
+ * @returns {Promise<Array>}
90
+ */
91
+ async listAvailableServices() {
92
+ try {
93
+ const services = await this.registryClient.listServices();
94
+ return services
95
+ .filter(service => service.status === 'active')
96
+ .map(service => service.name);
97
+ } catch (error) {
98
+ this.options.logger.error('Failed to list services:', error);
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get service queue name
105
+ * @param {string} serviceName - Service name
106
+ * @param {Object} options - Options
107
+ * @returns {Promise<string>}
108
+ */
109
+ async getServiceQueue(serviceName, options = {}) {
110
+ const serviceInfo = await this.getServiceInfo(serviceName);
111
+
112
+ if (serviceInfo.endpoints && serviceInfo.endpoints.workflow) {
113
+ return serviceInfo.endpoints.workflow;
114
+ }
115
+
116
+ if (options.useDefault) {
117
+ return `${serviceName}.workflow`;
118
+ }
119
+
120
+ throw new Error(`No workflow queue defined for service: ${serviceName}`);
121
+ }
122
+
123
+ /**
124
+ * Invalidate cache
125
+ * @param {string} serviceName - Service name (optional)
126
+ */
127
+ invalidateCache(serviceName) {
128
+ if (serviceName) {
129
+ this.cache.delete(serviceName);
130
+ this.options.logger.debug(`Invalidating cache for ${serviceName}`);
131
+ } else {
132
+ this.cache.clear();
133
+ this.options.logger.debug('Invalidating entire cache');
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get cached service data
139
+ * @private
140
+ */
141
+ getCached(serviceName) {
142
+ if (!this.options.cacheEnabled) {
143
+ return null;
144
+ }
145
+
146
+ const cached = this.cache.get(serviceName);
147
+
148
+ if (!cached) {
149
+ return null;
150
+ }
151
+
152
+ const age = Date.now() - cached.timestamp;
153
+ if (age > this.options.cacheTTL) {
154
+ this.cache.delete(serviceName);
155
+ return null;
156
+ }
157
+
158
+ return cached.data;
159
+ }
160
+
161
+ /**
162
+ * Set cached service data
163
+ * @private
164
+ */
165
+ setCached(serviceName, data) {
166
+ this.cache.set(serviceName, {
167
+ data,
168
+ timestamp: Date.now()
169
+ });
170
+ }
171
+
172
+ }
173
+
174
+ module.exports = ServiceDiscovery;