Skip to content

Instantly share code, notes, and snippets.

@tehbilly
Created February 18, 2021 22:20
Show Gist options
  • Select an option

  • Save tehbilly/a62f023b5d43f3048f17c6f1486bef42 to your computer and use it in GitHub Desktop.

Select an option

Save tehbilly/a62f023b5d43f3048f17c6f1486bef42 to your computer and use it in GitHub Desktop.
Simple TypeScript state machine
export type Supplier<T> = () => T;
export type StateSupplier = string | Supplier<string>;
export interface State {
name?: string;
on: {
[name: string]: StateSupplier;
};
onEnter?: (prev: State) => void;
onLeave?: (next: State) => void;
}
export interface StateMachineSetup {
initial: string;
states: { [state: string]: State };
}
export class StateMachine {
private states: Map<string, State> = new Map<string, State>();
private _current: State;
constructor(opts: StateMachineSetup) {
for (let name in opts.states) {
const state = { name, ...opts.states[name] };
this.states.set(name, state);
}
// Throw an error if an invalid initial state was provided
if (!this.states.has(opts.initial)) {
throw new Error(`unknown initial state: ${opts.initial}`);
}
this._current = this.states.get(opts.initial);
}
get current(): string {
return this._current.name;
}
emit(name: string) {
// Throw if transition doesn't exist
if (!this._current.on[name]) {
throw new Error(`State(${this.current}) has no transition: '${name}'`);
}
const current: State = this._current;
// Get the name of the next state.
const supplier: StateSupplier = current.on[name];
let nextName: string;
if (typeof supplier === 'string') {
nextName = supplier;
} else {
nextName = supplier();
}
// Get the next state. Throw if it doesn't exist.
const next: State | undefined = this.states.get(nextName);
if (next === undefined) {
throw new Error(`State(${current.name}) Transition(${name}) returned unknown next state: '${nextName}'`);
}
current.onLeave?.(next);
next.onEnter?.(current);
this._current = next;
}
}
// State machine describing water
const sm = new StateMachine({
// Name of initial state
initial: 'liquid',
// State configuration objects. Key/name is used as identifier
states: {
liquid: {
// Actions, where the name (cool|heat here) are used in StateMachine.emit to cause transitions between states
on: {
cool: () => 'solid',
heat: () => 'gas',
},
// Optional state enter/leave callbacks
onEnter: prev => console.log(`Transitioned: '${prev.name}' => 'liquid'`),
onLeave: next => console.log(`Transitioned: 'liquid' => '${next.name}'`),
},
// States can optionally simply provide a string=>string mapping of actions to new statess
solid: {
on: {
heat: 'liquid'
}
},
gas: { on: { cool: 'liquid' } },
},
});
console.log('Initial state:', sm.current);
sm.emit('heat');
sm.emit('cool');
sm.emit('cool');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment