@doeixd/machine 0.0.4 → 0.0.5

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.
@@ -0,0 +1,838 @@
1
+ /**
2
+ * @file multi.ts - Advanced operational patterns for state machine orchestration.
3
+ * @description
4
+ * This module provides optional, higher-level abstractions for managing machines.
5
+ * They solve common ergonomic and integration challenges without compromising the
6
+ * immutable core of the library.
7
+ *
8
+ * It introduces three patterns:
9
+ *
10
+ * 1. **Runner (`createRunner`):** A stateful controller for ergonomic control
11
+ * of a single, immutable machine. Solves state reassignment.
12
+ *
13
+ * 2. **Ensemble (`createEnsemble`):** A functional pattern for orchestrating logic
14
+ * over an external, framework-agnostic state store.
15
+ *
16
+ * 3. **MultiMachine (`createMultiMachine`):** A class-based alternative to the
17
+ * Ensemble for OOP-style orchestration.
18
+ */
19
+ import { Machine, Context, TransitionArgs, TransitionNames } from './index';
20
+ /**
21
+ * A mapped type that creates a new object type with the same transition methods
22
+ * as the machine `M`, but pre-bound to update a Runner's internal state.
23
+ *
24
+ * When you call a method on `BoundTransitions`, it automatically transitions the
25
+ * runner's state and returns the new machine instance. This is the key mechanism
26
+ * that eliminates the need for manual state reassignment in imperative code.
27
+ *
28
+ * @template M - The machine type, which can be a union of multiple machine states.
29
+ *
30
+ * @example
31
+ * // If your machine has these transitions:
32
+ * // increment: () => Machine
33
+ * // add: (n: number) => Machine
34
+ * // Then BoundTransitions<typeof machine> provides:
35
+ * // increment: () => Machine (auto-updates runner state)
36
+ * // add: (n: number) => Machine (auto-updates runner state)
37
+ */
38
+ export type BoundTransitions<M extends Machine<any>> = {
39
+ [K in TransitionNames<M>]: (...args: TransitionArgs<M, K>) => M[K] extends (...args: any[]) => infer R ? R : never;
40
+ };
41
+ /**
42
+ * A stateful controller that wraps an immutable machine instance, providing a
43
+ * stable API for imperative state transitions without manual reassignment.
44
+ *
45
+ * The Runner holds the "current" machine state internally and updates it whenever
46
+ * an action is called. This solves the ergonomic problem of having to write:
47
+ * `machine = machine.transition()` over and over. Instead, you just call
48
+ * `runner.actions.transition()` and the runner manages the state for you.
49
+ *
50
+ * **Use Runner for:**
51
+ * - Complex local component state (React, Vue, Svelte components)
52
+ * - Scripts that need clean imperative state management
53
+ * - Situations where you have a single, self-contained state machine
54
+ *
55
+ * **Don't use Runner for:**
56
+ * - Global application state (use Ensemble instead)
57
+ * - Multiple interconnected machines
58
+ *
59
+ * @template M - The machine type (can be a union of states for Type-State patterns).
60
+ */
61
+ export type Runner<M extends Machine<any>> = {
62
+ /**
63
+ * The current, raw machine instance. This property is essential for
64
+ * type-narrowing in Type-State Programming patterns.
65
+ *
66
+ * Since machines can be unions of different state types, you can narrow
67
+ * the type by checking `runner.state.context` properties, and TypeScript
68
+ * will automatically narrow which transitions are available.
69
+ *
70
+ * @example
71
+ * if (runner.state.context.status === 'loggedIn') {
72
+ * // runner.state is now typed as LoggedInMachine
73
+ * console.log(runner.state.context.username);
74
+ * runner.actions.logout(); // Only available when logged in
75
+ * }
76
+ */
77
+ readonly state: M;
78
+ /**
79
+ * A direct, readonly accessor to the context of the current machine state.
80
+ * This is a convenience property equivalent to `runner.state.context`.
81
+ *
82
+ * @example
83
+ * console.log(runner.context.count); // Same as runner.state.context.count
84
+ */
85
+ readonly context: Context<M>;
86
+ /**
87
+ * A stable object containing all available transition methods, pre-bound to
88
+ * update the runner's state. This is the primary way to trigger transitions.
89
+ *
90
+ * When you call `runner.actions.someTransition()`, the runner automatically:
91
+ * 1. Calls the transition on the current machine
92
+ * 2. Updates `runner.state` with the new machine instance
93
+ * 3. Fires the `onChange` callback (if provided to createRunner)
94
+ * 4. Returns the new machine instance
95
+ *
96
+ * Note: For union-type machines, you must first narrow the type of `runner.state`
97
+ * to ensure a given action is available at compile time.
98
+ *
99
+ * @example
100
+ * runner.actions.increment(); // Automatically updates runner.state
101
+ * runner.actions.add(5); // Returns new machine instance
102
+ */
103
+ readonly actions: BoundTransitions<M>;
104
+ /**
105
+ * Manually sets the runner to a new machine state. Useful for resetting state
106
+ * or synchronizing with external events.
107
+ *
108
+ * This method bypasses the normal transition path and directly updates the
109
+ * runner's internal state. The `onChange` callback will be called.
110
+ *
111
+ * @param newState - The new machine instance to set.
112
+ *
113
+ * @example
114
+ * const reset = createCounterMachine({ count: 0 });
115
+ * runner.setState(reset); // Jump back to initial state
116
+ */
117
+ setState(newState: M): void;
118
+ };
119
+ /**
120
+ * Creates a Managed State Runner by wrapping a pure, immutable machine instance
121
+ * in a stateful controller. This eliminates the need for `machine = machine.transition()`
122
+ * reassignment, providing a more ergonomic, imperative API for complex local state.
123
+ *
124
+ * **How it works:**
125
+ * 1. The runner holds a reference to the current machine internally
126
+ * 2. When you call `runner.actions.transition()`, it calls the transition on the
127
+ * current machine and automatically updates the runner's internal state
128
+ * 3. The runner exposes a stable `actions` object that always reflects what
129
+ * transitions are available on the *current* machine (important for Type-State)
130
+ * 4. The `onChange` callback is invoked after every state change
131
+ *
132
+ * **Key difference from just calling transitions directly:**
133
+ * Instead of: `let machine = createMachine(...); machine = machine.increment();`
134
+ * You write: `const runner = createRunner(machine); runner.actions.increment();`
135
+ *
136
+ * The runner *is* the state holder, so you never need to reassign variables.
137
+ *
138
+ * @template M - The machine type.
139
+ * @param initialMachine - The starting machine instance.
140
+ * @param onChange - Optional callback fired after every state transition. Receives
141
+ * the new machine state, allowing you to react to changes (e.g., update a UI,
142
+ * log state changes, or trigger side effects).
143
+ * @returns A `Runner` instance with `state`, `context`, `actions`, and `setState()`.
144
+ *
145
+ * @example
146
+ * // Simple counter example
147
+ * const counterMachine = createCounterMachine({ count: 0 });
148
+ * const runner = createRunner(counterMachine, (newState) => {
149
+ * console.log('Count is now:', newState.context.count);
150
+ * });
151
+ *
152
+ * runner.actions.increment(); // Logs: "Count is now: 1"
153
+ * runner.actions.add(5); // Logs: "Count is now: 6"
154
+ * console.log(runner.context.count); // 6
155
+ *
156
+ * @example
157
+ * // Type-State example with conditional narrowing
158
+ * type AuthMachine = LoggedOutState | LoggedInState;
159
+ *
160
+ * const runner = createRunner(createLoggedOutMachine());
161
+ *
162
+ * // Narrow the type to access login
163
+ * if (runner.state.context.status === 'loggedOut') {
164
+ * runner.actions.login('alice'); // Only works in loggedOut state
165
+ * }
166
+ *
167
+ * // Now it's logged in, so we can call logout
168
+ * if (runner.state.context.status === 'loggedIn') {
169
+ * runner.actions.logout();
170
+ * }
171
+ */
172
+ export declare function createRunner<M extends Machine<any>>(initialMachine: M, onChange?: (newState: M) => void): Runner<M>;
173
+ /**
174
+ * Defines the contract for an external, user-provided state store. The Ensemble
175
+ * uses this interface to read and write the machine's context, allowing it to
176
+ * plug into any state management solution (React, Solid, Zustand, etc.).
177
+ *
178
+ * **The power of this abstraction:**
179
+ * Your machine logic is completely decoupled from how or where the state is stored.
180
+ * The same machine factories can work with React's `useState`, Solid's `createSignal`,
181
+ * a plain object, or any custom store implementation.
182
+ *
183
+ * **Implementation examples:**
184
+ * - React: `{ getContext: () => state, setContext: setState }`
185
+ * - Solid: `{ getContext: () => store, setContext: (newCtx) => Object.assign(store, newCtx) }`
186
+ * - Plain object: `{ getContext: () => context, setContext: (ctx) => Object.assign(context, ctx) }`
187
+ *
188
+ * @template C - The shared context object type.
189
+ *
190
+ * @example
191
+ * // Implement a simple in-memory store
192
+ * let sharedContext = { status: 'idle' };
193
+ * const store: StateStore<typeof sharedContext> = {
194
+ * getContext: () => sharedContext,
195
+ * setContext: (newCtx) => { sharedContext = newCtx; }
196
+ * };
197
+ *
198
+ * @example
199
+ * // Implement a React-based store
200
+ * function useAppStore() {
201
+ * const [state, setState] = useState({ status: 'idle' });
202
+ * return {
203
+ * getContext: () => state,
204
+ * setContext: setState
205
+ * };
206
+ * }
207
+ */
208
+ export interface StateStore<C extends object> {
209
+ /**
210
+ * A function that returns the current, up-to-date context from the external store.
211
+ * Called whenever the Ensemble needs the latest state.
212
+ */
213
+ getContext: () => C;
214
+ /**
215
+ * A function that takes a new context and updates the external store.
216
+ * Called by transitions to persist state changes.
217
+ *
218
+ * @param newContext - The new context object to persist.
219
+ */
220
+ setContext: (newContext: C) => void;
221
+ }
222
+ /**
223
+ * A mapped type that finds all unique transition names across a union of machine types.
224
+ *
225
+ * This type extracts the union of all methods from all possible machine states,
226
+ * excluding the `context` property. This is used to create the `actions` object
227
+ * on an Ensemble, which can have methods from any of the machine states.
228
+ *
229
+ * At runtime, the Ensemble validates that an action is valid for the current state
230
+ * before executing it.
231
+ *
232
+ * @template AllMachines - A union of all possible machine types in an Ensemble.
233
+ *
234
+ * @example
235
+ * type IdleState = Machine<{ status: 'idle' }> & { fetch: () => LoadingState };
236
+ * type LoadingState = Machine<{ status: 'loading' }> & { cancel: () => IdleState };
237
+ * type AllStates = IdleState | LoadingState;
238
+ *
239
+ * // AllTransitions<AllStates> = { fetch: (...) => ..., cancel: (...) => ... }
240
+ * // (Both fetch and cancel are available, but each is only valid in its state)
241
+ */
242
+ type AllTransitions<AllMachines extends Machine<any>> = Omit<{
243
+ [K in keyof AllMachines]: AllMachines[K];
244
+ }[keyof AllMachines], 'context'>;
245
+ /**
246
+ * The Ensemble object. It provides a stable, unified API for orchestrating a
247
+ * state machine whose context is managed by an external store.
248
+ *
249
+ * The Ensemble acts as the "director," determining which machine "actor" is
250
+ * currently active based on the state of the shared context. Unlike a Runner,
251
+ * which manages local state, an Ensemble plugs into external state management
252
+ * (like React's useState, Solid's signal, or a global store).
253
+ *
254
+ * **Key characteristics:**
255
+ * - Dynamically reconstructs the current machine based on context
256
+ * - Validates transitions at runtime for the current state
257
+ * - Integrates seamlessly with framework state managers
258
+ * - Same factories can be reused across different frameworks
259
+ *
260
+ * **Use Ensemble for:**
261
+ * - Global application state
262
+ * - Framework integration (React, Solid, Vue, etc.)
263
+ * - Complex workflows that span multiple components
264
+ * - Decoupling business logic from UI framework
265
+ *
266
+ * @template AllMachines - A union type of all possible machine states.
267
+ * @template C - The shared context type.
268
+ */
269
+ export type Ensemble<AllMachines extends Machine<any>, C extends object> = {
270
+ /**
271
+ * A direct, readonly accessor to the context from the provided `StateStore`.
272
+ * This is always up-to-date with the external store.
273
+ */
274
+ readonly context: C;
275
+ /**
276
+ * The current, fully-typed machine instance. This is dynamically created on-demand
277
+ * based on the context state. Use this for type-narrowing with Type-State patterns.
278
+ *
279
+ * The machine is reconstructed on every access, so it always reflects the
280
+ * current state of the context.
281
+ */
282
+ readonly state: AllMachines;
283
+ /**
284
+ * A stable object containing all possible actions from all machine states.
285
+ * The Ensemble performs a runtime check to ensure an action is valid for the
286
+ * current state before executing it.
287
+ *
288
+ * The `actions` object itself is stable (doesn't change), but the methods
289
+ * available on it dynamically change based on the current state.
290
+ */
291
+ readonly actions: AllTransitions<AllMachines>;
292
+ };
293
+ /**
294
+ * Creates an Ensemble to orchestrate a state machine over an external state store.
295
+ * This is the primary tool for framework integration, as it decouples pure state
296
+ * logic (defined in factories) from an application's state management solution
297
+ * (defined in store).
298
+ *
299
+ * **How it works:**
300
+ * 1. You provide a `StateStore` that can read and write your application's state
301
+ * 2. You define factory functions that create machines for each state
302
+ * 3. You provide a `getDiscriminant` accessor that tells the Ensemble which
303
+ * factory to use based on the current context
304
+ * 4. The Ensemble dynamically constructs the right machine and provides a stable
305
+ * `actions` object to call transitions
306
+ *
307
+ * **Why this pattern?**
308
+ * Your business logic (machines) is completely separated from your state management
309
+ * (React, Solid, Zustand). You can change state managers without rewriting machines,
310
+ * and you can test machines in isolation without framework dependencies.
311
+ *
312
+ * @template C - The shared context type.
313
+ * @template F - An object of functions that create machine instances for each state.
314
+ * Each factory receives the context and returns a Machine instance for that state.
315
+ * @param store - The user-provided `StateStore` that reads/writes the context.
316
+ * @param factories - An object mapping state discriminant keys to factory functions.
317
+ * Each factory receives the context and returns a machine instance.
318
+ * @param getDiscriminant - An accessor function that takes the context and returns
319
+ * the key of the current state in the `factories` object. This provides full
320
+ * refactoring safety—if you rename a property in your context, TypeScript will
321
+ * catch it at the accessor function.
322
+ * @returns An `Ensemble` instance with `context`, `state`, and `actions`.
323
+ *
324
+ * @example
325
+ * // Using a simple in-memory store
326
+ * let sharedContext = { status: 'idle' as const, data: null };
327
+ * const store = {
328
+ * getContext: () => sharedContext,
329
+ * setContext: (newCtx) => { sharedContext = newCtx; }
330
+ * };
331
+ *
332
+ * // Define factories for each state
333
+ * const factories = {
334
+ * idle: (ctx) => createMachine(ctx, {
335
+ * fetch: () => store.setContext({ ...ctx, status: 'loading' })
336
+ * }),
337
+ * loading: (ctx) => createMachine(ctx, {
338
+ * succeed: (data: any) => store.setContext({ status: 'success', data }),
339
+ * fail: (error: string) => store.setContext({ status: 'error', error })
340
+ * }),
341
+ * success: (ctx) => createMachine(ctx, {
342
+ * retry: () => store.setContext({ status: 'loading', data: null })
343
+ * }),
344
+ * error: (ctx) => createMachine(ctx, {
345
+ * retry: () => store.setContext({ status: 'loading', data: null })
346
+ * })
347
+ * };
348
+ *
349
+ * // Create the ensemble with a discriminant accessor
350
+ * const ensemble = createEnsemble(store, factories, (ctx) => ctx.status);
351
+ *
352
+ * // Use the ensemble
353
+ * ensemble.actions.fetch();
354
+ * console.log(ensemble.context.status); // 'loading'
355
+ *
356
+ * @example
357
+ * // React integration example
358
+ * function useAppEnsemble() {
359
+ * const [context, setContext] = useState({ status: 'idle' as const, data: null });
360
+ *
361
+ * const store: StateStore<typeof context> = {
362
+ * getContext: () => context,
363
+ * setContext: (newCtx) => setContext(newCtx)
364
+ * };
365
+ *
366
+ * const ensemble = useMemo(() =>
367
+ * createEnsemble(store, factories, (ctx) => ctx.status),
368
+ * [context] // Re-create ensemble if context changes
369
+ * );
370
+ *
371
+ * return ensemble;
372
+ * }
373
+ *
374
+ * // In your component:
375
+ * function MyComponent() {
376
+ * const ensemble = useAppEnsemble();
377
+ * return (
378
+ * <>
379
+ * <p>Status: {ensemble.context.status}</p>
380
+ * <button onClick={() => ensemble.actions.fetch()}>
381
+ * Fetch Data
382
+ * </button>
383
+ * </>
384
+ * );
385
+ * }
386
+ */
387
+ export declare function createEnsemble<C extends object, F extends Record<string, (context: C) => Machine<C>>>(store: StateStore<C>, factories: F, getDiscriminant: (context: C) => keyof F): Ensemble<ReturnType<F[keyof F]>, C>;
388
+ /**
389
+ * Executes a generator-based workflow using a Managed State Runner.
390
+ *
391
+ * This provides the cleanest syntax for multi-step imperative workflows, as the
392
+ * `yield` keyword is only used for control flow, not state passing. Unlike the
393
+ * basic `run()` function from the core library, this works directly with a Runner,
394
+ * making it perfect for complex local state orchestration.
395
+ *
396
+ * **Syntax benefits:**
397
+ * - No need to manually thread state through a chain of transitions
398
+ * - `yield` is purely for control flow, not for passing state
399
+ * - Can use regular `if`/`for` statements without helpers
400
+ * - Generator return value is automatically your final result
401
+ *
402
+ * @param flow - A generator function that receives the `Runner` instance. The
403
+ * generator can yield values (returned by transitions) and use them for control
404
+ * flow, or just yield for side effects.
405
+ * @param initialMachine - The machine to start the flow with. A runner will be
406
+ * created from this automatically.
407
+ * @returns The final value returned by the generator (the `return` statement).
408
+ *
409
+ * @example
410
+ * // Simple sequential transitions
411
+ * const result = runWithRunner(function* (runner) {
412
+ * yield runner.actions.increment();
413
+ * yield runner.actions.add(10);
414
+ * if (runner.context.count > 5) {
415
+ * yield runner.actions.reset();
416
+ * }
417
+ * return runner.context;
418
+ * }, createCounterMachine());
419
+ * console.log(result); // { count: 0 }
420
+ *
421
+ * @example
422
+ * // Complex workflow with Type-State narrowing
423
+ * const result = runWithRunner(function* (runner) {
424
+ * // Start logged out
425
+ * if (runner.state.context.status === 'loggedOut') {
426
+ * yield runner.actions.login('alice');
427
+ * }
428
+ *
429
+ * // Now logged in, fetch profile
430
+ * if (runner.state.context.status === 'loggedIn') {
431
+ * yield runner.actions.fetchProfile();
432
+ * }
433
+ *
434
+ * // Return final context
435
+ * return runner.context;
436
+ * }, createAuthMachine());
437
+ */
438
+ export declare function runWithRunner<M extends Machine<any>, T>(flow: (runner: Runner<M>) => Generator<any, T, any>, initialMachine: M): T;
439
+ /**
440
+ * Executes a generator-based workflow using an Ensemble.
441
+ *
442
+ * This pattern is ideal for orchestrating complex sagas or workflows that
443
+ * interact with a global, framework-managed state. Like `runWithRunner`,
444
+ * it provides clean imperative syntax for multi-step workflows, but operates
445
+ * on an Ensemble's external store rather than internal state.
446
+ *
447
+ * **Key differences from runWithRunner:**
448
+ * - Works with external state stores (React, Solid, etc.)
449
+ * - Useful for global workflows and sagas
450
+ * - State changes automatically propagate to the framework
451
+ * - Great for testing framework-agnostic state logic
452
+ *
453
+ * @param flow - A generator function that receives the `Ensemble` instance.
454
+ * The generator can read `ensemble.context` and call `ensemble.actions`.
455
+ * @param ensemble - The `Ensemble` to run the workflow against. Its context
456
+ * is shared across the entire workflow.
457
+ * @returns The final value returned by the generator (the `return` statement).
458
+ *
459
+ * @example
460
+ * // Multi-step workflow with an ensemble
461
+ * const result = runWithEnsemble(function* (ensemble) {
462
+ * // Fetch initial data
463
+ * if (ensemble.context.status === 'idle') {
464
+ * yield ensemble.actions.fetch();
465
+ * }
466
+ *
467
+ * // Process the data
468
+ * if (ensemble.context.status === 'success') {
469
+ * yield ensemble.actions.process(ensemble.context.data);
470
+ * }
471
+ *
472
+ * return ensemble.context;
473
+ * }, ensemble);
474
+ *
475
+ * @example
476
+ * // Testing a workflow without a UI framework
477
+ * const store: StateStore<AppContext> = {
478
+ * getContext: () => context,
479
+ * setContext: (newCtx) => Object.assign(context, newCtx)
480
+ * };
481
+ *
482
+ * const ensemble = createEnsemble(store, factories, (ctx) => ctx.status);
483
+ *
484
+ * // Run a complex workflow and assert the result
485
+ * const result = runWithEnsemble(function* (e) {
486
+ * yield e.actions.login('alice');
487
+ * yield e.actions.fetchProfile();
488
+ * yield e.actions.updateEmail('alice@example.com');
489
+ * return e.context;
490
+ * }, ensemble);
491
+ *
492
+ * expect(result.userEmail).toBe('alice@example.com');
493
+ */
494
+ export declare function runWithEnsemble<AllMachines extends Machine<any>, C extends object, T>(flow: (ensemble: Ensemble<AllMachines, C>) => Generator<any, T, any>, ensemble: Ensemble<AllMachines, C>): T;
495
+ /**
496
+ * The base class for creating a class-based state machine (MultiMachine).
497
+ * Extend this class to define your state machine's logic using instance methods
498
+ * as transitions.
499
+ *
500
+ * This approach is ideal for developers who prefer class-based architectures
501
+ * and want to manage a shared context directly through an external StateStore.
502
+ * It provides a familiar OOP interface while maintaining the decoupling benefits
503
+ * of the StateStore pattern.
504
+ *
505
+ * **Key features:**
506
+ * - Extend this class and define transition methods as instance methods
507
+ * - Protected `context` getter provides access to the current state
508
+ * - Protected `setContext()` method updates the external store
509
+ * - Works seamlessly with `createMultiMachine()`
510
+ *
511
+ * @template C - The shared context type. Should typically contain a discriminant
512
+ * property (like `status`) that identifies the current state.
513
+ *
514
+ * @example
515
+ * // Define your context type
516
+ * type AppContext = { status: 'idle' | 'loading' | 'error'; data?: any; error?: string };
517
+ *
518
+ * // Extend MultiMachineBase and define transitions as methods
519
+ * class AppMachine extends MultiMachineBase<AppContext> {
520
+ * async fetch(url: string) {
521
+ * // Notify subscribers we're loading
522
+ * this.setContext({ ...this.context, status: 'loading' });
523
+ *
524
+ * try {
525
+ * const data = await fetch(url).then(r => r.json());
526
+ * // Update state when done
527
+ * this.setContext({ ...this.context, status: 'idle', data });
528
+ * } catch (error) {
529
+ * // Handle errors
530
+ * this.setContext({
531
+ * ...this.context,
532
+ * status: 'error',
533
+ * error: error.message
534
+ * });
535
+ * }
536
+ * }
537
+ *
538
+ * reset() {
539
+ * this.setContext({ status: 'idle' });
540
+ * }
541
+ * }
542
+ */
543
+ export declare abstract class MultiMachineBase<C extends object> {
544
+ /**
545
+ * The external state store that manages the machine's context.
546
+ * @protected
547
+ */
548
+ protected store: StateStore<C>;
549
+ /**
550
+ * @param store - The StateStore that will manage this machine's context.
551
+ */
552
+ constructor(store: StateStore<C>);
553
+ /**
554
+ * Read-only access to the current context from the external store.
555
+ * This getter always returns the latest context from the store.
556
+ *
557
+ * @protected
558
+ *
559
+ * @example
560
+ * const currentStatus = this.context.status;
561
+ * const currentData = this.context.data;
562
+ */
563
+ protected get context(): C;
564
+ /**
565
+ * Update the shared context in the external store.
566
+ * Call this method in your transition methods to update the state.
567
+ *
568
+ * @protected
569
+ * @param newContext - The new context object. Should typically be a shallow
570
+ * copy with only the properties you're changing, merged with the current
571
+ * context using spread operators.
572
+ *
573
+ * @example
574
+ * // In a transition method:
575
+ * this.setContext({ ...this.context, status: 'loading' });
576
+ *
577
+ * @example
578
+ * // Updating nested properties:
579
+ * this.setContext({
580
+ * ...this.context,
581
+ * user: { ...this.context.user, name: 'Alice' }
582
+ * });
583
+ */
584
+ protected setContext(newContext: C): void;
585
+ }
586
+ /**
587
+ * Creates a live, type-safe instance of a class-based state machine (MultiMachine).
588
+ *
589
+ * This is the class-based alternative to the functional `createEnsemble` pattern,
590
+ * designed for developers who prefer an OOP-style architecture. This function takes
591
+ * your MultiMachine class blueprint and an external state store, and wires them
592
+ * together. The returned object is a Proxy that dynamically exposes both context
593
+ * properties and the available transition methods from your class.
594
+ *
595
+ * **Key features:**
596
+ * - Directly access context properties as if they were on the machine object
597
+ * - Call transition methods to update state through the store
598
+ * - Type-safe integration with TypeScript
599
+ * - Seamless Proxy-based API (no special method names or API quirks)
600
+ *
601
+ * **How it works:**
602
+ * The returned Proxy intercepts property access. For context properties, it returns
603
+ * values from the store. For methods, it calls them on the MultiMachine instance.
604
+ * This creates the illusion of a single object that is both data and behavior.
605
+ *
606
+ * @template C - The shared context type.
607
+ * @template T - The MultiMachine class type.
608
+ *
609
+ * @param MachineClass - The class you defined that extends `MultiMachineBase<C>`.
610
+ * @param store - The `StateStore` that will manage the machine's context.
611
+ * @returns A Proxy that merges context properties with class methods, allowing
612
+ * direct access to both via a unified object interface.
613
+ *
614
+ * @example
615
+ * // Define your context type
616
+ * type CounterContext = { count: number };
617
+ *
618
+ * // Define your machine class
619
+ * class CounterMachine extends MultiMachineBase<CounterContext> {
620
+ * increment() {
621
+ * this.setContext({ count: this.context.count + 1 });
622
+ * }
623
+ *
624
+ * add(n: number) {
625
+ * this.setContext({ count: this.context.count + n });
626
+ * }
627
+ *
628
+ * reset() {
629
+ * this.setContext({ count: 0 });
630
+ * }
631
+ * }
632
+ *
633
+ * // Create a store
634
+ * let sharedContext = { count: 0 };
635
+ * const store = {
636
+ * getContext: () => sharedContext,
637
+ * setContext: (ctx) => { sharedContext = ctx; }
638
+ * };
639
+ *
640
+ * // Create the machine instance
641
+ * const machine = createMultiMachine(CounterMachine, store);
642
+ *
643
+ * // Use it naturally - properties and methods seamlessly integrated
644
+ * console.log(machine.count); // 0
645
+ * machine.increment();
646
+ * console.log(machine.count); // 1
647
+ * machine.add(5);
648
+ * console.log(machine.count); // 6
649
+ * machine.reset();
650
+ * console.log(machine.count); // 0
651
+ *
652
+ * @example
653
+ * // Status-based state machine with type discrimination
654
+ * type AppContext = {
655
+ * status: 'idle' | 'loading' | 'success' | 'error';
656
+ * data?: any;
657
+ * error?: string;
658
+ * };
659
+ *
660
+ * class AppMachine extends MultiMachineBase<AppContext> {
661
+ * async fetch() {
662
+ * this.setContext({ ...this.context, status: 'loading' });
663
+ * try {
664
+ * const data = await fetch('/api/data').then(r => r.json());
665
+ * this.setContext({ status: 'success', data });
666
+ * } catch (error) {
667
+ * this.setContext({
668
+ * status: 'error',
669
+ * error: error instanceof Error ? error.message : 'Unknown error'
670
+ * });
671
+ * }
672
+ * }
673
+ *
674
+ * reset() {
675
+ * this.setContext({ status: 'idle' });
676
+ * }
677
+ * }
678
+ *
679
+ * // Set up
680
+ * let context: AppContext = { status: 'idle' };
681
+ * const store = {
682
+ * getContext: () => context,
683
+ * setContext: (ctx) => { context = ctx; }
684
+ * };
685
+ *
686
+ * const app = createMultiMachine(AppMachine, store);
687
+ *
688
+ * // Use naturally with type discrimination
689
+ * console.log(app.status); // 'idle'
690
+ *
691
+ * if (app.status === 'idle') {
692
+ * app.fetch(); // Transition to loading
693
+ * }
694
+ *
695
+ * // Later: app.status === 'success'
696
+ * // console.log(app.data); // Access the data
697
+ */
698
+ export declare function createMultiMachine<C extends object, T extends MultiMachineBase<C>>(MachineClass: new (store: StateStore<C>) => T, store: StateStore<C>): C & T;
699
+ /**
700
+ * A mapped type that defines the shape of a Mutable Machine: an intersection
701
+ * of the context `C` and all possible transitions.
702
+ */
703
+ type MutableMachine<C extends object, AllMachines extends Machine<any>> = C & AllTransitions<AllMachines>;
704
+ /**
705
+ * Creates a Mutable Machine that uses a shared, mutable context. This primitive
706
+ * provides a stable object reference whose properties are mutated in place,
707
+ * offering a direct, imperative API.
708
+ *
709
+ * ---
710
+ *
711
+ * ### Key Characteristics & Trade-offs
712
+ *
713
+ * - **Stable Object Reference**: The machine is a single object. You can pass this
714
+ * reference around, and it will always reflect the current state.
715
+ * - **Direct Imperative API**: Transitions are called like methods directly on the
716
+ * object (`machine.login('user')`), and the object's properties update immediately.
717
+ * - **No State History**: Since the context is mutated, the history of previous
718
+ * states is not preserved, which makes patterns like time-travel debugging impossible.
719
+ * - **Not for Reactive UIs**: Most UI frameworks (React, Solid, Vue) rely on
720
+ * immutable state changes to trigger updates. Mutating the context directly
721
+ * will not cause components to re-render. Use the `Ensemble` primitive for UI integration.
722
+ *
723
+ * ---
724
+ *
725
+ * ### Best Suited For
726
+ *
727
+ * - **Backend Services & Game Logic**: Ideal for managing state in server-side
728
+ * processes, game loops, or other non-UI environments where performance and a
729
+ * stable state object are priorities.
730
+ * - **Complex Synchronous Scripts**: Useful for orchestrating data processing
731
+ * pipelines, command-line tools, or any script where state needs to be managed
732
+ * imperatively without passing it through a function chain.
733
+ *
734
+ * @template C - The shared context type.
735
+ * @template F - An object of functions that create machine instances for each state.
736
+ * **Crucially, transitions inside these machines must be pure functions that
737
+ * return the *next context object*, not a new machine instance.**
738
+ * @param sharedContext - The initial context object. This object will be mutated.
739
+ * @param factories - An object mapping state names to functions that create machine instances.
740
+ * @param getDiscriminant - An accessor function that takes the context and returns the key
741
+ * of the current state in the `factories` object. Provides refactoring safety.
742
+ * @returns A Proxy that acts as a stable, mutable machine instance.
743
+ *
744
+ * @example
745
+ * // ===== 1. Basic Authentication Example =====
746
+ *
747
+ * type AuthContext =
748
+ * | { status: 'loggedOut'; error?: string }
749
+ * | { status: 'loggedIn'; username: string };
750
+ *
751
+ * const authFactories = {
752
+ * loggedOut: (ctx: AuthContext) => ({
753
+ * context: ctx,
754
+ * // This transition is a PURE function that returns the NEXT CONTEXT
755
+ * login: (username: string) => ({ status: 'loggedIn', username }),
756
+ * }),
757
+ * loggedIn: (ctx: AuthContext) => ({
758
+ * context: ctx,
759
+ * logout: () => ({ status: 'loggedOut' }),
760
+ * }),
761
+ * };
762
+ *
763
+ * const authUser = createMutableMachine(
764
+ * { status: 'loggedOut' } as AuthContext,
765
+ * authFactories,
766
+ * 'status'
767
+ * );
768
+ *
769
+ * const userReference = authUser; // Store a reference to the object
770
+ *
771
+ * console.log(authUser.status); // 'loggedOut'
772
+ *
773
+ * authUser.login('alice'); // Mutates the object in place
774
+ *
775
+ * console.log(authUser.status); // 'loggedIn'
776
+ * console.log(authUser.username); // 'alice'
777
+ *
778
+ * // The original reference points to the same, mutated object
779
+ * console.log(userReference.status); // 'loggedIn'
780
+ * console.log(userReference === authUser); // true
781
+ *
782
+ * // --- Type-safe transitions ---
783
+ * // `authUser.login('bob')` would now throw a runtime error because `login`
784
+ * // is not a valid action in the 'loggedIn' state.
785
+ *
786
+ * if (authUser.status === 'loggedIn') {
787
+ * // TypeScript correctly narrows the type here, allowing a safe call.
788
+ * authUser.logout();
789
+ * }
790
+ * console.log(authUser.status); // 'loggedOut'
791
+ *
792
+ * @example
793
+ * // ===== 2. Game State Loop Example =====
794
+ *
795
+ * type PlayerContext = {
796
+ * state: 'idle' | 'walking' | 'attacking';
797
+ * hp: number;
798
+ * position: { x: number; y: number };
799
+ * };
800
+ *
801
+ * const playerFactories = {
802
+ * idle: (ctx: PlayerContext) => ({
803
+ * context: ctx,
804
+ * walk: (dx: number, dy: number) => ({ ...ctx, state: 'walking', position: { x: ctx.position.x + dx, y: ctx.position.y + dy } }),
805
+ * attack: () => ({ ...ctx, state: 'attacking' }),
806
+ * }),
807
+ * walking: (ctx: PlayerContext) => ({
808
+ * context: ctx,
809
+ * stop: () => ({ ...ctx, state: 'idle' }),
810
+ * }),
811
+ * attacking: (ctx: PlayerContext) => ({
812
+ * context: ctx,
813
+ * finishAttack: () => ({ ...ctx, state: 'idle' }),
814
+ * }),
815
+ * };
816
+ *
817
+ * const player = createMutableMachine(
818
+ * { state: 'idle', hp: 100, position: { x: 0, y: 0 } },
819
+ * playerFactories,
820
+ * (ctx) => ctx.state
821
+ * );
822
+ *
823
+ * // Simulate a game loop
824
+ * function processInput(input: 'move_right' | 'attack') {
825
+ * if (player.state === 'idle') {
826
+ * if (input === 'move_right') player.walk(1, 0);
827
+ * if (input === 'attack') player.attack();
828
+ * }
829
+ * console.log(`State: ${player.state}, Position: (${player.position.x}, ${player.position.y})`);
830
+ * }
831
+ *
832
+ * processInput('move_right'); // State: walking, Position: (1, 0)
833
+ * player.stop();
834
+ * processInput('attack'); // State: attacking, Position: (1, 0)
835
+ */
836
+ export declare function createMutableMachine<C extends object, F extends Record<string, (context: C) => Machine<C>>>(sharedContext: C, factories: F, getDiscriminant: (context: C) => keyof F): MutableMachine<C, ReturnType<F[keyof F]>>;
837
+ export {};
838
+ //# sourceMappingURL=multi.d.ts.map