@onlineapps/cookbook-router 1.0.36 → 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/README.md CHANGED
@@ -1,10 +1,14 @@
1
1
  # @onlineapps/cookbook-router
2
2
 
3
- Message routing for cookbook workflows - handles service discovery and queue routing.
3
+ Publishes a workflow message to a service's workflow queue, after checking with
4
+ the registry that the service is available.
4
5
 
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.
6
+ That is the whole package. It does not execute cookbooks, does not walk steps,
7
+ does not retry, does not complete or dead-letter a workflow, and does not
8
+ consume anything. All of that belongs to `WorkflowOrchestrator` in
9
+ `@onlineapps/conn-orch-orchestrator` — confirmation
10
+ [`cookbook-execution-owner.md`](/api/docs/governance/confirmations/cookbook-execution-owner.md)
11
+ 001 — which is also this package's only caller.
8
12
 
9
13
  ## Installation
10
14
 
@@ -12,204 +16,79 @@ This package provides routing capabilities for cookbook workflows in a distribut
12
16
  npm install @onlineapps/cookbook-router
13
17
  ```
14
18
 
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
19
+ ## The whole surface
116
20
 
117
21
  ```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();
22
+ const { createRouter, CookbookRouter, ServiceDiscovery, QueueManager } =
23
+ require('@onlineapps/cookbook-router');
134
24
  ```
135
25
 
136
- ## Configuration Options
26
+ | Export | What it is |
27
+ |---|---|
28
+ | `createRouter(mqClient, registryClient, options)` | Factory returning a `CookbookRouter`. This is what `WorkflowOrchestrator.js:71` takes. |
29
+ | `CookbookRouter` | One method: `routeToService(serviceName, message)`. |
30
+ | `ServiceDiscovery` | One method: `isServiceAvailable(serviceName)`, with a TTL cache. |
31
+ | `QueueManager` | Two methods: `publish(queue, message, options)` and `ensureQueue(queue, options)`. |
32
+ | `VERSION` | String constant. |
137
33
 
138
- ### CookbookRouter Options
34
+ ## Usage
139
35
 
140
36
  ```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
- ```
37
+ const { createRouter } = require('@onlineapps/cookbook-router');
150
38
 
151
- ### ServiceDiscovery Options
39
+ const router = createRouter(mqClient, registryClient, { logger });
152
40
 
153
- ```javascript
154
- {
155
- cacheTTL: 60000, // Cache TTL in ms
156
- logger: console // Logger instance
157
- }
41
+ // Publishes to `biz-invoicing.workflow`.
42
+ await router.routeToService('biz-invoicing', workflowMessage);
158
43
  ```
159
44
 
160
- ### QueueManager Options
45
+ `routeToService` fails fast and never publishes a message it could not place:
161
46
 
162
- ```javascript
163
- {
164
- defaultTTL: 30000, // Message TTL
165
- persistent: true, // Persistent messages
166
- logger: console // Logger instance
167
- }
168
- ```
47
+ | Condition | Result |
48
+ |---|---|
49
+ | `serviceName` missing or not a string | throws `[CookbookRouter] routeToService - serviceName is required and must be a string` |
50
+ | `message` missing or not an object | throws `[CookbookRouter] routeToService - message is required and must be an object` |
51
+ | registry says the service is not `active` | throws `[CookbookRouter] Service not available: <name>` |
52
+ | the publish itself fails | the underlying error propagates unchanged |
169
53
 
170
- ### RetryHandler Options
54
+ ## Options
171
55
 
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
- ```
56
+ The single caller passes `{ logger }` and nothing else. `options` is forwarded
57
+ verbatim to both collaborators, so their keys travel through it:
182
58
 
183
- ## Queue Naming Conventions
59
+ | Key | Read by | Default |
60
+ |---|---|---|
61
+ | `logger` | `CookbookRouter`, `ServiceDiscovery`, `QueueManager` | `console` |
62
+ | `cacheEnabled` | `ServiceDiscovery` | `true` |
63
+ | `cacheTTL` | `ServiceDiscovery` (ms) | `300000` |
64
+ | `ensureQueues` | `QueueManager` — assert the queue before the first publish | `true` |
65
+ | `defaultOptions` | `QueueManager` — merged into every publish and assert | `{ durable: true, persistent: true }` |
184
66
 
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
67
+ A key not in this table is not read by anything here.
190
68
 
191
- ## Error Handling
69
+ ## Queue naming
192
70
 
193
- The router distinguishes between retryable and non-retryable errors:
71
+ `routeToService` publishes to `<serviceName>.workflow`. The package builds no
72
+ other queue name; `workflow.completed` and the `.dlq` queues are the
73
+ orchestrator's, not this package's.
194
74
 
195
- **Retryable:**
196
- - Network timeouts
197
- - Connection refused
198
- - Service temporarily unavailable
75
+ ## Collaborator contracts
199
76
 
