@doeixd/machine 0.0.4 → 0.0.6
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 +952 -13
- package/dist/cjs/development/index.js +691 -0
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +5 -1
- package/dist/esm/development/index.js +698 -0
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +5 -1
- package/dist/types/extract.d.ts +71 -0
- package/dist/types/extract.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/multi.d.ts +838 -0
- package/dist/types/multi.d.ts.map +1 -0
- package/dist/types/primitives.d.ts +202 -0
- package/dist/types/primitives.d.ts.map +1 -0
- package/dist/types/runtime-extract.d.ts +53 -0
- package/dist/types/runtime-extract.d.ts.map +1 -0
- package/package.json +6 -2
- package/src/extract.ts +452 -67
- package/src/index.ts +49 -0
- package/src/multi.ts +1145 -0
- package/src/primitives.ts +135 -0
- package/src/react.ts +349 -28
- package/src/runtime-extract.ts +141 -0
- package/src/solid.ts +8 -8
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
[](https://badge.fury.io/js/@doeixd%2Fmachine)
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
1
3
|
[](https://deepwiki.com/doeixd/machine)
|
|
2
4
|
|
|
3
5
|
# Machine
|
|
4
6
|
|
|
5
|
-
A minimal, type-safe state machine library for TypeScript
|
|
7
|
+
A minimal, type-safe state machine library for TypeScript.
|
|
6
8
|
|
|
7
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.
|
|
8
10
|
|
|
@@ -40,7 +42,7 @@ An FSM is a 5-tuple: **M = (S, Σ, δ, s₀, F)** where:
|
|
|
40
42
|
|
|
41
43
|
```typescript
|
|
42
44
|
type Machine<C extends object> = {
|
|
43
|
-
|
|
45
|
+
context: C; // Encodes the current state (s ∈ S)
|
|
44
46
|
} & Record<string, (...args: any[]) => Machine<any>>; // Transition functions (δ)
|
|
45
47
|
```
|
|
46
48
|
|
|
@@ -61,6 +63,8 @@ type Machine<C extends object> = {
|
|
|
61
63
|
|
|
62
64
|
### Basic Counter (Simple State)
|
|
63
65
|
|
|
66
|
+
**Immutable approach (recommended):**
|
|
67
|
+
|
|
64
68
|
```typescript
|
|
65
69
|
import { createMachine } from "@doeixd/machine";
|
|
66
70
|
|
|
@@ -84,6 +88,26 @@ console.log(next.context.count); // 1
|
|
|
84
88
|
console.log(counter.context.count); // 0
|
|
85
89
|
```
|
|
86
90
|
|
|
91
|
+
**Mutable approach (also supported):**
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// If you prefer mutable state, just return `this`
|
|
95
|
+
const counter = createMachine(
|
|
96
|
+
{ count: 0 },
|
|
97
|
+
{
|
|
98
|
+
increment: function() {
|
|
99
|
+
(this.context as any).count++;
|
|
100
|
+
return this; // Return same instance
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
counter.increment();
|
|
106
|
+
console.log(counter.context.count); // 1 (mutated in place)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This shows the **flexibility** of the library: immutability is the default pattern because it's safer, but you can choose mutability when it makes sense for your use case.
|
|
110
|
+
|
|
87
111
|
### Type-State Programming (Compile-Time State Safety)
|
|
88
112
|
|
|
89
113
|
The most powerful pattern: different machine types represent different states.
|
|
@@ -619,6 +643,462 @@ function enterState(): MachineResult<{ timer: number }> {
|
|
|
619
643
|
|
|
620
644
|
## Advanced Features
|
|
621
645
|
|
|
646
|
+
### Ergonomic & Integration Patterns
|
|
647
|
+
|
|
648
|
+
For advanced use cases, the library provides optional patterns that offer better ergonomics and deep framework integration. These are available in @doeixd/machine/multi.
|
|
649
|
+
|
|
650
|
+
**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
|
+
|
|
652
|
+
**Ensemble (createEnsemble / createMultiMachine):** An orchestration engine that decouples state logic from state storage. It plugs into external stores (like React or Solid state) to create framework-agnostic, global state machines.
|
|
653
|
+
|
|
654
|
+
### Managed State with Runner & Ensemble
|
|
655
|
+
|
|
656
|
+
For stateful applications, `@doeixd/machine/multi` provides two advanced patterns that eliminate constant variable reassignment while maintaining immutability:
|
|
657
|
+
|
|
658
|
+
#### Runner: Stateful Controller for Local State
|
|
659
|
+
|
|
660
|
+
The `Runner` wraps an immutable machine in a stateful controller, providing stable `actions` object so you can call transitions imperatively without reassigning the machine variable.
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
import { createRunner } from "@doeixd/machine/multi";
|
|
664
|
+
|
|
665
|
+
const counterMachine = createCounterMachine({ count: 0 });
|
|
666
|
+
const runner = createRunner(counterMachine, (newState) => {
|
|
667
|
+
console.log('Count is now:', newState.context.count);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Call transitions without reassignment - runner updates internally
|
|
671
|
+
runner.actions.increment(); // Logs: "Count is now: 1"
|
|
672
|
+
runner.actions.add(5); // Logs: "Count is now: 6"
|
|
673
|
+
|
|
674
|
+
// Access current state
|
|
675
|
+
console.log(runner.context.count); // 6
|
|
676
|
+
console.log(runner.state.context.count); // 6 (full machine)
|
|
677
|
+
|
|
678
|
+
// Type narrowing works
|
|
679
|
+
if (runner.state.context.status === 'loggedIn') {
|
|
680
|
+
runner.actions.logout(); // TypeScript knows logout exists
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
**Benefits:**
|
|
685
|
+
- No more `machine = machine.transition()` reassignment chains
|
|
686
|
+
- Stable `actions` object for clean event handling
|
|
687
|
+
- Perfect for React hooks, component state, or form handling
|
|
688
|
+
- Type-safe state narrowing still works
|
|
689
|
+
|
|
690
|
+
#### Ensemble: Framework-Agnostic Global State Orchestration
|
|
691
|
+
|
|
692
|
+
The `Ensemble` decouples state logic (the machine) from state storage, plugging into any state management solution (React hooks, Solid stores, Zustand, Redux, etc.) via a simple `StateStore` interface.
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
import { createEnsemble } from "@doeixd/machine/multi";
|
|
696
|
+
|
|
697
|
+
// 1. Define your state store interface
|
|
698
|
+
const store = {
|
|
699
|
+
getContext: () => sharedContext,
|
|
700
|
+
setContext: (newCtx) => { sharedContext = newCtx; }
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// 2. Define machine factories for each state
|
|
704
|
+
const factories = {
|
|
705
|
+
idle: (ctx) => createMachine(ctx, {
|
|
706
|
+
fetch: () => store.setContext({ ...ctx, status: 'loading' })
|
|
707
|
+
}),
|
|
708
|
+
loading: (ctx) => createMachine(ctx, {
|
|
709
|
+
succeed: (data) => store.setContext({ ...ctx, status: 'success', data }),
|
|
710
|
+
fail: (error) => store.setContext({ ...ctx, status: 'error', error })
|
|
711
|
+
}),
|
|
712
|
+
success: (ctx) => createMachine(ctx, {
|
|
713
|
+
refetch: () => store.setContext({ ...ctx, status: 'loading' })
|
|
714
|
+
})
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
// 3. Create the Ensemble with an accessor function for refactoring safety
|
|
718
|
+
const ensemble = createEnsemble(store, factories, (ctx) => ctx.status);
|
|
719
|
+
|
|
720
|
+
// 4. Use it with type-safe dispatch
|
|
721
|
+
ensemble.actions.fetch(); // Transitions to loading
|
|
722
|
+
console.log(ensemble.context.status); // 'loading'
|
|
723
|
+
|
|
724
|
+
// Type narrowing
|
|
725
|
+
if (ensemble.state.context.status === 'success') {
|
|
726
|
+
console.log(ensemble.state.context.data); // TypeScript knows data exists
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**Perfect for:**
|
|
731
|
+
- Global application state orchestration
|
|
732
|
+
- Decoupling business logic from framework-specific state management
|
|
733
|
+
- 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
|
+
|
|
761
|
+
#### Generator-Based Workflows with Runner & Ensemble
|
|
762
|
+
|
|
763
|
+
Run complex, multi-step workflows imperatively using generators:
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
import { runWithRunner, runWithEnsemble } from "@doeixd/machine/multi";
|
|
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)
|
|
790
|
+
|
|
791
|
+
For non-UI environments where a stable object reference is critical, `createMutableMachine` provides a highly imperative API with direct in-place mutations.
|
|
792
|
+
|
|
793
|
+
**Key Characteristics:**
|
|
794
|
+
- **Stable Object Reference**: The machine is a single object whose properties mutate in place
|
|
795
|
+
- **Direct Imperative API**: Call transitions like methods (`machine.login('user')`) with immediate updates
|
|
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
|
+
}),
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const auth = createMutableMachine(
|
|
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;
|
|
832
|
+
|
|
833
|
+
console.log(auth.status); // 'loggedOut'
|
|
834
|
+
|
|
835
|
+
auth.login('alice'); // Mutates in place
|
|
836
|
+
|
|
837
|
+
console.log(auth.status); // 'loggedIn'
|
|
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 };
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const player = createMutableMachine(
|
|
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)
|
|
919
|
+
|
|
920
|
+
**The Pattern**: A stateful wrapper that handles internal reassignments for you.
|
|
921
|
+
|
|
922
|
+
```typescript
|
|
923
|
+
// Without Runner (verbose reassignment chain)
|
|
924
|
+
let machine = createCounterMachine();
|
|
925
|
+
machine = machine.increment();
|
|
926
|
+
machine = machine.add(5);
|
|
927
|
+
machine = machine.reset();
|
|
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)
|
|
952
|
+
|
|
953
|
+
**The Pattern**: Decouples machine logic from framework-specific state management.
|
|
954
|
+
|
|
955
|
+
```typescript
|
|
956
|
+
// Your pure machine logic
|
|
957
|
+
const factories = {
|
|
958
|
+
idle: (ctx) => createMachine(ctx, { fetch: () => { /* ... */ } }),
|
|
959
|
+
loading: (ctx) => createMachine(ctx, { succeed: (data) => { /* ... */ } }),
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// Your framework's state (React example)
|
|
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();
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
**How it Works:**
|
|
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.
|
|
996
|
+
|
|
997
|
+
##### Deep Dive: Mutable Machine (createMutableMachine)
|
|
998
|
+
|
|
999
|
+
**The Pattern**: A single stable object whose properties mutate in place.
|
|
1000
|
+
|
|
1001
|
+
```typescript
|
|
1002
|
+
// Single object reference that mutates
|
|
1003
|
+
const player = createMutableMachine(
|
|
1004
|
+
{ state: 'idle', hp: 100 },
|
|
1005
|
+
factories,
|
|
1006
|
+
(ctx) => ctx.state // Accessor function - refactor-safe
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
// Keep the reference - it will always reflect current state
|
|
1010
|
+
const playerRef = player;
|
|
1011
|
+
|
|
1012
|
+
player.takeDamage(10);
|
|
1013
|
+
console.log(playerRef.hp); // 90 - same object!
|
|
1014
|
+
console.log(playerRef === player); // true
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
**How it Works:**
|
|
1018
|
+
- Uses a JavaScript Proxy to merge context properties with machine methods
|
|
1019
|
+
- Transitions are pure functions that return the next context (not a new machine)
|
|
1020
|
+
- When a transition is called, the proxy overwrites the context object's properties in place
|
|
1021
|
+
- The object reference never changes; properties mutate
|
|
1022
|
+
|
|
1023
|
+
**When to Use:**
|
|
1024
|
+
- Backend services (session management, long-running processes)
|
|
1025
|
+
- Game development (high-performance loops where allocation matters)
|
|
1026
|
+
- CLI tools and scripts (orchestrating steps)
|
|
1027
|
+
- Non-UI environments where a stable reference is critical
|
|
1028
|
+
- Performance-critical code where garbage collection matters
|
|
1029
|
+
|
|
1030
|
+
**Never Use For:**
|
|
1031
|
+
- React, Solid, Vue, or any reactive UI framework
|
|
1032
|
+
- Anything where you need state history or time-travel debugging
|
|
1033
|
+
- Systems where multiple parts read stale references
|
|
1034
|
+
|
|
1035
|
+
**Analogy**: A go-kart. Stripped down for performance in a specific environment (the backend). No safety features like immutability, not built for daily-driver complexity, but incredibly direct and efficient on the race track.
|
|
1036
|
+
|
|
1037
|
+
#### Class-Based Approach: MultiMachine (createMultiMachine)
|
|
1038
|
+
|
|
1039
|
+
For developers who prefer object-oriented patterns, `createMultiMachine` provides a class-based wrapper around the Ensemble pattern.
|
|
1040
|
+
|
|
1041
|
+
```typescript
|
|
1042
|
+
import { createMultiMachine, MultiMachineBase } from "@doeixd/machine/multi";
|
|
1043
|
+
|
|
1044
|
+
type CounterContext = { count: number };
|
|
1045
|
+
|
|
1046
|
+
class CounterMachine extends MultiMachineBase<CounterContext> {
|
|
1047
|
+
increment() {
|
|
1048
|
+
this.setContext({ count: this.context.count + 1 });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
add(n: number) {
|
|
1052
|
+
this.setContext({ count: this.context.count + n });
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
reset() {
|
|
1056
|
+
this.setContext({ count: 0 });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const store = {
|
|
1061
|
+
getContext: () => ({ count: 0 }),
|
|
1062
|
+
setContext: (ctx) => { /* update your framework's state */ }
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
const machine = createMultiMachine(CounterMachine, store);
|
|
1066
|
+
|
|
1067
|
+
// Direct method calls - feels like traditional OOP
|
|
1068
|
+
machine.increment();
|
|
1069
|
+
console.log(machine.count); // 1
|
|
1070
|
+
|
|
1071
|
+
machine.add(5);
|
|
1072
|
+
console.log(machine.count); // 6
|
|
1073
|
+
|
|
1074
|
+
machine.reset();
|
|
1075
|
+
console.log(machine.count); // 0
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
**How it Works:**
|
|
1079
|
+
- Extend `MultiMachineBase<C>` to define your machine class
|
|
1080
|
+
- Methods in the class are your transitions
|
|
1081
|
+
- `this.context` gives read-only access to current state
|
|
1082
|
+
- `this.setContext()` updates the external store
|
|
1083
|
+
- `createMultiMachine()` returns a Proxy that merges context properties with class methods
|
|
1084
|
+
|
|
1085
|
+
**When to Use:**
|
|
1086
|
+
- You prefer class-based/OOP patterns
|
|
1087
|
+
- You want familiar `this` binding and method calls
|
|
1088
|
+
- Complex machines with lots of state logic (easier to organize in a class)
|
|
1089
|
+
- Integrating with existing OOP codebases
|
|
1090
|
+
|
|
1091
|
+
**Benefits vs. Ensemble:**
|
|
1092
|
+
- More familiar syntax for OOP developers
|
|
1093
|
+
- Methods are co-located with state they manage
|
|
1094
|
+
- Can use class constructors for initialization
|
|
1095
|
+
- Easier to extend or subclass if needed
|
|
1096
|
+
|
|
1097
|
+
**Benefits vs. Runner:**
|
|
1098
|
+
- For global/shared state (like Ensemble)
|
|
1099
|
+
- Better code organization for complex machines
|
|
1100
|
+
- Not limited to immutable snapshots
|
|
1101
|
+
|
|
622
1102
|
### Generator-Based Composition
|
|
623
1103
|
|
|
624
1104
|
For complex multi-step workflows, use generator-based composition. This provides an imperative, procedural style while maintaining immutability and type safety.
|
|
@@ -685,23 +1165,123 @@ const result = run(function* (m) {
|
|
|
685
1165
|
|
|
686
1166
|
### React Integration
|
|
687
1167
|
|
|
1168
|
+
`@doeixd/machine` offers a full suite of React hooks for everything from simple component state to complex, performance-optimized applications.
|
|
1169
|
+
|
|
1170
|
+
Get started by importing the hooks:
|
|
688
1171
|
```typescript
|
|
1172
|
+
import { useMachine, useMachineSelector } from '@doeixd/machine/react';
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
#### `useMachine` - For Local Component State
|
|
1176
|
+
|
|
1177
|
+
This is the primary hook for managing self-contained state within a component. It returns the reactive `machine` instance and a stable `actions` object for triggering transitions, providing an ergonomic and type-safe API.
|
|
1178
|
+
|
|
1179
|
+
```tsx
|
|
689
1180
|
import { useMachine } from "@doeixd/machine/react";
|
|
1181
|
+
import { createCounterMachine } from "./counterMachine";
|
|
690
1182
|
|
|
691
1183
|
function Counter() {
|
|
692
|
-
const [machine,
|
|
1184
|
+
const [machine, actions] = useMachine(() => createCounterMachine({ count: 0 }));
|
|
693
1185
|
|
|
694
1186
|
return (
|
|
695
1187
|
<div>
|
|
696
1188
|
<p>Count: {machine.context.count}</p>
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
</button>
|
|
1189
|
+
{/* Call transitions directly from the stable actions object */}
|
|
1190
|
+
<button onClick={() => actions.increment()}>Increment</button>
|
|
1191
|
+
<button onClick={() => actions.add(5)}>Add 5</button>
|
|
1192
|
+
</div>
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
#### `useMachineSelector` - For Performance
|
|
1198
|
+
|
|
1199
|
+
To prevent unnecessary re-renders in components that only care about a small part of a large machine's state, use `useMachineSelector`. It subscribes a component to a specific slice of the state, and only triggers a re-render when that slice changes.
|
|
1200
|
+
|
|
1201
|
+
```tsx
|
|
1202
|
+
function UserNameDisplay({ machine }) {
|
|
1203
|
+
// This component will NOT re-render if other parts of the machine's
|
|
1204
|
+
// context (e.g., user settings) change.
|
|
1205
|
+
const userName = useMachineSelector(machine, (m) => m.context.user.name);
|
|
1206
|
+
|
|
1207
|
+
return <p>User: {userName}</p>;
|
|
1208
|
+
}
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
#### `createMachineContext` - For Sharing State
|
|
1212
|
+
|
|
1213
|
+
To avoid passing your machine and actions through many layers of props ("prop-drilling"), `createMachineContext` provides a typed Context `Provider` and consumer hooks to share state across your component tree.
|
|
1214
|
+
|
|
1215
|
+
```tsx
|
|
1216
|
+
import { createMachineContext, useMachine } from "@doeixd/machine/react";
|
|
1217
|
+
|
|
1218
|
+
// 1. Create the context
|
|
1219
|
+
const { Provider, useSelector, useMachineActions } = createMachineContext<AuthMachine>();
|
|
1220
|
+
|
|
1221
|
+
// 2. Provide the machine in your root component
|
|
1222
|
+
function App() {
|
|
1223
|
+
const [machine, actions] = useMachine(() => createAuthMachine());
|
|
1224
|
+
return (
|
|
1225
|
+
<Provider machine={machine} actions={actions}>
|
|
1226
|
+
<Header />
|
|
1227
|
+
</Provider>
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// 3. Consume it in any child component
|
|
1232
|
+
function Header() {
|
|
1233
|
+
const status = useSelector(m => m.context.status);
|
|
1234
|
+
const actions = useMachineActions();
|
|
1235
|
+
|
|
1236
|
+
return (
|
|
1237
|
+
<header>
|
|
1238
|
+
{status === 'loggedIn' ? (
|
|
1239
|
+
<button onClick={() => actions.logout()}>Logout</button>
|
|
1240
|
+
) : (
|
|
1241
|
+
<button onClick={() => actions.login('user', 'pass')}>Login</button>
|
|
1242
|
+
)}
|
|
1243
|
+
</header>
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
#### `useEnsemble` - For Advanced Integration
|
|
1249
|
+
|
|
1250
|
+
For maximum testability and portability, `useEnsemble` decouples your pure, framework-agnostic state logic from React's state management. Your machine logic becomes fully portable, and React's `useState` simply acts as the "state store" for the ensemble.
|
|
1251
|
+
|
|
1252
|
+
```tsx
|
|
1253
|
+
import { useEnsemble } from "@doeixd/machine/react";
|
|
1254
|
+
import { fetchFactories } from "./fetchFactories"; // Pure, framework-agnostic logic
|
|
1255
|
+
|
|
1256
|
+
function DataFetcher() {
|
|
1257
|
+
const ensemble = useEnsemble(
|
|
1258
|
+
{ status: 'idle', data: null }, // Initial context
|
|
1259
|
+
fetchFactories, // Your pure machine factories
|
|
1260
|
+
(ctx) => ctx.status // Discriminant function
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
return (
|
|
1264
|
+
<div>
|
|
1265
|
+
<p>Status: {ensemble.context.status}</p>
|
|
1266
|
+
{ensemble.context.status === 'idle' && (
|
|
1267
|
+
<button onClick={() => ensemble.actions.fetch('/api/data')}>
|
|
1268
|
+
Fetch
|
|
1269
|
+
</button>
|
|
1270
|
+
)}
|
|
700
1271
|
</div>
|
|
701
1272
|
);
|
|
702
1273
|
}
|
|
703
1274
|
```
|
|
704
1275
|
|
|
1276
|
+
#### Which Hook Should I Use?
|
|
1277
|
+
|
|
1278
|
+
| Hook | Best For | Key Feature |
|
|
1279
|
+
| :--- | :--- | :--- |
|
|
1280
|
+
| **`useMachine`** | **Local component state** | The simplest way to get started. Ergonomic `[machine, actions]` API. |
|
|
1281
|
+
| **`useMachineSelector`** | **Performance optimization** | Prevents re-renders by subscribing to slices of state. |
|
|
1282
|
+
| **`createMachineContext`** | **Sharing state / DI** | Avoids prop-drilling a machine through the component tree. |
|
|
1283
|
+
| **`useEnsemble`** | **Complex or shared state** | Decouples business logic from React for maximum portability and testability. |
|
|
1284
|
+
|
|
705
1285
|
### Solid.js Integration
|
|
706
1286
|
|
|
707
1287
|
Comprehensive Solid.js integration with signals, stores, and fine-grained reactivity:
|
|
@@ -884,6 +1464,269 @@ pipeTransitions(
|
|
|
884
1464
|
);
|
|
885
1465
|
```
|
|
886
1466
|
|
|
1467
|
+
## 📊 Statechart Extraction & Visualization
|
|
1468
|
+
|
|
1469
|
+
**NEW**: Generate formal statechart definitions from your TypeScript machines for visualization and documentation.
|
|
1470
|
+
|
|
1471
|
+
### Overview
|
|
1472
|
+
|
|
1473
|
+
The statechart extraction system performs build-time static analysis to generate [XState](https://xstate.js.org/)-compatible JSON from your type-safe machines. This enables:
|
|
1474
|
+
|
|
1475
|
+
- 🎨 **Visualization** in [Stately Viz](https://stately.ai/viz) and other tools
|
|
1476
|
+
- 📖 **Documentation** with auto-generated state diagrams
|
|
1477
|
+
- ✅ **Formal verification** using XState tooling
|
|
1478
|
+
- 🔄 **Team communication** via visual state charts
|
|
1479
|
+
|
|
1480
|
+
### Quick Example
|
|
1481
|
+
|
|
1482
|
+
**1. Annotate your machines with metadata:**
|
|
1483
|
+
|
|
1484
|
+
```typescript
|
|
1485
|
+
import { MachineBase } from '@doeixd/machine';
|
|
1486
|
+
import { transitionTo, describe, action, guarded } from '@doeixd/machine/primitives';
|
|
1487
|
+
|
|
1488
|
+
class LoggedOut extends MachineBase<{ status: 'loggedOut' }> {
|
|
1489
|
+
login = describe(
|
|
1490
|
+
"Start the login process",
|
|
1491
|
+
action(
|
|
1492
|
+
{ name: "logAttempt", description: "Track login attempts" },
|
|
1493
|
+
transitionTo(LoggingIn, (username: string) => new LoggingIn({ username }))
|
|
1494
|
+
)
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
class LoggingIn extends MachineBase<{ status: 'loggingIn'; username: string }> {
|
|
1499
|
+
success = transitionTo(LoggedIn, (token: string) => new LoggedIn({ token }));
|
|
1500
|
+
failure = transitionTo(LoggedOut, () => new LoggedOut());
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
class LoggedIn extends MachineBase<{ status: 'loggedIn'; token: string }> {
|
|
1504
|
+
logout = describe(
|
|
1505
|
+
"Log out and clear session",
|
|
1506
|
+
action(
|
|
1507
|
+
{ name: "clearSession" },
|
|
1508
|
+
transitionTo(LoggedOut, () => new LoggedOut())
|
|
1509
|
+
)
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
deleteAccount = guarded(
|
|
1513
|
+
{ name: "isAdmin", description: "Only admins can delete accounts" },
|
|
1514
|
+
transitionTo(LoggedOut, () => new LoggedOut())
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
```
|
|
1518
|
+
|
|
1519
|
+
**2. Create extraction config (`.statechart.config.ts`):**
|
|
1520
|
+
|
|
1521
|
+
```typescript
|
|
1522
|
+
import type { ExtractionConfig } from '@doeixd/machine';
|
|
1523
|
+
|
|
1524
|
+
export default {
|
|
1525
|
+
machines: [{
|
|
1526
|
+
input: 'src/auth.ts',
|
|
1527
|
+
classes: ['LoggedOut', 'LoggingIn', 'LoggedIn'],
|
|
1528
|
+
output: 'statecharts/auth.json',
|
|
1529
|
+
id: 'auth',
|
|
1530
|
+
initialState: 'LoggedOut'
|
|
1531
|
+
}],
|
|
1532
|
+
verbose: true
|
|
1533
|
+
} satisfies ExtractionConfig;
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
**3. Run extraction:**
|
|
1537
|
+
|
|
1538
|
+
```bash
|
|
1539
|
+
npm run extract
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
**4. Generated output (`statecharts/auth.json`):**
|
|
1543
|
+
|
|
1544
|
+
```json
|
|
1545
|
+
{
|
|
1546
|
+
"id": "auth",
|
|
1547
|
+
"initial": "LoggedOut",
|
|
1548
|
+
"states": {
|
|
1549
|
+
"LoggedOut": {
|
|
1550
|
+
"on": {
|
|
1551
|
+
"login": {
|
|
1552
|
+
"target": "LoggingIn",
|
|
1553
|
+
"description": "Start the login process",
|
|
1554
|
+
"actions": ["logAttempt"]
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
},
|
|
1558
|
+
"LoggingIn": {
|
|
1559
|
+
"on": {
|
|
1560
|
+
"success": { "target": "LoggedIn" },
|
|
1561
|
+
"failure": { "target": "LoggedOut" }
|
|
1562
|
+
}
|
|
1563
|
+
},
|
|
1564
|
+
"LoggedIn": {
|
|
1565
|
+
"on": {
|
|
1566
|
+
"logout": {
|
|
1567
|
+
"target": "LoggedOut",
|
|
1568
|
+
"description": "Log out and clear session",
|
|
1569
|
+
"actions": ["clearSession"]
|
|
1570
|
+
},
|
|
1571
|
+
"deleteAccount": {
|
|
1572
|
+
"target": "LoggedOut",
|
|
1573
|
+
"cond": "isAdmin"
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
```
|
|
1580
|
+
|
|
1581
|
+
**5. Visualize in [Stately Viz](https://stately.ai/viz):**
|
|
1582
|
+
|
|
1583
|
+
Paste the JSON into Stately Viz to see your state machine as an interactive diagram!
|
|
1584
|
+
|
|
1585
|
+
### Metadata Primitives
|
|
1586
|
+
|
|
1587
|
+
The extraction system recognizes these annotation primitives:
|
|
1588
|
+
|
|
1589
|
+
| Primitive | Purpose | Extracted As |
|
|
1590
|
+
|-----------|---------|--------------|
|
|
1591
|
+
| `transitionTo(Target, impl)` | Declare target state | `"target": "TargetClass"` |
|
|
1592
|
+
| `describe(text, transition)` | Add description | `"description": "..."` |
|
|
1593
|
+
| `guarded(guard, transition)` | Add guard condition | `"cond": "guardName"` |
|
|
1594
|
+
| `action(action, transition)` | Add side effect | `"actions": ["actionName"]` |
|
|
1595
|
+
| `invoke(service, impl)` | Async service | `"invoke": [...]` |
|
|
1596
|
+
|
|
1597
|
+
**All primitives are identity functions** - they have **zero runtime overhead**. They exist purely for:
|
|
1598
|
+
1. Type-level documentation
|
|
1599
|
+
2. Build-time extraction
|
|
1600
|
+
3. IDE autocomplete
|
|
1601
|
+
|
|
1602
|
+
### CLI Usage
|
|
1603
|
+
|
|
1604
|
+
```bash
|
|
1605
|
+
# Extract from config
|
|
1606
|
+
npx tsx scripts/extract-statechart.ts --config .statechart.config.ts
|
|
1607
|
+
|
|
1608
|
+
# Extract single machine
|
|
1609
|
+
npx tsx scripts/extract-statechart.ts \
|
|
1610
|
+
--input src/machine.ts \
|
|
1611
|
+
--id myMachine \
|
|
1612
|
+
--classes State1,State2 \
|
|
1613
|
+
--initial State1
|
|
1614
|
+
|
|
1615
|
+
# Watch mode
|
|
1616
|
+
npx tsx scripts/extract-statechart.ts --config .statechart.config.ts --watch
|
|
1617
|
+
|
|
1618
|
+
# With validation
|
|
1619
|
+
npx tsx scripts/extract-statechart.ts --config .statechart.config.ts --validate
|
|
1620
|
+
```
|
|
1621
|
+
|
|
1622
|
+
### npm Scripts
|
|
1623
|
+
|
|
1624
|
+
Add to your `package.json`:
|
|
1625
|
+
|
|
1626
|
+
```json
|
|
1627
|
+
{
|
|
1628
|
+
"scripts": {
|
|
1629
|
+
"extract": "tsx scripts/extract-statechart.ts --config .statechart.config.ts",
|
|
1630
|
+
"extract:watch": "tsx scripts/extract-statechart.ts --config .statechart.config.ts --watch"
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
```
|
|
1634
|
+
|
|
1635
|
+
### How It Works
|
|
1636
|
+
|
|
1637
|
+
The extraction system uses **AST-based static analysis**:
|
|
1638
|
+
|
|
1639
|
+
1. **Parse TypeScript source** using ts-morph (TypeScript Compiler API)
|
|
1640
|
+
2. **Find DSL primitive calls** in class property initializers
|
|
1641
|
+
3. **Extract literal arguments** from the Abstract Syntax Tree
|
|
1642
|
+
4. **Resolve class name identifiers** to target states
|
|
1643
|
+
5. **Generate XState-compatible JSON** with full metadata
|
|
1644
|
+
|
|
1645
|
+
**Why AST-based?** TypeScript's type system resolves generic parameters in branded types (`WithMeta<F, M>`) as constraints rather than concrete values. AST parsing reads the actual source code, bypassing type system limitations. This is the same approach used by XState's extraction tooling.
|
|
1646
|
+
|
|
1647
|
+
### Static Extraction API (Build-Time)
|
|
1648
|
+
|
|
1649
|
+
```typescript
|
|
1650
|
+
import { extractMachine, extractMachines } from '@doeixd/machine';
|
|
1651
|
+
import { Project } from 'ts-morph';
|
|
1652
|
+
|
|
1653
|
+
// Extract single machine
|
|
1654
|
+
const project = new Project();
|
|
1655
|
+
project.addSourceFilesAtPaths('src/**/*.ts');
|
|
1656
|
+
|
|
1657
|
+
const chart = extractMachine({
|
|
1658
|
+
input: 'src/auth.ts',
|
|
1659
|
+
classes: ['LoggedOut', 'LoggedIn'],
|
|
1660
|
+
id: 'auth',
|
|
1661
|
+
initialState: 'LoggedOut'
|
|
1662
|
+
}, project);
|
|
1663
|
+
|
|
1664
|
+
console.log(JSON.stringify(chart, null, 2));
|
|
1665
|
+
|
|
1666
|
+
// Extract multiple machines
|
|
1667
|
+
const charts = extractMachines({
|
|
1668
|
+
machines: [
|
|
1669
|
+
{ input: 'src/auth.ts', classes: [...], id: 'auth', initialState: 'LoggedOut' },
|
|
1670
|
+
{ input: 'src/fetch.ts', classes: [...], id: 'fetch', initialState: 'Idle' }
|
|
1671
|
+
],
|
|
1672
|
+
verbose: true
|
|
1673
|
+
});
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
### Runtime Extraction API
|
|
1677
|
+
|
|
1678
|
+
Extract statecharts from **running machine instances** without requiring source code access:
|
|
1679
|
+
|
|
1680
|
+
```typescript
|
|
1681
|
+
import { generateStatechart, extractFromInstance } from '@doeixd/machine';
|
|
1682
|
+
|
|
1683
|
+
// Create machine instances (annotated with DSL primitives)
|
|
1684
|
+
const loggedOutMachine = new LoggedOut({ status: 'loggedOut' });
|
|
1685
|
+
const loggedInMachine = new LoggedIn({ status: 'loggedIn', token: 'abc' });
|
|
1686
|
+
|
|
1687
|
+
// Generate complete statechart from multiple states
|
|
1688
|
+
const chart = generateStatechart({
|
|
1689
|
+
LoggedOut: loggedOutMachine,
|
|
1690
|
+
LoggedIn: loggedInMachine
|
|
1691
|
+
}, {
|
|
1692
|
+
id: 'auth',
|
|
1693
|
+
initial: 'LoggedOut'
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
// Or extract from a single instance
|
|
1697
|
+
const singleChart = extractFromInstance(loggedOutMachine, {
|
|
1698
|
+
id: 'auth',
|
|
1699
|
+
stateName: 'LoggedOut'
|
|
1700
|
+
});
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
**Use cases:**
|
|
1704
|
+
- 🐛 Debug production machines without source access
|
|
1705
|
+
- 🌐 Extract statecharts in browser DevTools
|
|
1706
|
+
- 🧪 Generate diagrams from test instances
|
|
1707
|
+
- 📦 Work with dynamically created machines
|
|
1708
|
+
|
|
1709
|
+
The DSL primitives (`transitionTo`, `describe`, etc.) attach metadata at runtime via non-enumerable Symbols with zero performance overhead.
|
|
1710
|
+
|
|
1711
|
+
### Full Documentation
|
|
1712
|
+
|
|
1713
|
+
For comprehensive documentation including:
|
|
1714
|
+
- Type-level metadata DSL reference
|
|
1715
|
+
- Configuration options
|
|
1716
|
+
- Limitations and gotchas
|
|
1717
|
+
- Troubleshooting guide
|
|
1718
|
+
- Advanced usage patterns
|
|
1719
|
+
|
|
1720
|
+
See **[docs/statechart-extraction.md](./docs/statechart-extraction.md)**
|
|
1721
|
+
|
|
1722
|
+
### Examples
|
|
1723
|
+
|
|
1724
|
+
Complete annotated examples in the `examples/` directory:
|
|
1725
|
+
- [`examples/authMachine.ts`](./examples/authMachine.ts) - Authentication flow with guards and actions
|
|
1726
|
+
- [`examples/fetchMachine.ts`](./examples/fetchMachine.ts) - Data fetching with invoke services
|
|
1727
|
+
- [`examples/formMachine.ts`](./examples/formMachine.ts) - Multi-step wizard
|
|
1728
|
+
- [`examples/trafficLightMachine.ts`](./examples/trafficLightMachine.ts) - Simple cyclic machine
|
|
1729
|
+
|
|
887
1730
|
## Philosophy & Design Principles
|
|
888
1731
|
|
|
889
1732
|
### 1. Type-State Programming First
|
|
@@ -899,13 +1742,7 @@ This isn't just a feature—it's the fundamental way you should think about stat
|
|
|
899
1742
|
|
|
900
1743
|
### 2. Minimal Primitives
|
|
901
1744
|
|
|
902
|
-
The core library provides
|
|
903
|
-
- `Machine<C>` and `AsyncMachine<C>` types
|
|
904
|
-
- `createMachine()` and `createAsyncMachine()` functions
|
|
905
|
-
- `runMachine()` for async runtime
|
|
906
|
-
- Basic composition utilities
|
|
907
|
-
|
|
908
|
-
Everything else is built on top of these primitives. We give you the foundation; you build what you need.
|
|
1745
|
+
The core library provides one true primitive: the pure, immutable Machine. Everything else is built on this foundation. To handle real-world complexity, we provide optional factories (createRunner, createEnsemble) that compose this primitive into different operational patterns. This keeps the core minimal while providing powerful, opt-in capabilities for ergonomics and framework integration.
|
|
909
1746
|
|
|
910
1747
|
### 3. TypeScript as the Compiler
|
|
911
1748
|
|
|
@@ -1061,6 +1898,108 @@ runAsync<C, T>(flow: (m: Machine<C>) => AsyncGenerator<...>, initial: Machine<C>
|
|
|
1061
1898
|
stepAsync<C>(machine: Machine<C>): AsyncGenerator<...>
|
|
1062
1899
|
```
|
|
1063
1900
|
|
|
1901
|
+
### Multi Module (Stateful Controllers & Framework Integration)
|
|
1902
|
+
|
|
1903
|
+
**Import:** `import { ... } from "@doeixd/machine/multi"`
|
|
1904
|
+
|
|
1905
|
+
#### Types
|
|
1906
|
+
|
|
1907
|
+
```typescript
|
|
1908
|
+
// Stateful controller for a single machine
|
|
1909
|
+
type Runner<M extends Machine<any>> = {
|
|
1910
|
+
readonly state: M;
|
|
1911
|
+
readonly context: Context<M>;
|
|
1912
|
+
readonly actions: BoundTransitions<M>;
|
|
1913
|
+
setState(newState: M): void;
|
|
1914
|
+
};
|
|
1915
|
+
|
|
1916
|
+
// Mapped type: all transition methods pre-bound to update a Runner
|
|
1917
|
+
type BoundTransitions<M extends Machine<any>> = {
|
|
1918
|
+
[K in TransitionNames<M>]: (...args: TransitionArgs<M, K>) => ReturnType<M[K]>;
|
|
1919
|
+
};
|
|
1920
|
+
|
|
1921
|
+
// External state storage interface for Ensemble
|
|
1922
|
+
interface StateStore<C extends object> {
|
|
1923
|
+
getContext: () => C;
|
|
1924
|
+
setContext: (newContext: C) => void;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// Orchestration engine for global state
|
|
1928
|
+
type Ensemble<AllMachines extends Machine<any>, C extends object> = {
|
|
1929
|
+
readonly context: C;
|
|
1930
|
+
readonly state: AllMachines;
|
|
1931
|
+
readonly actions: AllTransitions<AllMachines>;
|
|
1932
|
+
};
|
|
1933
|
+
```
|
|
1934
|
+
|
|
1935
|
+
#### Functions
|
|
1936
|
+
|
|
1937
|
+
```typescript
|
|
1938
|
+
// Create a stateful wrapper for local state management
|
|
1939
|
+
createRunner<M extends Machine<any>>(
|
|
1940
|
+
initialMachine: M,
|
|
1941
|
+
onChange?: (newState: M) => void
|
|
1942
|
+
): Runner<M>
|
|
1943
|
+
|
|
1944
|
+
// Create an ensemble for framework-agnostic global state orchestration
|
|
1945
|
+
createEnsemble<
|
|
1946
|
+
C extends object,
|
|
1947
|
+
F extends Record<string, (context: C) => Machine<C>>
|
|
1948
|
+
>(
|
|
1949
|
+
store: StateStore<C>,
|
|
1950
|
+
factories: F,
|
|
1951
|
+
getDiscriminant: (context: C) => keyof F // Accessor function - refactor-safe
|
|
1952
|
+
): Ensemble<ReturnType<F[keyof F]>, C>
|
|
1953
|
+
|
|
1954
|
+
// Execute a generator workflow with a Runner
|
|
1955
|
+
runWithRunner<M extends Machine<any>, T>(
|
|
1956
|
+
flow: (runner: Runner<M>) => Generator<any, T, any>,
|
|
1957
|
+
initialMachine: M
|
|
1958
|
+
): T
|
|
1959
|
+
|
|
1960
|
+
// Execute a generator workflow with an Ensemble
|
|
1961
|
+
runWithEnsemble<AllMachines extends Machine<any>, C extends object, T>(
|
|
1962
|
+
flow: (ensemble: Ensemble<AllMachines, C>) => Generator<any, T, any>,
|
|
1963
|
+
ensemble: Ensemble<AllMachines, C>
|
|
1964
|
+
): T
|
|
1965
|
+
|
|
1966
|
+
// Create a mutable machine (EXPERIMENTAL - use with caution)
|
|
1967
|
+
createMutableMachine<
|
|
1968
|
+
C extends object,
|
|
1969
|
+
F extends Record<string, (context: C) => Machine<C>>
|
|
1970
|
+
>(
|
|
1971
|
+
sharedContext: C,
|
|
1972
|
+
factories: F,
|
|
1973
|
+
getDiscriminant: (context: C) => keyof F // Accessor function - refactor-safe
|
|
1974
|
+
): MutableMachine<C, ReturnType<F[keyof F]>>
|
|
1975
|
+
```
|
|
1976
|
+
|
|
1977
|
+
#### Additional Types (Multi Module)
|
|
1978
|
+
|
|
1979
|
+
```typescript
|
|
1980
|
+
// Mutable machine combining context and transitions (EXPERIMENTAL)
|
|
1981
|
+
type MutableMachine<C extends object, AllMachines extends Machine<any>> = C &
|
|
1982
|
+
AllTransitions<AllMachines>;
|
|
1983
|
+
|
|
1984
|
+
// Base class for MultiMachine OOP approach
|
|
1985
|
+
abstract class MultiMachineBase<C extends object> {
|
|
1986
|
+
protected store: StateStore<C>;
|
|
1987
|
+
protected get context(): C;
|
|
1988
|
+
protected setContext(newContext: C): void;
|
|
1989
|
+
}
|
|
1990
|
+
```
|
|
1991
|
+
|
|
1992
|
+
#### Additional Functions (Multi Module)
|
|
1993
|
+
|
|
1994
|
+
```typescript
|
|
1995
|
+
// Create a class-based MultiMachine instance
|
|
1996
|
+
createMultiMachine<C extends object, T extends MultiMachineBase<C>>(
|
|
1997
|
+
MachineClass: new (store: StateStore<C>) => T,
|
|
1998
|
+
store: StateStore<C>,
|
|
1999
|
+
getDiscriminant?: (context: C) => string // Optional accessor function - refactor-safe
|
|
2000
|
+
): C & T
|
|
2001
|
+
```
|
|
2002
|
+
|
|
1064
2003
|
## License
|
|
1065
2004
|
|
|
1066
2005
|
MIT
|