@ronaldroe/micro-flow 1.0.0 → 1.2.1

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 (101) hide show
  1. package/README.md +63 -153
  2. package/dist/index.js +2 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/src/classes/base.js +4 -0
  5. package/dist/src/classes/base.js.map +7 -0
  6. package/dist/src/classes/events/broadcast.js +2 -0
  7. package/dist/src/classes/events/broadcast.js.map +7 -0
  8. package/dist/src/classes/events/event.js +2 -0
  9. package/dist/src/classes/events/event.js.map +7 -0
  10. package/dist/src/classes/events/index.js +2 -0
  11. package/dist/src/classes/events/index.js.map +7 -0
  12. package/dist/src/classes/events/state_event.js +2 -0
  13. package/dist/src/classes/events/state_event.js.map +7 -0
  14. package/dist/src/classes/events/step_event.js +2 -0
  15. package/dist/src/classes/events/step_event.js.map +7 -0
  16. package/dist/src/classes/events/workflow_event.js +2 -0
  17. package/dist/src/classes/events/workflow_event.js.map +7 -0
  18. package/dist/src/classes/index.js +2 -0
  19. package/dist/src/classes/index.js.map +7 -0
  20. package/dist/src/classes/state.js +2 -0
  21. package/dist/src/classes/state.js.map +7 -0
  22. package/dist/src/classes/steps/case.js +2 -0
  23. package/dist/src/classes/steps/case.js.map +7 -0
  24. package/dist/src/classes/steps/conditional_step.js +2 -0
  25. package/dist/src/classes/steps/conditional_step.js.map +7 -0
  26. package/dist/src/classes/steps/delay_step.js +2 -0
  27. package/dist/src/classes/steps/delay_step.js.map +7 -0
  28. package/dist/src/classes/steps/flow_control_step.js +2 -0
  29. package/dist/src/classes/steps/flow_control_step.js.map +7 -0
  30. package/dist/src/classes/steps/index.js +2 -0
  31. package/dist/src/classes/steps/index.js.map +7 -0
  32. package/dist/src/classes/steps/logic_step.js +2 -0
  33. package/dist/src/classes/steps/logic_step.js.map +7 -0
  34. package/dist/src/classes/steps/loop_step.js +2 -0
  35. package/dist/src/classes/steps/loop_step.js.map +7 -0
  36. package/dist/src/classes/steps/step.js +2 -0
  37. package/dist/src/classes/steps/step.js.map +7 -0
  38. package/dist/src/classes/steps/switch_step.js +2 -0
  39. package/dist/src/classes/steps/switch_step.js.map +7 -0
  40. package/dist/src/classes/workflow.js +2 -0
  41. package/dist/src/classes/workflow.js.map +7 -0
  42. package/dist/src/classes/workflow.test.js +2 -0
  43. package/dist/src/classes/workflow.test.js.map +7 -0
  44. package/dist/src/enums/base_types.js +2 -0
  45. package/dist/src/enums/base_types.js.map +7 -0
  46. package/dist/src/enums/conditional_step_comparators.js +2 -0
  47. package/dist/src/enums/conditional_step_comparators.js.map +7 -0
  48. package/dist/src/enums/delay_types.js +2 -0
  49. package/dist/src/enums/delay_types.js.map +7 -0
  50. package/dist/src/enums/errors.js +6 -0
  51. package/dist/src/enums/errors.js.map +7 -0
  52. package/dist/src/enums/flow_control_types.js +2 -0
  53. package/dist/src/enums/flow_control_types.js.map +7 -0
  54. package/dist/src/enums/index.js +2 -0
  55. package/dist/src/enums/index.js.map +7 -0
  56. package/dist/src/enums/logic_step_types.js +2 -0
  57. package/dist/src/enums/logic_step_types.js.map +7 -0
  58. package/dist/src/enums/loop_types.js +2 -0
  59. package/dist/src/enums/loop_types.js.map +7 -0
  60. package/dist/src/enums/state_event_names.js +2 -0
  61. package/dist/src/enums/state_event_names.js.map +7 -0
  62. package/dist/src/enums/step_event_names.js +2 -0
  63. package/dist/src/enums/step_event_names.js.map +7 -0
  64. package/dist/src/enums/step_statuses.js +2 -0
  65. package/dist/src/enums/step_statuses.js.map +7 -0
  66. package/dist/src/enums/step_types.js +2 -0
  67. package/dist/src/enums/step_types.js.map +7 -0
  68. package/dist/src/enums/sub_step_types.js +2 -0
  69. package/dist/src/enums/sub_step_types.js.map +7 -0
  70. package/dist/src/enums/workflow_event_names.js +2 -0
  71. package/dist/src/enums/workflow_event_names.js.map +7 -0
  72. package/dist/src/enums/workflow_statuses.js +2 -0
  73. package/dist/src/enums/workflow_statuses.js.map +7 -0
  74. package/package.json +10 -6
  75. package/src/classes/base.js +4 -5
  76. package/src/classes/events/event.js +16 -9
  77. package/src/classes/events/index.js +1 -1
  78. package/src/classes/events/state_event.js +28 -0
  79. package/src/classes/state.js +161 -54
  80. package/src/classes/steps/case.js +70 -0
  81. package/src/classes/steps/conditional_step.js +20 -7
  82. package/src/classes/steps/delay_step.js +103 -0
  83. package/src/classes/steps/flow_control_step.js +8 -7
  84. package/src/classes/steps/index.js +4 -0
  85. package/src/classes/steps/logic_step.js +89 -29
  86. package/src/classes/steps/loop_step.js +148 -0
  87. package/src/classes/steps/step.js +59 -18
  88. package/src/classes/steps/switch_step.js +77 -0
  89. package/src/classes/workflow.js +74 -26
  90. package/src/enums/conditional_step_comparators.js +38 -8
  91. package/src/enums/delay_types.js +0 -7
  92. package/src/enums/errors.js +2 -0
  93. package/src/enums/index.js +1 -0
  94. package/src/enums/loop_types.js +3 -1
  95. package/src/enums/state_event_names.js +17 -0
  96. package/src/enums/step_event_names.js +3 -0
  97. package/src/enums/step_types.js +2 -1
  98. package/src/enums/sub_step_types.js +15 -125
  99. package/src/enums/workflow_event_names.js +1 -0
  100. package/index.js +0 -1
  101. package/src/classes/events/broadcast.js +0 -57
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/enums/workflow_event_names.js"],
4
+ "sourcesContent": ["/**\n * Enumeration of workflow lifecycle event names.\n * Workflows emit these events during their creation and execution lifecycle.\n * \n * @enum {string}\n * @readonly\n */\nconst workflow_event_names = {\n WORKFLOW_BREAK_EXECUTED: 'workflow_break_executed',\n WORKFLOW_CANCELLED: 'workflow_cancelled',\n WORKFLOW_COMPLETE: 'workflow_complete',\n WORKFLOW_CREATED: 'workflow_created',\n WORKFLOW_ERRORED: 'workflow_errored',\n WORKFLOW_FAILED: 'workflow_failed',\n WORKFLOW_PAUSED: 'workflow_paused',\n WORKFLOW_RESUMED: 'workflow_resumed',\n WORKFLOW_RUNNING: 'workflow_running',\n WORKFLOW_STEP_ADDED: 'workflow_step_added',\n WORKFLOW_STEP_MOVED: 'workflow_step_moved',\n WORKFLOW_STEP_REMOVED: 'workflow_step_removed',\n WORKFLOW_STEP_SHIFTED: 'workflow_step_shifted',\n WORKFLOW_STEP_SKIPPED: 'workflow_step_skipped',\n WORKFLOW_STEPS_ADDED: 'workflow_steps_added',\n WORKFLOW_STEPS_CLEARED: 'workflow_steps_cleared',\n}\n\nexport default workflow_event_names;\n"],
5
+ "mappings": "AAOA,MAAMA,EAAuB,CAC3B,wBAAyB,0BACzB,mBAAoB,qBACpB,kBAAmB,oBACnB,iBAAkB,mBAClB,iBAAkB,mBAClB,gBAAiB,kBACjB,gBAAiB,kBACjB,iBAAkB,mBAClB,iBAAkB,mBAClB,oBAAqB,sBACrB,oBAAqB,sBACrB,sBAAuB,wBACvB,sBAAuB,wBACvB,sBAAuB,wBACvB,qBAAsB,uBACtB,uBAAwB,wBAC1B,EAEA,IAAOC,EAAQD",
6
+ "names": ["workflow_event_names", "workflow_event_names_default"]
7
+ }
@@ -0,0 +1,2 @@
1
+ const e={CANCELLED:"cancelled",COMPLETE:"complete",CREATED:"created",ERRORED:"errored",FROZEN:"frozen",FAILED:"failed",PAUSED:"paused",PENDING:"pending",RUNNING:"running",SKIPPED:"skipped"};var E=e;export{E as default};
2
+ //# sourceMappingURL=workflow_statuses.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/enums/workflow_statuses.js"],
4
+ "sourcesContent": ["/**\n * Enumeration of possible workflow execution statuses.\n * Workflows transition through these statuses during their lifecycle.\n * \n * @enum {string}\n * @readonly\n */\nconst workflow_statuses = {\n CANCELLED: 'cancelled',\n COMPLETE: 'complete',\n CREATED: 'created',\n ERRORED: 'errored',\n FROZEN: 'frozen',\n FAILED: 'failed',\n PAUSED: 'paused',\n PENDING: 'pending',\n RUNNING: 'running',\n SKIPPED: 'skipped'\n};\n\nexport default workflow_statuses;\n"],
5
+ "mappings": "AAOA,MAAMA,EAAoB,CACxB,UAAW,YACX,SAAU,WACV,QAAS,UACT,QAAS,UACT,OAAQ,SACR,OAAQ,SACR,OAAQ,SACR,QAAS,UACT,QAAS,UACT,QAAS,SACX,EAEA,IAAOC,EAAQD",
6
+ "names": ["workflow_statuses", "workflow_statuses_default"]
7
+ }
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "@ronaldroe/micro-flow",
3
- "version": "1.0.0",
3
+ "version": "1.2.1",
4
4
  "description": "A lightweight, flexible workflow orchestration library for Node.js and browser environments. Build complex, sequential processes with ease using an intuitive API that supports conditional logic, flow control, event handling, and state management.",
