GithubHelp home page GithubHelp logo

ifandelse / machina.js Goto Github PK

View Code? Open in Web Editor NEW
1.9K 77.0 148.0 1.98 MB

js ex machina - finite state machines in JavaScript

Home Page: http://machina-js.org/

License: Other

HTML 4.48% JavaScript 88.30% CSS 7.23%

machina.js's Introduction

machina v5.0.0-pre.1

Renewed Activity

A quick note here to say two things: First, a heartfelt thank you to everyone that has used machina over the years, and for the many who I've had the honor of collaborating & conversing with to improve it over time. Second, while the commit log here on the repo has been very quiet, things won't stay that way for long! As many fellow OSS authors know (and like the old Nationwide commercials would say): "Life comes at you fast". In the last decade my family has grown, I've moved, I've consulted, lead product teams, co-founded a startup, volunteered in a state agency (related to service during natural disasters) and so much more. I've focused on my family, and then the product teams I've had the honor to be a part of. Finally, though, I have the bandwidth I've wanted to focus on continuing the goals I have for machina. I plan to keep a running list of what's ahead at the bottom of this README.

What is it?

Machina.js is a JavaScript framework for highly customizable finite state machines (FSMs). Many of the ideas for machina have been loosely inspired by the Erlang/OTP FSM behaviors.

Why Would I Use It?

Finite state machines are a great conceptual model for many concerns facing developers โ€“ from conditional UI, connectivity monitoring & management to initialization and more. State machines can simplify tangled paths of asynchronous code, they're easy to test, and they inherently lend themselves to helping you avoid unexpected edge-case-state pitfalls. machina aims to give you the tools you need to model state machines in JavaScript, without being too prescriptive on the problem domain you're solving for.

Some frequent use cases for machina:

  • online/offline connectivity management
  • conditional UI (menus, navigation, workflow)
  • initialization of node.js processes or single-page-apps
  • responding to user input devices (remotes, keyboard, mouse, etc.)

Quick Example

First - you need to include it in your environment (browser, node, etc.):

// If you're not using an AMD loader, machina is available on the window
// Just make sure you have lodash loaded before machina
var MyFsm = machina.Fsm.extend({ /* your stuff */});

// If you're using an AMD loader:
require(['machina'], function(machina){
    return machina.Fsm.extend({ /* your stuff */});
});

// node.js/CommonJS:
var machina = require('machina');

// FYI machina v0.3.x & earlier returned a factory
// function in CommonJS environments:
var lodash = require('lodash');
var machina = require('machina')(lodash);
var MyFsm = machina.Fsm.extend({ /* your stuff */});

Great, now that we know how to pull it in, let's create an FSM to represent a vehicle traffic light at a pedestrian crosswalk:

var vehicleSignal = new machina.Fsm( {

    // the initialize method is called right after the FSM
    // instance is constructed, giving you a place for any
    // setup behavior, etc. It receives the same arguments
    // (options) as the constructor function.
    initialize: function( options ) {
        // your setup code goes here...
    },

    namespace: "vehicle-signal",

    // `initialState` tells machina what state to start the FSM in.
    // The default value is "uninitialized". Not providing
    // this value will throw an exception in v1.0+
    initialState: "uninitialized",

    // The states object's top level properties are the
    // states in which the FSM can exist. Each state object
    // contains input handlers for the different inputs
    // handled while in that state.
    states: {
        uninitialized: {
            // Input handlers are usually functions. They can
            // take arguments, too (even though this one doesn't)
            // The "*" handler is special (more on that in a bit)
            "*": function() {
                this.deferUntilTransition();
                // the `transition` method takes a target state (as a string)
                // and transitions to it. You should NEVER directly assign the
                // state property on an FSM. Also - while it's certainly OK to
                // call `transition` externally, you usually end up with the
                // cleanest approach if you endeavor to transition *internally*
                // and just pass input to the FSM.
                this.transition( "green" );
            }
        },
        green: {
            // _onEnter is a special handler that is invoked
            // immediately as the FSM transitions into the new state
            _onEnter: function() {
                this.timer = setTimeout( function() {
                    this.handle( "timeout" );
                }.bind( this ), 30000 );
                this.emit( "vehicles", { status: "GREEN" } );
            },
            // If all you need to do is transition to a new state
            // inside an input handler, you can provide the string
            // name of the state in place of the input handler function.
            timeout: "green-interruptible",
            pedestrianWaiting: function() {
                this.deferUntilTransition( "green-interruptible" );
            },
            // _onExit is a special handler that is invoked just before
            // the FSM leaves the current state and transitions to another
            _onExit: function() {
                clearTimeout( this.timer );
            }
        },
        "green-interruptible": {
            pedestrianWaiting: "yellow"
        },
        yellow: {
            _onEnter: function() {
                this.timer = setTimeout( function() {
                    this.handle( "timeout" );
                }.bind( this ), 5000 );
                // machina FSMs are event emitters. Here we're
                // emitting a custom event and data, etc.
                this.emit( "vehicles", { status: "YELLOW" } );
            },
            timeout: "red",
            _onExit: function() {
                clearTimeout( this.timer );
            }
        },
        red: {
            _onEnter: function() {
                this.timer = setTimeout( function() {
                    this.handle( "timeout" );
                }.bind( this ), 1000 );
                this.emit( "vehicles", { status: "RED" } );
            },
            _reset: "green",
            _onExit: function() {
                clearTimeout(this.timer);
            }
        }
    },

    // While you can call the FSM's `handle` method externally, it doesn't
    // make for a terribly expressive API. As a general rule, you wrap calls
    // to `handle` with more semantically meaningful method calls like these:
    reset: function() {
        this.handle( "_reset" );
    },

    pedestrianWaiting: function() {
        this.handle( "pedestrianWaiting" );
    }
} );

// Now, to use it:
// This call causes the FSM to transition from uninitialized -> green
// & queues up pedestrianWaiting input, which replays after the timeout
// causes a transition to green-interruptible....which immediately
// transitions to yellow since we have a pedestrian waiting. After the
// next timeout, we end up in "red".
vehicleSignal.pedestrianWaiting();
// Once the FSM is in the "red" state, we can reset it to "green" by calling:
vehicleSignal.reset();

Though the code comments give you a lot of detail, let's break down what's happening in the above FSM:

  • When you are creating an FSM, the constructor takes one argument, the options arg - which is an object that contains (at least) the states & initialState values for your FSM, as well as an optional initialize method (which is invoked at the end of the underlying constructor function) and any additional properties or methods you want on the FSM.
  • It can exist in one of five possible states: uninitialized, green, green-interruptible, yellow and red. (Only one state can be active at a time.)
  • The states themselves are objects under the states property on the FSM, and contain handlers whose names match the input types that the FSM accepts while in that state.
  • It starts in the uninitialized state.
  • It accepts input either by calling handle directly and passing the input type as a string (plus any arguments), or by calling top level methods you put on your FSM's prototype that wrap the calls to handle with a more expressive API.
  • You do not assign the state value of the FSM directly, instead, you use transition(stateName) to transition to a different state.
  • Special "input handlers" exist in machina: _onEnter, _onExit and *. In fact, the very first state (uninitialized) in this FSM is using *. It's the "catch-all" handler which, if provided, will match any input in that state that's not explicitly matched by name. In this case, any input handled in uninitialized will cause the FSM to defer the input (queue it up for replay after transitioning), and immediately transfer to green. (This is just to demonstrate how a start-up-only state can automatically transfer into active state(s) as clients begin using the FSM. )

Note - input handlers can return values. Just be aware that this is not reliable in hierarchical FSMs.

Going Further

machina provides two constructor functions for creating an FSM: machina.Fsm and machina.BehavioralFsm:

The BehavioralFsm Constructor

BehavioralFsm is new to machina as of v1.0 (though the Fsm constructor now inherits from it). The BehavioralFsm constructor lets you create an FSM that defines behavior (hence the name) that you want applied to multiple, separate instances of state. A BehavioralFsm instance does not (should not!) track state locally, on itself. For example, consider this scenario....where we get to twist our vehicleSignal FSM beyond reason: ๐Ÿ˜„

var vehicleSignal = new machina.BehavioralFsm( {

    initialize: function( options ) {
        // your setup code goes here...
    },

    namespace: "vehicle-signal",

    initialState: "uninitialized",

    states: {
        uninitialized: {
            "*": function( client ) {
                this.deferUntilTransition( client );
                this.transition( client, "green" );
            }
        },
        green: {
            _onEnter: function( client ) {
                client.timer = setTimeout( function() {
                    this.handle(  client, "timeout" );
                }.bind( this ), 30000 );
                this.emit( "vehicles", { client: client, status: GREEN } );
            },
            timeout: "green-interruptible",
            pedestrianWaiting: function( client ) {
                this.deferUntilTransition(  client, "green-interruptible" );
            },
            _onExit: function( client ) {
                clearTimeout( client.timer );
            }
        },
        "green-interruptible": {
            pedestrianWaiting: "yellow"
        },
        yellow: {
            _onEnter: function( client ) {
                client.timer = setTimeout( function() {
                    this.handle( client, "timeout" );
                }.bind( this ), 5000 );
                this.emit( "vehicles", { client: client, status: YELLOW } );
            },
            timeout: "red",
            _onExit: function( client ) {
                clearTimeout( client.timer );
            }
        },
        red: {
            _onEnter: function( client ) {
                client.timer = setTimeout( function() {
                    this.handle( client, "timeout" );
                }.bind( this ), 1000 );
            },
            _reset: "green",
            _onExit: function( client ) {
                clearTimeout( client.timer );
            }
        }
    },

    reset: function( client ) {
        this.handle(  client, "_reset" );
    },

    pedestrianWaiting: function( client ) {
        this.handle( client, "pedestrianWaiting" );
    }
} );

// Now we can have multiple 'instances' of traffic lights that all share the same FSM:
var light1 = { location: "Dijsktra Ave & Hunt Blvd", direction: "north-south" };
var light2 = { location: "Dijsktra Ave & Hunt Blvd", direction: "east-west" };

// to use the behavioral fsm, we pass the "client" in as the first arg to API calls:
vehicleSignal.pedestrianWaiting( light1 );

// Now let's signal a pedestrian waiting at light2
vehicleSignal.pedestrianWaiting( light2 );

// if you were to inspect light1 and light2, you'd see they both have
// a __machina__ property, which contains metadata related to this FSM.
// For example, light1.__machina__.vehicleSignal.state might be "green"
// and light2.__machina__.vehicleSignal.state might be "yellow" (depending
// on when you check). The point is - the "clients' state" is tracked
// separately from each other, and from the FSM. Here's a snapshot of
// light1 right after the vehicleSignal.pedestrianWaiting( light1 ) call:

{
  "location": "Dijsktra Ave & Hunt Blvd",
  "direction": "north-south",
  "__machina__": {
    "vehicle-signal": {
      "inputQueue": [
        {
          "type": "transition",
          "untilState": "green-interruptible",
          "args": [
            {
              "inputType": "pedestrianWaiting",
              "delegated": false
            }
          ]
        }
      ],
      "targetReplayState": "green",
      "state": "green",
      "priorState": "uninitialized",
      "priorAction": "",
      "currentAction": "",
      "currentActionArgs": [
        {
          "inputType": "pedestrianWaiting",
          "delegated": false
        }
      ],
      "inExitHandler": false
    }
  },
  "timer": 11
}

Though we're using the same FSM for behavior, the state is tracked separately. This enables you to keep a smaller memory footprint, especially in situations where you'd otherwise have lots of individual instances of the same FSM in play. More importantly, though, it allows you to take a more functional approach to FSM behavior and state should you prefer to do so. (As a side note, it also makes it much simpler to store a client's state and re-load it later and have the FSM pick up where it left off, etc.)

The Fsm Constructor

If you've used machina prior to v1.0, the Fsm constructor is what you're familiar with. It's functionally equivalent to the BehavioralFsm (in fact, it inherits from it), except that it can only deal with one client: itself. There's no need to pass a client argument to the API calls on an Fsm instance, since it only acts on itself. All of the metadata that was stamped on our light1 and light2 clients above (under the __machina__ property) is at the instance level on an Fsm (as it has been historically for this constructor).

Wait - What's This About Inheritance?

machina's FSM constructor functions are simple to extend. If you don't need an instance, but just want a modified constructor function to use later to create instances, you can do something like this:

var TrafficLightFsm = machina.Fsm.extend({ /* your options */ });

// later/elsewhere in your code:
var trafficLight = new TrafficLightFsm();

// you can also override any of the options:
var anotherLight = new TrafficLightFsm({ initialState: "go" });

The extend method works similar to other frameworks (like Backbone, for example). The primary difference is this: the states object will be deep merged across the prototype chain into an instance-level states property (so it doesn't mutate the prototype chain). This means you can add new states as well as add new (or override existing) handlers to existing states as you inherit from "parent" FSMs. This can be very useful, but โ€“ as with all things inheritance-related โ€“ use with caution!

And You Mentioned Events?

machina FSMs are event emitters, and subscribing to them is pretty easy:

// I'd like to know when the transition event occurs
trafficLight.on("transition", function (data){
    console.log("we just transitioned from " + data.fromState + " to " + data.toState);
});

// Or, maybe I want to know when ANY event occurs
trafficLight.on("*", function (eventName, data){
    console.log("this thing happened:", eventName);
});

Unsubscribing can be done a couple of ways:

//each listener gets a return value
var sub = trafficLight.on("transition", someCallback);
sub.off(); // unsubscribes the handler

// OR, we can use the FSM's prototype method -
// remove this specific subscription:
trafficLight.off("transition", someCallback);
// remove all transition subscribers
trafficLight.off("transition");
// remove ALL subscribers, period:
trafficLight.off();

You can emit your own custom events in addition to the built-in events machina emits. To read more about these events, see the wiki.

Things Suddenly Got Hierarchical!

One of the most exciting additions in v1.0: machina now supports hierarchical state machines. Remember our earlier example of the vehicleSignal FSM? Well, that's only part of a pedestrian crosswalk. Pedestrians need their own signal as well - typically a sign that signals "Walk" and "Do Not Walk". Let's peek at what an FSM for this might look like:

var pedestrianSignal = new machina.Fsm( {
    namespace: "pedestrian-signal",
    initialState: "uninitialized",
    reset: function() {
        this.transition( "walking" );
    },
    states: {
        uninitialized: {
            "*": function() {
                this.deferUntilTransition();
                this.transition( "walking" );
            }
        },
        walking: {
            _onEnter: function() {
                this.timer = setTimeout( function() {
                    this.handle( "timeout" );
                }.bind( this ), 30000 );
                this.emit( "pedestrians", { status: WALK } );
            },
            timeout: "flashing",
            _onExit: function() {
                clearTimeout( this.timer );
            }
        },
        flashing: {
            _onEnter: function() {
                this.timer = setTimeout( function() {
                    this.handle( "timeout" );
                }.bind( this ), 5000 );
                this.emit( "pedestrians", { status: DO_NOT_WALK, flashing: true } );
            },
            timeout: "dontwalk",
            _onExit: function() {
                clearTimeout( this.timer );
            }
        },
        dontwalk: {
            _onEnter: function() {
                this.timer = setTimeout( function() {
                    this.handle( "timeout" );
                }.bind( this ), 1000 );
            },
            _reset: "walking",
            _onExit: function() {
                clearTimeout( this.timer );
            }
        }
    }
} )

In many ways, our pedestrianSignal is similar to the vehicleSignal FSM:

  • It starts in the uninitialized state, and the first input causes it to transition to walking before actually processing the input.
  • It can only be in one of four states: uninitialized, walking, flashing and dontwalk.
  • This FSM's input is primarily internally-executed, based on timers (setTimeout calls).

Now - we could stand up an instance of pedestrianSignal and vehicleSignal, and subscribe them to each other's transition events. This would make them "siblings" - where pedestrianSignal could, for example, only transition to walking when vehicleSignal is in the red state, etc. While there are scenarios where this sort of "sibling" approach is useful, what we really have is a hierarchy. There are two higher level states that each FSM represents, a "vehicles-can-cross" state and a "pedestrians-can-cross" state. With machina v1.0, we can create an FSM to model these higher states, and attach our pedestrianSignal and vehicleSignal FSMs to their parent states:

var crosswalk = new machina.Fsm( {
    namespace: "crosswalk",
    initialState: "vehiclesEnabled",
    states: {
        vehiclesEnabled: {
            _child: vehicleSignal,
            _onEnter: function() {
                this.emit( "pedestrians", { status: DO_NOT_WALK } );
            },
            timeout: "pedestriansEnabled"
        },
        pedestriansEnabled: {
            _child: pedestrianSignal,
            _onEnter: function() {
                this.emit( "vehicles", { status: RED } );
            },
            timeout: "vehiclesEnabled"
        }
    }
} );

Notice how each state has a _child property? This property can be used to assign an FSM instance to act as a child FSM for this parent state (or a factory function that produces an instance to be used, etc.). Here's how it works:

  • When an FSM is handling input, it attempts to let the child FSM handle it first. If the child emits a nohandler event, the parent FSM will take over and attempt to handle it. For example - if a pedestrianWaiting input is fed to the above FSM while in the vehiclesEnabled state, it will be passed on to the vehicleSignal FSM to be handled there.
  • Events emitted from the child FSM are bubbled up to be emitted by the top level parent (except for the nohandler event).
  • If a child FSM handles input that it does not have a handler for, it will bubble the input up to the parent FSM to be handled there. Did you notice that both our pedestrianSignal and vehicleSignal FSMs queue up a timeout input in the dontwalk and red states, respectively? However, neither of those FSMs have an input handler for timeout in those states. When these FSMs become part of the hierarchy above, as children of the crosswalk FSM, the timeout input will bubble up to the parent FSM to be handled, where there are handlers for it.
  • When the parent FSM transitions to a new state, any child FSM from a previous state is ignored entirely (i.e. - events emitted, or input bubbled, will not be handled in the parent). If the parent FSM transitions back to that state, it will resume listening to the child FSM, etc.
  • As the parent state transitions into any of its states, it will tell the child FSM to handle a _reset input. This gives you a hook to move the child FSM to the correct state before handling any further input. For example, you'll notice our pedestrianSignal FSM has a _reset input handler in the dontwalk state, which transitions the FSM to the walking state.

In v1.1.0, machina added the compositeState() method to the BehavioralFsm and Fsm prototypes. This means you can get the current state of the FSM hierarchy. For example:

// calling compositeState on Fsm instances
console.log( crosswalk.compositeState() ); // vehiclesEnabled.green

// calling compositeState on BehavioralFsm instances
// (you have to pass the client arg)
console.log( crosswalk.compositeState( fsmClient ) ); // pedestriansEnabled.walking

Caveats: This feature is very new to machina, so expect it to evolve a bit. I plan to fine-tune how events bubble in a hierarchy a bit more.

The Top Level machina object

The top level machina object has the following members:

  • Fsm - the constructor function used to create FSMs.
  • BehavioralFsm โ€“ the constructor function used to create BehavioralFSM instances.
  • utils - contains helper functions that can be overridden to change default behavior(s) in machina:
    • makeFsmNamespace - function that provides a default "channel" or "exchange" for an FSM instance. (e.g. - fsm.0, fsm.1, etc.)
  • on - method used to subscribe a callback to top-level machina events (currently the only event published at this level is newFsm)
  • off - method used to unsubscribe a callback to top-level machina events.
  • emit - top-level method used to emit events.
  • eventListeners - an object literal containing the susbcribers to any top-level events.

Build, Tests & Examples

machina.js uses gulp.js to build.

  • Install node.js (and consider using nvm to manage your node versions)
  • run npm install & bower install to install all dependencies
  • To build, run npm run build - then check the lib folder for the output
  • To run the examples:
  • To run tests & examples:
    • To run node-based tests: npm run test
    • To run istanbul (code test coverage): npm run coverage
    • To see a browser-based istanbul report: npm run show-coverage

Release Notes

Go here to see the changelog.

Have More Questions?

Read the wiki and the source โ€“ you might find your answer and more! Check out the issue opened by @burin - a great example of how to use github issues to ask questions, provide sample code, etc. I only ask that if you open an issue, that it be focused on a specific problem or bug (not wide-open ambiguity, please).

What's Ahead?

Wow, a lot has changed in the JavaScript world since machina was first written in 2012. I plan to bring some of the amazing tools that have arisen to bear on machina, first to update the dev dependencies and then to improve the code base. In no particular order of intent:

  • Add initial typescript definitions based on the latest version 4 (likely to ship as v5)
  • A ground-up rewrite in Typescript (in progress - this is slated for v6, and will likely involve breaking changes)
  • Convert tests to Jest
  • Convert build to use Typescript compiler
  • Retire the ancient examples (along with usage of bower & component, etc.)
  • Add a React and Vue example (likely based on the hierarchical FSM concepts)
  • Improve/Simplify Hierarchical FSM usage
  • Add a couple of additional formal FSM instance types

machina.js's People

Contributors

ifandelse avatar dcneiner avatar rniemeyer avatar aikeda avatar bradfol avatar joelpurra avatar mtrimpe avatar alanmoo avatar arobson avatar andmis avatar callumlocke avatar eddiezane avatar fwg avatar igncp avatar codelica avatar steve384 avatar heckj avatar supremebeing7 avatar timcharper avatar tristanls avatar bmavity avatar

Stargazers

Pete Warnock avatar kkkkkaiqi avatar Anne Thorpe avatar cncjb avatar Jordi Rivero avatar Andrea Bรธe Abrahamsen avatar jacklong avatar Giorgio Lucca avatar fedix avatar Liqiu Huang avatar Steven Myers avatar gavin avatar Quang Van avatar Prawee Wongsa avatar  avatar Angelo B. J. Luidens avatar  avatar AuxCoder avatar Wildan Zulfikar avatar XingChao avatar Richard Scoop avatar Tomas Polach avatar Harald Wartig avatar  avatar ะ’ะปะฐะดะธะผะธั€ ะจะปะตะตะฒ avatar guog avatar Vimlee avatar  avatar abjutus avatar  avatar Kennan Chan avatar Alexander Matveev avatar  avatar monkeytao avatar Christian Rotzoll avatar Micah avatar Rocco Musolino avatar Mark Vayngrib avatar Haitao Lee avatar Dimitris Vichos avatar  avatar Jason Chao avatar  avatar linzhi avatar Giovanni Delgado avatar Dapeng Gong avatar stanhua avatar  avatar David Miguel Lozano avatar Andrii Oriekhov avatar Yasin ATEลž avatar  avatar Saad Shahd avatar Nishant Tilve avatar  avatar Eduardo Sigrist Ciciliato avatar cristianminicaionut avatar Teerapat Prommarak avatar Brian Solon avatar David Oliveira avatar Roy Yair avatar Matt Morris avatar Doppo avatar Christian avatar Lei Zhen avatar Cody avatar  avatar  avatar  avatar Mr. Rosario avatar  avatar Joseph Cheng avatar ้ฉฌไผ ไฝณ avatar  avatar Nachiket Patel avatar  avatar Arttu Hanska avatar  avatar  avatar  avatar Mizanur Rahman avatar James Mulholland avatar Vish Desai avatar Greg Wolanski avatar  avatar Darwin Gautalius avatar Hadrien TOMA avatar Mark Vital avatar Fehmi ร–zรผseven avatar libz avatar ๆ™“่ท avatar Ehab Alsharif avatar  avatar Danny Brian avatar Mat Warger avatar  avatar Mathew Hawley avatar Tom Tobias avatar Bob Hwang avatar Yuichiro Katsume avatar

Watchers

Tom McKelvey avatar Samuel Fortier-Galarneau avatar jason franklin-stokes avatar Art Green avatar Facundo Cabrera avatar Aurรฉlio A. Heckert avatar edwardt avatar Brad Jones avatar Jeff Zerger avatar Miguel avatar Neustradamus avatar Nuba Princigalli avatar davidwei_001 avatar Dave Lee avatar  avatar  avatar yury avatar send2vinnie avatar Mr. Rosario avatar Marcus Dalgren avatar Richard Hess avatar roadlabs avatar  avatar Jason Belich avatar Pierre Laveklint avatar Giorgio Tedesco avatar Howard Mill avatar  avatar Michael Knell avatar leon avatar Jeroen Zuijderhoudt avatar James Cloos avatar Josh Harbaugh avatar Christian Burgas avatar hunslater avatar Aaron Buchanan avatar Joe Hosteny avatar  avatar Stephan avatar SatanWoo avatar Michael Anthony avatar  avatar Thiago F avatar Manjunath K G avatar  avatar Xianliang Wu avatar MAHIZ avatar Dmitry Myadzelets avatar  avatar ArtyProg avatar  avatar Andrรฉ Abadesso avatar  avatar Maximilian Spogis avatar Gรถkhan Akgรผl avatar Brad Murry avatar Hermes avatar Marius Podean avatar Tom Mielke avatar  avatar Chatchai Daecha avatar ๅŽๅพท็ฆน avatar Salman avatar  avatar babaca avatar JL Wang avatar Dat Pham avatar hbcode avatar  avatar Zach Lintz avatar  avatar John avatar Ruslan Bogun avatar Gurdev avatar  avatar nitish avatar Natallia avatar

machina.js's Issues

Be able to export state machine diagram

Hi Author,

The library is in pretty good shape, I would like to use it in our project. My question is that is there any plan to support exporting state machine diagram?

The state machine diagram is very useful in documentation and also be able to communicate with non-technical people.

Errors Updating machina to latest v1.0.0-1

var underscore = require('underscore');
var machina = require('machina')(underscore); // 1.  Complains here undefined underscore

var DeviceFsm = machina.Fsm.extend({  // Removed (underscore) problem 1 and it still complains TypeError: Cannot call method 'extend' of undefined
});

Any suggestions of how to fix this would be very appreciated. Thanks!

Bug: Must allow duplicate event names across different states

I believe I found a bug.

Consider the following code snippet:

'month': {
    _onEnter: function () {
        _self.currentView(this.state);
        if (this.priorState !== _self.kInitialized) {
            _self.publishZoom(_self.kIn);
        }
        this.publishViewChanged();
    },
    '/view/change/months': function (selection) {
        //When transitioning to the months state, we need
        //to set both the month and the year since both are
        //relevant in the months state
        this.setSelectedMonth(selection.selection.month());
        this.setSelectedYear(selection.selection.year());
        this.transition(_self.kMonths);
    },
    '/view/change/years': function (selection) {
        this.setSelectedYear(selection.selection.year());
        this.transition(_self.kYears);
    }
},
'months': {
    _onEnter: function () {
        _self.currentView(this.state);
        _self.publishZoom(this.priorState === _self.kYears
            ? _self.kIn
            : _self.kOut);
        this.publishViewChanged();
    },
    '/view/change/month': function (selection) {
        this.setSelectedMonth(selection.selection.month());
        this.transition(_self.kMonth);
    },
    '/view/change/years': function (selection) {
        this.setSelectedYear(selection.selection.year());
        this.transition(_self.kYears);
    }
},
'years': {
    _onEnter: function () {
        _self.currentView(this.state);
        _self.publishZoom(_self.kOut);
        this.publishViewChanged();
    },
    '/view/change/months': function (selection) {
        this.setSelectedYear(selection.selection.year());
        this.transition(_self.kMonths);
    }
}

Notice that certain events repeat, but under different states. The "/view/change/years" event and the "/view/change/months" event repeat, and those are firing twice, regardless of which state the FSM is in.

It seems to me that, to allow for repeats like this, you would need to create a dictionary of states internally that maps a unique identifier to the event. For example:

"year:/view/change/months" to "/view/change/months" in the year state
"month:/view/change/months" to "/view/change/months in the the month state

In other words, the event emitted should be formed from the name of the state as well as from the name of the event, and then mapped appropriately.

Log exception trace instead of string

Hi there,

I bring one suggestion to the table. I am working with your library and when an exception appears, only the string of the exception is logged (src/fsm.js, line 21). In our case and for development, showing the entire trace would make our life easier, because it would show the file and line number. For example, in line 21 of fsm.js:

console.log(exception.stack);

Opinions?

TypeError: _ is undefined

Hi,
I try to use machina.js 0-4-1.
I encounter a type_error "_ is undefined" in line 58, in Firefox or Chrome (Windows) when I try to include the machina.js file in a page (via tag <script src="machina.js"...)

A made a test with a page that only try to load machina.js.

I copied the machina.js and the machina.min.js from your "lib" folder. I am certainly doing something wrong, perhaps machina needs another library ? could you help ?
Thanks.

Separation of construction and configuration

How do we go about configuring the FSM after its construction? For example, I would like to simply construct an FSM in my constructor:

this.stateMachine = new machina.Fsm()

but then configure it elsewhere (I'm using Durandal, so I would do that in the #activate handler).

Thank you.

Optionally pass an array of handlers

In the current version if one has multiple handlers for an onExit or onEnter they have to be called like so

_onExit : function() {
  handler1();
  handler2();
  //....
  handlerN();
}

It would be a nice bit of syntactic sugar to allow the onExit and onEnter properties to be either a function or an array of functions

_onExit : [handler1, handler2, handler3]

Errors in unit tests

I can't get the unit tests to pass.

I cloned the repository, ran npm install, and ran anvil. I get the following error:

Uglifying 1 files
starting activity, push
    plugin: 'anvil.headers'
    plugin: 'anvil.output'
starting activity, test
    plugin: 'anvil.testem'
 error running activity test : Error: bindAll must be passed function names
Error: bindAll must be passed function names

This is with the latest version of Underscore installed, 1.5.2. I then install the lowest version of Underscore allowed by package.json, 1.1.7, and run anvil again. The same test fails.

It seems like it could conceivably be an issue with anvil.testem, but I don't know too much about anvil, or testem. Any thoughts?

using machina as a state machine for multiple entities

I like the structure of the code that is written with machina. I'd like to use it for a light document workflow. The only problem is that obviously each document in the system will have its own state. I can't see in the API a way to "restore" an instance of a FSM with the current state of a document on a document CRUD event and then let the state machine determine what to do next. The API documentation says not to assign the "state" property directly. Is there a way to essentially restore state to a FSM for a moment when a Document is modified by assigning its status field as the state of the FSM, call handle() on the FSM, and then restore it to the next Document's status that comes through, and call handle() on it again - this time in the context of the next Document's status field?

Other options I'm considering are - create the Fsm from scratch on every CRUD event, and dynamically make the initialState be the current state of the Document, but that would seem pretty heavy. The other thought is to just call transition() artificially to set the state. But since this is being done as a means to artificially "reset" the state on the FSM, it would mean that _onEnter() could not be easily used because it would be hard to tell the difference between an actual state transition as the result of an event, and just a restore.

Any guidance is appreciated.

Support for asynchronous actions

I would love to have support for asynchronous state actions such that the handled event would not fire until the action was really complete. I could envision a second parameter on the action that could be called back when the action was complete. Here is an example of how it might look with a promise and a callback:

states : {
    "online" : {
        _onEnter: function() {
            this.handle("sync.customer");
        },

        // Using Promises
        "save.customer.promise" : function( payload, done ) {
            if( this.verifyState() ) {
                storage.saveToRemote( payload )
                    .then(done);
            }
        },

        // Using Callbacks
        "save.customer.callback" : function( payload, done ) {
            if( this.verifyState() ) {
                storage.saveToRemote( payload, function( err, data ) {
                    done();
                });
            }
        }
    }
}

Remove Factory Function in CommonJS Module Export

At this point, there's not really a need to return a factory function from the CommonJS side of things - it should just return machina itself, and require underscore internally. This will be a breaking API change though, so it will be included in the next big release.

v0.3.7 change breaks my implementation (and I don't think it has to)

In the v0.3.7 changelog, you referenced a bug with instance members sharing state objects and changes to 1 affecting the other. The fix applied there breaks my code, but I don't think it needs to.

It turns out that the deep-extend used incorrectly clones an object I'm passing in. (as a result, it completely breaks all my instances) The object I'm talking about is an instance of a constructor function, and it has a couple mixins applied as well. (which is where the break in the chain happens for some reason)

I think the deep extend should only be performed on properties known to machina. (eg: states)

machina and require.js doesn't work correct

If i try the example:

var underscore = require('underscore');
var machina = require('machina')(underscore);
var MyFsm = machina.Fsm.extend({
    //your code
});

i get follow js error:

Uncaught Error: Module name "machina" has not been loaded yet for context: _. Use require([])

Handling 'back' events

I've been using machina.js to model workflows and it's been working great so far. One thing I support is the ability for a user to go back in the workflow.

In any given state, a user can "save" their progress, transitioning to the next state, or go "back", which will take them back to the previous state.

Currently, each state has an explicit handler for "back", which calls transition() with the appropriate name of the expected action to go back to. Unfortunately, this only allows for "linear" progress, and conditional transitions are a little tricky to deal with.

Here is a stripped down example of a workflow I have created for adding a flight to an itinerary.

define([
    'machina', 'mediator'
], function(
    machina, mediator
) {
var AddFlightWorkflow = window.workflow = new machina.Fsm({
    initialState: 'uninitialized',

    states: {
        uninitialized: {
            initialize: function() {
                this.transition( 'showingForm' );
            }
        },

        // gathering user input
        showingForm: {
            _onEnter: function() {
                // show flights:add view,
            },
            selectDate: function() {
                this.transition( 'selectingDate' );
            },
            selectCarrier: function() {
                this.transition( 'selectingCarrier' );
            },
            save: function() {
                this.transition( 'selectingOriginDestinationAirports' );
            },
            back: function() {
                // kill this workflow
            }
        },

        selectingDate: {
            _onEnter: function() {
                // show calendar widget
            },
            save: function() {
                // save the new user date selection to the model
                // kill the widget
                // transition back
                this.transition( 'showingForm' );
            },
            back: function() {
                // kill the view/widget
                this.transition( 'showingForm' );
            }
        },

        selectingCarrier: {
            _onEnter: function() {
                // show carrier selection widget
            },
            save: function () {
                // save the user selected carrier
                // kill the widget
                // transition back
                this.transition( 'showingForm' );
            },
            back: function() {
                // kill the view/widget
                this.transition( 'showingForm' );
            }
        },

        // after submitting user data to svc (flight paths)
        selectingOriginDestinationAirports: {
            _onEnter: function() {
                // if the response had more than one option, show view
                // otherwise transition to confirmingFlight
            },
            save: function() {
                this.transition( 'confirmingFlight' );
            },
            back: function() {
                // transition to the last thing
                this.transition( 'showingForm' );
            }
        },

        // prior to sending data to svc (monitor flights)
        confirmingFlight: {
            _onEnter: function() {
                // show confirmation view
            },
            save: function() {
                // finally! we can save all the data
                this.transition( 'uninitialized' );
            },
            back: function() {
                // transition to the last thing
                this.transition( 'selectingOriginDestinationAirports' );
            }

        }
    }
});

mediator.subscribe( 'add_flight_workflow:initialize', function() {
    AddFlightWorkflow.handle('initialize');
});

mediator.subscribe( 'add_flight_workflow:save', function() {
    AddFlightWorkflow.handle('save');
});

mediator.subscribe( 'add_flight_workflow:back', function() {
    AddFlightWorkflow.handle('back');
});

mediator.subscribe( 'add_flight_workflow:selectDate', function() {
    AddFlightWorkflow.handle('selectDate');
});

mediator.subscribe( 'add_flight_workflow:selectCarrier', function() {
    AddFlightWorkflow.handle('selectCarrier');
});

return AddFlightWorkflow;
});

My question is whether this is a good approach to tackle this problem or if there is some alternative. On one hand, it's good because it's very explicit. On the other, it doesn't seem very DRY and I'm not sure how to go "back" if a particular state has two or more possible entry points (confirmingFlight can be transitioned to from showingForm in one case, or selectingOriginDestinationAirports in another)

I tried looking through the machina.js code to see if there was a "previous state" of some sort, but I didn't want to dig into something that could possibly change in the future (private API or such).

Any tips would be appreciated! And thanks for publishing your blog post about FSMs and creating a super clean library with a nice API!

Documentation Request

Could you make it explicit which libraries Machina relies on, especially for those of us who don't use bower?

catch all (*) handler passing in a different payload

Being fairly new to this library I'm not sure if this behavior is intended or a bug. If I call a handler that is explicitly defined the payload is different than if I were to define a catch all (*) handler.

var rootFsm = new machina.Fsm({
    states: {
        "example1": {
            "foobar": function (payload) {
                console.log(payload);  // logs the object I send in
            }
        },
        "example2": {
            "*": function (payload) {
                console.log(payload);  // logs the name of the handler & not my object
            }
        }
    }
});

Exceeding stack size by passing fsm object in another fsm constructor

Hi!

I'm trying to implement master-slave system of state machines in node.js. The reference to master SM is passed to slave SM constructor as first argument. This causes stack size. Somehow, instead of calling slave SM initialize function, master SM function is called, causing recursion.

Is it bug, or I misunderstood something?

var underscore = require('underscore');
var machina = require('machina')(underscore);

var Master = machina.Fsm.extend({
    initialState : 'CLOSED',
    initialize : function(){
        this.slave = new Slave(this);
    },
    states : {
        CLOSED : {
            open : function(){
                this.transition('READY');
            },
        },
        READY : {
        },
    },
});

var Slave = machina.Fsm.extend({
    initialState : 'CLOSED',
    initialize : function(master){
        this.master = master;
    },
    states : {
        CLOSED : {
            open : function(){
                this.transition('READY');
            },
        },
        READY : {
        },
    },
});

var master = new Master();

machine.transition('mystate',args...)

I frequently find my machines requiring transient properties to hold arguments from input handlers while myMachine.transition('toTheState') because .transition doesnt accept arguments and marshall them thru to the _onEnter of the next state.

It is trivial to pass along arguments from .transition into the _onEnter of the destination state, but before I submit a PR and such I wanted to make sure this isn't an intentional design decision.

This would let you do

var Machine= machina.Fsm.extend({
    initialState: 'bar'
    ,states: {
        bar: {
            foo: function(args) {
                   this.transition('foo',args);
        }
        ,foo: {
            _onEnter: function(args) {
                //args === {please: 'includeme'}
                 this.thing = new MyThings(args)
            }
        }
    }
}

var machine = new Machine()
machine.handle('foo',{please:'includeme'})

Upgrade/fix Underscore dependencies

According to bower.json, Machina depends on a 3 year old version of Underscore. It should be updated to support the latest version. I can help out / make a PR if you'd like.

Also, the dependencies in the package.json don't make any sense:

"dependencies" : {
  "underscore" : ">=1.1.7"
},

Anything greater than 1.1.7 doesn't guard against non-backwards compatible changes to Underscore. In any future event there could be a complete rewrite of _.each with brand new syntax. If that happened, you wouldn't even be able to install a working copy of machina anymore! This should probably be ~1.1.7.

>= is also used for node in package.json.

Updates

It seems like there are more problems, too. This is registered as a bower package, but seems to ignore the benefits of using bower. Why is there an ext folder? Those shouldn't be included in the repo; they should be loaded as bower components. I can help to restructure this repository if you'd like to get the dependencies sorted out.

Multiple handler catch

Is it possible to have a handler that matches more than one state? Example, if I want a handler that matches "red" or "blue", and another that matches "yellow" or "green"

states: {
myState:
"red blue" : function() { .. red blue.. }
"yellow green" : function() { ... yellow green }

initialState lost in a chain of extends

Using v0.3.7, I've created a state machine that I use as a base for some other very similar state machines. When I extend from that "base" unit to create my "sub" FSM instances, they lose their initialState property, and simply default to "uninitialized". On top of that, they do not transition upon initialization, breaking my implementation.

A code example:

var Base = machina.Fsm.extend({
    initialState: "hidden"
});

var Fsm1 = Base.extend({ ... });

var fsm1 = new Fsm1({ ... });
// fsm1.initialState = "uninitialized" (instead of "hidden")
// fsm1.state = undefined (instead of "hidden")

Suggestions for the best way to provide persistence to the state of the FSM?

My team was discussing the best way to provide persistence to an FSM extended from machina.js in the case where the process running the javascript code is killed and/or lost and restarted.

Do you have any suggestions for approaches or best places to "hook" into the FSM so that we can update some local persistence with it's state as it moves, and load in relevant state on startup if need be?

Our current thoughts are to write out some state identifier using the onEnter: function() { ... } and then setting initialState variable on the FSM from that persistence (if it exists) when we start up the FSM in the javascript loading code.

This Workflow possible

I will want to create a work flow something like this

         Root
           |
         Start
      /         \
   Node0       Node1
     \          /
    Node2     Node3
      \        /
         Node4

Is this possible with this API. If yes how can thanks

Feature request: strings in states hash & specify target object

This is actually two separate feature requests that I'm using on my own projects in conjunction with one another.

The first request is the ability to specify function names in the events hash, rather than actual functions. Example:

// Presently:
states: {
  stateOne: {
    "some.event": this.someCallback
  }
}

// This feature would allow you to write that as this instead
states: {
  stateOne: {
    "some.event": "someCallback"
  }
}

This allows you to specify functions that might not exist at the time that you're setting up the states object.

Then, when the FSM is started, it would search the fsm for those methods and replace the function name with the function reference itself. See an example of something like this here.

The second feature allows you to pass in an object that it searches instead of the FSM. This allows you to separate out the states from the callbacks in more complex applications. To explain further, in the above example, the string "someCallback" must be found and replaced with the method on some object. By default, that object would be the FSM. But you could pass in another object that it would search for a method named "someCallback."

If this is something you'd be interested in adding I can write up a PR implementing it.

Anyone having issue loading machina with RequireJS 1.0.5 ?

I've this... Just load underscope, backbone and then machina in order... and copied and paste the example,,
Like this..

require.config({
    baseUrl:'.',
    paths: {
        underscore:     'underscore-min',
        backbone:       'backbone-min',
        machina:        'machina',
        order:          'order'
    }
});

define(['order!underscore', 'order!backbone', 'order!machina'], function () {
     // tried to exe _.* and it works in here...

    var storageFsm = new machina.Fsm({
        applicationOffline: function() {
            var offline = true;
            // checks window.navigator.online and more, sets the offline value
            return offline;
        },

...

It gives me error..

_ is null
[Break On This Error]   

opt = _.extend(defaults , options || {});
machina.js (line 66)

I did verified that underscope is loaded by executing a underscope method before creating the FSM.. Would that be possible that the internal loading of machina is not working??
I use the browser version ...

Event for steady state?

Does it make sense to have an event emitted when the FSM has finished a "round" of handlers? Most of my fsm.handle() calls will cause an emit('doSomething'), but I have some handlers that are waiting for a few parallel processes to complete. In those cases, a handle() call may not generate an emit().

Outside of my FSM, I need to be able to detect that the FSM is done with its work even though there have been no emit() calls. This is when an event could be emitted to let listeners know the cycle has completed. I could see this being accomplished by adding something like this to the end of the processQueue() function:

processQueue : function ( type ) {
    // ...
    _.each( toProcess, function ( item ) {
        this.handle.apply( this, item.args );
    }, this );

    this.emit.call( this, CYCLE_END );
}

I have not used the deferUntilXXX handlers, so I am not sure if this approach will suffice. I imagine that the process of handling deferred events could actually cause another queue of deferred events which will have to be subsequently handled, but I am not sure this is the case.

I think my use case is a bit different than most as my FSM instance is extremely short lived. I am using the FSM to process workflow execution history from Amazon SWF services. My FSM is initialized, fed the event history, handle('next'), then destroyed. So, under these conditions, knowing when the FSM has completed processing all events would be beneficial.

Allow handlers to return values?

Hello,

Excellent work! I've been using machina as part of a small node module to abstract/simplify interfacing to some older SOAP/WSDL based services we have to work with. Hoping to program away the pain into something tolerable for us to use. :)

Everything has been working well, except one snag I haven't been able to reason around. Basically I'd like the handlers (machina handle() calls) to be able to return values in order to simplify things like conditional followup steps/transitions (outside the handler) or returning promises for chaining.

This is the quick change I've been using so far:
commit removed - see below will update with proper pull request.

So, I guess my questions are just this.

  1. Does this seem like a reasonable addition, or am I a better way or potential problem?
  2. If it does seem reasonable, would you consider the change?

Thanks in advance...

-James

Minor doc clarification with _onExit handler

Hi, I ran into a bit of confusion regarding the value of Fsm.state during an _onExit handler. The doc at http://machina-js.org/ lead me to believe the state of Fsm fields wouldn't change until after it completed:

_onExit is fired immediately before transitioning to a new state.

Upon inspection I noticed that current/prior state references are actually updated immediately prior to calling _onExit.

If that's the intended behavior, a quick edit to clarify the state of fields during _onEnter and _onExit would be helpful.

Great library by the way, has worked great for my purposes so far!

Pass source state to _onEnter

Hi,

It would be nice to know which state I'm coming from in the _onEnter method.

Specific use case: I'm modeling my UI page flow as a state machine. The page transition should depend on whether you're going back of forward through the fsm.

Thanks.

Preserve stack trace for exceptions and perhaps use console.error

I can issue a PR if you want this. It's really a drag to lose our stack trace and would be nice to write to console.error if available:

if ( console  ) {
    var logError = (console.error || console.log).bind(console)
    logError( exception.toString(),exception.stack  );
}

Crazy thought: could (all) Business Logic be defined as a FNM?

I'm pretty sure the answer is yes (turing complete and all that), but I'm thinking about a way to lets domain experts model business logic by actually declaratively defining a FNM in Machina to model their Business Logic.

In other words, the states and transitions should be meaningful to the domain expert (not too technical/ finely grained) while transitions should be still expressible through some kind of DSL.

I realize this may sound like a braindump question to which there really is no yes/no answer, but I'm happy with your gut feel as well.

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.