mooremachine
Advanced tools
Comparing version 1.4.2 to 2.0.0
261
lib/fsm.js
@@ -9,2 +9,96 @@ // Copyright 2015 Joyent, Inc. | ||
function FSMStateHandle(fsm, state, link) { | ||
this.fsh_fsm = fsm; | ||
this.fsh_link = link; | ||
this.fsh_state = state; | ||
this.fsh_valid = true; | ||
this.fsh_listeners = []; | ||
this.fsh_timeouts = []; | ||
this.fsh_intervals = []; | ||
this.fsh_immediates = []; | ||
this.fsh_validTransitions = undefined; | ||
this.fsh_nextState = undefined; | ||
} | ||
FSMStateHandle.prototype.validTransitions = function (states) { | ||
assert.arrayOfString(states, 'states'); | ||
this.fsh_validTransitions = states; | ||
}; | ||
FSMStateHandle.prototype.gotoState = function (state) { | ||
if (!this.fsh_valid) { | ||
throw (new Error('FSM attempted to leave state ' + | ||
this.fsh_state + ' towards ' + state + ' via a handle ' + | ||
'that was already used to enter state ' + | ||
this.fsh_nextState)); | ||
} | ||
if (this.fsh_validTransitions !== undefined) { | ||
if (this.fsh_validTransitions.indexOf(state) === -1) { | ||
throw (new Error('Invalid FSM transition: ' + | ||
this.fsh_state + ' => ' + state)); | ||
} | ||
} | ||
this.fsh_valid = false; | ||
this.fsh_nextState = state; | ||
return (this.fsh_fsm._gotoState(state)); | ||
}; | ||
FSMStateHandle.prototype.disconnect = function () { | ||
var ls = this.fsh_listeners; | ||
for (var i = 0; i < ls.length; ++i) { | ||
ls[i][0].removeListener(ls[i][1], ls[i][2]); | ||
} | ||
var ts = this.fsh_timeouts; | ||
for (i = 0; i < ts.length; ++i) { | ||
clearTimeout(ts[i]); | ||
} | ||
var is = this.fsh_intervals; | ||
for (i = 0; i < is.length; ++i) { | ||
clearInterval(is[i]); | ||
} | ||
var ims = this.fsh_immediates; | ||
for (i = 0; i < ims.length; ++i) { | ||
clearImmediate(ims[i]); | ||
} | ||
this.fsh_listeners = []; | ||
this.fsh_timeouts = []; | ||
this.fsh_intervals = []; | ||
this.fsh_immediates = []; | ||
if (this.fsh_link !== undefined) | ||
this.fsh_link.disconnect(); | ||
}; | ||
FSMStateHandle.prototype.on = function (obj, evt, cb) { | ||
obj.on(evt, cb); | ||
this.fsh_listeners.push([obj, evt, cb]); | ||
}; | ||
FSMStateHandle.prototype.interval = function (interval, cb) { | ||
var timer = setInterval(cb, interval); | ||
this.fsh_intervals.push(timer); | ||
return (timer); | ||
}; | ||
FSMStateHandle.prototype.timeout = function (timeout, cb) { | ||
var timer = setTimeout(cb, timeout); | ||
this.fsh_timeouts.push(timer); | ||
return (timer); | ||
}; | ||
FSMStateHandle.prototype.immediate = function (cb) { | ||
var timer = setImmediate(cb); | ||
this.fsh_immediates.push(timer); | ||
return (timer); | ||
}; | ||
FSMStateHandle.prototype.callback = function (cb) { | ||
var s = this; | ||
return (function () { | ||
var args = arguments; | ||
if (s.fsh_valid) | ||
return (cb.apply(this, args)); | ||
return (undefined); | ||
}); | ||
}; | ||
/* | ||
@@ -18,11 +112,11 @@ * fsm.js: a small library for Moore finite state machines. | ||
* | ||
* The state function takes up to 4 arguments -- on, once, timeout and onState. | ||
* These are used in order to gang together callbacks that result in a state | ||
* transition out of this state. The "on" and "once" functions act on an | ||
* EventEmitter, "timeout" is a wrapper around setTimeout, and "onState" allows | ||
* you to make your FSM transition when another FSM reaches a given state. The | ||
* idea behind using these is that all callbacks you register in this way will | ||
* automatically get de-registered (and any timers cleaned up) as soon as the | ||
* FSM leaves its current state. This way we avoid any stale callbacks from a | ||
* previous state being called with new data. | ||
* The state function takes one argument -- the state handle. This is used in | ||
* order to gang together callbacks that result in a state transition out of | ||
* this state. The "on" function acts on an EventEmitter, "timeout" is a | ||
* wrapper around setTimeout. The state handle also contains the "gotoState" | ||
* method, which is used to transition to a new state. The idea behind using | ||
* the on/timeout/etc functions is that all callbacks you register in this way | ||
* will automatically get de-registered (and any timers cleaned up) as soon as | ||
* the FSM leaves its current state. This way we avoid any stale callbacks | ||
* from a previous state being called with new data. | ||
* | ||
@@ -35,13 +129,11 @@ * FSM also supports "sub-states", which share their callbacks with the rest of | ||
assert.string(defState, 'default state'); | ||
this.fsm_stListeners = []; | ||
this.fsm_stTimers = []; | ||
this.fsm_history = []; | ||
this.fsm_handle = undefined; | ||
this.fsm_inTransition = false; | ||
this.fsm_nextState = undefined; | ||
this.fsm_validTransitions = undefined; | ||
if (this.fsm_allStateEvents === undefined) | ||
this.fsm_allStateEvents = []; | ||
this.fsm_state = undefined; | ||
this.fsm_toEmit = []; | ||
EventEmitter.call(this); | ||
this.gotoState(defState); | ||
this._gotoState(defState); | ||
} | ||
@@ -59,7 +151,2 @@ util.inherits(FSM, EventEmitter); | ||
FSM.prototype.validTransitions = function (states) { | ||
assert.arrayOfString(states, 'states'); | ||
this.fsm_validTransitions = states; | ||
}; | ||
FSM.prototype.allStateEvent = function (evt) { | ||
@@ -72,29 +159,4 @@ assert.string(evt, 'event'); | ||
/* | ||
* Calls a callback when this FSM reaches a given state. This is for use by | ||
* external non-FSM things -- if an FSM wants to listen to another FSM, it | ||
* should use sOnState (or the onState argument to a state func). | ||
*/ | ||
FSM.prototype.onState = function (state, cb) { | ||
assert.string(state, 'state'); | ||
assert.func(cb, 'callback'); | ||
if (this.fsm_state === state || | ||
this.fsm_state.indexOf(state + '.') === 0) { | ||
cb(); | ||
return; | ||
} | ||
var self = this; | ||
function stateCb(newState) { | ||
if (newState !== state && | ||
newState.indexOf(state + '.') !== 0) { | ||
self.once('stateChanged', stateCb); | ||
return; | ||
} | ||
cb(); | ||
} | ||
self.once('stateChanged', stateCb); | ||
}; | ||
/* Transition the FSM to a new state. */ | ||
FSM.prototype.gotoState = function (state) { | ||
FSM.prototype._gotoState = function (state) { | ||
assert.string(state, 'state'); | ||
@@ -108,9 +170,2 @@ | ||
if (this.fsm_validTransitions !== undefined) { | ||
if (this.fsm_validTransitions.indexOf(state) === -1) { | ||
throw (new Error('Invalid FSM transition: ' + | ||
this.fsm_state + ' => ' + state)); | ||
} | ||
} | ||
/* | ||
@@ -122,13 +177,5 @@ * If we're changing to a state that is not a sub-state of this one, | ||
var newParts = state.split('.'); | ||
if (parts[0] !== newParts[0]) { | ||
var ls = this.fsm_stListeners; | ||
for (var i = 0; i < ls.length; ++i) { | ||
ls[i][0].removeListener(ls[i][1], ls[i][2]); | ||
} | ||
var ts = this.fsm_stTimers; | ||
for (i = 0; i < ts.length; ++i) { | ||
clearTimeout(ts[i]); | ||
} | ||
this.fsm_stTimers = []; | ||
this.fsm_stListeners = []; | ||
if (parts[0] !== newParts[0] && this.fsm_handle !== undefined) { | ||
this.fsm_handle.disconnect(); | ||
this.fsm_handle = undefined; | ||
} | ||
@@ -146,2 +193,4 @@ | ||
this.fsm_handle = new FSMStateHandle(this, state, this.fsm_handle); | ||
this.fsm_history.push(state); | ||
@@ -151,7 +200,4 @@ if (this.fsm_history.length >= 8) | ||
this.fsm_validTransitions = undefined; | ||
this.fsm_inTransition = true; | ||
f.call(this, this.sOn.bind(this), this.sOnce.bind(this), | ||
this.sTimeout.bind(this), this.sOnState.bind(this)); | ||
f.call(this, this.fsm_handle); | ||
this.fsm_inTransition = false; | ||
@@ -169,3 +215,12 @@ | ||
this.emit('stateChanged', state); | ||
this.fsm_toEmit.push(state); | ||
if (this.fsm_toEmit.length === 1) { | ||
setImmediate(function () { | ||
var ss = self.fsm_toEmit; | ||
self.fsm_toEmit = []; | ||
ss.forEach(function (s) { | ||
self.emit('stateChanged', s); | ||
}); | ||
}); | ||
} | ||
@@ -175,72 +230,4 @@ var next = this.fsm_nextState; | ||
this.fsm_nextState = undefined; | ||
this.gotoState(next); | ||
this._gotoState(next); | ||
} | ||
}; | ||
/* | ||
* These are the per-state event registration functions, which are bound and | ||
* then passed as the args to state funcs. | ||
*/ | ||
FSM.prototype.sOn = function (obj, evt, cb) { | ||
obj.on(evt, cb); | ||
this.fsm_stListeners.push([obj, evt, cb]); | ||
}; | ||
FSM.prototype.sOnce = function (obj, evt, cb) { | ||
obj.once(evt, cb); | ||
this.fsm_stListeners.push([obj, evt, cb]); | ||
}; | ||
FSM.prototype.sTimeout = function (timeout, cb) { | ||
var timer = setTimeout(cb, timeout); | ||
this.fsm_stTimers.push(timer); | ||
return (timer); | ||
}; | ||
FSM.prototype.sOnState = function (obj, state, cb) { | ||
assert.string(state, 'state'); | ||
assert.func(cb, 'callback'); | ||
if (obj.fsm_state === state || | ||
obj.fsm_state.indexOf(state + '.') === 0) { | ||
cb(); | ||
return; | ||
} | ||
var self = this; | ||
function stateCb(newState) { | ||
if (newState !== state && | ||
newState.indexOf(state + '.') !== 0) { | ||
self.sOnce(obj, 'stateChanged', stateCb); | ||
return; | ||
} | ||
cb(); | ||
} | ||
this.sOnce(obj, 'stateChanged', stateCb); | ||
}; | ||
/* | ||
* Wraps a conventional node callback async function up into an EventEmitter so | ||
* that it can be used as a state transition trigger. | ||
*/ | ||
FSM.wrap = function _fsmWrap(fun) { | ||
function fsm_cb_wrapper() { | ||
var args = Array.prototype.slice.call(arguments); | ||
var eve = new EventEmitter(); | ||
var self = this; | ||
var cb = function () { | ||
var resArgs = Array.prototype.slice.call(arguments); | ||
var err = resArgs.shift(); | ||
if (err) { | ||
eve.emit('error', err); | ||
} else { | ||
resArgs.unshift('return'); | ||
eve.emit.apply(eve, resArgs); | ||
} | ||
}; | ||
args.push(cb); | ||
eve.run = function () { | ||
fun.apply(self, args); | ||
}; | ||
return (eve); | ||
} | ||
return (fsm_cb_wrapper); | ||
}; |
{ | ||
"name": "mooremachine", | ||
"version": "1.4.2", | ||
"version": "2.0.0", | ||
"description": "Moore finite state machines", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
114
README.md
@@ -81,22 +81,21 @@ mooremachine | ||
ThingFSM.prototype.state_stopped = function (on) { | ||
var self = this; | ||
on(this, 'startAsserted', function () { | ||
self.gotoState('connecting'); | ||
ThingFSM.prototype.state_stopped = function (S) { | ||
S.on(this, 'startAsserted', function () { | ||
S.gotoState('connecting'); | ||
}); | ||
}; | ||
ThingFSM.prototype.state_connecting = function (on) { | ||
ThingFSM.prototype.state_connecting = function (S) { | ||
var self = this; | ||
this.tf_sock = mod_net.connect(...); | ||
on(this.tf_sock, 'connect', function () { | ||
self.gotoState('connected'); | ||
S.on(this.tf_sock, 'connect', function () { | ||
S.gotoState('connected'); | ||
}); | ||
on(this.tf_sock, 'error', function (err) { | ||
S.on(this.tf_sock, 'error', function (err) { | ||
self.tf_lastError = err; | ||
self.gotoState('error'); | ||
S.gotoState('error'); | ||
}); | ||
}; | ||
ThingFSM.prototype.state_error = function (on, once, timeout) { | ||
ThingFSM.prototype.state_error = function (S) { | ||
var self = this; | ||
@@ -109,4 +108,4 @@ if (this.tf_sock !== undefined) | ||
/* Retry the connection in 5 seconds */ | ||
timeout(5000, function () { | ||
self.gotoState('connecting'); | ||
S.timeout(5000, function () { | ||
S.gotoState('connecting'); | ||
}); | ||
@@ -137,3 +136,3 @@ }; | ||
### `mod_mooremachine#state_name(on, once, timeout, onState)` | ||
### `FSM#state_name(stateHandle)` | ||
@@ -144,32 +143,23 @@ State entry functions. These run exactly once, at entry to the new state. They | ||
The `on`, `once`, `timeout` and `onState` arguments are functions that should be | ||
used to set up events that can lead to a state transition. The `on` function is | ||
like `EventEmitter#on`, but any handlers set up with it will be automatically | ||
torn down as soon as the FSM leaves its current state. Similar for `once` and | ||
`timeout`. The `onState` function is used in place of calling | ||
`mod_mooremachine#onState` on another FSM. | ||
The `stateHandle` argument is a handle giving access to functions that should be | ||
used to set up events that can lead to a state transition. It provides | ||
replacements for `EventEmitter#on`, `setTimeout`, and other mechanisms for async | ||
event handling, which are automatically torn down as soon as the FSM leaves its | ||
current state. This prevents erroneous state transitions from a dangling | ||
callback left behind by a previous state. | ||
Parameters: | ||
- `on`: Function `(emitter, event, cb)`, sets up an event callback like | ||
`EventEmitter#on`. Parameters: | ||
- `emitter`: an EventEmitter | ||
- `event`: a String, name of the event | ||
- `cb`: a Function, callback to run when the event happens | ||
- `once`: Function `(emitter, event, cb)`, like `on` but only runs once | ||
- `timeout`: Function `(timeout, cb)`, like `setTimeout()`. Parameters: | ||
- `timeout`: Number, milliseconds until the callback runs | ||
- `cb`: a Function, callback to run | ||
- `onState`: Function `(fsm, state, cb)` | ||
It is permissible to call `stateHandle.gotoState()` immediately within the | ||
`state_` function. | ||
### `mod_mooremachine#validTransitions(possibleStates)` | ||
Caution should be used when emitting events or making synchronous calls within a | ||
`state_` function -- if it is possible for the handler of the event or callee to | ||
call back into the FSM or emit an event itself that may cause the FSM to | ||
transition, then the results of this occurring synchronously within the state | ||
entry function may be undesirable. It is highly recommended to emit any events | ||
within a `setImmediate()` callback. | ||
Should be called from a state entry function. Sets the list of valid transitions | ||
that are possible out of the current state. Any attempt to transition the FSM | ||
out of the current state to a state not on this list (using `gotoState()`) will | ||
throw an error. | ||
Parameters: | ||
- `possibleStates`: Array of String, names of valid states | ||
- `stateHandle`, an Object, instance of `mod_mooremachine.FSMStateHandle` | ||
### `mod_mooremachine#allStateEvent(name)` | ||
### `FSM#allStateEvent(name)` | ||
@@ -184,7 +174,7 @@ Adds an "all-state event". Should be called in the constructor for an FSM | ||
### `mod_mooremachine#getState()` | ||
### `FSM#getState()` | ||
Returns a String, full current state of the FSM (including sub-state). | ||
### `mod_mooremachine#isInState(state)` | ||
### `FSM#isInState(state)` | ||
@@ -198,24 +188,42 @@ Tests whether the FSM is in the given state, or any sub-state of it. | ||
### `mod_mooremachine#onState(state, cb)` | ||
## State handles | ||
Runs a callback on the next time that the FSM enters a given state or any | ||
sub-state of it. | ||
### `FSMStateHandle#gotoState(state)` | ||
Parameters: | ||
- `state`: String, state to test for | ||
- `cb`: Function `(newState)` | ||
Transitions the FSM into the given new state. Can only be called once per state | ||
handle. | ||
### `mod_mooremachine#gotoState(state)` | ||
### `FSMStateHandle#on(emitter, event, cb)` | ||
Causes the FSM to enter the given new state. | ||
Works like `EventEmitter#on`: equivalent to `emitter.on(event, cb)` but | ||
registers the callback for removal as soon as the FSM moves out of the current | ||
state. | ||
### `FSMStateHandle#timeout(timeoutMs, cb)` | ||
Equivalent to `setTimeout(cb, timeoutMs)`, but registers the timer for clearing | ||
as soon as the FSM moves out of the current state. | ||
Returns: the timer handle. | ||
### `FSMStateHandle#interval(intervalMs, cb)` | ||
Equivalent to `setInterval(cb, intervalMs)`, but registers the timer for | ||
clearing as soon as the FSM moves out of the current state. | ||
### `FSMStateHandle#validTransitions(possibleStates)` | ||
Should be called from a state entry function. Sets the list of valid transitions | ||
that are possible out of the current state. Any attempt to transition the FSM | ||
out of the current state to a state not on this list (using `gotoState()`) will | ||
throw an error. | ||
Parameters: | ||
- `state`: String, state to enter | ||
- `possibleStates`: Array of String, names of valid states | ||
### `mod_mooremachine.FSM.wrap(fun)` | ||
### `FSMStateHandle#gotoState(state)` | ||
Wraps a conventional node callback function up into an EventEmitter, to make | ||
life a little easier with `on()`. | ||
Causes the FSM to enter the given new state. | ||
Parameters: | ||
- `fun`: Function `(cb)` | ||
- `state`: String, state to enter |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
224
14903
202
1