@ronaldroe/micro-flow 1.3.9 → 3.0.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.
Files changed (54) hide show
  1. package/README.md +51 -10
  2. package/dist/src/classes/base.js +2 -2
  3. package/dist/src/classes/base.js.map +3 -3
  4. package/dist/src/classes/callable_registry.js +1 -1
  5. package/dist/src/classes/callable_registry.js.map +2 -2
  6. package/dist/src/classes/events/event.js +1 -1
  7. package/dist/src/classes/events/event.js.map +3 -3
  8. package/dist/src/classes/index.js +1 -1
  9. package/dist/src/classes/index.js.map +3 -3
  10. package/dist/src/classes/instance_state.js +2 -0
  11. package/dist/src/classes/instance_state.js.map +7 -0
  12. package/dist/src/classes/state.js +1 -1
  13. package/dist/src/classes/state.js.map +3 -3
  14. package/dist/src/classes/steps/case.js +1 -1
  15. package/dist/src/classes/steps/case.js.map +3 -3
  16. package/dist/src/classes/steps/conditional_step.js +1 -1
  17. package/dist/src/classes/steps/conditional_step.js.map +3 -3
  18. package/dist/src/classes/steps/delay_step.js +1 -1
  19. package/dist/src/classes/steps/delay_step.js.map +3 -3
  20. package/dist/src/classes/steps/flow_control_step.js +1 -1
  21. package/dist/src/classes/steps/flow_control_step.js.map +3 -3
  22. package/dist/src/classes/steps/logic_step.js +1 -1
  23. package/dist/src/classes/steps/logic_step.js.map +3 -3
  24. package/dist/src/classes/steps/loop_step.js +1 -1
  25. package/dist/src/classes/steps/loop_step.js.map +3 -3
  26. package/dist/src/classes/steps/step.js +1 -1
  27. package/dist/src/classes/steps/step.js.map +3 -3
  28. package/dist/src/classes/steps/switch_step.js +1 -1
  29. package/dist/src/classes/steps/switch_step.js.map +3 -3
  30. package/dist/src/classes/workflow.js +1 -1
  31. package/dist/src/classes/workflow.js.map +3 -3
  32. package/dist/src/enums/delay_types.js.map +1 -1
  33. package/dist/src/enums/logic_step_types.js.map +3 -3
  34. package/dist/src/enums/sub_step_types.js +1 -1
  35. package/dist/src/enums/sub_step_types.js.map +2 -2
  36. package/package.json +1 -1
  37. package/src/classes/base.js +42 -10
  38. package/src/classes/callable_registry.js +82 -0
  39. package/src/classes/events/event.js +3 -3
  40. package/src/classes/index.js +2 -0
  41. package/src/classes/instance_state.js +277 -0
  42. package/src/classes/state.js +20 -49
  43. package/src/classes/steps/case.js +34 -4
  44. package/src/classes/steps/conditional_step.js +72 -5
  45. package/src/classes/steps/delay_step.js +23 -8
  46. package/src/classes/steps/flow_control_step.js +21 -4
  47. package/src/classes/steps/logic_step.js +63 -45
  48. package/src/classes/steps/loop_step.js +88 -3
  49. package/src/classes/steps/step.js +238 -20
  50. package/src/classes/steps/switch_step.js +68 -9
  51. package/src/classes/workflow.js +280 -62
  52. package/src/enums/delay_types.js +1 -1
  53. package/src/enums/logic_step_types.js +2 -2
  54. package/src/enums/sub_step_types.js +10 -10
@@ -1,6 +1,7 @@
1
1
  import LogicStep from './logic_step.js';
2
2
  import flow_control_types from '../../enums/flow_control_types.js';
3
3
  import { conditional_step_comparators } from '../../enums/index.js';
4
+ import { event_names } from '../instance_state.js';
4
5
 
