@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/README.md ADDED
@@ -0,0 +1,1070 @@
1
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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.