class Stativus::Statechart

Attributes

active_subtrees[RW]
all_states[RW]
current_state[RW]
current_subtrees[RW]
goto_state_locked[RW]
pending_events[RW]
pending_state_transitions[RW]
send_event_locked[RW]
states_with_concurrent_substates[RW]

Public Class Methods

new() click to toggle source
# File lib/stativus.rb, line 110
def initialize()
  @all_states = {}
  @all_states[DEFAULT_TREE] = {}
  @states_with_concurrent_substates = {}
  @current_subsates = {}
  @current_state = {}
  @current_state[DEFAULT_TREE] = nil
  @goto_state = false
  @send_event_locked = false
  @pending_state_transitions = []
  @pending_events = []
  @active_subtrees = {}
  @goto_state_locked = false
end

Public Instance Methods

add_state(state_class) click to toggle source
# File lib/stativus.rb, line 125
def add_state(state_class)
  state = state_class.new(self)
  tree = state.global_concurrent_state
  parent_state = state.parent_state
  current_tree = @states_with_concurrent_substates[tree]

  if(state.has_concurrent_substates)
    obj = @states_with_concurrent_substates[tree] || {}
    obj[state.name] = true
    @states_with_concurrent_substates[tree] = obj
  end

  if(parent_state and current_tree and current_tree[parent_state])
    parent_state = @all_states[tree][parent_state]
    if(parent_state)
      parent_state.substates.push(state.name)
    end
  end

  obj = @all_states[tree] || {}

  obj[state.name] = state

  sub_states = state.states || []
  
  sub_states.each do |sub_state|
    sub_state.parent_state = state
    sub_state.global_concurrent_state = tree
    add_state(sub_state)
  end
end
goto_state(requested_state_name, tree, concurrent_tree=nil) click to toggle source
# File lib/stativus.rb, line 167
def goto_state(requested_state_name, tree, concurrent_tree=nil)
  all_states = @all_states[tree]
 
  #First, find the current tree off of the concurrentTree, then the main tree
  curr_state = concurrent_tree ? @current_state[concurrent_tree] : @current_state[tree]
  
  requested_state = all_states[requested_state_name]

  # if the current state is the same as the requested state do nothing
  return if(check_all_current_states(requested_state, concurrent_tree || tree))
  
  
  if(@goto_state_locked)
    #There is a state transition currently happening. Add this requested
    #state transition to the queue of pending state transitions. The req
    #will be invoked after the current state transition is finished
    @pending_state_transitions.push({
      :requested_state => requested_state_name,
      :tree => tree
    })
    return
  end
  # Lock for the current state transition, so that it all gets sorted out
  # in the right order
  @goto_state_locked = true
  
  # Get the parent states for the current state and the registered state.
  # we will use them to find the commen parent state
  enter_states = parent_states_with_root(requested_state)
  exit_states = curr_state ? parent_states_with_root(curr_state) : []
  #
  # continue by finding the common parent state for the current and
  # requested states:
  #
  # At most, this takes O(m^2) time, where m is the maximum depth from the
  # root of the tree to either the requested state or the current state.
  # Will always be less than or equal to O(n^2), where n is the number
  # of states in the tree
  enter_match_index = nil
  exit_match_index = 0
  exit_states.each_index do |idx|
    exit_match_index = idx
    enter_match_index = enter_states.index(exit_states[idx])
    break if(enter_match_index != nil)    
  end
  
  # In the case where we don't find a common parent state, we
  # must enter from the root state
  enter_match_index = enter_states.length()-1 if(enter_match_index == nil)

  #setup the enter state sequence
  @enter_states = enter_states
  @enter_state_match_index = enter_match_index
  @enter_state_concurrent_tree = concurrent_tree
  @enter_state_tree = tree
  
  # Now, we will exit all the underlying states till we reach the common
  # parent state. We do not exit the parent state because we transition
  # within it.
  @exit_state_stack = []
  full_exit_from_substates(tree, curr_state) if(curr_state != nil and curr_state.has_concurrent_substates)
  
  if exit_match_index == nil || exit_match_index-1 < 0
    exit_match_index = 0 
  else
    0.upto(exit_match_index-1) do |i|
      @exit_state_stack.push(exit_states[i])
    end
  end
  
  #Now that we have the full stack of states to exit
  #we can exit them...
  
  unwind_exit_state_stack();
  
end
send_event(evt, *args) click to toggle source
# File lib/stativus.rb, line 244
def send_event(evt, *args)
  
  # We want to prevent any events from occurring until
  # we have completed the state transitions and events
  if @in_initial_setup or @goto_state_locked or @send_event_locked
    @pending_events.push({
      :evt => evt,
      :args => args
    })
    return
  end
  
  @send_event_locked = true
  
  structure_crawl(evt, args)
  
  # Now, that the states have a chance to process the first action
  # we can go ahead and flush the queued events
  @send_event_locked = false;
  
  flush_pending_events() unless @in_initial_setup      
    
end
start(state) click to toggle source

call this in your programs main state is the initial state of the application in the default tree

# File lib/stativus.rb, line 160
def start(state)
  @in_initial_setup = true
  self.goto_state(state, DEFAULT_TREE)
  @in_initial_setup = false
  flush_pending_events
end

Private Instance Methods

cascade_enter_substates(start, required_states, index, tree, all_states) click to toggle source
# File lib/stativus.rb, line 445
def cascade_enter_substates(start, required_states, index, tree, all_states)
  return unless start
  name = start.name
  @enter_state_stack.push(start)
  @current_state[tree] = start
  start.local_concurrent_state = tree
  if(start.has_concurrent_substates)
    tree = start.global_concurrent_state || DEFAULT_TREE
    next_tree = [SUBSTATE_DELIM,tree,name].join("=>")
    start.history = start.history || {}
    substates = start.substates || []
    substates.each do |x|
      next_tree = tree +"=>"+x
      curr_state = all_states[x]
      
      # Now, we have to push the item onto the active subtrees for
      # the base tree for later use of the events.
      b_tree = curr_state.global_concurrent_state || DEFAULT_TREE
      a_trees = active_subtrees[b_tree] || []
      a_trees.unshift(next_tree)
      @active_subtrees[b_tree] = a_trees
      index -=1 if(index > -1 && required_states[index] == curr_state)
      cascade_enter_substates(curr_state, required_states, index, next_tree, all_states)
    end
    return
  else
    curr_state = required_states[index]
    if(curr_state and curr_state.is_a?(State))
      parent_state = all_states[curr_state.parent_state]
      if(parent_state)
        if(parent_state.has_concurrent_substates)
          parent_state.history[tree] = curr_state.name
        else
          parent_state.history = current_state.name
        end
      end #end parent state
      
      index -=1 if(index > -1 && required_states[index] == curr_state)
      cascade_enter_substates(curr_state, required_states, index, tree, all_states)
    else
      curr_state = all_states[start.initial_substate]
      cascade_enter_substates(curr_state, required_states, index, tree, all_states)
    end
  end
  
end
cascade_events(evt, args, responder, all_states, tree) click to toggle source
# File lib/stativus.rb, line 312
def cascade_events(evt, args, responder, all_states, tree)
  
  found = false
  handled = nil
  if(tree)
    trees = tree.split('=>')
    ss_name = trees.last
  end
  
  while(not handled and responder)
    if(responder.respond_to?(evt))
      method = responder.method(evt)
      #if (DEBUG_MODE) console.log(['EVENT:',responder.name,'fires','['+evt+']', 'with', args.length || 0, 'argument(s)'].join(' '));
      if(method.arity != 0)
        handled = method.call(args)
      else
        handled = method.call()
      end
      #ruby has implict returns therefore you actually
      #have to explicitly return true
      handled = handled == false ? false : true
      found = true
    end
    
    #check to see if we're at the end of the tree
    return [handled, found] if(tree and ss_name == responder.name)
    responder = (!handled && responder.parent_state) ? all_states[responder.parent_state] : nil
  end
  
  return [handled, found]
