@ronaldroe/micro-flow 1.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.
@@ -0,0 +1,143 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { base_types } from '../enums/index.js';
3
+ import State from './state.js';
4
+
5
+ /**
6
+ * Base class for workflows and steps.
7
+ * Provides common functionality for timing, status management, logging, and state access.
8
+ * @class Base
9
+ */
10
+ export default class Base {
11
+ /**
12
+ * Creates a new Base instance.
13
+ * @param {Object} options - Configuration options.
14
+ * @param {string} [options.name] - Name of the instance.
15
+ * @param {string} [options.base_type=base_types.STEP] - Type of the base instance.
16
+ */
17
+ constructor({ name, base_type = base_types.STEP }) {
18
+ this.id = uuidv4();
19
+ this.name = name ?? `${base_type}-${this.id}`;
20
+
21
+ this.base_type = base_type;
22
+ this.timing = {
23
+ cancel_time: null,
24
+ complete_time: null,
25
+ end_time: null,
26
+ execution_time_ms: null,
27
+ start_time: null,
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Executes the instance. Must be overridden by subclasses.
33
+ * @async
34
+ * @throws {Error} Throws if not implemented in subclass.
35
+ */
36
+ async execute() {
37
+ throw new Error('Execute method not implemented');
38
+ }
39
+
40
+ /**
41
+ * Logs an event and emits it to the appropriate event emitter.
42
+ * @param {string} event_name - Name of the event to log.
43
+ * @param {string} [message=null] - Optional message to log.
44
+ * @throws {Error} Throws if event name is invalid or event emitter not found.
45
+ */
46
+ log(event_name, message = null) {
47
+ if (!event_name || !State.get(`events.${this.base_type}`)) {
48
+ throw new Error('Invalid event name or event emitter not found');
49
+ }
50
+
51
+ State.get(`events.${this.base_type}`).emit(event_name, this);
52
+ if (State.get('log_suppress')) {
53
+ return;
54
+ }
55
+
56
+ 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';
58
+
59
+ console[logType](logMessage);
60
+ }
61
+
62
+ /**
63
+ * Marks the instance as complete and calculates execution time.
64
+ */
65
+ markAsComplete() {
66
+ this.timing.complete_time = new Date();
67
+ this.status = State.get('statuses')[this.base_type].COMPLETE;
68
+ this.timing.execution_time_ms = this.timing.complete_time - this.timing.start_time;
69
+
70
+ if (this.steps_by_id) {
71
+ delete this.steps_by_id;
72
+ }
73
+
74
+ this.log(
75
+ State.get(`event_names.${this.base_type}`)[`${this.base_type.toUpperCase()}_COMPLETE`],
76
+ `${this.base_type.charAt(0).toUpperCase() + this.base_type.slice(1)} "${this.name}" complete.`
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Marks the instance as failed and calculates execution time.
82
+ */
83
+ markAsFailed() {
84
+ this.timing.complete_time = new Date();
85
+ this.status = State.get('statuses')[this.base_type].FAILED;
86
+ this.timing.execution_time_ms = this.timing.complete_time - this.timing.start_time;
87
+
88
+ this.log(
89
+ State.get(`event_names.${this.base_type}`)[`${this.base_type.toUpperCase()}_FAILED`],
90
+ `${this.base_type.charAt(0).toUpperCase() + this.base_type.slice(1)} "${this.name}" failed.`
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Marks the instance as waiting. To be implemented by subclasses.
96
+ */
97
+ markAsWaiting() { }
98
+
99
+ /**
100
+ * Marks the instance as pending. To be implemented by subclasses.
101
+ */
102
+ markAsPending() { }
103
+
104
+ /**
105
+ * Marks the instance as running and sets the start time.
106
+ */
107
+ markAsRunning() {
108
+ this.timing.start_time = new Date();
109
+ this.status = State.get('statuses')[this.base_type].RUNNING;
110
+
111
+ this.log(
112
+ State.get(`event_names.${this.base_type}`)[`${this.base_type.toUpperCase()}_RUNNING`],
113
+ `${this.base_type.charAt(0).toUpperCase() + this.base_type.slice(1)} "${this.name}" started.`
114
+ );
115
+ }
116
+
117
+ // State management methods
118
+ /**
119
+ * Gets a value from the global state.
120
+ * @param {string} path - Path to the state property.
121
+ * @returns {*} The state value at the specified path.
122
+ */
123
+ getState(path) {
124
+ return State.get(path);
125
+ }
126
+
127
+ /**
128
+ * Sets a value in the global state.
129
+ * @param {string} path - Path to the state property.
130
+ * @param {*} value - Value to set.
131
+ */
132
+ setState(path, value) {
133
+ State.set(path, value);
134
+ }
135
+
136
+ /**
137
+ * Deletes a property from the global state.
138
+ * @param {string} path - Path to the state property to delete.
139
+ */
140
+ deleteState(path) {
141
+ State.delete(path);
142
+ }
143
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Broadcast class provides a simplified wrapper around the BroadcastChannel API
3
+ * for cross-context communication (e.g., between tabs, windows, workers).
4
+ *
5
+ * This class allows you to send and receive messages across different browsing contexts
6
+ * that share the same origin. It encapsulates the creation and management of a BroadcastChannel,
7
+ * providing a simplified API for sending and receiving messages.
8
+ *
9
+ * @class Broadcast
10
+ * @extends BroadcastChannel
11
+ */
12
+ export default class Broadcast extends BroadcastChannel {
13
+ /**
14
+ * Creates a new Broadcast instance for a named channel.
15
+ *
16
+ * @constructor
17
+ * @param {string} channelName - The name of the broadcast channel to create or connect to.
18
+ * Multiple Broadcast instances with the same channel name can communicate with each other.
19
+ */
20
+ constructor(channelName) {
21
+ super(channelName);
22
+ }
23
+
24
+ /**
25
+ * Sends data to all other contexts listening on this channel.
26
+ *
27
+ * @param {*} data - The data to broadcast. Can be any structured-cloneable value
28
+ * (primitives, objects, arrays, etc.). Functions and DOM nodes cannot be sent.
29
+ * @returns {void}
30
+ */
31
+ send(data) {
32
+ this.postMessage(data);
33
+ }
34
+
35
+ /**
36
+ * Registers a callback to handle incoming messages on this channel.
37
+ *
38
+ * @param {Function} callback - Function to call when a message is received.
39
+ * Receives the message data as its only parameter.
40
+ * @returns {void}
41
+ */
42
+ onReceive(callback) {
43
+ this.onmessage = (event) => {
44
+ callback(event.data);
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Closes the broadcast channel and releases its resources.
50
+ * After calling this method, the Broadcast instance can no longer send or receive messages.
51
+ *
52
+ * @returns {void}
53
+ */
54
+ destroy() {
55
+ this.close();
56
+ }
57
+ }
@@ -0,0 +1,144 @@
1
+ import { errors, warnings } from '../../enums/index.js';
2
+ import Broadcast from './broadcast.js';
3
+
4
+ /**
5
+ * Event class for micro-flow
6
+ * Provides a simple event emitter implementation for workflow steps and state changes.
7
+ *
8
+ * 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.
10
+ */
11
+ class Event extends EventTarget {
12
+ /**
13
+ * Creates a new Event instance.
14
+ * @constructor
15
+ */
16
+ constructor() {
17
+ super();
18
+ this.events = {};
19
+ this._listener_map = new Map();
20
+ }
21
+
22
+ /**
23
+ * Registers multiple events by creating Event instances for each event name.
24
+ * @param {Object} event_names - An object containing event name constants.
25
+ * @returns {void}
26
+ */
27
+ registerEvents(event_names) {
28
+ for (const event_name of Object.values(event_names)) {
29
+ this.events[event_name] = new Event();
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Emits a custom event with optional data payload.
35
+ * This method maintains API compatibility with EventEmitter while using CustomEvent.
36
+ * @param {string} event_name - The name of the event to emit.
37
+ * @param {*} [data] - Optional data to pass with the event in the detail property.
38
+ * @param {boolean} [bubbles=false] - Whether the event should bubble up through the DOM.
39
+ * @param {boolean} [cancelable=true] - Whether the event is cancelable.
40
+ * @returns {boolean} True if the event was not cancelled, false if it was cancelled.
41
+ */
42
+ emit(event_name, data, bubbles = false, cancelable = true) {
43
+ const workingData = JSON.parse(JSON.stringify(data));
44
+
45
+ const custom_event = new CustomEvent(event_name, {
46
+ detail: workingData,
47
+ bubbles,
48
+ cancelable
49
+ });
50
+ const result = this.dispatchEvent(custom_event);
51
+
52
+ try {
53
+ const channel = new Broadcast(event_name);
54
+ channel.send(workingData);
55
+ channel.destroy();
56
+ } catch (e) {
57
+ console.warn(warnings.BROADCAST_FAILED, e);
58
+ }
59
+ return result;
60
+ }
61
+
62
+ /**
63
+ * Listen for broadcasts on a given event name (channel).
64
+ * @param {string} event_name - The event name/channel to listen for.
65
+ * @param {Function} listener - Callback for broadcasted data.
66
+ * @returns {Broadcast} Returns the Broadcast instance for manual control.
67
+ */
68
+ onBroadcast(event_name, listener) {
69
+ const channel = new Broadcast(event_name);
70
+ channel.onReceive(listener);
71
+ return channel;
72
+ }
73
+
74
+ /**
75
+ * Listen for both local and broadcast events.
76
+ * @param {string} event_name - The event name/channel to listen for.
77
+ * @param {Function} listener - Callback for event data.
78
+ * @returns {Object} Returns { event: this, broadcast: Broadcast instance }
79
+ */
80
+ onAny(event_name, listener) {
81
+ this.on(event_name, listener);
82
+ const broadcast = this.onBroadcast(event_name, listener);
83
+ return { event: this, broadcast };
84
+ }
85
+
86
+ /**
87
+ * Adds an event listener with EventEmitter-style API.
88
+ * Maintains compatibility with the original API while using addEventListener.
89
+ * @param {string} event_name - The name of the event to listen for.
90
+ * @param {Function} listener - The callback function to execute when the event fires.
91
+ * @returns {Event} Returns this for chaining.
92
+ */
93
+ on(event_name, listener) {
94
+ const wrapped_listener = (event) => {
95
+ // Call the listener with the detail (data) from CustomEvent
96
+ listener(event.detail);
97
+ };
98
+ // Store the original listener reference for removeListener
99
+ this._listener_map.set(listener, wrapped_listener);
100
+ this.addEventListener(event_name, wrapped_listener);
101
+ return this;
102
+ }
103
+
104
+ /**
105
+ * Adds a one-time event listener with EventEmitter-style API.
106
+ * @param {string} event_name - The name of the event to listen for.
107
+ * @param {Function} listener - The callback function to execute when the event fires.
108
+ * @returns {Event} Returns this for chaining.
109
+ */
110
+ once(event_name, listener) {
111
+ const wrapped_listener = (event) => {
112
+ listener(event.detail);
113
+ };
114
+ this.addEventListener(event_name, wrapped_listener, { once: true });
115
+ return this;
116
+ }
117
+
118
+ /**
119
+ * Removes an event listener with EventEmitter-style API.
120
+ * @param {string} event_name - The name of the event.
121
+ * @param {Function} listener - The callback function to remove.
122
+ * @returns {Event} Returns this for chaining.
123
+ */
124
+ off(event_name, listener) {
125
+ if (this._listener_map && this._listener_map.has(listener)) {
126
+ const wrapped_listener = this._listener_map.get(listener);
127
+ this.removeEventListener(event_name, wrapped_listener);
128
+ this._listener_map.delete(listener);
129
+ }
130
+ return this;
131
+ }
132
+
133
+ /**
134
+ * Alias for off() to maintain EventEmitter API compatibility.
135
+ * @param {string} event_name - The name of the event.
136
+ * @param {Function} listener - The callback function to remove.
137
+ * @returns {Event} Returns this for chaining.
138
+ */
139
+ removeListener(event_name, listener) {
140
+ return this.off(event_name, listener);
141
+ }
142
+ }
143
+
144
+ export default Event;
@@ -0,0 +1,4 @@
1
+ export { default as Broadcast } from './broadcast.js';
2
+ export { default as Event } from './event.js';
3
+ export { default as StepEvent } from './step_event.js';
4
+ export { default as WorkflowEvent } from './workflow_event.js';
@@ -0,0 +1,28 @@
1
+ import { Event } from './index.js';
2
+ import { step_event_names } from '../../enums/index.js';
3
+
4
+ /**
5
+ * Manages step-specific events by extending the base Event class.
6
+ * @class StepEvent
7
+ * @extends Event
8
+ */
9
+ export default class StepEvent extends Event {
10
+ event_names = step_event_names;
11
+
12
+ /**
13
+ * Creates a new StepEvent instance and registers all step events.
14
+ * @constructor
15
+ */
16
+ constructor() {
17
+ super();
18
+ this.registerStepEvents();
19
+ }
20
+
21
+ /**
22
+ * Registers all step event names defined in the step_event_names enum.
23
+ * @returns {void}
24
+ */
25
+ registerStepEvents() {
26
+ this.registerEvents(this.event_names);
27
+ }
28
+ }
@@ -0,0 +1,28 @@
1
+ import { Event } from './index.js';
2
+ import { workflow_event_names } from '../../enums/index.js';
3
+
4
+ /**
5
+ * Manages workflow-specific events by extending the base Event class.
6
+ * @class WorkflowEvent
7
+ * @extends Event
8
+ */
9
+ export default class WorkflowEvent extends Event {
10
+ event_names = workflow_event_names;
11
+
12
+ /**
13
+ * Creates a new WorkflowEvent instance and registers all workflow events.
14
+ * @constructor
15
+ */
16
+ constructor() {
17
+ super();
18
+ this.registerWorkflowEvents();
19
+ }
20
+
21
+ /**
22
+ * Registers all workflow event names defined in the workflow_event_names enum.
23
+ * @returns {void}
24
+ */
25
+ registerWorkflowEvents() {
26
+ this.registerEvents(this.event_names);
27
+ }
28
+ }
@@ -0,0 +1,5 @@
1
+ export * from './events/index.js';
2
+ export { default as Base } from './base.js';
3
+ export { default as State } from './state.js';
4
+ export { default as Workflow } from './workflow.js';
5
+ export * from './steps/index.js';
@@ -0,0 +1,197 @@
1
+ import { errors, warnings } from '../enums/errors.js';
2
+ import { StepEvent, WorkflowEvent } from './events/index.js';
3
+ import {
4
+ base_types,
5
+ conditional_step_comparators,
6
+ step_event_names,
7
+ step_statuses,
8
+ step_types,
9
+ sub_step_types,
10
+ workflow_event_names,
11
+ workflow_statuses,
12
+ } from '../enums/index.js';
13
+
14
+ let state = {
15
+ messages: {
16
+ errors,
17
+ warnings,
18
+ },
19
+ statuses: {
20
+ workflow: workflow_statuses,
21
+ step: step_statuses
22
+ },
23
+ event_names: {
24
+ workflow: workflow_event_names,
25
+ step: step_event_names
26
+ },
27
+ events: {
28
+ workflow: new WorkflowEvent(),
29
+ step: new StepEvent()
30
+ },
31
+ types: {
32
+ base_types,
33
+ step_types,
34
+ sub_step_types,
35
+ },
36
+ workflows: {},
37
+ conditional_step_comparators
38
+ }
39
+
40
+ /**
41
+ * Singleton class representing the global state for workflows, steps, and processes.
42
+ * Provides methods for managing state with getter/setter functionality, nested path access,
43
+ * and immutability options. The state is shared across all workflow and step instances.
44
+ *
45
+ * @class State
46
+ */
47
+ class State {
48
+ /**
49
+ * Deletes a state property using dot-notation or bracket-notation path access.
50
+ *
51
+ * @param {string} path - The path of the state property to delete (e.g., "user.profile.email" or "users[0].email").
52
+ * @returns {void}
53
+ * @throws {Error} Throws if path is empty or invalid.
54
+ */
55
+ static delete(path) {
56
+ if (!path) {
57
+ throw new Error(errors.INVALID_STATE_PATH);
58
+ }
59
+
60
+ const parts = this.parsePath(path);
61
+ let current = state;
62
+
63
+ for (let i = 0; i < parts.length - 1; i++) {
64
+ const part = parts[i];
65
+
66
+ if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
67
+ return;
68
+ }
69
+
70
+ current = current[part];
71
+ }
72
+
73
+ delete current[parts[parts.length - 1]];
74
+ }
75
+
76
+ /**
77
+ * Gets the value of a state property using dot-notation or bracket-notation path access.
78
+ *
79
+ * @param {string} path - The path of the state property to get. Supports both dot notation
80
+ * (e.g., "user.profile.name") and bracket notation (e.g., "users[0].name" or "data['key-name']").
81
+ * Special values:
82
+ * - Falsy values (null, undefined, false): Returns entire state object
83
+ * - "*" or "": Returns entire state object
84
+ * @param {*} [defaultValue=null] - Default value to return if the path doesn't exist.
85
+ * @returns {*} The value of the state property, or defaultValue if not found. null if not found
86
+ * and no defaultValue provided.
87
+ */
88
+ static get(path, defaultValue = null) {
89
+ if (!path || ['*', ''].includes(path)) {
90
+ return state;
91
+ }
92
+
93
+ return this.getFromPropertyPath(path) ?? defaultValue;
94
+ }
95
+
96
+ /**
97
+ * Gets the entire state object.
98
+ * @returns {Object} The entire state object.
99
+ */
100
+ static getState() {
101
+ return state;
102
+ }
103
+
104
+ /**
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.
107
+ *
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.
113
+ */
114
+ static set(path, value) {
115
+ if (!path) {
116
+ throw new Error(errors.INVALID_STATE_PATH);
117
+ }
118
+
119
+ this.setToPropertyPath(path, value);
120
+ }
121
+
122
+ /**
123
+ * Merges an object into the current State.
124
+ * @param {Object} newState - The object to merge into the current State.
125
+ * @returns {void}
126
+ */
127
+ static merge(newState) {
128
+ state = { ...state, ...newState };
129
+ }
130
+
131
+ /**
132
+ * Parses a property path string into an array of keys, supporting both dot notation
133
+ * and bracket notation.
134
+ *
135
+ * @param {string} path - The path to parse (e.g., "user.profile.name", "users[0].name", "data['key-name']").
136
+ * @returns {string[]} Array of property keys.
137
+ */
138
+ static parsePath(path) {
139
+ const matches = path.match(/[^.[\]]+|(?<=\[)([^\]]+)(?=\])/g);
140
+
141
+ if (!matches) {
142
+ return [];
143
+ }
144
+
145
+ return matches.map(part => part.replace(/^['"]|['"]$/g, ''));
146
+ }
147
+
148
+ /**
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.
154
+ */
155
+ static getFromPropertyPath(path) {
156
+ const parts = this.parsePath(path);
157
+ let current = state;
158
+
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
+ }
165
+ }
166
+
167
+ return current;
168
+ }
169
+
170
+ /**
171
+ * Sets a nested property value within the state object based on a path.
172
+ * Supports both dot notation and bracket notation. Creates intermediate objects/arrays as needed.
173
+ *
174
+ * @param {string} path - The path to the property (e.g., "user.profile.name", "users[0].name", "data['key-name']").
175
+ * @param {*} value - The value to set at the specified path.
176
+ */
177
+ static setToPropertyPath(path, value) {
178
+ const parts = this.parsePath(path);
179
+ let current = state;
180
+
181
+ for (let i = 0; i < parts.length - 1; i++) {
182
+ const part = parts[i];
183
+ const nextPart = parts[i + 1];
184
+
185
+ if (!Object.prototype.hasOwnProperty.call(current, part) || typeof current[part] !== 'object') {
186
+ // Determine if next part is an array index (numeric)
187
+ const isNextPartNumeric = /^\d+$/.test(nextPart);
188
+ current[part] = isNextPartNumeric ? [] : {};
189
+ }
190
+ current = current[part];
191
+ }
192
+
193
+ current[parts[parts.length - 1]] = value;
194
+ }
195
+ }
196
+
197
+ export default State;
@@ -0,0 +1,80 @@
1
+ import { LogicStep } from './index.js';
2
+
3
+ /**
4
+ * ConditionalStep class for branching logic based on conditions.
5
+ * @class ConditionalStep
6
+ * @extends LogicStep
7
+ */
8
+ export default class ConditionalStep extends LogicStep {
9
+ static step_name = 'conditional';
10
+
11
+ /**
12
+ * Creates a new ConditionalStep instance.
13
+ * @param {Object} options - Configuration options.
14
+ * @param {string} [options.name] - Name of the step.
15
+ * @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.
19
+ * @param {Function|Step|Workflow} [options.true_callable=async () => {}] - Callable to execute if condition is true.
20
+ * @param {Function|Step|Workflow} [options.false_callable=async () => {}] - Callable to execute if condition is false.
21
+ */
22
+ constructor({
23
+ name,
24
+ conditional = {
25
+ subject: null,
26
+ operator: null,
27
+ value: null,
28
+ },
29
+ true_callable = async () => {},
30
+ false_callable = async () => {},
31
+ }) {
32
+ super({
33
+ name,
34
+ conditional
35
+ });
36
+
37
+ this.true_callable = true_callable;
38
+ this.false_callable = false_callable;
39
+
40
+ this.callable = this.conditional.bind(this);
41
+ }
42
+
43
+ /**
44
+ * Executes the appropriate branch based on the condition evaluation.
45
+ * @async
46
+ * @returns {Promise<*>} The result of the executed branch.
47
+ */
48
+ async conditional() {
49
+ const true_callable = this.true_callable;
50
+ const false_callable = this.false_callable;
51
+
52
+ let result = null;
53
+
54
+ if (this.checkCondition()) {
55
+ this.log(
56
+ this.getState('events.step.event_names.CONDITIONAL_TRUE_BRANCH_EXECUTED'),
57
+ `Condition met for step: ${this.name}, executing true branch`
58
+ );
59
+
60
+ if (typeof true_callable === 'function') {
61
+ result = await true_callable();
62
+ } else {
63
+ result = await true_callable.execute();
64
+ }
65
+ } else {
66
+ this.log(
67
+ this.getState('events.step.event_names.CONDITIONAL_FALSE_BRANCH_EXECUTED'),
68
+ `Condition not met for step: ${this.name}, executing false branch`
69
+ );
70
+
71
+ if (typeof false_callable === 'function') {
72
+ result = await false_callable();
73
+ } else {
74
+ result = await false_callable.execute();
75
+ }
76
+ }
77
+
78
+ return result;
79
+ }
80
+ }