lib/houston/boot/observer.rb
module Houston
class Observer
attr_accessor :async
class UnregisteredEventError < ArgumentError; end
class MissingParamError < ArgumentError; end
class UnregisteredParamError < ArgumentError; end
def initialize
@async = true
clear!
end
def on(event, options={}, &block)
assert_registered! event
observers_of(event).push Callback.new(self, event, options, block)
nil
end
def once(event, options={}, &block)
assert_registered! event
observers_of(event).push CallbackOnce.new(self, event, options, block)
nil
end
def off(callback, &block)
if block_given?
event = callback
callback = observers_of(event).detect { |callback| callback.block == block }
return nil unless callback
end
observers_of(callback.event).delete callback
nil
end
def observed?(event)
assert_registered! event
observers_of(event).any?
end
def fire(event, params={})
assert_registered! event
unless params.is_a?(Hash)
raise ArgumentError, "params must be a Hash" unless params.respond_to?(:to_h)
params = params.to_h
end
assert_registered_params! event, params
assert_serializable! params
params = ReadonlyHash.new(params)
observers_of(event).each do |callback|
callback.call params
end
observers_of(:*).each do |callback|
callback.call event, params
end
nil
end
def clear!
@observers = {}
end
private
attr_reader :observers
def observers_of(event)
observers[event] ||= Concurrent::Array.new
end
def assert_registered!(event_name)
return if event_name == :*
return if Houston.events.registered?(event_name)
raise UnregisteredEventError, "#{event_name.inspect} is not a registered event"
end
def assert_registered_params!(event_name, params)
event = Houston.events[event_name]
missing_params = event.params - params.keys.map(&:to_s)
unregistered_params = params.keys.map(&:to_s) - event.params
if missing_params.any?
raise MissingParamError, "#{missing_params.first.inspect} is a required param of the event #{event_name.inspect}"
end
if unregistered_params.any?
raise UnregisteredParamError, "#{unregistered_params.first.inspect} is a not a registered param of the event #{event_name.inspect}"
end
end
def assert_serializable!(params)
Houston::Serializer.new.assert_serializable!(params)
end
class Callback
attr_reader :observer, :event, :block
def initialize(observer, event, options, block)
@observer = observer
@event = event
@invoke_async = options.fetch(:async, nil)
@raise_exceptions = options.fetch(:raise, false)
@block = block
end
def invoke_async?
return @invoke_async unless @invoke_async.nil?
Houston.observer.async
end
def raise_exceptions?
@raise_exceptions
end
def call(*args)
Houston.async(invoke_async?) do
begin
block.call(*args)
rescue Exception # rescues StandardError by default; but we want to rescue and report all errors
raise if raise_exceptions?
$!.additional_information[:event] = event
$!.additional_information[:async] = invoke_async?
$!.additional_information[:raise_exceptions] = raise_exceptions?
Houston.report_exception($!)
end
end
end
end
class CallbackOnce < Callback
def call(*args)
observer.off self
super
end
end
end
class ReadonlyHash
def initialize(hash)
@hash = hash.symbolize_keys
@hash.keys.each do |key|
define_singleton_method(key) { @hash[key] }
end
end
def [](key)
@hash[key.to_sym]
end
def count
@hash.count
end
def to_h
@hash.dup
end
end
end