yay-machine
Version:
A modern, simple, lightweight, zero-dependency, TypeScript state-machine library
534 lines (493 loc) • 14.6 kB
text/typescript
import type { ExtractEvent, MachineEvent } from "./MachineEvent";
import type { Unsubscribe } from "./MachineInstance";
import type {
ExtractState,
IsStateDataHomogenous,
MachineState,
StateData,
} from "./MachineState";
import type { OneOrMore } from "./OneOrMore";
export const MDC = Symbol("MDC");
/**
* Machine definition configuration - the blueprint for the machine instances.
*/
export type MachineDefinitionConfig<
StateType extends MachineState,
EventType extends MachineEvent,
> =
IsStateDataHomogenous<StateType> extends true
?
| HomogenousStateMachineDefinitionConfigCopyDataOnTransitionTrue<
StateType,
EventType
>
| HomogenousStateMachineDefinitionConfigCopyDataOnTransitionFalse<
StateType,
EventType
>
: HeterogenousStateMachineDefinitionConfig<StateType, EventType, false>;
export interface HeterogenousStateMachineDefinitionConfig<
StateType extends MachineState,
EventType extends MachineEvent,
CopyDataOnTransition extends boolean,
> {
/**
* Default initial state of each new machine.
* Can be overridden by optional `MachineInstanceConfig` passed to
* `MachineDefinition.newInstance()`
*/
readonly initialState: StateType;
/**
* The machine states configuration.
* Defines state-specific event- and/or immediate-transitions
*/
readonly states?: StatesConfig<StateType, EventType, CopyDataOnTransition>;
/**
* Optional side-effect, run when a machine instance is started.
* Should return a tear-down function so any resources can be freed when
* the machine is stopped.
*/
readonly onStart?: MachineOnStartSideEffectFunction<StateType, EventType>;
/**
* Optional side-effect, run when a machine instance is stopped.
* May return a tear-down function so any resources can be freed when
* the machine is stopped.
*/
readonly onStop?: MachineOnStopSideEffectFunction<StateType>;
/**
* Any states configuration.
* Defines state-agnostic event-transitions
*/
readonly on?: AnyStateTransitionsConfig<StateType, EventType, StateType>;
}
export type MachineOnStartSideEffectFunction<
StateType extends MachineState,
EventType extends MachineEvent,
> = (
param: MachineOnStartSideEffectParam<StateType, EventType>,
) => EffectReturnValue;
export type MachineOnStartSideEffectParam<
StateType extends MachineState,
EventType extends MachineEvent,
> = {
readonly state: StateType;
readonly send: SendFunction<EventType>;
};
export type MachineOnStopSideEffectFunction<StateType extends MachineState> = (
param: MachineOnStopSideEffectParam<StateType>,
) => EffectReturnValue;
export type MachineOnStopSideEffectParam<StateType extends MachineState> = {
readonly state: StateType;
};
/**
* For machines whose states have the same data structure
*/
export interface HomogenousStateMachineDefinitionConfigCopyDataOnTransitionTrue<
StateType extends MachineState,
EventType extends MachineEvent,
> extends HeterogenousStateMachineDefinitionConfig<StateType, EventType, true> {
/**
* If `true`, data is automatically copied between states when transitioning, and the
* transition does not provide its own `data()` callback implementation.
* This avoids boilerplate `data()` callbacks config, like `{ to: 'foo', data: ({ state }) => state }`.
* @default false
*/
readonly enableCopyDataOnTransition: true;
}
/**
* For machines whose states have the same data structure
*/
export interface HomogenousStateMachineDefinitionConfigCopyDataOnTransitionFalse<
StateType extends MachineState,
EventType extends MachineEvent,
> extends HeterogenousStateMachineDefinitionConfig<
StateType,
EventType,
false
> {
/**
* If `true`, data is automatically copied between states when transitioning, and the
* transition does not provide its own `data()` callback implementation.
* This avoids boilerplate `data()` callbacks config, like `{ to: 'foo', data: ({ state }) => state }`.
* @default false
*/
readonly enableCopyDataOnTransition?: false;
}
export type StatesConfig<
StateType extends MachineState,
EventType extends MachineEvent,
CopyDataOnTransition extends boolean,
> = {
readonly [Name in StateType["name"]]?: StateConfig<
StateType,
EventType,
ExtractState<StateType, Name>,
CopyDataOnTransition
>;
};
/**
* Configuration for a machine-state:
* * event- and immediate-transitions
* * side-effects
*/
export type StateConfig<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CopyDataOnTransition extends boolean,
> = {
/**
* Define a map of transitions keyed by event `type`
*/
readonly on?: StateTransitionsConfig<
StateType,
EventType,
CurrentState,
CopyDataOnTransition
>;
/**
* Define one or more immediate transitions, that are always attempted when entering the state
*/
readonly always?: OneOrMore<
TransitionConfig<
StateType,
EventType,
CurrentState,
undefined,
CopyDataOnTransition,
true
>
>;
/**
* Optional side-effect, run when the state is entered.
* Should return a tear-down function so any resources can be freed when
* the state is exited.
*/
readonly onEnter?: StateLifecycleSideEffectFunction<CurrentState, EventType>;
/**
* Optional side-effect, run when the state is exited.
* May return a tear-down function so any resources can be freed when
* the state is exited.
*/
readonly onExit?: StateLifecycleSideEffectFunction<CurrentState, EventType>;
};
export type StateLifecycleSideEffectFunction<
CurrentState extends MachineState,
EventType extends MachineEvent,
> = (
param: StateLifecycleSideEffectParam<CurrentState, EventType>,
) => EffectReturnValue;
export type StateLifecycleSideEffectParam<
CurrentState extends MachineState,
EventType extends MachineEvent,
> = {
readonly state: CurrentState;
readonly event: EventType | undefined;
readonly send: SendFunction<EventType>;
};
export type StateTransitionsConfig<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CopyDataOnTransition extends boolean,
> = {
readonly [Type in EventType["type"]]?: OneOrMore<
TransitionConfig<
StateType,
EventType,
CurrentState,
ExtractEvent<EventType, Type>,
CopyDataOnTransition,
false
>
>;
};
export type TransitionConfig<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
CopyDataOnTransition extends boolean,
IsImmediateTransition extends boolean,
> = {
readonly [Name in StateType["name"]]: Transition<
StateType,
EventType,
CurrentState,
CurrentEvent,
ExtractState<StateType, Name>,
CopyDataOnTransition,
IsImmediateTransition
>;
}[StateType["name"]];
export type Transition<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
NextState extends StateType,
CopyDataOnTransition extends boolean,
IsImmediateTransition extends boolean,
> = keyof Omit<NextState, "name"> extends never
? BasicTransition<StateType, EventType, CurrentState, CurrentEvent, NextState>
: OtherTransition<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState,
CopyDataOnTransition,
IsImmediateTransition
>;
/**
* Defines a potential transition from the current state to the next state
*/
export interface BasicTransition<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
NextState extends StateType,
> {
/**
* The name of the next state
*/
readonly to: NextState["name"];
/**
* Optional predicate function if the transition is conditional
* @param param an object containing the current state and event
* @returns true if the transition should be taken
*/
readonly when?: (param: {
readonly state: CurrentState;
readonly event: CurrentEvent;
}) => boolean;
/**
* Optional side-effect, run when the transition is taken.
* May return a tear-down function so any resources can be freed when
* the state is exited.
*/
readonly onTransition?: OnTransitionSideEffectFunction<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState
>;
}
export type OnTransitionSideEffectFunction<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
NextState extends StateType,
> = (
param: OnTransitionSideEffectParam<
StateType,
CurrentState,
CurrentEvent,
NextState,
EventType
>,
) => EffectReturnValue;
type OnTransitionSideEffectParam<
StateType extends MachineState,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
NextState extends StateType,
EventType extends MachineEvent,
> = {
readonly state: CurrentState;
readonly event: CurrentEvent;
readonly next: NextState;
readonly send: SendFunction<EventType>;
};
export type OtherTransition<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
NextState extends StateType,
CopyDataOnTransition extends boolean,
IsImmediateTransition extends boolean,
> = BasicTransition<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState
> &
(NextState["name"] extends CurrentState["name"]
? IsImmediateTransition extends false
?
| ReenterTransition<false>
| DataTransition<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState,
CopyDataOnTransition
>
: DataTransition<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState,
CopyDataOnTransition
>
: DataTransition<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState,
CopyDataOnTransition
>);
export type DataTransition<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
NextState extends StateType,
CopyDataOnTransition extends boolean,
> = CopyDataOnTransition extends true
? Partial<
TransitionData<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState
>
>
: TransitionData<StateType, EventType, CurrentState, CurrentEvent, NextState>;
export interface ReenterTransition<Reenter extends boolean> {
/**
* If false, the transition does not re-enter the current state, so side-effects are not stopped/re-started
* @default true
*/
readonly reenter: Reenter;
}
export type TransitionData<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
NextState extends StateType,
> = {
/**
* Generate state-data for the next state,
* usually by combining existing state-data with the event-payload
*/
readonly data: (param: {
readonly state: CurrentState;
readonly event: CurrentEvent;
}) => StateData<NextState, NextState["name"]>;
};
export type SendFunction<EventType extends MachineEvent> = (
event: EventType,
) => void;
// biome-ignore lint/suspicious/noConfusingVoidType: adding void to union as we don't want to force users to explicity return
export type EffectReturnValue = Unsubscribe | undefined | null | void;
export type AnyStateTransitionsConfig<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
> = {
readonly [Type in EventType["type"]]?: OneOrMore<
AnyStateTransitionConfig<
StateType,
EventType,
CurrentState,
ExtractEvent<EventType, Type>
>
>;
};
export type AnyStateTransitionConfig<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
> = {
readonly [Name in StateType["name"]]: keyof Omit<
ExtractState<StateType, Name>,
"name"
> extends never
? AnyStateTransition<
StateType,
EventType,
CurrentState,
CurrentEvent,
ExtractState<StateType, Name>
>
: AnyStateTransitionWith<
StateType,
EventType,
CurrentState,
CurrentEvent,
ExtractState<StateType, Name>
>;
}[StateType["name"]];
export interface AnyStateTransition<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
NextState extends StateType,
> {
/**
* The name of the next state
*/
readonly to?: NextState["name"]; // current state if not specified
/**
* Optional predicate function if the transition is conditional
* @param param the current state and event
* @returns true if the transition should be taken
*/
readonly when?: (param: {
readonly state: CurrentState;
readonly event: CurrentEvent;
}) => boolean;
/**
* Optional side-effect, run when the transition is taken.
* May return a tear-down function so any resources can be freed when
* the state is exited.
*/
readonly onTransition?: (param: {
readonly state: CurrentState;
readonly event: CurrentEvent;
readonly send: SendFunction<EventType>;
}) => EffectReturnValue;
}
export type AnyStateTransitionWith<
StateType extends MachineState,
EventType extends MachineEvent,
CurrentState extends StateType,
CurrentEvent extends EventType | undefined,
NextState extends StateType,
> = AnyStateTransition<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState
> &
(IsStateDataHomogenous<StateType> extends true
? Partial<
TransitionData<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState
>
>
: TransitionData<
StateType,
EventType,
CurrentState,
CurrentEvent,
NextState
>);