@onlineapps/conn-orch-orchestrator 1.0.48 → 1.0.50

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.50",
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,94 @@ 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
+ _execution: {
149
+ status: 'in_progress',
150
+ started_at: new Date().toISOString()
151
+ }
152
+ };
153
+
154
+ // Execute the step (pass api_input from cookbook as context for operations)
155
+ const stepContext = {
156
+ ...cookbookDef.api_input,
157
+ _system: cookbookDef._system,
158
+ delivery: cookbookDef.delivery
159
+ };
160
+
155
161
  let result;
156
162
  if (this.cache && step.type === 'task') {
157
- const cacheKey = this._getCacheKey(step, enrichedContext);
163
+ const cacheKey = this._getCacheKey(step, stepContext);
158
164
  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);
165
+ if (!result) {
166
+ result = await this._executeStep(step, stepContext, cookbookDef);
165
167
  await this.cache.set(cacheKey, result, { ttl: 300 });
166
168
  }
167
169
  } else {
168
- // Execute without cache
169
- result = await this._executeStep(step, enrichedContext, cookbookDef);
170
+ result = await this._executeStep(step, stepContext, cookbookDef);
170
171
  }
171
172
 
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
187
- status: 'completed',
188
- completed_at: new Date().toISOString()
173
+ // Calculate duration
174
+ const duration_ms = Date.now() - startTime;
175
+
176
+ // Determine next step
177
+ const nextStep = stepsArray[currentIndex + 1];
178
+ const nextPointer = nextStep
179
+ ? this._getStepId(nextStep)
180
+ : 'api_delivery_dispatcher'; // Last step -> dispatcher
181
+
182
+ // Update step with output and execution metadata
183
+ updatedSteps[currentIndex] = {
184
+ ...step,
185
+ output: result,
186
+ _execution: {
187
+ status: 'completed',
188
+ started_at: updatedSteps[currentIndex]._execution?.started_at || new Date().toISOString(),
189
+ completed_at: new Date().toISOString(),
190
+ duration_ms
191
+ }
189
192
  };
190
-
191
- const updatedContext = {
192
- ...enrichedContext,
193
- steps: existingSteps
193
+
194
+ // Build UPDATED UNIFIED COOKBOOK
195
+ const updatedCookbook = {
196
+ ...cookbookDef,
197
+ steps: updatedSteps,
198
+ _pointer: { next: nextStep ? this._getStepId(nextStep) : null }
194
199
  };
195
200
 
196
- // Publish workflow.progress AFTER execution (so we have step output)
201
+ // Publish workflow.progress with UNIFIED COOKBOOK
197
202
  const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
198
203
  try {
199
- console.log(`[WorkflowOrchestrator] [PROGRESS] Publishing progress for ${workflow_id}, step: ${current_step}, index: ${currentIndex}`);
200
204
  await publishToMonitoringWorkflow(this.mqClient, {
201
205
  event_type: 'progress',
202
206
  workflow_id: workflow_id,
203
207
  service_name: serviceName,
204
- step_index: currentIndex >= 0 ? currentIndex : 0,
208
+ step_index: currentIndex,
205
209
  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)
210
+ cookbook: updatedCookbook, // UNIFIED - contains everything
211
+ output: result,
212
+ status: 'completed',
210
213
  timestamp: new Date().toISOString()
211
214
  }, this.logger, { workflow_id, step_id: current_step });
212
- console.log(`[WorkflowOrchestrator] [PROGRESS] ✓ Progress published for ${workflow_id}`);
213
215
  } catch (monitoringError) {
214
- // Don't fail workflow if monitoring publish fails
215
- console.error(`[WorkflowOrchestrator] [PROGRESS] ✗ Failed to publish progress:`, monitoringError);
216
216
  this.logger.warn('Failed to publish workflow.progress to monitoring', {
217
217
  workflow_id,
218
- error: monitoringError.message || String(monitoringError)
218
+ error: monitoringError.message
219
219
  });
220
220
  }
221
221
 
