So I needed very basic state management and notification for a small game-like thing I'm building. I decided to implement something like a finite state machine (but not quite, it doesn't transition upon events, but instead is told to transition). This is mostly used for changing visual behavior and animation or whatnot, so the most important ability of a client is to simply check which state it is in. I had a few objectives:
- There needs to be a finite list of states configured at creation, and I as the user need to be warned if I try to enter a state that doesn't exist
- States need to have parameters that hold auxiliary information. For example, a state called
selected
might have parametersselectedPort
andselectionContext
- I need basic event handling, so that other parts of the application can be notified upon entering or leaving a state
As far as the implementation, the class holds an objects of states, who themselves are objects that contain their own parameters
Here's the class I threw together:
function FSM(states) {
this.states = states
// Set the _fsmName of each state to its name in the states object
// This makes it much easier to check if we're in a particular state
for (var prop in this.states) {
this.states[prop]._fsmName = prop
}
// Initialize to an arbitrary state, client can transition to
// whatever their desired initial state is
this.state = this.states[Object.keys(this.states)[0]]
this.cbReg = {}
}
FSM.prototype.transition = function(next) {
for (var prop in this.states) {
if (prop == next) {
// Handle onLeave subscribers
if (this.state._fsmName in this.cbReg) {
this.cbReg[this.state._fsmName].forEach(function(val, ind, arr) {
if (val.when == "onLeave") {
val.callback()
}
})
}
this.state = this.states[prop]
// Handle onEnter subscribers
if (this.state._fsmName in this.cbReg) {
this.cbReg[this.state._fsmName].forEach(function(val, ind, arr) {
if (val.when == "onEnter") {
val.callback()
}
})
}
return
}
}
throw "FSM State doesn't exist"
}
FSM.prototype.inState = function(st) {
return (this.state._fsmName == st)
}
FSM.prototype.get = function(p) {
// Try to get a state parameter, but throw if the
// parameter doesn't exist
if (p in this.state) {
return this.state[p]
} else {
throw "FSM Invalid Parameter"
}
}
FSM.prototype.set = function(p, v) {
// Try to set a state parameter, but throw if the
// parameter doesn't exist, instead of silently adding it
if (p in this.state) {
this.state[p] = v
} else {
throw "FSM Invalid Parameter"
}
}
FSM.prototype.register = function(stateName, when, cb) {
// Register a callback upon entering or leaving a state
if (!(stateName in this.states)) {
throw "FSM State doesn't exist"
}
if ((when != "onEnter") && (when != "onLeave")) {
throw "FSM Invalid callback time specifier"
}
if (!(stateName in this.cbReg)) {
this.cbReg[stateName] = []
}
this.cbReg[stateName].push({
when: when,
callback: cb
})
}
A client might use it like:
this.SM = new FSM({
normal: {},
selected: {
selectedPort: null
}
})
this.SM.transition("normal")
this.SM.register("selected", "onEnter", function() { ... })
...
if (this.SM.inState("normal"))
...
if (this.SM.inState("selected")) {
var p = this.SM.get("selectedPort")
...
}
A few things:
- Existing FSM libraries seemed much more focuses on managing state in async situations, like callbacks from web requests and so forth. Are there any libraries that implement a simpler model, and particularly with the existence of auxiliary state information?
- Any obvious problems with functionality or style in this code?
- Are there other design patterns that might accomplish my intent here?