@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
@@ -0,0 +1,277 @@
1
+ import { errors, warnings } from '../enums/errors.js';
2
+ import { StepEvent, WorkflowEvent, StateEvent } from './events/index.js';
3
+ import {
4
+ base_types,
5
+ conditional_step_comparators,
6
+ state_event_names,
7
+ step_event_names,
8
+ step_statuses,
9
+ step_types,
10
+ sub_step_types,
11
+ workflow_event_names,
12
+ workflow_statuses,
13
+ } from '../enums/index.js';
14
+
15
+ /**
16
+ * Parses a property path string into an array of keys, supporting both dot notation
17
+ * and bracket notation.
18
+ *
19
+ * @param {string} path - The path to parse (e.g., "user.profile.name", "users[0].name", "data['key-name']").
20
+ * @returns {string[]} Array of property keys.
21
+ */
22
+ function parsePath(path) {
23
+ const matches = path.match(/[^.[\]]+|(?<=\[)([^\]]+)(?=\])/g);
24
+
25
+ if (!matches) {
26
+ return [];
27
+ }
28
+
29
+ return matches.map(part => part.replace(/^['"]|['"]$/g, ''));
30
+ }
31
+
32
+ /**
33
+ * Resolves a nested property path within an arbitrary object.
34
+ * @param {Object} target - The object to read from.
35
+ * @param {string} path - The path to the property.
36
+ * @returns {*} The value at the specified path, or undefined if not found.
37
+ */
38
+ function getAtPath(target, path) {
39
+ const parts = parsePath(path);
40
+ let current = target;
41
+
42
+ for (const part of parts) {
43
+ if (current && Object.prototype.hasOwnProperty.call(current, part)) {
44
+ current = current[part];
45
+ } else {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ return current;
51
+ }
52
+
53
+ /**
54
+ * Sets a nested property value within an arbitrary object based on a path.
55
+ * Creates intermediate objects/arrays as needed.
56
+ * @param {Object} target - The object to write to.
57
+ * @param {string} path - The path to the property.
58
+ * @param {*} value - The value to set at the specified path.
59
+ */
60
+ function setAtPath(target, path, value) {
61
+ const parts = parsePath(path);
62
+ let current = target;
63
+
64
+ for (let i = 0; i < parts.length - 1; i++) {
65
+ const part = parts[i];
66
+ const next_part = parts[i + 1];
67
+
68
+ if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
69
+ const is_next_part_numeric = /^\d+$/.test(next_part);
70
+ current[part] = is_next_part_numeric ? [] : {};
71
+ }
72
+ current = current[part];
73
+ }
74
+
75
+ current[parts[parts.length - 1]] = value;
76
+ }
77
+
78
+ /**
79
+ * Deletes a property from an arbitrary object using a path.
80
+ * @param {Object} target - The object to delete from.
81
+ * @param {string} path - The path of the property to delete.
82
+ */
83
+ function deleteAtPath(target, path) {
84
+ const parts = parsePath(path);
85
+ let current = target;
86
+
87
+ for (let i = 0; i < parts.length - 1; i++) {
88
+ const part = parts[i];
89
+
90
+ if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
91
+ return;
92
+ }
93
+
94
+ current = current[part];
95
+ }
96
+
97
+ delete current[parts[parts.length - 1]];
98
+ }
99
+
100
+ /**
101
+ * Converts a resolved state value to the requested output type.
102
+ * @param {*} value - The value to convert.
103
+ * @param {string|null} type - One of "string", "number", "boolean".
104
+ * @returns {*} The converted value, or the original value if conversion fails or type is unrecognized.
105
+ */
106
+ function convertType(value, type) {
107
+ if (!type) {
108
+ return value;
109
+ }
110
+
111
+ try {
112
+ switch (type) {
113
+ case 'string':
114
+ return String(value);
115
+ case 'number':
116
+ return Number(value);
117
+ case 'boolean':
118
+ return Boolean(value);
119
+ default:
120
+ return value;
121
+ }
122
+ } catch (error) {
123
+ console.error('Error converting state value: ', error);
124
+ return value;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Framework constants, built once here as the single canonical source. `Workflow` (see
130
+ * `workflow.js`) assigns these as its own static members - the public surface library
131
+ * consumers should reach them through - while step classes that need one directly may import
132
+ * the named export straight from this module instead of going through `Workflow` (avoiding an
133
+ * import cycle). The deprecated `State` singleton (`state.js`) also builds its `default_state`
134
+ * from these same values, so the `events.*` instances stay identical (by reference) regardless
135
+ * of whether a given `Workflow`/`Step` has opted into `use_state_singleton` - listeners
136
+ * registered via `State.get('events.workflow')` keep receiving events either way.
137
+ */
138
+ export const messages = { errors, warnings };
139
+ export const statuses = { workflow: workflow_statuses, step: step_statuses };
140
+ export const event_names = { workflow: workflow_event_names, step: step_event_names, state: state_event_names };
141
+ export const events = { workflow: new WorkflowEvent(), step: new StepEvent(), state: new StateEvent() };
142
+ export const types = { base_types, step_types, sub_step_types };
143
+ export { conditional_step_comparators };
144
+
145
+ /**
146
+ * Per-instance replacement for the deprecated `State` singleton. Each `Workflow` owns one of
147
+ * these (created in its constructor), and shares it with every `Step` added to it, so that
148
+ * `getState()`/`setState()`/`deleteState()` calls made anywhere in that workflow's tree read and
149
+ * write the same, workflow-scoped data instead of a single process-wide object. A `Workflow`
150
+ * registers itself under the `workflow` key of its own `InstanceState` (see
151
+ * `initializeWorkflowState()` in `workflow.js`), so `getState('workflow')` resolves to the live
152
+ * owning `Workflow` instance; any other path is arbitrary user data set via `setState()`.
153
+ *
154
+ * Supports the same dot-notation/bracket-notation path access as `State`.
155
+ *
156
+ * @class InstanceState
157
+ */
158
+ export class InstanceState {
159
+ /**
160
+ * Creates a new InstanceState.
161
+ * @param {Object} [initial={}] - Initial data.
162
+ */
163
+ constructor(initial = {}) {
164
+ this.data = initial;
165
+ }
166
+
167
+ /**
168
+ * Gets the value of a state property using dot-notation or bracket-notation path access.
169
+ * @param {string} path - The path of the state property to get. Falsy values, or "*", return the entire state.
170
+ * @param {*} [defaultValue=null] - Default value to return if the path doesn't exist.
171
+ * @param {string|null} [type=null] - The output type to convert the value to ("string", "number", "boolean").
172
+ * @returns {*} The value of the state property, or defaultValue if not found.
173
+ */
174
+ get(path, defaultValue = null, type = null) {
175
+ if (!path || ['*', ''].includes(path)) {
176
+ return this.data ?? defaultValue;
177
+ }
178
+
179
+ const gotten = getAtPath(this.data, path) ?? defaultValue;
180
+
181
+ return convertType(gotten, type) ?? defaultValue;
182
+ }
183
+
184
+ /**
185
+ * Sets the value of a state property using dot-notation or bracket-notation path access.
186
+ * Creates intermediate objects if they don't exist.
187
+ * @param {string} path - The path of the state property to set.
188
+ * @param {*} value - The value to set for the state property.
189
+ * @throws {Error} Throws if path is empty or invalid.
190
+ */
191
+ set(path, value) {
192
+ if (!path) {
193
+ throw new Error(errors.INVALID_STATE_PATH);
194
+ }
195
+
196
+ setAtPath(this.data, path, value);
197
+ }
198
+
199
+ /**
200
+ * Resolves a nested property path within this instance's data. Low-level counterpart to
201
+ * `get()` - unlike `get()`, a falsy/`'*'` path is not special-cased to mean "entire state".
202
+ * @param {string} path - The path to the property.
203
+ * @returns {*} The value at the specified path, or undefined if not found.
204
+ */
205
+ getStateFromPropertyPath(path) {
206
+ return getAtPath(this.data, path);
207
+ }
208
+
209
+ /**
210
+ * Parses a property path string into an array of keys, supporting both dot notation
211
+ * and bracket notation.
212
+ * @param {string} path - The path to parse.
213
+ * @returns {string[]} Array of property keys.
214
+ */
215
+ parseStatePath(path) {
216
+ return parsePath(path);
217
+ }
218
+
219
+ /**
220
+ * Sets a nested property value within this instance's data based on a path. Low-level
221
+ * counterpart to `set()` - unlike `set()`, does not throw on an empty path.
222
+ * @param {string} path - The path to the property.
223
+ * @param {*} value - The value to set at the specified path.
224
+ */
225
+ setStateToPropertyPath(path, value) {
226
+ setAtPath(this.data, path, value);
227
+ }
228
+
229
+ /**
230
+ * Deletes a state property using dot-notation or bracket-notation path access.
231
+ * @param {string} path - The path of the state property to delete.
232
+ * @throws {Error} Throws if path is empty or invalid.
233
+ */
234
+ delete(path) {
235
+ if (!path) {
236
+ throw new Error(errors.INVALID_STATE_PATH);
237
+ }
238
+
239
+ deleteAtPath(this.data, path);
240
+ }
241
+
242
+ /**
243
+ * Merges an object into the current instance state.
244
+ * @param {Object} newState - The object to merge in.
245
+ * @returns {Object} The updated state data.
246
+ */
247
+ merge(newState) {
248
+ this.data = { ...this.data, ...newState };
249
+ return this.data;
250
+ }
251
+
252
+ /**
253
+ * Iterates over a collection (array or object) located at the specified path,
254
+ * executing a callback function for each item.
255
+ * @param {string} path - The path of the property to iterate over.
256
+ * @param {Function} callback - The function to execute for each item in the collection.
257
+ * @throws {Error} Throws if the value at the path is not an array or object.
258
+ */
259
+ async each(path, callback) {
260
+ const collection = this.get(path);
261
+
262
+ if (Array.isArray(collection)) {
263
+ for (const [index, item] of collection.entries()) {
264
+ await callback(item, index);
265
+ }
266
+ } else if (
267
+ typeof collection === 'object' &&
268
+ Object.prototype.toString.call(collection) === '[object Object]'
269
+ ) {
270
+ for (const key of Object.keys(collection)) {
271
+ await callback(collection[key], key);
272
+ }
273
+ } else {
274
+ throw new Error(errors.VALUE_NOT_ITERABLE);
275
+ }
276
+ }
277
+ }
@@ -1,50 +1,21 @@
1
- import { errors, warnings } from '../enums/errors.js';
2
- import { StepEvent, WorkflowEvent, StateEvent } from './events/index.js';
3
- import {
4
- base_types,
5
- conditional_step_comparators,
6
- state_event_names,
7
- step_event_names,
8
- step_statuses,
9
- step_types,
10
- sub_step_types,
11
- workflow_event_names,
12
- workflow_statuses,
13
- } from '../enums/index.js';
1
+ import { errors } from '../enums/errors.js';
2
+ import { messages, statuses, event_names, events, types, conditional_step_comparators } from './instance_state.js';
14
3
 
15
- const defaultState = {
16
- messages: {
17
- errors,
18
- warnings,
19
- },
20
- statuses: {
21
- workflow: workflow_statuses,
22
- step: step_statuses
23
- },
24
- event_names: {
25
- workflow: workflow_event_names,
26
- step: step_event_names,
27
- state: state_event_names,
28
- },
29
- events: {
30
- workflow: new WorkflowEvent(),
31
- step: new StepEvent(),
32
- state: new StateEvent(),
33
- },
34
- types: {
35
- base_types,
36
- step_types,
37
- sub_step_types,
38
- },
4
+ // Built from the same constants `Workflow` exposes as static members (see instance_state.js),
5
+ // so `events.*` stays the same instance regardless of whether a given Workflow/Step has opted
6
+ // into `use_state_singleton`. Only `workflows` is singleton-only - it was never meant to be
7
+ // copied into per-instance state.
8
+ export const default_state = {
9
+ messages,
10
+ statuses,
11
+ event_names,
12
+ events,
13
+ types,
39
14
  workflows: {},
40
- conditional_step_comparators
15
+ conditional_step_comparators,
41
16
  };
42
17
 
43
- let state = { ...defaultState };
44
-
45
- // Module-level shortcuts for events and event_names
46
- const events = state.events;
47
- const event_names = state.event_names;
18
+ let state = { ...default_state };
48
19
 
49
20
  /**
50
21
  * Singleton class representing the global state for workflows, steps, and processes.
@@ -118,9 +89,9 @@ class State {
118
89
  * @returns {void}
119
90
  */
120
91
  static freeze() {
121
- const frozenState = Object.freeze(state);
92
+ const frozen_state = Object.freeze(state);
122
93
  events.state.emit(event_names.state.FROZEN, { state });
123
- return frozenState;
94
+ return frozen_state;
124
95
  }
125
96
 
126
97
  /**
@@ -242,7 +213,7 @@ class State {
242
213
  */
243
214
  static reset() {
244
215
  state = {
245
- ...defaultState,
216
+ ...default_state,
246
217
  workflows: {}, // Always create fresh to avoid shared reference mutation
247
218
  };
248
219
  events.state.emit(event_names.state.RESET, { state });
@@ -283,12 +254,12 @@ class State {
283
254
 
284
255
  for (let i = 0; i < parts.length - 1; i++) {
285
256
  const part = parts[i];
286
- const nextPart = parts[i + 1];
257
+ const next_part = parts[i + 1];
287
258
 
288
259
  if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
289
260
  // Determine if next part is an array index (numeric)
290
- const isNextPartNumeric = /^\d+$/.test(nextPart);
291
- current[part] = isNextPartNumeric ? [] : {};
261
+ const is_next_part_numeric = /^\d+$/.test(next_part);
262
+ current[part] = is_next_part_numeric ? [] : {};
292
263
  }
293
264
  current = current[part];
294
265
  }
@@ -20,6 +20,7 @@ export default class Case extends LogicStep {
20
20
  * @param {conditional_step_comparators|string} [options.conditional.operator=null] - Comparison operator.
21
21
  * @param {*|Function} [options.conditional.value=null] - Value to compare against. Can be a function that returns the value.
22
22
  * @param {Function|Step|Workflow} [options.callable=async () => {}] - Function, Step, or Workflow to execute when case matches.
23
+ * @param {string|null} [options.callable_registry_key=null] - Optional key to reference the callable to be rehydrated after serialization.
23
24
  * @param {boolean} [options.force_subject_override=false] - Force override of subject even if already set.
24
25
  */
25
26
  constructor({
@@ -30,12 +31,14 @@ export default class Case extends LogicStep {
30
31
  value: null,
31
32
  },
32
33
  callable = async () => {},
34
+ callable_registry_key = null,
33
35
  force_subject_override = false,
34
36
  }) {
35
37
  super({
36
38
  name,
37
39
  step_type: Case.step_name,
38
40
  callable,
41
+ callable_registry_key,
39
42
  });
40
43
 
41
44
  this.conditional_config = conditional;
@@ -52,14 +55,14 @@ export default class Case extends LogicStep {
52
55
  * @throws {Error} If the resulting conditional configuration is invalid.
53
56
  */
54
57
  set switch_subject(subject) {
55
- const subjectProvided = subject !== null && subject !== undefined;
56
- const hasExistingSubject = this.conditional_config.subject !== null && this.conditional_config.subject !== undefined;
58
+ const subject_provided = subject !== null && subject !== undefined;
59
+ const has_existing_subject = this.conditional_config.subject !== null && this.conditional_config.subject !== undefined;
57
60
 
58
- if (!subjectProvided && !hasExistingSubject) {
61
+ if (!subject_provided && !has_existing_subject) {
59
62
  throw new Error(`No subject set for case step: ${this.name}, using default equality check`);
60
63
  }
61
64
 
62
- if (subjectProvided && (!hasExistingSubject || this.force_subject_override)) {
65
+ if (subject_provided && (!has_existing_subject || this.force_subject_override)) {
63
66
  this.conditional_config.subject = subject;
64
67
  }
65
68
 
@@ -67,4 +70,31 @@ export default class Case extends LogicStep {
67
70
  throw new Error(`Invalid conditional configuration for case step: ${this.name}`);
68
71
  }
69
72
  }
73
+
74
+ /**
75
+ * Inserts safely serializable properties of the step into a new object for serialization.
76
+ * @returns {Object} An object containing the step's properties ready for serialization.
77
+ */
78
+ prepareForSerialization() {
79
+ return {
80
+ ...super.prepareForSerialization(),
81
+ force_subject_override: this.force_subject_override,
82
+ is_matched: this.is_matched,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Hydrates a parsed step object into a Case instance, restoring match state.
88
+ * @param {Object} parsed_step - The parsed step object.
89
+ * @param {import('../callable_registry.js').default|null} [callable_registry] - Registry used to resolve function callables.
90
+ * @returns {Case} The hydrated Case instance.
91
+ */
92
+ static hydrate(parsed_step, callable_registry = null) {
93
+ const instance = super.hydrate(parsed_step, callable_registry);
94
+ instance.is_matched = parsed_step.is_matched ?? false;
95
+
96
+ return instance;
97
+ }
70
98
  }
99
+
100
+ Case.registerStepClass(Case);
@@ -1,5 +1,7 @@
1
+ import Step from './step.js';
1
2
  import LogicStep from './logic_step.js';
2
3
  import { conditional_step_comparators } from '../../enums/index.js';
4
+ import { event_names } from '../instance_state.js';
3
5
 
4
6
  /**
5
7
  * ConditionalStep class for branching logic based on conditions.
@@ -19,6 +21,8 @@ export default class ConditionalStep extends LogicStep {
19
21
  * @param {*|Function} [options.conditional.value] - Value to compare against. Can be a function that returns the value.
20
22
  * @param {Function|Step|Workflow} [options.true_callable=async () => {}] - Callable to execute if condition is true.
21
23
  * @param {Function|Step|Workflow} [options.false_callable=async () => {}] - Callable to execute if condition is false.
24
+ * @param {string|null} [options.true_callable_registry_key=null] - Optional key to reference true_callable to be rehydrated after serialization.
25
+ * @param {string|null} [options.false_callable_registry_key=null] - Optional key to reference false_callable to be rehydrated after serialization.
22
26
  */
23
27
  constructor({
24
28
  name,
@@ -29,12 +33,23 @@ export default class ConditionalStep extends LogicStep {
29
33
  },
30
34
  true_callable = async () => {},
31
35
  false_callable = async () => {},
36
+ true_callable_registry_key = null,
37
+ false_callable_registry_key = null,
32
38
  }) {
33
39
  super({
34
40
  name,
35
41
  conditional
36
42
  });
37
43
 
44
+ // Optional keys to reference true_callable/false_callable to be rehydrated after serialization.
45
+ this.true_callable_registry_key = true_callable_registry_key;
46
+ this.false_callable_registry_key = false_callable_registry_key;
47
+
48
+ // Keep the raw (unbound) originals for serialization - binding renames a function
49
+ // (e.g. "falseBranch" -> "bound falseBranch"), which would break registry lookups on hydrate.
50
+ this._true_callable_raw = true_callable;
51
+ this._false_callable_raw = false_callable;
52
+
38
53
  // Bind function callables to this step instance for state access
39
54
  if (typeof true_callable === 'function') {
40
55
  this.true_callable = true_callable.bind(this);
@@ -52,7 +67,11 @@ export default class ConditionalStep extends LogicStep {
52
67
  }
53
68
 
54
69
  /**
55
- * Executes the appropriate branch based on the condition evaluation.
70
+ * Executes the appropriate branch based on the condition evaluation. When the executed branch
71
+ * is a `Step`/`Workflow` (not a plain function), it is stamped with this step's own
72
+ * `parent_workflow_id`/`use_state_singleton`/`state` first - true/false_callable are never
73
+ * added to the parent workflow via `addStep()`, so this is the only way they end up sharing
74
+ * its state instead of their own, independent one.
56
75
  * @async
57
76
  * @returns {Promise<*>} The result of the executed branch.
58
77
  */
@@ -64,30 +83,78 @@ export default class ConditionalStep extends LogicStep {
64
83
 
65
84
  if (this.checkCondition()) {
66
85
  this.log(
67
- this.getState('events.step.event_names.CONDITIONAL_TRUE_BRANCH_EXECUTED'),
86
+ event_names.step.CONDITIONAL_TRUE_BRANCH_EXECUTED,
68
87
  `Condition met for step: ${this.name}, executing true branch`
69
88
  );
70
89
 
71
90
  if (typeof true_callable === 'function') {
72
91
  result = await true_callable();
73
92
  } else {
74
- true_callable.parentWorkflowId = this.parentWorkflowId;
93
+ true_callable.parent_workflow_id = this.parent_workflow_id;
94
+ true_callable.use_state_singleton = this.use_state_singleton;
95
+ true_callable.state = this.state;
75
96
  result = await true_callable.execute();
76
97
  }
77
98
  } else {
78
99
  this.log(
79
- this.getState('events.step.event_names.CONDITIONAL_FALSE_BRANCH_EXECUTED'),
100
+ event_names.step.CONDITIONAL_FALSE_BRANCH_EXECUTED,
80
101
  `Condition not met for step: ${this.name}, executing false branch`
81
102
  );
82
103
 
83
104
  if (typeof false_callable === 'function') {
84
105
  result = await false_callable();
85
106
  } else {
86
- false_callable.parentWorkflowId = this.parentWorkflowId;
107
+ false_callable.parent_workflow_id = this.parent_workflow_id;
108
+ false_callable.use_state_singleton = this.use_state_singleton;
109
+ false_callable.state = this.state;
87
110
  result = await false_callable.execute();
88
111
  }
89
112
  }
90
113
 
91
114
  return { message: `Conditional step ${this.name} completed`, result };
92
115
  }
116
+
117
+ /**
118
+ * Inserts safely serializable properties of the step into a new object for serialization.
119
+ * @returns {Object} An object containing the step's properties ready for serialization.
120
+ */
121
+ prepareForSerialization() {
122
+ return {
123
+ ...super.prepareForSerialization(),
124
+ // The base `callable` is an internal wiring detail (the bound `conditional` method) -
125
+ // ConditionalStep's constructor doesn't take a callable, so it isn't real data to persist.
126
+ callable: null,
127
+ true_callable: this.true_callable_registry_key
128
+ ? { type: Step.callable_types.FUNCTION, value: this.true_callable_registry_key }
129
+ : Step.serializeCallableField(this._true_callable_raw),
130
+ false_callable: this.false_callable_registry_key
131
+ ? { type: Step.callable_types.FUNCTION, value: this.false_callable_registry_key }
132
+ : Step.serializeCallableField(this._false_callable_raw),
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Hydrates a parsed step object into a ConditionalStep instance, resolving the true/false branch callables.
138
+ * @param {Object} parsed_step - The parsed step object.
139
+ * @param {import('../callable_registry.js').default|null} [callable_registry] - Registry used to resolve function callables.
140
+ * @returns {ConditionalStep} The hydrated ConditionalStep instance.
141
+ */
142
+ static hydrate(parsed_step, callable_registry = null) {
143
+ const true_descriptor = parsed_step.true_callable;
144
+ const false_descriptor = parsed_step.false_callable;
145
+
146
+ return super.hydrate({
147
+ ...parsed_step,
148
+ true_callable: Step.hydrateCallableField(true_descriptor, callable_registry),
149
+ false_callable: Step.hydrateCallableField(false_descriptor, callable_registry),
150
+ true_callable_registry_key: true_descriptor?.type === Step.callable_types.FUNCTION
151
+ ? true_descriptor.value
152
+ : null,
153
+ false_callable_registry_key: false_descriptor?.type === Step.callable_types.FUNCTION
154
+ ? false_descriptor.value
155
+ : null,
156
+ }, callable_registry);
157
+ }
93
158
  }
159
+
160
+ ConditionalStep.registerStepClass(ConditionalStep);
@@ -1,5 +1,6 @@
1
1
  import Step from './step.js';
2
2
  import { delay_types, step_types } from '../../enums/index.js';
3
+ import { event_names } from '../instance_state.js';
3
4
  import schedule from 'node-schedule';
4
5
  import { addMilliseconds } from 'date-fns';
5
6
 
@@ -47,7 +48,7 @@ export default class DelayStep extends Step {
47
48
 
48
49
  if (this.absolute_timestamp.getTime() <= now.getTime()) {
49
50
  this.log(
50
- this.getState('events.step.event_names.DELAY_STEP_ABSOLUTE_COMPLETE'),
51
+ event_names.step.DELAY_STEP_ABSOLUTE_COMPLETE,
51
52
  `No delay for step: ${this.name}. Continuing.`
52
53
  );
53
54
  return { delayed: false, delay_type: this.delay_type, timestamp: now.toISOString() };
@@ -63,17 +64,13 @@ export default class DelayStep extends Step {
63
64
  async delay(delay_until) {
64
65
  return new Promise((resolve) => {
65
66
  this.log(
66
- this.getState(
67
- `events.step.event_names.DELAY_STEP_${this.delay_type.toUpperCase()}_SCHEDULED`
68
- ),
67
+ event_names.step[`DELAY_STEP_${this.delay_type.toUpperCase()}_SCHEDULED`],
69
68
  `Delay scheduled for step: ${this.name} until ${delay_until.toISOString()}`
70
69
  );
71
70
 
72
71
  const job = schedule.scheduleJob(delay_until, () => {
73
72
  this.log(
74
- this.getState(
75
- `events.step.event_names.DELAY_STEP_${this.delay_type.toUpperCase()}_COMPLETE`
76
- ),
73
+ event_names.step[`DELAY_STEP_${this.delay_type.toUpperCase()}_COMPLETE`],
77
74
  `Delay complete for step: ${this.name}. Continuing.`
78
75
  );
79
76
  resolve({ delayed: true, delay_type: this.delay_type, timestamp: new Date().toISOString() });
@@ -90,7 +87,7 @@ export default class DelayStep extends Step {
90
87
  async relative() {
91
88
  if (this.relative_delay_ms <= 0) {
92
89
  this.log(
93
- this.getState('events.step.event_names.DELAY_STEP_RELATIVE_COMPLETE'),
90
+ event_names.step.DELAY_STEP_RELATIVE_COMPLETE,
94
91
  `No delay for step: ${this.name}. Continuing.`
95
92
  );
96
93
  return { delayed: false, delay_type: this.delay_type, timestamp: new Date().toISOString() };
@@ -100,4 +97,22 @@ export default class DelayStep extends Step {
100
97
 
101
98
  return this.delay(delay_until);
102
99
  }
100
+
101
+ /**
102
+ * Inserts safely serializable properties of the step into a new object for serialization.
103
+ * @returns {Object} An object containing the step's properties ready for serialization.
104
+ */
105
+ prepareForSerialization() {
106
+ return {
107
+ ...super.prepareForSerialization(),
108
+ // The base `callable` is an internal wiring detail (the bound absolute/relative method) -
109
+ // DelayStep's constructor doesn't take a callable, so it isn't real data to persist.
110
+ callable: null,
111
+ delay_type: this.delay_type,
112
+ absolute_timestamp: this.absolute_timestamp,
113
+ relative_delay_ms: this.relative_delay_ms,
114
+ };
115
+ }
103
116
  }
117
+
118
+ DelayStep.registerStepClass(DelayStep);