@doeixd/machine 0.0.7 β 0.0.9
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 +130 -272
- package/dist/cjs/development/index.js +1121 -18
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +5 -5
- package/dist/esm/development/index.js +1121 -18
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +5 -5
- package/dist/types/extract.d.ts.map +1 -1
- package/dist/types/index.d.ts +64 -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 +205 -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 +111 -6
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +10 -7
- package/scripts/extract-statechart.ts +351 -0
- package/src/adapters.ts +407 -0
- package/src/extract.ts +60 -20
- package/src/index.ts +201 -8
- package/src/middleware.ts +2325 -0
- package/src/primitives.ts +454 -3
- package/src/runtime-extract.ts +15 -0
- package/src/utils.ts +221 -6
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)
|
|
@@ -656,6 +662,41 @@ const alice = buildUser({ id: 1, name: "Alice" });
|
|
|
656
662
|
const bob = buildUser({ id: 2, name: "Bob" });
|
|
657
663
|
```
|
|
658
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
|
+
|
|
659
700
|
### Type Utilities
|
|
660
701
|
|
|
661
702
|
#### Type Extraction
|
|
@@ -711,7 +752,7 @@ For advanced use cases, the library provides optional patterns that offer better
|
|
|
711
752
|
|
|
712
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.
|
|
713
754
|
|
|
714
|
-
**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.
|
|
715
756
|
|
|
716
757
|
### Managed State with Runner & Ensemble
|
|
717
758
|
|
|
@@ -749,312 +790,129 @@ if (runner.state.context.status === 'loggedIn') {
|
|
|
749
790
|
- Perfect for React hooks, component state, or form handling
|
|
750
791
|
- Type-safe state narrowing still works
|
|
751
792
|
|
|
752
|
-
#### Ensemble:
|
|
793
|
+
#### Ensemble: Coordinating Multiple Machines with Shared Context
|
|
753
794
|
|
|
754
|
-
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.
|
|
755
796
|
|
|
756
797
|
```typescript
|
|
757
798
|
import { createEnsemble } from "@doeixd/machine/multi";
|
|
758
799
|
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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' };
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
const globalStore = {
|
|
808
|
+
getContext: () => appState,
|
|
809
|
+
setContext: (newState) => setAppState(newState)
|
|
763
810
|
};
|
|
764
811
|
|
|
765
|
-
//
|
|
766
|
-
const
|
|
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, {
|
|
767
824
|
idle: (ctx) => createMachine(ctx, {
|
|
768
|
-
fetch: () =>
|
|
825
|
+
fetch: async () => {
|
|
826
|
+
const items = await api.fetch();
|
|
827
|
+
return { ...ctx, data: { status: 'success', items } };
|
|
828
|
+
}
|
|
769
829
|
}),
|
|
770
|
-
loading: (ctx) => createMachine(ctx, {
|
|
771
|
-
|
|
772
|
-
|
|
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' } })
|
|
773
838
|
}),
|
|
774
|
-
|
|
775
|
-
|
|
839
|
+
open: (ctx) => createMachine(ctx, {
|
|
840
|
+
close: () => ({ ...ctx, ui: { ...ctx.ui, modal: 'closed' } })
|
|
776
841
|
})
|
|
777
|
-
};
|
|
842
|
+
}, (ctx) => ctx.ui.modal);
|
|
778
843
|
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
//
|
|
783
|
-
ensemble.actions.fetch(); // Transitions to loading
|
|
784
|
-
console.log(ensemble.context.status); // 'loading'
|
|
785
|
-
|
|
786
|
-
// Type narrowing
|
|
787
|
-
if (ensemble.state.context.status === 'success') {
|
|
788
|
-
console.log(ensemble.state.context.data); // TypeScript knows data exists
|
|
789
|
-
}
|
|
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
|
|
790
848
|
```
|
|
791
849
|
|
|
792
850
|
**Perfect for:**
|
|
793
|
-
-
|
|
794
|
-
-
|
|
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.
|
|
795
856
|
- Testing (swap the store for a test stub)
|
|
796
|
-
- Multiple framework support (same machine logic for React, Solid, Vue, etc.)
|
|
797
|
-
|
|
798
|
-
**Workflow Pattern:**
|
|
799
|
-
```typescript
|
|
800
|
-
// React example
|
|
801
|
-
function MyComponent() {
|
|
802
|
-
const [context, setContext] = useState(initialContext);
|
|
803
|
-
|
|
804
|
-
const store = {
|
|
805
|
-
getContext: () => context,
|
|
806
|
-
setContext: setContext
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
const ensemble = useMemo(() =>
|
|
810
|
-
createEnsemble(store, factories, (ctx) => ctx.status),
|
|
811
|
-
[context]
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
return (
|
|
815
|
-
<div>
|
|
816
|
-
<p>Status: {ensemble.context.status}</p>
|
|
817
|
-
<button onClick={() => ensemble.actions.fetch()}>Load Data</button>
|
|
818
|
-
</div>
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
```
|
|
822
|
-
|
|
823
|
-
#### Generator-Based Workflows with Runner & Ensemble
|
|
824
|
-
|
|
825
|
-
Run complex, multi-step workflows imperatively using generators:
|
|
826
|
-
|
|
827
|
-
```typescript
|
|
828
|
-
import { runWithRunner, runWithEnsemble } from "@doeixd/machine/multi";
|
|
829
|
-
|
|
830
|
-
// With Runner (local state)
|
|
831
|
-
const result = runWithRunner(function* (runner) {
|
|
832
|
-
yield runner.actions.increment();
|
|
833
|
-
yield runner.actions.add(10);
|
|
834
|
-
if (runner.context.count > 5) {
|
|
835
|
-
yield runner.actions.reset();
|
|
836
|
-
}
|
|
837
|
-
return runner.context;
|
|
838
|
-
}, createCounterMachine());
|
|
839
|
-
|
|
840
|
-
// With Ensemble (global state)
|
|
841
|
-
const result = runWithEnsemble(function* (ensemble) {
|
|
842
|
-
yield ensemble.actions.fetch();
|
|
843
|
-
yield ensemble.actions.process();
|
|
844
|
-
if (ensemble.context.status === 'success') {
|
|
845
|
-
yield ensemble.actions.commit();
|
|
846
|
-
}
|
|
847
|
-
return ensemble.context.data;
|
|
848
|
-
}, ensemble);
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
#### Mutable Machine (Experimental)
|
|
852
857
|
|
|
853
|
-
|
|
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.
|
|
854
859
|
|
|
855
|
-
**
|
|
856
|
-
|
|
857
|
-
- **Direct Imperative API**: Call transitions like methods (`machine.login('user')`) with immediate updates
|
|
858
|
-
- **No State History**: Previous states are not preserved (no time-travel debugging)
|
|
859
|
-
- **Not for Reactive UIs**: Won't trigger component re-renders in React, Solid, Vue, etc.
|
|
860
|
-
|
|
861
|
-
**Best for:**
|
|
862
|
-
- Backend services and game loops
|
|
863
|
-
- Complex synchronous scripts and data pipelines
|
|
864
|
-
- Non-UI environments where a stable state object is essential
|
|
865
|
-
|
|
866
|
-
**Example: Authentication State**
|
|
860
|
+
**Great for External Integration:**
|
|
861
|
+
The `StateStore` interface makes it trivial to sync with external systems:
|
|
867
862
|
|
|
868
863
|
```typescript
|
|
869
|
-
|
|
864
|
+
// Zustand store integration
|
|
865
|
+
import { create } from 'zustand';
|
|
870
866
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
867
|
+
const useAppStore = create<AppState>((set, get) => ({
|
|
868
|
+
// ... your Zustand store
|
|
869
|
+
}));
|
|
874
870
|
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
login: (username: string) => ({ status: 'loggedIn', username }),
|
|
879
|
-
}),
|
|
880
|
-
loggedIn: (ctx: AuthContext) => ({
|
|
881
|
-
context: ctx,
|
|
882
|
-
logout: () => ({ status: 'loggedOut' }),
|
|
883
|
-
}),
|
|
871
|
+
const zustandStore = {
|
|
872
|
+
getContext: () => useAppStore.getState(),
|
|
873
|
+
setContext: (newState) => useAppStore.setState(newState)
|
|
884
874
|
};
|
|
885
875
|
|
|
886
|
-
const
|
|
887
|
-
{ status: 'loggedOut' } as AuthContext,
|
|
888
|
-
authFactories,
|
|
889
|
-
(ctx) => ctx.status // Accessor function - refactor-safe
|
|
890
|
-
);
|
|
891
|
-
|
|
892
|
-
// Stable reference - keep this, the object will mutate
|
|
893
|
-
const userRef = auth;
|
|
894
|
-
|
|
895
|
-
console.log(auth.status); // 'loggedOut'
|
|
896
|
-
|
|
897
|
-
auth.login('alice'); // Mutates in place
|
|
898
|
-
|
|
899
|
-
console.log(auth.status); // 'loggedIn'
|
|
900
|
-
console.log(auth.username); // 'alice'
|
|
901
|
-
console.log(userRef === auth); // true - same object reference
|
|
902
|
-
```
|
|
876
|
+
const ensemble = createEnsemble(zustandStore, factories, (ctx) => ctx.status);
|
|
903
877
|
|
|
904
|
-
|
|
878
|
+
// Redux integration
|
|
879
|
+
import { store } from './reduxStore';
|
|
905
880
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
hp: number;
|
|
910
|
-
position: { x: number; y: number };
|
|
881
|
+
const reduxStore = {
|
|
882
|
+
getContext: () => store.getState(),
|
|
883
|
+
setContext: (newState) => store.dispatch(setAppState(newState))
|
|
911
884
|
};
|
|
912
885
|
|
|
913
|
-
const
|
|
914
|
-
{ state: 'idle', hp: 100, position: { x: 0, y: 0 } },
|
|
915
|
-
{
|
|
916
|
-
idle: (ctx) => ({
|
|
917
|
-
context: ctx,
|
|
918
|
-
walk: (dx: number, dy: number) => ({
|
|
919
|
-
...ctx,
|
|
920
|
-
state: 'walking',
|
|
921
|
-
position: { x: ctx.position.x + dx, y: ctx.position.y + dy }
|
|
922
|
-
}),
|
|
923
|
-
attack: () => ({ ...ctx, state: 'attacking' }),
|
|
924
|
-
}),
|
|
925
|
-
walking: (ctx) => ({
|
|
926
|
-
context: ctx,
|
|
927
|
-
stop: () => ({ ...ctx, state: 'idle' }),
|
|
928
|
-
}),
|
|
929
|
-
attacking: (ctx) => ({
|
|
930
|
-
context: ctx,
|
|
931
|
-
finishAttack: () => ({ ...ctx, state: 'idle' }),
|
|
932
|
-
}),
|
|
933
|
-
},
|
|
934
|
-
(ctx) => ctx.state // Accessor function - refactor-safe
|
|
935
|
-
);
|
|
936
|
-
|
|
937
|
-
// Game loop
|
|
938
|
-
player.walk(1, 0);
|
|
939
|
-
console.log(player.position); // { x: 1, y: 0 }
|
|
940
|
-
console.log(player.state); // 'walking'
|
|
941
|
-
|
|
942
|
-
player.stop();
|
|
943
|
-
console.log(player.state); // 'idle'
|
|
944
|
-
```
|
|
945
|
-
|
|
946
|
-
β οΈ **Trade-offs**: Breaks immutability principle. Only use when:
|
|
947
|
-
- Working in non-UI environments (backend, CLI, game logic)
|
|
948
|
-
- Stable object reference is critical
|
|
949
|
-
- You accept no reactive UI updates or state history
|
|
950
|
-
|
|
951
|
-
**Not suitable for**: React, Solid, Vue, or any reactive framework.
|
|
952
|
-
|
|
953
|
-
#### Comparison: Runner vs Ensemble vs Mutable Machine
|
|
954
|
-
|
|
955
|
-
| Feature | Runner | Ensemble | Mutable Machine |
|
|
956
|
-
|---------|--------|----------|-----------------|
|
|
957
|
-
| **State Philosophy** | Immutable core with ergonomic wrapper | External immutable state store integration | Mutable in-place context |
|
|
958
|
-
| **Primary Use Case** | Complex local/component state | Global state with framework integration | Backend, game loops, non-UI |
|
|
959
|
-
| **API Style** | `runner.actions.increment()` | `ensemble.actions.increment()` | `machine.increment()` |
|
|
960
|
-
| **UI Frameworks** | β
Excellent (React, Solid, Vue) | β
Designed for frameworks | β Won't trigger re-renders |
|
|
961
|
-
| **State History** | β
Preserved (immutable snapshots) | β
Preserved (by external store) | β Lost (mutated in place) |
|
|
962
|
-
| **Object Stability** | Runner reference stable, internal machine changes | Ensemble reference stable, reconstructed per access | β
Single object reference |
|
|
963
|
-
| **Time-Travel Debugging** | β
Possible | β
Possible | β Not possible |
|
|
964
|
-
| **Performance** | Standard | Standard | β
Optimal (no allocations) |
|
|
965
|
-
|
|
966
|
-
**Quick Decision Tree:**
|
|
967
|
-
|
|
968
|
-
1. **Do you need a UI framework** (React, Solid, Vue)?
|
|
969
|
-
- **Yes** β Use **Ensemble** if global/shared state, or **Runner** if local/component state
|
|
970
|
-
- **No** (Backend, game, CLI) β Use **Mutable Machine**
|
|
971
|
-
|
|
972
|
-
2. **Is state global/shared across your app?**
|
|
973
|
-
- **Yes** β Use **Ensemble** (hooks into your framework's state manager)
|
|
974
|
-
- **No** β Use **Runner** (simpler, still immutable)
|
|
975
|
-
|
|
976
|
-
3. **Do you need immutability for debugging/testing?**
|
|
977
|
-
- **Yes** β Use **Runner** or **Ensemble**
|
|
978
|
-
- **No** (performance critical) β Use **Mutable Machine**
|
|
886
|
+
const ensemble = createEnsemble(reduxStore, factories, (ctx) => ctx.status);
|
|
979
887
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
//
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
machine = machine.reset();
|
|
990
|
-
|
|
991
|
-
// With Runner (stable reference, less boilerplate)
|
|
992
|
-
const runner = createRunner(createCounterMachine());
|
|
993
|
-
runner.actions.increment();
|
|
994
|
-
runner.actions.add(5);
|
|
995
|
-
runner.actions.reset();
|
|
996
|
-
console.log(runner.context); // Access state directly
|
|
997
|
-
```
|
|
998
|
-
|
|
999
|
-
**How it Works:**
|
|
1000
|
-
- Holds a private `currentMachine` variable
|
|
1001
|
-
- Wraps each transition method to update `currentMachine` before returning
|
|
1002
|
-
- Provides stable `runner.actions` and `runner.context` references
|
|
1003
|
-
- The underlying immutable machine is still pure; the Runner just manages the reassignments
|
|
1004
|
-
|
|
1005
|
-
**When to Use:**
|
|
1006
|
-
- Complex local state (forms, multi-step wizards, component logic)
|
|
1007
|
-
- Generator-based workflows (cleaner syntax with `yield runner.actions.xxx()`)
|
|
1008
|
-
- You want immutability's safety without constant `machine = machine.xxx()` chains
|
|
1009
|
-
- Perfect for React component state or Solid signals
|
|
1010
|
-
|
|
1011
|
-
**Analogy**: An automatic transmission. The immutable engine is still doing powerful, pure work. The Runner just handles the "gear shifting" automatically.
|
|
1012
|
-
|
|
1013
|
-
##### 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
|
+
};
|
|
1014
897
|
|
|
1015
|
-
|
|
898
|
+
const ensemble = createEnsemble(apiStore, factories, (ctx) => ctx.status);
|
|
1016
899
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
+
}
|
|
1022
910
|
};
|
|
1023
911
|
|
|
1024
|
-
|
|
1025
|
-
const [context, setContext] = useState(initialContext);
|
|
1026
|
-
|
|
1027
|
-
// The Ensemble bridges them - use an accessor function for refactoring safety
|
|
1028
|
-
const ensemble = useMemo(() =>
|
|
1029
|
-
createEnsemble(
|
|
1030
|
-
{ getContext: () => context, setContext },
|
|
1031
|
-
factories,
|
|
1032
|
-
(ctx) => ctx.status // Accessor function - fully refactor-safe!
|
|
1033
|
-
),
|
|
1034
|
-
[context]
|
|
1035
|
-
);
|
|
1036
|
-
|
|
1037
|
-
// Type-safe dispatch from any part of your app
|
|
1038
|
-
ensemble.actions.fetch();
|
|
912
|
+
const ensemble = createEnsemble(persistentStore, factories, (ctx) => ctx.status);
|
|
1039
913
|
```
|
|
1040
914
|
|
|
1041
|
-
**
|
|
1042
|
-
- Takes a `StateStore` (get/set functions) that communicate with your state manager
|
|
1043
|
-
- Takes a set of factory functions that create machines for each state
|
|
1044
|
-
- When you call `ensemble.actions.fetch()`:
|
|
1045
|
-
1. Gets current context from the store
|
|
1046
|
-
2. Determines the active machine based on discriminant key
|
|
1047
|
-
3. Executes the transition (which calls `store.setContext()` internally)
|
|
1048
|
-
4. Reconstructs the machine with updated context on next access
|
|
1049
|
-
|
|
1050
|
-
**When to Use:**
|
|
1051
|
-
- Global/application state that multiple components need
|
|
1052
|
-
- You want machine logic completely decoupled from your UI framework
|
|
1053
|
-
- Testing (swap the store for a test stub)
|
|
1054
|
-
- Portable state logic (same machine works with React, Solid, Vue, etc.)
|
|
1055
|
-
- Complex state that's read/updated from multiple places in your app
|
|
1056
|
-
|
|
1057
|
-
**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.
|
|
1058
916
|
|
|
1059
917
|
##### Deep Dive: Mutable Machine (createMutableMachine)
|
|
1060
918
|
|