/** * This module is a simple state machine implementation. We model states as a * graph, and the state machine simply keeps track of the current and final * state. The state also has a display field as metadata. This serves as the * state name and is also used to represent the remaining text to be input at * that particular state. */ export enum TransitionResult { FAILED, SUCCESS, SKIPPED, } interface StateMap { [index: string]: State; } interface StateTransitionList { [index: string]: State; } export interface Observer { (result: TransitionResult, meta: Meta, finished: boolean): void; } export class State { display: string; meta: Meta; transitions: StateTransitionList; constructor(display: string, meta: Meta) { this.display = display; this.meta = meta; this.transitions = {}; } addTransition(input: string, state: State): void { this.transitions[input] = state; } transition(input: string): State | undefined { return this.transitions[input]; } merge(other: State): State { const newState = this.clone(); for (const key in other.transitions) { const otherNextState = other.transitions[key]; const thisNextState = this.transition(key); if (thisNextState === undefined) { newState.addTransition(key, otherNextState); } else { newState.addTransition(key, thisNextState.merge(otherNextState)); } } return newState; } transform(fn: (state: State) => [string, Meta]): State { const [newDisplay, newMeta] = fn(this); const newState = new State(newDisplay, newMeta); for (const key in this.transitions) { newState.transitions[key] = this.transitions[key].transform(fn); } return newState; } closure(): Set> { const closure: Set> = new Set([this]); for (const key in this.transitions) { const nextState = this.transitions[key]; if (!closure.has(nextState)) { nextState.closure().forEach((state) => { closure.add(state); }); } } return closure; } isEnd(): boolean { return Object.values(this.transitions).length === 0; } clone(): State { const state = new State(this.display, this.meta); state.transitions = { ...this.transitions }; return state; } debug(name?: string): this { if (name) { console.group(name); } this.closure().forEach((state) => console.log(state.toJSON())); if (name) { console.groupEnd(); } return this; } toJSON(): string { const transitions = []; for (const key in this.transitions) { transitions.push(`${key}->${this.transitions[key].display}`); } return `${this.display}(${this.meta}): ${transitions.join(',')}`; } } export class MetaStateMachine { initialState: State; currentState: State; observers: Set>; nextMachine: MetaStateMachine | null; constructor(initialState: State) { this.initialState = initialState; this.currentState = initialState; this.observers = new Set(); this.nextMachine = null; } transition(input: string) { const nextState = this.currentState.transition(input); if (nextState === undefined) { this.skipTransition(input); } else { this.currentState = nextState; this.notifyResult( TransitionResult.SUCCESS, this.currentState.meta, this.currentState.isEnd() ); } } private skipTransition(input: string): boolean { let potentialNextStates: Array> = Object.keys( this.currentState.transitions ).map((k) => this.currentState.transitions[k]); for (let i = 0; i < potentialNextStates.length; ++i) { const state = potentialNextStates[i]; if (state.isEnd()) { if (this.nextMachine != null) { let result = this.nextMachine.initialState.transition(input); if (result != null) { const newState = result; this.currentState = state; this.nextMachine.currentState = newState; this.notifyResult( TransitionResult.SKIPPED, state.meta, state.isEnd() ); this.nextMachine.notifyResult( TransitionResult.SUCCESS, newState.meta, newState.isEnd() ); return true; } } } else { let result = state.transition(input); if (result != null) { const newState = result; this.currentState = newState; this.notifyResult( TransitionResult.SKIPPED, state.meta, state.isEnd() ); this.notifyResult( TransitionResult.SUCCESS, newState.meta, newState.isEnd() ); return true; } } } this.notifyResult(TransitionResult.FAILED, this.currentState.meta, false); return false; } isNew(): boolean { return this.currentState === this.initialState; } isFinished(): boolean { return this.currentState.isEnd(); } reset(): void { this.currentState = this.initialState; } clone(): MetaStateMachine { return new MetaStateMachine(this.initialState); } getWord(): string { return this.initialState.display; } getDisplay(): string { return this.currentState.display; } addObserver(observer: Observer): void { this.observers.add(observer); } removeObserver(observer: Observer): void { this.observers.delete(observer); } notifyResult(result: TransitionResult, meta: Meta, finished: boolean): void { this.observers.forEach((o) => o(result, meta, finished)); } debug(): this { this.initialState.debug(this.initialState.display); return this; } } export interface Transition { from: string; input: string; to: string; meta: number; } export class StateMachine extends MetaStateMachine {} export function buildFromTransitions( initial: string, transitions: Transition[] ): StateMachine { let states: StateMap = {}; function getState(name: string, meta: number): State { if (states[name] === undefined) { states[name] = new State(name, meta); } return states[name]; } transitions.forEach((t) => { let fromState = getState(t.from, 0); let toState = getState(t.to, Math.max(fromState.meta, t.meta)); fromState.addTransition(t.input, toState); }); let initialState = getState(initial, 0); return new StateMachine(initialState); } export function mergeMachines(...machines: StateMachine[]): StateMachine { const newState = machines .map((machine) => machine.initialState) .reduce((acc, state) => acc.merge(state)); return new StateMachine(newState); } export function appendMachines(...machines: StateMachine[]): StateMachine { const newState = machines .map((machine) => machine.initialState) .reduce(appendStates); return new StateMachine(newState); } export function makeTransition( from: string, input: string, to: string, meta: number = 0 ): Transition { return { from, input, to, meta }; } export function appendStates( a: State, b: State ): State { const newState = a.transform((state) => { return [state.display + b.display, state.meta]; }); const finalStates: Set> = new Set(); let lastMeta = 0; for (const state of newState.closure()) { if (state.isEnd()) { lastMeta = state.meta; finalStates.add(state); } } const { transitions } = b.transform((state) => { return [state.display, state.meta + lastMeta]; }); finalStates.forEach((finalState) => { finalState.transitions = transitions; }); return newState; }