@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,5 +1,9 @@
1
1
  import crypto from 'crypto';
2
2
  import Base from './base.js';
3
+ import CallableRegistry from './callable_registry.js';
4
+ import Step from './steps/step.js';
5
+ import State from './state.js';
6
+ import { statuses, event_names, events, types, conditional_step_comparators, messages } from './instance_state.js';
3
7
  import { base_types } from '../enums/index.js';
4
8
 
5
9
  /**
@@ -12,30 +16,48 @@ export default class Workflow extends Base {
12
16
  * Creates a new Workflow instance.
13
17
  * @param {Object} options - Configuration options.
14
18
  * @param {string} [options.name] - Name of the workflow.
19
+ * @param {CallableRegistry|null} [options.callable_registry=null] - Registry for callable objects.
15
20
  * @param {boolean} [options.exit_on_error=false] - Whether to exit on error.
16
21
  * @param {Array<Step>} [options.steps=[]] - Array of steps to add to the workflow.
17
22
  * @param {boolean} [options.throw_on_empty=false] - Whether to throw error if workflow is empty.
23
+ * @param {boolean} [options.use_state_singleton=false] - Deprecated. When true, this workflow (and every
24
+ * `Step` it owns) reads/writes `getState`/`setState`/`deleteState` calls through the process-wide `State`
25
+ * singleton instead of this workflow's own state.
18
26
  */
19
27
  constructor({
20
28
  name,
29
+ callable_registry = null,
21
30
  exit_on_error = false,
31
+ result_per_step = false,
32
+ result_per_step_function = null,
22
33
  steps = [],
23
- throw_on_empty = false
34
+ throw_on_empty = false,
35
+ use_state_singleton = false,
24
36
  }) {
25
- super({ name, base_type: base_types.WORKFLOW });
26
-
27
- this.initializeWorkflowState();
28
-
29
- this.addSteps(steps);
37
+ super({ name, base_type: base_types.WORKFLOW, use_state_singleton });
30
38
 
39
+ this.callable_registry = callable_registry ?? new CallableRegistry();
40
+ this.current_session_id = null;
31
41
  this.exit_on_error = exit_on_error;
32
- this.throw_on_empty = throw_on_empty;
42
+ this.result_per_step = result_per_step;
33
43
  this.sessions = {};
34
- this.current_session_id = null;
44
+ this.throw_on_empty = throw_on_empty;
45
+ this.result_per_step_function = result_per_step_function;
46
+
47
+ // _steps/steps_by_id must exist before initializeWorkflowState(): it reads this._steps
48
+ // (to set current_step) and logs, which serializes `this` - both need this._steps to
49
+ // already be an array, even when no steps are passed (addSteps([]) never calls addStep,
50
+ // so it wouldn't otherwise get initialized).
51
+ this._steps = [];
52
+ this.steps_by_id = {};
53
+ this.addSteps(steps);
54
+ this.initializeWorkflowState();
35
55
  }
36
56
 
