@doeixd/machine 0.0.18 β†’ 0.0.20

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,7 +8,6 @@ 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
12
  ## Installation
14
13
 
@@ -41,7 +40,7 @@ An FSM is a 5-tuple: **M = (S, Ξ£, Ξ΄, sβ‚€, F)** where:
41
40
  3. **Finite States**: Only a limited number of discrete configurations exist
42
41
 
43
42
  ### How `@doeixd/machine` Implements These Tenets
44
-
43
+ A smiplified version of the core type / primitive:
45
44
  ```typescript
46
45
  type Machine<C extends object> = {
47
46
  context: C; // Encodes the current state (s ∈ S)
@@ -659,6 +658,77 @@ const message = matchMachine(machine, "status", {
659
658
 
660
659
  TypeScript enforces exhaustive checking - you must handle all cases!
661
660
 
661
+ #### `createMatcher(...cases)`
662
+
663
+ The `createMatcher` utility provides a more advanced, reusable way to define pattern matching logic for your state machines. It creates a single object that provides type guards, exhaustive pattern matching, and simple state checking all in one.
664
+
665
+ ```typescript
666
+ import { createMatcher, classCase, discriminantCase, forContext } from "@doeixd/machine";
667
+
668
+ // 1. Define the matcher
669
+ const match = createMatcher(
670
+ classCase('idle', IdleMachine),
671
+ classCase('loading', LoadingMachine),
672
+ classCase('success', SuccessMachine)
673
+ );
674
+
675
+ // 2. Use it for Type Guards
676
+ if (match.is.loading(machine)) {
677
+ // machine is narrowed to LoadingMachine
678
+ console.log(machine.context.url);
679
+ }
680
+
681
+ // 3. Use it for Exhaustive Pattern Matching
682
+ const result = match.when(machine).is(
683
+ match.case.idle(() => "Ready"),
684
+ match.case.loading(() => "Loading..."),
685
+ match.case.success((m) => `Data: ${m.context.data}`),
686
+ match.exhaustive
687
+ );
688
+
689
+ // 4. Use it for Simple Matching
690
+ const state = match(machine); // 'idle' | 'loading' | 'success' | null
691
+ ```
692
+
693
+ **Advanced Context Matching:**
694
+
695
+ For discriminated unions in context, use the `forContext` helper for maximum type safety:
696
+
697
+ ```typescript
698
+ type FetchContext =
699
+ | { status: 'idle' }
700
+ | { status: 'success'; data: string };
701
+
702
+ const builder = forContext<FetchContext>();
703
+
704
+ const match = createMatcher(
705
+ builder.case('idle', 'status', 'idle'),
706
+ builder.case('success', 'status', 'success')
707
+ );
708
+
709
+ if (match.is.success(machine)) {
710
+ // machine.context is narrowed to { status: 'success'; data: string }
711
+ console.log(machine.context.data);
712
+ }
713
+ ```
714
+
715
+ **Explicit vs. Inferred Return Types:**
716
+
717
+ ```typescript
718
+ // Implicit: helper infers return type union and enforces exhaustiveness
719
+ const res1 = match.when(machine).is(
720
+ match.case.idle(() => 1),
721
+ match.case.success(() => 2),
722
+ match.exhaustive
723
+ ); // number - Exhaustive check ENABLED
724
+
725
+ // Explicit generic: You specify return type, exhaustiveness check is optional (but recommended)
726
+ const res2 = match.when(machine).is<string>(
727
+ match.case.idle(() => "Idle")
728
+ ); // string - Exhaustive check DISABLED (useful for partial matching)
729
+ ```
730
+
731
+
662
732
  #### `hasState<M, K, V>(machine, key, value)`
663
733
 
664
734
  Type guard for state checking with type narrowing.
@@ -819,6 +889,160 @@ function enterState(): MachineResult<{ timer: number }> {
819
889
  }
820
890
  ```
821
891
 
