class FiniteMachine::Observer

A class responsible for observing state changes

Attributes

hooks[R]

The hooks to trigger around the transition lifecycle.

machine[R]

The current state machine

Public Class Methods

new(machine) click to toggle source

Initialize an Observer

@param [StateMachine] machine

reference to the current machine

@api public

# File lib/finite_machine/observer.rb, line 30
def initialize(machine)
  @machine = machine
  @hooks   = Hooks.new

  @machine.subscribe(self)
end

Public Instance Methods

call(&block) click to toggle source

Evaluate in current context

@api private

# File lib/finite_machine/observer.rb, line 40
def call(&block)
  instance_eval(&block)
end
cancel_event(msg = nil) click to toggle source

Cancel the current event

This should be called inside a on_before or on_exit callbacks to prevent event transition.

@param [String] msg

the message used for failure

@api public

# File lib/finite_machine/observer.rb, line 152
def cancel_event(msg = nil)
  raise CallbackError.new(msg)
end
emit(event, *data) click to toggle source

Execute each of the hooks in order with supplied data

@param [HookEvent] event

the hook event

@param [Array] data

@return [nil]

@api public

# File lib/finite_machine/observer.rb, line 129
def emit(event, *data)
  sync_exclusive do
    [event.type].each do |hook_type|
      any_state_or_event = HookEvent.any_state_or_event(hook_type)
      [any_state_or_event, event.name].each do |event_name|
        hooks[hook_type][event_name].each do |hook|
          handle_callback(hook, event, *data)
          off(hook_type, event_name, &hook) if hook.is_a?(Once)
        end
      end
    end
  end
end
off(hook_type, name = ANY_STATE, &callback) click to toggle source

Unregister callback for a given event

@api public

# File lib/finite_machine/observer.rb, line 69
def off(hook_type, name = ANY_STATE, &callback)
  sync_exclusive do
    hooks.unregister hook_type, name, callback
  end
end
on(hook_type, state_or_event_name = nil, async = nil, &callback) click to toggle source

Register callback for a given hook type

@param [HookEvent] hook_type @param [Symbol] state_or_event_name @param [Proc] callback

@example

observer.on HookEvent::Enter, :green

@api public

# File lib/finite_machine/observer.rb, line 54
def on(hook_type, state_or_event_name = nil, async = nil, &callback)
  sync_exclusive do
    if state_or_event_name.nil?
      state_or_event_name = HookEvent.any_state_or_event(hook_type)
    end
    async = false if async.nil?
    ensure_valid_callback_name!(hook_type, state_or_event_name)
    callback.extend(Async) if async == :async
    hooks.register(hook_type, state_or_event_name, callback)
  end
end
on_after(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 107
def on_after(*args, &callback)
  on HookEvent::After, *args, &callback
end
on_before(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 103
def on_before(*args, &callback)
  on HookEvent::Before, *args, &callback
end
on_enter(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 79
def on_enter(*args, &callback)
  on HookEvent::Enter, *args, &callback
end
on_exit(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 87
def on_exit(*args, &callback)
  on HookEvent::Exit, *args, &callback
end
on_transition(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 83
def on_transition(*args, &callback)
  on HookEvent::Transition, *args, &callback
end
once_on_after(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 115
def once_on_after(*args, &callback)
  on HookEvent::After, *args, &callback.extend(Once)
end
once_on_before(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 111
def once_on_before(*args, &callback)
  on HookEvent::Before, *args, &callback.extend(Once)
end
once_on_enter(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 91
def once_on_enter(*args, &callback)
  on HookEvent::Enter, *args, &callback.extend(Once)
end
once_on_exit(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 99
def once_on_exit(*args, &callback)
  on HookEvent::Exit, *args, &callback.extend(Once)
end
once_on_transition(*args, &callback) click to toggle source
# File lib/finite_machine/observer.rb, line 95
def once_on_transition(*args, &callback)
  on HookEvent::Transition, *args, &callback.extend(Once)
end

Private Instance Methods

callback_names() click to toggle source

Callback names including all states and events

@return [Array]

valid callback names

@api private

# File lib/finite_machine/observer.rb, line 235
def callback_names
  machine.states + machine.events + [ANY_EVENT, ANY_STATE]
end
callback_queue() click to toggle source

Get an existing callback queue or create a new one

@return [FiniteMachine::MessageQueue]

@api private

# File lib/finite_machine/observer.rb, line 195
def callback_queue
  @callback_queue ||= MessageQueue.new.tap do
    @queue_id = SecureRandom.uuid
    ObjectSpace.define_finalizer(@queue_id, proc do
      cleanup_callback_queue
    end)
  end
end
cleanup_callback_queue() click to toggle source

Clean up the callback queue

@return [Boolean, nil]

@api private

# File lib/finite_machine/observer.rb, line 209
def cleanup_callback_queue
  ObjectSpace.undefine_finalizer(@queue_id) if @queue_id
  return unless @callback_queue && callback_queue.alive?

  begin
    callback_queue.shutdown
  rescue MessageQueueDeadError
  end
end
create_callable(hook) click to toggle source

Create callable instance

@api private

# File lib/finite_machine/observer.rb, line 222
def create_callable(hook)
  callback = proc do |trans_event, *data|
    machine.instance_exec(trans_event, *data, &hook)
  end
  Callable.new(callback)
end
defer(callable, trans_event, *data) click to toggle source

Defer callback execution

@api private

# File lib/finite_machine/observer.rb, line 184
def defer(callable, trans_event, *data)
  async_call = AsyncCall.new(machine, callable, trans_event, *data)
  callback_queue.start unless callback_queue.running?
  callback_queue << async_call
end
handle_callback(hook, event, *data) click to toggle source

Handle callback and decide if run synchronously or asynchronously

@param [Proc] :hook

The hook to evaluate

@param [HookEvent] :event

The event for which the hook is called

@param [Array] :data

@api private

# File lib/finite_machine/observer.rb, line 169
def handle_callback(hook, event, *data)
  to = machine.events_map.move_to(event.event_name, event.from, *data)
  trans_event = TransitionEvent.new(event.event_name, event.from, to)
  callable    = create_callable(hook)

  if hook.is_a?(Async)
    defer(callable, trans_event, *data)
  else
    callable.(trans_event, *data)
  end
end
method_missing(method_name, *args, &block) click to toggle source

Forward the message to observer

@param [String] method_name

@param [Array] args

@return [self]

@api private

# File lib/finite_machine/observer.rb, line 248
def method_missing(method_name, *args, &block)
  _, event_name, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/)
  if callback_name && callback_names.include?(callback_name.to_sym)
    public_send(event_name, :"#{callback_name}", *args, &block)
  else
    super
  end
end
respond_to_missing?(method_name, include_private = false) click to toggle source

Test if a message can be handled by observer

@param [String] method_name

@param [Boolean] include_private

@return [Boolean]

@api private

# File lib/finite_machine/observer.rb, line 266
def respond_to_missing?(method_name, include_private = false)
  *_, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/)
  callback_name && callback_names.include?(:"#{callback_name}")
end