@ronaldroe/micro-flow 1.3.1 → 1.3.3

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 CHANGED
@@ -1,18 +1,24 @@
1
- # Micro-Flow
1
+ # Micro-Flow: Stop fighting "Wall-of-Await" spaghetti.
2
2
 
3
- Micro-Flow is a simple, lightweight, cross platform (browser and runtime) logic orchestration library. Micro-Flow makes async logic flows first-class objects — named, observable, pauseable, and composable so a multi-step process is something you can reason about, monitor, and control, not just a wall of awaits.
3
+ Micro-Flow is a simple, lightweight, cross-platform logic orchestration library. It turns messy, imperative async chains into observable, resilient "logic flows" that run anywhere—from your React frontend to your Node.js backend.
4
+
5
+ ## Why Micro-Flow?
6
+
7
+ We've all been there: a 100-line async function that acts as a "black box" when it fails. You have to manually hard-code retries, timeouts, state logging, and progress tracking for every single task. It’s brittle, a nightmare to unit test, and impossible to pause or resume.
8
+
9
+ **Micro-Flow** makes your logic a first-class object. Instead of one giant function, you build a **Workflow** where every step is automatically tracked, timed, and controlled. It replaces "Try-Catch" boilerplate with professional orchestration.
4
10
 
5
11
  ## Features
6
12
 
7
- - 👀 **Observable** - Every step and logic flow emits lifecycle events (`STEP_FAILED`, `WORKFLOW_COMPLETE`, etc.) so you always know what's running, what finished, and what broke — no log-sprinkling required
8
- - ⏸️ **Pauseable & Resumeable** - Suspend a running logic flow mid-pipeline (e.g. waiting on user input or an external signal) and resume it without losing state
9
- - 🌿 **First-Class Branching** - `ConditionalStep` and `SwitchStep` keep branching logic out of your callables and in the logic flow structure where it belongs
10
- - 🎯 **Fine-Grained Flow Control** - Break out of or skip steps in a logic flow dynamically at runtime
11
- - 💾 **Shared State Singleton** - Steps communicate through a global `State` object with dot/bracket-notation path access no threading data through function arguments
12
- - 📢 **Cross-Tab/Worker Communication** - Events broadcast automatically via `BroadcastChannel`, reaching other tabs and workers with no extra wiring
13
- - 🌐 **Universal** - Works in Node.js (≥18) and all modern browsers
14
- - 🎨 **Framework Friendly** - Integrates seamlessly with React, Vue, your favorite framework, and vanilla JS
15
- - ⚡ **Minimal Dependencies** - Lightweight and focused
13
+ - 🔍 **Zero-Effort Observability** - Lifecycle events (`STEP_FAILED`, `WORKFLOW_COMPLETE`) emit automatically eliminate manual log-sprinkling.
14
+ - ⏸️ **Pause, Resume, & Rewind** - Suspend any logic flow mid-pipeline and resume it later without losing local state.
15
+ - 🌿 **Declarative Branching** - Use `ConditionalStep` and `SwitchStep` to keep complex branching logic out of your callables and in the workflow structure.
16
+ - 🎯 **Dynamic Flow Control** - Break out of or skip steps dynamically at runtime.
17
+ - 💾 **Namespaced State Management** - Access global state through a namespaced singleton with dot-notation supporteliminate data-threading through arguments.
18
+ - **Cross-Tab/Worker Sync** - Broadcast events automatically via `BroadcastChannel` to reach other tabs and workers with zero configuration.
19
+ - 🌍 **Isomorphic by Design** - Run the same API in Node.js (≥18) and all modern browsers.
20
+ - 🎨 **Framework Agnostic** - Integrate seamlessly with React, Vue, Svelte, or vanilla JS.
21
+ - ⚡ **Lightweight Core** - ESM-first design with minimal production dependencies.
16
22
 
17
23
  ## Installation
18
24
 
@@ -27,12 +33,12 @@ npm install --save micro-flow
27
33
  ```javascript
28
34
  import { Workflow, Step } from 'micro-flow';
29
35
 
30
- // Create a simple workflow
31
36
  const workflow = new Workflow({
32
37
  name: 'data-processor',
33
38
  steps: [
34
39
  new Step({
35
40
  name: 'fetch-data',
41
+ max_retries: 3, // Built-in resilience for flaky APIs
36
42
  callable: async () => {
37
43
  const response = await fetch('https://api.example.com/data');
38
44
  return response.json();
@@ -40,27 +46,34 @@ const workflow = new Workflow({
40
46
  }),
41
47
  new Step({
42
48
  name: 'process-data',
43
- callable: async () => {
44
- console.log('Processing data...');
45
- return { processed: true };
46
- }
49
+ callable: async () => ({ processed: true })
47
50
  }),
48
51
  new Step({
49
52
  name: 'save-results',
50
- callable: async () => {
51
- console.log('Saving results...');
52
- return { saved: true };
53
- }
53
+ callable: async () => ({ saved: true })
54
54
  })
55
55
  ]
56
56
  });
57
57
 
58
- // Execute the workflow
59
58
  const result = await workflow.execute();
60
- console.log('Workflow complete!', result.results);
61
59
  ```
62
60
 
63
- ### Browser Example
61
+ ### Feature Spotlight: Cross-Tab Sync
62
+ Trigger logic in one tab and react to it in another. Events sync across workers and browser windows automatically:
63
+
64
+ ```javascript
65
+ import { State } from 'micro-flow';
66
+
67
+ // Listen for updates from other tabs/workers
68
+ State.get('events.workflow').on('sync-event', (data) => {
69
+ updateUI(data);
70
+ });
71
+
72
+ // Broadcast to all other contexts
73
+ State.get('events.workflow').emit('sync-event', { status: 'updated' });
74
+ ```
75
+
76
+ ### Browser: Coordinating UI Logic
64
77
 
