arazzo-runner 0.0.18 → 0.0.19

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": "arazzo-runner",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "A runner to run through Arazzo Document workflows",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/Arazzo.js CHANGED
@@ -8,6 +8,8 @@ const https = require("node:https");
8
8
  const path = require("node:path");
9
9
 
10
10
  const Document = require("./Document");
11
+ const ExecutionContext = require('./ExecutionContext');
12
+ const ExecutionContextStack = require('./ExecutionContextStack');
11
13
  const Expression = require("./Expression");
12
14
  const Operation = require('./Operation');
13
15
  const Rules = require("./Rules");
@@ -25,6 +27,8 @@ class Arazzo extends Document {
25
27
  this.stepRunRules = {};
26
28
  this.workflowRunRules = {};
27
29
  this.retrySet = new Set();
30
+ this.context = new Map();
31
+ this.executionStack = new ExecutionContextStack();
28
32
  this.retryLimits = {};
29
33
  this.stepContext = {};
30
34
  }
@@ -92,61 +96,105 @@ class Arazzo extends Document {
92
96
  this.abortWorkflowController = new AbortController();
93
97
  }
94
98
 
95
- const rules = new Rules(this.expression, { logger: this.logger });
99
+
96
100
  const workflow = await this.JSONPickerToIndex("workflows", index);
97
101
 
