@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 +174 -13
- 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 -439
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/core.js +1 -1
- 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 -446
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/core.js +1 -1
- package/dist/esm/production/extract.js +5 -0
- package/dist/esm/production/index.js +4 -5
- package/dist/types/extract.d.ts +15 -1
- package/dist/types/extract.d.ts.map +1 -1
- package/dist/types/index.d.ts +58 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/matcher.d.ts +318 -0
- package/dist/types/matcher.d.ts.map +1 -0
- package/package.json +13 -1
- package/src/extract.ts +61 -61
- package/src/generators.ts +6 -6
- package/src/index.ts +89 -10
- package/src/matcher.ts +544 -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)
|
|
@@ -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
|
-
|
|
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
|