Skip to content

Commit

Permalink
Merge pull request #5 from mihaidma/extendsm
Browse files Browse the repository at this point in the history
Extend state machine. 

Thank you @mihaidma
  • Loading branch information
mirceaalexandru committed Feb 2, 2016
2 parents 1360fa1 + 85eb728 commit f57dbe2
Show file tree
Hide file tree
Showing 11 changed files with 519 additions and 131 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ npm-debug.log

*~
.idea
.vscode
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ npm install seneca-sm

#### Initialisation

```sh
```js
seneca.act( "role: 'sm', create: 'instance'", {config: sm_configuration}, function( err, context ) {
})
```

#### Executing commands

```sh
```js
seneca.act( "role: 'sm-name', cmd: 'command-name'", some_data, function( err, data ) {
})
```
Expand All @@ -47,7 +47,7 @@ where:

#### Retrieving state machine context

```sh
```js
seneca.act( "role: 'sm-name', get: 'context'", function( err, context ) {
})
```
Expand All @@ -56,21 +56,29 @@ seneca.act( "role: 'sm-name', get: 'context'", function( err, context ) {

This command will set some data in the state machine context. This data will be sent to all commands executed on the state machine.

```sh
```js
seneca.act( "role: 'sm-name', set: 'data'", some_data, function( err, context ) {
})
```

### Load a specific state-machine context

This command can be called after a sm is initialized to change its internal state from the default state to a specific one

```js
seneca.act( "role: 'sm-name', load: 'state'", { sm_name: some_sm_name, state: state_to_load}, function( err, context ) {
})
```

#### Remove state-machine context

This command will close the state machine. This state machine cannot be used anymore. A new state machine with same name can be started.

