State Behaviour Object
The State Behaviour Object is used to represent an individual state in a state machine. The object can be defined explicitly, or be the return value of a function. The function syntax is useful as it allows variables to be encapsulated within the state inside a standard JavaScript closure.
The behaviour of the state is controlled by the properties of the State Behaviour Object. The allowed property names (listed and explained in more detail below) are guard, setTA, entry, actions and exit. This list is 'extended' by the plug-in mechanism, although in every case the additional properties only serve as a quick and convenient way to define the same five properties above.
The guard, entry and exit properties should all define functions. When the state machine makes a transition to a particular state the guard function is called initially. If the guard condition is satisfied the machine enters the state and the setTA function is called, followed by the entry function. When the machine leaves the state its exit function is called. The actions property should define an object (or a function returning an object). The property names of the actions object are used to determine the names of the events that the state will respond and take action to.
The guard function, entry function and properties of the actions object can all initiate state transitions. This is done by setting the functions return value, or in the case of non-function properties on the actions object, the property value itself. The return values should either be String or Array types.
When a String is returned it is interpreted as a state name and this causes the machine to transition to the named state. In general, state names should be referred to directly however in certain cases aliases can and should be used. When an Array is returned then a data qualified transition is made. The first element of the array should be a String naming the target state, followed by elements used to set new values for the transition arguments. If the return value is missing (or is null) then the state machine remains in the current state, making an Internal Transition.
guard
When the state machine attempts to transition to a particular state sometimes you need to ensure that the state is only entered if some condition is satisfied. This can be done by adding an optional guard property to the State Behaviour Object. The guard property should contain either a synchronous function or an indirect reference to an asynchronous function (specified by name (String) using the async property, see examples below).
The guard function (if defined) is called before the state is entered. If the function returns null (using the less specific == equality test) then the machine goes on to enter the state, causing the entry function to be called. If not, then the machine diverts away from the state by making a transition to the state specified in the returned value.
Examples
The first example shows the state Admin using a synchronous guard function to test if the appropriate validation has been acquired to enter the state. The guard function performs a simple binary test on the transition argument hasValidation and either allows entry into the state (null return) or causes a transition to a different state AquireValidation ('AquireValidation' return).
Admin: {
guard: function (hasValidation) {
return hasValidation ? null : 'AquireValidation';
}
....
},
AquireValidation: {
....
}
The next example shows the state ListProcessor which uses a synchronous guard function to control entry based on the number of elements in a list (list). Notice that in this case list is not one of the guard function arguments and therefore is not being passed to the state as a transition argument. This means that list has been defined either globally, within the State Machine Generator Function closure, or within the State Behaviour Object closure.
ListProcessor: {
guard: function () {
if (list.length > 0) {
// more work to be done, so entry the
// state
return null;
} else {
// no more work to be done, so move on
return 'AnotherState';
}
}
....
}
The final example illustrates the use of an asynchronous guard function. The FileProcessor state outlined below tests a path by calling the fs.stat function, passing it the first element of the transition arguments (in this example a path). The state is only entered (null return) if stat.isFile() returns true.
FileProcessor: {
guard: {
async: 'fs.stat',
actions: {
'.done': function (stat) {
if (stat.isFile()) {
// path is a file so enter the state
return null;
} else if (stat.isDirectory()) {
// path is a directory so transition to
// DirectoryProcessor state
return 'DirectoryProcessor';
} else {
// handle default fs case
return 'DefaultFS';
}
},
'.err': '@error'
}
}
....
}
setTA
If the guard function allows entry into the state, or the guard function is missing, then the machine technically enters the state. This is significant because it means that when the machine subsequently leaves the state the states exit function will be called, whereas if the guard function denies entry into the state its exit function is not called.
If the setTA function is defined it is the first to be called when the machine enters the state. Its purpose is to set the transition arguments used by the state. This can be done by either defining a static array containing the arguments or by defining a function that receives the current transition arguments as its arguments and returns a new set to be used by the state.
entry
Once the setTA function has been called (if defined) then the entry function is called. entry functions are typically used to start something asynchronous - typically an asynchronous function call or an EventEmitter, but they can also be used to initialize variables or allocate resources required in the state.
Examples
The following example shows how the entry function can be used to register an EventEmitter (a fs.ReadStream object). Notice that in this example the State Behaviour Object is defined using a function to allow the fs.ReadStream object to be captured in a JavaScript closure.
StreamReader: function () {
// @type {fs.ReadStream}
var readStream;
return {
entry: function (filePath) {
// create a new EventEmitter
readStream = fs.createReadStream(filePath);
// register the event emitter with the
// state machine
fire.$regEmitter('rs', readstream);
},
....
};
}
In the next example the entry function is used to start an asynchronous function (fs.readFile).
FileReader: {
entry: function (filePath) {
fire.fs.readFile(filePath);
},
....
}
In the above two examples, when the asynchronous processes returns, either partially by emitting an event or fully by calling a completion callback function, then the state machine needs a way to intercept this and act accordingly. As is explained in the next section, this is what the actions object is used for.
actions
The actions object is used by the state to register specific event listeners and specify actions to be taken on receipt of the respective events.
The keys within an actions object act as a lookups to control what happens when an event is received into a state. If the key's value is a String, then this specifies that the action is a simple state transition to another state (note that an Array can also be returned to make a data qualified transition). If it is a function, then that function is called - it can do some work (for example call write() on a fs.WriteStream object), but should not initiate any additional asynchronous calls. The return value of the function indicates which state to transition to. If the return value is null (or evaluates to null, e.g. if there is no return), then the state machine simply stays in the current state (i.e. it does an internal transition).
Examples
The examples in this section relate to the examples shown above in the entry function section. First consider the StreamReader state. Adding actions to this state determines how the state should handle any data received from the stream. In this example the data is simply accumulated (using the state closure variable streamData) and then passed (using a data qualified transition, see: line 21) to another state (called ProcessData) once the stream has ended.
StreamReader: function () {
// @type {fs.ReadStream}
var readStream;
// @type {Buffer|String}
var streamData;
return {
entry: function (filePath) {
// create a new EventEmitter
readStream = fs.createReadStream(filePath);
// register the event emitter with the
// state machine
fire.$regEmitter('rs', readstream);
},
actions: {
'rs.data': function (data) {
// accumulate the data
streamData += data;
},
// data qualified transition to the state called
// 'ProcessData', passing data streamData
'rs.end': ['ProcessData', streamData],
// transition to the error state using the '@error' alias
'rs.error': '@error'
}
};
}
The following example builds on the FileReader state from above. This state uses the asynchronous function call proxy mechanism to segregate the return behaviour of the fs.readFile asynchronous function call into events. These events are then handled by separate functions in the actions object leading to a natural isolation of the code used to handle the possible outcomes.
FileReader: {
entry: function (filePath) {
fire.fs.readFile(filePath);
},
actions: {
'fs.readFile.done': function (data, filePathArgs) {
// count lines and transition to the state called 'Lines'
var numberOfLines = String(data).split('\n').length;
return ['Lines', numberOfLines, filePathArgs[0]];
},
// transition to the error state using the '@error' alias
'fs.readFile.err': '@error'
}
}
It is worth noting a few things from the above example. First, notice that by using the fire.fs.readFile proxy to call the fs.readFile function causes an anonymous callback function to be created and used. When fs.readFile completes, this function is called causing either done or err events to be emitted. If the done event is emitted then the transition arguments are set to the function callback arguments that follow the initial null error argument (i.e. data), followed by the initial function call argument (i.e. the first element of the filePathArgs array which contains filePath). If the err event is emitted then the transition arguments are set to the first of the function callback arguments (i.e. the error variable).
Second, notice how the fs.readFile.done action function receives the current transition arguments (data followed by filePath) as its arguments. These are then modified (to numberOfLines, filePath) by making a data qualified transition to the state called Lines.
exit
When the state machine makes a transition from state A to state B then if state A has an exit function, this will be called immediately prior to the transition being made. Note that if state A has a guard function and this diverts the machine to another state (before technically entering state A) then the exit function on A is not called. exit functions are can be useful to tidy up after a state, for example releasing resources.
Example
In the example below the exit function is used to close the underlying file descriptor so that the stream will not emit any more events (Line 20).
StreamReader: function () {
// @type {fs.ReadStream}
var readStream;
// @type {Buffer|String}
var streamData;
return {
entry: function (filePath) {
// create a new EventEmitter
readStream = fs.createReadStream(filePath);
// register the event emitter with the
// state machine
fire.$regEmitter('rs', readstream);
},
actions: {
....
},
exit: {
// Close the underlying file descriptor so that
// the stream will not emit any more events.
streamData.destroy();
}
};
}
Plug-ins
The above five properties of the State Behaviour Object together with the event handling mechanics can be used to describe a very rich set of states. We have however found that there a few relatively frequent behaviours that crop up in many problems and to expedite the definition of these state we have created plugins.
Plug-ins work by allowing additional properties to be specified on the State Behaviour Object, these are then used by the plug-in to create or modify the standard set of five properties described above. At the time of writing there were 11 plugins, split between 4 categories (List handling, Hierarchical machines, Timing and Work). A detailed description of all the available plug-ins can be found here (Plug-in Index).