philiprehberger-event_emitter
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_emitterUsage
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 runsMax 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 warningWildcard 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 ** listenerWildcard 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: trueonon()oronce()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 secondwait 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 rubocopSupport
If you find this project useful: