@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 +235 -3
- package/dist/cjs/development/core.js.map +2 -2
- package/dist/cjs/development/extract.js +500 -0
- package/dist/cjs/development/extract.js.map +7 -0
- package/dist/cjs/development/index.js +135 -477
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/extract.js +5 -0
- package/dist/cjs/production/index.js +4 -5
- package/dist/esm/development/core.js.map +2 -2
- package/dist/esm/development/extract.js +486 -0
- package/dist/esm/development/extract.js.map +7 -0
- package/dist/esm/development/index.js +135 -484
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/extract.js +5 -0
- package/dist/esm/production/index.js +4 -5
- package/dist/types/index.d.ts +45 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/matcher.d.ts +325 -0
- package/dist/types/matcher.d.ts.map +1 -0
- package/package.json +13 -1
- package/src/index.ts +73 -9
- package/src/matcher.ts +560 -0
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
|
-
|
|
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
|