/** * @file Type-level primitives for formal state machine verification. * @description * This file provides a Domain Specific Language (DSL) of transition decorators. * These functions serve two purposes: * 1. At runtime, they preserve the transition function and attach non-enumerable metadata. * 2. At design/build time, they brand transition functions with rich type metadata. * * This allows a static analysis tool (like `ts-morph`) to read your source code * and generate a formal Statechart (JSON) that perfectly matches your implementation, * including resolving Class Constructors to their names. */ // ============================================================================= // SECTION: CORE METADATA TYPES // ============================================================================= /** * Options passed to async transition functions, including cancellation support. */ export interface TransitionOptions { /** AbortSignal for cancelling long-running async operations. */ signal: AbortSignal; } /** * A unique symbol used to "brand" a type with metadata. * This key allows the static analyzer to find the metadata within a complex type signature. */ export const META_KEY = Symbol("MachineMeta"); /** * Non-enumerable property key for storing metadata on function objects at runtime. * @internal */ export const RUNTIME_META = Symbol('__machine_runtime_meta__'); /** * Local definition of Machine type to avoid circular imports. * @internal */ type Machine = { readonly context: C; }; /** * Helper type representing a Class Constructor. * Used to reference target states by their class definition rather than magic strings. */ export type ClassConstructor = new (...args: any[]) => any; /** * Metadata describing a Guard condition. */ export interface GuardMeta { /** The name of the guard (e.g., "isAdmin"). */ name: string; /** Optional documentation explaining the logic. */ description?: string; } /** * Metadata describing an Invoked Service (async operation). */ export interface InvokeMeta { /** The name of the service source (e.g., "fetchUserData"). */ src: string; /** The state class to transition to on success. */ onDone: ClassConstructor; /** The state class to transition to on error. */ onError: ClassConstructor; /** Optional description. */ description?: string; } /** * Metadata describing a generic Action (side effect). */ export interface ActionMeta { /** The name of the action (e.g., "logAnalytics"). */ name: string; /** Optional description. */ description?: string; } /** * The comprehensive shape of metadata that can be encoded into a transition's type. */ export interface TransitionMeta { /** The target state class this transition leads to. */ target?: ClassConstructor; /** A human-readable description of the transition. */ description?: string; /** An array of guards that must be true for this transition to be enabled. */ guards?: GuardMeta[]; /** A service to invoke upon taking this transition (or entering the state). */ invoke?: InvokeMeta; /** Fire-and-forget side effects associated with this transition. */ actions?: ActionMeta[]; } /** * The Branded Type. * It takes a function type `F` and intersects it with a hidden metadata object `M`. * This is the mechanism that carries information from your code to the compiler API. * * @typeParam F - Original transition function type. * @typeParam M - Metadata merged onto that function type. */ export type WithMeta< F extends (...args: any[]) => any, M extends TransitionMeta > = Annotated; /** * Value carrying type-level metadata without changing its callable runtime shape. * * @typeParam T - Annotated object or function. * @typeParam M - Metadata represented by the annotation. */ export type Annotated = T & { [META_KEY]: M }; type AnyFunction = (...args: any[]) => any; /** * Extracts metadata already carried by an annotated transition. * * @typeParam F - Possibly annotated function type. */ export type MetadataOf = F extends { [META_KEY]: infer M extends TransitionMeta } ? M : {}; /** * Unary type transformation suitable for use with {@link pipe}. * @typeParam Input - Accepted value type. * @typeParam Output - Resulting value type. */ export type Operator = (value: Input) => Output; /** * Reusable decorator that adds metadata without changing a transition's call signature. * @typeParam M - Metadata fragment added by the operator. */ export type MetadataOperator = ( transition: F ) => WithMeta & M>; /** * Applies operators from left to right while preserving each intermediate type. * * This is deliberately a standalone function: machine snapshots stay plain values, * and the same composition helper works for transition functions or other values. * * @param value - Initial value passed to the first operator. * @param operators - Unary transformations applied in declaration order. * @returns The final operator result, or `value` when no operators are supplied. * @example * ```ts * const login = pipe( * (user: User) => new LoggedIn({ user }), * transitionTo(LoggedIn), * describe('Authenticate the current user'), * action({ name: 'auditLogin' }), * ); * ``` */ /** * Returns `value` unchanged. * @typeParam A - Input and result type. */ export function pipe(value: A): A; /** * Applies one typed operator. * @typeParam A - Input type. * @typeParam B - Result type. */ export function pipe(value: A, ab: Operator): B; /** * Applies two typed operators. * @typeParam A - Input type. * @typeParam B - Intermediate type. * @typeParam C - Result type. */ export function pipe(value: A, ab: Operator, bc: Operator): C; /** * Applies three typed operators. * @typeParam A - Input type. * @typeParam B - First intermediate type. * @typeParam C - Second intermediate type. * @typeParam D - Result type. */ export function pipe(value: A, ab: Operator, bc: Operator, cd: Operator): D; /** * Applies four typed operators. * @typeParam A - Input type. * @typeParam B - First intermediate type. * @typeParam C - Second intermediate type. * @typeParam D - Third intermediate type. * @typeParam E - Result type. */ export function pipe(value: A, ab: Operator, bc: Operator, cd: Operator, de: Operator): E; /** * Applies five typed operators. * @typeParam A - Input type. * @typeParam B - First intermediate type. * @typeParam C - Second intermediate type. * @typeParam D - Third intermediate type. * @typeParam E - Fourth intermediate type. * @typeParam F - Result type. */ export function pipe(value: A, ab: Operator, bc: Operator, cd: Operator, de: Operator, ef: Operator): F; /** * Applies six typed operators. * @typeParam A - Input type. * @typeParam B - First intermediate type. * @typeParam C - Second intermediate type. * @typeParam D - Third intermediate type. * @typeParam E - Fourth intermediate type. * @typeParam F - Fifth intermediate type. * @typeParam G - Result type. */ export function pipe(value: A, ab: Operator, bc: Operator, cd: Operator, de: Operator, ef: Operator, fg: Operator): G; /** @internal Variadic runtime implementation for the typed overloads. */ export function pipe(value: unknown, ...operators: Array>): unknown { return operators.reduce((current, operator) => operator(current), value); } // ============================================================================= // SECTION: RUNTIME METADATA ATTACHMENT // ============================================================================= /** * Runtime metadata interface (resolved class names as strings) */ export interface RuntimeTransitionMeta { target?: string; description?: string; guards?: Array<{ name: string; description?: string }>; invoke?: { src: string; onDone: string; onError: string; description?: string; }; actions?: Array<{ name: string; description?: string }>; } /** * Attaches runtime metadata to an object or function. * Merges with existing metadata if present. * * @param value - The value to attach metadata to * @param metadata - Partial metadata to merge * @internal */ function attachRuntimeMeta(value: object, metadata: Partial): void { // Read existing metadata (may be undefined) const existing = (value as { [RUNTIME_META]?: RuntimeTransitionMeta })[RUNTIME_META] || {}; // Shallow merge for simple properties const merged: any = { ...existing, ...metadata }; // Deep merge for array properties // Prepend new items to preserve order (outer wraps first in call stack) if (metadata.guards && existing.guards) { merged.guards = [...metadata.guards, ...existing.guards]; } else if (metadata.guards) { merged.guards = [...metadata.guards]; } if (metadata.actions && existing.actions) { merged.actions = [...metadata.actions, ...existing.actions]; } else if (metadata.actions) { merged.actions = [...metadata.actions]; } // Replace invoke entirely (not an array, can't merge) // Last invoke wins (this matches XState semantics) // Define or redefine the metadata property Object.defineProperty(value, RUNTIME_META, { value: merged, enumerable: false, writable: false, configurable: true // CRITICAL: Must be configurable for re-definition }); } function guardedRuntimeMeta( transition: object, guard: { name: string; description: string }, description: string | undefined, fallbackDescription: string, ): Partial { const inherited = (transition as { [RUNTIME_META]?: RuntimeTransitionMeta })[RUNTIME_META]; return { ...inherited, description: description || inherited?.description || fallbackDescription, guards: [guard, ...(inherited?.guards ?? [])], }; } // ============================================================================= // SECTION: ANNOTATION PRIMITIVES (THE DSL) // ============================================================================= /** * Defines a transition to a target state class. * * @param target - The Class Constructor of the state being transitioned to. * @param implementation - The implementation function returning the new state instance. * @returns The implementation function, branded with target metadata. * @typeParam T - Target state constructor. * @typeParam F - Transition implementation type, including parameters and return. * * @example * login = transitionTo(LoggedInMachine, (user) => new LoggedInMachine({ user })); */ export function transitionTo(target: T): MetadataOperator<{ target: T }>; /** * Direct form of {@link transitionTo}; annotates `implementation` immediately. * @typeParam T - Target constructor. * @typeParam F - Implementation type. */ export function transitionTo( target: T, implementation: F ): WithMeta & { target: T }>; /** @internal Runtime implementation shared by direct and curried forms. */ export function transitionTo( _target: T, implementation?: F ): WithMeta & { target: T }> | MetadataOperator<{ target: T }> { if (implementation === undefined) { return ((transition: AnyFunction) => transitionTo(_target, transition)) as MetadataOperator<{ target: T }>; } // Attach runtime metadata with class name attachRuntimeMeta(implementation, { target: _target.name || _target.toString() }); return implementation as any; } /** * Annotates a transition with a description for documentation generation. * * @param text - The description text. * @param transition - The transition function (or wrapper) to annotate. * @returns A reusable operator when `transition` is omitted, otherwise the same callable transition. * @typeParam F - Annotated transition function type. * @example * logout = describe("Logs the user out", transitionTo(LoggedOut, ...)); */ export function describe(text: string): MetadataOperator<{ description: string }>; /** * Direct form of {@link describe}; annotates `transition` immediately. * @typeParam F - Annotated transition type. */ export function describe( text: string, transition: F ): WithMeta & { description: string }>; /** @internal Runtime implementation shared by direct and curried forms. */ export function describe( _text: string, transition?: F ): WithMeta & { description: string }> | MetadataOperator<{ description: string }> { if (transition === undefined) { return ((value: AnyFunction) => describe(_text, value)) as MetadataOperator<{ description: string }>; } // Attach runtime metadata attachRuntimeMeta(transition, { description: _text }); return transition as any; } /** * Annotates a transition with a Guard condition. * Note: This only adds metadata. You must still implement the `if` check inside your function. * * @deprecated Use the runtime `guard()` primitive instead. Its `options.description` is used for static analysis. * @param guard - Object containing the name and optional description of the guard. * @param transition - The transition function to guard. * @returns A reusable metadata operator or the annotated transition. * @typeParam G - Guard metadata literal. * @typeParam F - Annotated transition function type. * @example * delete = guarded({ name: "isAdmin" }, transitionTo(Deleted, ...)); */ export function guarded(guard: G): MetadataOperator<{ guards: [G] }>; /** * Direct form of {@link guarded}; annotates `transition` immediately. * @typeParam G - Guard metadata. * @typeParam F - Annotated transition type. */ export function guarded( guard: G, transition: F ): WithMeta & { guards: [G] }>; /** @internal Runtime implementation shared by direct and curried forms. */ export function guarded( guard: G, transition?: F ): WithMeta & { guards: [G] }> | MetadataOperator<{ guards: [G] }> { if (transition === undefined) { return ((value: AnyFunction) => guarded(guard, value)) as MetadataOperator<{ guards: [G] }>; } // Attach runtime metadata // Note: guards is an array, will be merged by attachRuntimeMeta attachRuntimeMeta(transition, { guards: [guard] }); return transition as any; } /** * Annotates a transition with an Invoked Service (asynchronous effect). * * This decorator records extraction metadata; it does not schedule or execute * the function. Execution remains the responsibility of the caller or runner. * * @param service - configuration for the service (source, onDone target, onError target). * @param implementation - The async function implementation that receives an AbortSignal. * @returns A reusable operator or the same callable implementation with invoke metadata. * @typeParam D - Success target constructor. * @typeParam E - Error target constructor. * @typeParam F - Abort-aware implementation function. * @example * load = invoke( * { src: "fetchData", onDone: LoadedMachine, onError: ErrorMachine }, * async ({ signal }) => { * const response = await fetch('/api/data', { signal }); * return new LoadedMachine({ data: await response.json() }); * } * ); */ type InvokeService = { src: string; onDone: D; onError: E; description?: string; }; type InvokeOperator = any>( implementation: F ) => WithMeta & { invoke: InvokeService }>; /** * Curried form of `invoke`; returns an operator for an abort-aware implementation. * @typeParam D - Success target constructor. * @typeParam E - Error target constructor. */ export function invoke( service: InvokeService ): InvokeOperator; /** * Direct form of {@link invoke}; annotates `implementation` immediately. * @typeParam D - Success target constructor. * @typeParam E - Error target constructor. * @typeParam F - Abort-aware implementation type. */ export function invoke any>( service: InvokeService, implementation: F ): WithMeta & { invoke: InvokeService }>; /** @internal Runtime implementation shared by direct and curried forms. */ export function invoke any>( service: InvokeService, implementation?: F ): WithMeta & { invoke: InvokeService }> | InvokeOperator { if (implementation === undefined) { return ((value: F) => invoke(service, value)) as InvokeOperator; } // Attach runtime metadata with class names resolved attachRuntimeMeta(implementation, { invoke: { src: service.src, onDone: service.onDone.name || service.onDone.toString(), onError: service.onError.name || service.onError.toString(), description: service.description } }); return implementation as any; } /** * Annotates a transition with a side-effect Action. * Useful for logging, analytics, or external event firing that doesn't change state structure. * This decorator records metadata; it does not execute the described action. * * @param action - Object containing the name and optional description. * @param transition - The transition function to annotate. * @returns A reusable operator or the same callable transition with action metadata. * @typeParam A - Action metadata literal. * @typeParam F - Annotated transition function type. * @example * click = action({ name: "trackClick" }, (ctx) => ...); */ export function action(action: A): MetadataOperator<{ actions: [A] }>; /** * Direct form of {@link action}; annotates `transition` immediately. * @typeParam A - Action metadata. * @typeParam F - Annotated transition type. */ export function action( actionMeta: A, transition: F ): WithMeta & { actions: [A] }>; /** @internal Runtime implementation shared by direct and curried forms. */ export function action( actionMeta: A, transition?: F ): WithMeta & { actions: [A] }> | MetadataOperator<{ actions: [A] }> { if (transition === undefined) { return ((value: AnyFunction) => action(actionMeta, value)) as MetadataOperator<{ actions: [A] }>; } // Attach runtime metadata // Note: actions is an array, will be merged by attachRuntimeMeta attachRuntimeMeta(transition, { actions: [actionMeta] }); return transition as any; } // ============================================================================= // SECTION: RUNTIME GUARDS // ============================================================================= /** * Configuration options for guard behavior when conditions fail. * * @typeParam C - Context read by the condition and fallback. * @typeParam TFailure - Machine type returned by a configured fallback. */ export interface GuardOptions = Machine> { /** Failure policy. Defaults to throwing `errorMessage` or `Guard condition failed`. */ onFail?: 'throw' | 'ignore' | GuardFallback; /** Custom error message used only by `'throw'` mode. */ errorMessage?: string; /** Human-readable description attached to runtime extraction metadata. */ description?: string; } /** * Machine or machine-bound function returned when a guard fails. * * Function fallbacks receive the original transition arguments and the current * machine as `this`. They cannot be used with context-only invocation. * * @typeParam C - Current context type. * @typeParam TFailure - Machine returned by the fallback. */ export type GuardFallback = Machine> = | ((this: Machine, ...args: any[]) => TFailure) | TFailure; /** * A guarded transition that checks conditions at runtime before executing. * Can be called with either machine or context as 'this' binding. * * @typeParam C - Context inspected by the guard condition. * @typeParam TSuccess - Machine returned when the condition passes. * @typeParam TFailure - Machine returned by the configured failure policy. */ export type GuardedTransition< C extends object, TSuccess extends Machine, TFailure extends Machine = Machine > = { (...args: any[]): TSuccess | TFailure | Promise; readonly __guard: true; readonly condition: (ctx: C, ...args: any[]) => boolean | Promise; readonly transition: (...args: any[]) => TSuccess; }; /** * Creates a synchronous runtime guard that checks conditions before executing transitions. * This provides actual runtime protection with synchronous execution - use this for the majority of cases. * * **IMPORTANT - Context-Bound Limitation:** * Guards accept calls with either `this === machine` or `this === context`, but when called * with context-only binding, the guard normalizes to `{ context }` before passing to the transition. * This means: * - ✅ Transitions can access `this.context` * - ❌ Transitions CANNOT call `this.otherTransition()` (no transitions property) * - Recommended: Use guards only with machine-bound transitions for full composition support * * @template C - The context type * @template TSuccess - The transition return type when condition passes * @template TFailure - The fallback return type when condition fails (defaults to Machine) * @param condition - Synchronous function that returns true if transition should proceed * @param transition - The transition function to execute if condition passes * @param options - Configuration for guard failure behavior * @returns A synchronous guarded transition function * @throws {Error} When the condition fails in `'throw'` mode. * @throws {Error} When `'ignore'` or a function fallback is used with context-only binding. * * @example * ```typescript * const machine = createMachine({ balance: 100 }, { * withdraw: guard( * (ctx, amount) => ctx.balance >= amount, * function(this: Machine<{balance: number}>, amount: number) { * // ✅ Can access this.context * return createMachine({ balance: this.context.balance - amount }, this); * // ❌ Cannot call this.otherTransition() if guard was called with context-only binding * }, * { onFail: 'throw', errorMessage: 'Insufficient funds' } * ) * }); * * machine.withdraw(50); // ✅ Works synchronously * machine.withdraw(200); // ❌ Throws "Insufficient funds" * ``` */ export function guard< C extends object, TSuccess extends Machine, TFailure extends Machine = Machine >( condition: (ctx: C, ...args: any[]) => boolean, transition: (...args: any[]) => TSuccess, options: GuardOptions = {} ): (...args: any[]) => TSuccess | TFailure { const { onFail = 'throw', errorMessage, description } = options; // Merge defaults into options for the metadata const fullOptions = { ...options, onFail, errorMessage, description }; // Create the guarded transition function (synchronous) const guardedTransition = function(this: C | Machine, ...args: any[]): TSuccess | TFailure { // Detect if 'this' is a machine or just context const isMachine = typeof this === 'object' && 'context' in this; const ctx = isMachine ? (this as Machine).context : (this as C); // Evaluate the condition (synchronously) const conditionResult = condition(ctx, ...args); if (conditionResult) { // Condition passed, execute the transition // Transition functions expect 'this' to be the machine const machineForTransition = isMachine ? (this as Machine) : { context: this as C }; return transition.apply(machineForTransition, args); } else { // Condition failed, handle according to options if (onFail === 'throw') { const message = errorMessage || 'Guard condition failed'; throw new Error(message); } else if (onFail === 'ignore') { if (isMachine) { // Return the current machine unchanged return this as TSuccess | TFailure; } else { // Cannot ignore when called with context binding throw new Error('Cannot use "ignore" mode with context-only binding. Use full machine binding or provide fallback.'); } } else if (typeof onFail === 'function') { // Custom fallback function - call with machine as 'this' if (isMachine) { return onFail.apply(this as Machine, args) as TSuccess | TFailure; } else { throw new Error('Cannot use function fallback with context-only binding. Use full machine binding.'); } } else { // Static fallback machine return onFail as TSuccess | TFailure; } } }; // Attach metadata for type branding and statechart extraction Object.defineProperty(guardedTransition, '__guard', { value: true, enumerable: false }); Object.defineProperty(guardedTransition, 'condition', { value: condition, enumerable: false }); Object.defineProperty(guardedTransition, 'transition', { value: transition, enumerable: false }); Object.defineProperty(guardedTransition, 'options', { value: fullOptions, enumerable: false }); // Attach runtime metadata for statechart extraction attachRuntimeMeta(guardedTransition, guardedRuntimeMeta( transition, { name: 'runtime_guard', description: description || 'Synchronous condition check' }, description, 'Synchronous guarded transition', )); return guardedTransition; } /** * Creates a runtime guard that checks conditions before executing transitions. * This provides actual runtime protection, unlike the `guarded` primitive which only adds metadata. * Use this when your condition or transition logic is asynchronous. * * **IMPORTANT - Context-Bound Limitation:** * Guards accept calls with either `this === machine` or `this === context`, but when called * with context-only binding, the guard normalizes to `{ context }` before passing to the transition. * This means: * - ✅ Transitions can access `this.context` * - ❌ Transitions CANNOT call `this.otherTransition()` (no transitions property) * - Recommended: Use guards only with machine-bound transitions for full composition support * * @template C - The context type * @template TSuccess - The transition return type when condition passes * @template TFailure - The fallback return type when condition fails (defaults to Machine) * @param condition - Function that returns true if transition should proceed (can be async) * @param transition - The transition function to execute if condition passes * @param options - Configuration for guard failure behavior * @returns A guarded transition function that returns a Promise * @throws {Error} Through the returned promise when the condition fails in `'throw'` mode. * @throws {Error} Through the returned promise when a machine-only failure policy is used with context-only binding. * * @example * ```typescript * const machine = createMachine({ balance: 100 }, { * withdraw: guardAsync( * async (ctx, amount) => { * // Simulate API call to check balance * await new Promise(resolve => setTimeout(resolve, 100)); * return ctx.balance >= amount; * }, * async function(this: Machine<{balance: number}>, amount: number) { * // Simulate API call to process withdrawal * await new Promise(resolve => setTimeout(resolve, 100)); * // ✅ Can access this.context * return createMachine({ balance: this.context.balance - amount }, this); * // ❌ Cannot call this.otherTransition() if guard was called with context-only binding * }, * { onFail: 'throw', errorMessage: 'Insufficient funds' } * ) * }); * * await machine.withdraw(50); // ✅ Works * await machine.withdraw(200); // ❌ Throws "Insufficient funds" * ``` */ export function guardAsync< C extends object, TSuccess extends Machine, TFailure extends Machine = Machine >( condition: (ctx: C, ...args: any[]) => boolean | Promise, transition: (...args: any[]) => TSuccess, options: GuardOptions = {} ): GuardedTransition { const { onFail = 'throw', errorMessage, description } = options; // Merge defaults into options for the metadata const fullOptions = { ...options, onFail, errorMessage, description }; // Create the guarded transition function const guardedTransition = async function(this: C | Machine, ...args: any[]): Promise { // Detect if 'this' is a machine or just context const isMachine = typeof this === 'object' && 'context' in this; const ctx = isMachine ? (this as Machine).context : (this as C); // Evaluate the condition const conditionResult = await Promise.resolve(condition(ctx, ...args)); if (conditionResult) { // Condition passed, execute the transition // Transition functions expect 'this' to be the machine const machineForTransition = isMachine ? (this as Machine) : { context: this as C }; return transition.apply(machineForTransition, args); } else { // Condition failed, handle according to options if (onFail === 'throw') { const message = errorMessage || 'Guard condition failed'; throw new Error(message); } else if (onFail === 'ignore') { if (isMachine) { // Return the current machine unchanged return this as TSuccess | TFailure; } else { // Cannot ignore when called with context binding throw new Error('Cannot use "ignore" mode with context-only binding. Use full machine binding or provide fallback.'); } } else if (typeof onFail === 'function') { // Custom fallback function - call with machine as 'this' if (isMachine) { return onFail.apply(this as Machine, args) as TSuccess | TFailure; } else { throw new Error('Cannot use function fallback with context-only binding. Use full machine binding.'); } } else { // Static fallback machine return onFail as TSuccess | TFailure; } } }; // Attach metadata for type branding and statechart extraction Object.defineProperty(guardedTransition, '__guard', { value: true, enumerable: false }); Object.defineProperty(guardedTransition, 'condition', { value: condition, enumerable: false }); Object.defineProperty(guardedTransition, 'transition', { value: transition, enumerable: false }); Object.defineProperty(guardedTransition, 'options', { value: fullOptions, enumerable: false }); // Attach runtime metadata for statechart extraction attachRuntimeMeta(guardedTransition, guardedRuntimeMeta( transition, { name: 'runtime_guard', description: description || 'Runtime condition check' }, description, 'Runtime guarded transition', )); return guardedTransition as GuardedTransition; } /** * Creates a branded synchronous guard with condition and transition metadata. * * `guard()` is also synchronous; `guardSync()` additionally returns the * `GuardedTransition` inspection surface (`__guard`, `condition`, and * `transition`). Use `guardAsync()` for promise-returning conditions. * * @template C - The context type * @template TSuccess - The transition return type when the condition passes * @template TFailure - The fallback return type when the condition fails * @param condition - Function that returns true if transition should proceed (must be synchronous) * @param transition - The transition function to execute if condition passes (must be synchronous) * @param options - Configuration for guard failure behavior * @returns A synchronous guarded transition function * @throws {Error} When the condition fails in `'throw'` mode. * @throws {Error} When a machine-only failure policy is used with context-only binding. * * @example * ```typescript * const machine = createMachine({ balance: 100 }, { * withdraw: guardSync( * (ctx, amount) => ctx.balance >= amount, * function(amount: number) { * return createMachine({ balance: this.context.balance - amount }, this); * }, * { onFail: 'throw', errorMessage: 'Insufficient funds' } * ) * }); * * machine.withdraw(50); // ✅ Works synchronously * machine.withdraw(200); // ❌ Throws "Insufficient funds" * ``` */ export function guardSync< C extends object, TSuccess extends Machine, TFailure extends Machine = Machine >( condition: (ctx: C, ...args: any[]) => boolean, transition: (...args: any[]) => TSuccess, options: GuardOptions = {} ): GuardedTransition { const { onFail = 'throw', errorMessage, description } = options; // Merge defaults into options for the metadata const fullOptions = { ...options, onFail, errorMessage, description }; // Create the guarded transition function (synchronous) const guardedTransition = function(this: C | Machine, ...args: any[]): TSuccess | TFailure { // Detect if 'this' is a machine or just context const isMachine = typeof this === 'object' && 'context' in this; const ctx = isMachine ? (this as Machine).context : (this as C); // Evaluate the condition (synchronously) const conditionResult = condition(ctx, ...args); if (conditionResult) { // Condition passed, execute the transition // Transition functions expect 'this' to be the machine const machineForTransition = isMachine ? (this as Machine) : { context: this as C }; return transition.apply(machineForTransition, args); } else { // Condition failed, handle according to options if (onFail === 'throw') { const message = errorMessage || 'Guard condition failed'; throw new Error(message); } else if (onFail === 'ignore') { if (isMachine) { // Return the current machine unchanged return this as TSuccess | TFailure; } else { // Cannot ignore when called with context binding throw new Error('Cannot use "ignore" mode with context-only binding. Use full machine binding or provide fallback.'); } } else if (typeof onFail === 'function') { // Custom fallback function - call with machine as 'this' if (isMachine) { return onFail.apply(this as Machine, args) as TSuccess | TFailure; } else { throw new Error('Cannot use function fallback with context-only binding. Use full machine binding.'); } } else { // Static fallback machine return onFail as TSuccess | TFailure; } } }; // Attach metadata for type branding and statechart extraction Object.defineProperty(guardedTransition, '__guard', { value: true, enumerable: false }); Object.defineProperty(guardedTransition, 'condition', { value: condition, enumerable: false }); Object.defineProperty(guardedTransition, 'transition', { value: transition, enumerable: false }); Object.defineProperty(guardedTransition, 'options', { value: fullOptions, enumerable: false }); // Attach runtime metadata for statechart extraction attachRuntimeMeta(guardedTransition, guardedRuntimeMeta( transition, { name: 'runtime_guard_sync', description: description || 'Synchronous condition check' }, description, 'Synchronous guarded transition', )); return guardedTransition as GuardedTransition; } /** * Fluent API for creating synchronous guarded transitions. * Provides a more readable way to define conditional transitions with synchronous execution. * * @template C - The context type * @param condition - Synchronous function that returns true if transition should proceed * @returns A fluent interface for defining the guarded transition * * @example * ```typescript * const machine = createMachine({ isAdmin: false }, { * deleteUser: whenGuard((ctx) => ctx.isAdmin) * .do(function(userId: string) { * return createMachine({ ...this.context, deleted: userId }, this); * }) * .else(function() { * return createMachine({ ...this.context, error: 'Unauthorized' }, this); * }) * }); * ``` */ export function whenGuard( condition: (ctx: C, ...args: any[]) => boolean ) { return { /** * Define the transition to execute when the condition passes. * Returns a guarded transition that can optionally have an else clause. */ do>(transition: (...args: any[]) => T) { const guarded = guard(condition, transition); // Add fluent else method to the guarded transition (guarded as any).else = function>(fallback: (...args: any[]) => F) { return guard(condition, transition, { onFail: fallback }); }; return guarded; } }; } /** * Fluent API for creating asynchronous guarded transitions. * Provides a more readable way to define conditional transitions with async execution. * * @template C - The context type * @param condition - Function that returns true if transition should proceed (can be async) * @returns A fluent interface for defining the guarded transition * * @example * ```typescript * const machine = createMachine({ isAdmin: false }, { * deleteUser: whenGuardAsync(async (ctx) => { * // Simulate API call * await checkPermissions(ctx.userId); * return ctx.isAdmin; * }) * .do(async function(userId: string) { * await deleteUserFromDB(userId); * return createMachine({ ...this.context, deleted: userId }, this); * }) * .else(function() { * return createMachine({ ...this.context, error: 'Unauthorized' }, this); * }) * }); * ``` */ export function whenGuardAsync( condition: (ctx: C, ...args: any[]) => boolean | Promise ) { return { /** * Define the transition to execute when the condition passes. * Returns a guarded transition that can optionally have an else clause. */ do>(transition: (...args: any[]) => T) { const guarded = guardAsync(condition, transition); // Add fluent else method to the guarded transition (guarded as any).else = function>(fallback: (...args: any[]) => F) { return guardAsync(condition, transition, { onFail: fallback }); }; return guarded; } }; } /** * Flexible metadata wrapper for functional and type-state patterns. * * This function allows attaching metadata to values that don't use the class-based * MachineBase pattern. It's particularly useful for: * - Functional machines created with createMachine() * - Type-state discriminated unions * - Generic machine configurations * * @param meta - Partial metadata object describing states, transitions, etc. * @param value - The value to annotate (machine, config, factory function, etc.) * @returns The value unchanged, or a reusable unary annotation when value is omitted * @typeParam M - Metadata fragment attached to the value. * @typeParam T - Annotated object or function type. * @throws {TypeError} If the direct form receives `undefined` instead of an object or function. * * @example * // Annotate a functional machine * const machine = metadata( * { * target: IdleState, * description: "Counter machine with increment/decrement" * }, * createMachine({ count: 0 }, { ... }) * ); * * @example * // Annotate a factory function * export const createCounter = metadata( * { description: "Creates a counter starting at 0" }, * () => createMachine({ count: 0 }, { ... }) * ); */ export function metadata>(meta: M): (value: T) => Annotated; /** * Direct form of {@link metadata}; annotates `value` immediately. * @typeParam M - Metadata shape. * @typeParam T - Annotated value type. */ export function metadata, T extends object>(meta: M, value: T): Annotated; /** @internal Runtime implementation shared by direct and curried forms. */ export function metadata, T extends object>( meta: M, value?: T, ): Annotated | ((value: V) => Annotated) { if (arguments.length === 1) { return (annotated: V): Annotated => metadata(meta, annotated); } if (value === undefined) { throw new TypeError('metadata requires an object or function to annotate.'); } const runtimeMeta: Partial = {}; if (meta.target) runtimeMeta.target = meta.target.name; if (meta.description !== undefined) runtimeMeta.description = meta.description; if (meta.guards) runtimeMeta.guards = meta.guards; if (meta.actions) runtimeMeta.actions = meta.actions; if (meta.invoke) { runtimeMeta.invoke = { ...meta.invoke, onDone: meta.invoke.onDone.name, onError: meta.invoke.onError.name, }; } attachRuntimeMeta(value, runtimeMeta); return value as Annotated; }