possum
v1.0.4
Published
I am able. A State Machine.
Downloads
28
Maintainers
Readme
_ _ _ _ _
|_)(_)_)_)|_||||
| state machine
I am able.
- From potis (“able, capable”) + sum (“I am”).
========
License
Install
npm install --save possum
Size
- unminified 3.05kb
- minified 1.68kb
Building
make build
=> ./build/possum.js
minified builds are broken atm
Documentation
Found here
IMPORTANT BREAKING CHANGE FOR v1+... EventEmitter api is no longer mixed in by default...that is up to you!!!
Example
make example
Module Support
Works in nodejs and the browser.
Getting Started
This is in the
app.js
for the example.
//listening to Kiss' Love Gun on my record player
//first, define our state machine spec
let gun = possum
.config({
namespace: 'kiss'
, initialState: 'uninitialized'
})
.methods({
enable: function(actions) {
Object.keys(controls).forEach(function(key){
controls[key].setAttribute('disabled','disabled')
})
if(!actions || !actions.length) {
return
}
actions.forEach(function(action){
controls[action].removeAttribute('disabled')
})
}
,loadWeapon: function(bullets){
return this.bullets = bullets || [
'I Stole Your Love'
,'Shock Me'
,'Love Gun'
]
}
,fire: function(){
//asynchronous, returns a Promise
return this.recordPlayer.play(this.song)
}
})
.states({
'uninitialized': {
_enter: function(){
this.enable(['initialize'])
return this.recordPlayer.turnOn()
}
,'initialize': function(args) {
return this.recordPlayer.spinRecord()
.then(this.transition.bind(this,'initialized'))
}
}
,'initialized': {
_enter: function(){
this.enable(['pullTrigger','load'])
}
,'pullTrigger': function() {
this.deferUntilTransition('aimed')
}
,'load': function(args) {
this.deferUntilTransition('loading')
return this.transition('loading')
}
}
,'loading': {
_enter: function(){
this.enable(['load'])
}
,'load': function(args) {
this.loadWeapon(args && args.bullets)
return this.transition('loaded')
}
,'reload': function() {
return this.handle('load')
}
}
,'loaded': {
_enter: function(){
document.querySelector('.remaining').innerHTML = ''
this.enable(['aim'])
}
,'aim': function(args) {
this.song = (args && args.target) || this.bullets.shift()
this.recordPlayer.hoverNeedleOver(this.song)
return this.transition('aimed')
}
}
,'aimed':{
_enter: function(){
this.enable(['pullTrigger'])
}
,'pullTrigger': function() {
return this.fire()
.then(this.transition.bind(this,'smoking'))
}
}
,'smoking': {
_enter: function(){
console.log('light cigarette, sit back and relax')
this.enable(['liftNeedle'])
}
,'aim': function(args){
this.deferUntilNextHandler()
return this.handle('liftNeedle')
}
,'liftNeedle': function(){
if(this.bullets.length) {
return this.transition('loaded')
}
return this.transition('emptied')
}
}
,'emptied': {
_enter: function(){
this.enable(['reload'])
return this.handle('aim')
}
,'aim': function(args){
return this.recordPlayer.returnArm()
}
,'reload': function(){
this.deferUntilTransition('loading')
return this.transition('loading')
}
}
})
.create()
gun.currentState == 'uninitialized' // true
What is a Possum?
A marsupial.
But for our purposes, possum
uses stampit under-the-hood for
model prototyping.
This means that when you do this:
var p = possum.states({ ... }).create()
A possum will compose any settings you put on the top level possum call and then exposes this to the api (see below for more details):
possum
.config( /* configure initialState, namespace, etc */) // alias to stampit `props`
.states( /* configure states */)
.methods(/* special methods */) //stampit method
.init(function(){
//initializing stuff
var secretData = 'shhhh'
this.prop == 'erty' // -> true
}) //stampit method
.compose(/* mixin other stamps into the machine */) //stampit method
.target(/* the state object to use (default is the machine itself) */)
.create() //this creates the possum instance; you may also just call it as a function
Isolation and prototypes
Every call to the builder functions on possum
return a new stamp. This is important
to know and also to take advantage of. Consider the following:
let loggable = possum.compose(logger)
let eventable = possum.compose(eventSourcer)
let kitchen = loggable.compose(eventable)
let model = loggable.config({initialState: 'a'}).create() //has loggable, not eventable, behavior
let model2 = eventable.config({initialState:'a'}).create() //has eventable, not loggable, behavior
let everything = kitchen.config({initialState:'a' }).create() // has both
See the tests here for seeing in action.
Asynchronous and synchronous transitions and handlers
Oftentimes handlers end up being async, breaking all the callers. Initially
possum
did Promises for all api calls.
As of v0.1.0
possum supports both synchronous and Promised handlers.
It is worth noting that the events which are emitted are ordered differently depending on the flow model you choose.
Behavioral State Machine support
Possum supports splitting the control state from the target model being acted upon using target()
.
The value of target
by default is the machine instance itself but each handler, except for _enter
and _exit
,
will receive the target
instance as the second argument in every handler.
// one approach
let machine = possum.config({initialState:'ready'})
.states({
ready: {
// target is passed in as secon argument
go(args, target) {
//1. mutate the target model here. It can consume the target's API, or whatever
//2. determine the next state to transition based on the target state
//3. do it
target.doThatThing(args)
if(target.isGoing) {
retun this.transition('going')
}
//nextAction (manually calced)
if(target.isWaiting) {
return this.handle('wait', args)
}
},
wait(args, target) {
}
}
going: {
// special handlers only receive the target as argument
_enter: (target) {
this.emit('going')
},
complete(args, target) { }
}
})
let modelApi = {
doThatThing(args) {
this.isGoing = args.isGoing; // like from a checkbox
this.isWaiting = !args.isGoing;
},
}
//create machine instance and set the target
// you may also set `target` on the machine prototype...
let state = machine().target(modelApi)
// from view
state.handle('go', { isGoing: true }); //target will always be passed as second arg in input handler!
state.currentState == 'going' // true
Possum Builder API (extensions atop stampit)
config
{Object} required
These attributes can (should) be set here:
namespace
{String} [optional]
The namespace for this instance
var p = possum.config({
namespace: 'secure'
})
initialState
{String} required
The state to transition to initially
var p = possum.config({
initialState: 'uninitialized'
})
NOTE:
- the
_enter
callback will NOT get called immediately upon construction if the state designated byinitialState
has one. This is to avoid the need for asynchronous instantiation support. If really need an initialization routing you can just use stampit's built in facility for this:
var p = possum
.config({
initialState: 'a'
})
.states({
'a': {
'_enter': function(){
//wont get called on creation!
}
}
})
.init(function(){
//do init stuff here
//...you can even return a Promise and stampit will return the promise
//for you to control it
})
states
{Object} required
The states handlers configuration in the shape of:
var states = {
'myState': {
_enter: function( target ){
//optional
//steps to perform right when entering a state
//can return an Promise for async support
}
,'doIt': function(args, target) {
//optionally make changes to the state target
//handle the command 'doIt'
//receiving exactly ONE argument
return this.doIt()
}
...
,_exit: function( target ) {
//optional
//steps to perform right before transitioning
//out of this state
}
}
}
Note that each state's input handler, will receive one arguments
parameter. That means you must
invoke the handlers this way:
model.handle('doIt','myArgument')
Additional arguments will be ignored.
emit
{Function} required
The implementation of event emitter is left up to you, the implementer. That means that
possum
doesn't come bundled with a eventing API out-of-the-box.
To use possum with eventemitter2
you can do something like this:
var EventEmitter2 = require('eventemitter2').EventEmitter2;
var emittable = stampit.convertConstructor(EventEmitter2);
var machine = possum.compose(emittable).states(...)
Possum Instance API
currentState
{String}
The current state of the possum instance.
This is the same as initialState
upon creation
priorState
{String}
The priorState state of the possum instance, if any.
This is undefined
until a transition has occurred.
namespaced([str,namespace])
{Function}
Receives an optional str
argument to produce an namespaced string using underlying delimiter rules.
If str
is not provided, the instance's namespace
is returned (from the spec).
If the instance has an undefined
namespace the input namespace
argument is used; if that is undefined
the str
is returned.
This utility is used for producing underlying events for subscription; eg :
possumInstance.on(possumInstance.namespaced('handled'),function(){...})
handle(inputType, args)
{Function}
Queues the command inputType and processes it with the singular args payload. Note that only one argument will be used. Other arguments will be ignored.
Returns the result of the handler (Promise or not)
transition(toState)
{Function}
Convenience method that queues the transition commands exit
, _transition
, and _onEnter
and processes them.
Returns a Promise if an _enter
or _exit
handler does so; otherwise, it returns
the possum instance.
deferUntilTransition(toState)
{Function}
Queues the current message (input) to be replayed after the possum has transitioned
to toState
. If toState
is not provided, then it will replay after any
transition has occurred.
Returns this
possum instance.
Possum Events
{namespace}.handling
Emitted during an input handler.
Event properties:
- topic: 'handling'
- inputType: {String} the name of the handler you just called
- payload: {Any} The arguments passed into the
handle
call - action: {String} the path of the handler you just called ; eg 'myState.myHandler'
- namespace: {String} the namespace of the possum
{namespace}.invoked
Emitted just after an input handler has been invoked but possibly before the handler has completed (asynchronously).
Event properties:
- topic: 'invoked'
- inputType: {String} the name of the handler you just called
- payload: {Any} The arguments passed into the
handle
call - action: {String} the path of the handler you just called ; eg 'myState.myHandler'
- namespace: {String} the namespace of the possum
{namespace}.handled
Emitted after an input handler Promise has resolved (if async).
Event properties:
- topic: 'handled'
- inputType: {String} the name of the handler you just called
- payload: {Any} The arguments passed into the
handle
call - action: {String} the path of the handler you just called ; eg 'myState.myHandler'
- namespace: {String} the namespace of the possum
{namespace}.transitioned
Emitted after a possum has transitioned into a state,
and after its entry callback has been invoked (_enter
).
Event properties:
- topic: 'transitioned'
- inputType: {String} the name of the handler you just called
- payload: {Object} having these properties
toState
The state you just transitioned tofromState
The state you just transitioned from
- action: {String} the path of the handler you just called ; eg 'myState.myHandler'
- namespace: {String} the namespace of the possum
{namespace}.noHandler
Emitted when an input has been attempted on a state that does not declare it.
{namespace}.invalidTransition
Emitted when an transition is attempted to a state that does not exist.
Tests
make test
will by default run tests on Node.make browser
will run tape tests on any browser you visit athttp://localhost:2222
Roadmap
- Hierarchical State Machine support
- Behavior tree generation (ala machine.js).
Acknowledgements
API inspiration from machina.js.