State Machine Generator Function (SMGF)
The State Machine Generator Function (SMGF) is a function which is used to describe individual state machines. It is called each time a State Machine Object is created. Its purpose is to add properties specifying the behaviour of the state machine to the newly created State Machine Object. In this respect it is very similar to a JavaScript constructor function, although instances of state machines objects are created using methods on a State Machine Factory object rather than directly with the new operator.
There are two properties which the SMGF should always add to State Machine Object. The first is the states property, an object, which is used to define the available states within the machine. The states object should have properties with names equal to the names of the states within the machine and values equal to the respective State Behaviour Objects describing them.
The second is the startState property, a string, used when the state machine is started to indicate the initial state the machine should enter. The startState string must name a valid state within the state machine and therefore should be equal to the name of one of the properties of the states object described above.
Currently ignite.js provides two ways to use the SMGF to specify the states and startState properties. It can either be done by adding the properties directly to the this object within the body of the SMGF, or by returning an object from the SMFG which contains the desired properties. The following two code snippets show examples of both.
Example
The following shows the outline of a SMGF describing a state machine with three states (StateOne, StateTwo and StateThree), starting in the state StateOne:
function exampleStateMachine (fire, arg1, arg2) {
this.startState = 'StateOne';
this.states = {
StateOne: {
// State Behaviour Object representing StateOne
....
},
StateTwo: {
// State Behaviour Object representing StateTwo
....
},
StateThree: {
// State Behaviour Object representing StateThree
....
}
};
}
The next example uses exactly the same state machine description as above but expressed with the object return syntax rather than the JavaScript constructor syntax.
function exampleStateMachine (fire, arg1, arg2) {
return {
startState: 'StateOne',
states: {
StateOne: { ... },
StateTwo: { ... },
StateThree: { ... }
}
};
}
The SMGF function is called internally by the State Machine Factory when either its spawn or spawnWithCb methods are called. It is passed several arguments. The first argument is the fire object. The subsequent arguments are the initial transition arguments, if any have been supplied.
Transition arguments
For each running state machine, ignite.js maintains a list of variables containing important information related to the current condition of the machine. States within the machine access the list whenever a transition is made or whenever the state responds to an event injected into the state machine. The list is therefore extremely useful for transferring information between states and for processing information from events.
More specifically, the list can be accessed inside any function defined in a State Behaviour Object, this is done directly by using the this.args array (see: guard and entry functions and the actions object). However given the central role the information in the list plays, its elements are also passed to the functions as arguments. This allows specific variables to be defined for each element of the list, leading to more natural and understandable code. For this reason the list of variables has become known as the state machines transition arguments.
Transition arguments can be modified in one of two ways. The first is through a data qualified transition, where both the name of the target state and new values for the transitions arguments are specified when the transition occurs. The second is when events are injected into the state machine. Any arguments assigned to the event are used to set new values for the transition Arguments. The source of the events could be either, the result of an asynchronous Function Proxy, an EventEmitter, or an auto-generated callback.
In most cases the Transition Arguments should be accessed as function arguments rather than directly from the this.args array. An important aspect of state machine design is to properly match these function signatures with the transition arguments that are expected, given the likely transitions into a state. For example, in the following code the FileProcess state expects that transitions to it will be made from the FileReader state. Consequently, the function signature of its entry function exactly matches the transition arguments set by the FileReader state, which in this case are the file data followed by an array containing the original function call (fs.readFile) arguments (i.e. [path]).
FileReader: {
entry: function (path) {
fire.fs.readFile(path);
},
actions: {
'.done': 'FileProcess',
'.err': '@error'
}
},
FileProcess: {
entry: function (data, readFileArgs) {
...
}
}
The transition arguments are initially passed into the state machine when the SMGF is called by the State Machine Factory. This allows the behaviour of the machine to be influenced before the machine starts.
The fire object
The fire object is a utility function object available within a state machine to help control asynchronous calls and manage the machine itself. It is passed into the SMGF as the functions first argument and therefore is accessible anywhere with the generator function.
Asynchronous function call proxy
node.js has a standard signature for the majority of its asynchronous functions:
- Asynchronous functions take a callback function as the last argument,
- callback functions take an error variable as their first argument.
As this is so common, ignite.js has a handy shortcut for using these functions. This uses the fire object, which acts as a proxy to any functions listed as properties in the imports object supplied as the second argument to the State Machine Factory constructor. For example, if the imports object is set as:
imports = {
fs: require('fs')
};
then from within the state machine, any of the functions on the fs object can be called using the fire.fs proxy object. For example, to call fs.readFile(filePath, function (err, data) { ... }), the following can be used:
fire.fs.readFile(filepath);
The proxy function has two big advantages over the regular asynchronous function call. The first is that there is no need to explicitly create a callback function. The proxy function calls a curried version of the client function (in this example fs.readFile) with an auto-generated callback function inserted as its final argument.
The second is how the auto-generated callback function behaves. When it is called it generates events which are injected back into the current state of the state machine. The benefit of this is that different event names are generated for the success and error return scenarios of the function. This allows dedicated listeners (see: actions) to handle the separate cases, leading to much cleaner code.
The names of the events for the success and error return scenarios are derived simply from the name of the function by adding either '.done' or '.err' respectively. For example, a call to fire.fs.readFile would generate a 'fs.readFile.done' or 'fs.readFile.err' event. The segregation of events in based on the value of the first argument of the callback function (i.e. the error variable). If this is null then it is assumed that the function returned successfully and the '.done' event is generated. Alternatively, if the error variable has value then an error is assumed to have occurred and the '.err' event is generated.
If a state needs to respond to these events it registers listeners (see: actions). When this is done a pattern string is specified which is used to match the listener to incoming events based on event name (see: Events). The matching is done in one of two ways. If the pattern string starts with a '.' then postfix matching is used, otherwise prefix matching is used. For example, '.done' would match event names: 'fs.readFile.done', 'fs.writeFile.done', etc. Whereas 'fs.readFile' would match both 'fs.readFile.done' and 'fs.readFile.err'.
When events are generated in this way, new values for the transition arguments are set. The arguments set for the '.done' and '.err' events are specific to the return cases they represent. The '.done' arguments are all those from the callback function except the first, (i.e. the error variable) which is removed as it is null in this case. The '.err' event has a single argument set to the error variable. The arguments set for the general event 'fs.readFile' are simply the complete arguments from the callback. In each of the above cases an array containing the initial function call arguments is added as the final argument.
Example:
Consider the proxy function call fire.fs.readFile(filePath). The following table shows how the transition arguments are set for some selected event patterns.
| pattern | matched event | callback function signature |
|---|---|---|
'fs.readFile.done' | 'fs.readFile.done' | function(data, [filePath]) |
'.done' | 'fs.readFile.done' | function(data, [filePath]) |
'fs.readFile.err' | 'fs.readFile.err' | function(err, [filePath]) |
'.err' | 'fs.readFile.err' | function(err, [filePath]) |
'fs.readFile' | all fs.readFile events | function(err, data, [filePath]) |
Event Emitters
The other main source of events in node.js are EventEmitters. These are easily handled within ignite.js and they also take advantage of the event name pattern matching introduced above and described in more detail in the Events section.
The initial step to using an EventEmitter with ignite.js is to register the EventEmitter within the state machine intending to use it. This is done by calling the $regEmitter method on the fire object from anywhere within the SMGF. When the EventEmitter is no longer needed, and it has not been automatically deregistered, it should be deresistered explicitly by calling the $deregEmitter method on the fire object.
From the time an EventEmitter is registered up until it is deregistered, any events it emits will be injected back into the current state of the state machine. The events can then be handled using actions specified in the states actions object. In most cases specific EventEmitters are temporary and only needed within the state they are defined in, therefore by default EventEmitters are deregistered automatically when the machine leaves the current state.
This default behaviour can be modified by specifying the EventEmitter as persistent. This is done when the EventEmitter is registered by setting the third argument of the $regEmitter method call to true. Any events then emitted by the EventEmitter will continue to be injected into the current state of the state machine regardless of any state transitions which may occur.
The name of the events injected into the state machine are derived from the name of the emitted event by simply adding the name of the EventEmitter followed by a '.' to the beginning. For example a Readable Stream object registered as 'rs' would emit events called 'rs.data', 'rs.end', 'rs.error', 'rs.close' and 'rs.fd'. The name of the EventEmitter is set at the time of registration using the first argument of the $regEmitter method, which accepts a string. The second argument of this method should contain the EventEmitter itself.
Examples
An example entry function being used to register a temporary EventEmitter.
'entry': function () {
// Create a Read Stream EventEmitter
readStream = fs.createReadStream(filePath);
// Temporary registration
fire.$regEmitter('rs', readstream);
}
An example entry function being used to register a persistent EventEmitter.
'entry': function (options) {
// Create an HTTP Request EventEmitter
httpReq = http.request(options);
// Persistent registration
fire.$regEmitter('req', httpReq, true);
....
}
An example exit function being used to deregister an EventEmitter.
'exit': function () {
// Deregister previously registered EventEmitter
fire.$deregEmitter('req');
}
Auto-generated callbacks
There are some asynchronous functions where the callback function is not the final argument of the function call. For example the node.js function setInterval expects the callback function as its first argument. This means that for these types of functions the current Function Proxy mechanism described above can not be used.
To solve this problem, the function should be called directly with a callback function auto-generated using the fire.$cb method. This method returns a callback function. It should be called with a single string argument indicating the name of the event injected into the state machine when the callback function is called. All callback arguments will be assigned to the event.
Example
The following example will generate an event called 'tick' every 200 ms.
'entry': function () {
timerId = setInterval(fire.$cb('tick'), 200);
},
....