Skip to content

Commit

Permalink
mostly attempted to flesh out new implementation for statecharts and …
Browse files Browse the repository at this point in the history
…failed. only slight tweaks to existing impl.
  • Loading branch information
Brec Carson committed Mar 19, 2012
1 parent bdcd143 commit 75ee0be
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 15 deletions.
54 changes: 54 additions & 0 deletions examples/foo-tree.coffee
@@ -0,0 +1,54 @@
# DOES NOT WORK YET. BASED ON STATECHART IMPLEMENTATION THAT IS NOT DONE

StateChartTree = require '../src/stateForest'

class BeerStein extends StateChartTree

@addStateTree 'EMPTY':
actions: 'filling'
methods:
onStartFilling: -> console.log "i'm filling up"
onStopFilling: -> console.log "i'm halfway up"

@addStateTree 'HALFWAY':
actions: 'filling'
methods:
onStartFilling: -> console.log "filling up continued"
onStopFilling: -> console.log "i'm full"

@addStateTree 'FULL':
actions: 'drinking,pouring'
methods: ->
onStartDrinking: -> console.log "i'm getting drunk (get it :))"
onStopDrinking: -> console.log "boo!"
onStartPouring: -> console.log "better be for your homies."
onStopPouring: -> console.log ""


constructor: ->
console.log "new foo object constructed."
console.log "default state is #{@state}"


mug = new BeerStein

mug.start 'filling'
mug.stop 'filling'

mug.state = "FULL"
try
mug.start 'filling'
catch e
console.log "ERROR: #{e}"

mug.start 'drinking'

try
mug.state = "HALFWAY"
catch e
console.log "ERROR: #{e}"

mug.stop 'drinking'

mug.state = "HALFWAY"
mug.start 'filling'
8 changes: 4 additions & 4 deletions examples/foo.coffee → examples/pint.coffee
@@ -1,6 +1,6 @@
{StateChartGraph} = require '../src/stateful'
Stateful = require '../src/stateful'

class Foo extends StateChartGraph
class PintGlass extends Stateful

@addState "EMPTY",
transitions:
Expand Down Expand Up @@ -32,9 +32,9 @@ class Foo extends StateChartGraph
@buildStateChart()

doTest: -> console.log "should not get called. maybe if super is called?"
testCommon: -> return "class Foo has two states: 'EMPTY' and 'FULL' we're currently in #{@state}"
testCommon: -> return "class Foo has #{@numStates} states: #{@listStates}. we're currently in #{@state}"

foo = new Foo
foo = new PintGlass

foo.on "stateChange", (from, to) -> console.log "state was #{from}, state is #{to}"

Expand Down
124 changes: 124 additions & 0 deletions src/stateForest.coffee
@@ -0,0 +1,124 @@
# WORK IN PROGRESS


# --------------------------------------------------------------------------------------------------------------------

# think of it this way, States are Adjectives and action are Verbs
# A class becomes a state but it starts/stops an action

###
@addStateTree 'UNSAVED': # I am an Unsaved Model
actions: 'Editing,Saving' # As an Unsaved Model, my actions are Editing and Saving
methods:
foo: -> console.log bar
@addStateTree 'SAVED': # I am a Saved Model
'UNLOADED': # I am a Saved Model but am UNLOADED
actions: 'Loading' # As a Saved, Unloaded model my actions are Loading
methods: require './model-states/unloaded'
'LOADED': # etc...
actions: 'Updating,Editing' # do actions need designated exit states?
'DIRTY':
actions: 'Syncing'
'CONFLICTED':
actions: 'Editing'
###
# how do we change between major tree branches?
# Is it legal to jump from SAVED->LOADED->DIRTY to UNSAVED? What prevents this?

# Legal Statechanges:
#
# Can only change between siblings of the same parent or to a direct descendant or to any ancestor
#
# IE:
# UNLOADED can change to LOADED but not DIRTY.
# LOADED can become DIRTY but not CONFLICTED
# CONFLICTED can become DIRTY or LOADED