5
- "main": "index.js",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": "./dist/index.js"
8
+ },
6
9
  "scripts": {
7
10
  "build": "node build.js",
8
- "test": "echo \"Error: no test specified\" && exit 1",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest run --coverage",
9
14
  "prepublishOnly": "npm run build"
10
15
  },
11
16
  "keywords": [
@@ -32,7 +37,7 @@
32
37
  },
33
38
  "homepage": "https://github.com/starkeysoft/micro-flow#readme",
34
39
  "files": [
35
- "index.js",
40
+ "dist/",
36
41
  "src/",
37
42
  "README.md",
38
43
  "LICENSE"
@@ -42,8 +47,7 @@
42
47
  },
43
48
  "dependencies": {
44
49
  "date-fns": "^4.1.0",
45
- "node-schedule": "^2.1.1",
46
- "uuid": "^13.0.0"
50
+ "node-schedule": "^2.1.1"
47
51
  },
48
52
  "devDependencies": {
49
53
  "@vitest/coverage-v8": "^4.0.14",
@@ -1,4 +1,4 @@
1
- import { v4 as uuidv4 } from 'uuid';
1
+ import crypto from 'crypto';
2
2
  import { base_types } from '../enums/index.js';
3
3
  import State from './state.js';
4
4
 
@@ -15,14 +15,13 @@ export default class Base {
15
15
  * @param {string} [options.base_type=base_types.STEP] - Type of the base instance.
16
16
  */
17
17
  constructor({ name, base_type = base_types.STEP }) {
18
- this.id = uuidv4();
18
+ this.id = crypto.randomUUID();
19
19
  this.name = name ?? `${base_type}-${this.id}`;
20
20
 
21
21
  this.base_type = base_type;
22
22
  this.timing = {
23
23
  cancel_time: null,
24
24
  complete_time: null,
25
- end_time: null,
26
25
  execution_time_ms: null,
27
26
  start_time: null,
28
27
  }
@@ -54,7 +53,7 @@ export default class Base {
54
53
  }
55
54
 
56
55
  const logMessage = message ? `\n[${this.base_type.toUpperCase()} - ${this.name}] ${message}` : `\n[${this.base_type.toUpperCase()} - ${this.name}] Event: ${event_name}`;
57
- const logType = event_name.endsWith('_FAILED') ? 'error' : 'log';
56
+ const logType = event_name.endsWith('_failed') ? 'error' : 'log';
58
57
 
59
58
  console[logType](logMessage);
60
59
  }
@@ -105,7 +104,7 @@ export default class Base {
105
104
  * Marks the instance as running and sets the start time.
106
105
  */
107
106
  markAsRunning() {
108
- this.timing.start_time = new Date();
107
+ this.timing.start_time = this.timing.start_time ?? new Date();
109
108
  this.status = State.get('statuses')[this.base_type].RUNNING;
110
109
 
111
110
  this.log(
@@ -1,12 +1,11 @@
1
1
  import { errors, warnings } from '../../enums/index.js';
2
- import Broadcast from './broadcast.js';
3
2
 
4
3
  /**
5
4
  * Event class for micro-flow
6
5
  * Provides a simple event emitter implementation for workflow steps and state changes.
7
6
  *
8
7
  * This class is used for emitting and listening to events within workflows and steps.
9
- * For broadcasting events across multiple workflows or listeners, see the Broadcast class.
8
+ * For broadcasting events across multiple workflows or listeners, it uses BroadcastChannel.
10
9
  */
11
10
  class Event extends EventTarget {
12
11
  /**
@@ -50,9 +49,9 @@ class Event extends EventTarget {
50
49
  const result = this.dispatchEvent(custom_event);
51
50
 
52
51
  try {
53
- const channel = new Broadcast(event_name);
54
- channel.send(workingData);
55
- channel.destroy();
52
+ const channel = new BroadcastChannel(event_name);
53
+ channel.postMessage(workingData);
54
+ channel.close();
56
55
  } catch (e) {
57
56
  console.warn(warnings.BROADCAST_FAILED, e);
58
57
  }
@@ -63,11 +62,19 @@ class Event extends EventTarget {
63
62
  * Listen for broadcasts on a given event name (channel).
64
63
  * @param {string} event_name - The event name/channel to listen for.
65
64
  * @param {Function} listener - Callback for broadcasted data.
66
- * @returns {Broadcast} Returns the Broadcast instance for manual control.
65
+ * @returns {BroadcastChannel} Returns the channel with send() and destroy() aliases.
67
66
  */
68
67
  onBroadcast(event_name, listener) {
69
- const channel = new Broadcast(event_name);
70
- channel.onReceive(listener);
68
+ const channel = new BroadcastChannel(event_name);
69
+ channel.onmessage = (event) => {
70
+ listener(event.data);
71
+ };
72
+ channel.send = (data) => {
73
+ channel.postMessage(data);
74
+ };
75
+ channel.destroy = () => {
76
+ channel.close();
77
+ };
71
78
  return channel;
72
79
  }
73
80
 
@@ -75,7 +82,7 @@ class Event extends EventTarget {
75
82
  * Listen for both local and broadcast events.
76
83
  * @param {string} event_name - The event name/channel to listen for.
77
84
  * @param {Function} listener - Callback for event data.
78
- * @returns {Object} Returns { event: this, broadcast: Broadcast instance }
85
+ * @returns {Object} Returns { event: this, broadcast: BroadcastChannel }
79
86
  */
80
87
  onAny(event_name, listener) {
81
88
  this.on(event_name, listener);
@@ -1,4 +1,4 @@
1
- export { default as Broadcast } from './broadcast.js';
2
1
  export { default as Event } from './event.js';
2
+ export { default as StateEvent } from './state_event.js';
3
3
  export { default as StepEvent } from './step_event.js';
4
4
  export { default as WorkflowEvent } from './workflow_event.js';
@@ -0,0 +1,28 @@
1
+ import { Event } from './index.js';
2
+ import { state_event_names } from '../../enums/index.js';
3
+
4
+ /**
5
+ * Manages state-specific events by extending the base Event class.
6
+ * @class StateEvent
7
+ * @extends Event
8
+ */
9
+ export default class StateEvent extends Event {
10
+ event_names = state_event_names;
11
+
12
+ /**
13
+ * Creates a new StateEvent instance and registers all state events.
14
+ * @constructor
15
+ */
16
+ constructor() {
17
+ super();
18
+ this.registerStateEvents();
19
+ }
20
+
21
+ /**
22
+ * Registers all state event names defined in the state_event_names enum.
23
+ * @returns {void}
24
+ */
25
+ registerStateEvents() {
26
+ this.registerEvents(this.event_names);
27
+ }
28
+ }
@@ -1,8 +1,9 @@
1
1
  import { errors, warnings } from '../enums/errors.js';
2
- import { StepEvent, WorkflowEvent } from './events/index.js';
2
+ import { StepEvent, WorkflowEvent, StateEvent } from './events/index.js';
3
3
  import {
4
4
  base_types,
5
5
  conditional_step_comparators,
6
+ state_event_names,
6
7
  step_event_names,
7
8
  step_statuses,
8
9
  step_types,
@@ -11,7 +12,7 @@ import {
11
12
  workflow_statuses,
12
13
  } from '../enums/index.js';
13
14
 
14
- let state = {
15
+ const defaultState = {
15
16
  messages: {
16
17
  errors,
17
18
  warnings,
@@ -22,11 +23,13 @@ let state = {
22
23
  },
23
24
  event_names: {
24
25
  workflow: workflow_event_names,
25
- step: step_event_names
26
+ step: step_event_names,
27
+ state: state_event_names,
26
28
  },
27
29
  events: {
28
30
  workflow: new WorkflowEvent(),
29
- step: new StepEvent()
31
+ step: new StepEvent(),
32
+ state: new StateEvent(),
30
33
  },
31
34
  types: {
32
35
  base_types,
@@ -35,7 +38,13 @@ let state = {
35
38
  },
36
39
  workflows: {},
37
40
  conditional_step_comparators
38
- }
41
+ };
42
+
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;
39
48
 
40
49
  /**
41
50
  * Singleton class representing the global state for workflows, steps, and processes.
@@ -56,21 +65,62 @@ class State {
56
65
  if (!path) {
57
66
  throw new Error(errors.INVALID_STATE_PATH);
58
67
  }
59
-
60
- const parts = this.parsePath(path);
68
+
69
+ const parts = State.parsePath(path);
61
70
  let current = state;
62
-
71
+
63
72
  for (let i = 0; i < parts.length - 1; i++) {
64
73
  const part = parts[i];
65
-
74
+
66
75
  if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
67
76
  return;
68
77
  }
69
-
78
+
70
79
  current = current[part];
71
80
  }
72
-
81
+
73
82
  delete current[parts[parts.length - 1]];
83
+
84
+ events.state.emit(event_names.state.DELETED, { state });
85
+ }
86
+
87
+ /**
88
+ * Iterates over a collection (array or object) located at the specified state path,
89
+ * executing a callback function for each item.
90
+ *
91
+ * @param {string} path - The path of the state property to iterate over.
92
+ * @param {Function} callback - The function to execute for each item in the collection.
93
+ * @throws {Error} Throws if the state property at the path is not an array or object.
94
+ */
95
+ static async each(path, callback) {
96
+ const collection = State.get(path);
97
+
98
+ if (Array.isArray(collection)) {
99
+ for (const [index, item] of collection.entries()) {
100
+ events.state.emit(event_names.state.EACH, { state });
101
+ await callback(item, index);
102
+ }
103
+ } else if (
104
+ typeof collection === 'object' &&
105
+ Object.prototype.toString.call(collection) === '[object Object]'
106
+ ) {
107
+ for (const key of Object.keys(collection)) {
108
+ events.state.emit(event_names.state.EACH, { state });
109
+ await callback(collection[key], key);
110
+ }
111
+ } else {
112
+ throw new Error(errors.VALUE_NOT_ITERABLE);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Freezes the entire state object, making it immutable.
118
+ * @returns {void}
119
+ */
120
+ static freeze() {
121
+ const frozenState = Object.freeze(state);
122
+ events.state.emit(event_names.state.FROZEN, { state });
123
+ return frozenState;
74
124
  }
75
125
 
76
126
  /**
@@ -79,53 +129,94 @@ class State {
79
129
  * @param {string} path - The path of the state property to get. Supports both dot notation
80
130
  * (e.g., "user.profile.name") and bracket notation (e.g., "users[0].name" or "data['key-name']").
81
131
  * Special values:
82
- * - Falsy values (null, undefined, false): Returns entire state object
83
- * - "*" or "": Returns entire state object
132
+ * - Falsy values (null, undefined, false, ""): Returns entire state object
133
+ * - "*": Returns entire state object
84
134
  * @param {*} [defaultValue=null] - Default value to return if the path doesn't exist.
135
+ * @param {string} [type='string'] - The output type to convert the value to.
136
+ * Supported types: "string", "number", "boolean".
85
137
  * @returns {*} The value of the state property, or defaultValue if not found. null if not found
86
138
  * and no defaultValue provided.
139
+ * @throws {Error} Throws if the value cannot be converted to the specified type.
87
140
  */
88
- static get(path, defaultValue = null) {
141
+ static get(path, defaultValue = null, type = null) {
142
+ let gotten = state;
89
143
  if (!path || ['*', ''].includes(path)) {
90
- return state;
144
+ events.state.emit(event_names.state.GET, { state: gotten ?? defaultValue });
145
+ return gotten;
91
146
  }
92
147
 
93
- return this.getFromPropertyPath(path) ?? defaultValue;
94
- }
148
+ gotten = State.getFromPropertyPath(path, false) ?? defaultValue;
95
149
 
96
- /**
97
- * Gets the entire state object.
98
- * @returns {Object} The entire state object.
99
- */
100
- static getState() {
101
- return state;
150
+ if (type) {
151
+ try {
152
+ switch (type) {
153
+ case 'string':
154
+ gotten = String(gotten);
155
+ break;
156
+ case 'number':
157
+ gotten = Number(gotten);
158
+ break;
159
+ case 'boolean':
160
+ gotten = Boolean(gotten);
161
+ break;
162
+ default:
163
+ break;
164
+ }
165
+ } catch (error) {
166
+ console.error("Error converting state value: ", error);
167
+ }
168
+ }
169
+
170
+ events.state.emit(event_names.state.GET, { state: gotten });
171
+
172
+ return gotten ?? defaultValue;
102
173
  }
103
174
 
104
175
  /**
105
- * Sets the value of a state property using dot-notation or bracket-notation path access.
106
- * Creates intermediate objects if they don't exist.
176
+ * Resolves a nested property path within the state object.
177
+ * Supports both dot notation and bracket notation.
107
178
  *
108
- * @param {string} path - The path of the state property to set. Supports both dot notation
109
- * (e.g., "user.profile.name") and bracket notation (e.g., "users[0].name" or "data['key-name']").
110
- * @param {*} value - The value to set for the state property.
111
- * @returns {void}
112
- * @throws {Error} Throws if path is empty or invalid.
179
+ * @param {string} path - The path to the property (e.g., "user.profile.name", "users[0].name", "data['key-name']").
180
+ * @param {boolean} [emit=true] - Whether to emit the GET_FROM_PROPERTY_PATH event.
181
+ * @returns {*} The value at the specified path, or undefined if not found.
113
182
  */
114
- static set(path, value) {
115
- if (!path) {
116
- throw new Error(errors.INVALID_STATE_PATH);
183
+ static getFromPropertyPath(path, emit = true) {
184
+ const parts = State.parsePath(path);
185
+ let current = state;
186
+
187
+ for (const part of parts) {
188
+ if (current && Object.prototype.hasOwnProperty.call(current, part)) {
189
+ current = current[part];
190
+ } else {
191
+ return undefined;
192
+ }
193
+ }
194
+
195
+ if (emit) {
196
+ events.state.emit(event_names.state.GET_FROM_PROPERTY_PATH, { state });
117
197
  }
118
198
 
119
- this.setToPropertyPath(path, value);
199
+ return current;
200
+ }
201
+
202
+ /**
203
+ * Gets the entire state object.
204
+ * @returns {Object} The entire state object.
205
+ */
206
+ static getState() {
207
+ events.state.emit(event_names.state.GET_STATE, { state });
208
+ return state;
120
209
  }
121
210
 
122
211
  /**
123
212
  * Merges an object into the current State.
124
213
  * @param {Object} newState - The object to merge into the current State.
125
- * @returns {void}
214
+ * @returns {object} The updated state object.
126
215
  */
127
216
  static merge(newState) {
128
217
  state = { ...state, ...newState };
218
+ events.state.emit(event_names.state.MERGE, { state });
219
+ return state;
129
220
  }
130
221
 
131
222
  /**
@@ -146,25 +237,36 @@ class State {
146
237
  }
147
238
 
148
239
  /**
149
- * Resolves a nested property path within the state object.
150
- * Supports both dot notation and bracket notation.
151
- *
152
- * @param {string} path - The path to the property (e.g., "user.profile.name", "users[0].name", "data['key-name']").
153
- * @returns {*} The value at the specified path, or undefined if not found.
240
+ * Resets the state to its default values.
241
+ * @returns {object} The reset state object.
154
242
  */
155
- static getFromPropertyPath(path) {
156
- const parts = this.parsePath(path);
157
- let current = state;
243
+ static reset() {
244
+ state = {
245
+ ...defaultState,
246
+ workflows: {}, // Always create fresh to avoid shared reference mutation
247
+ };
248
+ events.state.emit(event_names.state.RESET, { state });
249
+ return state;
250
+ }
158
251
 
159
- for (const part of parts) {
160
- if (current && Object.prototype.hasOwnProperty.call(current, part)) {
161
- current = current[part];
162
- } else {
163
- return undefined;
164
- }
252
+ /**
253
+ * Sets the value of a state property using dot-notation or bracket-notation path access.
254
+ * Creates intermediate objects if they don't exist.
255
+ *
256
+ * @param {string} path - The path of the state property to set. Supports both dot notation
257
+ * (e.g., "user.profile.name") and bracket notation (e.g., "users[0].name" or "data['key-name']").
258
+ * @param {*} value - The value to set for the state property.
259
+ * @returns {void}
260
+ * @throws {Error} Throws if path is empty or invalid.
261
+ */
262
+ static set(path, value) {
263
+ if (!path) {
264
+ throw new Error(errors.INVALID_STATE_PATH);
165
265
  }
166
266
 
167
- return current;
267
+ events.state.emit(event_names.state.SET, { state });
268
+
269
+ State.setToPropertyPath(path, value, false);
168
270
  }
169
271
 
170
272
  /**
@@ -173,15 +275,16 @@ class State {
173
275
  *
174
276
  * @param {string} path - The path to the property (e.g., "user.profile.name", "users[0].name", "data['key-name']").
175
277
  * @param {*} value - The value to set at the specified path.
278
+ * @param {boolean} [emit=true] - Whether to emit the SET_TO_PROPERTY_PATH event.
176
279
  */
177
- static setToPropertyPath(path, value) {
178
- const parts = this.parsePath(path);
280
+ static setToPropertyPath(path, value, emit = true) {
281
+ const parts = State.parsePath(path);
179
282
  let current = state;
180
-
283
+
181
284
  for (let i = 0; i < parts.length - 1; i++) {
182
285
  const part = parts[i];
183
286
  const nextPart = parts[i + 1];
184
-
287
+
185
288
  if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
186
289
  // Determine if next part is an array index (numeric)
187
290
  const isNextPartNumeric = /^\d+$/.test(nextPart);
@@ -189,6 +292,10 @@ class State {
189
292
  }
190
293
  current = current[part];
191
294
  }
295
+
296
+ if (emit) {
297
+ events.state.emit(event_names.state.SET_TO_PROPERTY_PATH, { state });
298
+ }
192
299
 
193
300
  current[parts[parts.length - 1]] = value;
194
301
  }
@@ -0,0 +1,70 @@
1
+ import LogicStep from './logic_step.js';
2
+ import { conditional_step_comparators } from '../../enums/index.js';
3
+
4
+ /**
5
+ * Case class representing a single case in a switch statement.
6
+ * Used in conjunction with SwitchStep to create switch/case logic.
7
+ * @class Case
8
+ * @extends LogicStep
9
+ */
10
+ export default class Case extends LogicStep {
11
+ static step_name = 'case';
12
+
13
+ /**
14
+ * Creates a new Case instance.
15
+ * Note: Plain LogicStep instances can be used in place of Case, but they MUST have conditional.subject set.
16
+ * @param {Object} options - Configuration options.
17
+ * @param {string} [options.name] - Name of the case.
18
+ * @param {Object} [options.conditional] - Conditional configuration.
19
+ * @param {*|Function} [options.conditional.subject=null] - Subject to evaluate (typically set by SwitchStep). Can be a function.
20
+ * @param {conditional_step_comparators|string} [options.conditional.operator=null] - Comparison operator.
21
+ * @param {*|Function} [options.conditional.value=null] - Value to compare against. Can be a function that returns the value.
22
+ * @param {Function|Step|Workflow} [options.callable=async () => {}] - Function, Step, or Workflow to execute when case matches.
23
+ * @param {boolean} [options.force_subject_override=false] - Force override of subject even if already set.
24
+ */
25
+ constructor({
26
+ name,
27
+ conditional = {
28
+ subject: null,
29
+ operator: null,
30
+ value: null,
31
+ },
32
+ callable = async () => {},
33
+ force_subject_override = false,
34
+ }) {
35
+ super({
36
+ name,
37
+ step_type: Case.step_name,
38
+ callable,
39
+ });
40
+
41
+ this.conditional_config = conditional;
42
+ this.force_subject_override = force_subject_override;
43
+
44
+ this.is_matched = false;
45
+ }
46
+
47
+ /**
48
+ * Sets the switch subject from the parent SwitchStep.
49
+ * Automatically sets the conditional subject if not already set or if force_subject_override is true.
50
+ * @param {*} subject - The subject value from the SwitchStep.
51
+ * @throws {Error} If no subject is provided and conditional.subject is not set.
52
+ * @throws {Error} If the resulting conditional configuration is invalid.
53
+ */
54
+ set switch_subject(subject) {
55
+ const subjectProvided = subject !== null && subject !== undefined;
56
+ const hasExistingSubject = this.conditional_config.subject !== null && this.conditional_config.subject !== undefined;
57
+
58
+ if (!subjectProvided && !hasExistingSubject) {
59
+ throw new Error(`No subject set for case step: ${this.name}, using default equality check`);
60
+ }
61
+
62
+ if (subjectProvided && (!hasExistingSubject || this.force_subject_override)) {
63
+ this.conditional_config.subject = subject;
64
+ }
65
+
66
+ if (!this.conditionalIsValid()) {
67
+ throw new Error(`Invalid conditional configuration for case step: ${this.name}`);
68
+ }
69
+ }
70
+ }
@@ -1,4 +1,5 @@
1
- import { LogicStep } from './index.js';
1
+ import LogicStep from './logic_step.js';
2
+ import { conditional_step_comparators } from '../../enums/index.js';
2
3
 
3
4
  /**
4
5
  * ConditionalStep class for branching logic based on conditions.
@@ -13,9 +14,9 @@ export default class ConditionalStep extends LogicStep {
13
14
  * @param {Object} options - Configuration options.
14
15
  * @param {string} [options.name] - Name of the step.
15
16
  * @param {Object} [options.conditional] - Conditional configuration.
16
- * @param {*} [options.conditional.subject] - Subject to evaluate.
17
- * @param {string} [options.conditional.operator] - Comparison operator.
18
- * @param {*} [options.conditional.value] - Value to compare against.
17
+ * @param {*|Function} [options.conditional.subject] - Subject to evaluate. Can be a function that returns the value.
18
+ * @param {conditional_step_comparators|string} [options.conditional.operator] - Comparison operator.
19
+ * @param {*|Function} [options.conditional.value] - Value to compare against. Can be a function that returns the value.
19
20
  * @param {Function|Step|Workflow} [options.true_callable=async () => {}] - Callable to execute if condition is true.
20
21
  * @param {Function|Step|Workflow} [options.false_callable=async () => {}] - Callable to execute if condition is false.
21
22
  */
@@ -34,8 +35,18 @@ export default class ConditionalStep extends LogicStep {
34
35
  conditional
35
36
  });
36
37
 
37
- this.true_callable = true_callable;
38
- this.false_callable = false_callable;
38
+ // Bind function callables to this step instance for state access
39
+ if (typeof true_callable === 'function') {
40
+ this.true_callable = true_callable.bind(this);
41
+ } else {
42
+ this.true_callable = true_callable;
43
+ }
44
+
45
+ if (typeof false_callable === 'function') {
46
+ this.false_callable = false_callable.bind(this);
47
+ } else {
48
+ this.false_callable = false_callable;
49
+ }
39
50
 
40
51
  this.callable = this.conditional.bind(this);
41
52
  }
@@ -60,6 +71,7 @@ export default class ConditionalStep extends LogicStep {
60
71
  if (typeof true_callable === 'function') {
61
72
  result = await true_callable();
62
73
  } else {
74
+ true_callable.parentWorkflowId = this.parentWorkflowId;
63
75
  result = await true_callable.execute();
64
76
  }
65
77
  } else {
@@ -71,10 +83,11 @@ export default class ConditionalStep extends LogicStep {
71
83
  if (typeof false_callable === 'function') {
72
84
  result = await false_callable();
73
85
  } else {
86
+ false_callable.parentWorkflowId = this.parentWorkflowId;
74
87
  result = await false_callable.execute();
75
88
  }
76
89
  }
77
90
 
78
- return result;
91
+ return { message: `Conditional step ${this.name} completed`, result };
79
92
  }
80
93
  }