@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 CHANGED
@@ -1,8 +1,10 @@
1
+ [![npm version](https://badge.fury.io/js/@doeixd%2Fmachine.svg)](https://badge.fury.io/js/@doeixd%2Fmachine)
2
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
1
3
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/doeixd/machine)
2
4
 
3
5
  # Machine
4
6
 
5
- A minimal, type-safe state machine library for TypeScript built on mathematical foundations.
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
- readonly context: C; // Encodes the current state (s ∈ S)
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, dispatch] = useMachine(() => createCounterMachine());
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
- <button onClick={() => dispatch({ type: "increment", args: [] })}>
698
- Increment
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 only the essential building blocks:
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