# what role do actions play?
# Actions are handled like delegated events but only when in the right state. what causes the event? -> code, programmatic
# model.start('editing') ... model.stop('editing') -> good
# model.start('editing') ... model.start('saving') -> error
# model.start('editing') ... model.become('DIRTY') -> error
# model.state = "LOADED" ... model.start('syncing') -> error

# how do we incorporate state specific methods or method implementations as part of the configuration?
# methods hash that can either be filled in directly or use a require to load a separate file.


class Tree extends Emitter

@addStateTree: (tree) ->
@::__stateForest = {} unless @::__stateForest?

# TODO: validate Tree

@::__stateForest = _.extend @::__stateForest, tree

Object.defineProperty @prototype, 'states',
get: -> @__states or @__states = _.keys @__stateForest
Object.defineProperty @prototype, 'tree',
get: -> @__tree
Object.defineProperty @prototype, 'state',
get: -> @__state
set: (state) -> @changeState state

constructor: (config) ->
unless config.state then @state = @states[0]

changeState: (to) ->
from = @state
return if from is to

# if from isnt undefined and not @isValidStateChange(from, to)
# throw new Error "Bad state change: can't change from the '#{@state}' state to '#{to}' state on #{@constructor.name}"

# Could introduce an extension point here that would allow child class to do a check before the state changes
# and if the callback returns true would abort the state change.

@state = to

@onStateChange(from, to) if @onStateChange?

# remove methods for actions in the fromState
@removeActionMethods from

# add methods for actions in the toState
@addActionMethods to

# unapply methods in the fromState
fromMethods = @__stateChart[from]?.methods
@unapplyMethod method for method of fromMethods

# apply methods in the toState
toMethods = @__stateChart[to]?.methods
@applyMethod method, impl for method, impl of toMethods

@emit 'stateChange', from, to
@emit "stateChange:#{to}", from


# isValidState: (state) -> @__stateChart[state]?
# isValidStateChange: (from, to) ->
# fromConfig = @__stateChart[from]
# toConfig = @__stateChart[to]
#
# if not fromConfig? then throw new Error("Bad state change: can't change from the non-existent state '#{from}'")
# if not toConfig? then throw new Error("Bad state change: can't change to the non-existent state '#{to}'")
#
# return _.contains(fromConfig.exit, to) and _.contains(toConfig.enter, from)

unapplyMethod: (method) ->
console.log "unapply #{method}"
if @[method]?
delete @[method]
@[method] = null

applyMethod: (method, impl) ->
console.log "apply #{method} -> #{impl}"
@[method] = impl

exports.StateChartTree = Tree
20 changes: 9 additions & 11 deletions src/stateful.coffee
@@ -1,11 +1,17 @@
Emitter = require 'common-emitter'
_ = require 'underscore'

class Graph extends Emitter
# NOTE: I'm considering this deprecated in favor of the new Tree implementation
# (assuming I can get that to work!)

class Stateful extends Emitter

Object.defineProperty @prototype, 'state',
get: -> @__state
set: (state) -> @changeState(state)

Object.defineProperty @prototype, 'numStates', get: -> _.size @__stateChart
Object.defineProperty @prototype, 'listStates', get: -> _.keys(@__stateChart).join ', '

@addState: (stateName, config) ->
@::__stateChart = {} unless @::__stateChart?
Expand Down Expand Up @@ -49,8 +55,7 @@ class Graph extends Emitter
validateDirection(state, config, 'enter')
validateDirection(state, config, 'exit')

constructor: (config) ->
super(config)
constructor: ->
@state = @__initialState

is: (state) ->
Expand Down Expand Up @@ -118,11 +123,4 @@ class Graph extends Emitter
@once "stateChange:#{state}", callback


exports.StateChartGraph = Graph

class Tree extends Emitter

@addState: (stateNameOrTree, config) ->
console.log "implement me"

exports.StateChartTree = Tree
module.exports = Stateful

0 comments on commit 75ee0be

Please sign in to comment.