require 'ably/modules/safe_yield' module Ably module Modules # EventEmitter provides methods to attach to public events and emit events on any class instance # # EventEmitter are typically used for public interfaces, and as such, may be overriden in # the classes to enforce `event` names match expected values. # # @note This module requires that the method #logger is defined. # # @example # class Example # include Modules::EventEmitter # end # # event_emitter = Example.new # event_emitter.on(:signal) { |name| puts "Signal #{name} received" } # event_emitter.emit :signal, "Test" # #=> "Signal Test received" # module EventEmitter include Ably::Modules::SafeYield module ClassMethods attr_reader :event_emitter_coerce_proc # Configure included EventEmitter # # @param [Hash] options the options for the {EventEmitter} # @option options [Proc] :coerce_into A lambda/Proc that is used to coerce the event names for all events. This is useful to ensure the event names conform to a naming or type convention. # # @example # configure_event_emitter coerce_into: Proc.new { |event| event.to_sym } # def configure_event_emitter(options = {}) @event_emitter_coerce_proc = options[:coerce_into] end # Ensure @event_emitter_coerce_proc option is passed down to any classes that inherit the class with callbacks def inherited(subclass) subclass.instance_variable_set('@event_emitter_coerce_proc', @event_emitter_coerce_proc) super end end # On receiving an event matching the event_name, call the provided block # # @param [Array] event_names event name # # @return [void] def on(*event_names, &block) event_names.each do |event_name| callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block) end end # Equivalent of {#on} but any exception raised in a block will bubble up and cause this client library to fail. # This method should only be used internally by the client library. # @api private def unsafe_on(*event_names, &block) event_names.each do |event_name| callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, unsafe: true) end end # On receiving an event maching the event_name, call the provided block only once and remove the registered callback # # @param [Array] event_names event name # # @return [void] def once(*event_names, &block) event_names.each do |event_name| callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, delete_once_run: true) end end # Equivalent of {#once} but any exception raised in a block will bubble up and cause this client library to fail. # This method should only be used internally by the client library. # @api private def unsafe_once(*event_names, &block) event_names.each do |event_name| callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, delete_once_run: true, unsafe: true) end end # Emit an event with event_name that will in turn call all matching callbacks setup with `on` def emit(event_name, *args) callbacks[callbacks_event_coerced(event_name)]. clone. select do |proc_hash| if proc_hash[:unsafe] proc_hash[:emit_proc].call *args else safe_yield proc_hash[:emit_proc], *args end end.each do |callback| callbacks[callbacks_event_coerced(event_name)].delete callback end end # Remove all callbacks for event_name. # # If a block is provided, only callbacks matching that block signature will be removed. # If block is not provided, all callbacks matching the event_name will be removed. # # @param [Array] event_names event name # # @return [void] def off(*event_names, &block) keys = if event_names.empty? callbacks.keys else event_names end keys.each do |event_name| if block_given? callbacks[callbacks_event_coerced(event_name)].delete_if { |proc_hash| proc_hash[:block] == block } else callbacks[callbacks_event_coerced(event_name)].clear end end end private def self.included(klass) klass.extend ClassMethods end # Create a Hash with a proc that calls the provided block and returns true if option :delete_once_run is set to true. # #emit automatically deletes any blocks that return true thus allowing a block to be run once def proc_for_block(block, options = {}) { emit_proc: Proc.new do |*args| block.call *args true if options[:delete_once_run] end, block: block, unsafe: options[:unsafe] } end def callbacks @callbacks ||= Hash.new { |hash, key| hash[key] = [] } end def callbacks_event_coerced(event_name) if self.class.event_emitter_coerce_proc self.class.event_emitter_coerce_proc.call(event_name) else event_name end end end end end