import { isFailure, makeResult, Result } from './Result'; import { CB } from './utilityTypes'; const STATE_WILD_CARD = '*'; type TStateWildCard = typeof STATE_WILD_CARD; type TransitionTarget = S | [S, CB]; type StatesMap = Record< S | TStateWildCard, Partial>> >; export type TStateMachine< S extends string = string, A extends string = string, > = { initial: S; states: Partial>; }; export type TStateMachineApi< S extends string = string, A extends string = string, > = { getState: CB; initial: CB; staticTransition: (from: S, action: A, payload?: any) => S; transition: (action: A, payload?: any) => Result; }; type TransitionValue = V extends [infer T, any] ? T extends string ? T : never : V extends string ? V : never; type Values = T[keyof T]; type StateConfigs }> = Values< M['states'] >; type ActionFromConfig }> = StateConfigs extends infer SC ? SC extends any ? Extract : never : never; type TargetStatesFromConfig }> = StateConfigs extends infer SC ? SC extends any ? TransitionValue> : never : never; type StateFromConfig< M extends { initial: string; states: Record }, > = | M['initial'] | Extract | TargetStatesFromConfig; export function StateMachine< M extends { initial: string; states: Record }, >(machine: M): TStateMachineApi, ActionFromConfig> { type SMState = StateFromConfig; type SMAction = ActionFromConfig; const typedMachine = machine as TStateMachine; let state = typedMachine.initial; const api = { getState, initial, staticTransition, transition }; return api; function getState(): SMState { return state; } function initial(): SMState { return machine.initial; } function transition(action: SMAction, payload?: any): Result { const result = calculateNextState(typedMachine, state, action, payload); if (isFailure(result)) { return makeResult.Err(result.error); } state = result.value; return makeResult.Ok(undefined); } function staticTransition( from: SMState, action: SMAction, payload?: any, ): SMState { const transitionTo = getTransitionTarget(typedMachine, from, action); const target = Array.isArray(transitionTo) ? evaluateConditionalTarget(transitionTo, from, payload) : transitionTo; return !target || target === from ? from : (target as SMState); } } function getTransitionTarget( machine: TStateMachine, from: S, action: A, ): TransitionTarget | undefined { return ( machine.states[from]?.[action] ?? machine.states[STATE_WILD_CARD]?.[action] ); } function evaluateConditionalTarget( target: [S, CB], from: S, payload?: any, ): S { const [nextState, conditional] = target; return conditional(payload) ? nextState : from; } function calculateNextState( machine: TStateMachine, from: S, action: A, payload?: any, ): Result { const transitionTo = getTransitionTarget(machine, from, action); if (!transitionTo) { return makeResult.Err( `Invalid transition: "${action}" from state "${from}"`, ); } if (Array.isArray(transitionTo)) { const [candidateState, conditional] = transitionTo; if (!conditional(payload)) { return makeResult.Err( `Invalid transition: "${action}" from state "${from}" (conditional failed)`, ); } return makeResult.Ok(candidateState); } return makeResult.Ok(transitionTo as S); }