arazzo-runner 0.0.17 → 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.17",
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);
@@ -230,22 +292,42 @@ class Arazzo extends Document {
230
292
 
231
293
  if (this.openAPISteps) {
232
294
  await this.runOpenAPIStep();
295
+ } else if (this.isAWorkflowId) {
296
+ await this.runWorkflowStep()
233
297
  }
234
298
 
235
299
  this.isAnOperationId = false;
236
300
  this.isAWorkflowId = false;
301
+ this.isExternalWorkflow = false;
237
302
  this.isAnOperationPath = false;
238
303
  this.openAPISteps = false;
239
304
 
240
305
  this.logger.success(`Step ${step.stepId} completed`);
241
- return { noMoreSteps: false };
242
- } else {
243
- // this.logger.notice(`All steps in ${this.workflow.workflowId} have run`);
244
306
 
245
- return { noMoreSteps: true };
307
+ } finally {
308
+ this.executionStack.pop();
246
309
  }
247
310
  }
248
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
+ }
324
+ } else {
325
+ await this.runWorkflowById(this.step.workflowId);
326
+ }
327
+ this.stepIndex = this.currentStepIndex;
328
+ this.workflow = this.currentWorkflow;
329
+ }
330
+
249
331
  /**
250
332
  * @private
251
333
  */
@@ -262,7 +344,7 @@ class Arazzo extends Document {
262
344
  };
263
345
 
264
346
  const apiOperation = new Operation(this.operation, this.sourceDescriptionFile, this.expression, this.inputs, this.logger, miniStep);
265
- const response = await apiOperation.runOperation()
347
+ const response = await apiOperation.runOperation(this.retryAfter)
266
348
  await this.dealWithResponse(response)
267
349
  }
268
350
 
@@ -279,23 +361,25 @@ class Arazzo extends Document {
279
361
 
280
362
  if (passedSuccessCriteria) {
281
363
  this.logger.success("All criteria checks passed");
364
+
282
365
  if (this.currentRetryRule) {
283
366
  if (this.retryContext.doNotDeleteRetryLimits) {
284
367
  this.retryLimits[this.currentRetryRule] = 0;
285
368
  this.logger.notice("Retries stopped");
369
+ delete this.retryAfter;
286
370
  }
287
371
  }
288
372
 
289
373
  await this.dealWithPassedRule(response);
290
374
  } else {
291
375
  this.logger.error("Not all criteria checks passed");
292
- if (this.step.onFailure) {
293
- await this.dealWithFailedRule();
294
- } else {
295
- throw new Error(
296
- `${this.step.stepId} step of the ${this.workflow.workflowId} workflow failed the successCriteria`,
297
- );
298
- }
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
+ // }
299
383
  }
300
384
  } else {
301
385
  if (this.step?.outputs) {
@@ -432,6 +516,11 @@ class Arazzo extends Document {
432
516
  this.logger.notice(
433
517
  `${this.step.stepId} onFailure Retry rule triggered`,
434
518
  );
519
+
520
+ if (response.headers.has('retry-after')) {
521
+ this.retryAfter = response.headers.get('retry-after');
522
+ }
523
+
435
524
  await this.retryProcessing(whatNext);
436
525
  }
437
526
  }
@@ -527,8 +616,15 @@ class Arazzo extends Document {
527
616
  return `${num}${suffix}`;
528
617
  };
529
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
+
530
625
  this.retryContext = {
531
626
  doNotDeleteRetryLimits: true,
627
+ originContext: currentContext,
532
628
  };
533
629
 
534
630
  let shouldRunRule = true;
@@ -549,34 +645,13 @@ class Arazzo extends Document {
549
645
  this.retryContext.doNotDeleteRetryLimits = false;
550
646
 
551
647
  if (whatNext.stepId) {
552
- this.logger.notice(
553
- `Rule ${whatNext.name} requires Step ${whatNext.stepId} running first`,
554
- );
555
-
556
- const stepIndex = this.findStepIndexInWorkflowByStepId(
557
- whatNext.stepId,
558
- );
559
-
560
- await this.runStep(stepIndex);
561
-
562
- this.logger.notice(
563
- `Rule ${whatNext.name} Step ${whatNext.stepId} has run`,
564
- );
648
+ await this.retryStep(whatNext)
565
649
  } else {
566
- this.logger.notice(
567
- `Rule ${whatNext.name} requires Workflow ${whatNext.workflowId} running first`,
568
- );
569
-
570
- const workflowIndex = this.findWorkflowIndexByWorkflowId(
571
- whatNext.workflowId,
572
- );
573
-
574
- await this.runWorkflow(workflowIndex);
575
-
576
- this.logger.notice(
577
- `Rule ${whatNext.name} Workflow ${whatNext.workflowId} has run`,
578
- );
650
+ await this.retryWorkflow(whatNext)
579
651
  }
652
+
653
+ // After retry step/workflow completes, restore our context
654
+ await this.restoreContextFromRetry(currentContext);
580
655
  }
581
656
 
582
657
  if (!this.retryAfter && whatNext.retryAfter)
@@ -590,7 +665,7 @@ class Arazzo extends Document {
590
665
 
591
666
  let count = this.retryLimits[whatNext.name];
592
667
 
593
- await this.runStep(this.stepIndex);
668
+ await this.runStep(currentContext.stepIndex);
594
669
 
595
670
  if (this.retryLimits[whatNext.name] !== 0) {
596
671
  count--;
@@ -600,10 +675,69 @@ class Arazzo extends Document {
600
675
  } while (this.retryLimits[whatNext.name] > 0);
601
676
  }
602
677
 
678
+ currentContext.isRetrying = false;
679
+
603
680
  if (this.retryLimits[whatNext.name] === 0)
604
681
  this.retrySet.delete(whatNext.name);
605
682
  }
606
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
+
607
741
  /**
608
742
  * @private
609
743
  * @param {*} response
@@ -653,11 +787,13 @@ class Arazzo extends Document {
653
787
  } else {
654
788
  let workflowIdArr = this.step?.workflowId?.split(".") || [];
655
789
 
656
- if (workflowIdArr.length === 1) {
657
- await this.runWorkflowById(workflowIdArr.at(0));
658
- } else {
790
+ if (workflowIdArr.length !== 1) {
791
+ this.step.workflowId = workflowIdArr.at(-1)
792
+ this.isExternalWorkflow = true;
793
+ // await this.runWorkflowById(workflowIdArr.at(0));
794
+ // } else {
659
795
  await this.sourceDescriptionFile.loadWorkflowData(this.inputFile);
660
- await this.sourceDescriptionFile.runWorkflowById(workflowIdArr.at(-1));
796
+ // await this.sourceDescriptionFile.runWorkflowById(workflowIdArr.at(-1));
661
797
  }
662
798
  }
663
799
  }
@@ -673,6 +809,15 @@ class Arazzo extends Document {
673
809
  if (this.sourceDescriptions.length === 1) {
674
810
  return this.sourceDescriptions[0];
675
811
  } else {
812
+ if (this.isAnOperationId) {
813
+ const multipleOpenAPISourceDescriptions = this.sourceDescriptions.filter(obj => obj.type === 'openapi');
814
+ if (multipleOpenAPISourceDescriptions.length > 1) {
815
+
816
+ } else {
817
+ return multipleOpenAPISourceDescriptions[0]
818
+ }
819
+ }
820
+
676
821
  const operationOrWorkflowPointerArr =
677
822
  operationOrWorkflowPointer.split(".");
678
823
 
@@ -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