{"id":1052,"date":"2018-03-13T17:11:12","date_gmt":"2018-03-13T16:11:12","guid":{"rendered":"https:\/\/solidt.eu\/site\/?p=1052"},"modified":"2019-02-04T16:33:37","modified_gmt":"2019-02-04T15:33:37","slug":"typescript-statemachine","status":"publish","type":"post","link":"https:\/\/solidt.eu\/site\/typescript-statemachine\/","title":{"rendered":"Typescript statemachine"},"content":{"rendered":"<pre class=\"lang:js decode:true \">import Promise from 'bluebird';\n\nexport interface Transition {\n    name: string,\n    from: string,\n    to: string\n}\n\nexport class StateMachine {\n    private previousState : string;\n    private currentState : string;\n    private transitions : Transition[];\n    private transitionCallbacks : any = {};\n    private enterCallbacks : any = {};\n    private leaveCallbacks : any = {};\n    constructor(intitialState : string, transitions: Transition[])\n    {\n        this.previousState = intitialState;\n        this.currentState = intitialState;\n        this.transitions = transitions;\n    }\n    private changeState(transitionName : string, newState : string) {\n        console.log(`${transitionName}: ${this.currentState} -&gt; ${newState}`);\n        this.previousState = this.currentState;\n        this.currentState = newState;\n\n        let cb;\n        cb = this.leaveCallbacks[this.previousState];\n        if (cb) {\n            setTimeout(cb, 0);\n        }\n        cb = this.enterCallbacks[this.currentState];\n        if (cb) {\n            setTimeout(cb, 0);\n        }\n    }\n    findTransition(transitionName: string, doThrow : boolean = false) : Transition | null {\n        let transitions = this.transitions.filter(t =&gt; t.name == transitionName);\n        if (transitions.length &lt; 1) {\n            if (doThrow) {\n                throw new Error(`transition '${transitionName}' does not exist`);\n            }\n            return null;\n        }\n        let transition = transitions.filter(t =&gt; t.from == '*' || t.from == this.currentState).pop();\n        if (!transition) {\n            if (doThrow) {\n                throw new Error(`transition '${transitionName}' not possible from state '${this.currentState}'`);\n            }\n            return null;\n        }\n        return transition;\n    }\n    trigger(transitionName: string) : void {\n        let transition = this.findTransition(transitionName, true);\n        console.log(`triggered: ${transitionName}`);\n        let cb = this.transitionCallbacks[transitionName];\n        if (cb) {\n            Promise.method(cb)({})\n                .then(() =&gt; this.changeState(transitionName, transition!.to))\n                .catch(() =&gt; Promise.resolve());\n        } else {\n            this.changeState(transitionName, transition!.to);\n        }\n    }\n    canTrigger(transitionName: string) : boolean {\n        let transition = this.findTransition(transitionName, false);\n        if (!transition) {\n            return false;\n        }\n        return true;\n    }\n    getState() : string {\n        return this.currentState;\n    }\n    public on(transitionName : string, callback: Function) : void {\n        this.transitionCallbacks[transitionName] = callback;\n    }\n    public onEnter(stateName : string, callback: Function) : void {\n        this.enterCallbacks[stateName] = callback;\n    }\n    public onLeave(stateName : string, callback: Function) : void {\n        this.leaveCallbacks[stateName] = callback;\n    }\n}\n<\/pre>\n<pre class=\"lang:js decode:true \">import Promise from 'bluebird';\n\nimport {StateMachine} from '.\/StateMachine'\n\n\ndescribe('StateMachine', () =&gt; {\n\n    it('should change state', (done) =&gt; {\n\n        let stateMachine = new StateMachine('created', [\n            { name: 'load', from: 'created', to: 'loading'},\n            { name: 'setReady', from: 'loading', to: 'ready'},\n            { name: 'play', from: 'ready', to: 'playing'},\n            { name: 'stop', from: 'playing', to: 'ended'},\n            { name: 'cancel', from: '*', to: 'ended'}\n        ]);\n\n\n        stateMachine.trigger('load');\n\n        stateMachine.on('setReady', () =&gt; Promise.delay(1000));\n\n        stateMachine.onLeave('loading', () =&gt; {\n            console.log('I am loaded');\n        });\n\n        stateMachine.onEnter('ended', () =&gt; {\n            console.log('I am ended');\n        });\n\n        stateMachine.onEnter('ready', () =&gt; {\n            console.log('I am ready');\n\n            stateMachine.trigger('cancel');\n\n            stateMachine.trigger('play');\n\n            stateMachine.trigger('stop');\n        });\n\n        stateMachine.trigger('setReady');\n\n        setTimeout(done, 3000);\n    })\n});\n<\/pre>\n<pre class=\"lang:default decode:true \" title=\"StateMachine.js\">const Promise = require('bluebird');\n\n\/\/ Transition { name, from, to }\n\/\/ OnStateChanged = (transition, currentState, newState) {};\n\nclass StateMachine {\n    constructor(intitialState, transitions, onStateChanged)\n    {\n        this.previousState = intitialState;\n        this.currentState = intitialState;\n        this.transitions = transitions;\n        this.onStateChanged = onStateChanged;\n        this.transitionCallbacks = {};\n        this.enterCallbacks = {};\n        this.leaveCallbacks = {};\n    }\n    changeState(transitionName, newState) {\n        this.previousState = this.currentState;\n        this.currentState = newState;\n        this.onStateChanged(transitionName, this.previousState, this.currentState);\n\n        let cb;\n        cb = this.leaveCallbacks[this.previousState];\n        if (cb) {\n            setTimeout(cb, 0);\n        }\n        cb = this.enterCallbacks[this.currentState];\n        if (cb) {\n            setTimeout(cb, 0);\n        }\n    }\n    findTransition(transitionName, doThrow) {\n        let transitions = this.transitions.filter(t =&gt; t.name == transitionName);\n        if (transitions.length &lt; 1) {\n            if (doThrow) {\n                throw new Error(`transition '${transitionName}' does not exist`);\n            }\n            return null;\n        }\n        let transition = transitions.filter(t =&gt; t.from == '*' || t.from == this.currentState).pop();\n        if (!transition) {\n            if (doThrow) {\n                throw new Error(`transition '${transitionName}' not possible from state '${this.currentState}'`);\n            }\n            return null;\n        }\n        return transition;\n    }\n    trigger(transitionName) {\n        let self = this;\n        let transition = this.findTransition(transitionName, true);\n        let cb = this.transitionCallbacks[transitionName];\n        setTimeout(() =&gt; {\n            if (cb) {\n                Promise.method(cb)({})\n                    .then(() =&gt; self.changeState(transitionName, transition.to))\n                    .catch(() =&gt; Promise.resolve());\n            } else {\n                self.changeState(transitionName, transition.to);\n            }\n        }, 0);\n    }\n    canTrigger(transitionName) {\n        let transition = this.findTransition(transitionName, false);\n        if (!transition) {\n            return false;\n        }\n        return true;\n    }\n    getState() {\n        return this.currentState;\n    }\n    on(transitionName, callback) {\n        this.transitionCallbacks[transitionName] = callback;\n    }\n    onEnter(stateName, callback) {\n        this.enterCallbacks[stateName] = callback;\n    }\n    onLeave(stateName, callback) {\n        this.leaveCallbacks[stateName] = callback;\n    }\n}\n\nmodule.exports = StateMachine;\n<\/pre>\n<p>&nbsp;<\/p>\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">const Promise = require('bluebird');\n\n\/\/ Transition { name, from, to }\n\/\/ OnStateChanged = (transition, currentState, newState) {};\n\nclass StateMachine {\n    constructor(intitialState, transitions, onStateChanged) {\n        this.previousState = intitialState;\n        this.currentState = intitialState;\n        this.transitions = transitions;\n        this.onStateChanged = onStateChanged;\n        this.transitionCallbacks = {};\n        this.enterCallbacks = {};\n        this.leaveCallbacks = {};\n    }\n    changeState(transitionName, newState) {\n        this.previousState = this.currentState;\n        this.currentState = newState;\n        this.onStateChanged(transitionName, this.previousState, this.currentState);\n\n        let cb;\n        cb = this.leaveCallbacks[this.previousState];\n        if (cb) {\n            setTimeout(cb, 0);\n        }\n        cb = this.enterCallbacks[this.currentState];\n        if (cb) {\n            setTimeout(cb, 0);\n        }\n    }\n    findTransition(transitionName, doThrow) {\n        let transitions = this.transitions.filter(t => t.name == transitionName);\n        if (transitions.length &lt; 1) {\n            if (doThrow) {\n                throw new Error(`transition '${transitionName}' does not exist`);\n            } else {\n                console.log(`transition '${transitionName}' does not exist`);\n            }\n            return null;\n        }\n        let transition = transitions.filter(t => t.from == '*' || t.from == this.currentState).pop();\n        if (!transition) {\n            if(transitions[0] &amp;&amp; transitions[0].to == this.currentState) {\n                console.log(`already in state '${this.currentState}', skipping transition: '${transitionName}'`);\n            } else if (doThrow) {\n                throw new Error(`transition '${transitionName}' not possible from state '${this.currentState}'`);\n            } else {\n                console.log(`transition '${transitionName}' not possible from state '${this.currentState}'`);\n            }\n            return null;\n        }\n        return transition;\n    }\n    trigger(transitionName) {\n        let self = this;\n        let transition = this.findTransition(transitionName, false);\n        let cb = this.transitionCallbacks[transitionName];\n        if (transition) {\n            setTimeout(() => {\n                if (cb) {\n                    Promise.method(cb)({})\n                        .then(() => self.changeState(transitionName, transition.to))\n                        .catch(() => Promise.resolve());\n                } else {\n                    self.changeState(transitionName, transition.to);\n                }\n            }, 0);\n        }\n    }\n    canTrigger(transitionName) {\n        let transition = this.findTransition(transitionName, false);\n        if (!transition) {\n            return false;\n        }\n        return true;\n    }\n    getState() {\n        return this.currentState;\n    }\n    on(transitionName, callback) {\n        this.transitionCallbacks[transitionName] = callback;\n    }\n    onEnter(stateName, callback) {\n        this.enterCallbacks[stateName] = callback;\n    }\n    onLeave(stateName, callback) {\n        this.leaveCallbacks[stateName] = callback;\n    }\n}\n\nmodule.exports = StateMachine;\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>import Promise from &#8216;bluebird&#8217;; export interface Transition { name: string, from: string, to: string } export class StateMachine { private previousState : string; private currentState : string; private transitions : Transition[]; private transitionCallbacks : any = {}; private enterCallbacks : any = {}; private leaveCallbacks : any = {}; constructor(intitialState : string, transitions: Transition[]) { [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-1052","post","type-post","status-publish","format-standard","hentry","category-javascript"],"_links":{"self":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/1052","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/comments?post=1052"}],"version-history":[{"count":3,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/1052\/revisions"}],"predecessor-version":[{"id":1853,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/1052\/revisions\/1853"}],"wp:attachment":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/media?parent=1052"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/categories?post=1052"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/tags?post=1052"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}