Home

Awesome

Chine: a finite-state machine execution engine

Chine (pronounced “sheen”) allows you to split a task into a variety of steps that can be executed asynchronously by a finite-state machine (fsm). The machine can be executed in a deterministic, but arbitrarily complex, sequence.

Example

Here's a simple example that asks for a username (it's only a couple dozen or so lines without the comments):


var readline = require('readline');

var Chine = require('./lib');

// Create a new state machine

var fsm = new Chine('initial');

// Define the initial state. This will be our
// starting point.

fsm.state(function() {
    // Assign a name

    this.name('initial');

    // Define the states from which it is legal
    // to transition to this state. Attempts to transition
    // to or from anything else will result in an exception

    this.incoming('wait for username');
    this.outgoing('wait for username');

    // Execute this code as soon as the machine transitions into
    // this state

    this.enter(function() {
        // In this case, force the machine to run as soon as you
        // transition, triggering the execution of this state automatically

        this.machine.run();
    });

    // This code is execute when the machine is run in this state
    // This closure *must* either transition to a new state; attempting
    // to run the machine on the same state twice without transitioning
    // to a new state first will result in an exception.

    this.run(function() {
        console.log('Enter username');

        // Force a transition
        this.transition('wait for username');
    });
});

// A second state.

fsm.state(function() {
    this.name('wait for username');
    this.incoming('initial');
    this.outgoing('success', 'initial');

    this.run(function(input) {
        if (input == 'marco') {
            console.log('Congratulations! You unlock the secret');

            this.transition('success');
        } else {
            console.log('Invalid username. No prize for you.\n');

            this.transition('initial');
        }
    });
});

// A final state.

fsm.state(function() {
    this.name('success');
    this.incoming('wait for username');

    this.enter(function() {
        this.emit('success');
    });
});

// Instantiate an interface to stdin

var rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
});

// When we get a line, we run the machine. Note that
// this portion of the app knows nothing about state--
// that's all saved in the machine itself.

rl.on('line', fsm.run.bind(fsm));

// The machine is also an event emitter, so we
// can listen for a success condition.

fsm.on('success', function() {
    console.log('Closing down. Goodbye!');
    rl.close();
});

// We can now compile and perform the initial run of
// the machine.

fsm.compile();
fsm.run();

What happens in this script

Any time the run method of the machine is executed, the corresponding run method of the current state is invoked. At each state, you get to decide what can be done and what the next state is going to be. Chine helps you by giving you way to express which steps are admissible in input and output, thus reducing the complexity of the overall task.

Here's what happens in this script as you go through its execution (you can run it a copy of it in the /demo directory):

node demo/auth.js

As the script starts, it defines the various states of the fsm and specifies the starting state, initial. It then creates a readline object and starts listening for user input.

The initial state's run method is invoked when the last line of the script is executed; it outputs a string of text and then transitions to wait for username. The transition is legal, because wait for username is in initial's outgoing routes, and initial is in wait for username's incoming routes. If this weren't the case, or if you tried to transition to a non-existing route, Chine would throw an exception.

> Enter a username
invalid

We have now entered a new line of text, which causes the line event to trigger on our readline instance. The fsm.run method is used as its handler; it receives the text of the line, which is transparently passed to the run method of the current state.

Here, we check the input and transition to success if it's correct. In success's run method, we output some more text and, since Chine is a subclass of EventEmitter, emit the success event, which is caught by a handler that terminates the script.

If the value the user has input is incorrect, on the other hand, we transition back to initial. In initial, we now have an enter handler, which is triggered as soon as the machine enters the state. In enter, we simply tell the machine to run again, which causes our state to be executed and the cycle to start from the beginning.

This is important, because transitioning to a state does not cause it to be executed—if we didn't have an enter handler that forces the machine to execute one more time, the process would just stall. (Incidentally, the enter handler is not called when the machine is first run in its starting state.)

There is also a corresponding leave handler, but we're not using it here.

Cloning and serializing

Because creating a new machine is a very expensive operation, you can actually clone one, thus creating a copy that has its own runtime context:


var fsm = new Chine('initial state');

// Configure your fsm

var clone = fsm.clone();

You can clone a machine as many times as you want; note that the clone method doesn't create a copy of the current machine execution state—it always creates a new one.

If you actually want to “freeze” a machine and its current execution state—including any data you may have stored in it at runtime, you can instead serialize it:


var serialized = fsm.serialize();

// Later:

var fsm_new = fsm.unserialize(serialized);

The unserialize method takes control of the serialized object you pass to it, which can no longer be reused. If you need unserialize the same machine, in the same execution state, more than once, you must feed unserialize a copy of the original serialized object (you can just use JSON.stringify() and JSON.parse() to create a quick clone of a serialized fsm).

Note that serialization will likely fail if you attach any functions to the runtime context of a fsm, particularly if you intend to serialize to an external resource. Also, note that unserialize(), much like clone() returns a new fsm. The original fsm is left intact and is just used as a factory.

When to use Chine

The script above also serves as a good example of when not to use Chine. This trivial login system could be written in a few lines of code and doesn't require anything as complex as a finite-state machine.

Chine gives you a couple of interesting features. The first is that it helps you to break down a complex process into a series of discrete steps. You can focus on each state individually, and avoid having to deal with a massive amount of logic all in one place. Because state is incorporated in the fsm's design, you can also use it as a session object to maintain information across multiple requests, for example in a web app.

In general, fsms are useful whenever you have complex tasks that follow a determistic but very complex flow. A typical example would be an online shopping cart, which follows a set of discrete steps (shipping, billing, charging, confirmation, processing, notifications, etc.), some of which could happen days or even weeks apart from each other. Chine can greatly simplify their implementation, and serialization allows you to “freeze and thaw” a machine as needed.

Contribute

Patches are welcome, but only if accompanied by a matching test case. Bug reports, questions, and comments are equally appreciated! You can also reach the author directly on Twitter.