892
+ ### Pattern Matching
893
+
894
+ **NEW**: Advanced pattern matching utilities for type-safe discrimination between machine states.
895
+
896
+ The `createMatcher` function provides three complementary APIs for matching and narrowing machine types:
897
+
898
+ #### Quick Example
899
+
900
+ ```typescript
901
+ import { createMatcher, classCase, MachineBase } from "@doeixd/machine";
902
+
903
+ // Define state machines
904
+ class IdleMachine extends MachineBase<{ status: 'idle' }> {
905
+ start() { return new LoadingMachine(); }
906
+ }
907
+
908
+ class LoadingMachine extends MachineBase<{ status: 'loading' }> {
909
+ success(data: string) { return new SuccessMachine(data); }
910
+ error(err: Error) { return new ErrorMachine(err); }
911
+ }
912
+
913
+ class SuccessMachine extends MachineBase<{ status: 'success'; data: string }> {
914
+ reset() { return new IdleMachine(); }
915
+ }
916
+
917
+ class ErrorMachine extends MachineBase<{ status: 'error'; error: Error }> {
918
+ retry() { return new LoadingMachine(); }
919
+ }
920
+
921
+ // Create reusable matcher
922
+ const match = createMatcher(
923
+ classCase('idle', IdleMachine),
924
+ classCase('loading', LoadingMachine),
925
+ classCase('success', SuccessMachine),
926
+ classCase('error', ErrorMachine)
927
+ );
928
+
929
+ type FetchMachine = IdleMachine | LoadingMachine | SuccessMachine | ErrorMachine;
930
+
931
+ const machine: FetchMachine = new LoadingMachine();
932
+ ```
933
+
934
+ #### API 1: Type Guards
935
+
936
+ Use `match.is.<case>()` for type narrowing in conditionals:
937
+
938
+ ```typescript
939
+ if (match.is.loading(machine)) {
940
+ // βœ“ machine is narrowed to LoadingMachine
941
+ console.log(machine.context.startTime);
942
+ }
943
+
944
+ if (match.is.success(machine)) {
945
+ // βœ“ machine is narrowed to SuccessMachine
946
+ console.log(machine.context.data);
947
+ }
948
+ ```
949
+
950
+ #### API 2: Exhaustive Pattern Matching
951
+
952
+ Use `match.when(...).is(...)` for complex branching with compile-time exhaustiveness checking:
953
+
954
+ ```typescript
955
+ const message = match.when(machine).is<string>(
956
+ match.case.idle(() => 'Ready to start'),
957
+ match.case.loading(() => 'Loading...'),
958
+ match.case.success(m => `Done: ${m.context.data}`),
959
+ match.case.error(m => `Error: ${m.context.error.message}`),
960
+ match.exhaustive // ← TypeScript error if any case is missing
961
+ );
962
+ ```
963
+
964
+ **Benefits:**
965
+ - **Compile-time exhaustiveness** - TypeScript catches missing cases
966
+ - **Type narrowing** - Each handler receives the narrowed machine type
967
+ - **Reusable** - Define matcher once, use everywhere
968
+
969
+ #### API 3: Simple Match
970
+
971
+ Use `match(machine)` to get the matched case name:
972
+
973
+ ```typescript
974
+ const stateName = match(machine); // 'idle' | 'loading' | 'success' | 'error' | null
975
+
976
+ switch (stateName) {
977
+ case 'idle': return 'Ready';
978
+ case 'loading': return 'In progress';
979
+ case 'success': return 'Complete';
980
+ case 'error': return 'Failed';
981
+ default: return 'Unknown';
982
+ }
983
+ ```
984
+
985
+ #### Helper Functions
986
+
987
+ **`classCase`** - For class-based machines (most common):
988
+
989
+ ```typescript
990
+ createMatcher(
991
+ classCase('idle', IdleMachine),
992
+ classCase('loading', LoadingMachine)
993
+ );
994
+ ```
995
+
996
+ **`discriminantCase`** - For discriminated unions:
997
+
998
+ ```typescript
999
+ type Context =
1000
+ | { status: 'idle' }
1001
+ | { status: 'loading' }
1002
+ | { status: 'success'; data: string };
1003
+
1004
+ const match = createMatcher(
1005
+ discriminantCase<'idle', Machine<Context>, 'status', 'idle'>('idle', 'status', 'idle'),
1006
+ discriminantCase<'loading', Machine<Context>, 'status', 'loading'>('loading', 'status', 'loading'),
1007
+ discriminantCase<'success', Machine<Context>, 'status', 'success'>('success', 'status', 'success')
1008
+ );
1009
+
1010
+ const machine = createMachine<Context>({ status: 'success', data: 'test' }, {});
1011
+
1012
+ if (match.is.success(machine)) {
1013
+ console.log(machine.context.data); // βœ“ TypeScript knows data exists
1014
+ }
1015
+ ```
1016
+
1017
+ **`customCase`** - For custom predicates:
1018
+
1019
+ ```typescript
1020
+ createMatcher(
1021
+ customCase('complex', (m): m is ComplexMachine => {
1022
+ return m.context.value > 10 && m.context.status === 'active';
1023
+ })
1024
+ );
1025
+ ```
1026
+
1027
+ #### Comparison with Existing Utilities
1028
+
1029
+ | Utility | Use Case | Type Narrowing | Reusable |
1030
+ |---------|----------|----------------|----------|
1031
+ | `hasState(m, key, value)` | Single discriminant check | βœ… | ❌ |
1032
+ | `isState(m, Class)` | Single class check | βœ… | ❌ |
1033
+ | `matchMachine(m, key, handlers)` | Exhaustive matching | βœ… | ❌ |
1034
+ | `createMatcher(...)` | All of the above | βœ… | βœ… |
1035
+
1036
+ **When to use `createMatcher`:**
1037
+ - You need to match the same states in multiple places
1038
+ - You want exhaustive pattern matching with compile-time checking
1039
+ - You're working with union types of multiple machine classes
1040
+ - You need flexible type guards for conditionals
1041
+
1042
+ **When to use simpler utilities:**
1043
+ - One-off checks: Use `hasState` or `isState`
1044
+ - Single location matching: Use `matchMachine`
1045
+
822
1046
  ## Advanced Features
823
1047
 
824
1048
  ### Ergonomic & Integration Patterns
@@ -1641,8 +1865,16 @@ The extraction system uses **AST-based static analysis**:
1641
1865
 
1642
1866
  ### Static Extraction API (Build-Time)
1643
1867
 
1868
+ > **Tree-Shaking**: Extraction tools are in a separate entry point (`@doeixd/machine/extract`) and are NOT included in your production bundle when you import from the main package. The heavy `ts-morph` dependency (used for AST parsing) will only be included if you explicitly import from `/extract`.
1869
+ >
1870
+ > **Type Imports**: Configuration types (`MachineConfig`, `ExtractionConfig`, etc.) are available from the main package for type safety without bundle impact:
1871
+ > ```typescript
1872
+ > import type { MachineConfig, ExtractionConfig } from '@doeixd/machine';
1873
+ > ```
1874
+
1644
1875
  ```typescript
1645
- import { extractMachine, extractMachines } from '@doeixd/machine';
1876
+ // Import from the separate extract entry point (NOT included in main bundle)
1877
+ import { extractMachine, extractMachines } from '@doeixd/machine/extract';
1646
1878
  import { Project } from 'ts-morph';
1647
1879
 
1648
1880
  // Extract single machine