end
check_all_current_states(requested_state, tree) click to toggle source
# File lib/stativus.rb, line 541
def check_all_current_states(requested_state, tree)
  current_states = @current_state[tree] || []
  
  if(current_states == requested_state)
    return true
  elsif(current_states.class == String and requested_state == @all_states[tree][current_states])
    return true
  elsif(current_states.class == Array and current_states.include?(requested_state))
    return true
  else
    return false
  end
end
flush_pending_events() click to toggle source
# File lib/stativus.rb, line 535
def flush_pending_events
  pending_event = @pending_events.shift()
  return if(pending_event == nil)
  self.send_event(pending_event.evt, pending_event.args)
end
flush_pending_state_transitions() click to toggle source
# File lib/stativus.rb, line 528
def flush_pending_state_transitions
  pending = @pending_state_transitions.shift
  return false unless pending
  goto_state(pending.requested_state, pending.tree)
  return true
end
full_enter(state) click to toggle source
# File lib/stativus.rb, line 378
def full_enter(state)
  return unless state
  enter_state_handled = false
  #if (DEBUG_MODE) console.log('ENTER: '+state.name);
  state.enter if state.respond_to?(:enter)
  state.did_enter_state if state.respond_to?(:did_enter_state)
  unwind_enter_state_stack()
end
full_exit(state) click to toggle source
# File lib/stativus.rb, line 369
def full_exit(state)
  return unless state
  exit_state_handled = false
  state.exit if(state.respond_to?(:exit))
  state.did_exit_state if(state.respond_to?(:did_exit_state))
  #todo: if (DEBUG_MODE) console.log('EXIT: '+state.name);
  unwind_exit_state_stack
end
full_exit_from_substates(tree, stop_state) click to toggle source
# File lib/stativus.rb, line 388
def full_exit_from_substates(tree, stop_state)
  
  return if(!tree || !stop_state)
  
  all_states = @all_states[tree]
  curr_states = @current_state
  @exit_state_stack = @exit_state_stack || []
  
  stop_state.substates.each do |state|
    substate_tree = [SUBSTATE_DELIM, tree, stop_state.name, state].join("=>")
    curr_state = curr_states[substate_tree]
    
    while(curr_state and curr_state != stop_state)
      exit_state_handled = false
      @exit_state_stack.unshift(curr_state)
      
      #check to see if it has substates
      full_exit_from_substates(tree, curr_state) if(curr_state.has_concurrent_substates)
      
      #up to the next parent
      curr = curr_state.parent_state
      curr_state = all_states[curr]
    end
    
  end
  
end
initiate_enter_state_sequence() click to toggle source
# File lib/stativus.rb, line 416
def initiate_enter_state_sequence
  enter_states = @enter_states
  enter_match_index = @enter_state_match_index
  concurrent_tree = @enter_state_concurrent_tree
  tree = @enter_state_tree
  all_states = @all_states[tree]
  
  #initalize the enter state stack
  @enter_state_stack = @enter_state_stack || []
  
  # Finally, from the common parent state, but not including the parent state,
  # enter the sub states down to the requested state. If the requested state
  # has an initial sub state, then we must enter it too
  i = enter_match_index - 1
  curr_state = enter_states[i]
  if(curr_state)
    cascade_enter_substates(curr_state, enter_states, 
                            (i-1), concurrent_tree || tree, all_states)
  end
  
  #once, we have fully hydrated the Enter State Stack, we must actually async unwind it
  unwind_enter_state_stack
  
  #cleanup
  @enter_states = @enter_state_match_index = @enter_state_concurrent_tree = 
  @enter_state_tree = nil
  
end
parent_states(state) click to toggle source

returns an array of all the parent states of the passed state

# File lib/stativus.rb, line 566
def parent_states(state)
  ret = []
  curr = state
  
  ret.push(curr)
  curr = state_object(curr.parent_state, curr.global_concurrent_state)
  
  while(curr)
    ret.push(curr)
    curr = state_object(curr.parent_state, curr.global_concurrent_state)
  end
  
  return ret
end
parent_states_with_root(state) click to toggle source

creates an array of all a states parent states ending with a string of “root” to indcate the root state

