@ronaldroe/micro-flow 1.3.8 → 2.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 (51) hide show
  1. package/README.md +53 -12
  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/state.js +1 -1
  11. package/dist/src/classes/state.js.map +3 -3
  12. package/dist/src/classes/steps/case.js +1 -1
  13. package/dist/src/classes/steps/case.js.map +3 -3
  14. package/dist/src/classes/steps/conditional_step.js +1 -1
  15. package/dist/src/classes/steps/conditional_step.js.map +3 -3
  16. package/dist/src/classes/steps/delay_step.js +1 -1
  17. package/dist/src/classes/steps/delay_step.js.map +2 -2
  18. package/dist/src/classes/steps/flow_control_step.js +1 -1
  19. package/dist/src/classes/steps/flow_control_step.js.map +2 -2
  20. package/dist/src/classes/steps/logic_step.js +1 -1
  21. package/dist/src/classes/steps/logic_step.js.map +3 -3
  22. package/dist/src/classes/steps/loop_step.js +1 -1
  23. package/dist/src/classes/steps/loop_step.js.map +3 -3
  24. package/dist/src/classes/steps/step.js +1 -1
  25. package/dist/src/classes/steps/step.js.map +3 -3
  26. package/dist/src/classes/steps/switch_step.js +1 -1
  27. package/dist/src/classes/steps/switch_step.js.map +3 -3
  28. package/dist/src/classes/workflow.js +1 -1
  29. package/dist/src/classes/workflow.js.map +3 -3
  30. package/dist/src/enums/delay_types.js.map +1 -1
  31. package/dist/src/enums/logic_step_types.js.map +3 -3
  32. package/dist/src/enums/sub_step_types.js +1 -1
  33. package/dist/src/enums/sub_step_types.js.map +2 -2
  34. package/package.json +1 -1
  35. package/src/classes/base.js +42 -11
  36. package/src/classes/callable_registry.js +82 -0
  37. package/src/classes/events/event.js +3 -3
  38. package/src/classes/index.js +2 -1
  39. package/src/classes/state.js +278 -8
  40. package/src/classes/steps/case.js +34 -4
  41. package/src/classes/steps/conditional_step.js +69 -3
  42. package/src/classes/steps/delay_step.js +18 -0
  43. package/src/classes/steps/flow_control_step.js +18 -2
  44. package/src/classes/steps/logic_step.js +26 -8
  45. package/src/classes/steps/loop_step.js +88 -3
  46. package/src/classes/steps/step.js +228 -16
  47. package/src/classes/steps/switch_step.js +66 -8
  48. package/src/classes/workflow.js +237 -40
  49. package/src/enums/delay_types.js +1 -1
  50. package/src/enums/logic_step_types.js +2 -2
  51. package/src/enums/sub_step_types.js +10 -10
@@ -1,3 +1,4 @@
1
+ import Step from './step.js';
1
2
  import LogicStep from './logic_step.js';
2
3
  import { conditional_step_comparators } from '../../enums/index.js';
3
4
 
@@ -19,6 +20,8 @@ export default class ConditionalStep extends LogicStep {
19
20
  * @param {*|Function} [options.conditional.value] - Value to compare against. Can be a function that returns the value.
20
21
  * @param {Function|Step|Workflow} [options.true_callable=async () => {}] - Callable to execute if condition is true.
21
22
  * @param {Function|Step|Workflow} [options.false_callable=async () => {}] - Callable to execute if condition is false.
23
+ * @param {string|null} [options.true_callable_registry_key=null] - Optional key to reference true_callable to be rehydrated after serialization.
24
+ * @param {string|null} [options.false_callable_registry_key=null] - Optional key to reference false_callable to be rehydrated after serialization.
22
25
  */
23
26
  constructor({
24
27
  name,
@@ -29,12 +32,23 @@ export default class ConditionalStep extends LogicStep {
29
32
  },
30
33
  true_callable = async () => {},
31
34
  false_callable = async () => {},
35
+ true_callable_registry_key = null,
36
+ false_callable_registry_key = null,
32
37
  }) {
33
38
  super({
34
39
  name,
35
40
  conditional
36
41
  });
37
42
 
43
+ // Optional keys to reference true_callable/false_callable to be rehydrated after serialization.
44
+ this.true_callable_registry_key = true_callable_registry_key;
45
+ this.false_callable_registry_key = false_callable_registry_key;
46
+
47
+ // Keep the raw (unbound) originals for serialization - binding renames a function
48
+ // (e.g. "falseBranch" -> "bound falseBranch"), which would break registry lookups on hydrate.
49
+ this._true_callable_raw = true_callable;
50
+ this._false_callable_raw = false_callable;
51
+
38
52
  // Bind function callables to this step instance for state access
39
53
  if (typeof true_callable === 'function') {
40
54
  this.true_callable = true_callable.bind(this);
@@ -52,7 +66,11 @@ export default class ConditionalStep extends LogicStep {
52
66
  }
53
67
 
54
68
  /**
55
- * Executes the appropriate branch based on the condition evaluation.
69
+ * Executes the appropriate branch based on the condition evaluation. When the executed branch
70
+ * is a `Step`/`Workflow` (not a plain function), it is stamped with this step's own
71
+ * `parent_workflow_id`/`use_state_singleton`/`state` first - true/false_callable are never
72
+ * added to the parent workflow via `addStep()`, so this is the only way they end up sharing
73
+ * its state instead of their own, independent one.
56
74
  * @async
57
75
  * @returns {Promise<*>} The result of the executed branch.
58
76
  */
@@ -71,7 +89,9 @@ export default class ConditionalStep extends LogicStep {
71
89
  if (typeof true_callable === 'function') {
72
90
  result = await true_callable();
73
91
  } else {
74
- true_callable.parentWorkflowId = this.parentWorkflowId;
92
+ true_callable.parent_workflow_id = this.parent_workflow_id;
93
+ true_callable.use_state_singleton = this.use_state_singleton;
94
+ true_callable.state = this.state;
75
95
  result = await true_callable.execute();
76
96
  }
77
97
  } else {
@@ -83,11 +103,57 @@ export default class ConditionalStep extends LogicStep {
83
103
  if (typeof false_callable === 'function') {
84
104
  result = await false_callable();
85
105
  } else {
86
- false_callable.parentWorkflowId = this.parentWorkflowId;
106
+ false_callable.parent_workflow_id = this.parent_workflow_id;
107
+ false_callable.use_state_singleton = this.use_state_singleton;
108
+ false_callable.state = this.state;
87
109
  result = await false_callable.execute();
88
110
  }
89
111
  }
90
112
 
91
113
  return { message: `Conditional step ${this.name} completed`, result };
92
114
  }
115
+
116
+ /**
117
+ * Inserts safely serializable properties of the step into a new object for serialization.
118
+ * @returns {Object} An object containing the step's properties ready for serialization.
119
+ */
120
+ prepareForSerialization() {
121
+ return {
122
+ ...super.prepareForSerialization(),
123
+ // The base `callable` is an internal wiring detail (the bound `conditional` method) -
124
+ // ConditionalStep's constructor doesn't take a callable, so it isn't real data to persist.
125
+ callable: null,
126
+ true_callable: this.true_callable_registry_key
127
+ ? { type: Step.callable_types.FUNCTION, value: this.true_callable_registry_key }
128
+ : Step.serializeCallableField(this._true_callable_raw),
129
+ false_callable: this.false_callable_registry_key
130
+ ? { type: Step.callable_types.FUNCTION, value: this.false_callable_registry_key }
131
+ : Step.serializeCallableField(this._false_callable_raw),
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Hydrates a parsed step object into a ConditionalStep instance, resolving the true/false branch callables.
137
+ * @param {Object} parsed_step - The parsed step object.
138
+ * @param {import('../callable_registry.js').default|null} [callable_registry] - Registry used to resolve function callables.
139
+ * @returns {ConditionalStep} The hydrated ConditionalStep instance.
140
+ */
141
+ static hydrate(parsed_step, callable_registry = null) {
142
+ const true_descriptor = parsed_step.true_callable;
143
+ const false_descriptor = parsed_step.false_callable;
144
+
145
+ return super.hydrate({
146
+ ...parsed_step,
147
+ true_callable: Step.hydrateCallableField(true_descriptor, callable_registry),
148
+ false_callable: Step.hydrateCallableField(false_descriptor, callable_registry),
149
+ true_callable_registry_key: true_descriptor?.type === Step.callable_types.FUNCTION
150
+ ? true_descriptor.value
151
+ : null,
152
+ false_callable_registry_key: false_descriptor?.type === Step.callable_types.FUNCTION
153
+ ? false_descriptor.value
154
+ : null,
155
+ }, callable_registry);
156
+ }
93
157
  }
158
+
159
+ ConditionalStep.registerStepClass(ConditionalStep);
@@ -100,4 +100,22 @@ export default class DelayStep extends Step {
100
100
 
101
101
  return this.delay(delay_until);
102
102
  }
103
+
104
+ /**
105
+ * Inserts safely serializable properties of the step into a new object for serialization.
106
+ * @returns {Object} An object containing the step's properties ready for serialization.
107
+ */
108
+ prepareForSerialization() {
109
+ return {
110
+ ...super.prepareForSerialization(),
111
+ // The base `callable` is an internal wiring detail (the bound absolute/relative method) -
112
+ // DelayStep's constructor doesn't take a callable, so it isn't real data to persist.
113
+ callable: null,
114
+ delay_type: this.delay_type,
115
+ absolute_timestamp: this.absolute_timestamp,
116
+ relative_delay_ms: this.relative_delay_ms,
117
+ };
118
+ }
103
119
  }
120
+
121
+ DelayStep.registerStepClass(DelayStep);
@@ -54,7 +54,7 @@ export default class FlowControlStep extends LogicStep {
54
54
  this.getState('events.step.event_names.CONDITIONAL_TRUE_BRANCH_EXECUTED'),
55
55
  `Break condition met for step: ${this.name}`
56
56
  );
57
- this.setParentWorkflowValue(this.parentWorkflowId, `should_${this.flow_control_type}`, true);
57
+ this.setParentWorkflowValue(this.parent_workflow_id, `should_${this.flow_control_type}`, true);
58
58
 
59
59
  return true;
60
60
  } else {
@@ -62,9 +62,25 @@ export default class FlowControlStep extends LogicStep {
62
62
  this.getState('events.step.event_names.CONDITIONAL_FALSE_BRANCH_EXECUTED'),
63
63
  `Break condition not met for step: ${this.name}`
64
64
  );
65
- this.setParentWorkflowValue(this.parentWorkflowId, `should_${this.flow_control_type}`, false);
65
+ this.setParentWorkflowValue(this.parent_workflow_id, `should_${this.flow_control_type}`, false);
66
66
 
67
67
  return false;
68
68
  }
69
69
  }
70
+
71
+ /**
72
+ * Inserts safely serializable properties of the step into a new object for serialization.
73
+ * @returns {Object} An object containing the step's properties ready for serialization.
74
+ */
75
+ prepareForSerialization() {
76
+ return {
77
+ ...super.prepareForSerialization(),
78
+ // The base `callable` is an internal wiring detail (the bound `shouldFlowControl` method) -
79
+ // FlowControlStep's constructor doesn't take a callable, so it isn't real data to persist.
80
+ callable: null,
81
+ flow_control_type: this.flow_control_type,
82
+ };
83
+ }
70
84
  }
85
+
86
+ 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,16 +47,16 @@ 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 === this.getState('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
62
  case this.getState('conditional_step_comparators.STRICT_EQUALS'):
@@ -108,8 +111,8 @@ export default class LogicStep extends Step {
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);
114
+ const not_match_regex = new RegExp(value);
115
+ return !not_match_regex.test(subject);
113
116
  case this.getState('conditional_step_comparators.STRING_STARTS_WITH'):
114
117
  return typeof subject === 'string' && typeof value === 'string' && subject.startsWith(value);
115
118
  case this.getState('conditional_step_comparators.STRING_ENDS_WITH'):
@@ -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);