@doeixd/machine 0.0.17 β†’ 0.0.19

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)
@@ -165,11 +164,11 @@ The most powerful pattern: different machine types represent different states.
165
164
  import { createMachine, Machine } from "@doeixd/machine";
166
165
 
167
166
  // Define distinct machine types for each state
168
- type LoggedOut = Machine<{ status: "loggedOut" }> & {
167
+ type LoggedOut = Machine<{ status: "loggedOut" }, {
169
168
  login: (username: string) => LoggedIn;
170
169
  };
171
170
 
172
- type LoggedIn = Machine<{ status: "loggedIn"; username: string }> & {
171
+ type LoggedIn = Machine<{ status: "loggedIn"; username: string }, {
173
172
  logout: () => LoggedOut;
174
173
  viewProfile: () => LoggedIn;
175
174
  };
@@ -236,12 +235,12 @@ logout(state); // Runtime error!
236
235
  **Type-State Approach (Compile-Time Enforcement):**
237
236
  ```typescript
238
237
  // βœ… States are distinct types - compiler enforces validity
239
- type LoggedOut = Machine<{ status: "loggedOut" }> & {
238
+ type LoggedOut = Machine<{ status: "loggedOut" }, {
240
239
  login: (user: string) => LoggedIn;
241
240
  // No logout method - impossible to call
242
241
  };
243
242
 
244
- type LoggedIn = Machine<{ status: "loggedIn"; username: string }> & {
243
+ type LoggedIn = Machine<{ status: "loggedIn"; username: string }, {
245
244
  logout: () => LoggedOut;
246
245
  // No login method - impossible to call
247
246
  };
@@ -308,7 +307,7 @@ if (hasState(machine, "status", "success")) {
308
307
 
309
308
  #### 5. Event Type Safety
310
309
  ```typescript
311
- type FetchMachine = AsyncMachine<{ status: string }> & {
310
+ type FetchMachine = AsyncMachine<{ status: string }, {
312
311
  fetch: (id: number) => Promise<FetchMachine>;
313
312
  retry: () => Promise<FetchMachine>;
314
313
  };
@@ -370,22 +369,22 @@ This shows the full power of Type-State Programming:
370
369
 
371
370
  ```typescript
372
371
  // Define the states as distinct types
373
- type IdleState = Machine<{ status: "idle" }> & {
372
+ type IdleState = Machine<{ status: "idle" }, {
374
373
  fetch: (url: string) => LoadingState;
375
374
  };
376
375
 
377
- type LoadingState = Machine<{ status: "loading"; url: string }> & {
376
+ type LoadingState = Machine<{ status: "loading"; url: string }, {
378
377
  cancel: () => IdleState;
379
378
  // Note: No fetch - can't start new request while loading
380
379
  };
381
380
 
382
- type SuccessState = Machine<{ status: "success"; data: any }> & {
381
+ type SuccessState = Machine<{ status: "success"; data: any }, {
383
382
  refetch: () => LoadingState;
384
383
  clear: () => IdleState;
385
384
  // Note: No cancel - nothing to cancel
386
385
  };
387
386
 
388
- type ErrorState = Machine<{ status: "error"; error: string }> & {
387
+ type ErrorState = Machine<{ status: "error"; error: string }, {
389
388
  retry: () => LoadingState;
390
389
  clear: () => IdleState;
391
390
  };
@@ -779,7 +778,7 @@ const tracked = withAnalytics(machine, trackEvent);
779
778
  ```typescript
780
779
  import { Context, Transitions, Event, TransitionArgs } from "@doeixd/machine";
781
780
 
782
- type MyMachine = Machine<{ count: number }> & {
781
+ type MyMachine = Machine<{ count: number }, {
783
782
  add: (n: number) => MyMachine;
784
783
  };
785
784
 
@@ -819,6 +818,160 @@ function enterState(): MachineResult<{ timer: number }> {
819
818
  }
820
819
  ```
821
820
 
821
+ ### Pattern Matching
822
+
823
+ **NEW**: Advanced pattern matching utilities for type-safe discrimination between machine states.
824
+
825
+ The `createMatcher` function provides three complementary APIs for matching and narrowing machine types:
826
+
827
+ #### Quick Example
828
+
829
+ ```typescript
830
+ import { createMatcher, classCase, MachineBase } from "@doeixd/machine";
831
+
832
+ // Define state machines
833
+ class IdleMachine extends MachineBase<{ status: 'idle' }> {
834
+ start() { return new LoadingMachine(); }
835
+ }
836
+
837
+ class LoadingMachine extends MachineBase<{ status: 'loading' }> {
838
+ success(data: string) { return new SuccessMachine(data); }
839
+ error(err: Error) { return new ErrorMachine(err); }
840
+ }
841
+
842
+ class SuccessMachine extends MachineBase<{ status: 'success'; data: string }> {
843
+ reset() { return new IdleMachine(); }
844
+ }
845
+
846
+ class ErrorMachine extends MachineBase<{ status: 'error'; error: Error }> {
847
+ retry() { return new LoadingMachine(); }
848
+ }
849
+
850
+ // Create reusable matcher
851
+ const match = createMatcher(
852
+ classCase('idle', IdleMachine),
853
+ classCase('loading', LoadingMachine),
854
+ classCase('success', SuccessMachine),
855
+ classCase('error', ErrorMachine)
856
+ );
857
+
858
+ type FetchMachine = IdleMachine | LoadingMachine | SuccessMachine | ErrorMachine;
859
+
860
+ const machine: FetchMachine = new LoadingMachine();
861
+ ```
862
+
863
+ #### API 1: Type Guards
864
+
865
+ Use `match.is.<case>()` for type narrowing in conditionals:
866
+
867
+ ```typescript
868
+ if (match.is.loading(machine)) {
869
+ // βœ“ machine is narrowed to LoadingMachine
870
+ console.log(machine.context.startTime);
871
+ }
872
+
873
+ if (match.is.success(machine)) {
874
+ // βœ“ machine is narrowed to SuccessMachine
875
+ console.log(machine.context.data);
876
+ }
877
+ ```
878
+
879
+ #### API 2: Exhaustive Pattern Matching
880
+
881
+ Use `match.when(...).is(...)` for complex branching with compile-time exhaustiveness checking:
882
+
883
+ ```typescript
884
+ const message = match.when(machine).is<string>(
885
+ match.case.idle(() => 'Ready to start'),
886
+ match.case.loading(() => 'Loading...'),
887
+ match.case.success(m => `Done: ${m.context.data}`),
888
+ match.case.error(m => `Error: ${m.context.error.message}`),
889
+ match.exhaustive // ← TypeScript error if any case is missing
890
+ );
891
+ ```
892
+
893
+ **Benefits:**
894
+ - **Compile-time exhaustiveness** - TypeScript catches missing cases
895
+ - **Type narrowing** - Each handler receives the narrowed machine type
896
+ - **Reusable** - Define matcher once, use everywhere
897
+
898
+ #### API 3: Simple Match
899
+
900
+ Use `match(machine)` to get the matched case name:
901
+
902
+ ```typescript
903
+ const stateName = match(machine); // 'idle' | 'loading' | 'success' | 'error' | null
904
+
905
+ switch (stateName) {
906
+ case 'idle': return 'Ready';
907
+ case 'loading': return 'In progress';
908
+ case 'success': return 'Complete';
909
+ case 'error': return 'Failed';
910
+ default: return 'Unknown';
911
+ }
912
+ ```
913
+
914
+ #### Helper Functions
915
+
916
+ **`classCase`** - For class-based machines (most common):
917
+
918
+ ```typescript
919
+ createMatcher(
920
+ classCase('idle', IdleMachine),
921
+ classCase('loading', LoadingMachine)
922
+ );
923
+ ```
924
+
925
+ **`discriminantCase`** - For discriminated unions:
926
+
927
+ ```typescript
928
+ type Context =
929
+ | { status: 'idle' }
930
+ | { status: 'loading' }
931
+ | { status: 'success'; data: string };
932
+
933
+ const match = createMatcher(
934
+ discriminantCase<'idle', Machine<Context>, 'status', 'idle'>('idle', 'status', 'idle'),
935
+ discriminantCase<'loading', Machine<Context>, 'status', 'loading'>('loading', 'status', 'loading'),
936
+ discriminantCase<'success', Machine<Context>, 'status', 'success'>('success', 'status', 'success')
937
+ );
938
+
939
+ const machine = createMachine<Context>({ status: 'success', data: 'test' }, {});
940
+
941
+ if (match.is.success(machine)) {
942
+ console.log(machine.context.data); // βœ“ TypeScript knows data exists
943
+ }
944
+ ```
945
+
946
+ **`customCase`** - For custom predicates:
947
+
948
+ ```typescript
949
+ createMatcher(
950
+ customCase('complex', (m): m is ComplexMachine => {
951
+ return m.context.value > 10 && m.context.status === 'active';
952
+ })
953
+ );
954
+ ```
955
+
956
+ #### Comparison with Existing Utilities
957
+
958
+ | Utility | Use Case | Type Narrowing | Reusable |
959
+ |---------|----------|----------------|----------|
960
+ | `hasState(m, key, value)` | Single discriminant check | βœ… | ❌ |
961
+ | `isState(m, Class)` | Single class check | βœ… | ❌ |
962
+ | `matchMachine(m, key, handlers)` | Exhaustive matching | βœ… | ❌ |
963
+ | `createMatcher(...)` | All of the above | βœ… | βœ… |
964
+
965
+ **When to use `createMatcher`:**
966
+ - You need to match the same states in multiple places
967
+ - You want exhaustive pattern matching with compile-time checking
968
+ - You're working with union types of multiple machine classes
969
+ - You need flexible type guards for conditionals
970
+
971
+ **When to use simpler utilities:**
972
+ - One-off checks: Use `hasState` or `isState`
973
+ - Single location matching: Use `matchMachine`
974
+
822
975
  ## Advanced Features
823
976
 
824
977
  ### Ergonomic & Integration Patterns
@@ -1641,8 +1794,16 @@ The extraction system uses **AST-based static analysis**:
1641
1794
 
1642
1795
  ### Static Extraction API (Build-Time)
1643
1796
 
1797
+ > **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`.
1798
+ >
1799
+ > **Type Imports**: Configuration types (`MachineConfig`, `ExtractionConfig`, etc.) are available from the main package for type safety without bundle impact:
1800
+ > ```typescript
1801
+ > import type { MachineConfig, ExtractionConfig } from '@doeixd/machine';
1802
+ > ```
1803
+
1644
1804
  ```typescript
1645
- import { extractMachine, extractMachines } from '@doeixd/machine';
1805
+ // Import from the separate extract entry point (NOT included in main bundle)
1806
+ import { extractMachine, extractMachines } from '@doeixd/machine/extract';
1646
1807
  import { Project } from 'ts-morph';
1647
1808
 
1648
1809
  // Extract single machine