200
- **Non-retryable:**
201
- - Validation errors
202
- - Authentication failures
203
- - Not found errors
204
- - Bad requests
77
+ - `registryClient.getService(serviceName)` resolves to an object whose `status`
78
+ is `'active'` when the service is up. A rejection is logged and read as "not
79
+ available" — `isServiceAvailable` returns `false`, it does not throw.
80
+ - `mqClient.assertQueue(queue, options)` and `mqClient.publish(queue, message,
81
+ options)`. `publish` retries exactly once when the error message contains
82
+ `Connection lost`; every other error propagates.
205
83
 
206
- ## Related Packages
84
+ ## Related packages
207
85
 
208
- - `@onlineapps/cookbook-core` - Core parsing and validation
209
- - `@onlineapps/cookbook-executor` - Workflow execution engine
210
- <!-- cookbook-transformer removed in operations.json-first migration; see /api/docs/biz/30-operations/registration-wire.md -->
211
- - `@onlineapps/connector-cookbook` - Full-featured wrapper
86
+ - `@onlineapps/cookbook-core` — cookbook parsing and validation
87
+ - `@onlineapps/conn-orch-cookbook` — the wrapper that re-exports this package to
88
+ `ServiceWrapper`
89
+ - `@onlineapps/conn-orch-orchestrator` — the owner of execution, retry, DLQ and
90
+ completion
212
91
 
213
92
  ## License
214
93
 
215
- PROPRIETARY - All rights reserved
94
+ PROPRIETARY - All rights reserved
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/cookbook-router",
3
- "version": "1.0.36",
3
+ "version": "2.0.0",
4
4
  "description": "Message routing for cookbook workflows - handles service discovery and queue routing",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -3,14 +3,24 @@
3
3
  /**
4
4
  * @module @onlineapps/cookbook-router
5
5
  *
6
- * Message routing library for cookbook workflows.
7
- * Handles service discovery, queue routing, and retry logic.
6
+ * Message routing for cookbook workflows: given a service name and a workflow
7
+ * message, publish it to that service's workflow queue.
8
+ *
9
+ * The package does NOT execute cookbooks, drive flow control, retry, complete
10
+ * or dead-letter them. Those belong to `WorkflowOrchestrator` — confirmation
11
+ * `api/docs/governance/confirmations/cookbook-execution-owner.md` 001 — which
12
+ * is also the only caller: it takes `createRouter`
13
+ * (WorkflowOrchestrator.js:71) and calls `routeToService` on the result
14
+ * (:78,233,241,1223,1230). The second routing rail, the retry rail and the
15
+ * queue/registry administration this package used to carry had zero callers
16
+ * outside it and were removed on 2026-09-02.
17
+ *
18
+ * @see /api/shared/cookbook/cookbook-router/README.md
8
19
  */
9
20
 
10
21
  const CookbookRouter = require('./router');
11
22
  const ServiceDiscovery = require('./serviceDiscovery');
12
23
  const QueueManager = require('./queueManager');
13
- const RetryHandler = require('./retryHandler');
14
24
 
15
25
  module.exports = {
16
26
  // Main router class
@@ -19,7 +29,6 @@ module.exports = {
19
29
  // Supporting classes
20
30
  ServiceDiscovery,
21
31
  QueueManager,
22
- RetryHandler,
23
32
 
24
33
  // Factory function
25
34
  createRouter: (mqClient, registryClient, options = {}) => {
@@ -28,4 +37,4 @@ module.exports = {
28
37
 
29
38
  // Utility exports
30
39
  VERSION: '1.0.0'
31
- };
40
+ };
@@ -95,104 +95,6 @@ class QueueManager {
95
95
  throw error;
96
96
  }
97
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
- //
134
- // The signature is `nack(msg, options)`, NOT amqplib's raw
135
- // `nack(msg, allUpTo, requeue)`. This call read
136
- // `nack(message, false, false)` until 2026-08-28: `false` arrived as
137
- // `options`, `false.requeue` is `undefined`, and the client defaults an
138
- // undefined `requeue` to TRUE
139
- // (`@onlineapps/mq-client-core/src/transports/rabbitmqClient.js:2252`).
140
- // Every message whose handler threw was therefore requeued forever
141
- // instead of being discarded — the exact opposite of what the line above
142
- // it claimed. Measured against the live broker: one publish produced 5
143
- // deliveries in 1.2 s and kept going.
144
- this.mqClient.nack(message, { requeue: false });
145
- }
146
- };
147
-
148
- await this.mqClient.consume(queueName, wrappedHandler, consumerOptions);
149
- }
150
-
151
- /**
152
- * Get queue information
153
- * @param {string} queueName - Queue name
154
- * @returns {Promise<Object>}
155
- */
156
- async getQueueInfo(queueName) {
157
- return await this.mqClient.checkQueue(queueName);
158
- }
159
-
160
- /**
161
- * Purge queue (remove all messages)
162
- * @param {string} queueName - Queue name
163
- * @returns {Promise<Object>}
164
- */
165
- async purgeQueue(queueName) {
166
- const { logger } = this.options;
167
- const result = await this.mqClient.purgeQueue(queueName);
168
- logger.warn(`Purged ${result.messageCount} messages from ${queueName}`);
169
- return result;
170
- }
171
-
172
- /**
173
- * Delete queue
174
- * @param {string} queueName - Queue name
175
- * @returns {Promise<boolean>}
176
- */
177
- async deleteQueue(queueName) {
178
- const { logger } = this.options;
179
- const result = await this.mqClient.deleteQueue(queueName);
180
-
181
- // Remove from ensured queues cache
182
- this.ensuredQueues.delete(queueName);
183
-
184
- logger.warn(`Deleted queue ${queueName}`);
185
- return result;
186
- }
187
-
188
- /**
189
- * Reset connection and clear cache
190
- */
191
- resetConnection() {
192
- const { logger } = this.options;
193
- this.ensuredQueues.clear();
194
- logger.info('Connection reset, cleared queue cache');
195
- }
196
98
  }