65
78
  ```javascript
66
79
  import { Workflow, Step } from './micro-flow.js';
@@ -96,7 +109,7 @@ document.getElementById('loadBtn').addEventListener('click', () => {
96
109
  });
97
110
  ```
98
111
 
99
- ### React Example
112
+ ### React: Decoupling Logic from Components
100
113
 
101
114
  ```javascript
102
115
  import { Workflow, Step, State } from './micro-flow.js';
@@ -110,12 +123,7 @@ function DataFetcher() {
110
123
  const workflow = new Workflow({
111
124
  name: 'fetch-workflow',
112
125
  steps: [
113
- new Step({
114
- name: 'start',
115
- callable: async () => {
116
- setLoading(true);
117
- }
118
- }),
126
+ new Step({ name: 'start', callable: async () => setLoading(true) }),
119
127
  new Step({
120
128
  name: 'fetch',
121
129
  callable: async () => {
@@ -124,12 +132,7 @@ function DataFetcher() {
124
132
  setData(json);
125
133
  }
126
134
  }),
127
- new Step({
128
- name: 'complete',
129
- callable: async () => {
130
- setLoading(false);
131
- }
132
- })
135
+ new Step({ name: 'complete', callable: async () => setLoading(false) })
133
136
  ]
134
137
  });
135
138
 
@@ -147,16 +150,13 @@ function DataFetcher() {
147
150
  }
148
151
  ```
149
152
 
150
- ### Vue Example
153
+ ### Vue: Clean Reactive Lifecycle
151
154
 
152
155
  ```vue
153
156
  <template>
154
- <div>
155
- <button @click="runWorkflow" :disabled="isRunning">
156
- {{ isRunning ? 'Processing...' : 'Run Workflow' }}
157
- </button>
158
- <p>{{ result }}</p>
159
- </div>
157
+ <button @click="runWorkflow" :disabled="isRunning">
158
+ {{ isRunning ? 'Processing...' : 'Run Workflow' }}
159
+ </button>
160
160
  </template>
161
161
 
162
162
  <script setup>
@@ -164,33 +164,26 @@ import { ref } from 'vue';
164
164
  import { Workflow, Step } from './micro-flow.js';
165
165
 
166
166
  const isRunning = ref(false);
167
- const result = ref('');
168
167
 
169
168
  const runWorkflow = async () => {
170
169
  const workflow = new Workflow({
171
170
  name: 'vue-workflow',
172
171
  steps: [
173
172
  new Step({
174
- name: 'step-1',
173
+ name: 'process',
175
174
  callable: async () => {
176
175
  isRunning.value = true;
177
- await new Promise(resolve => setTimeout(resolve, 1000));
178
- return 'Step 1 complete';
176
+ await doAsyncWork();
179
177
  }
180
178
  }),
181
179
  new Step({
182
- name: 'step-2',
183
- callable: async () => {
184
- await new Promise(resolve => setTimeout(resolve, 1000));
185
- return 'Step 2 complete';
186
- }
180
+ name: 'finalize',
181
+ callable: async () => { isRunning.value = false; }
187
182
  })
188
183
  ]
189
184
  });
190
185
 
191
- const workflowResult = await workflow.execute();
192
- result.value = 'Workflow complete!';
193
- isRunning.value = false;
186
+ await workflow.execute();
194
187
  };
195
188
  </script>
196
189
  ```
@@ -198,294 +191,53 @@ const runWorkflow = async () => {
198
191
  ## Core Concepts
199
192
 
200
193
  ### Workflows
201
-
202
- Workflows are primary structures that execute a series of steps in sequence. They provide:
203
-
204
- - Sequential step execution
205
- - Error handling with `exit_on_error` option
206
- - Pause and resume capabilities
207
- - Event emission for monitoring
208
- - Result collection
209
-
210
- ```javascript
211
- import { Workflow } from 'micro-flow';
212
-
213
- const workflow = new Workflow({
214
- name: 'my-workflow',
215
- exit_on_error: true, // Stop on first error
216
- steps: [/* array of steps */]
217
- });
218
- ```
194
+ Workflows execute a series of steps in sequence. Use them to manage:
195
+ - Sequential execution and error handling.
196
+ - Fine-grained pause and resume control.
197
+ - Event emission for real-time monitoring.
198
+ - Result aggregation and session tracking.
219
199
 
220
200
  ### Steps
221
-
222
- Steps are individual units of work that execute functions, other steps, or even entire workflows. Steps have retry and timeout mechanisms.
223
-
224
- ```javascript
225
- import { Step } from 'micro-flow';
226
-
227
- const step = new Step({
228
- name: 'my-step',
229
- callable: async () => {
230
- // Your async code here
231
- return result;
232
- }
233
- });
234
- ```
201
+ Orchestrate functions, other steps, or entire workflows as individual units of work. Every step includes built-in retry and timeout policies.
235
202
 
236
203
  ### Callables
237
-
238
- Most step types accept a `callable` parameter. Callables are the individual actions a step can take.
239
-
240
- A callable can be any async function, another step, or even a whole workflow. That flexibility allows for everything from very simple logic flows to large, modularized flows broken down into logical units for execution.
204
+ Define logic using callables. Assign any async function, step, or workflow to a step's `callable` parameter. This flexibility enables everything from simple logic chains to modularized, enterprise-scale flows.
241
205
 
242
206
  ### State Management
243
-
244
- Access global state across all workflows and steps:
207
+ Manage namespaced global state across all workflows and steps:
245
208
 
246
209
  ```javascript
247
210
  import { State } from 'micro-flow';
248
211
 
249
- // Set values
212
+ // Set and get values with dot-notation
250
213
  State.set('user.name', 'John Doe');
251
- State.set('config.timeout', 5000);
252
-
253
- // Get values
254
- const userName = State.get('user.name');
255
- const timeout = State.get('config.timeout', 3000); // with default
256
-
257
- // Delete values
258
- State.delete('user.name');
259
-
260
- // Merge objects into state
261
- State.merge({ settings: { theme: 'dark', lang: 'en' } });
262
-
263
- // Iterate over collections
264
- State.set('users', [{ name: 'Alice' }, { name: 'Bob' }]);
265
- State.each('users', (user, index) => {
266
- console.log(`User ${index}: ${user.name}`);
267
- });
268
-
269
- // Freeze state (make immutable)
270
- State.freeze();
214
+ const timeout = State.get('config.timeout', 3000);
271
215
 
272
- // Reset state to defaults
273
- State.reset();
216
+ // Merge or iterate over collections
217
+ State.merge({ settings: { theme: 'dark' } });
218
+ State.each('users', (user) => console.log(user.name));
274
219
  ```
275
220
 
276
221
  ### Events
277
-
278
- Listen to workflow, step, and state lifecycle events. You can do this using Node's EventEmitter syntax or the browser's CustomEvent syntax. Both work in any environment:
279
-
280
- ```javascript
281
- import { State } from 'micro-flow';
282
-
283
- const workflowEvents = State.get('events.workflow');
284
-
285
- workflowEvents.on('workflow_complete', (data) => {
286
- console.log(`Workflow ${data.name} completed in ${data.timing.execution_time_ms}ms`);
287
- });
288
-
289
- const stepEvents = State.get('events.step');
290
-
291
- stepEvents.on('step_failed', (data) => {
292
- console.error(`Step ${data.name} failed:`, data.errors);
293
- });
294
-
295
- const stateEvents = State.get('events.state');
296
-
297
- stateEvents.on('set', (data) => {
298
- console.log('State modified:', data.state);
299
- });
300
-
301
- stateEvents.on('deleted', (data) => {
302
- console.log('State property deleted');
303
- });
304
- ```
305
-
306
- ### Cross-Tab/Worker Communication
307
-
308
- Events broadcast automatically between browser tabs and windows or across workers when emitted, with no extra wiring needed:
309
-
310
- ```javascript
311
- import { State } from './micro-flow.js';
312
-
313
- // All events broadcast automatically via BroadcastChannel
314
- const event = State.get('events.workflow');
315
-
316
- // Send event to other tabs
317
- event.emit('my-event', { type: 'update', data: { userId: 123 } });
318
-
319
- // Receive events from other tabs
320
- event.on('my-event', (data) => {
321
- console.log('Message from another tab:', data);
322
- if (data.type === 'update') {
323
- updateUI(data.data);
324
- }
325
- });
326
- ```
222
+ Monitor lifecycle events for workflows, steps, and state. Use Node's EventEmitter syntax or the browser's CustomEvent syntax—both support all environments.
327
223
 
328
224
  ## Use Cases
329
225
 
330
- ### Backend (Node.js)
331
-
332
- - **Data Processing Pipelines** - ETL workflows, data transformation
333
- - **API Integrations** - Multi-step API calls with retry logic
334
- - **Task Automation** - Scheduled jobs, batch processing
335
- - **Microservices Orchestration** - Coordinate service calls
336
- - **Testing Workflows** - Integration test sequences
337
-
338
- ### Frontend (Browser)
339
-
340
- - **Multi-Step Forms** - Registration, checkout, surveys
341
- - **Data Fetching** - Sequential API calls with caching
342
- - **Animation Sequences** - Complex UI animations
343
- - **User Onboarding** - Step-by-step tutorials
344
- - **State Machines** - UI state management
345
- - **Cross-Tab Synchronization** - Auth state, shopping cart, notifications
346
- - **Real-Time Collaboration** - Multi-tab editing, shared state
347
-
348
- ## Advanced Examples
349
-
350
- ### Node.js - Data Pipeline with Error Handling
351
-
352
- ```javascript
353
- import { Workflow, Step, ConditionalStep, State } from 'micro-flow';
354
-
355
- const pipeline = new Workflow({
356
- name: 'data-pipeline',
357
- exit_on_error: false,
358
- steps: [
359
- new Step({
360
- name: 'extract',
361
- callable: async () => {
362
- const data = await fetchFromDatabase();
363
- State.set('pipeline.raw', data);
364
- return data;
365
- }
366
- }),
367
- new ConditionalStep({
368
- name: 'validate',
369
- conditional: {
370
- subject: State.get('pipeline.raw')?.length,
371
- operator: '>',
372
- value: 0
373
- },
374
- true_callable: async () => ({ valid: true }),
375
- false_callable: async () => {
376
- throw new Error('No data to process');
377
- }
378
- }),
379
- new Step({
380
- name: 'transform',
381
- callable: async () => {
382
- const raw = State.get('pipeline.raw');
383
- const transformed = raw.map(transform);
384
- State.set('pipeline.transformed', transformed);
385
- return transformed;
386
- }
387
- }),
388
- new Step({
389
- name: 'load',
390
- callable: async () => {
391
- const data = State.get('pipeline.transformed');
392
- await saveToDatabase(data);
393
- return { saved: data.length };
394
- }
395
- })
396
- ]
397
- });
398
-
399
- await pipeline.execute();
400
- ```
401
-
402
- ### Browser - Multi-Step Form with Validation
226
+ ### Power Backend Processes (Node.js)
227
+ - **Data Pipelines** - Build ETL and transformation workflows.
228
+ - **API Integrations** - Orchestrate multi-step API calls with built-in retries.
229
+ - **Automation** - Automate scheduled jobs and batch processing.
230
+ - **Microservices** - Coordinate complex service calls.
403
231
 
404
- ```javascript
405
- import { Workflow, ConditionalStep } from './micro-flow.js';
406
-
407
- function createFormWorkflow(formData) {
408
- return new Workflow({
409
- name: 'form-submission',
410
- steps: [
411
- new ConditionalStep({
412
- name: 'validate-email',
413
- conditional: {
414
- subject: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email),
415
- operator: '===',
416
- value: true
417
- },
418
- true_callable: async () => ({ valid: true }),
419
- false_callable: async () => {
420
- throw new Error('Invalid email');
421
- }
422
- }),
423
- new Step({
424
- name: 'submit',
425
- callable: async () => {
426
- const response = await fetch('/api/submit', {
427
- method: 'POST',
428
- body: JSON.stringify(formData)
429
- });
430
- return response.json();
431
- }
432
- }),
433
- new Step({
434
- name: 'show-success',
435
- callable: async () => {
436
- document.getElementById('message').textContent = 'Success!';
437
- }
438
- })
439
- ]
440
- });
441
- }
442
- ```
232
+ ### Enhance Frontend Logic (Browser)
233
+ - **Multi-Step UI** - Build registration flows and checkout wizards.
234
+ - **Data Fetching** - Coordinate sequential API calls with caching.
235
+ - **Animations** - Sequence complex UI animations.
236
+ - **State Sync** - Sync auth state and shopping carts across tabs instantly.
443
237
 
444
238
  ## Documentation
445
-
446
- Full documentation is available in the [docs](docs/) directory:
447
-
448
- - [API Documentation](docs/index.md) - Complete API reference
449
- - [Classes](docs/classes/) - Workflow, Step, State, and more
450
- - [Events](docs/classes/events/) - Event system documentation
451
- - [Enums](docs/enums/) - Status codes and constants
452
- - [Examples](docs/examples/) - Comprehensive examples
453
-
454
- ### Quick Links
455
-
456
- **Core Classes:**
239
+ Explore the full documentation in the [docs](docs/) directory:
240
+ - [API Reference](docs/index.md)
457
241
  - [Workflow API](docs/classes/workflow.md)
458
242
  - [Step API](docs/classes/steps/step.md)
459
243
  - [State Management](docs/classes/state.md)
460
-
461
- **Logic Steps:**
462
- - [LogicStep API](docs/classes/steps/logic_step.md)
463
- - [ConditionalStep API](docs/classes/steps/conditional_step.md)
464
- - [FlowControlStep API](docs/classes/steps/flow_control_step.md)
465
- - [CaseStep API](docs/classes/steps/case.md)
466
- - [SwitchStep API](docs/classes/steps/switch_step.md)
467
- - [LoopStep API](docs/classes/steps/loop_step.md)
468
- - [DelayStep API](docs/classes/steps/delay_step.md)
469
-
470
- **Events:**
471
- - [Event System](docs/classes/events/event.md)
472
- - [WorkflowEvent API](docs/classes/events/workflow_event.md)
473
- - [StepEvent API](docs/classes/events/step_event.md)
474
- - [StateEvent API](docs/classes/events/state_event.md)
475
-
476
-
477
- **Enumerations:**
478
- - [Base Types](docs/enums/base_types.md)
479
- - [Step Types](docs/enums/step_types.md)
480
- - [Sub Step Types](docs/enums/sub_step_types.md)
481
- - [Logic Step Types](docs/enums/logic_step_types.md)
482
- - [Conditional Step Comparators](docs/enums/conditional_step_comparators.md)
483
- - [Flow Control Types](docs/enums/flow_control_types.md)
484
- - [Step Statuses](docs/enums/step_statuses.md)
485
- - [Workflow Statuses](docs/enums/workflow_statuses.md)
486
- - [Step Event Names](docs/enums/step_event_names.md)
487
- - [Workflow Event Names](docs/enums/workflow_event_names.md)
488
- - [State Event Names](docs/enums/state_event_names.md)
489
- - [Delay Types](docs/enums/delay_types.md)
490
- - [Loop Types](docs/enums/loop_types.md)
491
- - [Errors and Warnings](docs/enums/errors.md)
@@ -1,2 +1,2 @@
1
- var l=Object.defineProperty;var a=(c,e)=>l(c,"name",{value:e,configurable:!0});import{warnings as p}from"../../enums/index.js";class o extends EventTarget{static{a(this,"Event")}constructor(){super(),this.events={},this._listener_map=new Map}registerEvents(e){for(const t of Object.values(e))this.events[t]=new o}emit(e,t,s=!1,n=!0){const i=JSON.parse(JSON.stringify(t)),h=new CustomEvent(e,{detail:i,bubbles:s,cancelable:n}),d=this.dispatchEvent(h);try{const r=new BroadcastChannel(e);r.postMessage(i),r.close()}catch(r){console.warn(p.BROADCAST_FAILED,r)}return d}onBroadcast(e,t){const s=new BroadcastChannel(e);return s.onmessage=n=>{t(n.data)},s.send=n=>{s.postMessage(n)},s.destroy=()=>{s.close()},s}onAny(e,t){this.on(e,t);const s=this.onBroadcast(e,t);return{event:this,broadcast:s}}on(e,t){const s=a(n=>{t(n.detail)},"wrapped_listener");return this._listener_map.set(t,s),this.addEventListener(e,s),this}once(e,t){const s=a(n=>{t(n.detail)},"wrapped_listener");return this.addEventListener(e,s,{once:!0}),this}off(e,t){if(this._listener_map&&this._listener_map.has(t)){const s=this._listener_map.get(t);this.removeEventListener(e,s),this._listener_map.delete(t)}return this}removeListener(e,t){return this.off(e,t)}}var v=o;export{v as default};
1
+ var u=Object.defineProperty;var a=(d,e)=>u(d,"name",{value:e,configurable:!0});import{warnings as f}from"../../enums/index.js";class i extends EventTarget{static{a(this,"Event")}constructor(){super(),this.events={},this._listener_map=new Map}registerEvents(e){for(const t of Object.values(e))this.events[t]=new i}emit(e,t,s=!1,n=!0){const c=new WeakSet,h=JSON.parse(JSON.stringify(t,(r,o)=>{if(typeof o=="object"&&o!==null){if(c.has(o))return;c.add(o)}return o})),p=new CustomEvent(e,{detail:h,bubbles:s,cancelable:n}),l=this.dispatchEvent(p);try{const r=new BroadcastChannel(e);r.postMessage(h),r.close()}catch(r){console.warn(f.BROADCAST_FAILED,r)}return l}onBroadcast(e,t){const s=new BroadcastChannel(e);return s.onmessage=n=>{t(n.data)},s.send=n=>{s.postMessage(n)},s.destroy=()=>{s.close()},s}onAny(e,t){this.on(e,t);const s=this.onBroadcast(e,t);return{event:this,broadcast:s}}on(e,t){const s=a(n=>{t(n.detail)},"wrapped_listener");return this._listener_map.set(t,s),this.addEventListener(e,s),this}once(e,t){const s=a(n=>{t(n.detail)},"wrapped_listener");return this.addEventListener(e,s,{once:!0}),this}off(e,t){if(this._listener_map&&this._listener_map.has(t)){const s=this._listener_map.get(t);this.removeEventListener(e,s),this._listener_map.delete(t)}return this}removeListener(e,t){return this.off(e,t)}}var g=i;export{g as default};
2
2
  //# sourceMappingURL=event.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/classes/events/event.js"],
4
- "sourcesContent": ["import { errors, warnings } from '../../enums/index.js';\n\n/**\n * Event class for micro-flow\n * Provides a simple event emitter implementation for workflow steps and state changes.\n *\n * This class is used for emitting and listening to events within workflows and steps.\n * For broadcasting events across multiple workflows or listeners, it uses BroadcastChannel.\n */\nclass Event extends EventTarget {\n /**\n * Creates a new Event instance.\n * @constructor\n */\n constructor() {\n super();\n this.events = {};\n this._listener_map = new Map();\n }\n\n /**\n * Registers multiple events by creating Event instances for each event name.\n * @param {Object} event_names - An object containing event name constants.\n * @returns {void}\n */\n registerEvents(event_names) {\n for (const event_name of Object.values(event_names)) {\n this.events[event_name] = new Event();\n }\n }\n\n /**\n * Emits a custom event with optional data payload.\n * This method maintains API compatibility with EventEmitter while using CustomEvent.\n * @param {string} event_name - The name of the event to emit.\n * @param {*} [data] - Optional data to pass with the event in the detail property.\n * @param {boolean} [bubbles=false] - Whether the event should bubble up through the DOM.\n * @param {boolean} [cancelable=true] - Whether the event is cancelable.\n * @returns {boolean} True if the event was not cancelled, false if it was cancelled.\n */\n emit(event_name, data, bubbles = false, cancelable = true) {\n const workingData = JSON.parse(JSON.stringify(data));\n\n const custom_event = new CustomEvent(event_name, {\n detail: workingData,\n bubbles,\n cancelable\n });\n const result = this.dispatchEvent(custom_event);\n\n try {\n const channel = new BroadcastChannel(event_name);\n channel.postMessage(workingData);\n channel.close();\n } catch (e) {\n console.warn(warnings.BROADCAST_FAILED, e);\n }\n return result;\n }\n\n /**\n * Listen for broadcasts on a given event name (channel).\n * @param {string} event_name - The event name/channel to listen for.\n * @param {Function} listener - Callback for broadcasted data.\n * @returns {BroadcastChannel} Returns the channel with send() and destroy() aliases.\n */\n onBroadcast(event_name, listener) {\n const channel = new BroadcastChannel(event_name);\n channel.onmessage = (event) => {\n listener(event.data);\n };\n channel.send = (data) => {\n channel.postMessage(data);\n };\n channel.destroy = () => {\n channel.close();\n };\n return channel;\n }\n\n /**\n * Listen for both local and broadcast events.\n * @param {string} event_name - The event name/channel to listen for.\n * @param {Function} listener - Callback for event data.\n * @returns {Object} Returns { event: this, broadcast: BroadcastChannel }\n */\n onAny(event_name, listener) {\n this.on(event_name, listener);\n const broadcast = this.onBroadcast(event_name, listener);\n return { event: this, broadcast };\n }\n\n /**\n * Adds an event listener with EventEmitter-style API.\n * Maintains compatibility with the original API while using addEventListener.\n * @param {string} event_name - The name of the event to listen for.\n * @param {Function} listener - The callback function to execute when the event fires.\n * @returns {Event} Returns this for chaining.\n */\n on(event_name, listener) {\n const wrapped_listener = (event) => {\n // Call the listener with the detail (data) from CustomEvent\n listener(event.detail);\n };\n // Store the original listener reference for removeListener\n this._listener_map.set(listener, wrapped_listener);\n this.addEventListener(event_name, wrapped_listener);\n return this;\n }\n\n /**\n * Adds a one-time event listener with EventEmitter-style API.\n * @param {string} event_name - The name of the event to listen for.\n * @param {Function} listener - The callback function to execute when the event fires.\n * @returns {Event} Returns this for chaining.\n */\n once(event_name, listener) {\n const wrapped_listener = (event) => {\n listener(event.detail);\n };\n this.addEventListener(event_name, wrapped_listener, { once: true });\n return this;\n }\n\n /**\n * Removes an event listener with EventEmitter-style API.\n * @param {string} event_name - The name of the event.\n * @param {Function} listener - The callback function to remove.\n * @returns {Event} Returns this for chaining.\n */\n off(event_name, listener) {\n if (this._listener_map && this._listener_map.has(listener)) {\n const wrapped_listener = this._listener_map.get(listener);\n this.removeEventListener(event_name, wrapped_listener);\n this._listener_map.delete(listener);\n }\n return this;\n }\n\n /**\n * Alias for off() to maintain EventEmitter API compatibility.\n * @param {string} event_name - The name of the event.\n * @param {Function} listener - The callback function to remove.\n * @returns {Event} Returns this for chaining.\n */\n removeListener(event_name, listener) {\n return this.off(event_name, listener);\n }\n}\n\nexport default Event;\n"],
5
- "mappings": "+EAAA,OAAiB,YAAAA,MAAgB,uBASjC,MAAMC,UAAc,WAAY,CAThC,MASgC,CAAAC,EAAA,cAK9B,aAAc,CACZ,MAAM,EACN,KAAK,OAAS,CAAC,EACf,KAAK,cAAgB,IAAI,GAC3B,CAOA,eAAeC,EAAa,CAC1B,UAAWC,KAAc,OAAO,OAAOD,CAAW,EAChD,KAAK,OAAOC,CAAU,EAAI,IAAIH,CAElC,CAWA,KAAKG,EAAYC,EAAMC,EAAU,GAAOC,EAAa,GAAM,CACzD,MAAMC,EAAc,KAAK,MAAM,KAAK,UAAUH,CAAI,CAAC,EAE7CI,EAAe,IAAI,YAAYL,EAAY,CAC/C,OAAQI,EACR,QAAAF,EACA,WAAAC,CACF,CAAC,EACKG,EAAS,KAAK,cAAcD,CAAY,EAE9C,GAAI,CACF,MAAME,EAAU,IAAI,iBAAiBP,CAAU,EAC/CO,EAAQ,YAAYH,CAAW,EAC/BG,EAAQ,MAAM,CAChB,OAASC,EAAG,CACV,QAAQ,KAAKZ,EAAS,iBAAkBY,CAAC,CAC3C,CACA,OAAOF,CACT,CAQA,YAAYN,EAAYS,EAAU,CAChC,MAAMF,EAAU,IAAI,iBAAiBP,CAAU,EAC/C,OAAAO,EAAQ,UAAaG,GAAU,CAC7BD,EAASC,EAAM,IAAI,CACrB,EACAH,EAAQ,KAAQN,GAAS,CACvBM,EAAQ,YAAYN,CAAI,CAC1B,EACAM,EAAQ,QAAU,IAAM,CACtBA,EAAQ,MAAM,CAChB,EACOA,CACT,CAQA,MAAMP,EAAYS,EAAU,CAC1B,KAAK,GAAGT,EAAYS,CAAQ,EAC5B,MAAME,EAAY,KAAK,YAAYX,EAAYS,CAAQ,EACvD,MAAO,CAAE,MAAO,KAAM,UAAAE,CAAU,CAClC,CASA,GAAGX,EAAYS,EAAU,CACvB,MAAMG,EAAmBd,EAACY,GAAU,CAElCD,EAASC,EAAM,MAAM,CACvB,EAHyB,oBAKzB,YAAK,cAAc,IAAID,EAAUG,CAAgB,EACjD,KAAK,iBAAiBZ,EAAYY,CAAgB,EAC3C,IACT,CAQA,KAAKZ,EAAYS,EAAU,CACzB,MAAMG,EAAmBd,EAACY,GAAU,CAClCD,EAASC,EAAM,MAAM,CACvB,EAFyB,oBAGzB,YAAK,iBAAiBV,EAAYY,EAAkB,CAAE,KAAM,EAAK,CAAC,EAC3D,IACT,CAQA,IAAIZ,EAAYS,EAAU,CACxB,GAAI,KAAK,eAAiB,KAAK,cAAc,IAAIA,CAAQ,EAAG,CAC1D,MAAMG,EAAmB,KAAK,cAAc,IAAIH,CAAQ,EACxD,KAAK,oBAAoBT,EAAYY,CAAgB,EACrD,KAAK,cAAc,OAAOH,CAAQ,CACpC,CACA,OAAO,IACT,CAQA,eAAeT,EAAYS,EAAU,CACnC,OAAO,KAAK,IAAIT,EAAYS,CAAQ,CACtC,CACF,CAEA,IAAOI,EAAQhB",
6
- "names": ["warnings", "Event", "__name", "event_names", "event_name", "data", "bubbles", "cancelable", "workingData", "custom_event", "result", "channel", "e", "listener", "event", "broadcast", "wrapped_listener", "event_default"]
4
+ "sourcesContent": ["import { errors, warnings } from '../../enums/index.js';\n\n/**\n * Event class for micro-flow\n * Provides a simple event emitter implementation for workflow steps and state changes.\n *\n * This class is used for emitting and listening to events within workflows and steps.\n * For broadcasting events across multiple workflows or listeners, it uses BroadcastChannel.\n */\nclass Event extends EventTarget {\n /**\n * Creates a new Event instance.\n * @constructor\n */\n constructor() {\n super();\n this.events = {};\n this._listener_map = new Map();\n }\n\n /**\n * Registers multiple events by creating Event instances for each event name.\n * @param {Object} event_names - An object containing event name constants.\n * @returns {void}\n */\n registerEvents(event_names) {\n for (const event_name of Object.values(event_names)) {\n this.events[event_name] = new Event();\n }\n }\n\n /**\n * Emits a custom event with optional data payload.\n * This method maintains API compatibility with EventEmitter while using CustomEvent.\n * @param {string} event_name - The name of the event to emit.\n * @param {*} [data] - Optional data to pass with the event in the detail property.\n * @param {boolean} [bubbles=false] - Whether the event should bubble up through the DOM.\n * @param {boolean} [cancelable=true] - Whether the event is cancelable.\n * @returns {boolean} True if the event was not cancelled, false if it was cancelled.\n */\n emit(event_name, data, bubbles = false, cancelable = true) {\n const seen = new WeakSet();\n const workingData = JSON.parse(JSON.stringify(data, (key, value) => {\n if (typeof value === 'object' && value !== null) {\n if (seen.has(value)) return undefined;\n seen.add(value);\n }\n return value;\n }));\n\n const custom_event = new CustomEvent(event_name, {\n detail: workingData,\n bubbles,\n cancelable\n });\n const result = this.dispatchEvent(custom_event);\n\n try {\n const channel = new BroadcastChannel(event_name);\n channel.postMessage(workingData);\n channel.close();\n } catch (e) {\n console.warn(warnings.BROADCAST_FAILED, e);\n }\n return result;\n }\n\n /**\n * Listen for broadcasts on a given event name (channel).\n * @param {string} event_name - The event name/channel to listen for.\n * @param {Function} listener - Callback for broadcasted data.\n * @returns {BroadcastChannel} Returns the channel with send() and destroy() aliases.\n */\n onBroadcast(event_name, listener) {\n const channel = new BroadcastChannel(event_name);\n channel.onmessage = (event) => {\n listener(event.data);\n };\n channel.send = (data) => {\n channel.postMessage(data);\n };\n channel.destroy = () => {\n channel.close();\n };\n return channel;\n }\n\n /**\n * Listen for both local and broadcast events.\n * @param {string} event_name - The event name/channel to listen for.\n * @param {Function} listener - Callback for event data.\n * @returns {Object} Returns { event: this, broadcast: BroadcastChannel }\n */\n onAny(event_name, listener) {\n this.on(event_name, listener);\n const broadcast = this.onBroadcast(event_name, listener);\n return { event: this, broadcast };\n }\n\n /**\n * Adds an event listener with EventEmitter-style API.\n * Maintains compatibility with the original API while using addEventListener.\n * @param {string} event_name - The name of the event to listen for.\n * @param {Function} listener - The callback function to execute when the event fires.\n * @returns {Event} Returns this for chaining.\n */\n on(event_name, listener) {\n const wrapped_listener = (event) => {\n // Call the listener with the detail (data) from CustomEvent\n listener(event.detail);\n };\n // Store the original listener reference for removeListener\n this._listener_map.set(listener, wrapped_listener);\n this.addEventListener(event_name, wrapped_listener);\n return this;\n }\n\n /**\n * Adds a one-time event listener with EventEmitter-style API.\n * @param {string} event_name - The name of the event to listen for.\n * @param {Function} listener - The callback function to execute when the event fires.\n * @returns {Event} Returns this for chaining.\n */\n once(event_name, listener) {\n const wrapped_listener = (event) => {\n listener(event.detail);\n };\n this.addEventListener(event_name, wrapped_listener, { once: true });\n return this;\n }\n\n /**\n * Removes an event listener with EventEmitter-style API.\n * @param {string} event_name - The name of the event.\n * @param {Function} listener - The callback function to remove.\n * @returns {Event} Returns this for chaining.\n */\n off(event_name, listener) {\n if (this._listener_map && this._listener_map.has(listener)) {\n const wrapped_listener = this._listener_map.get(listener);\n this.removeEventListener(event_name, wrapped_listener);\n this._listener_map.delete(listener);\n }\n return this;\n }\n\n /**\n * Alias for off() to maintain EventEmitter API compatibility.\n * @param {string} event_name - The name of the event.\n * @param {Function} listener - The callback function to remove.\n * @returns {Event} Returns this for chaining.\n */\n removeListener(event_name, listener) {\n return this.off(event_name, listener);\n }\n}\n\nexport default Event;\n"],
5
+ "mappings": "+EAAA,OAAiB,YAAAA,MAAgB,uBASjC,MAAMC,UAAc,WAAY,CAThC,MASgC,CAAAC,EAAA,cAK9B,aAAc,CACZ,MAAM,EACN,KAAK,OAAS,CAAC,EACf,KAAK,cAAgB,IAAI,GAC3B,CAOA,eAAeC,EAAa,CAC1B,UAAWC,KAAc,OAAO,OAAOD,CAAW,EAChD,KAAK,OAAOC,CAAU,EAAI,IAAIH,CAElC,CAWA,KAAKG,EAAYC,EAAMC,EAAU,GAAOC,EAAa,GAAM,CACzD,MAAMC,EAAO,IAAI,QACXC,EAAc,KAAK,MAAM,KAAK,UAAUJ,EAAM,CAACK,EAAKC,IAAU,CAClE,GAAI,OAAOA,GAAU,UAAYA,IAAU,KAAM,CAC/C,GAAIH,EAAK,IAAIG,CAAK,EAAG,OACrBH,EAAK,IAAIG,CAAK,CAChB,CACA,OAAOA,CACT,CAAC,CAAC,EAEIC,EAAe,IAAI,YAAYR,EAAY,CAC/C,OAAQK,EACR,QAAAH,EACA,WAAAC,CACF,CAAC,EACKM,EAAS,KAAK,cAAcD,CAAY,EAE9C,GAAI,CACF,MAAME,EAAU,IAAI,iBAAiBV,CAAU,EAC/CU,EAAQ,YAAYL,CAAW,EAC/BK,EAAQ,MAAM,CAChB,OAASC,EAAG,CACV,QAAQ,KAAKf,EAAS,iBAAkBe,CAAC,CAC3C,CACA,OAAOF,CACT,CAQA,YAAYT,EAAYY,EAAU,CAChC,MAAMF,EAAU,IAAI,iBAAiBV,CAAU,EAC/C,OAAAU,EAAQ,UAAaG,GAAU,CAC7BD,EAASC,EAAM,IAAI,CACrB,EACAH,EAAQ,KAAQT,GAAS,CACvBS,EAAQ,YAAYT,CAAI,CAC1B,EACAS,EAAQ,QAAU,IAAM,CACtBA,EAAQ,MAAM,CAChB,EACOA,CACT,CAQA,MAAMV,EAAYY,EAAU,CAC1B,KAAK,GAAGZ,EAAYY,CAAQ,EAC5B,MAAME,EAAY,KAAK,YAAYd,EAAYY,CAAQ,EACvD,MAAO,CAAE,MAAO,KAAM,UAAAE,CAAU,CAClC,CASA,GAAGd,EAAYY,EAAU,CACvB,MAAMG,EAAmBjB,EAACe,GAAU,CAElCD,EAASC,EAAM,MAAM,CACvB,EAHyB,oBAKzB,YAAK,cAAc,IAAID,EAAUG,CAAgB,EACjD,KAAK,iBAAiBf,EAAYe,CAAgB,EAC3C,IACT,CAQA,KAAKf,EAAYY,EAAU,CACzB,MAAMG,EAAmBjB,EAACe,GAAU,CAClCD,EAASC,EAAM,MAAM,CACvB,EAFyB,oBAGzB,YAAK,iBAAiBb,EAAYe,EAAkB,CAAE,KAAM,EAAK,CAAC,EAC3D,IACT,CAQA,IAAIf,EAAYY,EAAU,CACxB,GAAI,KAAK,eAAiB,KAAK,cAAc,IAAIA,CAAQ,EAAG,CAC1D,MAAMG,EAAmB,KAAK,cAAc,IAAIH,CAAQ,EACxD,KAAK,oBAAoBZ,EAAYe,CAAgB,EACrD,KAAK,cAAc,OAAOH,CAAQ,CACpC,CACA,OAAO,IACT,CAQA,eAAeZ,EAAYY,EAAU,CACnC,OAAO,KAAK,IAAIZ,EAAYY,CAAQ,CACtC,CACF,CAEA,IAAOI,EAAQnB",
6
+ "names": ["warnings", "Event", "__name", "event_names", "event_name", "data", "bubbles", "cancelable", "seen", "workingData", "key", "value", "custom_event", "result", "channel", "e", "listener", "event", "broadcast", "wrapped_listener", "event_default"]
7
7
  }
