@ronaldroe/micro-flow 1.3.9 → 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.
- package/README.md +51 -10
- package/dist/src/classes/base.js +2 -2
- package/dist/src/classes/base.js.map +3 -3
- package/dist/src/classes/callable_registry.js +1 -1
- package/dist/src/classes/callable_registry.js.map +2 -2
- package/dist/src/classes/events/event.js +1 -1
- package/dist/src/classes/events/event.js.map +3 -3
- package/dist/src/classes/index.js +1 -1
- package/dist/src/classes/index.js.map +3 -3
- package/dist/src/classes/state.js +1 -1
- package/dist/src/classes/state.js.map +3 -3
- package/dist/src/classes/steps/case.js +1 -1
- package/dist/src/classes/steps/case.js.map +3 -3
- package/dist/src/classes/steps/conditional_step.js +1 -1
- package/dist/src/classes/steps/conditional_step.js.map +3 -3
- package/dist/src/classes/steps/delay_step.js +1 -1
- package/dist/src/classes/steps/delay_step.js.map +2 -2
- package/dist/src/classes/steps/flow_control_step.js +1 -1
- package/dist/src/classes/steps/flow_control_step.js.map +2 -2
- package/dist/src/classes/steps/logic_step.js +1 -1
- package/dist/src/classes/steps/logic_step.js.map +3 -3
- package/dist/src/classes/steps/loop_step.js +1 -1
- package/dist/src/classes/steps/loop_step.js.map +3 -3
- package/dist/src/classes/steps/step.js +1 -1
- package/dist/src/classes/steps/step.js.map +3 -3
- package/dist/src/classes/steps/switch_step.js +1 -1
- package/dist/src/classes/steps/switch_step.js.map +3 -3
- package/dist/src/classes/workflow.js +1 -1
- package/dist/src/classes/workflow.js.map +3 -3
- package/dist/src/enums/delay_types.js.map +1 -1
- package/dist/src/enums/logic_step_types.js.map +3 -3
- package/dist/src/enums/sub_step_types.js +1 -1
- package/dist/src/enums/sub_step_types.js.map +2 -2
- package/package.json +1 -1
- package/src/classes/base.js +42 -11
- package/src/classes/callable_registry.js +82 -0
- package/src/classes/events/event.js +3 -3
- package/src/classes/index.js +2 -1
- package/src/classes/state.js +278 -8
- package/src/classes/steps/case.js +34 -4
- package/src/classes/steps/conditional_step.js +69 -3
- package/src/classes/steps/delay_step.js +18 -0
- package/src/classes/steps/flow_control_step.js +18 -2
- package/src/classes/steps/logic_step.js +26 -8
- package/src/classes/steps/loop_step.js +88 -3
- package/src/classes/steps/step.js +228 -16
- package/src/classes/steps/switch_step.js +66 -8
- package/src/classes/workflow.js +237 -40
- package/src/enums/delay_types.js +1 -1
- package/src/enums/logic_step_types.js +2 -2
- package/src/enums/sub_step_types.js +10 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ronaldroe/micro-flow",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
package/src/classes/base.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { base_types } from '../enums/index.js';
|
|
3
|
-
import State from './state.js';
|
|
3
|
+
import State, { InstanceState } from './state.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Base class for workflows and steps.
|
|
@@ -13,12 +13,20 @@ export default class Base {
|
|
|
13
13
|
* @param {Object} options - Configuration options.
|
|
14
14
|
* @param {string} [options.name] - Name of the instance.
|
|
15
15
|
* @param {string} [options.base_type=base_types.STEP] - Type of the base instance.
|
|
16
|
+
* @param {boolean} [options.use_state_singleton=false] - Deprecated. When true, `getState`/`setState`/`deleteState`
|
|
17
|
+
* fall back to the process-wide `State` singleton instead of this instance's own state. `Workflow` passes this
|
|
18
|
+
* value down to every `Step` it owns, so it only needs to be set once, on the workflow.
|
|
19
|
+
* @param {InstanceState|null} [options.state=null] - The `InstanceState` this instance's `getState`/`setState`/
|
|
20
|
+
* `deleteState` calls should read and write. `Workflow` creates its own on construction and shares it with its
|
|
21
|
+
* `Step`s; a `Step` created standalone (not yet added to a workflow) gets its own until it's added to one.
|
|
16
22
|
*/
|
|
17
|
-
constructor({ name, base_type = base_types.STEP }) {
|
|
23
|
+
constructor({ name, base_type = base_types.STEP, use_state_singleton = false, state = null }) {
|
|
18
24
|
this.id = crypto.randomUUID();
|
|
19
25
|
this.name = name ?? `${base_type}-${this.id}`;
|
|
20
26
|
|
|
21
27
|
this.base_type = base_type;
|
|
28
|
+
this.use_state_singleton = use_state_singleton;
|
|
29
|
+
this.state = use_state_singleton ? null : (state ?? new InstanceState());
|
|
22
30
|
this.timing = {
|
|
23
31
|
cancel_time: null,
|
|
24
32
|
complete_time: null,
|
|
@@ -52,10 +60,10 @@ export default class Base {
|
|
|
52
60
|
return;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
63
|
+
const log_message = message ? `\n[${this.base_type.toUpperCase()} - ${this.name}] ${message}` : `\n[${this.base_type.toUpperCase()} - ${this.name}] Event: ${event_name}`;
|
|
64
|
+
const log_type = event_name.endsWith('_failed') ? 'error' : 'log';
|
|
57
65
|
|
|
58
|
-
console[
|
|
66
|
+
console[log_type](log_message);
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/**
|
|
@@ -115,28 +123,51 @@ export default class Base {
|
|
|
115
123
|
|
|
116
124
|
// State management methods
|
|
117
125
|
/**
|
|
118
|
-
* Gets a value from the
|
|
126
|
+
* Gets a value from this instance's own state (the `Workflow`'s state, shared with its `Step`s).
|
|
127
|
+
* Set `use_state_singleton: true` (on the owning `Workflow`) to instead read from the
|
|
128
|
+
* deprecated, process-wide `State` singleton.
|
|
119
129
|
* @param {string} path - Path to the state property.
|
|
120
130
|
* @returns {*} The state value at the specified path.
|
|
121
131
|
*/
|
|
122
132
|
getState(path) {
|
|
123
|
-
|
|
133
|
+
if (this.use_state_singleton) {
|
|
134
|
+
console.warn('The state singleton has been deprecated. Use the .prepareForSerialization() method on the workflow instance instead.');
|
|
135
|
+
return State.get(path);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return this.state.get(path);
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
/**
|
|
127
|
-
* Sets a value in the
|
|
142
|
+
* Sets a value in this instance's own state (the `Workflow`'s state, shared with its `Step`s).
|
|
143
|
+
* Set `use_state_singleton: true` (on the owning `Workflow`) to instead write to the
|
|
144
|
+
* deprecated, process-wide `State` singleton.
|
|
128
145
|
* @param {string} path - Path to the state property.
|
|
129
146
|
* @param {*} value - Value to set.
|
|
130
147
|
*/
|
|
131
148
|
setState(path, value) {
|
|
132
|
-
|
|
149
|
+
if (this.use_state_singleton) {
|
|
150
|
+
console.warn('The state singleton has been deprecated. Use the .prepareForSerialization() method on the workflow instance instead.');
|
|
151
|
+
State.set(path, value);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.state.set(path, value);
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
/**
|
|
136
|
-
* Deletes a property from the
|
|
159
|
+
* Deletes a property from this instance's own state (the `Workflow`'s state, shared with its `Step`s).
|
|
160
|
+
* Set `use_state_singleton: true` (on the owning `Workflow`) to instead delete from the
|
|
161
|
+
* deprecated, process-wide `State` singleton.
|
|
137
162
|
* @param {string} path - Path to the state property to delete.
|
|
138
163
|
*/
|
|
139
164
|
deleteState(path) {
|
|
140
|
-
|
|
165
|
+
if (this.use_state_singleton) {
|
|
166
|
+
console.warn('The state singleton has been deprecated. Use the .prepareForSerialization() method on the workflow instance instead.');
|
|
167
|
+
State.delete(path);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.state.delete(path);
|
|
141
172
|
}
|
|
142
173
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides a registry for callable functions to be used with persistence mode.
|
|
3
|
+
* This class allows you to register, retrieve, check for, and deregister callable functions by name.
|
|
4
|
+
*/
|
|
5
|
+
export default class CallableRegistry {
|
|
6
|
+
#registry;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registry for callable functions to be used with persistence mode.
|
|
10
|
+
*/
|
|
11
|
+
constructor() {
|
|
12
|
+
this.clear();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Clears all callable entries from the registry.
|
|
17
|
+
*/
|
|
18
|
+
clear() {
|
|
19
|
+
this.#registry = {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Removes a callable from the registry.
|
|
24
|
+
* @param {string} name - The name of the callable to remove.
|
|
25
|
+
* @throws Will throw an error if no callable is registered under the given name.
|
|
26
|
+
*/
|
|
27
|
+
deregister(name) {
|
|
28
|
+
if (!this.has(name)) {
|
|
29
|
+
throw new Error(`No callable registered under the name "${name}".`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
delete this.#registry[name];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Retrieves a callable from the registry.
|
|
37
|
+
* @param {string} name - The name of the callable to retrieve.
|
|
38
|
+
* @returns {Function} The callable function registered under the given name.
|
|
39
|
+
* @throws Will throw an error if no callable is registered under the given name.
|
|
40
|
+
*/
|
|
41
|
+
get(name) {
|
|
42
|
+
if (!this.has(name)) {
|
|
43
|
+
throw new Error(`No callable registered under the name "${name}".`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return this.#registry[name];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Checks if a callable is registered under the given name.
|
|
51
|
+
* @param {string} name - The name of the callable to check.
|
|
52
|
+
* @returns {boolean} True if a callable is registered under the given name, false otherwise.
|
|
53
|
+
*/
|
|
54
|
+
has(name) {
|
|
55
|
+
return Object.hasOwn(this.#registry, name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Registers a callable function under a given name.
|
|
60
|
+
* @param {string} name - The name to register the callable under.
|
|
61
|
+
* @param {Function} callable - The function to register as a callable.
|
|
62
|
+
* @throws Will throw an error if the provided callable is not a function.
|
|
63
|
+
*/
|
|
64
|
+
register(name, callable) {
|
|
65
|
+
if (typeof callable !== 'function') {
|
|
66
|
+
throw new Error('Only functions can be registered as callables.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.#registry[name] = callable;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Registers multiple callables from an object mapping names to functions.
|
|
74
|
+
* @param {Object} callables - An object where keys are names and values are functions to register.
|
|
75
|
+
* @throws Will throw an error if any of the provided callables is not a function.
|
|
76
|
+
*/
|
|
77
|
+
registerMany(callables) {
|
|
78
|
+
for (const [name, callable] of Object.entries(callables)) {
|
|
79
|
+
this.register(name, callable);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -40,7 +40,7 @@ class Event extends EventTarget {
|
|
|
40
40
|
*/
|
|
41
41
|
emit(event_name, data, bubbles = false, cancelable = true) {
|
|
42
42
|
const seen = new WeakSet();
|
|
43
|
-
const
|
|
43
|
+
const working_data = JSON.parse(JSON.stringify(data, (key, value) => {
|
|
44
44
|
if (typeof value === 'object' && value !== null) {
|
|
45
45
|
if (seen.has(value)) return undefined;
|
|
46
46
|
seen.add(value);
|
|
@@ -49,7 +49,7 @@ class Event extends EventTarget {
|
|
|
49
49
|
}));
|
|
50
50
|
|
|
51
51
|
const custom_event = new CustomEvent(event_name, {
|
|
52
|
-
detail:
|
|
52
|
+
detail: working_data,
|
|
53
53
|
bubbles,
|
|
54
54
|
cancelable
|
|
55
55
|
});
|
|
@@ -57,7 +57,7 @@ class Event extends EventTarget {
|
|
|
57
57
|
|
|
58
58
|
try {
|
|
59
59
|
const channel = new BroadcastChannel(event_name);
|
|
60
|
-
channel.postMessage(
|
|
60
|
+
channel.postMessage(working_data);
|
|
61
61
|
channel.close();
|
|
62
62
|
} catch (e) {
|
|
63
63
|
console.warn(warnings.BROADCAST_FAILED, e);
|
package/src/classes/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './events/index.js';
|
|
2
2
|
export { default as Base } from './base.js';
|
|
3
|
-
export { default as
|
|
3
|
+
export { default as CallableRegistry } from './callable_registry.js';
|
|
4
|
+
export { default as State, InstanceState } from './state.js';
|
|
4
5
|
export { default as Workflow } from './workflow.js';
|
|
5
6
|
export * from './steps/index.js';
|
package/src/classes/state.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
workflow_statuses,
|
|
13
13
|
} from '../enums/index.js';
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const default_state = {
|
|
16
16
|
messages: {
|
|
17
17
|
errors,
|
|
18
18
|
warnings,
|
|
@@ -40,12 +40,282 @@ const defaultState = {
|
|
|
40
40
|
conditional_step_comparators
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
let state = { ...
|
|
43
|
+
let state = { ...default_state };
|
|
44
44
|
|
|
45
45
|
// Module-level shortcuts for events and event_names
|
|
46
46
|
const events = state.events;
|
|
47
47
|
const event_names = state.event_names;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Parses a property path string into an array of keys, supporting both dot notation
|
|
51
|
+
* and bracket notation. Shared by both the deprecated singleton `State` and per-instance
|
|
52
|
+
* `InstanceState`.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} path - The path to parse (e.g., "user.profile.name", "users[0].name", "data['key-name']").
|
|
55
|
+
* @returns {string[]} Array of property keys.
|
|
56
|
+
*/
|
|
57
|
+
function parsePath(path) {
|
|
58
|
+
const matches = path.match(/[^.[\]]+|(?<=\[)([^\]]+)(?=\])/g);
|
|
59
|
+
|
|
60
|
+
if (!matches) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return matches.map(part => part.replace(/^['"]|['"]$/g, ''));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolves a nested property path within an arbitrary object.
|
|
69
|
+
* @param {Object} target - The object to read from.
|
|
70
|
+
* @param {string} path - The path to the property.
|
|
71
|
+
* @returns {*} The value at the specified path, or undefined if not found.
|
|
72
|
+
*/
|
|
73
|
+
function getAtPath(target, path) {
|
|
74
|
+
const parts = parsePath(path);
|
|
75
|
+
let current = target;
|
|
76
|
+
|
|
77
|
+
for (const part of parts) {
|
|
78
|
+
if (current && Object.prototype.hasOwnProperty.call(current, part)) {
|
|
79
|
+
current = current[part];
|
|
80
|
+
} else {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return current;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Sets a nested property value within an arbitrary object based on a path.
|
|
90
|
+
* Creates intermediate objects/arrays as needed.
|
|
91
|
+
* @param {Object} target - The object to write to.
|
|
92
|
+
* @param {string} path - The path to the property.
|
|
93
|
+
* @param {*} value - The value to set at the specified path.
|
|
94
|
+
*/
|
|
95
|
+
function setAtPath(target, path, value) {
|
|
96
|
+
const parts = parsePath(path);
|
|
97
|
+
let current = target;
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
100
|
+
const part = parts[i];
|
|
101
|
+
const next_part = parts[i + 1];
|
|
102
|
+
|
|
103
|
+
if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
|
|
104
|
+
const is_next_part_numeric = /^\d+$/.test(next_part);
|
|
105
|
+
current[part] = is_next_part_numeric ? [] : {};
|
|
106
|
+
}
|
|
107
|
+
current = current[part];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
current[parts[parts.length - 1]] = value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Deletes a property from an arbitrary object using a path.
|
|
115
|
+
* @param {Object} target - The object to delete from.
|
|
116
|
+
* @param {string} path - The path of the property to delete.
|
|
117
|
+
*/
|
|
118
|
+
function deleteAtPath(target, path) {
|
|
119
|
+
const parts = parsePath(path);
|
|
120
|
+
let current = target;
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
123
|
+
const part = parts[i];
|
|
124
|
+
|
|
125
|
+
if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
current = current[part];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
delete current[parts[parts.length - 1]];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Converts a resolved state value to the requested output type.
|
|
137
|
+
* @param {*} value - The value to convert.
|
|
138
|
+
* @param {string|null} type - One of "string", "number", "boolean".
|
|
139
|
+
* @returns {*} The converted value, or the original value if conversion fails or type is unrecognized.
|
|
140
|
+
*/
|
|
141
|
+
function convertType(value, type) {
|
|
142
|
+
if (!type) {
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
switch (type) {
|
|
148
|
+
case 'string':
|
|
149
|
+
return String(value);
|
|
150
|
+
case 'number':
|
|
151
|
+
return Number(value);
|
|
152
|
+
case 'boolean':
|
|
153
|
+
return Boolean(value);
|
|
154
|
+
default:
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Error converting state value: ', error);
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Builds a fresh data object for a `Workflow`/`Step` instance's own state, seeded with the
|
|
165
|
+
* same framework constants (statuses, event enums, event emitters, comparators) the deprecated
|
|
166
|
+
* `State` singleton used to provide - but with its own independent, empty `workflows` registry
|
|
167
|
+
* rather than sharing the process-wide one.
|
|
168
|
+
*
|
|
169
|
+
* The event emitters (and other constant objects) are shared by reference with the singleton's
|
|
170
|
+
* defaults so that `on()`/`off()` listeners registered via `State.get('events.workflow')` keep
|
|
171
|
+
* receiving events regardless of whether a given `Workflow`/`Step` has opted back into
|
|
172
|
+
* `use_state_singleton`.
|
|
173
|
+
*
|
|
174
|
+
* @returns {Object} A fresh instance-state data object.
|
|
175
|
+
*/
|
|
176
|
+
export function createInstanceStateData() {
|
|
177
|
+
return {
|
|
178
|
+
messages: default_state.messages,
|
|
179
|
+
statuses: default_state.statuses,
|
|
180
|
+
event_names: default_state.event_names,
|
|
181
|
+
events: default_state.events,
|
|
182
|
+
types: default_state.types,
|
|
183
|
+
conditional_step_comparators: default_state.conditional_step_comparators,
|
|
184
|
+
workflows: {},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Per-instance replacement for the deprecated `State` singleton. Each `Workflow` owns one of
|
|
190
|
+
* these (created in its constructor), and shares it with every `Step` added to it, so that
|
|
191
|
+
* `getState()`/`setState()`/`deleteState()` calls made anywhere in that workflow's tree read and
|
|
192
|
+
* write the same, workflow-scoped data instead of a single process-wide object.
|
|
193
|
+
*
|
|
194
|
+
* Supports the same dot-notation/bracket-notation path access as `State`.
|
|
195
|
+
*
|
|
196
|
+
* @class InstanceState
|
|
197
|
+
*/
|
|
198
|
+
export class InstanceState {
|
|
199
|
+
/**
|
|
200
|
+
* Creates a new InstanceState.
|
|
201
|
+
* @param {Object} [initial] - Initial data, defaults to a fresh `createInstanceStateData()` result.
|
|
202
|
+
*/
|
|
203
|
+
constructor(initial = createInstanceStateData()) {
|
|
204
|
+
this.data = initial;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Gets the value of a state property using dot-notation or bracket-notation path access.
|
|
209
|
+
* @param {string} path - The path of the state property to get. Falsy values, or "*", return the entire state.
|
|
210
|
+
* @param {*} [defaultValue=null] - Default value to return if the path doesn't exist.
|
|
211
|
+
* @param {string|null} [type=null] - The output type to convert the value to ("string", "number", "boolean").
|
|
212
|
+
* @returns {*} The value of the state property, or defaultValue if not found.
|
|
213
|
+
*/
|
|
214
|
+
get(path, defaultValue = null, type = null) {
|
|
215
|
+
if (!path || ['*', ''].includes(path)) {
|
|
216
|
+
return this.data ?? defaultValue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const gotten = getAtPath(this.data, path) ?? defaultValue;
|
|
220
|
+
|
|
221
|
+
return convertType(gotten, type) ?? defaultValue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Sets the value of a state property using dot-notation or bracket-notation path access.
|
|
226
|
+
* Creates intermediate objects if they don't exist.
|
|
227
|
+
* @param {string} path - The path of the state property to set.
|
|
228
|
+
* @param {*} value - The value to set for the state property.
|
|
229
|
+
* @throws {Error} Throws if path is empty or invalid.
|
|
230
|
+
*/
|
|
231
|
+
set(path, value) {
|
|
232
|
+
if (!path) {
|
|
233
|
+
throw new Error(errors.INVALID_STATE_PATH);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
setAtPath(this.data, path, value);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolves a nested property path within this instance's data. Low-level counterpart to
|
|
241
|
+
* `get()` - unlike `get()`, a falsy/`'*'` path is not special-cased to mean "entire state".
|
|
242
|
+
* @param {string} path - The path to the property.
|
|
243
|
+
* @returns {*} The value at the specified path, or undefined if not found.
|
|
244
|
+
*/
|
|
245
|
+
getStateFromPropertyPath(path) {
|
|
246
|
+
return getAtPath(this.data, path);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Parses a property path string into an array of keys, supporting both dot notation
|
|
251
|
+
* and bracket notation.
|
|
252
|
+
* @param {string} path - The path to parse.
|
|
253
|
+
* @returns {string[]} Array of property keys.
|
|
254
|
+
*/
|
|
255
|
+
parseStatePath(path) {
|
|
256
|
+
return parsePath(path);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Sets a nested property value within this instance's data based on a path. Low-level
|
|
261
|
+
* counterpart to `set()` - unlike `set()`, does not throw on an empty path.
|
|
262
|
+
* @param {string} path - The path to the property.
|
|
263
|
+
* @param {*} value - The value to set at the specified path.
|
|
264
|
+
*/
|
|
265
|
+
setStateToPropertyPath(path, value) {
|
|
266
|
+
setAtPath(this.data, path, value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Deletes a state property using dot-notation or bracket-notation path access.
|
|
271
|
+
* @param {string} path - The path of the state property to delete.
|
|
272
|
+
* @throws {Error} Throws if path is empty or invalid.
|
|
273
|
+
*/
|
|
274
|
+
delete(path) {
|
|
275
|
+
if (!path) {
|
|
276
|
+
throw new Error(errors.INVALID_STATE_PATH);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
deleteAtPath(this.data, path);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Merges an object into the current instance state.
|
|
284
|
+
* @param {Object} newState - The object to merge in.
|
|
285
|
+
* @returns {Object} The updated state data.
|
|
286
|
+
*/
|
|
287
|
+
merge(newState) {
|
|
288
|
+
this.data = { ...this.data, ...newState };
|
|
289
|
+
return this.data;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Iterates over a collection (array or object) located at the specified path,
|
|
294
|
+
* executing a callback function for each item.
|
|
295
|
+
* @param {string} path - The path of the property to iterate over.
|
|
296
|
+
* @param {Function} callback - The function to execute for each item in the collection.
|
|
297
|
+
* @throws {Error} Throws if the value at the path is not an array or object.
|
|
298
|
+
*/
|
|
299
|
+
async each(path, callback) {
|
|
300
|
+
const collection = this.get(path);
|
|
301
|
+
|
|
302
|
+
if (Array.isArray(collection)) {
|
|
303
|
+
for (const [index, item] of collection.entries()) {
|
|
304
|
+
await callback(item, index);
|
|
305
|
+
}
|
|
306
|
+
} else if (
|
|
307
|
+
typeof collection === 'object' &&
|
|
308
|
+
Object.prototype.toString.call(collection) === '[object Object]'
|
|
309
|
+
) {
|
|
310
|
+
for (const key of Object.keys(collection)) {
|
|
311
|
+
await callback(collection[key], key);
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
throw new Error(errors.VALUE_NOT_ITERABLE);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
49
319
|
/**
|
|
50
320
|
* Singleton class representing the global state for workflows, steps, and processes.
|
|
51
321
|
* Provides methods for managing state with getter/setter functionality, nested path access,
|
|
@@ -118,9 +388,9 @@ class State {
|
|
|
118
388
|
* @returns {void}
|
|
119
389
|
*/
|
|
120
390
|
static freeze() {
|
|
121
|
-
const
|
|
391
|
+
const frozen_state = Object.freeze(state);
|
|
122
392
|
events.state.emit(event_names.state.FROZEN, { state });
|
|
123
|
-
return
|
|
393
|
+
return frozen_state;
|
|
124
394
|
}
|
|
125
395
|
|
|
126
396
|
/**
|
|
@@ -242,7 +512,7 @@ class State {
|
|
|
242
512
|
*/
|
|
243
513
|
static reset() {
|
|
244
514
|
state = {
|
|
245
|
-
...
|
|
515
|
+
...default_state,
|
|
246
516
|
workflows: {}, // Always create fresh to avoid shared reference mutation
|
|
247
517
|
};
|
|
248
518
|
events.state.emit(event_names.state.RESET, { state });
|
|
@@ -283,12 +553,12 @@ class State {
|
|
|
283
553
|
|
|
284
554
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
285
555
|
const part = parts[i];
|
|
286
|
-
const
|
|
556
|
+
const next_part = parts[i + 1];
|
|
287
557
|
|
|
288
558
|
if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
|
|
289
559
|
// Determine if next part is an array index (numeric)
|
|
290
|
-
const
|
|
291
|
-
current[part] =
|
|
560
|
+
const is_next_part_numeric = /^\d+$/.test(next_part);
|
|
561
|
+
current[part] = is_next_part_numeric ? [] : {};
|
|
292
562
|
}
|
|
293
563
|
current = current[part];
|
|
294
564
|
}
|
|
@@ -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
|
|
56
|
-
const
|
|
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 (!
|
|
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 (
|
|
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);
|