/** * @file A tiny, immutable, and type-safe state machine library for TypeScript. * @author doeixd * @version 1.0.0 */ import { attachTransitions, getStoredTransitions, snapshotOwnTransitions } from './internal-transitions'; function assertContext(value: unknown): asserts value is object { if (value === null || typeof value !== 'object') { throw new TypeError('Machine context must be a non-null object.'); } } function assertTransitionMap(value: unknown): asserts value is Record any> { if (value === null || typeof value !== 'object') { throw new TypeError('Machine transitions must be an object of functions.'); } for (const [key, transition] of Object.entries(value)) { if (key === 'context') { throw new TypeError("'context' is reserved and cannot be used as a transition name."); } if (typeof transition !== 'function') { throw new TypeError(`Transition '${key}' must be a function.`); } } } // ============================================================================= // SECTION: CORE TYPES & INTERFACES // ============================================================================= /** * A utility type that represents either a value of type T or a Promise that resolves to T. * @template T - The value type. */ export type MaybePromise = T | Promise; /** * The fundamental shape of a synchronous machine. This is a highly advanced * generic type that performs two critical functions at compile time: * * 1. **Extraction:** It intelligently infers the pure transitions object from * the flexible argument `A` (which can be a plain object, a factory * function, or the augmented `this` from another transition). * * 2. **Filtering:** After extracting the transitions, it filters them, keeping * only the functions that return a valid `Machine`. * * This makes the `Machine` type itself the single source of truth for what * constitutes a valid, type-safe machine, enabling a remarkably clean and * powerful API for `createMachine`. * * @template C The context object type. * @template A The raw, flexible argument for transitions (object, factory, or `this`). */ export type Machine< C extends object, T extends object = {} > = { readonly context: C; } & T; /** * The shape of an asynchronous machine, where transitions can return Promises. * Async transitions receive an AbortSignal as the last parameter for cancellation support. * @template C - The context object type. * @template T - Transition methods exposed by the snapshot. */ export type AsyncMachine< C extends object, T extends object = {} > = { readonly context: C; } & T; /** * Utility type to extract the parameters of an async transition function, * which includes TransitionOptions as the last parameter. * * The runner supplies a trailing `TransitionOptions`, so it is removed from the * caller-facing tuple when present. * * @typeParam M - Async machine or typestate union to inspect. * @typeParam K - Transition name selected from the machine. */ export type AsyncTransitionArgs, K extends string> = M extends unknown ? K extends keyof M ? M[K] extends (...a: infer A) => any ? A extends [...infer Rest, TransitionOptions] ? Rest : A : never : never : never; /** * A helper type to define a distinct state in a state machine (a "typestate"). * Allows defining the context and transitions in a single generic type. * @template C - The context specific to this state. * @template T - The transitions available in this state. */ export type TypeState = Machine; /** * A helper type to define a distinct async state in a state machine. * @template C - The context specific to this state. * @template T - The transitions available in this state. */ export type AsyncTypeState = AsyncMachine; /** * Options passed to async transition functions, including cancellation support. */ export interface TransitionOptions { /** AbortSignal for cancelling long-running async operations. */ signal: AbortSignal; } // ============================================================================= // SECTION: TYPE UTILITIES & INTROSPECTION // ============================================================================= /** * Extracts the context type `C` from a machine type `M`. * @template M - The machine type. * @example type Ctx = Context> // { count: number } */ export type Context = M["context"]; /** * Extracts the transition function signatures from a machine, excluding the context property. * @template M - The machine type. */ export type Transitions> = M extends unknown ? Omit : never; /** * Extracts the argument types for a specific transition function in a Machine. * @template M - The machine type. * @template K - The transition function name. */ export type TransitionArgs, K extends string> = M extends unknown ? K extends keyof M ? M[K] extends (...args: infer A) => any ? A : never : never : never; /** * Extracts a transition's return type across every branch in a machine union. * * @typeParam M - Machine or typestate union to inspect. * @typeParam K - Transition name to select. */ export type TransitionReturn, K extends string> = M extends unknown ? K extends keyof M ? M[K] extends (...args: any[]) => infer R ? R : never : never : never; /** * Extracts the names of all transitions as a string union type. * @template M - The machine type. * @example * type Names = TransitionNames & { increment: () => any }> * // Names = "increment" */ export type TransitionNames> = M extends unknown ? keyof Omit & string : never; /** * Base machine type that both Machine and AsyncMachine extend from. * @template C - The context object type. */ export type BaseMachine = { /** The readonly state of the machine. */ readonly context: C; }; /** * Helper to make a type deeply readonly (freezes nested objects). * Useful for ensuring immutability of context at the type level. * @template T - The type to make readonly. */ export type DeepReadonly = { readonly [P in keyof T]: T[P] extends object ? T[P] extends (...args: any[]) => any ? T[P] : DeepReadonly : T[P]; }; /** * Infers the machine type from a machine factory function. * @template F - The factory function type. * @example * const factory = () => createMachine({ count: 0 }, { ... }); * type MyMachine = InferMachine; // Extracts the return type */ export type InferMachine any> = ReturnType; /** * Converts a transition record into a discriminated event union. * * Each transition key becomes `event.type`; its parameter tuple becomes * `event.args` without altering optional or rest parameters. * * @typeParam T - Transition-name to function mapping. * @example * ```ts * type CounterEvent = EventFromTransitions<{ * add(amount: number): Counter; * reset(): Counter; * }>; * // { type: 'add'; args: [number] } | { type: 'reset'; args: [] } * ``` */ export type EventFromTransitions any>> = { [K in keyof T & string]: { type: K; args: T[K] extends (...a: infer A) => any ? A : never } }[keyof T & string]; /** * A discriminated union type representing an event that can be dispatched to a machine. * This is automatically generated from a machine's type signature, ensuring full type safety. * @template M - The machine type. * @example * type CounterEvent = Event& { add: (n: number) => any }> * // CounterEvent = { type: "add"; args: [number] } */ export type Event> = { [K in TransitionNames]: { type: K; args: TransitionArgs } }[TransitionNames]; /** * Event union for `runMachine`. A trailing `TransitionOptions` parameter is * supplied by the runner and is therefore omitted from the caller's args. * * @typeParam M - Async machine or typestate union to convert to events. */ export type AsyncEvent> = { [K in TransitionNames]: { type: K; args: AsyncTransitionArgs } }[TransitionNames]; /** * A helper type for use with TypeScript's `satisfies` operator to provide * strong, immediate type-checking for standalone transition objects. * * This solves the "chicken-and-egg" problem where you need the final machine * type to correctly type the transitions object, but you need the transitions * object to create the machine. By forward-declaring the machine type and using * `satisfies TransitionsFor<...>`, you get full IntelliSense and error-checking * at the exact location of your transition definitions. * * @template C The context object type for the machine. * @template T The literal type of the transitions object itself (`typeof myTransitions`). * * @example * import { createMachine, Machine, TransitionsFor } from '@doeixd/machine'; * * // 1. Define the context for your machine. * type CounterContext = { count: number }; * * // 2. Forward-declare the final machine type. This is the key step that * // breaks the circular dependency for the type checker. * type CounterMachine = Machine & typeof counterTransitions; * * // 3. Define the transitions object, using `satisfies` to apply the helper type. * // This provides immediate type-checking and full autocompletion for `this`. * const counterTransitions = { * increment() { * // `this` is now fully typed! * // IntelliSense knows `this.context.count` is a number and * // `this.transitions.add` is a function. * return createMachine({ count: this.context.count + 1 }, this.transitions); * }, * add(n: number) { * return createMachine({ count: this.context.count + n }, this.transitions); * }, * // ❌ TypeScript will immediately throw a compile error on the next line * // because the return type 'string' does not satisfy 'Machine'. * invalidTransition() { * return "this is not a machine"; * } * } satisfies TransitionsFor; * * // 4. Create the machine instance. The `createMachine` call is now * // guaranteed to be type-safe because `counterTransitions` has already * // been validated. * export function createCounter(initialCount = 0): CounterMachine { * return createMachine({ count: initialCount }, counterTransitions); * } */ export type TransitionsFor> = { [K in keyof T]: (this: Machine, ...args: Parameters any ? (...a: A) => any : never>) => Machine; }; /** * A helper type for use with the `satisfies` operator to provide strong * type-checking for standalone asynchronous transition objects. * * @typeParam C - Context read by every transition. * @typeParam T - Transition record being validated. */ export type AsyncTransitionsFor> = { [K in keyof T]: (this: Machine, ...args: Parameters any ? (...a: A) => any : never>) => MaybePromise>; }; /** * A mapped type that iterates over a transitions object `T` and keeps only the * keys whose functions return a valid `Machine`. This provides a "self-correcting" * type that prevents the definition of invalid transitions at compile time. * * It acts as a filter at the type level. When used in the return type of a * function like `createMachine`, it ensures that the resulting machine object * will not have any properties corresponding to functions that were defined * with an incorrect return type. This provides immediate, precise feedback to * the developer, making it impossible to create a machine with an invalid * transition shape. * * @template T The raw transitions object type provided by the user. * * @example * import { createMachine, Machine } from '@doeixd/machine'; * * const machine = createMachine({ value: 'A' }, { * // This is a valid transition because it returns a `Machine`. * // The key 'goToB' will be PRESERVED in the final type. * goToB() { * return createMachine({ value: 'B' }, this.transitions); * }, * * // This is an INVALID transition because it returns a string. * // The key 'invalid' will be OMITTED from the final type. * invalid() { * return "This is not a Machine object"; * }, * * // This is also invalid as it's not a function. * // The key 'alsoInvalid' will be OMITTED from the final type. * alsoInvalid: 123 * }); * * // --- USAGE --- * * // ✅ This call is valid and works as expected. * const nextState = machine.goToB(); * * // ❌ This line will cause a COMPILE-TIME a ERROR because the `FilterValidTransitions` * // type has removed the 'invalid' key from the `machine`'s type signature. * // * // Error: Property 'invalid' does not exist on type * // 'Machine<{ value: string; }> & { goToB: () => Machine<...>; }'. * // * machine.invalid(); */ export type FilterValidTransitions = { [K in keyof T as T[K] extends (...args: any[]) => Machine ? K : never]: T[K]; }; /** * A conditional type that intelligently extracts the pure transitions object `T` * from the flexible second argument of `createMachine`. * * It handles three cases: * 1. If the argument is the augmented `this` context (`C & { transitions: T }`), it extracts `T`. * 2. If the argument is a factory function `((ctx: C) => T)`, it infers and returns `T`. * 3. If the argument is already the pure transitions object `T`, it returns it as is. * * @typeParam Arg - Factory, augmented context, or transition record to inspect. * @typeParam C - Machine context used when recognizing augmented forms. */ export type ExtractTransitions = Arg extends ( ...args: any[] ) => infer R ? R // Case 2: It's a factory function, extract the return type `R`. : Arg extends C & { transitions: infer T } ? T // Case 1: It's the augmented `this` context, extract `T` from `transitions`. : Arg; // Case 3: It's already the plain transitions object. /** * Keeps only functions that return a synchronous machine. * @typeParam T - Candidate transition record to filter. */ export type ValidTransitions = { [K in keyof T as T[K] extends (...a: any[]) => Machine ? K : never]: T[K] extends (...a: infer A) => Machine ? (...a: A) => Machine : never; }; /** * Keeps only functions returning an async machine or a promise of one. * @typeParam T - Candidate async transition record to filter. */ export type ValidAsyncTransitions = { [K in keyof T as T[K] extends (...a: any[]) => MaybePromise> ? K : never]: T[K] extends (...a: infer A) => MaybePromise> ? (...a: A) => MaybePromise> : never; }; // ============================================================================= // SECTION: MACHINE CREATION (FUNCTIONAL & OOP) // ============================================================================= /** * Creates a synchronous state machine from a context and transition functions. * This is the core factory for the functional approach. * * @template C - The context object type. * @param context - The initial state context. * @param fns - An object containing transition function definitions. * @returns A new machine instance. */ /** * Helper to transform transition functions to be bound (no 'this' requirement). */ export type BindTransitions = { [K in keyof T]: T[K] extends (this: any, ...args: infer A) => infer R ? (...args: A) => R : T[K]; }; /** * Creates a synchronous state machine from a context and a factory function. * This "Functional Builder" pattern allows for type-safe transitions without * manually passing `this` or `transitions`. * * @template C - The context object type. * @template T - The transitions object type. * @param context - The initial state context. * @param factory - A function that receives a `transition` helper and returns the transitions object. * @returns A new machine instance. */ export function createMachine, ...args: any[]) => any> = Record, ...args: any[]) => any>>( context: C, factory: (transition: (newContext: C) => Machine) => T ): Machine; /** * Creates a synchronous state machine from a context and transition functions. * This is the core factory for the functional approach. * Transitions receive the full machine as `this`, allowing them to access * `this.context` and call other transitions via `this.otherTransition()`. * * @template C - The context object type. * @param context - The initial state context. * @param fns - An object containing transition function definitions. * @returns A new machine instance. */ export function createMachine, ...args: any[]) => any> & { context?: any }>( context: C, fns: T ): Machine; /** * Creates a synchronous state machine by copying context and transitions from an existing machine. * This is useful for creating a new machine with updated context but the same transitions. * * @template C - The context object type. * @template M - The machine type to copy transitions from. * @param context - The new context. * @param machine - The machine to copy transitions from. * @returns A new machine instance with the given context and copied transitions. */ export function createMachine>( context: C, machine: M ): Machine>; /** @internal Runtime implementation shared by the public `createMachine` overloads. */ export function createMachine(context: any, fnsOrFactory: any): any { assertContext(context); if (typeof fnsOrFactory === 'function') { let self: any; let transitions: any; const transition = (newContext: any) => { return newContext === context ? self : createMachine(newContext, transitions); }; transitions = fnsOrFactory(transition); assertTransitionMap(transitions); self = attachTransitions(Object.assign({ context }, transitions), transitions); return self; } if (fnsOrFactory === null || typeof fnsOrFactory !== 'object') { throw new TypeError('Machine transitions must be an object or factory function.'); } // If fns is a machine (has context property), extract just the transition functions const stored = getStoredTransitions(fnsOrFactory); const transitions = stored ?? ('context' in fnsOrFactory ? snapshotOwnTransitions(fnsOrFactory) : fnsOrFactory); assertTransitionMap(transitions); const machine = Object.assign({ context }, transitions); return attachTransitions(machine, transitions); } /** * Creates an asynchronous state machine from a context and a factory function. * This "Functional Builder" pattern allows for type-safe transitions without * manually passing `this` or `transitions`. * * @template C - The context object type. * @template T - The transitions object type. * @param context - The initial state context. * @param factory - A function that receives a `transition` helper and returns the transitions object. * @returns A new async machine instance. */ export function createAsyncMachine, ...args: any[]) => any>>( context: C, factory: (transition: (newContext: C) => AsyncMachine) => T ): AsyncMachine; /** * Creates an asynchronous state machine by copying context and transitions from an existing machine. * This is useful for creating a new machine with updated context but the same transitions. * * @template C - The context object type. * @template M - The machine type to copy transitions from. * @param context - The new context. * @param machine - The machine to copy transitions from. * @returns A new async machine instance with the given context and copied transitions. */ export function createAsyncMachine>( context: C, machine: M ): AsyncMachine>; /** * Creates an asynchronous state machine from a context and async transition functions. * * @template C - The context object type. * @param context - The initial state context. * @param fns - An object containing async transition function definitions. * @returns A new async machine instance. */ export function createAsyncMachine, ...args: any[]) => any>>( context: C, fns: T ): AsyncMachine; /** @internal Runtime implementation shared by the public `createAsyncMachine` overloads. */ export function createAsyncMachine(context: any, fnsOrFactory: any): any { assertContext(context); if (typeof fnsOrFactory === 'function') { let transitions: any; const transition = (newContext: any) => { return createAsyncMachine(newContext, transitions); }; transitions = fnsOrFactory(transition); assertTransitionMap(transitions); return attachTransitions(Object.assign({ context }, transitions), transitions); } if (fnsOrFactory === null || typeof fnsOrFactory !== 'object') { throw new TypeError('Machine transitions must be an object or factory function.'); } // If fns is a machine (has context property), extract just the transition functions const stored = getStoredTransitions(fnsOrFactory); const transitions = stored ?? ('context' in fnsOrFactory ? snapshotOwnTransitions(fnsOrFactory) : fnsOrFactory); assertTransitionMap(transitions); const machine = Object.assign({ context }, transitions); return attachTransitions(machine, transitions); } /** * Creates a machine factory - a higher-order function that simplifies machine creation. * Instead of writing transition logic that creates new machines, you just write * pure context transformation functions. * * @template C - The context object type. * @returns A factory configurator function. * * @example * const counterFactory = createMachineFactory<{ count: number }>()({ * increment: (ctx) => ({ count: ctx.count + 1 }), * add: (ctx, n: number) => ({ count: ctx.count + n }) * }); * * const counter = counterFactory({ count: 0 }); * const next = counter.increment(); // Returns new machine with count: 1 */ export function createMachineFactory() { return C>>( transformers: T ) => { type MachineFns = { [K in keyof T]: ( this: Machine, ...args: T[K] extends (ctx: C, ...args: infer A) => C ? A : never ) => Machine; }; const fns = Object.fromEntries( Object.entries(transformers).map(([key, transform]) => [ key, function (this: Machine, ...args: any[]) { const newContext = (transform as any)(this.context, ...args); return createMachine(newContext, fns as any); }, ]) ) as MachineFns; return (initialContext: C): Machine & MachineFns => { return createMachine(initialContext, fns); }; }; } // ============================================================================= // SECTION: ADVANCED CREATION & IMMUTABLE HELPERS // ============================================================================= function cloneMachineWithContext>( machine: M, context: Context ): M { const clone = Object.create(Object.getPrototypeOf(machine)); let copiedContext = false; for (const key of Reflect.ownKeys(machine)) { const descriptor = Object.getOwnPropertyDescriptor(machine, key); if (!descriptor) continue; if (key === 'context') { Object.defineProperty(clone, key, { value: context, enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: 'writable' in descriptor ? descriptor.writable : false, }); copiedContext = true; } else { Object.defineProperty(clone, key, descriptor); } } if (!copiedContext) { Object.defineProperty(clone, 'context', { value: context, enumerable: true, configurable: true, writable: false, }); } return clone as M; } /** * Creates a new machine instance with an updated context, preserving all original transitions. * This is the primary, type-safe utility for applying state changes. * * @template M - The machine type. * @param machine - The original machine instance. * @param newContextOrFn - The new context object or an updater function. * @returns A new machine instance of the same type with the updated context. */ export function setContext>( machine: M, newContextOrFn: Context | ((ctx: Readonly>) => Context) ): M { const currentContext = machine.context; const newContext = typeof newContextOrFn === "function" ? (newContextOrFn as (ctx: Readonly>) => Context)(currentContext) : newContextOrFn; return cloneMachineWithContext(machine, newContext); } /** * Creates a minimal machine-like object with just a context property. * Useful for creating test fixtures and working with pattern matching utilities. * * @template C - The context type * @param context - The context object * @returns An object with a readonly context property * * @example * ```typescript * // For testing with discriminated unions * type FetchContext = * | { status: 'idle' } * | { status: 'success'; data: string }; * * const idleMachine = createContext({ status: 'idle' }); * const successMachine = createContext({ status: 'success', data: 'result' }); * * // Works with pattern matching * const match = createMatcher( * discriminantCase('idle', 'status', 'idle'), * discriminantCase('success', 'status', 'success') * ); * * if (match.is.success(successMachine)) { * console.log(successMachine.context.data); // TypeScript knows data exists * } * ``` */ export function createContext( context: C ): { readonly context: C } { return { context }; } /** * Creates a new machine by overriding or adding transition functions to an existing machine. * Ideal for mocking in tests or decorating functionality. The original machine is unchanged. * * @template M - The original machine type. * @template T - An object of new or overriding transition functions. * @param machine - The base machine instance. * @param overrides - An object containing the transitions to add or overwrite. * @returns A new machine instance with the merged transitions. */ export function overrideTransitions< M extends Machine, T extends Record any> >( machine: M, overrides: T ): Machine> & Omit, keyof T> & T { const context = machine.context; const originalTransitions = getStoredTransitions(machine) ?? snapshotOwnTransitions(machine); const newTransitions = { ...originalTransitions, ...overrides }; return createMachine(context, newTransitions as any) as any; } /** * Creates a new machine by adding new transition functions. * This utility will produce a compile-time error if you attempt to add a * transition that already exists, preventing accidental overrides. * * @template M - The original machine type. * @template T - An object of new transition functions, whose keys must not exist in M. * @param machine - The base machine instance. * @param newTransitions - An object containing the new transitions to add. * @returns A new machine instance with the combined original and new transitions. */ export function extendTransitions< M extends Machine, T extends Record any> & { [K in keyof T]: K extends keyof M ? never : T[K]; } >(machine: M, newTransitions: T): M & T { const context = machine.context; const originalTransitions = getStoredTransitions(machine) ?? snapshotOwnTransitions(machine); const combinedTransitions = { ...originalTransitions, ...newTransitions }; return createMachine(context, combinedTransitions as any) as M & T; } /** * Combines two machine factories into a single factory that creates machines with merged context and transitions. * This allows you to compose independent state machines that operate on different parts of the same context. * * The resulting factory takes the parameters of the first factory, while the second factory is called with no arguments. * Context properties are merged (second factory's context takes precedence on conflicts). * Transition names must not conflict between the two machines. * * @template F1 - The first factory function type. * @template F2 - The second factory function type. * @param factory1 - The first machine factory (provides parameters and primary context). * @param factory2 - The second machine factory (provides additional context and transitions). * @returns A new factory function that creates combined machines. * * @example * ```typescript * // Define two independent machines * const createCounter = (initial: number) => * createMachine({ count: initial }, { * increment: function() { return createMachine({ count: this.context.count + 1 }, this); }, * decrement: function() { return createMachine({ count: this.context.count - 1 }, this); } * }); * * const createLogger = () => * createMachine({ logs: [] as string[] }, { * log: function(message: string) { * return createMachine({ logs: [...this.context.logs, message] }, this); * }, * clear: function() { * return createMachine({ logs: [] }, this); * } * }); * * // Combine them * const createCounterWithLogging = combineFactories(createCounter, createLogger); * * // Use the combined factory * const machine = createCounterWithLogging(5); // { count: 5, logs: [] } * const incremented = machine.increment(); // { count: 6, logs: [] } * const logged = incremented.log("Count incremented"); // { count: 6, logs: ["Count incremented"] } * ``` */ export function combineFactories< F1 extends (...args: any[]) => Machine, F2 extends () => Machine >( factory1: F1, factory2: F2 ): ( ...args: Parameters ) => Machine> & Context>> & Omit, 'context'> & Omit, 'context'> { return (...args: Parameters) => { // Create instances from both factories const machine1 = factory1(...args); const machine2 = factory2(); // Merge contexts (machine2 takes precedence on conflicts) const combinedContext = { ...machine1.context, ...machine2.context }; // Extract transitions from both machines const transitions1 = getStoredTransitions(machine1) ?? snapshotOwnTransitions(machine1); const transitions2 = getStoredTransitions(machine2) ?? snapshotOwnTransitions(machine2); const transitionCollision = Object.keys(transitions1).find(key => key in transitions2); if (transitionCollision) { throw new Error(`Cannot combine factories: transition '${transitionCollision}' exists in both machines.`); } // Combine transitions (TypeScript will catch conflicts at compile time) const combinedTransitions = { ...transitions1, ...transitions2 }; // Create the combined machine return createMachine(combinedContext, combinedTransitions as any) as any; }; } /** * Creates a builder function from a "template" machine instance. * This captures the behavior of a machine and returns a factory that can stamp out * new instances with different initial contexts. Excellent for class-based machines. * * @template M - The machine type. * @param templateMachine - An instance of a machine to use as the template. * @returns A function that builds new machines of type M. */ export function createMachineBuilder>( templateMachine: M ): (context: Context) => M { return (newContext: Context): M => { return cloneMachineWithContext(templateMachine, newContext); }; } /** * Pattern match on a machine's state based on a discriminant property in the context. * This provides type-safe exhaustive matching for state machines. * * @template M - The machine type. * @template K - The discriminant key in the context. * @template R - The return type. * @param machine - The machine to match against. * @param discriminantKey - The key in the context to use for matching (e.g., "status"). * @param handlers - An object mapping each possible value to a handler function. * @returns The result of the matched handler. * * @example * const result = matchMachine( * machine, * 'status', * { * idle: (ctx) => "Machine is idle", * loading: (ctx) => "Loading...", * success: (ctx) => `Success: ${ctx.data}`, * error: (ctx) => `Error: ${ctx.error}` * } * ); */ export function matchMachine< M extends Machine, K extends keyof Context & string, R >( machine: M, discriminantKey: K, handlers: { [V in Context[K] & string]: (ctx: Context) => R; } ): R { const discriminant = machine.context[discriminantKey] as Context[K] & string; const handler = handlers[discriminant]; if (!handler) { throw new Error(`No handler found for state: ${String(discriminant)}`); } return handler(machine.context); } /** * Type-safe helper to assert that a machine's context has a specific discriminant value. * This narrows the type of the context based on the discriminant, properly handling * discriminated unions. * * @template M - The machine type. * @template K - The discriminant key. * @template V - The discriminant value. * @param machine - The machine to check. * @param key - The discriminant key to check. * @param value - The expected value. * @returns True if the discriminant matches, with type narrowing. * * @example * type Context = { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: string }; * const machine = createMachine({ status: 'success', data: 'test' }, {}); * * if (hasState(machine, 'status', 'success')) { * // machine.context is narrowed to { status: 'success'; data: string } * console.log(machine.context.data); // ✓ TypeScript knows about 'data' * } */ export function hasState< M extends Machine, K extends keyof Context, V extends Context[K] >( machine: M, key: K, value: V ): machine is M & { context: Extract, { [P in K]: V }> } { return machine.context[key] === value; } // ============================================================================= // SECTION: RUNTIME & EVENT DISPATCHER // ============================================================================= /** * Runs an asynchronous state machine with a managed lifecycle and event dispatch capability. * This is the "interpreter" for async machines, handling state updates and side effects. * Provides automatic AbortController management to prevent async race conditions. * * @template M - The initial machine type. * @param initial - The initial machine state. * @param onChange - Optional callback invoked with the new machine state after every transition. * @returns An object with a `state` getter for the current context, an async `dispatch` function, and a `stop` method. */ export function runMachine>( initial: M, onChange?: (m: M) => void ) { let current = initial; // Keep track of the controller for the currently-running async transition. let activeController: AbortController | null = null; async function dispatch>(event: E): Promise { // 1. If an async transition is already in progress, cancel it. if (activeController) { activeController.abort(); activeController = null; } const fn = (current as any)[event.type]; if (typeof fn !== 'function') { throw new Error(`[Machine] Unknown event type '${String(event.type)}' on current state.`); } // 2. Create a new AbortController for this new transition. const controller = new AbortController(); activeController = controller; try { // 3. Pass the signal to the transition function. const nextStatePromise = fn.apply(current, [...event.args, { signal: controller.signal }]); const nextState = await nextStatePromise; // 4. If this promise resolved but has since been aborted, do not update state. // This prevents the race condition. if (controller.signal.aborted) { // Return the *current* state, as if the transition never completed. return current; } if (nextState === null || typeof nextState !== 'object' || !('context' in nextState)) { throw new TypeError(`Transition '${String(event.type)}' did not return a machine with a context property.`); } current = nextState as M; onChange?.(current); return current; } finally { // 5. Clean up the controller once the transition is complete (resolved or rejected). // Only clear it if it's still the active one. if (activeController === controller) { activeController = null; } } } return { /** Gets the context of the current state of the machine. */ get state(): Context { return current.context; }, /** Dispatches a type-safe event to the machine, triggering a transition. */ dispatch, /** Stops any pending async operation and cleans up resources. */ stop: () => { if (activeController) { activeController.abort(); activeController = null; } }, }; } // Export MachineBase from separate file to avoid circular dependency issues export { MachineBase } from './base'; /** * Applies an update function to a machine's context, returning a new machine. * This is a simpler alternative to `setContext` when you always use an updater function. * * @template C - The context object type. * @param m - The machine to update. * @param update - A function that takes the current context and returns the new context. * @returns A new machine with the updated context. * * @example * const updated = next(counter, (ctx) => ({ count: ctx.count + 1 })); */ export function next>( m: M, update: (ctx: Readonly>) => Context ): M { return setContext(m, update); } /** * A type representing either a synchronous Machine or a Promise that resolves to a Machine. * Useful for functions that can return either sync or async machines. * * @template C - The context object type. * * @example * function getMachine(): MachineLike<{ count: number }> { * if (Math.random() > 0.5) { * return createMachine({ count: 0 }, { ... }); * } else { * return Promise.resolve(createMachine({ count: 0 }, { ... })); * } * } */ export type MachineLike = | Machine | Promise>; /** * A type representing the result of a machine transition. * Can be either: * - A new machine state * - A tuple of [machine, cleanup function] where cleanup is called when leaving the state * * This enables state machines with side effects that need cleanup (e.g., subscriptions, timers). * * @template C - The context object type. * * @example * function transition(): MachineResult<{ count: number }> { * const interval = setInterval(() => console.log("tick"), 1000); * const machine = createMachine({ count: 0 }, { ... }); * return [machine, () => clearInterval(interval)]; * } */ export type MachineResult = | Machine | [Machine, () => void | Promise]; // ============================================================================= // SECTION: GENERATOR-BASED COMPOSITION // ============================================================================= export { run, step, yieldMachine, runSequence, createFlow, runWithDebug, runAsync, stepAsync } from './generators'; // ============================================================================= // SECTION: TYPE-LEVEL METADATA PRIMITIVES // ============================================================================= export { pipe, transitionTo, describe, guarded, guard, guardSync, guardAsync, whenGuard, whenGuardAsync, invoke, action, metadata, META_KEY, type TransitionMeta, type GuardMeta, type InvokeMeta, type ActionMeta, type ClassConstructor, type Annotated, type WithMeta, type MetadataOf, type MetadataOperator, type Operator, type GuardOptions, type GuardFallback, type GuardedTransition } from './primitives'; // ============================================================================= // SECTION: STATECHART EXTRACTION (Build-time only) // ============================================================================= // Note: Extraction tools are available as dev dependencies for build-time use // They are not included in the runtime bundle for size optimization // Use: npx tsx scripts/extract-statechart.ts export type { MachineConfig, ExtractionConfig, ParallelRegionConfig, ChildStatesConfig } from './extract'; // Note: Extraction functions (extractMachine, extractMachines, generateChart) are NOT exported // to keep them out of the runtime bundle. Use the CLI tool or import directly from the source // file for build-time statechart generation. export * from './multi' export * from './higher-order' // ============================================================================= // SECTION: MIDDLEWARE & INTERCEPTION // ============================================================================= export * from './middleware/index'; export * from './mixins'; // ============================================================================= // SECTION: UTILITIES & HELPERS // ============================================================================= export { isState, createEvent, createTransition, mergeContext, pipeTransitions, logState, call, bindTransitions, BoundMachine } from './utils'; // ============================================================================= // SECTION: FUNCTIONAL COMBINATORS // ============================================================================= export { createTransitionFactory, createTransitionExtender, createFunctionalMachine, state } from './functional-combinators'; // ============================================================================= // SECTION: PATTERN MATCHING // ============================================================================= export { createMatcher, classCase, discriminantCase, customCase, forContext, type MatcherCase, type CasesToMapping, type MatcherUnion, type CaseNames, type CaseHandler, type ExhaustivenessMarker, type IsExhaustive, type WhenBuilder, type Matcher } from './matcher'; // ============================================================================= // SECTION: ACTOR MODEL // ============================================================================= export { Actor, createActor, spawn, fromPromise, fromObservable, type ActorRef, type InspectionEvent } from './actor'; // ============================================================================= // SECTION: CONTEXT-BOUND UTILITIES // ============================================================================= export { createContextBoundMachine, callWithContext, isContextBound, type ContextBoundMachine } from './context-bound'; // ============================================================================= // SECTION: MINIMAL & DELEGATED API // ============================================================================= export * as minimal from './minimal'; export * as delegate from './delegate'; // ============================================================================= // SECTION: TAGGED HELPERS & UTILITIES // ============================================================================= export * from './types';