5
6
  /**
6
7
  * FlowControlStep class for controlling workflow execution flow (break or skip).
@@ -51,20 +52,36 @@ export default class FlowControlStep extends LogicStep {
51
52
  async shouldFlowControl() {
52
53
  if (this.checkCondition()) {
53
54
  this.log(
54
- this.getState('events.step.event_names.CONDITIONAL_TRUE_BRANCH_EXECUTED'),
55
+ event_names.step.CONDITIONAL_TRUE_BRANCH_EXECUTED,
55
56
  `Break condition met for step: ${this.name}`
56
57
  );
57
- this.setParentWorkflowValue(this.parentWorkflowId, `should_${this.flow_control_type}`, true);
58
+ this.setParentWorkflowValue(this.parent_workflow_id, `should_${this.flow_control_type}`, true);
58
59
 
59
60
  return true;
60
61
  } else {
61
62
  this.log(
62
- this.getState('events.step.event_names.CONDITIONAL_FALSE_BRANCH_EXECUTED'),
63
+ event_names.step.CONDITIONAL_FALSE_BRANCH_EXECUTED,
63
64
  `Break condition not met for step: ${this.name}`
64
65
  );
65
- this.setParentWorkflowValue(this.parentWorkflowId, `should_${this.flow_control_type}`, false);
66
+ this.setParentWorkflowValue(this.parent_workflow_id, `should_${this.flow_control_type}`, false);
66
67
 
67
68
  return false;
68
69
  }
69
70
  }
71
+
72
+ /**
73
+ * Inserts safely serializable properties of the step into a new object for serialization.
74
+ * @returns {Object} An object containing the step's properties ready for serialization.
75
+ */
76
+ prepareForSerialization() {
77
+ return {
78
+ ...super.prepareForSerialization(),
79
+ // The base `callable` is an internal wiring detail (the bound `shouldFlowControl` method) -
80
+ // FlowControlStep's constructor doesn't take a callable, so it isn't real data to persist.
81
+ callable: null,
82
+ flow_control_type: this.flow_control_type,
83
+ };
84
+ }
70
85
  }
