Skip to content

Instantly share code, notes, and snippets.

@pveglia
Created November 13, 2017 22:58
Show Gist options
  • Save pveglia/aa9d22573f147d6d6dce259fed23adc2 to your computer and use it in GitHub Desktop.
Save pveglia/aa9d22573f147d6d6dce259fed23adc2 to your computer and use it in GitHub Desktop.
State machine in typescript
import {State, StateMachine, Transition} from './state-machine';
console.log('hey');
const stateMachine = new StateMachine(
{
'initial': {
onEnter: (input: any) => {
console.log('entering initial');
input.state = 'initial';
return input;
},
onExit: (input: any): any => {
console.log('exiting initial');
delete input.state;
},
transitions: {
'next': {
action: () => {
console.log('action of next transition');
},
nextState: 'second'
}
}
},
'second': {
transitions: {
'next': {
nextState: () => 'third'
},
'back': {
nextState: 'initial'
}
}
},
'third': {
transitions: {
'back': {
nextState: 'second'
}
}
}
},
'initial', {});
stateMachine.events.subscribe(event => {
console.log('event emitted', event);
})
console.log('next state:', stateMachine.handle('next'));
stateMachine.handle('next');
stateMachine.handle('back');
stateMachine.handle('back');
import {Subject} from 'rxjs/Subject';
type Transitioner = (context?: any) => any;
export enum EventType {
Event = 'event',
Transition = 'transition'
}
export interface State {
// name: string;
onEnter?: Transitioner, onExit?: Transitioner,
transitions: {[event_name: string]: Transition}
}
export interface Transition {
action?: Transitioner, nextState: (() => string)|string
}
export interface EventRecord {
type: EventType.Event;
value: string
}
export interface TransitionRecord {
type: EventType.Transition;
oldState?: string, newState: string,
context: any
}
export class StateMachine {
private eventEmitter = new Subject<EventRecord|TransitionRecord>();
public events = this.eventEmitter.asObservable();
private currentState: State;
private currentStateName: string;
private context: any;
constructor(
private configuration: {[state_name: string]: State},
initialState: string, initialContext: any) {
this.currentState = this.configuration[initialState];
this.currentStateName = initialState;
this.context = initialContext;
if (this.currentState) {
this.updateContext(this.currentState.onEnter);
}
}
private updateContext(fn?: Transitioner) {
if (fn) {
const result = fn(this.context);
if (result !== undefined) {
this.context = result;
}
}
}
handle(event: string): string {
this.eventEmitter.next({type: EventType.Event, value: event});
if (this.currentState && this.currentState.transitions[event]) {
const transition = this.currentState.transitions[event];
this.updateContext(this.currentState.onExit);
this.updateContext(transition.action);
const nextState = this.resolveNextState(transition.nextState);
if (this.configuration[nextState] === undefined) {
throw new Error(`State "${nextState}" is not configured`);
}
this.eventEmitter.next({
type: EventType.Transition,
oldState: this.currentStateName,
newState: nextState, context: this.context
});
this.currentState = this.configuration[nextState];
this.currentStateName = nextState;
this.updateContext(this.currentState.onEnter);
return nextState;
} else {
throw new Error('Could not transition to new state');
}
}
resolveNextState(nextState: string|((context: any) => string)): string {
if (typeof nextState === 'string') {
return nextState;
}
if (typeof nextState === 'function') {
return nextState(this.context);
}
throw new Error('Cannot resolve next state');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment