Project

resugan

0.02
No commit activity in last 3 years
No release in over 3 years
simple, powerful and unobstrusive event framework for ruby
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 0
~> 10.0
~> 3.0
 Project Readme

Gem Version CircleCI

Resugan

Simple, powerful and unobstrusive event driven architecture framework for ruby. This gem provides a base framework in order to build a more powerful event based system on top of it. Events cuts across multiple objects and allows you to cleanly separate business logic to other cross cutting concerns like analytics and logging. Multiple events are consolidated allowing you to efficiently batch related operations together.

Also allows for a customizable backend which enables the use of various evented queuing mechanisms like redis queue, amazon SQS with minimal changes to your code that generates the events.

Installation

Add this line to your application's Gemfile:

gem 'resugan'

And then execute:

$ bundle

Or install it yourself as:

$ gem install resugan

Basic Usage

Register listeners, using :

  _listener :event1 do |array_of_params|
    puts "hello! event 2 has been called!"
  end

  _listener :hay do |array_of_params|
    puts "hello! someone said hay!"
  end

Listeners are basically code that listens to an event, in this case :event1 and :hay. an array of params equal to the number of times that specific event was captured will be passed. So if :event1 was called twice, array_of_params will contain 2 elements.

Generate events using _fire and wrap them in a resugan block:

resugan {
  puts "I am now going to generate an event"

  _fire :event2

  _fire :hay

  _fire :bam, { some_param: 'param' } # you can pass hashes to add meta to the event
}

The _fire method is available inside all objects, however the events won't be collected unless within the context of a resugan block. The idea is that you can prepackage a library that fires those events but won't actually get used until someone specifically listens for it.

Note that events don't have to be fired at the top level of the block, even objects used inside the block can invoke fire to generate an event.

The two events should fire and should print:

hello! event 2 has been called!
hello! someone said hay!

Note that your listener will be executed once even if an event has been fired multiple times. However params will contain the payload of both events. This allows you to batch together requests and efficiently dispatch them as a group.

Object helpers

Helpers are available to make listening firing events a little bit cleaner:

class TestObject
  include Resugan::ObjectHelpers
end

This basically allows for the attach_hook to be available

class TestObject
  include Resugan::ObjectHelpers

  def method2
    _fire :event1
  end

  def method3
    _fire :event2, param1: "hello"
  end

  attach_hook :method2
  attach_hook :method3, namespace: "namespace1"
end

What this does is it essentially wraps the specified methods inside a resugan block.

Please see spec/resugan_spec.rb for more examples and details.

namespaces

Resugan supports namespaces, allowing you to group listeners and trigger them separately

  _listener :event1, namespace: "group1" do |array_of_params|
    puts "hello! event 2 has been called!"
  end

  _listener :event1, namespace: "group2" do |array_of_params|
    puts "hello! someone said hay!"
  end

  _listener :log, namespace: %w(group1 group2) do |array_of_params|
    array_of_params.each {
      puts "listener that belongs to 2 namespaces"
    }
  end

  resugan "group1" do
    _fire :event1
    _fire :log
  end

  resugan "group2" do
    _fire :event1
    _fire :log
  end

Behavior is as expected. Events under group1 will only be handled by listeners under group1 and so on.

The above should print:

hello! event 2 has been called!
listener that belongs to 2 namespaces
hello! someone said hay!
listener that belongs to 2 namespaces

Behavior of nested resugan blocks

Resugan will by default only resolve events at the outermost resugan context of a namespace.

_listener :event1 do |array_of_params|
  puts "hello! event 1"
end

_listener :event2, namespace: "group2" do |array_of_params|
  puts "hello! event 1"
end

resugan {
  _fire :event1

  resugan {
    _fire :event2
  }
}

If there are nested resugan blocks note that event dispatch will occur at the outermost context. Output will be:

hello! event 1
hello! event 2

To force the innermost block to immediately resolve, use resugan! instead (or set Kernel.config.reuse_top_level_context = false globally):

resugan {
  _fire :event1

  resugan! {
    _fire :event2
  }
}

Since the inner block will be resolved first, Output will be:

hello! event 2
hello! event 1

Unique Listeners

the _listener always creates a new listener for an event, so if it so happens that the code that creates those listeners gets executed again it will create another one. if you want to make sure that listener only gets executed once you can pass an id option:

_listener :event1, id: 'no_other_listener_like_this' do |array|
 # some code that gets executed
end

Or you can use the _listener! form which make sure a certain block is limited to only a single instance.

2.times do |i|
  _listener! :event1 do |array|
    # There will be only one instance of this listener no matter how many times it is defined
  end
end

Customizing the Event dispatcher

The way events are consumed is entirely customizable. You can register your own event dispatcher:

class MyCustomerDispatcher
  def dispatch(namespace, events)
    events.each do |k,v|
      Resugan::Kernel.invoke(namespace, k, v)
    end
  end
end

You need to implement your own dispatch method, captured events are passed as parameters.

You can then set it as the default dispatcher:

  Resugan::Kernel.set_default_dispatcher(MyCustomerDispatcher)

Or assign it to a specific namespace:

  Resugan::Kernel.register_dispatcher(MyCustomerDispatcher, 'CustomGroup')

This allows you to use various queue backends per namespace, like resugan-worker for example.

Debugging

Sometimes you need to track where events are fired. You can do so by enabling line tracing:

  Resugan::Kernel.config do |c|
    c.line_trace_enabled = true
  end

Line source should now be passed as params everytime you fire an event. You can also view it by dumping a resugan context.

puts(resugan {
  _fire :event1
}.dump)
{:event1=>[{:params=>{:_source=>"/Users/jedld/workspace/resugan/spec/resugan_spec.rb:144:in `block (5 levels) in <top (required)>'"}}]}

Using Resugan::Engine::MarshalledInlineDispatcher

By default, resugan uses the Resugan::Engine::InlineDispatcher as the default dispatcher for all namespaces. For performance reasons, params passed to the _fire method are passed as is, but there are times when you want to simulate params that are passed using JSON.parse as is the case when using a custom dispatcher that uses redis (see resugan-worker). In this case you may set MarshalledInlineDispatcher as the default dispatcher for test and development environment instead (e.g. rails):

Resugan::Kernel.set_default_dispatcher(Resugan::Engine::MarshalledInlineDispatcher) if Rails.env.development? || Rails.env.test?

Related Projects

Below are projects that extend resugan.

Resugan Worker

A project that wraps resugan listeners to be consumed using an external worker. Think of this as a redis queue backend. Can also be used as a sample on how to extend resugan.

https://github.com/jedld/resugan-worker

Testing

RSpec helpers are available:

https://github.com/jedld/resugan-rspec

Similar Projects

wisper (https://github.com/krisleech/wisper) - An excellent gem that focuses on a coupled pub-sub model. Though its global listeners somehow have the same effect though in a syntactically different way.

event_bus (https://github.com/kevinrutherford/event_bus) - Loosely coupled pub-sub similar to resugan

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/resugan. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.