@onlineapps/conn-orch-orchestrator 1.0.47 → 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.47",
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,39 +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
- }
255
-
256
- // NOTE: We do NOT publish workflow_progress on every retry attempt.
257
- // Instead, we publish ONE progress event at the end (either success or final failure with DLQ entry).
258
- // This keeps the trace clean - one entry per step, with complete _dlq_history in context.
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
+ });
259
257
 
260
- // Add attempt to DLQ history
258
+ // Add attempt record to step's _retry_history
261
259
  const attemptRecord = {
262
- step_id: current_step,
263
- step_index: failedStepIndex,
264
260
  attempt: attemptNumber,
265
- service: serviceName,
266
261
  error: error.message,
262
+ error_ref: errorRef, // Reference to full stack in logs
267
263
  at: new Date().toISOString()
268
264
  };
269
265
 
270
- const updatedDlqHistory = [...dlqHistory, attemptRecord];
271
- const updatedContext = {
272
- ...context,
273
- _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)
274
279
  };
275
280
 
276
281
  this.logger.warn(`[WorkflowOrchestrator] Step failed, attempt ${attemptNumber}/${this.retryConfig.maxAttempts}`, {
@@ -278,12 +283,35 @@ class WorkflowOrchestrator {
278
283
  current_step,
279
284
  service: serviceName,
280
285
  error: error.message,
286
+ error_ref: errorRef,
281
287
  attempt: attemptNumber
282
288
  });
283
289
 
290
+ // Publish progress event for EACH retry attempt
291
+ const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
292
+ try {
293
+ await publishToMonitoringWorkflow(this.mqClient, {
294
+ event_type: 'progress',
295
+ workflow_id: workflow_id,
296
+ service_name: serviceName,
297
+ step_index: currentIndex,
298
+ step_id: current_step,
299
+ cookbook: updatedCookbook, // UNIFIED cookbook with _retry_history in step
300
+ error: { message: error.message, error_ref: errorRef },
301
+ status: 'retry',
302
+ attempt: attemptNumber,
303
+ max_attempts: this.retryConfig.maxAttempts,
304
+ timestamp: new Date().toISOString()
305
+ }, this.logger, { workflow_id, step_id: current_step });
306
+ } catch (monitoringError) {
307
+ this.logger.warn('Failed to publish retry progress to monitoring', {
308
+ workflow_id,
309
+ error: monitoringError.message
310
+ });
311
+ }
312
+
284
313
  // Check if we should retry
285
314
  if (attemptNumber < this.retryConfig.maxAttempts) {
286
- // Calculate delay with exponential backoff
287
315
  const delay = Math.min(
288
316
  this.retryConfig.baseDelay * Math.pow(this.retryConfig.backoffMultiplier, attemptNumber - 1),
289
317
  this.retryConfig.maxDelay
@@ -295,16 +323,14 @@ class WorkflowOrchestrator {
295
323
  delay
296
324
  });
297
325
 
298
- // Wait for delay
299
326
  await new Promise(resolve => setTimeout(resolve, delay));
300
327
 
301
- // Republish to same service queue for retry
328
+ // Republish with UNIFIED cookbook
302
329
  const serviceQueue = `${serviceName}.workflow`;
303
330
  await this.mqClient.publish(serviceQueue, {
304
331
  workflow_id,
305
- cookbook: cookbookDef,
306
- current_step,
307
- context: updatedContext
332
+ cookbook: updatedCookbook,
333
+ current_step
308
334
  });
309
335
 
310
336
  this.logger.info(`[WorkflowOrchestrator] Message republished for retry`, {
@@ -314,25 +340,27 @@ class WorkflowOrchestrator {
314
340
  nextAttempt: attemptNumber + 1
315
341
  });
316
342
 
317
- // Don't throw - message will be retried
318
343
  return { retrying: true, attempt: attemptNumber, nextAttempt: attemptNumber + 1 };
319
344
  }
320
345
 
321
- // Max retries exhausted - send to workflow.failed (DLQ entry)
346
+ // Max retries exhausted - mark step as failed and send to DLQ
322
347
  const dlqEntryRecord = {
323
348
  dlq_entered: true,
324
- step_id: current_step,
325
- step_index: failedStepIndex,
326
- service: serviceName,
327
349
  reason: 'max_retries_exceeded',
328
350
  total_attempts: attemptNumber,
329
351
  at: new Date().toISOString()
330
352
  };
331
353
 
332
- const finalDlqHistory = [...updatedDlqHistory, dlqEntryRecord];
333
- const finalContext = {
334
- ...context,
335
- _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
336
364
  };
337
365
 
338
366
  try {
@@ -341,41 +369,32 @@ class WorkflowOrchestrator {
341
369
  workflow_id,
342
370
  current_step,
343
371
  attempts: attemptNumber,
344
- error: error.stack
372
+ error_ref: errorRef // Reference to full stack in logs
345
373
  });
346
374
 
347
375
  // Publish to workflow.failed (DLQ queue - message stays until manual requeue/discard)
376
+ // UNIFIED COOKBOOK contains everything including _retry_history in the step
348
377
  await this.mqClient.publish('workflow.failed', {
349
378
  workflow_id,
350
379
  current_step,
351
- step_id: current_step,
352
- step_index: failedStepIndex,
353
- service: serviceName,
354
- cookbook: cookbookDef,
355
- context: finalContext,
356
- error: error.message,
357
- errorStack: error.stack,
358
- _dlq_history: finalDlqHistory,
380
+ cookbook: finalCookbook, // UNIFIED cookbook with full state
359
381
  failed_at: new Date().toISOString()
360
382
  });
361
383
 
362
384
  // ALSO publish to monitoring.workflow so dashboard shows the DLQ entry
363
- // This is separate from the DLQ message - it's just for visibility
364
385
  try {
365
386
  await publishToMonitoringWorkflow(this.mqClient, {
366
387
  event_type: 'failed',
367
388
  workflow_id: workflow_id,
368
389
  service_name: serviceName,
369
- step_index: failedStepIndex,
390
+ step_index: currentIndex,
370
391
  step_id: current_step,
371
- cookbook: cookbookDef,
372
- context: finalContext,
373
- error: { message: error.message },
392
+ cookbook: finalCookbook, // UNIFIED cookbook
393
+ error: { message: error.message, error_ref: errorRef },
374
394
  status: 'failed',
375
395
  timestamp: new Date().toISOString()
376
396
  }, this.logger, { workflow_id, step_id: current_step });
377
397
  } catch (monitoringError) {
378
- // Don't fail if monitoring publish fails
379
398
  this.logger.warn('[WorkflowOrchestrator] Failed to publish DLQ entry to monitoring', {
380
399
  workflow_id,
381
400
  error: monitoringError.message
@@ -578,17 +597,17 @@ class WorkflowOrchestrator {
578
597
  * @private
579
598
  * @async
580
599
  * @param {Object} nextStep - Next step to execute
581
- * @param {Object} cookbookDef - Full cookbook definition
582
- * @param {Object} context - Updated context
600
+ * @param {Object} cookbook - UNIFIED cookbook (contains everything)
583
601
  * @param {string} workflow_id - Workflow ID
584
602
  */
585
- async _routeToNextStep(nextStep, cookbookDef, context, workflow_id) {
603
+ async _routeToNextStep(nextStep, cookbook, workflow_id) {
586
604
  const nextStepId = this._getStepId(nextStep);
605
+
606
+ // UNIFIED COOKBOOK: no separate context, everything is in cookbook
587
607
  const message = {
588
608
  workflow_id,
589
- cookbook: cookbookDef,
590
- current_step: nextStepId,
591
- context
609
+ cookbook: cookbook,
610
+ current_step: nextStepId
592
611
  };
593
612
 
594
613
  if (nextStep.type === 'task') {
@@ -620,38 +639,37 @@ class WorkflowOrchestrator {
620
639
  * @private
621
640
  * @async
622
641
  * @param {string} workflow_id - Workflow ID
623
- * @param {Object} finalContext - Final workflow context
642
+ * @param {Object} cookbook - UNIFIED cookbook (contains everything)
624
643
  * @param {string} serviceName - Service name that completed the workflow
625
644
  * @param {string} lastStepId - ID of the last completed step
626
645
  * @param {number} lastStepIndex - Index of the last completed step
627
646
  */
628
- async _completeWorkflow(workflow_id, finalContext, cookbookDef = {}, serviceName = 'unknown', lastStepId = null, lastStepIndex = 0) {
647
+ async _completeWorkflow(workflow_id, cookbook, serviceName = 'unknown', lastStepId = null, lastStepIndex = 0) {
629
648
  try {
630
649
  console.log(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed for ${workflow_id}`);
631
650
  this.logger.info(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed`, {
632
651
  workflow_id,
633
652
  serviceName,
634
653
  lastStepId,
635
- lastStepIndex,
636
- hasMqClient: !!this.mqClient,
637
- mqClientConnected: this.mqClient?._connected
654
+ lastStepIndex
638
655
  });
639
656
 
640
- // Extract delivery configuration from context (passed from Gateway)
641
- // Delivery Dispatcher requires: workflow_id, status, delivery (must be object, not null)
642
- 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
+ };
643
662
 
644
663
  // Build message in format expected by Delivery Dispatcher
664
+ // UNIFIED COOKBOOK contains everything including delivery config
645
665
  const workflowCompletedMessage = {
646
666
  workflow_id,
647
667
  status: 'completed',
648
- service: serviceName, // Service that completed the workflow (for monitoring)
649
- step_id: lastStepId, // Last step ID for monitoring
650
- step_index: lastStepIndex, // Last step index for monitoring
651
- cookbook: cookbookDef, // Full cookbook for monitoring trace
652
- delivery: delivery, // Delivery configuration from Gateway context (must be object)
653
- context: finalContext, // Full context for output resolution
654
- 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' },
655
673
  completed_at: new Date().toISOString()
656
674
  };
657
675