@doeixd/machine 0.0.4
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/LICENSE +7 -0
- package/README.md +1070 -0
- package/dist/cjs/development/index.js +198 -0
- package/dist/cjs/development/index.js.map +7 -0
- package/dist/cjs/production/index.js +1 -0
- package/dist/esm/development/index.js +175 -0
- package/dist/esm/development/index.js.map +7 -0
- package/dist/esm/production/index.js +1 -0
- package/dist/types/generators.d.ts +314 -0
- package/dist/types/generators.d.ts.map +1 -0
- package/dist/types/index.d.ts +339 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +110 -0
- package/src/devtools.ts +74 -0
- package/src/extract.ts +190 -0
- package/src/generators.ts +421 -0
- package/src/index.ts +528 -0
- package/src/primitives.ts +191 -0
- package/src/react.ts +44 -0
- package/src/solid.ts +502 -0
- package/src/test.ts +207 -0
- package/src/utils.ts +167 -0
package/README.md
ADDED
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
[](https://deepwiki.com/doeixd/machine)
|
|
2
|
+
|
|
3
|
+
# Machine
|
|
4
|
+
|
|
5
|
+
A minimal, type-safe state machine library for TypeScript built on mathematical foundations.
|
|
6
|
+
|
|
7
|
+
> **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.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @doeixd/machine
|
|
13
|
+
# or
|
|
14
|
+
yarn add @doeixd/machine
|
|
15
|
+
# or
|
|
16
|
+
pnpm add @doeixd/machine
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 🧩 Core Tenets of State Machines
|
|
20
|
+
|
|
21
|
+
A state machine (formally, a **finite state machine** or FSM) is a mathematical model of computation defined by:
|
|
22
|
+
|
|
23
|
+
### Formal Definition
|
|
24
|
+
|
|
25
|
+
An FSM is a 5-tuple: **M = (S, Σ, δ, s₀, F)** where:
|
|
26
|
+
|
|
27
|
+
- **S** - Finite set of states (the system can only be in one discrete configuration at a time)
|
|
28
|
+
- **Σ** - Input alphabet (the set of events/symbols the machine can respond to)
|
|
29
|
+
- **δ** - Transition function: `δ : S × Σ → S` (given current state and input, determine next state)
|
|
30
|
+
- **s₀** - Initial state (the defined starting state)
|
|
31
|
+
- **F** - Final/accepting states (optional, for recognizers)
|
|
32
|
+
|
|
33
|
+
### Key Properties
|
|
34
|
+
|
|
35
|
+
1. **Determinism**: A deterministic FSM yields exactly one next state per (state, input) pair
|
|
36
|
+
2. **Markov Property**: The next state depends only on the current state and input, not on history
|
|
37
|
+
3. **Finite States**: Only a limited number of discrete configurations exist
|
|
38
|
+
|
|
39
|
+
### How `@doeixd/machine` Implements These Tenets
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
type Machine<C extends object> = {
|
|
43
|
+
readonly context: C; // Encodes the current state (s ∈ S)
|
|
44
|
+
} & Record<string, (...args: any[]) => Machine<any>>; // Transition functions (δ)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Mapping to formal FSM:**
|
|
48
|
+
|
|
49
|
+
- **States (S)**: Represented by the machine's `context` and type signature. In Type-State Programming, different types = different states.
|
|
50
|
+
- **Input Alphabet (Σ)**: The transition function names (e.g., `increment`, `login`, `fetch`).
|
|
51
|
+
- **Transition Function (δ)**: Each method on the machine is a transition. It takes the current context (`this`) plus arguments (input symbols) and returns the next machine state.
|
|
52
|
+
- **Initial State (s₀)**: The first context passed to `createMachine()`.
|
|
53
|
+
- **Determinism**: Each transition is a pure function that deterministically computes the next state.
|
|
54
|
+
- **Markov Property**: Transitions only access `this.context` (current state) and their arguments (input). No hidden state or history.
|
|
55
|
+
|
|
56
|
+
**Flexibility**: Unlike rigid FSM implementations, you can choose your level of immutability. Want to mutate? You can. Want pure functions? You can. Want compile-time state validation? Type-State Programming gives you that.
|
|
57
|
+
|
|
58
|
+
**Read more about our core principles:** [ 📖 Core Principles Guide ](./docs/principles.md)
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
### Basic Counter (Simple State)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { createMachine } from "@doeixd/machine";
|
|
66
|
+
|
|
67
|
+
const counter = createMachine(
|
|
68
|
+
{ count: 0 }, // Initial state (s₀)
|
|
69
|
+
{
|
|
70
|
+
// Transitions (δ)
|
|
71
|
+
increment: function() {
|
|
72
|
+
return createMachine({ count: this.count + 1 }, this);
|
|
73
|
+
},
|
|
74
|
+
add: function(n: number) {
|
|
75
|
+
return createMachine({ count: this.count + n }, this);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const next = counter.increment();
|
|
81
|
+
console.log(next.context.count); // 1
|
|
82
|
+
|
|
83
|
+
// Original is untouched (immutability by default)
|
|
84
|
+
console.log(counter.context.count); // 0
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Type-State Programming (Compile-Time State Safety)
|
|
88
|
+
|
|
89
|
+
The most powerful pattern: different machine types represent different states.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { createMachine, Machine } from "@doeixd/machine";
|
|
93
|
+
|
|
94
|
+
// Define distinct machine types for each state
|
|
95
|
+
type LoggedOut = Machine<{ status: "loggedOut" }> & {
|
|
96
|
+
login: (username: string) => LoggedIn;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type LoggedIn = Machine<{ status: "loggedIn"; username: string }> & {
|
|
100
|
+
logout: () => LoggedOut;
|
|
101
|
+
viewProfile: () => LoggedIn;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Create factory functions
|
|
105
|
+
const createLoggedOut = (): LoggedOut => {
|
|
106
|
+
return createMachine({ status: "loggedOut" }, {
|
|
107
|
+
login: function(username: string): LoggedIn {
|
|
108
|
+
return createLoggedIn(username);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const createLoggedIn = (username: string): LoggedIn => {
|
|
114
|
+
return createMachine({ status: "loggedIn", username }, {
|
|
115
|
+
logout: function(): LoggedOut {
|
|
116
|
+
return createLoggedOut();
|
|
117
|
+
},
|
|
118
|
+
viewProfile: function(): LoggedIn {
|
|
119
|
+
console.log(`Viewing ${this.username}'s profile`);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Usage
|
|
126
|
+
const machine = createLoggedOut();
|
|
127
|
+
|
|
128
|
+
// TypeScript prevents invalid transitions at compile time!
|
|
129
|
+
// machine.logout(); // ❌ Error: Property 'logout' does not exist on type 'LoggedOut'
|
|
130
|
+
|
|
131
|
+
const loggedIn = machine.login("alice");
|
|
132
|
+
// loggedIn.login("bob"); // ❌ Error: Property 'login' does not exist on type 'LoggedIn'
|
|
133
|
+
|
|
134
|
+
const loggedOut = loggedIn.logout(); // ✅ Valid
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This pattern makes **illegal states unrepresentable** in your type system.
|
|
138
|
+
|
|
139
|
+
## 🎯 Type-State Programming: The Core Philosophy
|
|
140
|
+
|
|
141
|
+
Type-State Programming is **the fundamental philosophy** of this library. Instead of representing states as strings or enums that you check at runtime, **states are types themselves**. TypeScript's compiler enforces state validity at compile time.
|
|
142
|
+
|
|
143
|
+
### Why Type-State Programming?
|
|
144
|
+
|
|
145
|
+
**Traditional Approach (Runtime Checks):**
|
|
146
|
+
```typescript
|
|
147
|
+
// ❌ State is just data - compiler can't help
|
|
148
|
+
type State = { status: "loggedOut" } | { status: "loggedIn"; username: string };
|
|
149
|
+
|
|
150
|
+
function logout(state: State) {
|
|
151
|
+
if (state.status === "loggedOut") {
|
|
152
|
+
// Oops! Already logged out, but this only fails at runtime
|
|
153
|
+
throw new Error("Already logged out!");
|
|
154
|
+
}
|
|
155
|
+
return { status: "loggedOut" as const };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Nothing prevents you from calling logout on loggedOut state
|
|
159
|
+
const state: State = { status: "loggedOut" };
|
|
160
|
+
logout(state); // Runtime error!
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Type-State Approach (Compile-Time Enforcement):**
|
|
164
|
+
```typescript
|
|
165
|
+
// ✅ States are distinct types - compiler enforces validity
|
|
166
|
+
type LoggedOut = Machine<{ status: "loggedOut" }> & {
|
|
167
|
+
login: (user: string) => LoggedIn;
|
|
168
|
+
// No logout method - impossible to call
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
type LoggedIn = Machine<{ status: "loggedIn"; username: string }> & {
|
|
172
|
+
logout: () => LoggedOut;
|
|
173
|
+
// No login method - impossible to call
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const state: LoggedOut = createLoggedOut();
|
|
177
|
+
// state.logout(); // ❌ Compile error! Property 'logout' does not exist
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### How TypeScript Catches Bugs
|
|
181
|
+
|
|
182
|
+
The type system prevents entire categories of bugs:
|
|
183
|
+
|
|
184
|
+
#### 1. Invalid State Transitions
|
|
185
|
+
```typescript
|
|
186
|
+
const loggedOut: LoggedOut = createLoggedOut();
|
|
187
|
+
const loggedIn: LoggedIn = loggedOut.login("alice");
|
|
188
|
+
|
|
189
|
+
// ❌ Compile error! Can't login when already logged in
|
|
190
|
+
// loggedIn.login("bob");
|
|
191
|
+
// ^^^^^
|
|
192
|
+
// Property 'login' does not exist on type 'LoggedIn'
|
|
193
|
+
|
|
194
|
+
// ❌ Compile error! Can't logout when already logged out
|
|
195
|
+
// loggedOut.logout();
|
|
196
|
+
// ^^^^^^
|
|
197
|
+
// Property 'logout' does not exist on type 'LoggedOut'
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### 2. Accessing Invalid State Data
|
|
201
|
+
```typescript
|
|
202
|
+
const loggedOut: LoggedOut = createLoggedOut();
|
|
203
|
+
|
|
204
|
+
// ❌ Compile error! 'username' doesn't exist on LoggedOut
|
|
205
|
+
// console.log(loggedOut.context.username);
|
|
206
|
+
// ^^^^^^^^
|
|
207
|
+
// Property 'username' does not exist on type '{ status: "loggedOut" }'
|
|
208
|
+
|
|
209
|
+
const loggedIn: LoggedIn = loggedOut.login("alice");
|
|
210
|
+
console.log(loggedIn.context.username); // ✅ OK! TypeScript knows it exists
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### 3. Exhaustive Pattern Matching
|
|
214
|
+
```typescript
|
|
215
|
+
// TypeScript enforces handling ALL possible states
|
|
216
|
+
const message = matchMachine(machine, "status", {
|
|
217
|
+
idle: (ctx) => "Waiting...",
|
|
218
|
+
loading: (ctx) => "Loading...",
|
|
219
|
+
success: (ctx) => `Done: ${ctx.data}`,
|
|
220
|
+
error: (ctx) => `Error: ${ctx.error}`
|
|
221
|
+
// If you forget a case, TypeScript error!
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### 4. Type Narrowing with Guards
|
|
226
|
+
```typescript
|
|
227
|
+
declare const machine: IdleMachine | LoadingMachine | SuccessMachine;
|
|
228
|
+
|
|
229
|
+
if (hasState(machine, "status", "success")) {
|
|
230
|
+
// TypeScript narrows the type to SuccessMachine
|
|
231
|
+
console.log(machine.context.data); // ✅ 'data' is known to exist
|
|
232
|
+
machine.retry(); // ✅ Only methods available on SuccessMachine are accessible
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### 5. Event Type Safety
|
|
237
|
+
```typescript
|
|
238
|
+
type FetchMachine = AsyncMachine<{ status: string }> & {
|
|
239
|
+
fetch: (id: number) => Promise<FetchMachine>;
|
|
240
|
+
retry: () => Promise<FetchMachine>;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const runner = runMachine(createFetchMachine());
|
|
244
|
+
|
|
245
|
+
// ✅ TypeScript knows the exact event shape
|
|
246
|
+
await runner.dispatch({ type: "fetch", args: [123] });
|
|
247
|
+
|
|
248
|
+
// ❌ Compile error! Wrong argument type
|
|
249
|
+
// await runner.dispatch({ type: "fetch", args: ["abc"] });
|
|
250
|
+
// ^^^^^
|
|
251
|
+
|
|
252
|
+
// ❌ Compile error! Unknown event type
|
|
253
|
+
// await runner.dispatch({ type: "unknown", args: [] });
|
|
254
|
+
// ^^^^^^^^^^
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Type-State vs. String-Based State
|
|
258
|
+
|
|
259
|
+
| Aspect | String-Based | Type-State Programming |
|
|
260
|
+
|--------|-------------|------------------------|
|
|
261
|
+
| **State Representation** | String literals (`"idle"`, `"loading"`) | TypeScript types (different machine types) |
|
|
262
|
+
| **Validation** | Runtime checks (`if (state === "idle")`) | Compile-time (type system) |
|
|
263
|
+
| **Transition Safety** | No enforcement - any transition possible | Compiler prevents invalid transitions |
|
|
264
|
+
| **Available Actions** | All methods available, must check state | Only valid methods available per state |
|
|
265
|
+
| **Data Access** | May access undefined data | Type system ensures data exists |
|
|
266
|
+
| **Bugs Caught** | At runtime (in production) | At compile time (during development) |
|
|
267
|
+
| **Refactoring Safety** | Easy to miss edge cases | Compiler finds all affected code |
|
|
268
|
+
| **Learning Curve** | Familiar to most developers | Requires understanding advanced TypeScript |
|
|
269
|
+
|
|
270
|
+
### Benefits of Type-State Programming
|
|
271
|
+
|
|
272
|
+
1. **Bugs caught at compile time**, not in production
|
|
273
|
+
2. **Impossible to write invalid state transitions**
|
|
274
|
+
3. **Autocomplete shows only valid transitions** for current state
|
|
275
|
+
4. **Refactoring is safer** - compiler finds all breaking changes
|
|
276
|
+
5. **Self-documenting code** - types express the state machine structure
|
|
277
|
+
6. **No runtime overhead** - all checks happen at compile time
|
|
278
|
+
7. **Gradual adoption** - can mix with simpler approaches
|
|
279
|
+
|
|
280
|
+
### When to Use Type-State Programming
|
|
281
|
+
|
|
282
|
+
**Use Type-State when:**
|
|
283
|
+
- ✅ You have distinct states with different available actions
|
|
284
|
+
- ✅ Invalid state transitions would cause bugs
|
|
285
|
+
- ✅ Different states have different data available
|
|
286
|
+
- ✅ You want maximum compile-time safety
|
|
287
|
+
- ✅ Complex state machines (auth, network requests, multi-step forms)
|
|
288
|
+
|
|
289
|
+
**Use simple context-based state when:**
|
|
290
|
+
- ✅ Just tracking data changes (like a counter)
|
|
291
|
+
- ✅ All operations are always valid
|
|
292
|
+
- ✅ Simplicity is more important than exhaustive safety
|
|
293
|
+
|
|
294
|
+
### Example: Network Request State Machine
|
|
295
|
+
|
|
296
|
+
This shows the full power of Type-State Programming:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// Define the states as distinct types
|
|
300
|
+
type IdleState = Machine<{ status: "idle" }> & {
|
|
301
|
+
fetch: (url: string) => LoadingState;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
type LoadingState = Machine<{ status: "loading"; url: string }> & {
|
|
305
|
+
cancel: () => IdleState;
|
|
306
|
+
// Note: No fetch - can't start new request while loading
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
type SuccessState = Machine<{ status: "success"; data: any }> & {
|
|
310
|
+
refetch: () => LoadingState;
|
|
311
|
+
clear: () => IdleState;
|
|
312
|
+
// Note: No cancel - nothing to cancel
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
type ErrorState = Machine<{ status: "error"; error: string }> & {
|
|
316
|
+
retry: () => LoadingState;
|
|
317
|
+
clear: () => IdleState;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Union type for the overall machine
|
|
321
|
+
type FetchMachine = IdleState | LoadingState | SuccessState | ErrorState;
|
|
322
|
+
|
|
323
|
+
// Implementation
|
|
324
|
+
const createIdle = (): IdleState =>
|
|
325
|
+
createMachine({ status: "idle" }, {
|
|
326
|
+
fetch: function(url: string): LoadingState {
|
|
327
|
+
return createLoading(url);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const createLoading = (url: string): LoadingState =>
|
|
332
|
+
createMachine({ status: "loading", url }, {
|
|
333
|
+
cancel: function(): IdleState {
|
|
334
|
+
return createIdle();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ... implement other states
|
|
339
|
+
|
|
340
|
+
// Usage - TypeScript guides you
|
|
341
|
+
const machine: FetchMachine = createIdle();
|
|
342
|
+
|
|
343
|
+
if (hasState(machine, "status", "idle")) {
|
|
344
|
+
const loading = machine.fetch("/api/data"); // ✅ OK
|
|
345
|
+
// loading.fetch("/other"); // ❌ Error! Can't fetch while loading
|
|
346
|
+
const idle = loading.cancel(); // ✅ Can cancel loading
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**The compiler prevents you from:**
|
|
351
|
+
- Starting a new fetch while one is in progress
|
|
352
|
+
- Canceling when there's nothing to cancel
|
|
353
|
+
- Accessing `data` before the request succeeds
|
|
354
|
+
- Accessing `error` when request succeeds
|
|
355
|
+
- Any other invalid state transition
|
|
356
|
+
|
|
357
|
+
This is the essence of Type-State Programming: **Make illegal states unrepresentable**.
|
|
358
|
+
|
|
359
|
+
## Core API
|
|
360
|
+
|
|
361
|
+
### Machine Creation
|
|
362
|
+
|
|
363
|
+
#### `createMachine<C, T>(context, transitions)`
|
|
364
|
+
|
|
365
|
+
Creates a synchronous state machine.
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
const machine = createMachine(
|
|
369
|
+
{ count: 0 }, // Context (state data)
|
|
370
|
+
{ // Transitions (state transformations)
|
|
371
|
+
increment: function() {
|
|
372
|
+
return createMachine({ count: this.count + 1 }, this);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
#### `createAsyncMachine<C, T>(context, transitions)`
|
|
379
|
+
|
|
380
|
+
Creates an async state machine (for side effects, API calls, etc.).
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
const machine = createAsyncMachine(
|
|
384
|
+
{ status: "idle", data: null },
|
|
385
|
+
{
|
|
386
|
+
async fetch() {
|
|
387
|
+
try {
|
|
388
|
+
const data = await api.getData();
|
|
389
|
+
return createAsyncMachine({ status: "success", data }, this);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
return createAsyncMachine({ status: "error", data: null }, this);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### `createMachineFactory<C>()`
|
|
399
|
+
|
|
400
|
+
Higher-order function for cleaner machine creation. Write pure context transformers instead of full transition functions.
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { createMachineFactory } from "@doeixd/machine";
|
|
404
|
+
|
|
405
|
+
// Define pure transformations
|
|
406
|
+
const counterFactory = createMachineFactory<{ count: number }>()({
|
|
407
|
+
increment: (ctx) => ({ count: ctx.count + 1 }),
|
|
408
|
+
add: (ctx, n: number) => ({ count: ctx.count + n }),
|
|
409
|
+
reset: (ctx) => ({ count: 0 })
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Create instances
|
|
413
|
+
const counter = counterFactory({ count: 0 });
|
|
414
|
+
const next = counter.add(5); // { count: 5 }
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Benefits:
|
|
418
|
+
- Less boilerplate (no `createMachine` calls in transitions)
|
|
419
|
+
- Pure functions are easier to test
|
|
420
|
+
- Cleaner separation of logic and structure
|
|
421
|
+
|
|
422
|
+
### Runtime & Events
|
|
423
|
+
|
|
424
|
+
#### `runMachine<M>(initial, onChange?)`
|
|
425
|
+
|
|
426
|
+
Creates a managed runtime for async machines with event dispatching.
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import { runMachine, Event } from "@doeixd/machine";
|
|
430
|
+
|
|
431
|
+
const runner = runMachine(
|
|
432
|
+
createFetchMachine(),
|
|
433
|
+
(machine) => {
|
|
434
|
+
console.log("State changed:", machine.context);
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Type-safe event dispatch
|
|
439
|
+
await runner.dispatch({ type: "fetch", args: [123] });
|
|
440
|
+
|
|
441
|
+
// Access current state
|
|
442
|
+
console.log(runner.state); // Current context
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
The `Event<M>` type automatically generates a discriminated union of all valid events from your machine type:
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
type FetchEvent = Event<FetchMachine>;
|
|
449
|
+
// = { type: "fetch", args: [number] } | { type: "retry", args: [] } | ...
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### State Utilities
|
|
453
|
+
|
|
454
|
+
#### `setContext<M>(machine, newContext)`
|
|
455
|
+
|
|
456
|
+
Immutably updates a machine's context while preserving transitions.
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { setContext } from "@doeixd/machine";
|
|
460
|
+
|
|
461
|
+
// With updater function
|
|
462
|
+
const updated = setContext(machine, (ctx) => ({ count: ctx.count + 1 }));
|
|
463
|
+
|
|
464
|
+
// With direct value
|
|
465
|
+
const reset = setContext(machine, { count: 0 });
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
#### `next<C>(machine, update)`
|
|
469
|
+
|
|
470
|
+
Simpler version of `setContext` - applies an update function to the context.
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
import { next } from "@doeixd/machine";
|
|
474
|
+
|
|
475
|
+
const updated = next(counter, (ctx) => ({ count: ctx.count + 1 }));
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
#### `matchMachine<M, K, R>(machine, key, handlers)`
|
|
479
|
+
|
|
480
|
+
Type-safe pattern matching on discriminated unions in context.
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import { matchMachine } from "@doeixd/machine";
|
|
484
|
+
|
|
485
|
+
const message = matchMachine(machine, "status", {
|
|
486
|
+
idle: (ctx) => "Ready to start",
|
|
487
|
+
loading: (ctx) => "Loading...",
|
|
488
|
+
success: (ctx) => `Loaded: ${ctx.data}`,
|
|
489
|
+
error: (ctx) => `Error: ${ctx.error}`
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
TypeScript enforces exhaustive checking - you must handle all cases!
|
|
494
|
+
|
|
495
|
+
#### `hasState<M, K, V>(machine, key, value)`
|
|
496
|
+
|
|
497
|
+
Type guard for state checking with type narrowing.
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import { hasState } from "@doeixd/machine";
|
|
501
|
+
|
|
502
|
+
if (hasState(machine, "status", "loading")) {
|
|
503
|
+
// TypeScript knows machine.context.status === "loading"
|
|
504
|
+
console.log("Currently loading");
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Composition & Transformation
|
|
509
|
+
|
|
510
|
+
#### `overrideTransitions<M, T>(machine, overrides)`
|
|
511
|
+
|
|
512
|
+
Creates a new machine with replaced/added transitions. Perfect for testing and decoration.
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import { overrideTransitions } from "@doeixd/machine";
|
|
516
|
+
|
|
517
|
+
// Mock for testing
|
|
518
|
+
const mocked = overrideTransitions(counter, {
|
|
519
|
+
increment: function() {
|
|
520
|
+
return createMachine({ count: 999 }, this);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Decorate with logging
|
|
525
|
+
const logged = overrideTransitions(counter, {
|
|
526
|
+
increment: function() {
|
|
527
|
+
console.log("Before:", this.count);
|
|
528
|
+
const next = counter.increment.call(this);
|
|
529
|
+
console.log("After:", next.context.count);
|
|
530
|
+
return next;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
#### `extendTransitions<M, T>(machine, newTransitions)`
|
|
536
|
+
|
|
537
|
+
Safely adds new transitions. Prevents accidental overwrites with compile-time errors.
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
import { extendTransitions } from "@doeixd/machine";
|
|
541
|
+
|
|
542
|
+
const extended = extendTransitions(counter, {
|
|
543
|
+
reset: function() {
|
|
544
|
+
return createMachine({ count: 0 }, this);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Compile error if transition already exists:
|
|
549
|
+
// extendTransitions(counter, { increment: ... }); // ❌ Error!
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
#### `createMachineBuilder<M>(template)`
|
|
553
|
+
|
|
554
|
+
Creates a factory from a template machine. Excellent for class-based machines.
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
import { MachineBase, createMachineBuilder } from "@doeixd/machine";
|
|
558
|
+
|
|
559
|
+
class User extends MachineBase<{ id: number; name: string }> {
|
|
560
|
+
rename(name: string) {
|
|
561
|
+
return buildUser({ ...this.context, name });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const template = new User({ id: 0, name: "" });
|
|
566
|
+
const buildUser = createMachineBuilder(template);
|
|
567
|
+
|
|
568
|
+
// Stamp out instances
|
|
569
|
+
const alice = buildUser({ id: 1, name: "Alice" });
|
|
570
|
+
const bob = buildUser({ id: 2, name: "Bob" });
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Type Utilities
|
|
574
|
+
|
|
575
|
+
#### Type Extraction
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
import { Context, Transitions, Event, TransitionArgs } from "@doeixd/machine";
|
|
579
|
+
|
|
580
|
+
type MyMachine = Machine<{ count: number }> & {
|
|
581
|
+
add: (n: number) => MyMachine;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
type Ctx = Context<MyMachine>; // { count: number }
|
|
585
|
+
type Trans = Transitions<MyMachine>; // { add: (n: number) => MyMachine }
|
|
586
|
+
type Evt = Event<MyMachine>; // { type: "add", args: [number] }
|
|
587
|
+
type Args = TransitionArgs<MyMachine, "add">; // [number]
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
#### Additional Types
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import {
|
|
594
|
+
DeepReadonly, // Make types deeply immutable
|
|
595
|
+
InferMachine, // Extract machine type from factory
|
|
596
|
+
TransitionNames, // Get union of transition names
|
|
597
|
+
BaseMachine, // Base type for Machine & AsyncMachine
|
|
598
|
+
MachineLike, // Machine or Promise<Machine>
|
|
599
|
+
MachineResult // Machine or [Machine, cleanup]
|
|
600
|
+
} from "@doeixd/machine";
|
|
601
|
+
|
|
602
|
+
type Factory = () => createMachine({ count: 0 }, { ... });
|
|
603
|
+
type M = InferMachine<Factory>; // Extracts return type
|
|
604
|
+
|
|
605
|
+
type Names = TransitionNames<MyMachine>; // "add" | "increment" | ...
|
|
606
|
+
|
|
607
|
+
// For functions that can return sync or async machines
|
|
608
|
+
function getMachine(): MachineLike<{ count: number }> {
|
|
609
|
+
// Can return either Machine or Promise<Machine>
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// For transitions with cleanup effects
|
|
613
|
+
function enterState(): MachineResult<{ timer: number }> {
|
|
614
|
+
const interval = setInterval(() => tick(), 1000);
|
|
615
|
+
const machine = createMachine({ timer: 0 }, { ... });
|
|
616
|
+
return [machine, () => clearInterval(interval)];
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## Advanced Features
|
|
621
|
+
|
|
622
|
+
### Generator-Based Composition
|
|
623
|
+
|
|
624
|
+
For complex multi-step workflows, use generator-based composition. This provides an imperative, procedural style while maintaining immutability and type safety.
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
import { run, step } from "@doeixd/machine";
|
|
628
|
+
|
|
629
|
+
const result = run(function* (machine) {
|
|
630
|
+
// Write sequential code with generators
|
|
631
|
+
let m = yield* step(machine.increment());
|
|
632
|
+
m = yield* step(m.add(5));
|
|
633
|
+
|
|
634
|
+
// Use normal control flow
|
|
635
|
+
if (m.context.count > 10) {
|
|
636
|
+
m = yield* step(m.reset());
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Loops work naturally
|
|
640
|
+
for (let i = 0; i < 3; i++) {
|
|
641
|
+
m = yield* step(m.increment());
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return m.context.count;
|
|
645
|
+
}, counter);
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
**Benefits:**
|
|
649
|
+
- Write imperative code that feels sequential
|
|
650
|
+
- Maintain immutability (each step yields a new state)
|
|
651
|
+
- Full type safety maintained
|
|
652
|
+
- Use if/else, loops, try/catch naturally
|
|
653
|
+
- Great for testing and step-by-step workflows
|
|
654
|
+
|
|
655
|
+
**Utilities:**
|
|
656
|
+
- `run(flow, initial)` - Execute a generator flow
|
|
657
|
+
- `step(machine)` - Yield a state and receive the next
|
|
658
|
+
- `runSequence(initial, flows)` - Compose multiple flows
|
|
659
|
+
- `createFlow(fn)` - Create reusable flow patterns
|
|
660
|
+
- `runWithDebug(flow, initial)` - Debug with logging
|
|
661
|
+
- `runAsync(flow, initial)` - Async generator support
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
// Async generators for async machines
|
|
665
|
+
const result = await runAsync(async function* (m) {
|
|
666
|
+
m = yield* stepAsync(await m.fetchData());
|
|
667
|
+
m = yield* stepAsync(await m.processData());
|
|
668
|
+
return m.context;
|
|
669
|
+
}, asyncMachine);
|
|
670
|
+
|
|
671
|
+
// Reusable flows
|
|
672
|
+
const incrementThrice = createFlow(function* (m) {
|
|
673
|
+
m = yield* step(m.increment());
|
|
674
|
+
m = yield* step(m.increment());
|
|
675
|
+
m = yield* step(m.increment());
|
|
676
|
+
return m;
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const result = run(function* (m) {
|
|
680
|
+
m = yield* incrementThrice(m); // Compose flows
|
|
681
|
+
m = yield* step(m.add(10));
|
|
682
|
+
return m;
|
|
683
|
+
}, counter);
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### React Integration
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
import { useMachine } from "@doeixd/machine/react";
|
|
690
|
+
|
|
691
|
+
function Counter() {
|
|
692
|
+
const [machine, dispatch] = useMachine(() => createCounterMachine());
|
|
693
|
+
|
|
694
|
+
return (
|
|
695
|
+
<div>
|
|
696
|
+
<p>Count: {machine.context.count}</p>
|
|
697
|
+
<button onClick={() => dispatch({ type: "increment", args: [] })}>
|
|
698
|
+
Increment
|
|
699
|
+
</button>
|
|
700
|
+
</div>
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Solid.js Integration
|
|
706
|
+
|
|
707
|
+
Comprehensive Solid.js integration with signals, stores, and fine-grained reactivity:
|
|
708
|
+
|
|
709
|
+
```typescript
|
|
710
|
+
import { createMachine, createMachineStore, createAsyncMachine } from "@doeixd/machine/solid";
|
|
711
|
+
|
|
712
|
+
// Signal-based (simple state)
|
|
713
|
+
function Counter() {
|
|
714
|
+
const [machine, actions] = createMachine(() => createCounterMachine());
|
|
715
|
+
|
|
716
|
+
return (
|
|
717
|
+
<div>
|
|
718
|
+
<p>Count: {machine().context.count}</p>
|
|
719
|
+
<button onClick={actions.increment}>Increment</button>
|
|
720
|
+
</div>
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Store-based (fine-grained reactivity for complex context)
|
|
725
|
+
function UserProfile() {
|
|
726
|
+
const [machine, setMachine, actions] = createMachineStore(() =>
|
|
727
|
+
createUserMachine()
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
return (
|
|
731
|
+
<div>
|
|
732
|
+
<p>Name: {machine.context.profile.name}</p>
|
|
733
|
+
<p>Age: {machine.context.profile.age}</p>
|
|
734
|
+
<button onClick={() => actions.updateName('Alice')}>Change Name</button>
|
|
735
|
+
</div>
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Async machine with reactive state
|
|
740
|
+
function DataFetcher() {
|
|
741
|
+
const [state, dispatch] = createAsyncMachine(() => createFetchMachine());
|
|
742
|
+
|
|
743
|
+
return (
|
|
744
|
+
<Switch>
|
|
745
|
+
<Match when={state().context.status === 'idle'}>
|
|
746
|
+
<button onClick={() => dispatch({ type: 'fetch', args: [] })}>
|
|
747
|
+
Load
|
|
748
|
+
</button>
|
|
749
|
+
</Match>
|
|
750
|
+
<Match when={state().context.status === 'loading'}>
|
|
751
|
+
<p>Loading...</p>
|
|
752
|
+
</Match>
|
|
753
|
+
<Match when={state().context.status === 'success'}>
|
|
754
|
+
<p>Data: {state().context.data}</p>
|
|
755
|
+
</Match>
|
|
756
|
+
</Switch>
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**Solid utilities:**
|
|
762
|
+
- `createMachine()` - Signal-based reactive machine
|
|
763
|
+
- `createMachineStore()` - Store-based with fine-grained reactivity
|
|
764
|
+
- `createAsyncMachine()` - Async machine with signals
|
|
765
|
+
- `createMachineContext()` - Context-only store
|
|
766
|
+
- `createMachineSelector()` - Memoized derivations
|
|
767
|
+
- `createMachineEffect()` - Lifecycle effects on state changes
|
|
768
|
+
- `createMachineValueEffect()` - Effects on context values
|
|
769
|
+
|
|
770
|
+
### DevTools Integration
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
import { connectToDevTools } from "@doeixd/machine/devtools";
|
|
774
|
+
|
|
775
|
+
const runner = connectToDevTools(createMachine(...));
|
|
776
|
+
// Automatically sends state changes to browser extension
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Static Analysis & Visualization
|
|
780
|
+
|
|
781
|
+
Use type-level metadata to extract formal statecharts:
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
import { transitionTo, guarded, invoke, describe } from "@doeixd/machine/primitives";
|
|
785
|
+
|
|
786
|
+
class AuthMachine extends MachineBase<{ status: "idle" }> {
|
|
787
|
+
// Annotate transitions with metadata
|
|
788
|
+
login = describe(
|
|
789
|
+
"Authenticates the user",
|
|
790
|
+
transitionTo(LoggedInMachine, (username: string) => {
|
|
791
|
+
return new LoggedInMachine({ username });
|
|
792
|
+
})
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
// Add guards
|
|
796
|
+
adminAction = guarded(
|
|
797
|
+
{ name: "isAdmin" },
|
|
798
|
+
transitionTo(AdminMachine, () => new AdminMachine())
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
// Declare async effects
|
|
802
|
+
fetchData = invoke(
|
|
803
|
+
{
|
|
804
|
+
src: "fetchUserData",
|
|
805
|
+
onDone: SuccessMachine,
|
|
806
|
+
onError: ErrorMachine
|
|
807
|
+
},
|
|
808
|
+
async () => { /* ... */ }
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
Extract to JSON statechart:
|
|
814
|
+
|
|
815
|
+
```bash
|
|
816
|
+
npx ts-node src/extract.ts > statechart.json
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
This generates formal statechart definitions compatible with visualization tools like Stately.ai.
|
|
820
|
+
|
|
821
|
+
### OOP Style with `MachineBase`
|
|
822
|
+
|
|
823
|
+
For complex machines, use class-based approach:
|
|
824
|
+
|
|
825
|
+
```typescript
|
|
826
|
+
import { MachineBase, Context } from "@doeixd/machine";
|
|
827
|
+
|
|
828
|
+
class Counter extends MachineBase<{ count: number }> {
|
|
829
|
+
constructor(count = 0) {
|
|
830
|
+
super({ count });
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
increment(): Counter {
|
|
834
|
+
return new Counter(this.context.count + 1);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
add(n: number): Counter {
|
|
838
|
+
return new Counter(this.context.count + n);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const counter = new Counter(5);
|
|
843
|
+
const next = counter.increment(); // count: 6
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
## Utilities Module
|
|
847
|
+
|
|
848
|
+
Additional helpers in `@doeixd/machine/utils`:
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
import {
|
|
852
|
+
isState, // Type-safe state checking (for classes)
|
|
853
|
+
createEvent, // Event factory with inference
|
|
854
|
+
mergeContext, // Shallow merge context updates
|
|
855
|
+
pipeTransitions, // Compose transitions sequentially
|
|
856
|
+
logState // Debug helper (tap function)
|
|
857
|
+
} from "@doeixd/machine/utils";
|
|
858
|
+
|
|
859
|
+
// Type-safe class instance check
|
|
860
|
+
if (isState(machine, LoggedInMachine)) {
|
|
861
|
+
machine.logout(); // TypeScript knows it's LoggedInMachine
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Event creation
|
|
865
|
+
const event = createEvent<MyMachine, "add">("add", 5);
|
|
866
|
+
|
|
867
|
+
// Merge partial context
|
|
868
|
+
const updated = mergeContext(user, { status: "active" });
|
|
869
|
+
|
|
870
|
+
// Compose transitions
|
|
871
|
+
const result = await pipeTransitions(
|
|
872
|
+
machine,
|
|
873
|
+
(m) => m.increment(),
|
|
874
|
+
(m) => m.add(5),
|
|
875
|
+
(m) => m.increment()
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
// Debug logging
|
|
879
|
+
pipeTransitions(
|
|
880
|
+
machine,
|
|
881
|
+
logState, // Logs current state
|
|
882
|
+
(m) => m.increment(),
|
|
883
|
+
(m) => logState(m, "After increment:")
|
|
884
|
+
);
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
## Philosophy & Design Principles
|
|
888
|
+
|
|
889
|
+
### 1. Type-State Programming First
|
|
890
|
+
|
|
891
|
+
**Type-State Programming is the heart of this library.** The type system itself represents your state machine:
|
|
892
|
+
|
|
893
|
+
- **States are types**, not strings or enums
|
|
894
|
+
- **Invalid transitions are compile errors**, not runtime exceptions
|
|
895
|
+
- **TypeScript is your safety net** - bugs are caught during development
|
|
896
|
+
- **The compiler guides you** - autocomplete shows only valid transitions
|
|
897
|
+
|
|
898
|
+
This isn't just a feature—it's the fundamental way you should think about state machines in TypeScript. Make illegal states unrepresentable.
|
|
899
|
+
|
|
900
|
+
### 2. Minimal Primitives
|
|
901
|
+
|
|
902
|
+
The core library provides only the essential building blocks:
|
|
903
|
+
- `Machine<C>` and `AsyncMachine<C>` types
|
|
904
|
+
- `createMachine()` and `createAsyncMachine()` functions
|
|
905
|
+
- `runMachine()` for async runtime
|
|
906
|
+
- Basic composition utilities
|
|
907
|
+
|
|
908
|
+
Everything else is built on top of these primitives. We give you the foundation; you build what you need.
|
|
909
|
+
|
|
910
|
+
### 3. TypeScript as the Compiler
|
|
911
|
+
|
|
912
|
+
We rely heavily on TypeScript's type system to catch bugs:
|
|
913
|
+
|
|
914
|
+
- **Full type inference** - minimal annotations needed
|
|
915
|
+
- **Exhaustive checking** - compiler ensures all cases handled
|
|
916
|
+
- **Type narrowing** - guards refine types automatically
|
|
917
|
+
- **No escape hatches** - no `any` in public APIs
|
|
918
|
+
- **Compile-time validation** - zero runtime overhead for safety
|
|
919
|
+
|
|
920
|
+
The philosophy: if it compiles, it's safe.
|
|
921
|
+
|
|
922
|
+
### 4. No Magic Strings - Typed References Only
|
|
923
|
+
|
|
924
|
+
We avoid magic strings wherever possible. Instead, we use **typed object references** so TypeScript can infer types automatically:
|
|
925
|
+
|
|
926
|
+
```typescript
|
|
927
|
+
// ✅ Good: Typed method reference
|
|
928
|
+
const counter = createMachine({ count: 0 }, {
|
|
929
|
+
increment: function() {
|
|
930
|
+
return createMachine({ count: this.count + 1 }, this);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
counter.increment(); // TypeScript knows this exists
|
|
935
|
+
|
|
936
|
+
// ✅ Good: Events inferred from machine structure
|
|
937
|
+
type CounterEvent = Event<typeof counter>;
|
|
938
|
+
// Automatically: { type: "increment", args: [] }
|
|
939
|
+
|
|
940
|
+
// ❌ Bad (other libraries): Magic strings
|
|
941
|
+
// send({ type: "INCREMENT" }) // Easy to typo, no refactoring support
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
**Benefits:**
|
|
945
|
+
- **Rename refactoring works perfectly** - change method name, all call sites update
|
|
946
|
+
- **Impossible to typo** - TypeScript catches invalid references
|
|
947
|
+
- **Autocomplete everywhere** - IDE knows what methods exist
|
|
948
|
+
- **Type inference flows naturally** - no manual type annotations needed
|
|
949
|
+
- **No runtime string matching** - direct function calls are faster
|
|
950
|
+
|
|
951
|
+
### 5. Flexibility Over Prescription
|
|
952
|
+
|
|
953
|
+
- **Immutability by default but not enforced** - mutate if you need to
|
|
954
|
+
- **Multiple styles supported**: functional, OOP, factory pattern
|
|
955
|
+
- **No hidden magic** - what you see is what you get
|
|
956
|
+
- **Pay for what you use** - minimal runtime overhead
|
|
957
|
+
- **Progressive enhancement** - start simple, add Type-State when needed
|
|
958
|
+
|
|
959
|
+
### 6. Solid Foundation for Extension
|
|
960
|
+
|
|
961
|
+
This library is designed to be extended:
|
|
962
|
+
- Build your own abstractions on top
|
|
963
|
+
- Add custom primitives for your domain
|
|
964
|
+
- Use the type system to enforce your invariants
|
|
965
|
+
- Extract formal models with static analysis
|
|
966
|
+
- Create domain-specific state machine libraries
|
|
967
|
+
|
|
968
|
+
## Comparison with Other Libraries
|
|
969
|
+
|
|
970
|
+
> **📖 [Read the full in-depth comparison with XState](./docs/XSTATE_COMPARISON.md)** - Comprehensive analysis of philosophy, features, API differences, strengths/weaknesses, use cases, and code examples.
|
|
971
|
+
|
|
972
|
+
### vs. XState (Summary)
|
|
973
|
+
|
|
974
|
+
**XState** is a comprehensive implementation of Statecharts with nested states, parallel states, actors, and more.
|
|
975
|
+
|
|
976
|
+
**Key Differences:**
|
|
977
|
+
- **Paradigm**: XState is declarative (config objects). `@doeixd/machine` is imperative (method calls).
|
|
978
|
+
- **Type Safety**: XState uses string-based states with good TypeScript support. We use **Type-State Programming**—states ARE types, enforced at compile time.
|
|
979
|
+
- **Complexity**: XState provides full Statecharts features. `@doeixd/machine` provides minimal primitives to build upon.
|
|
980
|
+
- **Strings**: XState uses event strings (`send('ACTION')`). We use typed method references (`machine.action()`).
|
|
981
|
+
- **Use Case**: XState for complex app-wide orchestration. `@doeixd/machine` for type-safe component logic and custom abstractions.
|
|
982
|
+
- **Bundle Size**: XState ~15-20KB. `@doeixd/machine` ~1.3KB.
|
|
983
|
+
|
|
984
|
+
**When to use each:**
|
|
985
|
+
- **XState**: Need nested states, parallel states, actors, visual editor, or complex workflows
|
|
986
|
+
- **@doeixd/machine**: Want maximum type safety, minimal bundle, compile-time guarantees, or building on primitives
|
|
987
|
+
|
|
988
|
+
### vs. Robot3
|
|
989
|
+
|
|
990
|
+
**Robot3** is also minimal and functional.
|
|
991
|
+
|
|
992
|
+
- **API**: Robot3 uses message passing (`send()`). We use direct method calls (`machine.action()`).
|
|
993
|
+
- **Type-State**: Robot3 has good TS support, but Type-State Programming is more central here.
|
|
994
|
+
- **Flexibility**: Both are flexible, but we provide more compositional utilities out of the box.
|
|
995
|
+
- **Strings**: Robot3 uses event strings. We avoid magic strings entirely.
|
|
996
|
+
|
|
997
|
+
### Choose `@doeixd/machine` if you:
|
|
998
|
+
|
|
999
|
+
- Want to leverage TypeScript's type system for **compile-time correctness**
|
|
1000
|
+
- Prefer **minimal primitives** you can build upon
|
|
1001
|
+
- Need **Type-State Programming** for finite state validation
|
|
1002
|
+
- Want **flexibility** in how you model state (immutable, mutable, classes, functions)
|
|
1003
|
+
- Value **mathematical foundations** and formal correctness
|
|
1004
|
+
- Want to **avoid magic strings** and use typed references
|
|
1005
|
+
- Care about **bundle size** (1.3KB vs 15KB+)
|
|
1006
|
+
|
|
1007
|
+
## API Reference
|
|
1008
|
+
|
|
1009
|
+
### Core Types
|
|
1010
|
+
|
|
1011
|
+
```typescript
|
|
1012
|
+
// Machine types
|
|
1013
|
+
type Machine<C extends object>
|
|
1014
|
+
type AsyncMachine<C extends object>
|
|
1015
|
+
type BaseMachine<C extends object>
|
|
1016
|
+
|
|
1017
|
+
// Type utilities
|
|
1018
|
+
type Context<M>
|
|
1019
|
+
type Transitions<M>
|
|
1020
|
+
type Event<M>
|
|
1021
|
+
type TransitionArgs<M, K>
|
|
1022
|
+
type TransitionNames<M>
|
|
1023
|
+
type DeepReadonly<T>
|
|
1024
|
+
type InferMachine<F>
|
|
1025
|
+
type MachineLike<C>
|
|
1026
|
+
type MachineResult<C>
|
|
1027
|
+
|
|
1028
|
+
// Classes
|
|
1029
|
+
class MachineBase<C extends object>
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### Core Functions
|
|
1033
|
+
|
|
1034
|
+
```typescript
|
|
1035
|
+
// Creation
|
|
1036
|
+
createMachine<C, T>(context: C, fns: T): Machine<C> & T
|
|
1037
|
+
createAsyncMachine<C, T>(context: C, fns: T): AsyncMachine<C> & T
|
|
1038
|
+
createMachineFactory<C>(): (transformers) => (initialContext) => Machine<C>
|
|
1039
|
+
|
|
1040
|
+
// Runtime
|
|
1041
|
+
runMachine<M>(initial: M, onChange?: (m: M) => void): { state, dispatch }
|
|
1042
|
+
|
|
1043
|
+
// Composition & State Updates
|
|
1044
|
+
setContext<M>(machine: M, newContext): M
|
|
1045
|
+
next<C>(machine: Machine<C>, update: (ctx: C) => C): Machine<C>
|
|
1046
|
+
overrideTransitions<M, T>(machine: M, overrides: T): M & T
|
|
1047
|
+
extendTransitions<M, T>(machine: M, newTransitions: T): M & T
|
|
1048
|
+
createMachineBuilder<M>(template: M): (context) => M
|
|
1049
|
+
|
|
1050
|
+
// Pattern Matching
|
|
1051
|
+
matchMachine<M, K, R>(machine: M, key: K, handlers): R
|
|
1052
|
+
hasState<M, K, V>(machine: M, key: K, value: V): boolean
|
|
1053
|
+
|
|
1054
|
+
// Generator-Based Composition
|
|
1055
|
+
run<C, T>(flow: (m: Machine<C>) => Generator<...>, initial: Machine<C>): T
|
|
1056
|
+
step<C>(machine: Machine<C>): Generator<...>
|
|
1057
|
+
runSequence<C>(initial: Machine<C>, flows: Array<...>): Machine<C>
|
|
1058
|
+
createFlow<C>(flow: (m: Machine<C>) => Generator<...>): (m: Machine<C>) => Generator<...>
|
|
1059
|
+
runWithDebug<C, T>(flow: ..., initial: Machine<C>, logger?: ...): T
|
|
1060
|
+
runAsync<C, T>(flow: (m: Machine<C>) => AsyncGenerator<...>, initial: Machine<C>): Promise<T>
|
|
1061
|
+
stepAsync<C>(machine: Machine<C>): AsyncGenerator<...>
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
## License
|
|
1065
|
+
|
|
1066
|
+
MIT
|
|
1067
|
+
|
|
1068
|
+
## Contributing
|
|
1069
|
+
|
|
1070
|
+
Contributions welcome! This library aims to stay minimal while providing a solid foundation. When proposing features, consider whether they belong in the core or as a separate extension package.
|