```sh
```js
seneca.act( "role: 'sm-name', drop: 'instance'", function( err, context ) {
})
```


### Configuration

Configuration structure for state machine is:
Expand All @@ -80,6 +88,11 @@ Configuration structure for state machine is:
* _states_ object defining the states and commands. Key is the state and value an object with
* _defaults_ default behavior for this state - TBD
* _initState_ default state for state machine. One single state should have this parameter true
* _events_ allows adding event hooks trigered when the state changes
* _before_ called before the state execution - can be the child of the root _states_ object or child of a state
* _pattern_ seneca pattern defining the action to be called before the state execution
* _after_ called after a state is executed - can be the child of the root _states_ object or child of a state
* _pattern_ seneca pattern defining the action to be called after the state execution
* _commands_ array with all commands for current state
* _key_ command to be executed for this state
* _pattern_ seneca pattern defining the action to be called to execute the state
Expand All @@ -106,6 +119,14 @@ The configuration to be used for this state machine is:
validate: true,
name: 'sm1',
states: {
events: {
before: {
pattern: "role: 'transport', execute: 'before_any_state_change'"
},
after: {
pattern: "role: 'transport', execute: 'after_any_state_change'"
}
},
"INIT": {
initState: true,
defaults: {
Expand Down Expand Up @@ -144,6 +165,14 @@ The configuration to be used for this state machine is:
success: "DISCONNECTED"
}
}
},
events: {
before: {
pattern: "role: 'transport', execute: 'before_notconfigured_state_change'"
},
after: {
pattern: "role: 'transport', execute: 'after_notconfigured_state_change'"
}
}
},
"CONNECTED": {
Expand Down
134 changes: 82 additions & 52 deletions lib/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,99 @@

var _ = require('lodash')

function Configuration (config) {
this.config = config
}

Configuration.prototype.validate = function () {
var initStateOK = false
module.exports = {
validate: function (config) {
var initStateOK = false

for (var i in this.config.states) {
if (this.config.states[i].initState) {
if (initStateOK) {
return 'One single state should have initState: true'
for (var i in config.states) {
if (config.states[i].initState) {
if (initStateOK) {
return 'One single state should have initState: true'
}
initStateOK = true
}
initStateOK = true
}
}
if (!initStateOK) {
return 'One single state should have initState: true'
}
return
}
if (!initStateOK) {
return 'One single state should have initState: true'
}
return
},

Configuration.prototype.getInitState = function () {
for (var state_name in this.config.states) {
if (this.config.states[state_name].initState) {
return state_name
getInitState: function (config) {
for (var state_name in config.states) {
if (config.states[state_name].initState) {
return state_name
}
}
}
}
},

Configuration.prototype.getCommands = function () {
var cmds = {}
for (var state_name in this.config.states) {
for (var command_name in this.config.states[state_name].commands) {
cmds[command_name] = {
// maybe I will need something here
getCommands: function (config) {
var cmds = {}
for (var state_name in config.states) {
for (var command_name in config.states[state_name].commands) {
cmds[command_name] = {
// maybe I will need something here
}
}
}
}
return cmds
}
return cmds
},

Configuration.prototype.processDefaults = function () {
for (var state_name in this.config.states) {
if (this.config.states[state_name].defaults) {
for (var command_name in this.config.states[state_name].commands) {
this.config.states[state_name].commands[command_name].pattern =
this.config.states[state_name].commands[command_name].pattern ||
this.config.states[state_name].defaults.pattern
this.config.states[state_name].commands[command_name].next = _.extend(
{},
this.config.states[state_name].defaults.next || {},
this.config.states[state_name].commands[command_name].next || {}
)
processDefaults: function (config) {
for (var state_name in config.states) {
if (config.states[state_name].defaults) {
for (var command_name in config.states[state_name].commands) {
config.states[state_name].commands[command_name].pattern =
config.states[state_name].commands[command_name].pattern ||
config.states[state_name].defaults.pattern
config.states[state_name].commands[command_name].next = _.extend(
{},
config.states[state_name].defaults.next || {},
config.states[state_name].commands[command_name].next || {}
)
}
}
}
}
}
},

Configuration.prototype.retrieve_command = function (state, cmd) {
if (this.config.states[state].commands[cmd]) {
return this.config.states[state].commands[cmd]
retrieve_command: function (config, state, cmd) {
if (config.states[state].commands[cmd]) {
return config.states[state].commands[cmd]
}
return
},

retrieve_events: function (config, state) {
// local event handlers
var events = config.states[state].events
if (events) {
return events
}

// global event handlers
events = config.states.events
if (events) {
return events
}

return null
},

retrieve_before_command: function (config, state) {
var events = this.retrieve_events(config, state)
if (events && events.before) {
return events.before
}

return null
},

retrieve_after_command: function (config, state) {
var events = this.retrieve_events(config, state)
if (events && events.after) {
return events.after
}

return null
}
return
}

module.exports = Configuration
79 changes: 62 additions & 17 deletions lib/sm.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use strict'

var Configuration = require('./rules')
var State = require('./state')
var ConfigurationHelper = require('./rules')
var StateHelper = require('./state')
var _ = require('lodash')
var Async = require('async')

module.exports = function (options) {
var seneca = this
Expand All @@ -25,32 +26,30 @@ module.exports = function (options) {
}

function create (args, done) {
var rules = new Configuration(args)
var sm_name = rules.config.name
var config = args
var sm_name = config.name

if (internals[sm_name]) {
return done('SM already exists')
}

var validate_error = rules.validate()
var validate_error = ConfigurationHelper.validate(config)
if (validate_error) {
return done(validate_error)
}

var context = {
rules: rules,
config: config,
commands: {}
}
context.status = new State(seneca, context)

internals[sm_name] = context

context.current_status = rules.getInitState()
context.current_status = ConfigurationHelper.getInitState(config)

// now process defaults
rules.processDefaults()
ConfigurationHelper.processDefaults(config)

var cmds = rules.getCommands()
var cmds = ConfigurationHelper.getCommands(config)
for (var command_name in cmds) {
if (context.commands[command_name]) {
// this command was already registered
Expand All @@ -65,6 +64,7 @@ module.exports = function (options) {

seneca
.add({role: sm_name, get: 'context'}, get_context)
.add({role: sm_name, load: 'state'}, load_state)

done()
}
Expand All @@ -78,18 +78,63 @@ module.exports = function (options) {
done(null, _.clone(context))
}

function load_state (args, done) {
if (!args.sm_name && !args.state) {
return done('invalid load state arguments')
}

var context = internals[args.sm_name]

if (!context) {
return done('state-machine ' + args.sm_name + ' does not exist')
}

StateHelper.change(context, args.state)

return done(null, _.clone(context))
}

function execute_state (args, done) {
var seneca = this
var context = internals[args.role]
var command = context.rules.retrieve_command(context.current_status, args.cmd)
var command = ConfigurationHelper.retrieve_command(context.config, context.current_status, args.cmd)
var beforeCommand = ConfigurationHelper.retrieve_before_command(context.config, context.current_status)
var afterCommand = ConfigurationHelper.retrieve_after_command(context.config, context.current_status)

delete args.role
delete args.cmd

this.act(command.pattern, args, function (err, data) {
context.status.findNextState(command, err, data, function (state_err, nextState) {
context.status.change(nextState)
done(err, data)
})
Async.series({
before_command: function (callback) {
if (!beforeCommand) {
return callback()
}
seneca.act(beforeCommand.pattern, function(err, data) {
callback(err, data)
})
},
command: function (callback) {
seneca.act(command.pattern, args, function (err, data) {
StateHelper.findNextState(command, err, data, function (state_err, nextState) {
if (!nextState) {
return callback(new Error('undefined next state error'))
}
StateHelper.change(context, nextState)
return callback(err, data)
})
})
},
after_command: function (callback) {
if (!afterCommand) {
return callback()
}
seneca.act(afterCommand.pattern, function(err, data) {
callback(err, data)
})
}
}, function (err, results) {
// the "command" results are returned, the "before_command" and "after_command" ones are not returned
done(err, results.command)
})
}

Expand Down

0 comments on commit f57dbe2

Please sign in to comment.