37
57
  /**
38
58
  * Executes the workflow by running all steps in sequence.
59
+ * If the workflow is currently `paused`, resumes from the step after the one
60
+ * that was running when it paused, rather than starting over from the beginning.
39
61
  * @async
40
62
  * @returns {Promise<Workflow>} The workflow instance with execution results.
41
63
  * @throws {Error} Throws if workflow is empty and throw_on_empty is true.
@@ -51,21 +73,25 @@ export default class Workflow extends Base {
51
73
  }
52
74
 
53
75
  this.markAsComplete();
54
- this.prepareResult('Workflow is empty', null);
76
+ await this.prepareResult('Workflow is empty', null);
55
77
  return this;
56
78
  }
57
-
79
+
80
+ const is_resuming = this.status === statuses.workflow.PAUSED;
81
+ const paused_at_index = this._steps.findIndex(step => step.id === this.current_step);
82
+ const start_index = is_resuming ? paused_at_index + 1 : 0;
83
+
58
84
  this.markAsRunning();
59
85
 
60
- for (let i = 0; i < this._steps.length; i++) {
86
+ for (let i = start_index; i < this._steps.length; i++) {
61
87
  if (this.should_break) {
62
- this.log(this.getState('event_names.workflow').WORKFLOW_BREAK_EXECUTED, `Workflow "${this.name}" execution broken at step ${this._steps[i].name} - ${this._steps[i].id}.`);
88
+ this.log(event_names.workflow.WORKFLOW_BREAK_EXECUTED, `Workflow "${this.name}" execution broken at step ${this._steps[i].name} - ${this._steps[i].id}.`);
63
89
  break;
64
90
  }
65
91
 
66
92
  if (this.should_skip) {
67
93
  this.log(
68
- this.getState('events.workflow.event_names.WORKFLOW_STEP_SKIPPED'),
94
+ event_names.workflow.WORKFLOW_STEP_SKIPPED,
69
95
  `Workflow "${this.name}" skipping step ${this._steps[i].name} - ${this._steps[i].id}.`
70
96
  );
71
97
  this.should_skip = false;
@@ -76,10 +102,10 @@ export default class Workflow extends Base {
76
102
 
77
103
  try {
78
104
  const step_result = await this.step();
79
- this.prepareResult('Success', step_result);
105
+ await this.prepareResult('Success', step_result);
80
106
  } catch (error) {
81
107
  this.markAsFailed();
82
- this.prepareResult(`Workflow execution failed at step ${this.steps_by_id[this.current_step].name} - ${this.current_step}`, { error });
108
+ await this.prepareResult(`Workflow execution failed at step ${this.steps_by_id[this.current_step].name} - ${this.current_step}`, { error });
83
109
 
84
110
  if (this.exit_on_error) {
85
111
  return this;
@@ -94,7 +120,7 @@ export default class Workflow extends Base {
94
120
  }
95
121
 
96
122
  this.markAsComplete();
97
- return this;
123
+ return this.prepareForSerialization();
98
124
  }
99
125
 
100
126
  /**
@@ -106,8 +132,8 @@ export default class Workflow extends Base {
106
132
  this.should_pause = false;
107
133
  this.timing.resume_time = new Date();
108
134
 
109
- this.getState('events.workflow').emit(
110
- this.getState('event_names.workflow').WORKFLOW_RESUMED,
135
+ events.workflow.emit(
136
+ event_names.workflow.WORKFLOW_RESUMED,
111
137
  this.getState()
112
138
  );
113
139
  return this.execute();
@@ -121,10 +147,9 @@ export default class Workflow extends Base {
121
147
  async step() {
122
148
  const step = this.steps_by_id[this.current_step];
123
149
 
124
- step.parentWorkflowId = this.id;
125
150
  const result = await step.execute();
126
151
 
127
- if (step.status === this.getState('statuses.step.FAILED')) {
152
+ if (step.status === statuses.step.FAILED) {
128
153
  throw step.errors[step.errors.length - 1] ?? new Error(`Step "${step.name}" failed`);
129
154
  }
130
155
 
@@ -137,8 +162,10 @@ export default class Workflow extends Base {
137
162
  * @throws {Error} Throws if step is not a valid Step instance.
138
163
  */