86
+
87
+ FlowControlStep.registerStepClass(FlowControlStep);
@@ -18,10 +18,12 @@ export default class LogicStep extends Step {
18
18
  * @param {conditional_step_comparators|string} [options.conditional.operator] - Comparison operator.
19
19
  * @param {*|Function, optional} [options.conditional.value] - Value to compare against. Can be a function that returns the value.
20
20
  * @param {Function} [options.callable=async () => {}] - Function to execute.
21
+ * @param {string|null} [options.callable_registry_key=null] - Optional key to reference the callable to be rehydrated after serialization.
21
22
  */
22
23
  constructor({
23
24
  name,
24
25
  callable = async () => {},
26
+ callable_registry_key = null,
25
27
  conditional = {
26
28
  operator: null,
27
29
  subject: null,
@@ -31,7 +33,8 @@ export default class LogicStep extends Step {
31
33
  super({
32
34
  name,
33
35
  step_type: step_types.LOGIC,
34
- callable
36
+ callable,
37
+ callable_registry_key,
35
38
  });
36
39
 
37
40
  this.setConditional(conditional);
@@ -44,85 +47,85 @@ export default class LogicStep extends Step {
44
47
  * @throws {Error} Throws if operator is unknown.
45
48
  */
46
49
  checkCondition() {
47
- const rawSubject = this.conditional_config.subject;
48
- const rawValue = this.conditional_config.value;
50
+ const raw_subject = this.conditional_config.subject;
51
+ const raw_value = this.conditional_config.value;
49
52
  const operator = this.conditional_config.operator;
50
53
 
51
54
  // Resolve subject - call it if it's a function
52
- const subject = typeof rawSubject === 'function' ? rawSubject() : rawSubject;
55
+ const subject = typeof raw_subject === 'function' ? raw_subject() : raw_subject;
53
56
 
54
57
  // 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;
58
+ const is_custom_function = operator === conditional_step_comparators.CUSTOM_FUNCTION;
59
+ const value = (!is_custom_function && typeof raw_value === 'function') ? raw_value() : raw_value;
57
60
 
58
61
  switch (operator) {
59
- case this.getState('conditional_step_comparators.STRICT_EQUALS'):
60
- case this.getState('conditional_step_comparators.SIGN_STRICT_EQUALS'):
62
+ case conditional_step_comparators.STRICT_EQUALS:
63
+ case conditional_step_comparators.SIGN_STRICT_EQUALS:
61
64
  return subject === value;
62
- case this.getState('conditional_step_comparators.SIGN_EQUALS'):
63
- case this.getState('conditional_step_comparators.EQUALS'):
65
+ case conditional_step_comparators.SIGN_EQUALS:
66
+ case conditional_step_comparators.EQUALS:
64
67
  return subject == value;
65
- case this.getState('conditional_step_comparators.NOT_EQUALS'):
66
- case this.getState('conditional_step_comparators.SIGN_NOT_EQUALS'):
68
+ case conditional_step_comparators.NOT_EQUALS:
69
+ case conditional_step_comparators.SIGN_NOT_EQUALS:
67
70
  return subject != value;
68
- case this.getState('conditional_step_comparators.STRICT_NOT_EQUALS'):
69
- case this.getState('conditional_step_comparators.SIGN_STRICT_NOT_EQUALS'):
71
+ case conditional_step_comparators.STRICT_NOT_EQUALS:
72
+ case conditional_step_comparators.SIGN_STRICT_NOT_EQUALS:
70
73
  return subject !== value;
71
- case this.getState('conditional_step_comparators.GREATER_THAN'):
72
- case this.getState('conditional_step_comparators.SIGN_GREATER_THAN'):
74
+ case conditional_step_comparators.GREATER_THAN:
75
+ case conditional_step_comparators.SIGN_GREATER_THAN:
73
76
  return subject > value;
74
- case this.getState('conditional_step_comparators.LESS_THAN'):
75
- case this.getState('conditional_step_comparators.SIGN_LESS_THAN'):
77
+ case conditional_step_comparators.LESS_THAN:
78
+ case conditional_step_comparators.SIGN_LESS_THAN:
76
79
  return subject < value;
77
- case this.getState('conditional_step_comparators.GREATER_THAN_OR_EQUAL'):
78
- case this.getState('conditional_step_comparators.SIGN_GREATER_THAN_OR_EQUAL'):
80
+ case conditional_step_comparators.GREATER_THAN_OR_EQUAL:
81
+ case conditional_step_comparators.SIGN_GREATER_THAN_OR_EQUAL:
79
82
  return subject >= value;
80
- case this.getState('conditional_step_comparators.LESS_THAN_OR_EQUAL'):
81
- case this.getState('conditional_step_comparators.SIGN_LESS_THAN_OR_EQUAL'):
83
+ case conditional_step_comparators.LESS_THAN_OR_EQUAL:
84
+ case conditional_step_comparators.SIGN_LESS_THAN_OR_EQUAL:
82
85
  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'):
86
+ case conditional_step_comparators.STRING_CONTAINS:
87
+ case conditional_step_comparators.STRING_INCLUDES:
88
+ case conditional_step_comparators.ARRAY_CONTAINS:
89
+ case conditional_step_comparators.ARRAY_INCLUDES:
87
90
  return (Array.isArray(subject) || typeof subject === 'string') && subject.includes(value);
88
- case this.getState('conditional_step_comparators.IN'):
91
+ case conditional_step_comparators.IN:
89
92
  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'):
93
+ case conditional_step_comparators.STRING_NOT_CONTAINS:
94
+ case conditional_step_comparators.STRING_NOT_INCLUDES:
95
+ case conditional_step_comparators.ARRAY_NOT_CONTAINS:
96
+ case conditional_step_comparators.ARRAY_NOT_INCLUDES:
94
97
  return (Array.isArray(subject) || typeof subject === 'string') && !subject.includes(value);
95
- case this.getState('conditional_step_comparators.NOT_IN'):
98
+ case conditional_step_comparators.NOT_IN:
96
99
  return (Array.isArray(value) || typeof value === 'string') && !value.includes(subject);
97
- case this.getState('conditional_step_comparators.EMPTY'):
100
+ case conditional_step_comparators.EMPTY:
98
101
  return subject === '' || subject === null || subject === undefined || subject.length === 0;
99
- case this.getState('conditional_step_comparators.NOT_EMPTY'):
102
+ case conditional_step_comparators.NOT_EMPTY:
100
103
  return subject !== '' && subject !== null && subject !== undefined && subject.length > 0;
101
- case this.getState('conditional_step_comparators.REGEX_MATCH'):
104
+ case conditional_step_comparators.REGEX_MATCH:
102
105
  if (typeof value !== 'string') {
103
106
  throw new Error(`Regex input must be a string.`);
104
107
  }
105
108
  const regex = new RegExp(value);
106
109
  return regex.test(subject);
107
- case this.getState('conditional_step_comparators.REGEX_NOT_MATCH'):
110
+ case conditional_step_comparators.REGEX_NOT_MATCH:
108
111
  if (typeof value !== 'string') {
109
112
  throw new Error(`Regex input must be a string.`);
110
113
  }
111
- const notMatchRegex = new RegExp(value);
112
- return !notMatchRegex.test(subject);
113
- case this.getState('conditional_step_comparators.STRING_STARTS_WITH'):
114
+ const not_match_regex = new RegExp(value);
115
+ return !not_match_regex.test(subject);
116
+ case conditional_step_comparators.STRING_STARTS_WITH:
114
117
  return typeof subject === 'string' && typeof value === 'string' && subject.startsWith(value);
115
- case this.getState('conditional_step_comparators.STRING_ENDS_WITH'):
118
+ case conditional_step_comparators.STRING_ENDS_WITH:
116
119
  return typeof subject === 'string' && typeof value === 'string' && subject.endsWith(value);
117
- case this.getState('conditional_step_comparators.NULLISH'):
120
+ case conditional_step_comparators.NULLISH:
118
121
  return subject === null || subject === undefined;
119
- case this.getState('conditional_step_comparators.NOT_NULLISH'):
122
+ case conditional_step_comparators.NOT_NULLISH:
120
123
  return subject !== null && subject !== undefined;
121
- case this.getState('conditional_step_comparators.IS_TYPE'):
124
+ case conditional_step_comparators.IS_TYPE:
122
125
  return typeof subject === value;
123
- case this.getState('conditional_step_comparators.IS_NOT_TYPE'):
126
+ case conditional_step_comparators.IS_NOT_TYPE:
124
127
  return typeof subject !== value;
125
- case this.getState('conditional_step_comparators.CUSTOM_FUNCTION'):
128
+ case conditional_step_comparators.CUSTOM_FUNCTION:
126
129
  if (typeof value !== 'function') {
127
130
  throw new Error(`Invalid custom function: ${value}`);
128
131
  }
@@ -157,4 +160,19 @@ export default class LogicStep extends Step {
157
160
  setConditional(conditional) {
158
161
  this.conditional_config = { subject: conditional.subject, operator: conditional.operator, value: conditional.value };
159
162
  }
163
+
164
+ /**
165
+ * Inserts safely serializable properties of the step into a new object for serialization.
166
+ * Note: function-valued subject/value are not persisted - there's no registry for them,
167
+ * only the plain-callable field supports registry-based rehydration.
168
+ * @returns {Object} An object containing the step's properties ready for serialization.
169
+ */
170
+ prepareForSerialization() {
171
+ return {
172
+ ...super.prepareForSerialization(),
173
+ conditional: { ...this.conditional_config },
174
+ };
175
+ }
160
176
  }
177
+
178
+ LogicStep.registerStepClass(LogicStep);
@@ -1,4 +1,5 @@
1
1
  import { loop_types, step_types } from '../../enums/index.js';
2
+ import Step from './step.js';
2
3
  import LogicStep from './logic_step.js';
3
4
  import { conditional_step_comparators } from '../../enums/index.js';
4
5
 
@@ -23,6 +24,7 @@ export default class LoopStep extends LogicStep {
23
24
  * @param {string} [options.loop_type=loop_types.FOR_EACH] - Type of loop ('for', 'for_each', 'while', or 'generator').
24
25
  * @param {number} [options.iterations=0] - Number of iterations to execute. Only used for 'for' loops.
25
26
  * @param {number} [options.max_iterations=1000] - Maximum number of iterations to prevent infinite loops.
27
+ * @param {string|null} [options.loop_callable_registry_key=null] - Optional key to reference the per-iteration callable to be rehydrated after serialization.
26
28
  */
27
29
  constructor({
28
30
  name,
@@ -36,6 +38,7 @@ export default class LoopStep extends LogicStep {
36
38
  loop_type = loop_types.FOR_EACH,
37
39
  iterations = 0,
38
40
  max_iterations = 1000,
41
+ loop_callable_registry_key = null,
39
42
  }) {
40
43
  super({ name, conditional });
41
44
  this.iterable = iterable;
@@ -45,16 +48,38 @@ export default class LoopStep extends LogicStep {
45
48
  this.results = [];
46
49
  this.current_item = null;
47
50
 
51
+ // Optional key to reference the per-iteration callable to be rehydrated after serialization.
52
+ this.loop_callable_registry_key = loop_callable_registry_key;
53
+
48
54
  // Store the user's callable separately so loop methods can invoke it.
49
55
  // 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'
56
+ // The raw object is kept too (distinct from the bound version) so serialization
57
+ // can recover the original function/Step/Workflow instead of the loop-runner method.
58
+ this._loop_callable_type = this.getCallableType(callable);
59
+ this._loop_callable_object = callable;
60
+ this._loop_callable = this._loop_callable_type === 'function'
52
61
  ? callable.bind(this)
53
62
  : callable.execute.bind(callable);
54
63
 
55
64
  this.callable = this[`${loop_type}_loop`].bind(this);
56
65
  }
57
66
 
67
+ /**
68
+ * When the per-iteration callable is a `Step`/`Workflow` (not a plain function), stamps it with
69
+ * this loop step's own `parent_workflow_id`/`use_state_singleton`/`state` right before the loop
70
+ * runs - mirrors what `Workflow.addStep()` does for top-level steps, since a loop callable is
71
+ * never added to the workflow directly.
72
+ */
73
+ propagateStateToLoopCallable() {
74
+ if (this._loop_callable_type === 'function') {
75
+ return;
76
+ }
77
+
78
+ this._loop_callable_object.parent_workflow_id = this.parent_workflow_id;
79
+ this._loop_callable_object.use_state_singleton = this.use_state_singleton;
80
+ this._loop_callable_object.state = this.state;
81
+ }
82
+
58
83
  /**
59
84
  * Executes a generator/async generator and appends yielded values to results.
60
85
  * @throws {Error} If the callable is not a generator or async generator function.
@@ -65,6 +90,8 @@ export default class LoopStep extends LogicStep {
65
90
  throw new Error('Iterable must be a generator function for generator loops');
66
91
  }
67
92
 
93
+ this.propagateStateToLoopCallable();
94
+
68
95
  let iterations = 0;
69
96
  // Use for await...of to handle both sync and async generators
70
97
  for await (const item of this._loop_callable()) {
@@ -75,6 +102,8 @@ export default class LoopStep extends LogicStep {
75
102
  }
76
103
  }
77
104
 
105
+ this.iterations = iterations;
106
+
78
107
  return {
79
108
  message: `Generator loop ${this.name} completed after ${iterations} iterations`,
80
109
  result: this.results
@@ -86,11 +115,16 @@ export default class LoopStep extends LogicStep {
86
115
  * @returns {Object} - An object containing a message and the results of the loop.
87
116
  */
88
117
  async for_loop() {
118
+ this.propagateStateToLoopCallable();
119
+
120
+ const target = this.iterations;
89
121
  let i = 0;
90
- for (; i < this.iterations; i++) {
122
+ for (; i < target; i++) {
91
123
  this.results.push(await this._loop_callable());
92
124
  }
93
125
 
126
+ this.iterations = i;
127
+
94
128
  return {
95
129
  message: `For loop ${this.name} completed after ${i} iterations`,
96
130
  result: this.results
@@ -107,6 +141,8 @@ export default class LoopStep extends LogicStep {
107
141
  throw new Error('Iterable is required for for_each loops');
108
142
  }
109
143
 
144
+ this.propagateStateToLoopCallable();
145
+
110
146
  if (typeof this.iterable === 'function') {
111
147
  this.iterable = this.iterable();
112
148
  }
@@ -118,6 +154,8 @@ export default class LoopStep extends LogicStep {
118
154
  this.results.push(await this._loop_callable());
119
155
  }
120
156
 
157
+ this.iterations = iterations;
158
+
121
159
  return {
122
160
  message: `For each loop ${this.name} completed after ${iterations} iterations`,
123
161
  result: this.results
@@ -134,15 +172,62 @@ export default class LoopStep extends LogicStep {
134
172
  throw new Error('Valid conditional is required for while loops');
135
173
  }
136
174
 
175
+ this.propagateStateToLoopCallable();
176
+
137
177
  let iterations = 0;
138
178
  while (this.checkCondition() && iterations < this.max_iterations) {
139
179
  iterations++;
140
180
  this.results.push(await this._loop_callable());
141
181
  }
142
182
 
183
+ this.iterations = iterations;
184
+
143
185
  return {
144
186
  message: `While loop ${this.name} completed after ${iterations} iterations`,
145
187
  result: this.results
146
188
  };
147
189
  }
190
+
191
+ /**
192
+ * Inserts safely serializable properties of the step into a new object for serialization.
193
+ * Note: a function-valued `iterable` is not persisted, since there's no registry for it.
194
+ * @returns {Object} An object containing the step's properties ready for serialization.
195
+ */
196
+ prepareForSerialization() {
197
+ return {
198
+ ...super.prepareForSerialization(),
199
+ callable: this.loop_callable_registry_key
200
+ ? { type: Step.callable_types.FUNCTION, value: this.loop_callable_registry_key }
201
+ : Step.serializeCallableField(this._loop_callable_object),
202
+ loop_type: this.loop_type,
203
+ iterations: this.iterations,
204
+ max_iterations: this.max_iterations,
205
+ iterable: Array.isArray(this.iterable) ? this.iterable : null,
206
+ results: this.results,
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Hydrates a parsed step object into a LoopStep instance, resolving the per-iteration callable.
212
+ * @param {Object} parsed_step - The parsed step object.
213
+ * @param {import('../callable_registry.js').default|null} [callable_registry] - Registry used to resolve function callables.
214
+ * @returns {LoopStep} The hydrated LoopStep instance.
215
+ */
216
+ static hydrate(parsed_step, callable_registry = null) {
217
+ const callable_descriptor = parsed_step.callable;
218
+
219
+ const instance = super.hydrate({
220
+ ...parsed_step,
221
+ callable: Step.hydrateCallableField(callable_descriptor, callable_registry),
222
+ loop_callable_registry_key: callable_descriptor?.type === Step.callable_types.FUNCTION
223
+ ? callable_descriptor.value
224
+ : null,
225
+ }, callable_registry);
226
+
227
+ instance.results = parsed_step.results ?? [];
228
+
229
+ return instance;
230
+ }
148
231
  }
232
+
233
+ LoopStep.registerStepClass(LoopStep);