@doeixd/machine 0.0.7 β†’ 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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):** 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.
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: Framework-Agnostic Global State Orchestration
793
+ #### Ensemble: Coordinating Multiple Machines with Shared Context
753
794
 
754
- 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.
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
- // 1. Define your state store interface
760
- const store = {
761
- getContext: () => sharedContext,
762
- setContext: (newCtx) => { sharedContext = newCtx; }
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
- // 2. Define machine factories for each state
766
- const factories = {
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: () => store.setContext({ ...ctx, status: 'loading' })
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
- succeed: (data) => store.setContext({ ...ctx, status: 'success', data }),
772
- fail: (error) => store.setContext({ ...ctx, status: 'error', error })
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
- success: (ctx) => createMachine(ctx, {
775
- refetch: () => store.setContext({ ...ctx, status: 'loading' })
839
+ open: (ctx) => createMachine(ctx, {
840
+ close: () => ({ ...ctx, ui: { ...ctx.ui, modal: 'closed' } })
776
841
  })
777
- };
842
+ }, (ctx) => ctx.ui.modal);
778
843
 
779
- // 3. Create the Ensemble with an accessor function for refactoring safety
780
- const ensemble = createEnsemble(store, factories, (ctx) => ctx.status);
781
-
782
- // 4. Use it with type-safe dispatch
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
- - Global application state orchestration
794
- - Decoupling business logic from framework-specific state management
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
- For non-UI environments where a stable object reference is critical, `createMutableMachine` provides a highly imperative API with direct in-place mutations.
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
- **Key Characteristics:**
856
- - **Stable Object Reference**: The machine is a single object whose properties mutate in place
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
- import { createMutableMachine } from "@doeixd/machine/multi";
864
+ // Zustand store integration
865
+ import { create } from 'zustand';
870
866
 
871
- type AuthContext =
872
- | { status: 'loggedOut'; error?: string }
873
- | { status: 'loggedIn'; username: string };
867
+ const useAppStore = create<AppState>((set, get) => ({
868
+ // ... your Zustand store
869
+ }));
874
870
 
875
- const authFactories = {
876
- loggedOut: (ctx: AuthContext) => ({
877
- context: ctx,
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 auth = createMutableMachine(
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
- **Example: Game State Loop**
878
+ // Redux integration
879
+ import { store } from './reduxStore';
905
880
 
906
- ```typescript
907
- type PlayerContext = {
908
- state: 'idle' | 'walking' | 'attacking';
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 player = createMutableMachine(
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
- ##### Deep Dive: Runner (createRunner)
981
-
982
- **The Pattern**: A stateful wrapper that handles internal reassignments for you.
983
-
984
- ```typescript
985
- // Without Runner (verbose reassignment chain)
986
- let machine = createCounterMachine();
987
- machine = machine.increment();
988
- machine = machine.add(5);
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
- **The Pattern**: Decouples machine logic from framework-specific state management.
898
+ const ensemble = createEnsemble(apiStore, factories, (ctx) => ctx.status);
1016
899
 
1017
- ```typescript
1018
- // Your pure machine logic
1019
- const factories = {
1020
- idle: (ctx) => createMachine(ctx, { fetch: () => { /* ... */ } }),
1021
- loading: (ctx) => createMachine(ctx, { succeed: (data) => { /* ... */ } }),
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
- // Your framework's state (React example)
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
- **How it Works:**
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