@doeixd/machine 0.0.6 โ 0.0.8
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 +288 -272
- package/dist/cjs/development/index.js +1269 -16
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +5 -5
- package/dist/esm/development/index.js +1269 -16
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +5 -5
- package/dist/types/extract.d.ts +40 -4
- package/dist/types/extract.d.ts.map +1 -1
- package/dist/types/generators.d.ts +40 -9
- package/dist/types/generators.d.ts.map +1 -1
- package/dist/types/higher-order.d.ts +221 -0
- package/dist/types/higher-order.d.ts.map +1 -0
- package/dist/types/index.d.ts +65 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/middleware.d.ts +1048 -0
- package/dist/types/middleware.d.ts.map +1 -0
- package/dist/types/primitives.d.ts +105 -3
- package/dist/types/primitives.d.ts.map +1 -1
- package/dist/types/runtime-extract.d.ts.map +1 -1
- package/dist/types/utils.d.ts +313 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/adapters.ts +407 -0
- package/src/extract.ts +180 -8
- package/src/generators.ts +25 -25
- package/src/higher-order.ts +364 -0
- package/src/index.ts +215 -9
- package/src/middleware.ts +2325 -0
- package/src/primitives.ts +194 -3
- package/src/runtime-extract.ts +15 -0
- package/src/utils.ts +386 -5
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ A minimal, type-safe state machine library for TypeScript.
|
|
|
8
8
|
|
|
9
9
|
> **Philosophy**: Provide minimal primitives that capture the essence of finite state machines, with maximum type safety and flexibility. **Type-State Programming** is our core paradigmโwe use TypeScript's type system itself to represent finite states, making illegal states unrepresentable and invalid transitions impossible to write. The compiler becomes your safety net, catching state-related bugs before your code ever runs.
|
|
10
10
|
|
|
11
|
+
> **Middleware System**: For production-ready state machines, we provide a comprehensive middleware system for cross-cutting concerns like logging, analytics, validation, error handling, and debugging. **๐ [Read the Middleware Guide](./docs/middleware.md)**
|
|
12
|
+
|
|
11
13
|
## Installation
|
|
12
14
|
|
|
13
15
|
```bash
|
|
@@ -59,6 +61,10 @@ type Machine<C extends object> = {
|
|
|
59
61
|
|
|
60
62
|
**Read more about our core principles:** [ ๐ Core Principles Guide ](./docs/principles.md)
|
|
61
63
|
|
|
64
|
+
## Choosing the Right Pattern
|
|
65
|
+
|
|
66
|
+
The library offers multiple patterns for different use cases. **๐ [Pattern Decision Guide](./docs/patterns.md)** - A comprehensive guide to help you choose between Basic Machines, Runner, Ensemble, Generators, Classes, and more.
|
|
67
|
+
|
|
62
68
|
## Quick Start
|
|
63
69
|
|
|
64
70
|
### Basic Counter (Simple State)
|
|
@@ -499,6 +505,68 @@ import { next } from "@doeixd/machine";
|
|
|
499
505
|
const updated = next(counter, (ctx) => ({ count: ctx.count + 1 }));
|
|
500
506
|
```
|
|
501
507
|
|
|
508
|
+
### Transition Binding Helpers
|
|
509
|
+
|
|
510
|
+
These utilities eliminate the need for `.call(m.context, ...)` boilerplate when invoking transitions.
|
|
511
|
+
|
|
512
|
+
#### `call<C, F>(fn, context, ...args)`
|
|
513
|
+
|
|
514
|
+
Explicitly binds a transition function to a context and invokes it. Useful when you need to call a transition with proper `this` binding.
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
import { call } from "@doeixd/machine";
|
|
518
|
+
|
|
519
|
+
type MyContext = { count: number };
|
|
520
|
+
const increment = function(this: MyContext) {
|
|
521
|
+
return { count: this.count + 1 };
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const result = call(increment, { count: 5 }); // Returns { count: 6 }
|
|
525
|
+
|
|
526
|
+
// Particularly useful with generator-based flows:
|
|
527
|
+
const result = run(function* (m) {
|
|
528
|
+
m = yield* step(call(m.increment, m.context));
|
|
529
|
+
m = yield* step(call(m.add, m.context, 5));
|
|
530
|
+
return m;
|
|
531
|
+
}, counter);
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### `bindTransitions<M>(machine)`
|
|
535
|
+
|
|
536
|
+
Returns a Proxy that automatically binds all transition methods to the machine's context. Eliminates `.call(m.context, ...)` boilerplate entirely.
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { bindTransitions } from "@doeixd/machine";
|
|
540
|
+
|
|
541
|
+
const counter = bindTransitions(createMachine(
|
|
542
|
+
{ count: 0 },
|
|
543
|
+
{
|
|
544
|
+
increment(this: { count: number }) {
|
|
545
|
+
return createMachine({ count: this.count + 1 }, this);
|
|
546
|
+
},
|
|
547
|
+
add(this: { count: number }, n: number) {
|
|
548
|
+
return createMachine({ count: this.count + n }, this);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
));
|
|
552
|
+
|
|
553
|
+
// All methods are automatically bound - no need for .call()!
|
|
554
|
+
const next = counter.increment(); // Works!
|
|
555
|
+
const result = counter.add(5); // Works!
|
|
556
|
+
|
|
557
|
+
// Great for generator-based flows:
|
|
558
|
+
const result = run(function* (m) {
|
|
559
|
+
m = yield* step(m.increment()); // Clean syntax!
|
|
560
|
+
m = yield* step(m.add(5)); // No .call() needed
|
|
561
|
+
return m;
|
|
562
|
+
}, counter);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**How it works:**
|
|
566
|
+
The Proxy intercepts all property access on the machine. When a property is a function (transition method), it wraps it to automatically call `.apply(machine.context, args)` before invoking. Non-callable properties are returned as-is.
|
|
567
|
+
|
|
568
|
+
**Note:** The Proxy preserves type safety while providing ergonomic syntax. Use this when writing generator-based flows or any code that frequently calls transitions.
|
|
569
|
+
|
|
502
570
|
#### `matchMachine<M, K, R>(machine, key, handlers)`
|
|
503
571
|
|
|
504
572
|
Type-safe pattern matching on discriminated unions in context.
|
|
@@ -594,6 +662,41 @@ const alice = buildUser({ id: 1, name: "Alice" });
|
|
|
594
662
|
const bob = buildUser({ id: 2, name: "Bob" });
|
|
595
663
|
```
|
|
596
664
|
|
|
665
|
+
### Middleware System
|
|
666
|
+
|
|
667
|
+
For production-ready state machines with logging, analytics, validation, error handling, and debugging capabilities:
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
import { createMiddleware, withLogging, withValidation, withAnalytics } from "@doeixd/machine";
|
|
671
|
+
|
|
672
|
+
// Wrap machines with middleware
|
|
673
|
+
const instrumented = createMiddleware(machine, {
|
|
674
|
+
before: ({ transitionName, context, args }) => {
|
|
675
|
+
console.log(`โ ${transitionName}`, args);
|
|
676
|
+
},
|
|
677
|
+
after: ({ transitionName, prevContext, nextContext }) => {
|
|
678
|
+
console.log(`โ ${transitionName}`);
|
|
679
|
+
},
|
|
680
|
+
error: ({ transitionName, error }) => {
|
|
681
|
+
console.error(`โ ${transitionName}:`, error);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// Or use pre-built middleware
|
|
686
|
+
const logged = withLogging(machine);
|
|
687
|
+
const validated = withValidation(machine, validateFn);
|
|
688
|
+
const tracked = withAnalytics(machine, trackEvent);
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Features:**
|
|
692
|
+
- Type-safe interception layer
|
|
693
|
+
- Pre-built middleware for common use cases
|
|
694
|
+
- History tracking and time-travel debugging
|
|
695
|
+
- Performance monitoring and error reporting
|
|
696
|
+
- Composable and configurable
|
|
697
|
+
|
|
698
|
+
**๐ [Complete Middleware Documentation](./docs/middleware.md)**
|
|
699
|
+
|
|
597
700
|
### Type Utilities
|
|
598
701
|
|
|
599
702
|
#### Type Extraction
|
|
@@ -649,7 +752,7 @@ For advanced use cases, the library provides optional patterns that offer better
|
|
|
649
752
|
|
|
650
753
|
**Runner (createRunner):** A stateful controller that wraps a single machine. It provides a stable actions object (runner.actions.increment()) to eliminate the need for manual state reassignment, which is ideal for complex local state.
|
|
651
754
|
|
|
652
|
-
**Ensemble (createEnsemble / createMultiMachine):**
|
|
755
|
+
**Ensemble (createEnsemble / createMultiMachine):** Coordinates multiple independent state machines that share the same context store, like musicians in an orchestra following a shared conductor. Each machine handles its domain (auth, data, UI) while operating on the same global state.
|
|
653
756
|
|
|
654
757
|
### Managed State with Runner & Ensemble
|
|
655
758
|
|
|
@@ -687,312 +790,129 @@ if (runner.state.context.status === 'loggedIn') {
|
|
|
687
790
|
- Perfect for React hooks, component state, or form handling
|
|
688
791
|
- Type-safe state narrowing still works
|
|
689
792
|
|
|
690
|
-
#### Ensemble:
|
|
793
|
+
#### Ensemble: Coordinating Multiple Machines with Shared Context
|
|
691
794
|
|
|
692
|
-
The `Ensemble`
|
|
795
|
+
The `Ensemble` coordinates multiple independent state machines that all operate on the same shared context store, like musicians in an orchestra following a shared conductor. Each machine handles its own domain while reading/writing to the same global state.
|
|
693
796
|
|
|
694
797
|
```typescript
|
|
695
798
|
import { createEnsemble } from "@doeixd/machine/multi";
|
|
696
799
|
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
800
|
+
// Shared application state
|
|
801
|
+
type AppState = {
|
|
802
|
+
auth: { status: 'loggedIn' | 'loggedOut'; user?: string };
|
|
803
|
+
data: { status: 'idle' | 'loading' | 'success'; items?: any[] };
|
|
804
|
+
ui: { modal: 'open' | 'closed'; theme: 'light' | 'dark' };
|
|
701
805
|
};
|
|
702
806
|
|
|
703
|
-
|
|
704
|
-
|
|
807
|
+
const globalStore = {
|
|
808
|
+
getContext: () => appState,
|
|
809
|
+
setContext: (newState) => setAppState(newState)
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// Auth ensemble - manages auth slice
|
|
813
|
+
const authEnsemble = createEnsemble(globalStore, {
|
|
814
|
+
loggedOut: (ctx) => createMachine(ctx, {
|
|
815
|
+
login: (user) => ({ ...ctx, auth: { status: 'loggedIn', user } })
|
|
816
|
+
}),
|
|
817
|
+
loggedIn: (ctx) => createMachine(ctx, {
|
|
818
|
+
logout: () => ({ ...ctx, auth: { status: 'loggedOut' } })
|
|
819
|
+
})
|
|
820
|
+
}, (ctx) => ctx.auth.status);
|
|
821
|
+
|
|
822
|
+
// Data ensemble - manages data slice
|
|
823
|
+
const dataEnsemble = createEnsemble(globalStore, {
|
|
705
824
|
idle: (ctx) => createMachine(ctx, {
|
|
706
|
-
fetch: () =>
|
|
825
|
+
fetch: async () => {
|
|
826
|
+
const items = await api.fetch();
|
|
827
|
+
return { ...ctx, data: { status: 'success', items } };
|
|
828
|
+
}
|
|
707
829
|
}),
|
|
708
|
-
loading: (ctx) => createMachine(ctx, {
|
|
709
|
-
|
|
710
|
-
|
|
830
|
+
loading: (ctx) => createMachine(ctx, { /* ... */ }),
|
|
831
|
+
success: (ctx) => createMachine(ctx, { /* ... */ })
|
|
832
|
+
}, (ctx) => ctx.data.status);
|
|
833
|
+
|
|
834
|
+
// UI ensemble - manages UI slice
|
|
835
|
+
const uiEnsemble = createEnsemble(globalStore, {
|
|
836
|
+
closed: (ctx) => createMachine(ctx, {
|
|
837
|
+
open: () => ({ ...ctx, ui: { ...ctx.ui, modal: 'open' } })
|
|
711
838
|
}),
|
|
712
|
-
|
|
713
|
-
|
|
839
|
+
open: (ctx) => createMachine(ctx, {
|
|
840
|
+
close: () => ({ ...ctx, ui: { ...ctx.ui, modal: 'closed' } })
|
|
714
841
|
})
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
// 3. Create the Ensemble with an accessor function for refactoring safety
|
|
718
|
-
const ensemble = createEnsemble(store, factories, (ctx) => ctx.status);
|
|
842
|
+
}, (ctx) => ctx.ui.modal);
|
|
719
843
|
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
// Type narrowing
|
|
725
|
-
if (ensemble.state.context.status === 'success') {
|
|
726
|
-
console.log(ensemble.state.context.data); // TypeScript knows data exists
|
|
727
|
-
}
|
|
844
|
+
// They coordinate through shared state
|
|
845
|
+
authEnsemble.actions.login('alice'); // Updates global auth status
|
|
846
|
+
dataEnsemble.actions.fetch(); // Reads from same global state
|
|
847
|
+
uiEnsemble.actions.showModal(); // Also uses same global state
|
|
728
848
|
```
|
|
729
849
|
|
|
730
850
|
**Perfect for:**
|
|
731
|
-
-
|
|
732
|
-
-
|
|
851
|
+
- Coordinating multiple state machines that share context
|
|
852
|
+
- Complex applications with independent domains (auth, data, UI, etc.)
|
|
853
|
+
- Framework-agnostic state logic that works with React, Solid, Vue, etc.
|
|
854
|
+
- Global state orchestration across your entire application
|
|
855
|
+
- **Syncing to external libraries/stores** - easy integration with Zustand, Redux, databases, APIs, etc.
|
|
733
856
|
- Testing (swap the store for a test stub)
|
|
734
|
-
- Multiple framework support (same machine logic for React, Solid, Vue, etc.)
|
|
735
|
-
|
|
736
|
-
**Workflow Pattern:**
|
|
737
|
-
```typescript
|
|
738
|
-
// React example
|
|
739
|
-
function MyComponent() {
|
|
740
|
-
const [context, setContext] = useState(initialContext);
|
|
741
|
-
|
|
742
|
-
const store = {
|
|
743
|
-
getContext: () => context,
|
|
744
|
-
setContext: setContext
|
|
745
|
-
};
|
|
746
|
-
|
|
747
|
-
const ensemble = useMemo(() =>
|
|
748
|
-
createEnsemble(store, factories, (ctx) => ctx.status),
|
|
749
|
-
[context]
|
|
750
|
-
);
|
|
751
|
-
|
|
752
|
-
return (
|
|
753
|
-
<div>
|
|
754
|
-
<p>Status: {ensemble.context.status}</p>
|
|
755
|
-
<button onClick={() => ensemble.actions.fetch()}>Load Data</button>
|
|
756
|
-
</div>
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
```
|
|
760
857
|
|
|
761
|
-
|
|
858
|
+
**Analogy**: A musical ensemble. Each musician (machine) plays their part following the same conductor (shared store). Together they create coordinated harmony, where one instrument's change can influence the others.
|
|
762
859
|
|
|
763
|
-
|
|
860
|
+
**Great for External Integration:**
|
|
861
|
+
The `StateStore` interface makes it trivial to sync with external systems:
|
|
764
862
|
|
|
765
863
|
```typescript
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
// With Runner (local state)
|
|
769
|
-
const result = runWithRunner(function* (runner) {
|
|
770
|
-
yield runner.actions.increment();
|
|
771
|
-
yield runner.actions.add(10);
|
|
772
|
-
if (runner.context.count > 5) {
|
|
773
|
-
yield runner.actions.reset();
|
|
774
|
-
}
|
|
775
|
-
return runner.context;
|
|
776
|
-
}, createCounterMachine());
|
|
777
|
-
|
|
778
|
-
// With Ensemble (global state)
|
|
779
|
-
const result = runWithEnsemble(function* (ensemble) {
|
|
780
|
-
yield ensemble.actions.fetch();
|
|
781
|
-
yield ensemble.actions.process();
|
|
782
|
-
if (ensemble.context.status === 'success') {
|
|
783
|
-
yield ensemble.actions.commit();
|
|
784
|
-
}
|
|
785
|
-
return ensemble.context.data;
|
|
786
|
-
}, ensemble);
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
#### Mutable Machine (Experimental)
|
|
864
|
+
// Zustand store integration
|
|
865
|
+
import { create } from 'zustand';
|
|
790
866
|
|
|
791
|
-
|
|
867
|
+
const useAppStore = create<AppState>((set, get) => ({
|
|
868
|
+
// ... your Zustand store
|
|
869
|
+
}));
|
|
792
870
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
- **No State History**: Previous states are not preserved (no time-travel debugging)
|
|
797
|
-
- **Not for Reactive UIs**: Won't trigger component re-renders in React, Solid, Vue, etc.
|
|
798
|
-
|
|
799
|
-
**Best for:**
|
|
800
|
-
- Backend services and game loops
|
|
801
|
-
- Complex synchronous scripts and data pipelines
|
|
802
|
-
- Non-UI environments where a stable state object is essential
|
|
803
|
-
|
|
804
|
-
**Example: Authentication State**
|
|
805
|
-
|
|
806
|
-
```typescript
|
|
807
|
-
import { createMutableMachine } from "@doeixd/machine/multi";
|
|
808
|
-
|
|
809
|
-
type AuthContext =
|
|
810
|
-
| { status: 'loggedOut'; error?: string }
|
|
811
|
-
| { status: 'loggedIn'; username: string };
|
|
812
|
-
|
|
813
|
-
const authFactories = {
|
|
814
|
-
loggedOut: (ctx: AuthContext) => ({
|
|
815
|
-
context: ctx,
|
|
816
|
-
login: (username: string) => ({ status: 'loggedIn', username }),
|
|
817
|
-
}),
|
|
818
|
-
loggedIn: (ctx: AuthContext) => ({
|
|
819
|
-
context: ctx,
|
|
820
|
-
logout: () => ({ status: 'loggedOut' }),
|
|
821
|
-
}),
|
|
871
|
+
const zustandStore = {
|
|
872
|
+
getContext: () => useAppStore.getState(),
|
|
873
|
+
setContext: (newState) => useAppStore.setState(newState)
|
|
822
874
|
};
|
|
823
875
|
|
|
824
|
-
const
|
|
825
|
-
{ status: 'loggedOut' } as AuthContext,
|
|
826
|
-
authFactories,
|
|
827
|
-
(ctx) => ctx.status // Accessor function - refactor-safe
|
|
828
|
-
);
|
|
829
|
-
|
|
830
|
-
// Stable reference - keep this, the object will mutate
|
|
831
|
-
const userRef = auth;
|
|
876
|
+
const ensemble = createEnsemble(zustandStore, factories, (ctx) => ctx.status);
|
|
832
877
|
|
|
833
|
-
|
|
878
|
+
// Redux integration
|
|
879
|
+
import { store } from './reduxStore';
|
|
834
880
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
console.log(auth.username); // 'alice'
|
|
839
|
-
console.log(userRef === auth); // true - same object reference
|
|
840
|
-
```
|
|
841
|
-
|
|
842
|
-
**Example: Game State Loop**
|
|
843
|
-
|
|
844
|
-
```typescript
|
|
845
|
-
type PlayerContext = {
|
|
846
|
-
state: 'idle' | 'walking' | 'attacking';
|
|
847
|
-
hp: number;
|
|
848
|
-
position: { x: number; y: number };
|
|
881
|
+
const reduxStore = {
|
|
882
|
+
getContext: () => store.getState(),
|
|
883
|
+
setContext: (newState) => store.dispatch(setAppState(newState))
|
|
849
884
|
};
|
|
850
885
|
|
|
851
|
-
const
|
|
852
|
-
{ state: 'idle', hp: 100, position: { x: 0, y: 0 } },
|
|
853
|
-
{
|
|
854
|
-
idle: (ctx) => ({
|
|
855
|
-
context: ctx,
|
|
856
|
-
walk: (dx: number, dy: number) => ({
|
|
857
|
-
...ctx,
|
|
858
|
-
state: 'walking',
|
|
859
|
-
position: { x: ctx.position.x + dx, y: ctx.position.y + dy }
|
|
860
|
-
}),
|
|
861
|
-
attack: () => ({ ...ctx, state: 'attacking' }),
|
|
862
|
-
}),
|
|
863
|
-
walking: (ctx) => ({
|
|
864
|
-
context: ctx,
|
|
865
|
-
stop: () => ({ ...ctx, state: 'idle' }),
|
|
866
|
-
}),
|
|
867
|
-
attacking: (ctx) => ({
|
|
868
|
-
context: ctx,
|
|
869
|
-
finishAttack: () => ({ ...ctx, state: 'idle' }),
|
|
870
|
-
}),
|
|
871
|
-
},
|
|
872
|
-
(ctx) => ctx.state // Accessor function - refactor-safe
|
|
873
|
-
);
|
|
874
|
-
|
|
875
|
-
// Game loop
|
|
876
|
-
player.walk(1, 0);
|
|
877
|
-
console.log(player.position); // { x: 1, y: 0 }
|
|
878
|
-
console.log(player.state); // 'walking'
|
|
879
|
-
|
|
880
|
-
player.stop();
|
|
881
|
-
console.log(player.state); // 'idle'
|
|
882
|
-
```
|
|
883
|
-
|
|
884
|
-
โ ๏ธ **Trade-offs**: Breaks immutability principle. Only use when:
|
|
885
|
-
- Working in non-UI environments (backend, CLI, game logic)
|
|
886
|
-
- Stable object reference is critical
|
|
887
|
-
- You accept no reactive UI updates or state history
|
|
888
|
-
|
|
889
|
-
**Not suitable for**: React, Solid, Vue, or any reactive framework.
|
|
890
|
-
|
|
891
|
-
#### Comparison: Runner vs Ensemble vs Mutable Machine
|
|
892
|
-
|
|
893
|
-
| Feature | Runner | Ensemble | Mutable Machine |
|
|
894
|
-
|---------|--------|----------|-----------------|
|
|
895
|
-
| **State Philosophy** | Immutable core with ergonomic wrapper | External immutable state store integration | Mutable in-place context |
|
|
896
|
-
| **Primary Use Case** | Complex local/component state | Global state with framework integration | Backend, game loops, non-UI |
|
|
897
|
-
| **API Style** | `runner.actions.increment()` | `ensemble.actions.increment()` | `machine.increment()` |
|
|
898
|
-
| **UI Frameworks** | โ
Excellent (React, Solid, Vue) | โ
Designed for frameworks | โ Won't trigger re-renders |
|
|
899
|
-
| **State History** | โ
Preserved (immutable snapshots) | โ
Preserved (by external store) | โ Lost (mutated in place) |
|
|
900
|
-
| **Object Stability** | Runner reference stable, internal machine changes | Ensemble reference stable, reconstructed per access | โ
Single object reference |
|
|
901
|
-
| **Time-Travel Debugging** | โ
Possible | โ
Possible | โ Not possible |
|
|
902
|
-
| **Performance** | Standard | Standard | โ
Optimal (no allocations) |
|
|
903
|
-
|
|
904
|
-
**Quick Decision Tree:**
|
|
905
|
-
|
|
906
|
-
1. **Do you need a UI framework** (React, Solid, Vue)?
|
|
907
|
-
- **Yes** โ Use **Ensemble** if global/shared state, or **Runner** if local/component state
|
|
908
|
-
- **No** (Backend, game, CLI) โ Use **Mutable Machine**
|
|
909
|
-
|
|
910
|
-
2. **Is state global/shared across your app?**
|
|
911
|
-
- **Yes** โ Use **Ensemble** (hooks into your framework's state manager)
|
|
912
|
-
- **No** โ Use **Runner** (simpler, still immutable)
|
|
913
|
-
|
|
914
|
-
3. **Do you need immutability for debugging/testing?**
|
|
915
|
-
- **Yes** โ Use **Runner** or **Ensemble**
|
|
916
|
-
- **No** (performance critical) โ Use **Mutable Machine**
|
|
917
|
-
|
|
918
|
-
##### Deep Dive: Runner (createRunner)
|
|
886
|
+
const ensemble = createEnsemble(reduxStore, factories, (ctx) => ctx.status);
|
|
919
887
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
// With Runner (stable reference, less boilerplate)
|
|
930
|
-
const runner = createRunner(createCounterMachine());
|
|
931
|
-
runner.actions.increment();
|
|
932
|
-
runner.actions.add(5);
|
|
933
|
-
runner.actions.reset();
|
|
934
|
-
console.log(runner.context); // Access state directly
|
|
935
|
-
```
|
|
936
|
-
|
|
937
|
-
**How it Works:**
|
|
938
|
-
- Holds a private `currentMachine` variable
|
|
939
|
-
- Wraps each transition method to update `currentMachine` before returning
|
|
940
|
-
- Provides stable `runner.actions` and `runner.context` references
|
|
941
|
-
- The underlying immutable machine is still pure; the Runner just manages the reassignments
|
|
942
|
-
|
|
943
|
-
**When to Use:**
|
|
944
|
-
- Complex local state (forms, multi-step wizards, component logic)
|
|
945
|
-
- Generator-based workflows (cleaner syntax with `yield runner.actions.xxx()`)
|
|
946
|
-
- You want immutability's safety without constant `machine = machine.xxx()` chains
|
|
947
|
-
- Perfect for React component state or Solid signals
|
|
948
|
-
|
|
949
|
-
**Analogy**: An automatic transmission. The immutable engine is still doing powerful, pure work. The Runner just handles the "gear shifting" automatically.
|
|
950
|
-
|
|
951
|
-
##### Deep Dive: Ensemble (createEnsemble)
|
|
888
|
+
// Database/API integration
|
|
889
|
+
const apiStore = {
|
|
890
|
+
getContext: async () => await api.getAppState(),
|
|
891
|
+
setContext: async (newState) => {
|
|
892
|
+
await api.saveAppState(newState);
|
|
893
|
+
// Trigger real-time updates to other clients
|
|
894
|
+
socket.emit('state-changed', newState);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
952
897
|
|
|
953
|
-
|
|
898
|
+
const ensemble = createEnsemble(apiStore, factories, (ctx) => ctx.status);
|
|
954
899
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
900
|
+
// LocalStorage persistence
|
|
901
|
+
const persistentStore = {
|
|
902
|
+
getContext: () => {
|
|
903
|
+
const saved = localStorage.getItem('app-state');
|
|
904
|
+
return saved ? JSON.parse(saved) : defaultState;
|
|
905
|
+
},
|
|
906
|
+
setContext: (newState) => {
|
|
907
|
+
localStorage.setItem('app-state', JSON.stringify(newState));
|
|
908
|
+
return newState;
|
|
909
|
+
}
|
|
960
910
|
};
|
|
961
911
|
|
|
962
|
-
|
|
963
|
-
const [context, setContext] = useState(initialContext);
|
|
964
|
-
|
|
965
|
-
// The Ensemble bridges them - use an accessor function for refactoring safety
|
|
966
|
-
const ensemble = useMemo(() =>
|
|
967
|
-
createEnsemble(
|
|
968
|
-
{ getContext: () => context, setContext },
|
|
969
|
-
factories,
|
|
970
|
-
(ctx) => ctx.status // Accessor function - fully refactor-safe!
|
|
971
|
-
),
|
|
972
|
-
[context]
|
|
973
|
-
);
|
|
974
|
-
|
|
975
|
-
// Type-safe dispatch from any part of your app
|
|
976
|
-
ensemble.actions.fetch();
|
|
912
|
+
const ensemble = createEnsemble(persistentStore, factories, (ctx) => ctx.status);
|
|
977
913
|
```
|
|
978
914
|
|
|
979
|
-
**
|
|
980
|
-
- Takes a `StateStore` (get/set functions) that communicate with your state manager
|
|
981
|
-
- Takes a set of factory functions that create machines for each state
|
|
982
|
-
- When you call `ensemble.actions.fetch()`:
|
|
983
|
-
1. Gets current context from the store
|
|
984
|
-
2. Determines the active machine based on discriminant key
|
|
985
|
-
3. Executes the transition (which calls `store.setContext()` internally)
|
|
986
|
-
4. Reconstructs the machine with updated context on next access
|
|
987
|
-
|
|
988
|
-
**When to Use:**
|
|
989
|
-
- Global/application state that multiple components need
|
|
990
|
-
- You want machine logic completely decoupled from your UI framework
|
|
991
|
-
- Testing (swap the store for a test stub)
|
|
992
|
-
- Portable state logic (same machine works with React, Solid, Vue, etc.)
|
|
993
|
-
- Complex state that's read/updated from multiple places in your app
|
|
994
|
-
|
|
995
|
-
**Analogy**: A custom car build. The machine provides expert logic on "how a car behaves," but you provide the engine (your framework's state manager). Perfect integration, total flexibility.
|
|
915
|
+
**Your machine logic stays pure** - just swap the store implementation to change how state is persisted, synchronized, or shared.
|
|
996
916
|
|
|
997
917
|
##### Deep Dive: Mutable Machine (createMutableMachine)
|
|
998
918
|
|
|
@@ -1673,6 +1593,98 @@ const charts = extractMachines({
|
|
|
1673
1593
|
});
|
|
1674
1594
|
```
|
|
1675
1595
|
|
|
1596
|
+
### Advanced Patterns: Hierarchical and Parallel Machines
|
|
1597
|
+
|
|
1598
|
+
**NEW**: The extractor now supports advanced state machine patterns for complex systems.
|
|
1599
|
+
|
|
1600
|
+
#### Hierarchical (Nested States)
|
|
1601
|
+
|
|
1602
|
+
Model parent states containing child states:
|
|
1603
|
+
|
|
1604
|
+
```typescript
|
|
1605
|
+
const config: MachineConfig = {
|
|
1606
|
+
input: 'src/dashboard.ts',
|
|
1607
|
+
classes: ['Dashboard', 'ErrorState'],
|
|
1608
|
+
id: 'dashboard',
|
|
1609
|
+
initialState: 'Dashboard',
|
|
1610
|
+
children: {
|
|
1611
|
+
contextProperty: 'child',
|
|
1612
|
+
initialState: 'ViewingMachine',
|
|
1613
|
+
classes: ['ViewingMachine', 'EditingMachine']
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
Generates:
|
|
1619
|
+
|
|
1620
|
+
```json
|
|
1621
|
+
{
|
|
1622
|
+
"id": "dashboard",
|
|
1623
|
+
"initial": "Dashboard",
|
|
1624
|
+
"states": {
|
|
1625
|
+
"Dashboard": {
|
|
1626
|
+
"initial": "ViewingMachine",
|
|
1627
|
+
"states": {
|
|
1628
|
+
"ViewingMachine": { "on": { /* ... */ } },
|
|
1629
|
+
"EditingMachine": { "on": { /* ... */ } }
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
```
|
|
1635
|
+
|
|
1636
|
+
#### Parallel (Orthogonal Regions)
|
|
1637
|
+
|
|
1638
|
+
Model independent regions that evolve simultaneously:
|
|
1639
|
+
|
|
1640
|
+
```typescript
|
|
1641
|
+
const config: MachineConfig = {
|
|
1642
|
+
input: 'src/editor.ts',
|
|
1643
|
+
id: 'editor',
|
|
1644
|
+
parallel: {
|
|
1645
|
+
regions: [
|
|
1646
|
+
{
|
|
1647
|
+
name: 'fontWeight',
|
|
1648
|
+
initialState: 'Normal',
|
|
1649
|
+
classes: ['Normal', 'Bold']
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
name: 'textDecoration',
|
|
1653
|
+
initialState: 'None',
|
|
1654
|
+
classes: ['None', 'Underline']
|
|
1655
|
+
}
|
|
1656
|
+
]
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
```
|
|
1660
|
+
|
|
1661
|
+
Generates:
|
|
1662
|
+
|
|
1663
|
+
```json
|
|
1664
|
+
{
|
|
1665
|
+
"id": "editor",
|
|
1666
|
+
"type": "parallel",
|
|
1667
|
+
"states": {
|
|
1668
|
+
"fontWeight": {
|
|
1669
|
+
"initial": "Normal",
|
|
1670
|
+
"states": {
|
|
1671
|
+
"Normal": { "on": { /* ... */ } },
|
|
1672
|
+
"Bold": { "on": { /* ... */ } }
|
|
1673
|
+
}
|
|
1674
|
+
},
|
|
1675
|
+
"textDecoration": {
|
|
1676
|
+
"initial": "None",
|
|
1677
|
+
"states": {
|
|
1678
|
+
"None": { "on": { /* ... */ } },
|
|
1679
|
+
"Underline": { "on": { /* ... */ } }
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
**See [docs/ADVANCED_EXTRACTION.md](./docs/ADVANCED_EXTRACTION.md) for complete guide.**
|
|
1687
|
+
|
|
1676
1688
|
### Runtime Extraction API
|
|
1677
1689
|
|
|
1678
1690
|
Extract statecharts from **running machine instances** without requiring source code access:
|
|
@@ -1884,6 +1896,10 @@ overrideTransitions<M, T>(machine: M, overrides: T): M & T
|
|
|
1884
1896
|
extendTransitions<M, T>(machine: M, newTransitions: T): M & T
|
|
1885
1897
|
createMachineBuilder<M>(template: M): (context) => M
|
|
1886
1898
|
|
|
1899
|
+
// Transition Binding
|
|
1900
|
+
call<C, F>(fn: F, context: C, ...args): ReturnType<F>
|
|
1901
|
+
bindTransitions<M extends Machine<any>>(machine: M): M
|
|
1902
|
+
|
|
1887
1903
|
// Pattern Matching
|
|
1888
1904
|
matchMachine<M, K, R>(machine: M, key: K, handlers): R
|
|
1889
1905
|
hasState<M, K, V>(machine: M, key: K, value: V): boolean
|