@onlineapps/conn-orch-orchestrator 1.0.48 → 1.0.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-orchestrator",
3
- "version": "1.0.48",
3
+ "version": "1.0.49",
4
4
  "description": "Workflow orchestration connector for OA Drive - handles message routing and workflow execution",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -116,24 +116,19 @@ class WorkflowOrchestrator {
116
116
  * }, 'hello-service');
117
117
  */
118
118
  async processWorkflowMessage(message, serviceName) {
119
- const { workflow_id, cookbook: cookbookDef, current_step, context } = message;
119
+ const { workflow_id, cookbook: cookbookDef, current_step } = message;
120
+ const startTime = Date.now();
120
121
 
121
122
  try {
122
123
  // Validate cookbook structure
123
124
  this.cookbook.validateCookbook(cookbookDef);
124
125
 
125
- // Ensure delivery configuration is preserved in context (required by Delivery Dispatcher)
126
- // Delivery comes from cookbook.delivery (set by Gateway) and must be passed through to workflow.completed
127
- const enrichedContext = {
128
- ...context,
129
- // Preserve delivery from cookbook if not already in context
130
- delivery: context?.delivery || cookbookDef?.delivery || { handler: 'none' }
131
- };
132
-
133
- // Find current step (supports both V1 'id' and V2 'step_id')
134
- // V2 FORMAT: steps is an object keyed by step_id
126
+ // UNIFIED COOKBOOK: Everything is in cookbook, no separate context
127
+ // Find current step
135
128
  const stepsArray = this._getStepsArray(cookbookDef);
136
- const step = stepsArray.find(s => this._getStepId(s) === current_step);
129
+ const currentIndex = stepsArray.findIndex(s => this._getStepId(s) === current_step);
130
+ const step = stepsArray[currentIndex];
131
+
137
132
  if (!step) {
138
133
  throw new Error(`Step not found: ${current_step}`);
139
134
  }
@@ -141,93 +136,90 @@ class WorkflowOrchestrator {
141
136
  // Check if this step is for this service
142
137
  if (step.type === 'task' && step.service !== serviceName) {
143
138
  this.logger.warn(`Step ${current_step} is for ${step.service}, not ${serviceName}`);
144
- if (this.router?.routeToService) {
145
- await this.router.routeToService(step.service, message);
146
- } else {
147
- // Fallback: publish to service's workflow queue directly
148
- const serviceQueue = `${step.service}.workflow`;
149
- await this.mqClient.publish(serviceQueue, message);
150
- }
139
+ const serviceQueue = `${step.service}.workflow`;
140
+ await this.mqClient.publish(serviceQueue, message);
151
141
  return { skipped: true, reason: 'wrong_service' };
152
142
  }
153
143
 
154
- // Check cache if available
144
+ // Mark step as started
145
+ const updatedSteps = [...stepsArray];
146
+ updatedSteps[currentIndex] = {
147
+ ...step,
148
+ started_at: new Date().toISOString(),
149
+ status: 'in_progress'
150
+ };
151
+
152
+ // Execute the step (pass _input from cookbook as context for operations)
153
+ const stepContext = {
154
+ ...cookbookDef._input,
155
+ _system: cookbookDef._system,
156
+ delivery: cookbookDef.delivery
157
+ };
158
+
155
159
  let result;
156
160
  if (this.cache && step.type === 'task') {
157
- const cacheKey = this._getCacheKey(step, enrichedContext);
161
+ const cacheKey = this._getCacheKey(step, stepContext);
158
162
  result = await this.cache.get(cacheKey);
159
-
160
- if (result) {
161
- this.logger.info('Cache hit', { step: this._getStepId(step), cacheKey });
162
- } else {
163
- // Execute the step
164
- result = await this._executeStep(step, enrichedContext, cookbookDef);
163
+ if (!result) {
164
+ result = await this._executeStep(step, stepContext, cookbookDef);
165
165
  await this.cache.set(cacheKey, result, { ttl: 300 });
166
166
  }
167
167
  } else {
168
- // Execute without cache
169
- result = await this._executeStep(step, enrichedContext, cookbookDef);
168
+ result = await this._executeStep(step, stepContext, cookbookDef);
170
169
  }
171
170
 
172
- // Update context with result - steps as ARRAY (consistent with cookbook.steps)
173
- // Each step preserves its definition (id/step_id, type, service, operation, input) and adds output
174
- // stepsArray is already defined above via _getStepsArray()
175
- const currentIndex = stepsArray.findIndex(s => this._getStepId(s) === current_step);
176
- const stepDefinition = stepsArray[currentIndex];
177
-
178
- // Initialize steps array from cookbook if not present
179
- const existingSteps = Array.isArray(enrichedContext.steps)
180
- ? [...enrichedContext.steps]
181
- : stepsArray.map(s => ({ ...s })); // Deep copy of step definitions
182
-
183
- // Update the current step with output (preserve id, type, service, operation, input)
184
- existingSteps[currentIndex] = {
185
- ...stepDefinition, // id, type, service, operation, input
186
- output: result, // Add output from operation
171
+ // Calculate duration
172
+ const duration_ms = Date.now() - startTime;
173
+
174
+ // Determine next step
175
+ const nextStep = stepsArray[currentIndex + 1];
176
+ const nextPointer = nextStep
177
+ ? this._getStepId(nextStep)
178
+ : 'api_delivery_dispatcher'; // Last step -> dispatcher
179
+
180
+ // Update step with output and completion info
181
+ updatedSteps[currentIndex] = {
182
+ ...step,
183
+ started_at: updatedSteps[currentIndex].started_at,
184
+ output: result,
187
185
  status: 'completed',
188
- completed_at: new Date().toISOString()
186
+ completed_at: new Date().toISOString(),
187
+ duration_ms
189
188
  };
190
-
191
- const updatedContext = {
192
- ...enrichedContext,
193
- steps: existingSteps
189
+
190
+ // Build UPDATED UNIFIED COOKBOOK
191
+ const updatedCookbook = {
192
+ ...cookbookDef,
193
+ steps: updatedSteps,
194
+ _pointer: { next: nextStep ? this._getStepId(nextStep) : null }
194
195
  };
195
196
 
196
- // Publish workflow.progress AFTER execution (so we have step output)
197
+ // Publish workflow.progress with UNIFIED COOKBOOK
197
198
  const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
198
199
  try {
199
- console.log(`[WorkflowOrchestrator] [PROGRESS] Publishing progress for ${workflow_id}, step: ${current_step}, index: ${currentIndex}`);
200
200
  await publishToMonitoringWorkflow(this.mqClient, {
201
201
  event_type: 'progress',
202
202
  workflow_id: workflow_id,
203
203
  service_name: serviceName,
204
- step_index: currentIndex >= 0 ? currentIndex : 0,
204
+ step_index: currentIndex,
205
205
  step_id: current_step,
206
- cookbook: cookbookDef,
207
- context: updatedContext, // Context WITH step output
208
- output: result, // Step output directly
209
- status: 'completed', // Step completed (not in_progress)
206
+ cookbook: updatedCookbook, // UNIFIED - contains everything
207
+ output: result,
208
+ status: 'completed',
210
209
  timestamp: new Date().toISOString()
211
210
  }, this.logger, { workflow_id, step_id: current_step });
212
- console.log(`[WorkflowOrchestrator] [PROGRESS] ✓ Progress published for ${workflow_id}`);
213
211
  } catch (monitoringError) {
214
- // Don't fail workflow if monitoring publish fails
215
- console.error(`[WorkflowOrchestrator] [PROGRESS] ✗ Failed to publish progress:`, monitoringError);
216
212
  this.logger.warn('Failed to publish workflow.progress to monitoring', {
217
213
  workflow_id,
218
- error: monitoringError.message || String(monitoringError)
214
+ error: monitoringError.message
219
215
  });
220
216
  }
221
217
 
222
- // Find next step (currentIndex and stepsArray already defined above)
223
- const nextStep = stepsArray[currentIndex + 1];
224
-
218
+ // Route to next step or complete workflow
225
219
  if (nextStep) {
226
- // Route to next step
227
- await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id);
220
+ await this._routeToNextStep(nextStep, updatedCookbook, workflow_id);
228
221
  } else {
229
- // Workflow completed - pass serviceName, cookbook and last step info for monitoring
230
- await this._completeWorkflow(workflow_id, updatedContext, cookbookDef, serviceName, current_step, currentIndex);
222
+ await this._completeWorkflow(workflow_id, updatedCookbook, serviceName, current_step, currentIndex);
231
223
  }
232
224
 
233
225
  return {
@@ -238,35 +230,52 @@ class WorkflowOrchestrator {
238
230
  };
239
231
 
240
232
  } catch (error) {
241
- // DLQ/Retry mechanism: retry with exponential backoff before sending to DLQ
242
- const dlqHistory = context._dlq_history || [];
243
- const stepAttempts = dlqHistory.filter(h => h.step_id === current_step && h.attempt).length;
244
- const attemptNumber = stepAttempts + 1;
233
+ // UNIFIED COOKBOOK: _retry_history is stored INSIDE the step, not globally
234
+ const crypto = require('crypto');
245
235
 
246
- // Calculate step_index for failed step
247
- let failedStepIndex = 0;
248
- try {
249
- const stepsArr = this._getStepsArray(cookbookDef);
250
- const idx = stepsArr.findIndex(s => this._getStepId(s) === current_step);
251
- if (idx >= 0) failedStepIndex = idx;
252
- } catch (e) {
253
- // Ignore - use default 0
254
- }
236
+ // Find current step index
237
+ const stepsArray = this._getStepsArray(cookbookDef);
238
+ const currentIndex = stepsArray.findIndex(s => this._getStepId(s) === current_step);
239
+ const currentStepDef = stepsArray[currentIndex] || {};
240
+
241
+ // Get existing retry history from the step itself
242
+ const stepRetryHistory = currentStepDef._retry_history || [];
243
+ const attemptNumber = stepRetryHistory.length + 1;
244
+
245
+ // Generate error reference for log lookup (instead of full stack trace)
246
+ const errorRef = `err-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
247
+
248
+ // Log full stack trace with error_ref for debugging
249
+ this.logger.error(`[${errorRef}] Step execution failed`, {
250
+ workflow_id,
251
+ step_id: current_step,
252
+ service: serviceName,
253
+ attempt: attemptNumber,
254
+ error: error.message,
255
+ stack: error.stack
256
+ });
255
257
 
256
- // Add attempt to DLQ history
258
+ // Add attempt record to step's _retry_history
257
259
  const attemptRecord = {
258
- step_id: current_step,
259
- step_index: failedStepIndex,
260
260
  attempt: attemptNumber,
261
- service: serviceName,
262
261
  error: error.message,
262
+ error_ref: errorRef, // Reference to full stack in logs
263
263
  at: new Date().toISOString()
264
264
  };
265
265
 
266
- const updatedDlqHistory = [...dlqHistory, attemptRecord];
267
- const updatedContext = {
268
- ...context,
269
- _dlq_history: updatedDlqHistory
266
+ // Update steps array with retry history
267
+ const updatedSteps = [...stepsArray];
268
+ updatedSteps[currentIndex] = {
269
+ ...currentStepDef,
270
+ status: 'retrying',
271
+ _retry_history: [...stepRetryHistory, attemptRecord]
272
+ };
273
+
274
+ // Build updated UNIFIED COOKBOOK
275
+ const updatedCookbook = {
276
+ ...cookbookDef,
277
+ steps: updatedSteps,
278
+ _pointer: { next: current_step } // Still pointing to this step (retrying)
270
279
  };
271
280
 
272
281
  this.logger.warn(`[WorkflowOrchestrator] Step failed, attempt ${attemptNumber}/${this.retryConfig.maxAttempts}`, {
@@ -274,29 +283,27 @@ class WorkflowOrchestrator {
274
283
  current_step,
275
284
  service: serviceName,
276
285
  error: error.message,
286
+ error_ref: errorRef,
277
287
  attempt: attemptNumber
278
288
  });
279
289
 
280
- // Publish progress event for EACH retry attempt (granular tracking)
290
+ // Publish progress event for EACH retry attempt
281
291
  const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
282
292
  try {
283
293
  await publishToMonitoringWorkflow(this.mqClient, {
284
294
  event_type: 'progress',
285
295
  workflow_id: workflow_id,
286
296
  service_name: serviceName,
287
- step_index: failedStepIndex,
297
+ step_index: currentIndex,
288
298
  step_id: current_step,
289
- cookbook: cookbookDef,
290
- context: updatedContext, // Context with updated _dlq_history
291
- output: null,
292
- error: { message: error.message },
293
- status: 'retry', // Mark as retry attempt
299
+ cookbook: updatedCookbook, // UNIFIED cookbook with _retry_history in step
300
+ error: { message: error.message, error_ref: errorRef },
301
+ status: 'retry',
294
302
  attempt: attemptNumber,
295
303
  max_attempts: this.retryConfig.maxAttempts,
296
304
  timestamp: new Date().toISOString()
297
305
  }, this.logger, { workflow_id, step_id: current_step });
298
306
  } catch (monitoringError) {
299
- // Don't fail if monitoring publish fails
300
307
  this.logger.warn('Failed to publish retry progress to monitoring', {
301
308
  workflow_id,
302
309
  error: monitoringError.message
@@ -305,7 +312,6 @@ class WorkflowOrchestrator {
305
312
 
306
313
  // Check if we should retry
307
314
  if (attemptNumber < this.retryConfig.maxAttempts) {
308
- // Calculate delay with exponential backoff
309
315
  const delay = Math.min(
310
316
  this.retryConfig.baseDelay * Math.pow(this.retryConfig.backoffMultiplier, attemptNumber - 1),
311
317
  this.retryConfig.maxDelay
@@ -317,16 +323,14 @@ class WorkflowOrchestrator {
317
323
  delay
318
324
  });
319
325
 
320
- // Wait for delay
321
326
  await new Promise(resolve => setTimeout(resolve, delay));
322
327
 
323
- // Republish to same service queue for retry
328
+ // Republish with UNIFIED cookbook
324
329
  const serviceQueue = `${serviceName}.workflow`;
325
330
  await this.mqClient.publish(serviceQueue, {
326
331
  workflow_id,
327
- cookbook: cookbookDef,
328
- current_step,
329
- context: updatedContext
332
+ cookbook: updatedCookbook,
333
+ current_step
330
334
  });
331
335
 
332
336
  this.logger.info(`[WorkflowOrchestrator] Message republished for retry`, {
@@ -336,25 +340,27 @@ class WorkflowOrchestrator {
336
340
  nextAttempt: attemptNumber + 1
337
341
  });
338
342
 
339
- // Don't throw - message will be retried
340
343
  return { retrying: true, attempt: attemptNumber, nextAttempt: attemptNumber + 1 };
341
344
  }
342
345
 
343
- // Max retries exhausted - send to workflow.failed (DLQ entry)
346
+ // Max retries exhausted - mark step as failed and send to DLQ
344
347
  const dlqEntryRecord = {
345
348
  dlq_entered: true,
346
- step_id: current_step,
347
- step_index: failedStepIndex,
348
- service: serviceName,
349
349
  reason: 'max_retries_exceeded',
350
350
  total_attempts: attemptNumber,
351
351
  at: new Date().toISOString()
352
352
  };
353
353
 
354
- const finalDlqHistory = [...updatedDlqHistory, dlqEntryRecord];
355
- const finalContext = {
356
- ...context,
357
- _dlq_history: finalDlqHistory
354
+ updatedSteps[currentIndex] = {
355
+ ...updatedSteps[currentIndex],
356
+ status: 'failed',
357
+ _retry_history: [...updatedSteps[currentIndex]._retry_history, dlqEntryRecord]
358
+ };
359
+
360
+ const finalCookbook = {
361
+ ...cookbookDef,
362
+ steps: updatedSteps,
363
+ _pointer: { next: null } // Workflow stuck - waiting for human intervention
358
364
  };
359
365
 
360
366
  try {
@@ -363,41 +369,32 @@ class WorkflowOrchestrator {
363
369
  workflow_id,
364
370
  current_step,
365
371
  attempts: attemptNumber,
366
- error: error.stack
372
+ error_ref: errorRef // Reference to full stack in logs
367
373
  });
368
374
 
369
375
  // Publish to workflow.failed (DLQ queue - message stays until manual requeue/discard)
376
+ // UNIFIED COOKBOOK contains everything including _retry_history in the step
370
377
  await this.mqClient.publish('workflow.failed', {
371
378
  workflow_id,
372
379
  current_step,
373
- step_id: current_step,
374
- step_index: failedStepIndex,
375
- service: serviceName,
376
- cookbook: cookbookDef,
377
- context: finalContext,
378
- error: error.message,
379
- errorStack: error.stack,
380
- _dlq_history: finalDlqHistory,
380
+ cookbook: finalCookbook, // UNIFIED cookbook with full state
381
381
  failed_at: new Date().toISOString()
382
382
  });
383
383
 
384
384
  // ALSO publish to monitoring.workflow so dashboard shows the DLQ entry
385
- // This is separate from the DLQ message - it's just for visibility
386
385
  try {
387
386
  await publishToMonitoringWorkflow(this.mqClient, {
388
387
  event_type: 'failed',
389
388
  workflow_id: workflow_id,
390
389
  service_name: serviceName,
391
- step_index: failedStepIndex,
390
+ step_index: currentIndex,
392
391
  step_id: current_step,
393
- cookbook: cookbookDef,
394
- context: finalContext,
395
- error: { message: error.message },
392
+ cookbook: finalCookbook, // UNIFIED cookbook
393
+ error: { message: error.message, error_ref: errorRef },
396
394
  status: 'failed',
397
395
  timestamp: new Date().toISOString()
398
396
  }, this.logger, { workflow_id, step_id: current_step });
399
397
  } catch (monitoringError) {
400
- // Don't fail if monitoring publish fails
401
398
  this.logger.warn('[WorkflowOrchestrator] Failed to publish DLQ entry to monitoring', {
402
399
  workflow_id,
403
400
  error: monitoringError.message
@@ -600,17 +597,17 @@ class WorkflowOrchestrator {
600
597
  * @private
601
598
  * @async
602
599
  * @param {Object} nextStep - Next step to execute
603
- * @param {Object} cookbookDef - Full cookbook definition
604
- * @param {Object} context - Updated context
600
+ * @param {Object} cookbook - UNIFIED cookbook (contains everything)
605
601
  * @param {string} workflow_id - Workflow ID
606
602
  */
607
- async _routeToNextStep(nextStep, cookbookDef, context, workflow_id) {
603
+ async _routeToNextStep(nextStep, cookbook, workflow_id) {
608
604
  const nextStepId = this._getStepId(nextStep);
605
+
606
+ // UNIFIED COOKBOOK: no separate context, everything is in cookbook
609
607
  const message = {
610
608
  workflow_id,
611
- cookbook: cookbookDef,
612
- current_step: nextStepId,
613
- context
609
+ cookbook: cookbook,
610
+ current_step: nextStepId
614
611
  };
615
612
 
616
613
  if (nextStep.type === 'task') {
@@ -642,38 +639,37 @@ class WorkflowOrchestrator {
642
639
  * @private
643
640
  * @async
644
641
  * @param {string} workflow_id - Workflow ID
645
- * @param {Object} finalContext - Final workflow context
642
+ * @param {Object} cookbook - UNIFIED cookbook (contains everything)
646
643
  * @param {string} serviceName - Service name that completed the workflow
647
644
  * @param {string} lastStepId - ID of the last completed step
648
645
  * @param {number} lastStepIndex - Index of the last completed step
649
646
  */
650
- async _completeWorkflow(workflow_id, finalContext, cookbookDef = {}, serviceName = 'unknown', lastStepId = null, lastStepIndex = 0) {
647
+ async _completeWorkflow(workflow_id, cookbook, serviceName = 'unknown', lastStepId = null, lastStepIndex = 0) {
651
648
  try {
652
649
  console.log(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed for ${workflow_id}`);
653
650
  this.logger.info(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed`, {
654
651
  workflow_id,
655
652
  serviceName,
656
653
  lastStepId,
657
- lastStepIndex,
658
- hasMqClient: !!this.mqClient,
659
- mqClientConnected: this.mqClient?._connected
654
+ lastStepIndex
660
655
  });
661
656
 
662
- // Extract delivery configuration from context (passed from Gateway)
663
- // Delivery Dispatcher requires: workflow_id, status, delivery (must be object, not null)
664
- const delivery = finalContext?.delivery || cookbookDef?.delivery || { handler: 'none' };
657
+ // Update pointer to dispatcher (final step)
658
+ const finalCookbook = {
659
+ ...cookbook,
660
+ _pointer: { next: 'api_delivery_dispatcher' }
661
+ };
665
662
 
666
663
  // Build message in format expected by Delivery Dispatcher
664
+ // UNIFIED COOKBOOK contains everything including delivery config
667
665
  const workflowCompletedMessage = {
668
666
  workflow_id,
669
667
  status: 'completed',
670
- service: serviceName, // Service that completed the workflow (for monitoring)
671
- step_id: lastStepId, // Last step ID for monitoring
672
- step_index: lastStepIndex, // Last step index for monitoring
673
- cookbook: cookbookDef, // Full cookbook for monitoring trace
674
- delivery: delivery, // Delivery configuration from Gateway context (must be object)
675
- context: finalContext, // Full context for output resolution
676
- steps: finalContext?.steps || [], // Steps results (array) for output resolution
668
+ service: serviceName,
669
+ step_id: lastStepId,
670
+ step_index: lastStepIndex,
671
+ cookbook: finalCookbook, // UNIFIED cookbook with full state
672
+ delivery: cookbook.delivery || { handler: 'none' },
677
673
  completed_at: new Date().toISOString()
678
674
  };
679
675