Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend state machine #5

Merged
merged 7 commits into from
Feb 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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