A simple, asynchronous finite-state machine router
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.
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 machinevar fsm = 'initial';// Define the initial state. This will be our// starting point.fsmstate// Assign a namethisname'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 exceptionthisincoming'wait for username';thisoutgoing'wait for username';// Execute this code as soon as the machine transitions into// this statethisenter// In this case, force the machine to run as soon as you// transition, triggering the execution of this state automaticallythismachinerun;;// 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.thisrunconsole.log'Enter username';// Force a transitionthistransition'wait for username';;;// A second state.fsmstatethisname'wait for username';thisincoming'initial';thisoutgoing'success' 'initial';thisrunif input == 'marco'console.log'Congratulations! You unlock the secret';thistransition'success';elseconsole.log'Invalid username. No prize for you.\n';thistransition'initial';;;// A final state.fsmstatethisname'success';thisincoming'wait for username';thisenterthisemit'success';;;// Instantiate an interface to stdinvar rl = readlinecreateInterfaceinput: processstdinoutput: processstdout;// 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.rlon'line' fsmrunbindfsm;// The machine is also an event emitter, so we// can listen for a success condition.fsmon'success'console.log'Closing down. Goodbye!';rlclose;;// We can now compile and perform the initial run of// the machine.fsmcompile;fsmrun;
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
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.
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
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, 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.
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 = 'initial state';// Configure your fsmvar clone = fsmclone;
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);
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.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.
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.
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.