197
99
 
198
100
  module.exports = QueueManager;
package/src/router.js CHANGED
@@ -1,181 +1,40 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * CookbookRouter - Main routing class for workflow messages
4
+ * CookbookRouter - routes a workflow message to a service queue.
5
+ *
6
+ * ONE responsibility, ONE method: `routeToService`. Execution, flow control,
7
+ * retry, DLQ and completion belong to `WorkflowOrchestrator`
8
+ * (confirmation `api/docs/governance/confirmations/cookbook-execution-owner.md`
9
+ * 001), which is also this class's only caller —
10
+ * `WorkflowOrchestrator.js:78,233,241,1223,1230`.
11
+ *
12
+ * @see /api/shared/cookbook/cookbook-router/README.md
5
13
  */
6
14
 
7
15
  const ServiceDiscovery = require('./serviceDiscovery');
8
16
  const QueueManager = require('./queueManager');
9
- const RetryHandler = require('./retryHandler');
10
17
 
11
18
  class CookbookRouter {
12
19
  constructor(mqClient, registryClient, options = {}) {
13
20
  this.mqClient = mqClient;
14
21
  this.registryClient = registryClient;
22
+ // `logger` is the only key this class reads. The block used to declare
23
+ // `defaultQueue`, `completedQueue`, `dlqSuffix`, `maxRetries` and
24
+ // `retryDelay` as well: the first three fed the routing rail removed in
25
+ // DÁVKA 83, and the last two had no reader even before it — `RetryHandler`
26
+ // keyed on `maxAttempts`/`baseDelay`, never on these. A declaration nothing
27
+ // reads is dead (`.claude/rules/change-discipline.md` § Removing).
28
+ // `options` is still forwarded whole to the two collaborators below, so
29
+ // their own keys (`cacheEnabled`, `cacheTTL`, `ensureQueues`,
30
+ // `defaultOptions`) reach them unchanged.
15
31
  this.options = {
16
- defaultQueue: 'workflow.init',
17
- completedQueue: 'workflow.completed',
18
- dlqSuffix: '.dlq',
19
- maxRetries: 3,
20
- retryDelay: 2000,
21
32
  logger: console,
22
33
  ...options
23
34
  };
24
35
 
25
36
  this.serviceDiscovery = new ServiceDiscovery(registryClient, options);
26
37
  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
38
  }
180
39
 
181
40
  /**
@@ -206,31 +65,6 @@ class CookbookRouter {
206
65
 
207
66
  await this.queueManager.publish(queueName, message);
208
67
  }
209
-
210
- /**
211
- * Handle retry logic for steps
212
- * @param {Object} step - Step to retry
213
- * @param {Object} context - Current context
214
- * @returns {Promise<boolean>}
215
- */
216
- async handleRetry(step, context) {
217
- const stepId = step.id;
218
- const attempts = context.attempts?.[stepId] || 0;
219
-
220
- // Check if should retry
221
- const shouldRetry = this.retryHandler.shouldRetry(stepId, { attempts });
222
-
223
- if (shouldRetry) {
224
- // Record attempt
225
- this.retryHandler.recordAttempt(stepId);
226
-
227
- // Apply delay
228
- const delay = this.retryHandler.getRetryDelay(stepId);
229
- await new Promise(resolve => setTimeout(resolve, delay));
230
- }
231
-
232
- return shouldRetry;
233
- }
234
68
  }
235
69
 
236
- module.exports = CookbookRouter;
70
+ module.exports = CookbookRouter;
@@ -50,90 +50,6 @@ class ServiceDiscovery {
50
50
  }
51
51
  }
52
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
53
  /**
138
54
  * Get cached service data
139
55
  * @private
@@ -1,186 +0,0 @@
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;