# File lib/stativus.rb, line 584
def parent_states_with_root(state)
  ret = parent_states(state)
  ret.push('root')
  return ret
end
state_object(name, tree) click to toggle source

returns the state object for a passed name and tree was called _parentStateObject in js

# File lib/stativus.rb, line 557
def state_object(name, tree)
  if(name && tree && @all_states[tree] && @all_states[tree][name])
    return @all_states[tree][name]
  end
end
structure_crawl(evt, args) click to toggle source

Private functions

# File lib/stativus.rb, line 272
def structure_crawl(evt, args)
  current_states = @current_state
  ss = Stativus::SUBSTATE_DELIM
  
  for tree in current_states.keys
    next unless tree
    handled = false
    s_tree = nil
    responder = current_states[tree]
    
    next if(!responder or tree.slice(0, ss.length()) == ss)
    
    all_states = @all_states[tree]
    
    next unless all_states
    
    a_trees = @active_subtrees[tree] || []
    
    #0.upto(a_trees.length()-1) do |i|
    a_trees.each do |s_tree|
      #s_tree = a_trees[i]
      s_responder = current_states[s_tree]
      tmp = handled ? [true, true] : cascade_events(evt, args, s_responder, all_states, s_tree)
      handled = tmp[0]
      #if (DEBUG_MODE) found = tmp[1];
    end
    if(not handled)
      tmp = cascade_events(evt, args, responder, all_states, nil)
      handled = tmp[0]
      # if (DEBUG_MODE){
      #   if (!found) found = tmp[1];
      # }
      
    end        
    # if (DEBUG_MODE){
    #   if(!found) console.log(['ACTION/EVENT:{'+evt+'} with', args.length || 0, 'argument(s)','found NO state to handle this'].join(' '));
    # }
  end
end
unwind_enter_state_stack() click to toggle source
# File lib/stativus.rb, line 492
def unwind_enter_state_stack

  @exit_state_stack = @exit_state_stack || []
  state_to_enter = @enter_state_stack.shift()
  
  if state_to_enter
    if state_to_enter.respond_to?(:will_enter_state)
      
      state_restart = {
        :statechart => self,
        :start => state_to_enter
      }
      state_restart[:restart] = Proc.new{
        #if (DEBUG_MODE) console.log(['RESTART: after async processing on,', this._start.name, 'is about to fully enter'].join(' '));
        @statechart.full_enter(state_restart[:state_to_enter])
      }
      delay_for_async = state_to_enter.will_enter_state(state_restart)
      # if (DEBUG_MODE) {
      #   if (delayForAsync) { console.log('ASYNC: Delayed enter '+stateToEnter.name); }
      #   else { console.warn('ASYNC: Didn\'t return \'true\' willExitState on '+stateToEnter.name+' which is needed if you want async'); }
      # }
    end
    full_enter(state_to_enter) unless delay_for_async
  
  else  
    @enter_state_stack = nil
    # Ok, we're done with the current state transition. Make sure to unlock
    # the goToState and let other pending state transitions
    @goto_state_locked = false
    more = flush_pending_state_transitions
    # Once pending state transitions are flushed then go ahead and start flush
    # pending actions
    flush_pending_events if not more and not @in_initial_setup
  end
end
unwind_exit_state_stack() click to toggle source

this function exits all items next on the exit state stack

# File lib/stativus.rb, line 344
def unwind_exit_state_stack
  @exit_state_stack = @exit_state_stack || []
  state_to_exit = @exit_state_stack.shift
  
  if state_to_exit
    if(state_to_exit.respond_to?(:will_exit_state))
      
      state_restart = {
        :statechart => self,
        :start => state_to_exit
      }
      #todo : i'm pretty sure this won't work as written...
      state_restart[:restart] = Proc.new {
        #if(debugMode) puts ['RESTART: after async processing on,', self[:start].name, 'is about to fully exit'].join(' ')
        @statechart.full_exit(state_restart[:start])
      }
      delay_for_async = state_to_exit.will_exit_state(state_restart)
    end
    full_exit(state_to_exit) unless delay_for_async
  else
    @exit_state_stack = nil
    initiate_enter_state_sequence
  end
end