139
164
  addStep(step) {
165
+ // This check only ensures that the getCallableType method exists,
166
+ // which is a characteristic of Step instances
140
167
  if (typeof step.getCallableType !== 'function') {
141
- throw new Error('Invalid step type. Must be an instance of Step.');
168
+ throw new Error('Invalid input. Must be an instance of Step.');
142
169
  }
143
170
 
144
171
  if (!Array.isArray(this._steps)) {
@@ -151,7 +178,10 @@ export default class Workflow extends Base {
151
178
 
152
179
  this.steps_by_id[step.id] = step;
153
180
 
154
- step.parentWorkflowId = this.id;
181
+ step.parent_workflow_id = this.id;
182
+ step.parent_workflow = this.prepareForSerialization();
183
+ step.use_state_singleton = this.use_state_singleton;
184
+ step.state = this.state;
155
185
  this._steps.push(step);
156
186
  }
157
187
 
@@ -166,7 +196,10 @@ export default class Workflow extends Base {
166
196
  }
167
197
 
168
198
  this.steps_by_id[step.id] = step;
169
- step.parentWorkflowId = this.id;
199
+ step.parent_workflow_id = this.id;
200
+ step.parent_workflow = this.prepareForSerialization();
201
+ step.use_state_singleton = this.use_state_singleton;
202
+ step.state = this.state;
170
203
  this._steps.splice(index, 0, step);
171
204
  }
172
205
 
@@ -175,6 +208,10 @@ export default class Workflow extends Base {
175
208
  * @param {Step[]} steps - Array of steps to add.
176
209
  */
177
210
  addSteps(steps) {
211
+ if (!Array.isArray(steps)) {
212
+ throw new Error('Invalid input. Must be an array of Step instances.');
213
+ }
214
+
178
215
  steps.forEach(step => this.addStep(step));
179
216
  }
180
217
 
@@ -183,6 +220,7 @@ export default class Workflow extends Base {
183
220
  */
184
221
  clearSteps() {
185
222
  this._steps = [];
223
+ this.steps_by_id = {};
186
224
  }
187
225
 
188
226
  /**
@@ -207,6 +245,10 @@ export default class Workflow extends Base {
207
245
  * @param {string} stepId - The ID of the step to delete.
208
246
  */
209
247
  deleteStep(stepId) {
248
+ if (!Array.isArray(this._steps)) {
249
+ this._steps = [];
250
+ }
251
+
210
252
  this._steps = this._steps.filter(step => step.id !== stepId);
211
253
  }
212
254
 
@@ -215,36 +257,63 @@ export default class Workflow extends Base {
215
257
  * @param {number} index - The index of the step to delete.
216
258
  */
217
259
  deleteStepByIndex(index) {
260
+ if (!Array.isArray(this._steps)) {
261
+ this._steps = [];
262
+ }
263
+
218
264
  this._steps.splice(index, 1);
219
265
  }
220
266
 
267
+ /**
268
+ * Resolves a nested property path within this workflow's own state - the low-level counterpart
269
+ * to `getState()`. Falls back to the deprecated `State` singleton's resolver when
270
+ * `use_state_singleton` is `true`.
271
+ * @param {string} path - Path to the state property.
272
+ * @param {boolean} [emit=true] - Only meaningful when `use_state_singleton` is `true`; whether
273
+ * to emit the singleton's `GET_FROM_PROPERTY_PATH` state event.
274
+ * @returns {*} The value at the specified path, or undefined if not found.
275
+ */
276
+ getStateFromPropertyPath(path, emit = true) {
277
+ if (this.use_state_singleton) {
278
+ console.warn('The state singleton has been deprecated. Use the .prepareForSerialization() method on the workflow instance instead.');
279
+ return State.getFromPropertyPath(path, emit);
280
+ }
281
+
282
+ return this.state.getStateFromPropertyPath(path);
283
+ }
284
+
221
285
  /**
222
286
  * Initializes the workflow state with default values.
223
287
  */
224
288
  initializeWorkflowState() {
225
- this.results = [];
226
- this.exit_on_error = false;
227
- this.current_step = null;
228
- this.should_break = false;
229
- this.should_continue = false;
230
- this.should_pause = false;
231
- this.should_skip = false;
232
- this.status = this.getState('statuses.workflow').CREATED;
233
- this._steps = [];
234
- this.throw_on_empty = this.throw_on_empty;
289
+ this.current_step = ! this.isEmpty() ? this._steps[0].id : null;
290
+ this.results = this.results ?? [];
291
+ this.sessions = this.sessions ?? {};
292
+ this.should_break = this.should_break ?? false;
293
+ this.should_continue = this.should_continue ?? false;
294
+ this.should_pause = this.should_pause ?? false;
295
+ this.should_skip = this.should_skip ?? false;
296
+ this.status = this.status ?? statuses.workflow.CREATED;
235
297
  this.timing = {
236
298
  ...this.timing,
237
- create_time: new Date(),
238
- pause_time: null,
239
- resume_time: null,
299
+ create_time: this.timing?.create_time ?? new Date(),
300
+ pause_time: this.timing?.pause_time ?? null,
301
+ resume_time: this.timing?.resume_time ?? null,
240
302
  }
241
303
 
242
- const workflows = this.getState('workflows');
243
- workflows[this.id] = this;
244
- this.setState('workflows', workflows);
304
+ if (this.use_state_singleton) {
305
+ // The deprecated `State` singleton is shared by every workflow that opts into it, so it
306
+ // still needs an id-keyed registry (unlike per-instance state, which only ever has one
307
+ // workflow to represent and can just reference it directly - see the else branch).
308
+ const workflows = this.getState('workflows');
309
+ workflows[this.id] = this;
310
+ this.setState('workflows', workflows);
311
+ } else {
312
+ this.setState('workflow', this);
313
+ }
245
314
 
246
315
  this.log(
247
- this.getState('event_names.workflow').WORKFLOW_CREATED,
316
+ event_names.workflow.WORKFLOW_CREATED,
248
317
  `Workflow "${this.name}" initialized.`
249
318
  );
250
319
  }
@@ -254,7 +323,7 @@ export default class Workflow extends Base {
254
323
  * @returns {boolean} True if the workflow is empty.
255
324
  */
256
325
  isEmpty() {
257
- return !this._steps || !this._steps.length
326
+ return !Array.isArray(this._steps) || !this._steps.length;
258
327
  }
259
328
 
260
329
  /**
@@ -273,11 +342,11 @@ export default class Workflow extends Base {
273
342
  this.timing.create_time = new Date();
274
343
 
275
344
  this.log(
276
- this.getState('event_names.workflow').WORKFLOW_CREATED,
345
+ event_names.workflow.WORKFLOW_CREATED,
277
346
  `Workflow "${this.name}" created.`
278
347
  );
279
348
 
280
- return this.getState('statuses.workflow').CREATED;
349
+ return statuses.workflow.CREATED;
281
350
  }
282
351
 
283
352
  /**
@@ -293,23 +362,23 @@ export default class Workflow extends Base {
293
362
  */
294
363
  markAsPaused() {
295
364
  this.timing.pause_time = new Date();
296
- this.status = this.getState('statuses.workflow').PAUSED;
365
+ this.status = statuses.workflow.PAUSED;
297
366
 
298
- this.getState('events.workflow').emit(
299
- this.getState('event_names.workflow').WORKFLOW_PAUSED,
367
+ events.workflow.emit(
368
+ event_names.workflow.WORKFLOW_PAUSED,
300
369
  this.getState()
301
370
  );
302
371
  }
303
-
372
+
304
373
  /**
305
374
  * Marks the workflow as resumed.
306
375
  */
307
376
  markAsResumed() {
308
377
  this.timing.resume_time = new Date();
309
- this.status = this.getState('statuses.workflow').RUNNING;
378
+ this.status = statuses.workflow.RUNNING;
310
379
 
311
- this.getState('events.workflow').emit(
312
- this.getState('event_names.workflow').WORKFLOW_RESUMED,
380
+ events.workflow.emit(
381
+ event_names.workflow.WORKFLOW_RESUMED,
313
382
  this.getState()
314
383
  );
315
384
  }
@@ -323,12 +392,22 @@ export default class Workflow extends Base {
323
392
  const [step] = this._steps.splice(fromIndex, 1);
324
393
  this._steps.splice(toIndex, 0, step);
325
394
 
326
- this.getState('events.workflow').emit(
327
- this.getState('event_names.workflow').WORKFLOW_STEP_MOVED,
395
+ events.workflow.emit(
396
+ event_names.workflow.WORKFLOW_STEP_MOVED,
328
397
  this.getState()
329
398
  );
330
399
  }
331
400
 
401
+ /**
402
+ * Parses a property path string into an array of keys, supporting both dot notation and
403
+ * bracket notation (e.g. `"users[0].name"`). Pure utility - not affected by `use_state_singleton`.
404
+ * @param {string} path - The path to parse.
405
+ * @returns {string[]} Array of property keys.
406
+ */
407
+ parseStatePath(path) {
408
+ return this.use_state_singleton ? State.parsePath(path) : this.state.parseStatePath(path);
409
+ }
410
+
332
411
  /**
333
412
  * Pauses the workflow execution.
334
413
  */
@@ -336,8 +415,8 @@ export default class Workflow extends Base {
336
415
  this.should_pause = true;
337
416
  this.timing.pause_time = new Date();
338
417
 
339
- this.getState('events.workflow').emit(
340
- this.getState('event_names.workflow').WORKFLOW_PAUSED,
418
+ events.workflow.emit(
419
+ event_names.workflow.WORKFLOW_PAUSED,
341
420
  this.getState()
342
421
  );
343
422
  }
@@ -350,13 +429,41 @@ export default class Workflow extends Base {
350
429
  return this._steps.pop();
351
430
  }
352
431
 
432
+ /**
433
+ * Inserts safely serializable properties of the workflow into a new object for serialization.
434
+ * @returns {Object} An object containing the workflow's properties ready for serialization.
435
+ */
436
+ prepareForSerialization() {
437
+ const serialized_workflow = {
438
+ id: this.id,
439
+ current_session_id: this.current_session_id,
440
+ current_step: this.current_step,
441
+ exit_on_error: this.exit_on_error,
442
+ name: this.name,
443
+ sessions: this.sessions,
444
+ status: this.status,
445
+ steps: this._steps.map(step => step.prepareForSerialization()),
446
+ throw_on_empty: this.throw_on_empty,
447
+ timing: this.timing,
448
+ results: this.results,
449
+ use_state_singleton: this.use_state_singleton,
450
+ };
451
+
452
+ return serialized_workflow;
453
+ }
454
+
353
455
  /**
354
456
  * Prepares a result object and adds it to the results array.
355
457
  * @param {string} message - Result message.
356
458
  * @param {*} data - Result data.
357
459
  */
358
- prepareResult(message, data) {
359
- this.results.push({ message, data });
460
+ async prepareResult(message, data) {
461
+ if (this.result_per_step && typeof this.result_per_step_function === 'function') {
462
+ await this.result_per_step_function(this.prepareForSerialization());
463
+ }
464
+
465
+ const result = { message, data };
466
+ this.results.push(result);
360
467
  }
361
468
 
362
469
  /**
@@ -375,6 +482,33 @@ export default class Workflow extends Base {
375
482
  steps.forEach(step => this.addStep(step));
376
483
  }
377
484
 
485
+ /**
486
+ * Serializes the workflow into a JSON string.
487
+ * @returns {string} The JSON string representation of the workflow.
488
+ */
489
+ serialize() {
490
+ return JSON.stringify(this.prepareForSerialization());
491
+ }
492
+
493
+ /**
494
+ * Sets a nested property value within this workflow's own state, creating intermediate
495
+ * objects/arrays as needed - the low-level counterpart to `setState()`. Falls back to the
496
+ * deprecated `State` singleton's setter when `use_state_singleton` is `true`.
497
+ * @param {string} path - Path to the state property.
498
+ * @param {*} value - The value to set at the specified path.
499
+ * @param {boolean} [emit=true] - Only meaningful when `use_state_singleton` is `true`; whether
500
+ * to emit the singleton's `SET_TO_PROPERTY_PATH` state event.
501
+ */
502
+ setStateToPropertyPath(path, value, emit = true) {
503
+ if (this.use_state_singleton) {
504
+ console.warn('The state singleton has been deprecated. Use the .prepareForSerialization() method on the workflow instance instead.');
505
+ State.setToPropertyPath(path, value, emit);
506
+ return;
507
+ }
508
+
509
+ this.state.setStateToPropertyPath(path, value);
510
+ }
511
+
378
512
  /**
379
513
  * Removes and returns the first step from the workflow.
380
514
  * @returns {Step} The first step.
@@ -399,10 +533,20 @@ export default class Workflow extends Base {
399
533
 
400
534
  this.steps_by_id[step.id] = step;
401
535
 
402
- step.parentWorkflowId = this.id;
536
+ step.parent_workflow_id = this.id;
537
+ step.use_state_singleton = this.use_state_singleton;
538
+ step.state = this.state;
403
539
  this._steps.unshift(step);
404
540
  }
405
541
 
542
+ /**
543
+ * Custom JSON serializer
544
+ * @returns {Object} The JSON representation of the workflow.
545
+ */
546
+ toJSON() {
547
+ return this.prepareForSerialization();
548
+ }
549
+
406
550
  /**
407
551
  * Gets the array of steps in the workflow.
408
552
  * @returns {Step[]} Array of steps.
@@ -416,12 +560,86 @@ export default class Workflow extends Base {
416
560
  * @param {Step[]} steps - Array of steps to add.
417
561
  */
418
562
  set steps(steps) {
419
- steps.forEach((step, index) => {
420
- if (typeof step.getCallableType !== 'function') {
421
- throw new Error(`Invalid step type. Step at index ${index} is not an instance of Step.`);
422
- }
563
+ this.addSteps(steps);
564
+ }
565
+
566
+ /**
567
+ * Deserializes a JSON string into a Workflow instance and hydrates it.
568
+ * @param {string} serialized_workflow - The JSON string representation of the workflow.
569
+ * @param {CallableRegistry|null} [callable_registry] - Registry used to resolve function callables in the workflow's steps.
570
+ * @returns {Workflow} The hydrated Workflow instance.
571
+ * @throws {Error} Throws if the serialized workflow is not a string.
572
+ */
573
+ static hydrateSerialized(serialized_workflow, callable_registry = null) {
574
+ // TODO: Validate structure of serialized workflow
575
+ if (typeof serialized_workflow !== 'string') {
576
+ throw new Error('Invalid serialized workflow. Must be a string.');
577
+ }
578
+
579
+ const parsed = JSON.parse(serialized_workflow);
580
+
581
+ return Workflow.hydrate(parsed, callable_registry);
582
+ }
583
+
584
+ /**
585
+ * Hydrates a parsed workflow object into a Workflow instance.
586
+ * @param {Object} parsed_workflow - The parsed workflow object.
587
+ * @param {CallableRegistry|null} [callable_registry] - Registry used to resolve function callables in the workflow's steps.
588
+ * @returns {Workflow} The hydrated Workflow instance.
589
+ * @throws {Error} Throws if the parsed workflow is not a valid object.
590
+ */
591
+ static hydrate(parsed_workflow, callable_registry = null) {
592
+ // TODO: Validate structure of serialized workflow
593
+ // TODO: Use event system to handle errors?
594
+ if (typeof parsed_workflow !== 'object' || parsed_workflow === null) {
595
+ throw new Error('Invalid parsed workflow. Must be a valid object.');
596
+ }
597
+
598
+ const hydrated_workflow = new Workflow({
599
+ name: parsed_workflow.name,
600
+ callable_registry,
601
+ exit_on_error: parsed_workflow.exit_on_error,
602
+ steps: parsed_workflow.steps.map(step => Step.hydrateAny(step, callable_registry)),
603
+ throw_on_empty: parsed_workflow.throw_on_empty,
604
+ use_state_singleton: parsed_workflow.use_state_singleton ?? false,
423
605
  });
424
606
 
425
- this.addSteps(steps);
607
+ // The constructor above (via Base) always generates a fresh id, and addStep() has
608
+ // already stamped that fresh id onto each step's parent_workflow_id and (in singleton mode)
609
+ // registered the workflow under it in the singleton's `workflows` registry. Restoring the
610
+ // real id below would otherwise leave both referencing a discarded id, so fix them up here
611
+ // too. Per-instance state needs no such fixup for its `workflow` key - it's the same live
612
+ // object, so the id mutation below is reflected automatically.
613
+ const stale_id = hydrated_workflow.id;
614
+ hydrated_workflow.id = parsed_workflow.id;
615
+
616
+ if (hydrated_workflow.use_state_singleton) {
617
+ const workflows = hydrated_workflow.getState('workflows');
618
+ delete workflows[stale_id];
619
+ workflows[hydrated_workflow.id] = hydrated_workflow;
620
+ hydrated_workflow.setState('workflows', workflows);
621
+ }
622
+
623
+ hydrated_workflow.steps.forEach(step => {
624
+ step.parent_workflow_id = hydrated_workflow.id;
625
+ });
626
+
627
+ hydrated_workflow.current_session_id = parsed_workflow.current_session_id;
628
+ hydrated_workflow.current_step = parsed_workflow.current_step ?? hydrated_workflow.current_step;
629
+ hydrated_workflow.sessions = parsed_workflow.sessions ?? {};
630
+ hydrated_workflow.status = parsed_workflow.status;
631
+ hydrated_workflow.timing = parsed_workflow.timing;
632
+ hydrated_workflow.results = parsed_workflow.results;
633
+
634
+ return hydrated_workflow;
426
635
  }
427
636
  }
637
+
638
+ // Framework constants, exposed as static members rather than duplicated into every
639
+ // Workflow's own state (see instance_state.js, the canonical source for these values).
640
+ Workflow.statuses = statuses;
641
+ Workflow.event_names = event_names;
642
+ Workflow.events = events;
643
+ Workflow.types = types;
644
+ Workflow.conditional_step_comparators = conditional_step_comparators;
645
+ Workflow.messages = messages;
@@ -6,7 +6,7 @@
6
6
  * @example
7
7
  * import delay_types from 'micro-flow';
8
8
  *
9
- * const delayStep = new DelayStep({
9
+ * const delay_step = new DelayStep({
10
10
  * name: 'wait-5-seconds',
11
11
  * delay_type: delay_types.RELATIVE,
12
12
  * delay_duration: 5000
@@ -5,7 +5,7 @@
5
5
  * @enum {string}
6
6
  * @readonly
7
7
  */
8
- const LogicStepTypes = {
8
+ const logic_step_types = {
9
9
  CONDITIONAL: 'conditional',
10
10
  LOOP: 'loop',
11
11
  FLOW_CONTROL: 'flow_control',
@@ -13,4 +13,4 @@ const LogicStepTypes = {
13
13
  SKIP: 'skip'
14
14
  };
15
15
 
16
- export default LogicStepTypes;
16
+ export default logic_step_types;
@@ -3,18 +3,18 @@
3
3
  * @type {Object.<string, string>}
4
4
  * @readonly
5
5
  * @example
6
- * console.log(sub_step_types.Step); // "step"
7
- * console.log(sub_step_types.ConditionalStep); // "conditional"
6
+ * console.log(sub_step_types.step); // "step"
7
+ * console.log(sub_step_types.conditional_step); // "conditional"
8
8
  */
9
9
  const sub_step_types = {
10
- Step: 'step',
11
- LogicStep: 'logic',
12
- ConditionalStep: 'conditional',
13
- FlowControlStep: 'flow_control',
14
- LoopStep: 'loop',
15
- SwitchStep: 'switch',
16
- Case: 'case',
17
- DelayStep: 'delay',
10
+ step: 'step',
11
+ logic_step: 'logic',
12
+ conditional_step: 'conditional',
13
+ flow_control_step: 'flow_control',
14
+ loop_step: 'loop',
15
+ switch_step: 'switch',
16
+ case: 'case',
17
+ delay_step: 'delay',
18
18
  };
19
19
 
20
20
  export default sub_step_types;