@ronaldroe/micro-flow 1.0.0 → 1.2.0
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/README.md +63 -153
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/dist/src/classes/base.js +4 -0
- package/dist/src/classes/base.js.map +7 -0
- package/dist/src/classes/events/broadcast.js +2 -0
- package/dist/src/classes/events/broadcast.js.map +7 -0
- package/dist/src/classes/events/event.js +2 -0
- package/dist/src/classes/events/event.js.map +7 -0
- package/dist/src/classes/events/index.js +2 -0
- package/dist/src/classes/events/index.js.map +7 -0
- package/dist/src/classes/events/state_event.js +2 -0
- package/dist/src/classes/events/state_event.js.map +7 -0
- package/dist/src/classes/events/step_event.js +2 -0
- package/dist/src/classes/events/step_event.js.map +7 -0
- package/dist/src/classes/events/workflow_event.js +2 -0
- package/dist/src/classes/events/workflow_event.js.map +7 -0
- package/dist/src/classes/index.js +2 -0
- package/dist/src/classes/index.js.map +7 -0
- package/dist/src/classes/state.js +2 -0
- package/dist/src/classes/state.js.map +7 -0
- package/dist/src/classes/steps/case.js +2 -0
- package/dist/src/classes/steps/case.js.map +7 -0
- package/dist/src/classes/steps/conditional_step.js +2 -0
- package/dist/src/classes/steps/conditional_step.js.map +7 -0
- package/dist/src/classes/steps/delay_step.js +2 -0
- package/dist/src/classes/steps/delay_step.js.map +7 -0
- package/dist/src/classes/steps/flow_control_step.js +2 -0
- package/dist/src/classes/steps/flow_control_step.js.map +7 -0
- package/dist/src/classes/steps/index.js +2 -0
- package/dist/src/classes/steps/index.js.map +7 -0
- package/dist/src/classes/steps/logic_step.js +2 -0
- package/dist/src/classes/steps/logic_step.js.map +7 -0
- package/dist/src/classes/steps/loop_step.js +2 -0
- package/dist/src/classes/steps/loop_step.js.map +7 -0
- package/dist/src/classes/steps/step.js +2 -0
- package/dist/src/classes/steps/step.js.map +7 -0
- package/dist/src/classes/steps/switch_step.js +2 -0
- package/dist/src/classes/steps/switch_step.js.map +7 -0
- package/dist/src/classes/workflow.js +2 -0
- package/dist/src/classes/workflow.js.map +7 -0
- package/dist/src/classes/workflow.test.js +2 -0
- package/dist/src/classes/workflow.test.js.map +7 -0
- package/dist/src/enums/base_types.js +2 -0
- package/dist/src/enums/base_types.js.map +7 -0
- package/dist/src/enums/conditional_step_comparators.js +2 -0
- package/dist/src/enums/conditional_step_comparators.js.map +7 -0
- package/dist/src/enums/delay_types.js +2 -0
- package/dist/src/enums/delay_types.js.map +7 -0
- package/dist/src/enums/errors.js +6 -0
- package/dist/src/enums/errors.js.map +7 -0
- package/dist/src/enums/flow_control_types.js +2 -0
- package/dist/src/enums/flow_control_types.js.map +7 -0
- package/dist/src/enums/index.js +2 -0
- package/dist/src/enums/index.js.map +7 -0
- package/dist/src/enums/logic_step_types.js +2 -0
- package/dist/src/enums/logic_step_types.js.map +7 -0
- package/dist/src/enums/loop_types.js +2 -0
- package/dist/src/enums/loop_types.js.map +7 -0
- package/dist/src/enums/state_event_names.js +2 -0
- package/dist/src/enums/state_event_names.js.map +7 -0
- package/dist/src/enums/step_event_names.js +2 -0
- package/dist/src/enums/step_event_names.js.map +7 -0
- package/dist/src/enums/step_statuses.js +2 -0
- package/dist/src/enums/step_statuses.js.map +7 -0
- package/dist/src/enums/step_types.js +2 -0
- package/dist/src/enums/step_types.js.map +7 -0
- package/dist/src/enums/sub_step_types.js +2 -0
- package/dist/src/enums/sub_step_types.js.map +7 -0
- package/dist/src/enums/workflow_event_names.js +2 -0
- package/dist/src/enums/workflow_event_names.js.map +7 -0
- package/dist/src/enums/workflow_statuses.js +2 -0
- package/dist/src/enums/workflow_statuses.js.map +7 -0
- package/package.json +10 -6
- package/src/classes/base.js +4 -5
- package/src/classes/events/event.js +16 -9
- package/src/classes/events/index.js +1 -1
- package/src/classes/events/state_event.js +28 -0
- package/src/classes/state.js +161 -54
- package/src/classes/steps/case.js +70 -0
- package/src/classes/steps/conditional_step.js +20 -7
- package/src/classes/steps/delay_step.js +103 -0
- package/src/classes/steps/flow_control_step.js +8 -7
- package/src/classes/steps/index.js +4 -0
- package/src/classes/steps/logic_step.js +89 -29
- package/src/classes/steps/loop_step.js +148 -0
- package/src/classes/steps/step.js +59 -18
- package/src/classes/steps/switch_step.js +77 -0
- package/src/classes/workflow.js +74 -26
- package/src/enums/conditional_step_comparators.js +38 -8
- package/src/enums/delay_types.js +0 -7
- package/src/enums/errors.js +2 -0
- package/src/enums/index.js +1 -0
- package/src/enums/loop_types.js +3 -1
- package/src/enums/state_event_names.js +17 -0
- package/src/enums/step_event_names.js +3 -0
- package/src/enums/step_types.js +2 -1
- package/src/enums/sub_step_types.js +15 -125
- package/src/enums/workflow_event_names.js +1 -0
- package/index.js +0 -1
- package/src/classes/events/broadcast.js +0 -57
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import Step from './step.js';
|
|
2
|
+
import { delay_types, step_types } from '../../enums/index.js';
|
|
3
|
+
import schedule from 'node-schedule';
|
|
4
|
+
import { addMilliseconds } from 'date-fns';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DelayStep class for introducing delays in workflow execution.
|
|
8
|
+
* Supports both absolute and relative delays.
|
|
9
|
+
* @class DelayStep
|
|
10
|
+
* @extends Step
|
|
11
|
+
*/
|
|
12
|
+
export default class DelayStep extends Step {
|
|
13
|
+
static step_name = 'delay';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new DelayStep instance.
|
|
17
|
+
* @param {Object} options - Configuration options.
|
|
18
|
+
* @param {string} [options.name] - Name of the step.
|
|
19
|
+
* @param {Date|string} [options.absolute_timestamp=new Date()] - Absolute timestamp to delay until.
|
|
20
|
+
* @param {number} [options.relative_delay_ms=0] - Relative delay in milliseconds.
|
|
21
|
+
* @param {string} [options.delay_type=delay_types.RELATIVE] - Type of delay ('absolute' or 'relative').
|
|
22
|
+
*/
|
|
23
|
+
constructor({
|
|
24
|
+
name,
|
|
25
|
+
absolute_timestamp = new Date(),
|
|
26
|
+
relative_delay_ms = 0,
|
|
27
|
+
delay_type = delay_types.RELATIVE
|
|
28
|
+
}) {
|
|
29
|
+
super({
|
|
30
|
+
name,
|
|
31
|
+
step_type: step_types.DELAY,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.delay_type = delay_type;
|
|
35
|
+
this.absolute_timestamp = new Date(absolute_timestamp);
|
|
36
|
+
this.relative_delay_ms = relative_delay_ms;
|
|
37
|
+
|
|
38
|
+
this.callable = this[delay_type].bind(this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Executes an absolute delay until the specified timestamp. If the timestamp is in the past, it continues immediately.
|
|
43
|
+
* @returns {Promise<Object>} Resolves with delay completion info when delay completes.
|
|
44
|
+
*/
|
|
45
|
+
async absolute() {
|
|
46
|
+
const now = new Date();
|
|
47
|
+
|
|
48
|
+
if (this.absolute_timestamp.getTime() <= now.getTime()) {
|
|
49
|
+
this.log(
|
|
50
|
+
this.getState('events.step.event_names.DELAY_STEP_ABSOLUTE_COMPLETE'),
|
|
51
|
+
`No delay for step: ${this.name}. Continuing.`
|
|
52
|
+
);
|
|
53
|
+
return { delayed: false, delay_type: this.delay_type, timestamp: now.toISOString() };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return this.delay(this.absolute_timestamp);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Schedules a delay until the specified date and time.
|
|
60
|
+
* @param {Date} delay_until - The date and time to delay until.
|
|
61
|
+
* @returns {Promise<Object>} Resolves with delay completion info when delay completes.
|
|
62
|
+
*/
|
|
63
|
+
async delay(delay_until) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
this.log(
|
|
66
|
+
this.getState(
|
|
67
|
+
`events.step.event_names.DELAY_STEP_${this.delay_type.toUpperCase()}_SCHEDULED`
|
|
68
|
+
),
|
|
69
|
+
`Delay scheduled for step: ${this.name} until ${delay_until.toISOString()}`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const job = schedule.scheduleJob(delay_until, () => {
|
|
73
|
+
this.log(
|
|
74
|
+
this.getState(
|
|
75
|
+
`events.step.event_names.DELAY_STEP_${this.delay_type.toUpperCase()}_COMPLETE`
|
|
76
|
+
),
|
|
77
|
+
`Delay complete for step: ${this.name}. Continuing.`
|
|
78
|
+
);
|
|
79
|
+
resolve({ delayed: true, delay_type: this.delay_type, timestamp: new Date().toISOString() });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.scheduled_job = job;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Executes a relative delay for the specified duration. If the delay duration is zero or negative, it continues immediately.
|
|
88
|
+
* @returns {Promise<Object>} Resolves with delay completion info when delay completes.
|
|
89
|
+
*/
|
|
90
|
+
async relative() {
|
|
91
|
+
if (this.relative_delay_ms <= 0) {
|
|
92
|
+
this.log(
|
|
93
|
+
this.getState('events.step.event_names.DELAY_STEP_RELATIVE_COMPLETE'),
|
|
94
|
+
`No delay for step: ${this.name}. Continuing.`
|
|
95
|
+
);
|
|
96
|
+
return { delayed: false, delay_type: this.delay_type, timestamp: new Date().toISOString() };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const delay_until = addMilliseconds(new Date(), this.relative_delay_ms);
|
|
100
|
+
|
|
101
|
+
return this.delay(delay_until);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import LogicStep from './logic_step.js';
|
|
2
2
|
import flow_control_types from '../../enums/flow_control_types.js';
|
|
3
|
+
import { conditional_step_comparators } from '../../enums/index.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* FlowControlStep class for controlling workflow execution flow (break
|
|
6
|
+
* FlowControlStep class for controlling workflow execution flow (break or skip).
|
|
6
7
|
* @class FlowControlStep
|
|
7
8
|
* @extends LogicStep
|
|
8
9
|
*/
|
|
@@ -13,9 +14,9 @@ export default class FlowControlStep extends LogicStep {
|
|
|
13
14
|
* Creates a new FlowControlStep instance.
|
|
14
15
|
* @param {Object} options - Configuration options.
|
|
15
16
|
* @param {Object} [options.conditional] - Conditional configuration.
|
|
16
|
-
* @param {
|
|
17
|
-
* @param {string} [options.conditional.operator] - Comparison operator.
|
|
18
|
-
* @param {
|
|
17
|
+
* @param {*|Function} [options.conditional.subject] - Subject to evaluate. Can be a function that returns the value.
|
|
18
|
+
* @param {conditional_step_comparators|string} [options.conditional.operator] - Comparison operator.
|
|
19
|
+
* @param {*|Function} [options.conditional.value] - Value to compare against. Can be a function that returns the value.
|
|
19
20
|
* @param {string} [options.name] - Name of the step.
|
|
20
21
|
* @param {string} [options.flow_control_type=flow_control_types.BREAK] - Type of flow control.
|
|
21
22
|
* @throws {Error} Throws if flow_control_type is invalid.
|
|
@@ -53,7 +54,7 @@ export default class FlowControlStep extends LogicStep {
|
|
|
53
54
|
this.getState('events.step.event_names.CONDITIONAL_TRUE_BRANCH_EXECUTED'),
|
|
54
55
|
`Break condition met for step: ${this.name}`
|
|
55
56
|
);
|
|
56
|
-
this.
|
|
57
|
+
this.setParentWorkflowValue(this.parentWorkflowId, `should_${this.flow_control_type}`, true);
|
|
57
58
|
|
|
58
59
|
return true;
|
|
59
60
|
} else {
|
|
@@ -61,7 +62,7 @@ export default class FlowControlStep extends LogicStep {
|
|
|
61
62
|
this.getState('events.step.event_names.CONDITIONAL_FALSE_BRANCH_EXECUTED'),
|
|
62
63
|
`Break condition not met for step: ${this.name}`
|
|
63
64
|
);
|
|
64
|
-
this.
|
|
65
|
+
this.setParentWorkflowValue(this.parentWorkflowId, `should_${this.flow_control_type}`, false);
|
|
65
66
|
|
|
66
67
|
return false;
|
|
67
68
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { default as Step } from './step.js';
|
|
2
2
|
export { default as LogicStep } from './logic_step.js';
|
|
3
|
+
export { default as Case } from './case.js';
|
|
3
4
|
export { default as ConditionalStep } from './conditional_step.js';
|
|
5
|
+
export { default as DelayStep } from './delay_step.js';
|
|
4
6
|
export { default as FlowControlStep } from './flow_control_step.js';
|
|
7
|
+
export { default as LoopStep } from './loop_step.js';
|
|
8
|
+
export { default as SwitchStep } from './switch_step.js';
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import Step from './step.js';
|
|
2
|
-
import { step_types } from '../../enums/index.js';
|
|
3
|
-
|
|
4
|
-
const conditional_keys = [
|
|
5
|
-
'subject',
|
|
6
|
-
'operator',
|
|
7
|
-
'value',
|
|
8
|
-
];
|
|
2
|
+
import { conditional_step_comparators, step_types } from '../../enums/index.js';
|
|
9
3
|
|
|
10
4
|
/**
|
|
11
5
|
* LogicStep class for conditional logic operations.
|
|
@@ -20,38 +14,46 @@ export default class LogicStep extends Step {
|
|
|
20
14
|
* @param {Object} options - Configuration options.
|
|
21
15
|
* @param {string} [options.name] - Name of the step.
|
|
22
16
|
* @param {Object} [options.conditional] - Conditional configuration.
|
|
23
|
-
* @param {
|
|
24
|
-
* @param {string} [options.conditional.operator] - Comparison operator.
|
|
25
|
-
* @param {
|
|
17
|
+
* @param {*|Function} [options.conditional.subject] - Subject to evaluate. Can be a function that returns the value.
|
|
18
|
+
* @param {conditional_step_comparators|string} [options.conditional.operator] - Comparison operator.
|
|
19
|
+
* @param {*|Function} [options.conditional.value] - Value to compare against. Can be a function that returns the value.
|
|
26
20
|
* @param {Function} [options.callable=async () => {}] - Function to execute.
|
|
27
21
|
*/
|
|
28
22
|
constructor({
|
|
29
23
|
name,
|
|
24
|
+
callable = async () => {},
|
|
30
25
|
conditional = {
|
|
31
|
-
subject: null,
|
|
32
26
|
operator: null,
|
|
27
|
+
subject: null,
|
|
33
28
|
value: null,
|
|
34
29
|
},
|
|
35
|
-
callable = async () => {},
|
|
36
30
|
}) {
|
|
37
31
|
super({
|
|
38
32
|
name,
|
|
39
|
-
|
|
33
|
+
step_type: step_types.LOGIC,
|
|
40
34
|
callable
|
|
41
35
|
});
|
|
42
36
|
|
|
43
|
-
this.
|
|
37
|
+
this.setConditional(conditional);
|
|
44
38
|
}
|
|
45
39
|
|
|
46
40
|
/**
|
|
47
41
|
* Evaluates the conditional expression.
|
|
42
|
+
* Supports function subjects and values - they are called to get the actual value.
|
|
48
43
|
* @returns {boolean} True if the condition is met.
|
|
49
44
|
* @throws {Error} Throws if operator is unknown.
|
|
50
45
|
*/
|
|
51
46
|
checkCondition() {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
47
|
+
const rawSubject = this.conditional_config.subject;
|
|
48
|
+
const rawValue = this.conditional_config.value;
|
|
49
|
+
const operator = this.conditional_config.operator;
|
|
50
|
+
|
|
51
|
+
// Resolve subject - call it if it's a function
|
|
52
|
+
const subject = typeof rawSubject === 'function' ? rawSubject() : rawSubject;
|
|
53
|
+
|
|
54
|
+
// Don't resolve value for CUSTOM_FUNCTION - the value IS the function to call
|
|
55
|
+
const isCustomFunction = operator === this.getState('conditional_step_comparators.CUSTOM_FUNCTION');
|
|
56
|
+
const value = (!isCustomFunction && typeof rawValue === 'function') ? rawValue() : rawValue;
|
|
55
57
|
|
|
56
58
|
switch (operator) {
|
|
57
59
|
case this.getState('conditional_step_comparators.STRICT_EQUALS'):
|
|
@@ -78,25 +80,83 @@ export default class LogicStep extends Step {
|
|
|
78
80
|
case this.getState('conditional_step_comparators.LESS_THAN_OR_EQUAL'):
|
|
79
81
|
case this.getState('conditional_step_comparators.SIGN_LESS_THAN_OR_EQUAL'):
|
|
80
82
|
return subject <= value;
|
|
83
|
+
case this.getState('conditional_step_comparators.STRING_CONTAINS'):
|
|
84
|
+
case this.getState('conditional_step_comparators.STRING_INCLUDES'):
|
|
85
|
+
case this.getState('conditional_step_comparators.ARRAY_CONTAINS'):
|
|
86
|
+
case this.getState('conditional_step_comparators.ARRAY_INCLUDES'):
|
|
87
|
+
return (Array.isArray(subject) || typeof subject === 'string') && subject.includes(value);
|
|
88
|
+
case this.getState('conditional_step_comparators.IN'):
|
|
89
|
+
return (Array.isArray(value) || typeof value === 'string') && value.includes(subject);
|
|
90
|
+
case this.getState('conditional_step_comparators.STRING_NOT_CONTAINS'):
|
|
91
|
+
case this.getState('conditional_step_comparators.STRING_NOT_INCLUDES'):
|
|
92
|
+
case this.getState('conditional_step_comparators.ARRAY_NOT_CONTAINS'):
|
|
93
|
+
case this.getState('conditional_step_comparators.ARRAY_NOT_INCLUDES'):
|
|
94
|
+
return (Array.isArray(subject) || typeof subject === 'string') && !subject.includes(value);
|
|
95
|
+
case this.getState('conditional_step_comparators.NOT_IN'):
|
|
96
|
+
return (Array.isArray(value) || typeof value === 'string') && !value.includes(subject);
|
|
97
|
+
case this.getState('conditional_step_comparators.EMPTY'):
|
|
98
|
+
return subject === '' || subject === null || subject === undefined || subject.length === 0;
|
|
99
|
+
case this.getState('conditional_step_comparators.NOT_EMPTY'):
|
|
100
|
+
return subject !== '' && subject !== null && subject !== undefined && subject.length > 0;
|
|
101
|
+
case this.getState('conditional_step_comparators.REGEX_MATCH'):
|
|
102
|
+
if (typeof value !== 'string') {
|
|
103
|
+
throw new Error(`Regex input must be a string.`);
|
|
104
|
+
}
|
|
105
|
+
const regex = new RegExp(value);
|
|
106
|
+
return regex.test(subject);
|
|
107
|
+
case this.getState('conditional_step_comparators.REGEX_NOT_MATCH'):
|
|
108
|
+
if (typeof value !== 'string') {
|
|
109
|
+
throw new Error(`Regex input must be a string.`);
|
|
110
|
+
}
|
|
111
|
+
const notMatchRegex = new RegExp(value);
|
|
112
|
+
return !notMatchRegex.test(subject);
|
|
113
|
+
case this.getState('conditional_step_comparators.STRING_STARTS_WITH'):
|
|
114
|
+
return typeof subject === 'string' && typeof value === 'string' && subject.startsWith(value);
|
|
115
|
+
case this.getState('conditional_step_comparators.STRING_ENDS_WITH'):
|
|
116
|
+
return typeof subject === 'string' && typeof value === 'string' && subject.endsWith(value);
|
|
117
|
+
case this.getState('conditional_step_comparators.NULLISH'):
|
|
118
|
+
return subject === null || subject === undefined;
|
|
119
|
+
case this.getState('conditional_step_comparators.NOT_NULLISH'):
|
|
120
|
+
return subject !== null && subject !== undefined;
|
|
121
|
+
case this.getState('conditional_step_comparators.IS_TYPE'):
|
|
122
|
+
return typeof subject === value;
|
|
123
|
+
case this.getState('conditional_step_comparators.IS_NOT_TYPE'):
|
|
124
|
+
return typeof subject !== value;
|
|
125
|
+
case this.getState('conditional_step_comparators.CUSTOM_FUNCTION'):
|
|
126
|
+
if (typeof value !== 'function') {
|
|
127
|
+
throw new Error(`Invalid custom function: ${value}`);
|
|
128
|
+
}
|
|
129
|
+
return value(subject);
|
|
81
130
|
default:
|
|
82
131
|
throw new Error(`Unknown operator: ${operator}`);
|
|
83
132
|
}
|
|
84
133
|
}
|
|
85
134
|
|
|
86
135
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
136
|
+
* Checks if the conditional configuration is valid.
|
|
137
|
+
* A valid conditional has subject, operator, and value all set (not null/undefined).
|
|
138
|
+
* Functions are valid as subject or value - they will be called during checkCondition().
|
|
139
|
+
* @returns {boolean} True if conditional is valid.
|
|
90
140
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
conditionalIsValid() {
|
|
142
|
+
// Check if all conditional properties are set (not null or undefined)
|
|
143
|
+
// Can't use falsy check here because valid values could be falsy (e.g. empty string, 0, false)
|
|
144
|
+
// Functions are valid - they'll be called to get the actual value
|
|
145
|
+
return (
|
|
146
|
+
this.conditional_config.subject !== null &&
|
|
147
|
+
this.conditional_config.subject !== undefined &&
|
|
148
|
+
this.conditional_config.operator !== null &&
|
|
149
|
+
this.conditional_config.operator !== undefined &&
|
|
150
|
+
this.conditional_config.value !== null &&
|
|
151
|
+
this.conditional_config.value !== undefined
|
|
152
|
+
);
|
|
153
|
+
}
|
|
97
154
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Sets the conditional properties.
|
|
157
|
+
* @param {Object} conditional - Conditional configuration object.
|
|
158
|
+
*/
|
|
159
|
+
setConditional(conditional) {
|
|
160
|
+
this.conditional_config = { subject: conditional.subject, operator: conditional.operator, value: conditional.value };
|
|
101
161
|
}
|
|
102
162
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { loop_types, step_types } from '../../enums/index.js';
|
|
2
|
+
import LogicStep from './logic_step.js';
|
|
3
|
+
import { conditional_step_comparators } from '../../enums/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LoopStep class for executing loops within a workflow.
|
|
7
|
+
* @class LoopStep
|
|
8
|
+
* @extends LogicStep
|
|
9
|
+
*/
|
|
10
|
+
export default class LoopStep extends LogicStep {
|
|
11
|
+
static step_name = step_types.LOOP;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new LoopStep instance.
|
|
15
|
+
* @param {Object} options - Configuration options.
|
|
16
|
+
* @param {string} [options.name] - Name of the step.
|
|
17
|
+
* @param {Array|Iterable|Function} options.iterable - Iterable to loop over or function returning an iterable. Required for 'for_each' and 'generator' loops.
|
|
18
|
+
* @param {Function} [options.callable=async () => {}] - Function to execute for each iteration.
|
|
19
|
+
* @param {Object} [options.conditional] - Conditional configuration for while loops. Required for 'while' loops.
|
|
20
|
+
* @param {*|Function} [options.conditional.subject] - Subject to evaluate. Can be a function that returns the value.
|
|
21
|
+
* @param {conditional_step_comparators|string} [options.conditional.operator] - Comparison operator.
|
|
22
|
+
* @param {*|Function} [options.conditional.value] - Value to compare against. Can be a function that returns the value.
|
|
23
|
+
* @param {string} [options.loop_type=loop_types.FOR_EACH] - Type of loop ('for', 'for_each', 'while', or 'generator').
|
|
24
|
+
* @param {number} [options.iterations=0] - Number of iterations to execute. Only used for 'for' loops.
|
|
25
|
+
* @param {number} [options.max_iterations=1000] - Maximum number of iterations to prevent infinite loops.
|
|
26
|
+
*/
|
|
27
|
+
constructor({
|
|
28
|
+
name,
|
|
29
|
+
iterable,
|
|
30
|
+
callable = async () => {},
|
|
31
|
+
conditional = {
|
|
32
|
+
operator: null,
|
|
33
|
+
subject: null,
|
|
34
|
+
value: null,
|
|
35
|
+
},
|
|
36
|
+
loop_type = loop_types.FOR_EACH,
|
|
37
|
+
iterations = 0,
|
|
38
|
+
max_iterations = 1000,
|
|
39
|
+
}) {
|
|
40
|
+
super({ name, conditional });
|
|
41
|
+
this.iterable = iterable;
|
|
42
|
+
this.loop_type = loop_type;
|
|
43
|
+
this.iterations = iterations > max_iterations ? max_iterations : iterations;
|
|
44
|
+
this.max_iterations = max_iterations;
|
|
45
|
+
this.results = [];
|
|
46
|
+
this.current_item = null;
|
|
47
|
+
|
|
48
|
+
// Store the user's callable separately so loop methods can invoke it.
|
|
49
|
+
// this._callable will be set to the loop method by the setter below.
|
|
50
|
+
const userCallableType = this.getCallableType(callable);
|
|
51
|
+
this._loop_callable = userCallableType === 'function'
|
|
52
|
+
? callable.bind(this)
|
|
53
|
+
: callable.execute.bind(callable);
|
|
54
|
+
|
|
55
|
+
this.callable = this[`${loop_type}_loop`].bind(this);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Executes a generator/async generator and appends yielded values to results.
|
|
60
|
+
* @throws {Error} If the callable is not a generator or async generator function.
|
|
61
|
+
* @returns {Object} - An object containing a message and the results of the loop.
|
|
62
|
+
*/
|
|
63
|
+
async generator_loop() {
|
|
64
|
+
if (!this._loop_callable.constructor.name.includes('Generator')) {
|
|
65
|
+
throw new Error('Iterable must be a generator function for generator loops');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let iterations = 0;
|
|
69
|
+
// Use for await...of to handle both sync and async generators
|
|
70
|
+
for await (const item of this._loop_callable()) {
|
|
71
|
+
this.results.push(item);
|
|
72
|
+
|
|
73
|
+
if (++iterations >= this.max_iterations) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
message: `Generator loop ${this.name} completed after ${iterations} iterations`,
|
|
80
|
+
result: this.results
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Executes a for loop calling the callable for a set number of iterations
|
|
86
|
+
* @returns {Object} - An object containing a message and the results of the loop.
|
|
87
|
+
*/
|
|
88
|
+
async for_loop() {
|
|
89
|
+
let i = 0;
|
|
90
|
+
for (; i < this.iterations; i++) {
|
|
91
|
+
this.results.push(await this._loop_callable());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
message: `For loop ${this.name} completed after ${i} iterations`,
|
|
96
|
+
result: this.results
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Executes the callable for each item in the iterable.
|
|
102
|
+
* @throws {Error} If the iterable is not provided.
|
|
103
|
+
* @returns {Object} - An object containing a message and the results of the loop.
|
|
104
|
+
*/
|
|
105
|
+
async for_each_loop() {
|
|
106
|
+
if (!this.iterable) {
|
|
107
|
+
throw new Error('Iterable is required for for_each loops');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof this.iterable === 'function') {
|
|
111
|
+
this.iterable = this.iterable();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let iterations = 0;
|
|
115
|
+
for (const item of this.iterable) {
|
|
116
|
+
iterations++;
|
|
117
|
+
this.current_item = item;
|
|
118
|
+
this.results.push(await this._loop_callable());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
message: `For each loop ${this.name} completed after ${iterations} iterations`,
|
|
123
|
+
result: this.results
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Executes the callable while the condition is true.
|
|
129
|
+
* @throws {Error} If the conditional is not valid.
|
|
130
|
+
* @returns {Object} - An object containing a message and the results of the loop.
|
|
131
|
+
*/
|
|
132
|
+
async while_loop() {
|
|
133
|
+
if (!this.conditionalIsValid()) {
|
|
134
|
+
throw new Error('Valid conditional is required for while loops');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let iterations = 0;
|
|
138
|
+
while (this.checkCondition() && iterations < this.max_iterations) {
|
|
139
|
+
iterations++;
|
|
140
|
+
this.results.push(await this._loop_callable());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
message: `While loop ${this.name} completed after ${iterations} iterations`,
|
|
145
|
+
result: this.results
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -9,19 +9,24 @@ import { base_types, step_types } from '../../enums/index.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export default class Step extends Base {
|
|
11
11
|
static step_name = 'step';
|
|
12
|
+
#callable_object = null;
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Creates a new Step instance.
|
|
15
16
|
* @param {Object} options - Configuration options.
|
|
16
17
|
* @param {string} [options.name] - Name of the step.
|
|
17
|
-
* @param {string} [options.step_type=step_types.ACTION] - Type of the step.
|
|
18
18
|
* @param {Function|Step|Workflow} [options.callable=async () => {}] - Function, Step, or Workflow to execute.
|
|
19
|
-
* @param {
|
|
19
|
+
* @param {number} [options.max_retries=0] - Maximum number of retries on failure.
|
|
20
|
+
* @param {number} [options.max_timeout_ms=30000] - Maximum execution time in milliseconds before timing out.
|
|
21
|
+
* @param {string} [options.step_type=step_types.ACTION] - Type of the step.
|
|
22
|
+
* @param {sub_step_types|null} [options.sub_step_type=null] - Sub-type of the step (use values from the sub_step_types enum).
|
|
20
23
|
*/
|
|
21
24
|
constructor({
|
|
22
25
|
name,
|
|
23
|
-
step_type = step_types.ACTION,
|
|
24
26
|
callable = async () => {},
|
|
27
|
+
max_retries = 0,
|
|
28
|
+
max_timeout_ms = 30000,
|
|
29
|
+
step_type = step_types.ACTION,
|
|
25
30
|
sub_step_type = null,
|
|
26
31
|
}) {
|
|
27
32
|
super({ name, base_type: base_types.STEP });
|
|
@@ -30,14 +35,18 @@ export default class Step extends Base {
|
|
|
30
35
|
|
|
31
36
|
// Store off the original callable object, because if it's a Step or Workflow,
|
|
32
37
|
// this.callable is set to the execute method of that object, but we may need to access its properties later.
|
|
33
|
-
this
|
|
38
|
+
this.#callable_object = callable;
|
|
34
39
|
|
|
40
|
+
this.max_retries = max_retries;
|
|
41
|
+
this.retry_count = 0;
|
|
42
|
+
this.max_timeout_ms = max_timeout_ms;
|
|
35
43
|
this.step_type = step_type;
|
|
36
44
|
this.sub_step_type = sub_step_type;
|
|
37
45
|
|
|
38
46
|
this.errors = [];
|
|
39
47
|
this.result = null;
|
|
40
48
|
this.retry_results = [];
|
|
49
|
+
this.timeout = null;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/**
|
|
@@ -46,28 +55,43 @@ export default class Step extends Base {
|
|
|
46
55
|
* @returns {Promise<Step>} The step instance with execution results.
|
|
47
56
|
*/
|
|
48
57
|
async execute() {
|
|
58
|
+
if (!this.timeout ) {
|
|
59
|
+
this.timeout = new Promise((_, reject) =>
|
|
60
|
+
setTimeout(reject, this.max_timeout_ms, new Error(`Step "${this.name}" timed out after ${this.max_timeout_ms}ms`))
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
this.markAsRunning();
|
|
50
65
|
|
|
51
66
|
try {
|
|
52
|
-
this.result = await this._callable();
|
|
67
|
+
this.result = await Promise.race([this._callable(), this.timeout]);
|
|
53
68
|
} catch (error) {
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
if (this.max_retries && this.retry_count < this.max_retries) {
|
|
70
|
+
this.retry_count++;
|
|
71
|
+
this.retry_results.push({
|
|
72
|
+
retry_count: this.retry_count,
|
|
73
|
+
result: await this.execute(),
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
this.errors.push(error);
|
|
77
|
+
|
|
78
|
+
this.markAsFailed();
|
|
79
|
+
|
|
80
|
+
if (this.getState('exit_on_error')) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
62
83
|
}
|
|
84
|
+
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
|
|
87
|
+
const { FAILED, COMPLETE } = this.getState('statuses')[this.base_type];
|
|
88
|
+
|
|
89
|
+
if (! [FAILED, COMPLETE].includes(this.status)) {
|
|
66
90
|
this.markAsComplete();
|
|
67
91
|
}
|
|
68
92
|
|
|
69
93
|
if (['step', 'workflow'].includes(this.callable_type)) {
|
|
70
|
-
this
|
|
94
|
+
return this.#callable_object;
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
return this;
|
|
@@ -80,9 +104,9 @@ export default class Step extends Base {
|
|
|
80
104
|
* @throws {Error} Throws if callable type is invalid.
|
|
81
105
|
*/
|
|
82
106
|
getCallableType(callable) {
|
|
83
|
-
if (callable
|
|
107
|
+
if (callable && callable.base_type === base_types.WORKFLOW) {
|
|
84
108
|
return 'workflow';
|
|
85
|
-
} else if (callable
|
|
109
|
+
} else if (callable && callable.base_type === base_types.STEP) {
|
|
86
110
|
return 'step';
|
|
87
111
|
} else if (typeof callable === 'function') {
|
|
88
112
|
return 'function';
|
|
@@ -91,6 +115,23 @@ export default class Step extends Base {
|
|
|
91
115
|
throw new Error('Invalid callable type. Must be one of function, Step, or Workflow.');
|
|
92
116
|
}
|
|
93
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Sets a value in the parent workflow's state.
|
|
120
|
+
* @param {string} workflowId - ID of the parent workflow.
|
|
121
|
+
* @param {string} path - Path in the workflow state to set.
|
|
122
|
+
* @param {*} value - Value to set at the specified path.
|
|
123
|
+
* @throws {Error} Throws if parent workflow is not found.
|
|
124
|
+
*/
|
|
125
|
+
setParentWorkflowValue(workflowId, path, value) {
|
|
126
|
+
const parentWorkflow = this.getState('workflows')[workflowId];
|
|
127
|
+
|
|
128
|
+
if (!parentWorkflow) {
|
|
129
|
+
throw new Error(`Parent workflow with ID ${workflowId} not found.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
parentWorkflow[path] = value;
|
|
133
|
+
}
|
|
134
|
+
|
|
94
135
|
/**
|
|
95
136
|
* Sets the callable for the step and determines its type.
|
|
96
137
|
* @param {Function|Step|Workflow} callable - The callable to set.
|
|
@@ -105,7 +146,7 @@ export default class Step extends Base {
|
|
|
105
146
|
|
|
106
147
|
this._callable = callable.execute.bind(callable);
|
|
107
148
|
} else {
|
|
108
|
-
this._callable = callable;
|
|
149
|
+
this._callable = callable.bind(this);
|
|
109
150
|
}
|
|
110
151
|
}
|
|
111
152
|
}
|