Created
February 18, 2021 22:20
-
-
Save tehbilly/a62f023b5d43f3048f17c6f1486bef42 to your computer and use it in GitHub Desktop.
Simple TypeScript state machine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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