it-fsm/fsm.ts

231 lines
5.7 KiB
TypeScript
Raw Normal View History

2021-08-20 01:32:01 +03:00
type StateTransitions<Context> = WeakMap<
State<Context>,
WeakSet<State<Context>>
>;
2021-08-20 11:43:31 +03:00
type StateName = string;
type StateOrName<Context> = State<Context> | StateName;
2021-08-20 01:32:01 +03:00
export const _states = Symbol("states");
export const _stateTransitions = Symbol("state transitions");
export const _prevState = Symbol("previous state");
export const _currState = Symbol("current state");
export class StateMachineBuilder<Context> {
2021-08-20 11:43:31 +03:00
[_states]: Map<StateName, Actions<Context>>;
2021-08-20 01:32:01 +03:00
2021-08-20 11:43:31 +03:00
[_stateTransitions]: Array<[StateName, Array<StateName>]> | undefined;
2021-08-20 01:32:01 +03:00
constructor() {
this[_states] = new Map();
}
2021-08-20 11:43:31 +03:00
withTransitions(transitions: Array<[StateName, Array<StateName>]>) {
2021-08-20 01:32:01 +03:00
this[_stateTransitions] = transitions;
return this;
}
2021-08-20 11:43:31 +03:00
withStates(names: StateName[], actions?: Actions<Context>) {
2021-08-20 01:32:01 +03:00
names.forEach((name) => this.addStateUnchecked(name, actions));
return this;
}
2021-08-20 11:43:31 +03:00
withState(name: StateName, actions?: Actions<Context>) {
2021-08-20 01:32:01 +03:00
this.addStateUnchecked(name, actions);
return this;
}
2021-08-20 11:43:31 +03:00
private addStateUnchecked(name: StateName, actions?: Actions<Context>) {
2021-08-20 01:32:01 +03:00
const oldActions = this[_states].get(name);
return this[_states].set(name, { ...oldActions, ...actions });
}
2021-08-20 11:43:31 +03:00
build(currentStateName: StateName) {
2021-08-20 01:32:01 +03:00
const states = this.buildStates();
const transitions = this.buildTransitions(states);
const currState = validStateFromName(states, currentStateName);
return new StateMachine(states, transitions, currState);
}
private buildStates() {
return Array.from(this[_states].entries()).map((params) =>
new State(...params)
);
}
private buildTransitions(states: State<Context>[]) {
const sourceTransitions = this[_stateTransitions] || [];
return new WeakMap(
sourceTransitions.map(([from, toStates]) => [
validStateFromName(states, from),
new WeakSet(toStates.map(validStateFromName.bind(null, states))),
]),
);
}
}
export class StateMachine<Context> {
[_states]: State<Context>[];
[_stateTransitions]: StateTransitions<Context>;
[_prevState]: State<Context> | undefined;
[_currState]: State<Context>;
constructor(
states: State<Context>[],
transitions: StateTransitions<Context>,
currentState: State<Context>,
) {
this[_states] = states;
this[_stateTransitions] = transitions;
this[_currState] = currentState;
}
2021-08-20 11:43:31 +03:00
async changeState(sourceState: StateOrName<Context>, context?: Context) {
2021-08-20 01:32:01 +03:00
const fromState = validState(this[_currState]);
2021-08-20 11:43:31 +03:00
const toState = validNormalizedState(this[_states], sourceState);
2021-08-20 01:32:01 +03:00
if (
!this.hasTransition(toState) ||
!fromState.exit(fromState, toState, context)
) {
throw new FsmError(
`cannot change state from "${fromState.name}" to "${toState.name}"`,
);
}
await toState.entry(fromState, toState, context);
this[_currState] = toState;
this[_prevState] = fromState;
}
2021-08-20 11:43:31 +03:00
hasTransition(to: StateOrName<Context>) {
2021-08-20 01:32:01 +03:00
return hasTransition(
this[_stateTransitions],
this[_currState],
2021-08-20 11:43:31 +03:00
validNormalizedState(this[_states], to),
2021-08-20 01:32:01 +03:00
);
}
allowedTransitionStates() {
const fromState = validState(this[_currState]);
return this[_states].filter(
hasTransition.bind(null, this[_stateTransitions], fromState),
);
}
}
const _stateName = Symbol("state name");
const _stateActions = Symbol("state actions");
interface Actions<Context> {
beforeExit?(
fromState: State<Context>,
toState: State<Context>,
2021-08-20 11:43:31 +03:00
context?: Context,
2021-08-20 01:32:01 +03:00
): boolean;
onEntry?(
fromState: State<Context>,
toState: State<Context>,
2021-08-20 11:43:31 +03:00
context?: Context,
2021-08-20 01:32:01 +03:00
): Promise<void> | void;
}
export class State<Context> {
[_stateActions]: Actions<Context>;
2021-08-20 11:43:31 +03:00
[_stateName]: StateName;
2021-08-20 01:32:01 +03:00
2021-08-20 11:43:31 +03:00
get name(): StateName {
2021-08-20 01:32:01 +03:00
return this[_stateName];
}
2021-08-20 11:43:31 +03:00
constructor(name: StateName, actions: Actions<Context> = {}) {
2021-08-20 01:32:01 +03:00
this[_stateName] = name;
this[_stateActions] = actions;
}
async entry(
fromState: State<Context>,
toState: State<Context>,
2021-08-20 11:43:31 +03:00
context?: Context,
2021-08-20 01:32:01 +03:00
) {
const action = this[_stateActions].onEntry;
if (isFn(action)) {
await action(fromState, toState, context);
}
}
exit(fromState: State<Context>, toState: State<Context>, context: Context) {
const action = this[_stateActions].beforeExit;
return isFn(action) ? action(fromState, toState, context) : true;
}
toString() {
return this.name;
}
toJSON() {
return this.toString();
}
}
2021-08-20 11:43:31 +03:00
function validNormalizedState<Context>(
states: State<Context>[],
state: StateOrName<Context>,
) {
return validState<Context>(normalizeState(states, state));
2021-08-20 01:32:01 +03:00
}
function normalizeState<Context>(
states: State<Context>[],
2021-08-20 11:43:31 +03:00
state: StateOrName<Context>,
2021-08-20 01:32:01 +03:00
): State<Context> | undefined {
return isStr(state) ? stateFromName(states, state) : state;
}
2021-08-20 11:43:31 +03:00
function validStateFromName<Context>(
states: State<Context>[],
name: StateName,
) {
return validState<Context>(stateFromName(states, name));
}
function stateFromName<Context>(states: State<Context>[], name: StateName) {
return states.find((state) => state.name === name);
}
2021-08-20 01:32:01 +03:00
function validState<Context>(val: unknown): State<Context> {
if (!isState<Context>(val)) {
throw new TypeError("an instance of State class is expected");
}
return val;
}
function isState<Context>(val: unknown): val is State<Context> {
return val instanceof State;
}
function hasTransition<Context>(
transitions: StateTransitions<Context>,
from: State<Context>,
to: State<Context>,
) {
return transitions.get(from)?.has(to) || false;
}
function isStr(val: unknown): val is string {
return typeof val === "string";
}
// deno-lint-ignore ban-types
function isFn(val: unknown): val is Function {
return typeof val === "function";
}
export class FsmError extends Error {}