@@ -0,0 +1,2 @@
1
+ var o=Object.defineProperty;var e=(s,t)=>o(s,"name",{value:t,configurable:!0});import"./steps/index.js";const r=Symbol.for("@ronaldroe/micro-flow/registry");globalThis[r]||(globalThis[r]=new Map);class g{static{e(this,"Registry")}static get registry(){return globalThis[r]}static register(t,i){this.registry.set(t,i)}static get(t){return this.registry.get(t)}}export{g as default};
2
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/classes/registry.js"],
4
+ "sourcesContent": ["import * as Steps from './steps/index.js';\n\nconst _REGISTRY_KEY = Symbol.for('@ronaldroe/micro-flow/registry');\n\nif (!globalThis[_REGISTRY_KEY]) {\n globalThis[_REGISTRY_KEY] = new Map();\n}\n\nexport default class Registry {\n static get registry() {\n return globalThis[_REGISTRY_KEY];\n }\n\n static register(name, stepClass) {\n this.registry.set(name, stepClass);\n }\n\n static get(name) {\n return this.registry.get(name);\n }\n}\n"],
5
+ "mappings": "+EAAA,MAAuB,mBAEvB,MAAMA,EAAgB,OAAO,IAAI,gCAAgC,EAE5D,WAAWA,CAAa,IAC3B,WAAWA,CAAa,EAAI,IAAI,KAGlC,MAAOC,CAAuB,CAR9B,MAQ8B,CAAAC,EAAA,iBAC5B,WAAW,UAAW,CACpB,OAAO,WAAWF,CAAa,CACjC,CAEA,OAAO,SAASG,EAAMC,EAAW,CAC/B,KAAK,SAAS,IAAID,EAAMC,CAAS,CACnC,CAEA,OAAO,IAAID,EAAM,CACf,OAAO,KAAK,SAAS,IAAIA,CAAI,CAC/B,CACF",
6
+ "names": ["_REGISTRY_KEY", "Registry", "__name", "name", "stepClass"]
7
+ }
@@ -0,0 +1,2 @@
1
+ export*from"./classes/index.js";export*from"./enums/index.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/index.js"],
4
+ "sourcesContent": ["export * from './classes/index.js';\nexport * from './enums/index.js';\n"],
5
+ "mappings": "AAAA,WAAc,qBACd,WAAc",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronaldroe/micro-flow",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
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": {
@@ -39,7 +39,14 @@ class Event extends EventTarget {
39
39
  * @returns {boolean} True if the event was not cancelled, false if it was cancelled.
40
40
  */
41
41
  emit(event_name, data, bubbles = false, cancelable = true) {
42
- const workingData = JSON.parse(JSON.stringify(data));
42
+ const seen = new WeakSet();
43
+ const workingData = JSON.parse(JSON.stringify(data, (key, value) => {
44
+ if (typeof value === 'object' && value !== null) {
45
+ if (seen.has(value)) return undefined;
46
+ seen.add(value);
47
+ }
48
+ return value;
49
+ }));
43
50
 
44
51
  const custom_event = new CustomEvent(event_name, {
45
52
  detail: workingData,
@@ -0,0 +1,21 @@
1
+ import * as Steps from './steps/index.js';
2
+
3
+ const _REGISTRY_KEY = Symbol.for('@ronaldroe/micro-flow/registry');
4
+
5
+ if (!globalThis[_REGISTRY_KEY]) {
6
+ globalThis[_REGISTRY_KEY] = new Map();
7
+ }
8
+
9
+ export default class Registry {
10
+ static get registry() {
11
+ return globalThis[_REGISTRY_KEY];
12
+ }
13
+
14
+ static register(name, stepClass) {
15
+ this.registry.set(name, stepClass);
16
+ }
17
+
18
+ static get(name) {
19
+ return this.registry.get(name);
20
+ }
21
+ }