222
- // Find next step (currentIndex and stepsArray already defined above)
223
- const nextStep = stepsArray[currentIndex + 1];
224
-
222
+ // Route to next step or complete workflow
225
223
  if (nextStep) {
226
- // Route to next step
227
- await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id);
224
+ await this._routeToNextStep(nextStep, updatedCookbook, workflow_id);
228
225
  } 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);
226
+ await this._completeWorkflow(workflow_id, updatedCookbook, serviceName, current_step, currentIndex);
231
227
  }
232
228
 
233
229
  return {
@@ -238,35 +234,55 @@ class WorkflowOrchestrator {
238
234
  };
239
235
 
240
236
  } 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;
237
+ // UNIFIED COOKBOOK: _retry_history is stored INSIDE the step, not globally
238
+ const crypto = require('crypto');
245
239
 
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
- }
240
+ // Find current step index
241
+ const stepsArray = this._getStepsArray(cookbookDef);
242
+ const currentIndex = stepsArray.findIndex(s => this._getStepId(s) === current_step);
243
+ const currentStepDef = stepsArray[currentIndex] || {};
244
+
245
+ // Get existing retry history from the step itself
246
+ const stepRetryHistory = currentStepDef._retry_history || [];
247
+ const attemptNumber = stepRetryHistory.length + 1;
248
+
249
+ // Generate error reference for log lookup (instead of full stack trace)
250
+ const errorRef = `err-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
251
+
252
+ // Log full stack trace with error_ref for debugging
253
+ this.logger.error(`[${errorRef}] Step execution failed`, {
254
+ workflow_id,
255
+ step_id: current_step,
256
+ service: serviceName,
257
+ attempt: attemptNumber,
258
+ error: error.message,
259
+ stack: error.stack
260
+ });
255
261
 
256
- // Add attempt to DLQ history
262
+ // Add attempt record to step's _retry_history
257
263
  const attemptRecord = {
258
- step_id: current_step,
259
- step_index: failedStepIndex,
260
264
  attempt: attemptNumber,
261
- service: serviceName,
262
265
  error: error.message,
266
+ error_ref: errorRef, // Reference to full stack in logs
263
267
  at: new Date().toISOString()
264
268
  };
265
269
 
266
- const updatedDlqHistory = [...dlqHistory, attemptRecord];
267
- const updatedContext = {
268
- ...context,
269
- _dlq_history: updatedDlqHistory
270
+ // Update steps array with retry history
271
+ const updatedSteps = [...stepsArray];
272
+ updatedSteps[currentIndex] = {
273
+ ...currentStepDef,
274
+ _execution: {
275
+ ...currentStepDef._execution,
276
+ status: 'retrying'
277
+ },
278
+ _retry_history: [...stepRetryHistory, attemptRecord]
279
+ };
280
+
281
+ // Build updated UNIFIED COOKBOOK
282
+ const updatedCookbook = {
283
+ ...cookbookDef,
284
+ steps: updatedSteps,
285
+ _pointer: { next: current_step } // Still pointing to this step (retrying)
270
286
  };
271
287
 
272
288
  this.logger.warn(`[WorkflowOrchestrator] Step failed, attempt ${attemptNumber}/${this.retryConfig.maxAttempts}`, {
@@ -274,29 +290,27 @@ class WorkflowOrchestrator {
274
290
  current_step,
275
291
  service: serviceName,
276
292
  error: error.message,
293
+ error_ref: errorRef,
277
294
  attempt: attemptNumber
278
295
  });
279
296
 
280
- // Publish progress event for EACH retry attempt (granular tracking)
297
+ // Publish progress event for EACH retry attempt
281
298
  const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
282
299
  try {
283
300
  await publishToMonitoringWorkflow(this.mqClient, {
284
301
  event_type: 'progress',
285
302
  workflow_id: workflow_id,
286
303
  service_name: serviceName,
287
- step_index: failedStepIndex,
304
+ step_index: currentIndex,
288
305
  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
306
+ cookbook: updatedCookbook, // UNIFIED cookbook with _retry_history in step
307
+ error: { message: error.message, error_ref: errorRef },
308
+ status: 'retry',
294
309
  attempt: attemptNumber,
295
310
  max_attempts: this.retryConfig.maxAttempts,
296
311
  timestamp: new Date().toISOString()
297
312
  }, this.logger, { workflow_id, step_id: current_step });
298
313
  } catch (monitoringError) {
299
- // Don't fail if monitoring publish fails
300
314
  this.logger.warn('Failed to publish retry progress to monitoring', {
301
315
  workflow_id,
302
316
  error: monitoringError.message
@@ -305,7 +319,6 @@ class WorkflowOrchestrator {
305
319
 
306
320
  // Check if we should retry
307
321
  if (attemptNumber < this.retryConfig.maxAttempts) {
308
- // Calculate delay with exponential backoff
309
322
  const delay = Math.min(
310
323
  this.retryConfig.baseDelay * Math.pow(this.retryConfig.backoffMultiplier, attemptNumber - 1),
311
324
  this.retryConfig.maxDelay
@@ -317,16 +330,14 @@ class WorkflowOrchestrator {
317
330
  delay
318
331
  });
319
332
 
320
- // Wait for delay
321
333
  await new Promise(resolve => setTimeout(resolve, delay));
322
334
 
323
- // Republish to same service queue for retry
335
+ // Republish with UNIFIED cookbook
324
336
  const serviceQueue = `${serviceName}.workflow`;
325
337
  await this.mqClient.publish(serviceQueue, {
326
338
  workflow_id,
327
- cookbook: cookbookDef,
328
- current_step,
329
- context: updatedContext
339
+ cookbook: updatedCookbook,
340
+ current_step
330
341
  });
331
342
 
332
343
  this.logger.info(`[WorkflowOrchestrator] Message republished for retry`, {
@@ -336,25 +347,31 @@ class WorkflowOrchestrator {
336
347
  nextAttempt: attemptNumber + 1
337
348
  });
338
349
 
339
- // Don't throw - message will be retried
340
350
  return { retrying: true, attempt: attemptNumber, nextAttempt: attemptNumber + 1 };
341
351
  }
342
352
 
343
- // Max retries exhausted - send to workflow.failed (DLQ entry)
353
+ // Max retries exhausted - mark step as failed and send to DLQ
344
354
  const dlqEntryRecord = {
345
355
  dlq_entered: true,
346
- step_id: current_step,
347
- step_index: failedStepIndex,
348
- service: serviceName,
349
356
  reason: 'max_retries_exceeded',
350
357
  total_attempts: attemptNumber,
351
358
  at: new Date().toISOString()
352
359
  };
353
360
 
354
- const finalDlqHistory = [...updatedDlqHistory, dlqEntryRecord];
355
- const finalContext = {
356
- ...context,
357
- _dlq_history: finalDlqHistory
361
+ updatedSteps[currentIndex] = {
362
+ ...updatedSteps[currentIndex],
363
+ _execution: {
364
+ ...updatedSteps[currentIndex]._execution,
365
+ status: 'failed',
366
+ failed_at: new Date().toISOString()
367
+ },
368
+ _retry_history: [...updatedSteps[currentIndex]._retry_history, dlqEntryRecord]
369
+ };
370
+
371
+ const finalCookbook = {
372
+ ...cookbookDef,
373
+ steps: updatedSteps,
374
+ _pointer: { next: null } // Workflow stuck - waiting for human intervention
358
375
  };
359
376
 
360
377
  try {
@@ -363,41 +380,32 @@ class WorkflowOrchestrator {
363
380
  workflow_id,
364
381
  current_step,
365
382
  attempts: attemptNumber,
366
- error: error.stack
383
+ error_ref: errorRef // Reference to full stack in logs
367
384
  });
368
385
 
369
386
  // Publish to workflow.failed (DLQ queue - message stays until manual requeue/discard)
387
+ // UNIFIED COOKBOOK contains everything including _retry_history in the step
370
388
  await this.mqClient.publish('workflow.failed', {
371
389
  workflow_id,
372
390
  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,
391
+ cookbook: finalCookbook, // UNIFIED cookbook with full state
381
392
  failed_at: new Date().toISOString()
382
393
  });
383
394
 
384
395
  // 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
396
  try {
387
397
  await publishToMonitoringWorkflow(this.mqClient, {
388
398
  event_type: 'failed',
389
399
  workflow_id: workflow_id,
390
400
  service_name: serviceName,
391
- step_index: failedStepIndex,
401
+ step_index: currentIndex,
392
402
  step_id: current_step,
393
- cookbook: cookbookDef,
394
- context: finalContext,
395
- error: { message: error.message },
403
+ cookbook: finalCookbook, // UNIFIED cookbook
404
+ error: { message: error.message, error_ref: errorRef },
396
405
  status: 'failed',
397
406
  timestamp: new Date().toISOString()
398
407
  }, this.logger, { workflow_id, step_id: current_step });
399
408
  } catch (monitoringError) {
400
- // Don't fail if monitoring publish fails
401
409
  this.logger.warn('[WorkflowOrchestrator] Failed to publish DLQ entry to monitoring', {
402
410
  workflow_id,
403
411
  error: monitoringError.message
@@ -600,17 +608,17 @@ class WorkflowOrchestrator {
600
608
  * @private
601
609
  * @async
602
610
  * @param {Object} nextStep - Next step to execute
603
- * @param {Object} cookbookDef - Full cookbook definition
604
- * @param {Object} context - Updated context
611
+ * @param {Object} cookbook - UNIFIED cookbook (contains everything)
605
612
  * @param {string} workflow_id - Workflow ID
606
613
  */
607
- async _routeToNextStep(nextStep, cookbookDef, context, workflow_id) {
614
+ async _routeToNextStep(nextStep, cookbook, workflow_id) {
608
615
  const nextStepId = this._getStepId(nextStep);
616
+
617
+ // UNIFIED COOKBOOK: no separate context, everything is in cookbook
609
618
  const message = {
610
619
  workflow_id,
611
- cookbook: cookbookDef,
612
- current_step: nextStepId,
613
- context
620
+ cookbook: cookbook,
621
+ current_step: nextStepId
614
622
  };
615
623
 
616
624
  if (nextStep.type === 'task') {
@@ -642,38 +650,37 @@ class WorkflowOrchestrator {
642
650
  * @private
643
651
  * @async
644
652
  * @param {string} workflow_id - Workflow ID
645
- * @param {Object} finalContext - Final workflow context
653
+ * @param {Object} cookbook - UNIFIED cookbook (contains everything)
646
654
  * @param {string} serviceName - Service name that completed the workflow
647
655
  * @param {string} lastStepId - ID of the last completed step
648
656
  * @param {number} lastStepIndex - Index of the last completed step
649
657
  */
650
- async _completeWorkflow(workflow_id, finalContext, cookbookDef = {}, serviceName = 'unknown', lastStepId = null, lastStepIndex = 0) {
658
+ async _completeWorkflow(workflow_id, cookbook, serviceName = 'unknown', lastStepId = null, lastStepIndex = 0) {
651
659
  try {
652
660
  console.log(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed for ${workflow_id}`);
653
661
  this.logger.info(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed`, {
654
662
  workflow_id,
655
663
  serviceName,
656
664
  lastStepId,
657
- lastStepIndex,
658
- hasMqClient: !!this.mqClient,
659
- mqClientConnected: this.mqClient?._connected
665
+ lastStepIndex
660
666
  });
661
667
 
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' };
668
+ // Update pointer to dispatcher (final step)
669
+ const finalCookbook = {
670
+ ...cookbook,
671
+ _pointer: { next: 'api_delivery_dispatcher' }
672
+ };
665
673
 
666
674
  // Build message in format expected by Delivery Dispatcher
675
+ // UNIFIED COOKBOOK contains everything including delivery config
667
676
  const workflowCompletedMessage = {
668
677
  workflow_id,
669
678
  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
679
+ service: serviceName,
680
+ step_id: lastStepId,
681
+ step_index: lastStepIndex,
682
+ cookbook: finalCookbook, // UNIFIED cookbook with full state
683
+ delivery: cookbook.delivery || { handler: 'none' },
677
684
  completed_at: new Date().toISOString()
678
685
  };
679
686