The project is in a healthy, maintained state
A thread-safe event emitter for Ruby with sync and async listeners, wildcard event matching, listener priorities, event history with replay, event metadata, blocking wait, one-time listeners, and a mixin module.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-event_emitter

Tests Gem Version Last updated

Type-safe event emitter with sync and async listeners

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-event_emitter"

Or install directly:

gem install philiprehberger-event_emitter

Usage

Standalone emitter

require "philiprehberger/event_emitter"

emitter = Philiprehberger::EventEmitter.new

emitter.on(:user_created) do |user|
  puts "Welcome, #{user[:name]}!"
end

emitter.once(:user_created) do |user|
  puts "First user created (fires only once)"
end

emitter.emit(:user_created, { name: "Alice" })
# => Welcome, Alice!
# => First user created (fires only once)

emitter.emit(:user_created, { name: "Bob" })
# => Welcome, Bob!

Mixin module

Include Philiprehberger::EventEmitter::Mixin to add event capabilities to any class:

class OrderService
  include Philiprehberger::EventEmitter::Mixin

  def place_order(order)
    # ... process order ...
    emit(:order_placed, order)
  end
end

service = OrderService.new
service.on(:order_placed) { |order| puts "Order #{order[:id]} placed" }
service.place_order({ id: 42 })

Error Handling

By default, if a listener raises an exception, it propagates normally. Set an error handler to catch exceptions and allow remaining listeners to fire:

emitter = Philiprehberger::EventEmitter.new

emitter.on_error = ->(error) { puts "Listener error: #{error.message}" }

emitter.on(:test) { raise "boom" }
emitter.on(:test) { puts "still runs" }

emitter.emit(:test)
# => Listener error: boom
# => still runs

Max Listeners Warning

A warning is printed when more than 10 listeners are added to a single event (possible memory leak). Configure the threshold:

emitter.max_listeners = 20    # raise threshold
emitter.max_listeners = nil   # disable warning

Wildcard Event Matching

Subscribe to multiple events using glob-style patterns. Segments are separated by ..

emitter = Philiprehberger::EventEmitter.new

# * matches exactly one segment
emitter.on("user.*") do |event_name, data|
  puts "#{event_name}: #{data}"
end

emitter.emit("user.created", { id: 1 })  # triggers wildcard listener
emitter.emit("user.deleted", { id: 2 })  # also triggers it

# ** matches any number of segments (including zero)
emitter.on("app.**") do |event_name|
  puts "App event: #{event_name}"
end

emitter.emit("app.user.profile.updated")  # triggers ** listener

Wildcard listeners receive the actual event name as the first argument, followed by the emitted data.

Listener Priorities

Control the execution order of listeners with priority:. Higher values run first. Default priority is 0.

emitter.on(:save, priority: 10) { puts "runs first (validation)" }
emitter.on(:save, priority: 5)  { puts "runs second (transform)" }
emitter.on(:save)               { puts "runs last (default priority 0)" }

Within the same priority, listeners execute in registration order (FIFO).

Event History & Replay

Store recent events and let late-binding listeners replay them:

emitter = Philiprehberger::EventEmitter.new(history_size: 50)
emitter.emit(:init, { ready: true })

# Later, a new listener can catch up on missed events
emitter.on(:init, replay: true) { |data| puts data }
# => { ready: true }  (fires immediately with the stored event)
  • history_size: controls the maximum number of events stored (default: 0, meaning disabled)
  • replay: true on on() or once() replays matching historical events immediately upon subscription

Async Emission

Fire-and-forget listener execution in threads:

emitter.on(:heavy_work) { |data| process(data) }

threads = emitter.emit_async(:heavy_work, payload)
# Each listener runs in its own Thread
# Returns an array of Thread objects for optional joining
threads.each(&:join)

The on_error handler catches exceptions from async listeners just as it does for sync ones.

Event Metadata

Opt in to receive an EventMetadata object with event context:

emitter.on(:order_placed, metadata: true) do |data, meta|
  puts meta.event_name   # :order_placed
  puts meta.timestamp     # Time when emitted
end

emitter.emit(:order_placed, { id: 42 })

Listeners without metadata: true are unaffected and receive only the emitted data.

Waiting for an event

Block the calling thread until an event fires. Useful for tests, request/response patterns, and shutdown coordination.

emitter = Philiprehberger::EventEmitter.new

Thread.new do
  sleep 0.1
  emitter.emit(:ready, "payload")
end

args = emitter.wait(:ready)        # blocks until :ready is emitted
# => ["payload"]

emitter.wait(:never, timeout: 1)   # returns nil after 1 second

wait returns the array of positional args passed to emit, or nil on timeout. The internal listener is removed automatically on timeout, so it does not leak.

Removing listeners

emitter = Philiprehberger::EventEmitter.new

handler = proc { |msg| puts msg }
emitter.on(:message, &handler)

# Remove a specific listener
emitter.off(:message, &handler)

# Remove all listeners for an event
emitter.off(:message)

API

Method Description
on(event, priority: 0, replay: false, metadata: false, &block) Register a listener (supports wildcards, priorities, replay, metadata)
once(event, priority: 0, replay: false, metadata: false, &block) Register a one-time listener
emit(event, *args, **kwargs) Emit an event synchronously to all matching listeners
emit_async(event, *args, **kwargs) Emit an event asynchronously, each listener in its own Thread
wait(event, timeout: nil) Block until event fires; returns positional args, or nil on timeout
off(event, &block) Remove a specific listener (or all for that event/pattern)
listeners(event) Return an array of listener blocks for an event
listener_count(event) Return the number of listeners for an event
remove_all_listeners(event = nil) Remove all listeners (optionally for a specific event/pattern)
event_names Return an array of registered event names
on_error=(handler) Set an error handler for listener exceptions
max_listeners=(n) Set max listener warning threshold (default: 10, nil to disable)

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT