I decided to start a “series” of articles which will cover small projects that can be completed in less than a day and are fun enough for a nice Sunday morning.So yeah, hacking Sunday(that’s a cool name) starts now!

For the first one i’ll be rebuilding the node.js EventEmitter which is one of the core objects of node in plain ruby.
Before starting i already took a peak in the source for implementation ideas and read through the docs with which our ruby api will conform fully.

If you are already familiar with the node api it will be pretty straightforward since names and behaviors have been purposefully kept the same, if not, i guess that’s even more fun(or not?), the docs are really clear and not long.

I have also posted the whole thing plus two usage examples on a gist here.

Usage and looks

my_em = EventEmitter.new do |em|
  em.on(:new_listener) do |name, ref|
    puts "new listener #{name} added"
    puts "event proc: #{ref}"
  end

  em.on(:error) do |err|
    puts err
  end

  em.on(:i_throw_up) do
    throw "oh no..."
  end

  em.on(:remove_listener) do |name|
    puts "listener #{name} removed"
  end

  em.on(:greet) do |name|
    puts "hello #{name}"
  end

  em.once(:init) do
    puts "The process started running."
  end
end

my_em.emit(:greet, "AFineName")
my_em.emit(:i_throw_up)
my_em.emit(:init)

Implementation

Let’s support a block api as well

class EventEmitter
  def initialize
    @max_listeners = EventEmitter.default_max_listeners
    @events = {}
    @once = {}
    yield self if block_given?
  end
  #global limit configuration
  #set_max_listeners(n) has higher precedence
  def self.default_max_listeners(num=nil)
    @max_listeners = num || 10
  end
end

I will be showing the internal private methods first because we will be seeing them all over going forward.
They are three special kinds of events for new_listener, remove_listener and error which don’t follow the exact same logic as normal events do.
The check_listener_limit() will issue warnings if we have exceeded the max listener limit.

private
  #emit a new_listener event before an event is registered
  #with its name and reference
  def emit_new_listener(event)
    if @events[:new_listener]
      @events[:new_listener].each do |listener|
        listener.call(event, listener)
      end
    end
  end

  #emit a remove_event after an event is removed with its name
  def emit_remove_listener(event)
    if @events[:remove_listener]
      @events[:remove_listener].each do |listener|
        listener.call(event)
      end
    end
  end

  #emit an error event and pass the excepion
  def emit_error(e, *params)
    @events[:error].each do |listener|
      listener.call(e, *params)
    end
  end

  #print a warning if the listener limit is exceeded
  #@max_listeners == 0 means unlimited
  def check_listener_limit(event)
    if @events[event].count > @max_listeners && @max_listeners != 0
      puts "Listener limit of #{@max_listeners} for #{event} event has been exceeded"
      puts "Use #set_max_listeners(n) to change the instance limit, or"
      puts "EventEmitter.default_max_listeners(n) for a global change\n"
    end
 end

.on() and .emit() are our core methods.
For .on() first emit a new_listener event then add the listener proc to the event array and afterwards check if the listener limit has been exceeded(to issue a warning, as node does).
For .emit(), execute all listeners for an event,if there is a reference in the @once array(an array of references of listeners registered with .once() instead of .on()) remove the listener and the reference after emitting.

If there are listeners for error events we will delegate the exception there, if not we just reraise it and crash.

def on(event, &listener)
  emit_new_listener(event)
  @events[event] ||= []
  @events[event] << listener
  check_listener_limit(event)
  listener
end
alias_method :add_listener, :on

def emit(event, *params)
  if @events[event]
    @events[event].each do |listener|
      listener.call(*params)
      if @once[event] && @once[event].include?(listener.to_s)
        remove_listener(event, listener)
        @once[event].collect! { |entry| listener.to_s == entry ? nil : entry }.compact!
      end
    end
    @events[event].compact!
  end
rescue Exception => e
  if @events.key?(:error)
     emit_error(e, *params)
  else
    raise e
  end
end

The .once() method is same as .on() but it will also keep a reference of the proc name in the @once[event] array(so we can find it and remove it after emitting)

def once(event, &listener)
  @events[event] ||= []
  @once[event] ||= []
  @once[event] << listener.to_s
  @events[event] << listener
  check_listener_limit(event)
  listener
end

.remove_listener() does the actual work of removing the listeners(i guess we don’t want to remove new_listener, remove_listener and error).
.remove_all_listeners() loops through a specific event type or through all the listeners in the instance and calls .remove_listener()

def remove_listener(event, listener)
  if @events[event] && event != :new_listener && event != :remove_listener && event != :error
    @events[event].collect! { |entry| listener == entry ? nil : entry }
    emit_remove_listener(event)
  end
  self
end

# at first i used copies of listener arrays for the iterations
# and array.reject! to remove the listeners
# now i just fill with nils and call compact! afterwards
def remove_all_listeners(event_name=nil)
  if event_name && @events[event_name]
    @events[event_name].each do |listener|
      remove_listener(event_name, listener)
    end
    @events[event_name].compact!
  end
  if event_name.nil?
    @events.each_pair do |event, listeners|
      listeners.each { |listener| remove_listener(event, listener) }
      @events[event].compact!
    end
  end
  self
end

Convenience methods.

#return a copy of the listener procs for an event
def listeners(event)
  @events[event].clone
end

#return the listener count for an event
def listener_count(event)
  @events[event].count
end

Configuration.

def set_max_listeners(num)
  @max_listeners = num
end

def get_max_listeners
  @max_listeners
end

That’s it, have fun!