98
102
  if (workflow) {
99
- this.logger.notice(`Running Workflow: ${workflow.workflowId}`);
103
+ const context = new ExecutionContext(
104
+ 'workflow',
105
+ workflow.workflowId,
106
+ index,
107
+ null,
108
+ workflow.workflowId,
109
+ null
110
+ );
111
+ this.executionStack.push(context);
100
112
 
101
- this.workflow = workflow;
102
- this.workflowId = workflow.workflowId;
113
+ try {
114
+ await this.buildWorkflow(workflow);
115
+ // const prevWorkflowContexts = this.executionStack.getContextsByType('workflow')
116
+ // .slice(0, -1);
117
+ // if (prevWorkflowContexts.some(ctx => ctx.workflowId === workflow.workflowId)) {
118
+ // throw new Error(`Circular workflow dependency detected: ${workflow.workflowId}`);
119
+ // }
103
120
 
104
- if (workflow.dependsOn) {
105
- await this.runDependsOnWorkflows();
106
121
 
107
- this.workflow = workflow;
108
- }
122
+ this.logger.notice(`Running Workflow: ${this.workflow.workflowId}`);
109
123
 
110
- this.inputs = await this.inputFile.getWorkflowInputs(
111
- this.workflow.workflowId,
112
- this.workflow.inputs,
113
- );
124
+ // this.workflow = workflow;
125
+ // this.workflowId = workflow.workflowId;
114
126
 
115
- this.expression.addToContext("inputs", this.inputs);
127
+ if (this.workflow.dependsOn) {
128
+ await this.runDependsOnWorkflows();
116
129
 
117
- if (this.workflow.onSuccess) {
118
- // this.workflow.rules.set(this.workflow.onSuccess);
119
- rules.setSuccessRules(this.workflow.onSuccess);
120
- }
130
+ this.workflow = workflow;
131
+ }
121
132
 
122
- if (this.workflow.onFailure) {
123
- // this.workflow.rules.setWorkflowFailures(this.workflow.onFailure);
124
- rules.setFailureRules(this.workflow.onFailure);
125
- }
133
+ // this.inputs = await this.inputFile.getWorkflowInputs(
134
+ // this.workflow.workflowId,
135
+ // this.workflow.inputs,
136
+ // );
126
137
 
127
- this.workflow.rules = rules;
138
+ // this.expression.addToContext("inputs", this.inputs);
128
139
 
129
- await this.runSteps();
140
+ // if (this.workflow.successActions) {
141
+ // rules.setSuccessRules(this.workflow.successActions);
142
+ // }
130
143
 
131
- if (this.workflow.outputs) {
132
- const outputs = {};
144
+ // if (this.workflow.failureActions) {
145
+ // rules.setFailureRules(this.workflow.failureActions);
146
+ // }
133
147
 
134
- for (const key in this.workflow.outputs) {
135
- const value = this.expression.resolveExpression(
136
- this.workflow.outputs[key],
137
- );
148
+ // this.workflow.rules = rules;
149
+
150
+ await this.runSteps();
151
+
152
+ if (this.workflow.outputs) {
153
+ const outputs = {};
154
+
155
+ for (const key in this.workflow.outputs) {
156
+ const value = this.expression.resolveExpression(
157
+ this.workflow.outputs[key],
158
+ );
138
159
 
139
- Object.assign(outputs, { [key]: value });
160
+ Object.assign(outputs, { [key]: value });
161
+ }
162
+
163
+ this.expression.addToContext("workflows", {
164
+ [this.workflow.workflowId]: { outputs: outputs },
165
+ });
140
166
  }
141
167
 
142
- this.expression.addToContext("workflows", {
143
- [this.workflow.workflowId]: { outputs: outputs },
144
- });
168
+ this.logger.success(`Workflow ${workflow.workflowId} completed`);
169
+ return { noMoreWorkflows: false };
170
+ } finally {
171
+ this.executionStack.pop();
145
172
  }
173
+ }
174
+ }
175
+
176
+ async buildWorkflow(workflow) {
177
+ const rules = new Rules(this.expression, { logger: this.logger });
178
+
179
+ this.workflow = workflow;
180
+ this.workflowId = workflow.workflowId;
181
+
182
+ this.inputs = await this.inputFile.getWorkflowInputs(
183
+ this.workflow.workflowId,
184
+ this.workflow.inputs,
185
+ );
186
+
187
+ this.expression.addToContext("inputs", this.inputs);
188
+
189
+ if (this.workflow.successActions) {
190
+ rules.setSuccessRules(this.workflow.successActions);
191
+ }
146
192
 
147
- this.logger.success(`Workflow ${workflow.workflowId} completed`);
148
- return { noMoreWorkflows: false };
193
+ if (this.workflow.failureActions) {
194
+ rules.setFailureRules(this.workflow.failureActions);
149
195
  }
196
+
197
+ this.workflow.rules = rules;
150
198
  }
151
199
 
152
200
  /**
@@ -205,22 +253,36 @@ class Arazzo extends Document {
205
253
 
206
254
  const step = this.workflow.steps[index];
207
255
 
208
- if (step) {
256
+ const context = new ExecutionContext(
257
+ 'step',
258
+ step.stepId,
259
+ this.workflowIndex,
260
+ index,
261
+ this.workflow.workflowId,
262
+ step.stepId
263
+ );
264
+ this.executionStack.push(context);
265
+
266
+ try {
267
+ // if (this.executionStack.depth > 1) {
268
+ // const prevContexts = this.executionStack.getContextsByType('step')
269
+ // .slice(0, -1); // Exclude current
270
+ // if (prevContexts.some(ctx => ctx.stepId === step.stepId)) {
271
+ // throw new Error(`Circular step dependency detected: ${step.stepId}`);
272
+ // }
273
+ // }
274
+
209
275
  this.step = step;
210
276
  const rules = new Rules(this.expression, { logger: this.logger });
211
- // if (!this.stepContext[step.stepId])
212
- // Object.assign(this.stepContext, { [step.stepId]: {} });
213
277
 
214
278
  this.logger.notice(`Running Step: ${step.stepId}`);
215
279
 
216
280
  if (this.step.onSuccess) {
217
281
  rules.setSuccessRules(this.step.onSuccess);
218
- // this.workflow.rules.setStepSuccesses(this.step.onSuccess);
219
282
  }
220
283
 
221
284
  if (this.step.onFailure) {
222
285
  rules.setFailureRules(this.step.onFailure);
223
- // this.workflow.rules.setStepFailures(this.step.onFailure);
224
286
  }
225
287
 
226
288
  rules.combineRules(this.workflow.rules);
@@ -231,22 +293,7 @@ class Arazzo extends Document {
231
293
  if (this.openAPISteps) {
232
294
  await this.runOpenAPIStep();
233
295
  } else if (this.isAWorkflowId) {
234
- this.currentStepIndex = this.stepIndex;
235
- this.currentWorkflow = this.workflow;
236
- if (this.isExternalWorkflow) {
237
- await this.sourceDescriptionFile.runWorkflowById(this.step.workflowId)
238
- const sourceDesc = this.expression.context.sourceDescriptions[this.sourceDescriptionFile.name];
239
- if (!sourceDesc[this.step.workflowId]) {
240
- if (this.sourceDescriptionFile.expression?.context?.workflows?.[this.step.workflowId]?.outputs) {
241
- Object.assign(sourceDesc, { [this.step.workflowId]: { outputs: this.sourceDescriptionFile.expression.context.workflows[this.step.workflowId].outputs } });
242
- this.expression.context.sourceDescriptions[this.sourceDescriptionFile.name] = sourceDesc;
243
- }
244
- }
245
- } else {
246
- await this.runWorkflowById(this.step.workflowId);
247
- }
248
- this.stepIndex = this.currentStepIndex;
249
- this.workflow = this.currentWorkflow;
296
+ await this.runWorkflowStep()
250
297
  }
251
298
 
252
299
  this.isAnOperationId = false;
@@ -256,10 +303,29 @@ class Arazzo extends Document {
256
303
  this.openAPISteps = false;
257
304
 
258
305
  this.logger.success(`Step ${step.stepId} completed`);
259
- return { noMoreSteps: false };
306
+
307
+ } finally {
308
+ this.executionStack.pop();
309
+ }
310
+ }
311
+
312
+ async runWorkflowStep() {
313
+ this.currentStepIndex = this.stepIndex;
314
+ this.currentWorkflow = this.workflow;
315
+ if (this.isExternalWorkflow) {
316
+ await this.sourceDescriptionFile.runWorkflowById(this.step.workflowId)
317
+ const sourceDesc = this.expression.context.sourceDescriptions[this.sourceDescriptionFile.name];
318
+ if (!sourceDesc[this.step.workflowId]) {
319
+ if (this.sourceDescriptionFile.expression?.context?.workflows?.[this.step.workflowId]?.outputs) {
320
+ Object.assign(sourceDesc, { [this.step.workflowId]: { outputs: this.sourceDescriptionFile.expression.context.workflows[this.step.workflowId].outputs } });
321
+ this.expression.context.sourceDescriptions[this.sourceDescriptionFile.name] = sourceDesc;
322
+ }
323
+ }
260
324
  } else {
261
- return { noMoreSteps: true };
325
+ await this.runWorkflowById(this.step.workflowId);
262
326
  }
327
+ this.stepIndex = this.currentStepIndex;
328
+ this.workflow = this.currentWorkflow;
263
329
  }
264
330
 
265
331
  /**
@@ -278,7 +344,7 @@ class Arazzo extends Document {
278
344
  };
279
345
 
280
346
  const apiOperation = new Operation(this.operation, this.sourceDescriptionFile, this.expression, this.inputs, this.logger, miniStep);
281
- const response = await apiOperation.runOperation()
347
+ const response = await apiOperation.runOperation(this.retryAfter)
282
348
  await this.dealWithResponse(response)
283
349
  }
284
350
 
@@ -295,23 +361,25 @@ class Arazzo extends Document {
295
361
 
296
362
  if (passedSuccessCriteria) {
297
363
  this.logger.success("All criteria checks passed");
364
+
298
365
  if (this.currentRetryRule) {
299
366
  if (this.retryContext.doNotDeleteRetryLimits) {
300
367
  this.retryLimits[this.currentRetryRule] = 0;
301
368
  this.logger.notice("Retries stopped");
369
+ delete this.retryAfter;
302
370
  }
303
371
  }
304
372
 
305
373
  await this.dealWithPassedRule(response);
306
374
  } else {
307
375
  this.logger.error("Not all criteria checks passed");
308
- if (this.step.onFailure) {
309
- await this.dealWithFailedRule();
310
- } else {
311
- throw new Error(
312
- `${this.step.stepId} step of the ${this.workflow.workflowId} workflow failed the successCriteria`,
313
- );
314
- }
376
+ // if (this.step.onFailure) {
377
+ await this.dealWithFailedRule(response);
378
+ // } else {
379
+ // throw new Error(
380
+ // `${this.step.stepId} step of the ${this.workflow.workflowId} workflow failed the successCriteria`,
381
+ // );
382
+ // }
315
383
  }
316
384
  } else {
317
385
  if (this.step?.outputs) {
@@ -448,6 +516,11 @@ class Arazzo extends Document {
448
516
  this.logger.notice(
449
517
  `${this.step.stepId} onFailure Retry rule triggered`,
450
518
  );
519
+
520
+ if (response.headers.has('retry-after')) {
521
+ this.retryAfter = response.headers.get('retry-after');
522
+ }
523
+
451
524
  await this.retryProcessing(whatNext);
452
525
  }
453
526
  }
@@ -543,8 +616,15 @@ class Arazzo extends Document {
543
616
  return `${num}${suffix}`;
544
617
  };
545
618
 
619
+ const currentContext = this.executionStack.current;
620
+ currentContext.isRetrying = true;
621
+ currentContext.retryCount++;
622
+ // console.log(this.currentContext)
623
+ // this.currentContext.currentlyBeingRetried = true;
624
+
546
625
  this.retryContext = {
547
626
  doNotDeleteRetryLimits: true,
627
+ originContext: currentContext,
548
628
  };
549
629
 
550
630
  let shouldRunRule = true;
@@ -565,34 +645,13 @@ class Arazzo extends Document {
565
645
  this.retryContext.doNotDeleteRetryLimits = false;
566
646
 
567
647
  if (whatNext.stepId) {
568
- this.logger.notice(
569
- `Rule ${whatNext.name} requires Step ${whatNext.stepId} running first`,
570
- );
571
-
572
- const stepIndex = this.findStepIndexInWorkflowByStepId(
573
- whatNext.stepId,
574
- );
575
-
576
- await this.runStep(stepIndex);
577
-
578
- this.logger.notice(
579
- `Rule ${whatNext.name} Step ${whatNext.stepId} has run`,
580
- );
648
+ await this.retryStep(whatNext)
581
649
  } else {
582
- this.logger.notice(
583
- `Rule ${whatNext.name} requires Workflow ${whatNext.workflowId} running first`,
584
- );
585
-
586
- const workflowIndex = this.findWorkflowIndexByWorkflowId(
587
- whatNext.workflowId,
588
- );
589
-
590
- await this.runWorkflow(workflowIndex);
591
-
592
- this.logger.notice(
593
- `Rule ${whatNext.name} Workflow ${whatNext.workflowId} has run`,
594
- );
650
+ await this.retryWorkflow(whatNext)
595
651
  }
652
+
653
+ // After retry step/workflow completes, restore our context
654
+ await this.restoreContextFromRetry(currentContext);
596
655
  }
597
656
 
598
657
  if (!this.retryAfter && whatNext.retryAfter)
@@ -606,7 +665,7 @@ class Arazzo extends Document {
606
665
 
607
666
  let count = this.retryLimits[whatNext.name];
608
667
 
609
- await this.runStep(this.stepIndex);
668
+ await this.runStep(currentContext.stepIndex);
610
669
 
611
670
  if (this.retryLimits[whatNext.name] !== 0) {
612
671
  count--;
@@ -616,10 +675,69 @@ class Arazzo extends Document {
616
675
  } while (this.retryLimits[whatNext.name] > 0);
617
676
  }
618
677
 
678
+ currentContext.isRetrying = false;
679
+
619
680
  if (this.retryLimits[whatNext.name] === 0)
620
681
  this.retrySet.delete(whatNext.name);
621
682
  }
622
683
 
684
+ /**
685
+ * @private
686
+ * @param {*} whatNext
687
+ */
688
+ async retryStep(whatNext) {
689
+ const currentContext = this.executionStack.current;
690
+
691
+ this.logger.notice(
692
+ `Rule ${whatNext.name} requires Step ${whatNext.stepId} running first`,
693
+ );
694
+
695
+ const stepIndex = this.findStepIndexInWorkflowByStepId(
696
+ whatNext.stepId,
697
+ );
698
+
699
+ await this.runStep(stepIndex);
700
+
701
+ this.logger.notice(
702
+ `Rule ${whatNext.name} Step ${whatNext.stepId} has run`,
703
+ );
704
+ }
705
+
706
+ /**
707
+ * @private
708
+ * @param {*} whatNext
709
+ */
710
+ async retryWorkflow(whatNext) {
711
+ const currentContext = this.executionStack.current;
712
+
713
+ this.logger.notice(
714
+ `Rule ${whatNext.name} requires Workflow ${whatNext.workflowId} running first`,
715
+ );
716
+
717
+ const workflowIndex = this.findWorkflowIndexByWorkflowId(
718
+ whatNext.workflowId,
719
+ );
720
+
721
+ await this.runWorkflow(workflowIndex);
722
+
723
+ this.logger.notice(
724
+ `Rule ${whatNext.name} Workflow ${whatNext.workflowId} has run`,
725
+ );
726
+ }
727
+
728
+ async restoreContextFromRetry(context) {
729
+ // Restore the indexes and state
730
+ this.workflowIndex = context.workflowIndex;
731
+ this.stepIndex = context.stepIndex;
732
+ const workflow = await this.JSONPickerToIndex("workflows", context.workflowIndex);
733
+ await this.buildWorkflow(workflow)
734
+ this.step = this.workflow.steps[context.stepIndex];
735
+
736
+ this.logger.notice(
737
+ `Restored context: workflow=${context.workflowId}, step=${context.stepId}`
738
+ );
739
+ }
740
+
623
741
  /**
624
742
  * @private
625
743
  * @param {*} response
@@ -0,0 +1,16 @@
1
+ class ExecutionContext {
2
+ constructor(type, id, workflowIndex, stepIndex, workflowId, stepId, workflow) {
3
+ this.type = type; // 'workflow' or 'step'
4
+ this.id = id;
5
+ this.workflowIndex = workflowIndex;
6
+ this.stepIndex = stepIndex;
7
+ this.workflowId = workflowId;
8
+ this.stepId = stepId;
9
+ this.timestamp = Date.now();
10
+ this.retryCount = 0;
11
+ this.isRetrying = false;
12
+ this.workflow = workflow;
13
+ }
14
+ }
15
+
16
+ module.exports = ExecutionContext;
@@ -0,0 +1,75 @@
1
+ class ExecutionContextStack {
2
+ constructor() {
3
+ this.stack = [];
4
+ this.contextMap = new Map(); // Quick lookup by stepId or workflowId
5
+ }
6
+
7
+ push(context) {
8
+ this.stack.push(context);
9
+ this.contextMap.set(context.id, context);
10
+ return context;
11
+ }
12
+
13
+ pop() {
14
+ const context = this.stack.pop();
15
+ if (context) {
16
+ this.contextMap.delete(context.id);
17
+ }
18
+ return context;
19
+ }
20
+
21
+ peek() {
22
+ return this.stack[this.stack.length - 1];
23
+ }
24
+
25
+ get current() {
26
+ return this.peek();
27
+ }
28
+
29
+ get depth() {
30
+ return this.stack.length;
31
+ }
32
+
33
+ // Find the context where we should return after retry completes
34
+ findRetryOrigin() {
35
+ // Walk backwards to find the first context that initiated a retry
36
+ for (let i = this.stack.length - 1; i >= 0; i--) {
37
+ if (this.stack[i].isRetrying) {
38
+ return this.stack[i];
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ // Get all contexts of a specific type
45
+ getContextsByType(type) {
46
+ return this.stack.filter(ctx => ctx.type === type);
47
+ }
48
+
49
+ // Check if we're already executing a specific workflow/step (prevent infinite loops)
50
+ isAlreadyExecuting(type, id) {
51
+ return this.stack.some(ctx => ctx.type === type && ctx.id === id);
52
+ }
53
+
54
+ clear() {
55
+ this.stack = [];
56
+ this.contextMap.clear();
57
+ }
58
+
59
+ // Serialize for debugging or persistence
60
+ toJSON() {
61
+ return this.stack.map(ctx => ({
62
+ type: ctx.type,
63
+ id: ctx.id,
64
+ workflowIndex: ctx.workflowIndex,
65
+ stepIndex: ctx.stepIndex,
66
+ workflowId: ctx.workflowId,
67
+ stepId: ctx.stepId,
68
+ retryCount: ctx.retryCount,
69
+ isRetrying: ctx.isRetrying,
70
+ timestamp: ctx.timestamp
71
+ }));
72
+ }
73
+ }
74
+
75
+ module.exports = ExecutionContextStack;
package/src/Operation.js CHANGED
@@ -17,7 +17,8 @@ class Operation {
17
17
  this.operation = operation;
18
18
  }
19
19
 
20
- async runOperation() {
20
+ async runOperation(retryAfter) {
21
+ this.retryAfter = retryAfter;
21
22
  this.buildOperation()
22
23
 
23
24
  return await this.makeRequest()
@@ -78,7 +79,7 @@ class Operation {
78
79
 
79
80
  if (this.retryAfter) {
80
81
  this.logger.notice(
81
- `retryAfter was set: waiting ${this.retryAfter * 1000} seconds`,
82
+ `retryAfter was set: waiting ${this.retryAfter} seconds`,
82
83
  );
83
84
  await sleep(this.retryAfter * 1000);
84
85
  }
package/src/Rules.js CHANGED
@@ -40,9 +40,9 @@ class Rules {
40
40
  this.successRules = [];
41
41
 
42
42
  this.logger = options?.logger || {
43
- notice: () => {},
44
- error: () => {},
45
- success: () => {},
43
+ notice: () => { },
44
+ error: () => { },
45
+ success: () => { },
46
46
  };
47
47
  }
48
48
 
@@ -83,9 +83,9 @@ class Rules {
83
83
 
84
84
  const obj = {};
85
85
 
86
- if (successRules) {
86
+ if (successRules && this.rules.length) {
87
87
  this.logger.notice(`Running onSuccess Rules`);
88
- } else {
88
+ } else if (!successRules && this.rules.length) {
89
89
  this.logger.notice(`Running onFailure Rules`);
90
90
  }
91
91