Giter VIP home page Giter VIP logo

wisper's Introduction

Wisper

A micro library providing Ruby objects with Publish-Subscribe capabilities

Gem Version Code Climate Build Status Coverage Status

  • Decouple core business logic from external concerns in Hexagonal style architectures
  • Use as an alternative to ActiveRecord callbacks and Observers in Rails apps
  • Connect objects based on context without permanence
  • Publish events synchronously or asynchronously

Note: Wisper was originally extracted from a Rails codebase but is not dependent on Rails.

Please also see the Wiki for more additional information and articles.

For greenfield applications you might also be interested in WisperNext and Ma.

Installation

Add this line to your application's Gemfile:

gem 'wisper', '~> 3.0'

Usage

Any class with the Wisper::Publisher module included can broadcast events to subscribed listeners. Listeners subscribe, at runtime, to the publisher.

Publishing

class CancelOrder
  include Wisper::Publisher

  def call(order_id)
    order = Order.find_by_id(order_id)

    # business logic...

    if order.cancelled?
      broadcast(:cancel_order_successful, order.id)
    else
      broadcast(:cancel_order_failed, order.id)
    end
  end
end

When a publisher broadcasts an event it can include any number of arguments.

The broadcast method is also aliased as publish.

You can also include Wisper.publisher instead of Wisper::Publisher.

Subscribing

Objects

Any object can be subscribed as a listener.

cancel_order = CancelOrder.new

cancel_order.subscribe(OrderNotifier.new)

cancel_order.call(order_id)

The listener would need to implement a method for every event it wishes to receive.

class OrderNotifier
  def cancel_order_successful(order_id)
    order = Order.find_by_id(order_id)

    # notify someone ...
  end
end

Blocks

Blocks can be subscribed to single events and can be chained.

cancel_order = CancelOrder.new

cancel_order.on(:cancel_order_successful) { |order_id| ... }
            .on(:cancel_order_failed)     { |order_id| ... }

cancel_order.call(order_id)

You can also subscribe to multiple events using on by passing additional events as arguments.

cancel_order = CancelOrder.new

cancel_order.on(:cancel_order_successful) { |order_id| ... }
            .on(:cancel_order_failed,
                :cancel_order_invalid)    { |order_id| ... }

cancel_order.call(order_id)

Do not return from inside a subscribed block, due to the way Ruby treats blocks this will prevent any subsequent listeners having their events delivered.

Handling Events Asynchronously

cancel_order.subscribe(OrderNotifier.new, async: true)

Wisper has various adapters for asynchronous event handling, please refer to wisper-celluloid, wisper-sidekiq, wisper-activejob, wisper-que or wisper-resque.

Depending on the adapter used the listener may need to be a class instead of an object. In this situation, every method corresponding to events should be declared as a class method, too. For example:

class OrderNotifier
  # declare a class method if you are subscribing the listener class instead of its instance like:
  #   cancel_order.subscribe(OrderNotifier)
  #
  def self.cancel_order_successful(order_id)
    order = Order.find_by_id(order_id)

    # notify someone ...
  end
end

ActionController

class CancelOrderController < ApplicationController

  def create
    cancel_order = CancelOrder.new

    cancel_order.subscribe(OrderMailer,        async: true)
    cancel_order.subscribe(ActivityRecorder,   async: true)
    cancel_order.subscribe(StatisticsRecorder, async: true)

    cancel_order.on(:cancel_order_successful) { |order_id| redirect_to order_path(order_id) }
    cancel_order.on(:cancel_order_failed)     { |order_id| render action: :new }

    cancel_order.call(order_id)
  end
end

ActiveRecord

If you wish to publish directly from ActiveRecord models you can broadcast events from callbacks:

class Order < ActiveRecord::Base
  include Wisper::Publisher

  after_commit     :publish_creation_successful, on: :create
  after_validation :publish_creation_failed,     on: :create

  private

  def publish_creation_successful
    broadcast(:order_creation_successful, self)
  end

  def publish_creation_failed
    broadcast(:order_creation_failed, self) if errors.any?
  end
end

There are more examples in the Wiki.

Global Listeners

Global listeners receive all broadcast events which they can respond to.

This is useful for cross cutting concerns such as recording statistics, indexing, caching and logging.

Wisper.subscribe(MyListener.new)

In a Rails app you might want to add your global listeners in an initializer like:

# config/initializers/listeners.rb
Rails.application.reloader.to_prepare do
  Wisper.clear if Rails.env.development?

  Wisper.subscribe(MyListener.new)
end

Global listeners are threadsafe. Subscribers will receive events published on all threads.

Scoping by publisher class

You might want to globally subscribe a listener to publishers with a certain class.

Wisper.subscribe(MyListener.new, scope: :MyPublisher)
Wisper.subscribe(MyListener.new, scope: MyPublisher)
Wisper.subscribe(MyListener.new, scope: "MyPublisher")
Wisper.subscribe(MyListener.new, scope: [:MyPublisher, :MyOtherPublisher])

This will subscribe the listener to all instances of the specified class(es) and their subclasses.

Alternatively you can also do exactly the same with a publisher class itself:

MyPublisher.subscribe(MyListener.new)

Temporary Global Listeners

You can also globally subscribe listeners for the duration of a block.

Wisper.subscribe(MyListener.new, OtherListener.new) do
  # do stuff
end

Any events broadcast within the block by any publisher will be sent to the listeners.

This is useful for capturing events published by objects to which you do not have access in a given context.

Temporary Global Listeners are threadsafe. Subscribers will receive events published on the same thread.

Subscribing to selected events

By default a listener will get notified of all events it can respond to. You can limit which events a listener is notified of by passing a string, symbol, array or regular expression to on:

post_creator.subscribe(PusherListener.new, on: :create_post_successful)

Prefixing broadcast events

If you would prefer listeners to receive events with a prefix, for example on, you can do so by passing a string or symbol to prefix:.

post_creator.subscribe(PusherListener.new, prefix: :on)

If post_creator were to broadcast the event post_created the subscribed listeners would receive on_post_created. You can also pass true which will use the default prefix, "on".

Mapping an event to a different method

By default the method called on the listener is the same as the event broadcast. However it can be mapped to a different method using with:.

report_creator.subscribe(MailResponder.new, with: :successful)

This is pretty useless unless used in conjunction with on:, since all events will get mapped to :successful. Instead you might do something like this:

report_creator.subscribe(MailResponder.new, on:   :create_report_successful,
                                            with: :successful)

If you pass an array of events to on: each event will be mapped to the same method when with: is specified. If you need to listen for select events and map each one to a different method subscribe the listener once for each mapping:

report_creator.subscribe(MailResponder.new, on:   :create_report_successful,
                                            with: :successful)

report_creator.subscribe(MailResponder.new, on:   :create_report_failed,
                                            with: :failed)

You could also alias the method within your listener, as such alias successful create_report_successful.

Testing

Testing matchers and stubs are in separate gems.

Clearing Global Listeners

If you use global listeners in non-feature tests you might want to clear them in a hook to prevent global subscriptions persisting between tests.

after { Wisper.clear }

Need help?

The Wiki has more examples, articles and talks.

Got a specific question, try the Wisper tag on StackOverflow.

Compatibility

See the build status for details.

Running Specs

bundle exec rspec

To run the specs on code changes try entr:

ls **/*.rb | entr bundle exec rspec

Contributing

Please read the Contributing Guidelines.

Security

License

(The MIT License)

Copyright (c) 2013 Kris Leech

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

wisper's People

Contributors

adomokos avatar alexslade avatar andyw8 avatar bzurkowski avatar charlietran avatar davidstosik avatar dcoxall avatar doits avatar dwilkie avatar glaucocustodio avatar gondalez avatar hosseintoussi avatar joevandyk avatar keithrbennett avatar kianmeng avatar kmehkeri avatar krisleech avatar kyletolle avatar latortuga avatar maokomioko avatar martin91 avatar mezis avatar mhalmagiu avatar miltzi avatar nepalez avatar padi avatar shieldo avatar skwp avatar smostovoy avatar stgeneral avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wisper's Issues

Ruby Gems version

Them gem on 'RubyGems' is not with the regex option.

I am trying to use the github version in the meantime, but I am getting the following error.

Errno::ENOENT: No such file or directory @ rb_sysopen - /Users/alexandrecalvao/.ssh/gem-private_key.pem
An error occurred while installing wisper (1.6.0), and Bundler cannot continue.

Convert all specs to new `expect` style

We use rspec 3, with backwards compatibility for old should syntax. All specs should be changed to expect syntax and should syntax disabled in spec_helper.

Anyone who wants to take this please leave a comment and/or submit a PR referencing this issue 😄

on(...) Callbacks doesn't seem to work correctly inside Grape API

Hello,
I've tried using the listeners as desribed in the howto inside a Grape API with success and fail callbacks.

The basic code is as follows:

post do
  service = RatingService.new
  service.execute(rating)
  service.on(:rating_successful) do |rating|
    status(200)
    {
        message: "Successfully rated #{rating.topic.name}"
    }
  end
  service.on(:rating_failed) do |errors|
   render_api_error!(errors, 400)
  end
end

Now when I post to this Grape API I don't get the expected response defined in the on() blocks but instead a direct response object from the Whisper Service:

{
    async: false,
    local_registrations: [
    {
        listener: { },
        on: [
            "all"
        ],
        with: null,
        prefix: "",
        allowed_classes: [ ]
    },
    {
        listener: { },
        on: [
            "all"
        ],
        with: null,
        prefix: "",
        allowed_classes: [ ]
    },
    {
        listener: { },
        on: [
            "rating_successful"
        ]
    },
    {
        listener: { },
        on: [
        "rating_failed"
        ]
    }
    ]
}

I tried moving the execute() call AFTER the on() blocks but it didn't work either...

Am I missing something?
Any help would be greatly appreciated!

Make global listeners getter and setter threadsafe

https://github.com/krisleech/wisper/blob/master/lib/wisper/global_listeners.rb

The singleton instance used to store the global listeners is the same instance across all threads. Its setter method mutates internal state (the array of listeners) making it not threadsafe, one thread can clobber another. The same, to a lesser extent, applies to reading the array of listeners.

Generally all global listeners will be added once during app initialization, not ad-hoc during runtime, so its unlikely to be a problem in practice. But that said it would be good to make it threadsafe.

A copy of XXX has been removed from the module tree but is still active!

I'm getting this error very frequently:

A copy of EventDispatcher has been removed from the module tree but is still active!
/Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:479:in load_missing_constant' /Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:184:inconst_missing'
/Users/Tute/playspace/game_events/app/listeners/event_dispatcher.rb:16:in method_missing' /Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/wisper-celluloid-0.0.1/lib/wisper/celluloid_broadcaster.rb:17:inpublic_send'
/Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/wisper-celluloid-0.0.1/lib/wisper/celluloid_broadcaster.rb:17:in broadcast' /Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/celluloid-0.16.0/lib/celluloid/calls.rb:26:inpublic_send'
/Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/celluloid-0.16.0/lib/celluloid/calls.rb:26:in dispatch' /Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/celluloid-0.16.0/lib/celluloid/calls.rb:122:indispatch'
/Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/celluloid-0.16.0/lib/celluloid/cell.rb:60:in block in invoke' /Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/celluloid-0.16.0/lib/celluloid/cell.rb:71:inblock in task'
/Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/celluloid-0.16.0/lib/celluloid/actor.rb:357:in block in task' /Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/celluloid-0.16.0/lib/celluloid/tasks.rb:57:inblock in initialize'
/Users/Tute/.rvm/gems/ruby-2.2.2@game-events/gems/celluloid-0.16.0/lib/celluloid/tasks/task_fiber.rb:15:in `block in create'

I'm using an async global listener, loaded in an initializer:
Wisper.subscribe(EventDispatcher.new, prefix: true, async: true)

React to multiple events at once

I'm finding I sometimes have actions that raise multiple events to which I want one response. For example:

action = Something.new
action.on(:foo, :bar, :baz) do
# do a thing
end

What are your thoughts on supporting such a syntax? I could try to make a PR, just wanted to run it by you first, @krisleech

[FEATURE] Events pattern.

I have the following code

Wisper.add_listener(ItemListener.new, 
  on: [:item_query_rs,
    :item_discount_add_rs,
    :item_non_inventory_add_rs,
    :item_other_charge_add_rs,
    :item_payment_add_rs,
    :item_service_add_rs], 
  with: :add_item)

It would be nice to write something like this instead.

Wisper.add_listener(ItemListener.new, pattern: "item_*_rs", with: :add_item)

"A copy of <Subscriber> has been removed from the module tree but is still active!"

I'm using Wisper to add subscribers to model classes (not instances of classes). After some time, and after modyfing some code, I receive the exception noted above.

I know it has to do with Rails/Ruby class reloading in development. What I don't know is how to solve this issue. Does anyone have an idea, experience with this issue?

Block listener not working in controller

Hello!
I've just discovered wisper gem and i'm amazed of all the things i can accomplish with it. However, i've failed on the most basic thing possible.

I have a class which saves a record in a database. Here is it's code (totally simplified):

class CreateJob
  include Wisper::Publisher

  def call()
    @job = ...

    if @job.save
      broadcast(:create_job_success, @job)
    else
      broadcast(:create_job_failure, @job)
    end
  end
end

My controller method looks like that:

def create
  create_job_service.on(:create_job_success) do |job|
    render 'create_success', status: :created, job: job
  end

  create_job_service.on(:create_job_failure) do |job|
    render 'create_failure', status: :bad_request, job: job
  end

  create_job_service.call()
end

private

def create_job_service
  CreateJob.new
end

The CreateJob method is tested using rspec-wisper to check if events are broadcasted and yes, they are. However, for some reason, these events are not visible for my controller so none of the on block code is beeing triggered.

Am i doing something wrong in here? Can i somehow debug what events are emitted ? Or do anything else to check what's wrong in here?

Thanks in advance

Allow broadcaster to be set using name of broadcaster in subscribe options

Currently the broadcaster can be specified as broadcaster: XXX or async: true.

When there are more than just sync and async broadcasters in a project it would be useful to use the broadcasters key to set it.

For example if both wisper-celluloid and wisper-sidekiq in Gemfile then both will set the async broadcaster, in this case instead of doing broadcaster: :celluloid we can do celluloid: true.

subscribe(listener, celluloid: true)

Can't define an async listener

Hello again,

I'm having trouble configuring a global async Listener.

My first attempt was as stated in the README:

Wisper.subscribe QueueListener.new, async: true

But I get

wisper-1.6.0/lib/wisper/configuration.rb:25:in `fetch': broadcaster not found for async (KeyError)

Then, after reading some of the wisper-celluloid source code I tried this

Wisper.configure do |config|
    config.broadcaster :async, Engine1::AnotherModel
end
Wisper.subscribe QueueListener.new, async: true

(QueueListener is in another engine, Engine2)

But I get

activerecord (4.1.6) lib/active_record/dynamic_matchers.rb:26:in `method_missing'
wisper (1.6.0) lib/wisper/registration/object.rb:16:in `broadcast'
wisper (1.6.0) lib/wisper/publisher.rb:65:in `block in broadcast'

Finally, if I remove the new off the QueueListener it no longer throws an error, but the event doesn't seem to get to the listener.

Hopefully I don't have to do something like the second example, since the publisher and the listener are in separate engines, this would couple the listener to the publisher.

Am I doing something wrong?

Thanks in advance.

Safe subscription

Right now, integration tests are pretty much necessary to ensure that event names are the same on both the publisher and the subscriber side. I've implemented a monkey patch to make these explicit on my project.

What do you think of this technique? Would it be worth integrating directly in wisper?

module Publisher
  extend ActiveSupport::Concern
  include Wisper

  included do
    alias_method_chain :publish, :validation
    alias_method_chain :add_listener, :validation
    alias :subscribe :add_listener
  end

  module ClassMethods
    def publishes(*args)
      published_events.concat args.flatten.map(&:to_sym)
    end

    def published_events
      @published_events ||= []
    end

    def publishes?(event)
      published_events.include? event.to_sym
    end
  end

  def publishes?(event)
    self.class.publishes? event
  end

  def publish_with_validation(event, *args)
    raise CannotPublishEvent.new(self, event) unless publishes? event

    publish_without_validation(event, *args)
  end

  def add_listener_with_validation(subscriber, options = {})
    Array(options[:on]).each do |event|
      raise MissingEventHandler.new(subscriber, event) unless subscriber.respond_to? event
      raise PublisherDoesNotPublishEvent.new(self, event) unless publishes? event
    end

    add_listener_without_validation(subscriber, options)
  end

  class CannotPublishEvent < Exception
    def initialize(publisher, event)
      super "Attempted to publish unknown event :#{event} on #{publisher.class.name}.
Make sure it is included in the publishes clause:

publishes :#{event}"
    end
  end

  class MissingEventHandler < Exception
    def initialize(subscriber, event)
      super "Attempted to subscribe to event :#{event}, but
#{subscriber.class.name} does not respond_to? it."
    end
  end

  class PublisherDoesNotPublishEvent < Exception
    def initialize(publisher, event)
      super "Attempted to subscribe to event :#{event}, but
#{publisher.class.name} does not publish it."
    end
  end
end
require 'spec_helper'
require 'publisher'

describe Publisher do
  class MyPublisher
    include Publisher
    publishes :something_happened
  end

  class MySubscriber
    def something_happened
    end

    def something_else_done
    end
  end

  let(:publisher) { MyPublisher.new }

  describe '.publishes?' do
    it 'is true when the publisher publishes the event' do
      MyPublisher.publishes?(:something_happened).should be_true
    end

    it 'is false when the publisher does not publishe the event' do
      MyPublisher.publishes?(:something_else_happened).should be_false
    end
  end

  describe '#publishes?' do
    it 'is true when the publisher publishes the event' do
      publisher.publishes?(:something_happened).should be_true
    end

    it 'is false when the publisher does not publish the event' do
      publisher.publishes?(:something_else_happened).should be_false
    end
  end

  describe '#publish' do
    it 'allows publishing specified events' do
      subscriber = spy("Subscriber")
      publisher.subscribe subscriber

      publisher.send :publish, :something_happened

      subscriber.should have_received(:something_happened)
    end

    it 'prevents publishing unspecified events' do
      expect {
        publisher.send :publish, :something_else_happened
      }.to raise_error(Publisher::CannotPublishEvent)
    end
  end

  describe '#subscribe' do
    it 'allows subscriptions if the subscriber does have the method' do
      subscriber = MySubscriber.new

      publisher.subscribe subscriber, on: :something_happened
      publisher.listeners.map(&:listener).should include subscriber
    end

    it 'prevents subscriptions if the subscriber cannot handle the event' do
      subscriber = Object.new
      expect {
        publisher.subscribe subscriber, on: :something_happened
      }.to raise_error(Publisher::MissingEventHandler)
    end

    it 'prevents subscriptions if the publisher does not publish the event' do
      subscriber = MySubscriber.new

      expect {
        publisher.subscribe subscriber, on: :something_else_done

      }.to raise_error(Publisher::PublisherDoesNotPublishEvent)
    end
  end
end

Broadcasting to protected methods

First thanks for the hard work you have put in on Wisper. I love this library and use it in every Ruby project I participate in.

I am not sure how you will feel about this issue, but I do not particularly like that the message handlers must be public methods as it mucks up the public API of the class. Have you considered when determining if a listener will receive a broadcasted message with respond_to?( :some_message, true )?

If you wanted to get a little more restrictive you could only allow for public or protected methods and not use #respond_to?, but instead a custom method that looks at only public and protected methods.

C# handles this by exposing a public delegate and a protected handler method. This allows for a clear public API of the class.

Thanks!

Testing Wisper with Minitest

Hi,

this is not really an issue... just wanted to let you know that I've began porting wisper-rspec to a wisper-minitest gem!

Hopefully it'll be useful for people who want to use Minitest as their testing framework.

Get rid of warnings

/wisper-2.0.0.rc1/lib/wisper/configuration.rb:29: warning: method redefined; discarding old fetch
/forwardable.rb:181: warning: previous definition of fetch was here
/wisper-2.0.0.rc1/lib/wisper/broadcasters/logger_broadcaster.rb:19: warning: shadowing outer local variable - id_method

configuration might need to undef the method first.

`respond_to` name collision

Hello,

I was trying out Wisper, creating a global listener and using a scaffolded ActionController as a publisher. After the action was finished, the controller was supposed to broadcast a different message whether a save was successful or not. But I got this exception:

must give at least one event

Here is my code

 respond_to do |format|  # Exception marked here
      if @my_model.save
        broadcast(:create_model_success, @my_model)
        format.html { redirect_to @my_model, notice: 'My model was successfully created.' }
        format.json { render :show, status: :created, location: @my_model }
      else
        broadcast(:create_model_error, @my_model)
        format.html { render :new }
        format.json { render json: @my_model.errors, status: :unprocessable_entity }
      end
    end

I then noticed the publisher module has a respond_to method. So my guess is a name collision. Here is my working code:

# respond_to do |format|
      if @my_model.save
        broadcast(:create_model_success, @my_model)
        redirect_to @my_model, notice: 'My model was successfully created.'
        # format.html { redirect_to @my_model, notice: 'My model was successfully created.' }
        # format.json { render :show, status: :created, location: @my_model }
      else
        broadcast(:create_model_error, @my_model)
        render :new
        # format.html { render :new }
        # format.json { render json: @my_model.errors, status: :unprocessable_entity }
      end
    # end

You can check my complete project here: https://github.com/SebastianOsuna/wisper_mailing

Inheriting of events

Use case: One publisher calls another publisher, the second publisher need to inherit subscribers from the first.

class SecondPublisher
  include Wisper::Publisher
end

class FirstPublisher
  include Wisper::Publisher

  def execute
    # ...
    command = SecondPublisher.new
    command.inherit_listeners(self)  # NEW METHOD TO BE ADDED
    command.execute
    # ...
    broadcast(:successful)
  end
end

The SecondPublisher should inherit the successful event from FirstPublisher.

Get rid of warnings

/wisper-2.0.0.rc1/lib/wisper/configuration.rb:29: warning: method redefined; discarding old fetch
/forwardable.rb:181: warning: previous definition of fetch was here
/wisper-2.0.0.rc1/lib/wisper/broadcasters/logger_broadcaster.rb:19: warning: shadowing outer local variable - id_method

configuration might need to undef the method first.

Recommendations for testing wispered controllers?

I'm trying to figure out a good technique for testing wisper driven controllers. For example:

class UserServicesController < ApplicationController
  def create
    connector = Reverb::Actions::ConnectOauth.new(omniauth_request)

    connector.on(:user_oauth_failure) do |user|
      flash[:error] = "Sorry, we were unable to log you in. Please try again."
      @user = user
      return render :template => 'users/new'
    end

    connector.connect

    return_or_redirect_to root_path
  end

So what are we interested in from the controller perspective:

  1. that the business action was invoked. This can be done with ConnectOauth.should_receive
  2. that on failure the controller does the right thing. How do we test this part? Do we substitute the creation of the ConnectOauth action with some other test publisher?

Publish for class method

When I try to publish something from a class method it returns the error:

undefined method publish' for Fo:Class`

class Foo
   include Wisper::Publisher

   def self.do_something
      publish(:something_happened)
   end
end

What would be the suggested approach here ?

method name 'subscribe' is a bit confusing. 'add_as_listener' , 'add_listener' as aliases ?

From the docs, I see that the way to to 'couple' a Publisher and a Listener is to call the 'subscribe' method on the Publisher instance.
So, @publisher.subscribe(SomeListener.new)

Actually, the subscription is happening the other way around, i.e the Listener is subscribed to the publisher's events and the listener's methods get invoked when an event is 'fired'.

Maybe 'add_as_listener' or 'add_listener' are alternatives that better express the actual intent?
So, we could have
@publisher.add_listener(SomeListener)

Replace ActiveRecord example with one using callbacks

Instead of creating a new method commit as per the example in the README and Wiki instead callbacks could be used?

class Bid < ActiveRecord::Base
  include Wisper::Publisher

  after_create :publish_creation_successful
  after_validation :publish_creation_failed, on: :create

  private

  def publish_creation_successful
    broadbast(:bid_creation_successful, self)
  end

  def publish_creation_failed
    broadbast(:bid_creation_failed, self) if errors.any?
  end
end

Need to test of this actually works...

Allow broadcast events to be prefixed with "on"

subscribe allows a new option, prefix, the given value is prefixed to all broadcast events:

subscribe(MyListenter.new, :prefix => 'on')

Events are broadcast as normal:

broadcast(:create_delivery_successful, delivery)

But the subscribed listener, if it responds, receives on_create_delivery_successful:

class My Listener
  def on_create_delivery_successful(delivery)
    # ...
  end
end

I can't think of any other useful prefix other than "on" so prefix could also accept true and the prefix would default to "on":

subscribe(MyListenter.new, :prefix => true)

Ability to stub wisper publishers with message expectations (should_receive with args)

As we're testing our publishers with unit tests, we sometimes want to do something like:

publisher.should_receive(:do_a_thing).with(args).and_publish(:some_event, arg1, arg2)

I am starting to think about how to implement this, but if you have any insight or experience with RSpec, that would help tremendously. I think we should agree on the syntax first (my hacky stub_wisper_publisher needs rework), and then move on from there. Are you interested in this as a PR if we can figure out the right syntax?

thanks!

warn if broadcaster key overridden

If a broadcaster key is configured more than once, for example both wisper-sidekiq and wisper-celluloid gems are required, then a warning should be issued.

when registering a broadcaster
  and key is already present
    issue warning

One of my broadcast calls does not work in production

Hi,
i have a really ridiculous situation going on my production server. I'm using wisper with my rails app and, from among about 50 broadcast calls here and there in my source code, one of these broadcast calls is not beeing triggered.

I'm defining a global listeners in rails initializers:

# config/initializers/wisper.rb
Wisper.subscribe(LogListener.new)
# ... other listeners

And LogListener looks like that:

class LogListener

  def create_job_success(job)
    log "Job has been created [##{job.id}]"
  end

  def create_job_success2(job)
    log "Job has been created [##{job.id}]"
  end

  def test_event
    log "It works"
  end
end

And also i have a service which, let's say, have a following code:

class MyService
  def call
    broadcast :test_event
    broadcast :create_job_success
    broadcast :create_job_success2
    broadcast :test_event
  end
end

And here is the deal - when running my service with MyService.new.call, i will always get 'It works' produced by test_event broadcast and create_job_success2 is also beeing triggered (i have that fully covered by rspec tests). What's more - according to rspec results, create_job_success is also getting triggered properly (these specs covers only this single MyService class, nothing more. So the issue can be somewhere else) .

However, in production, this create_job_success event is not getting triggered (while the others are getting triggered properly - i know that by watching log results produced by LogListener). I also can't see any exception or antyhing like that - it just doesn't work and falls silently.

That situation drives me mad becuase i'm completely not able to debug what's going on and my boss want's to kill me so i have a question to you guys - have you any idea what can possibly create such situation, in which wisper loggers/listeners fails silently or have you any idea what can be wrong with my code?

Thanks in advance!

A copy of <SomeListenerHere> has been removed from the module tree but is still active!

Hello!
I'm trying to use global listeners in my rails app. According to wisper docs, i should put the following code in my initializer:

Wisper.subscribe(OutstandingWorkValueListener.new)
Wisper.subscribe(JobVideoCreatorListener.new)
Wisper.subscribe(JobRatingsListener.new)

However, for some reason, from time to time (it's completely unpredictible! sometimes restarting a server does the job, sometimes not) i'm getting an error that says for exmaple:

A copy of OutstandingWorkValueListener has been removed from the module tree but is still active!

Am i doing something wrong? What does that error mean? Thanks in advance for any clues.

Temporary Global Registration (thread local or request local)

I've been thinking about how to deal with multiple things happening on multiple children of an aggregate root. Something along these lines:

global_subscribe(listener1, listener2) do
  # ... do something ...
end

The idea is that I don't always want the global subscription, but I'm not able to or I don't want to add_listener on each child. Of course, the parent could "forward" the events, but I'm not sure that's the right pattern. It's a little more tedious.

Allow multiple listeners to be added using a block

my_publisher.listeners do
  add ActivityFeedListener.new
  add StatisticsListener.new
end

and

Wisper.global_listeners do
  add ActivityFeedListener.new
  add StatisticsListener.new
end

Work in progress in "new_add_listener_syntax" branch

License missing from gemspec

Some companies will only use gems with a certain license.
The canonical and easy way to check is via the gemspec,

via e.g.

spec.license = 'MIT'
# or
spec.licenses = ['MIT', 'GPL-2']

Even for projects that already specify a license, including a license in your gemspec is a good practice, since it is easily
discoverable there without having to check the readme or for a license file.

For example, there is a License Finder gem to help companies ensure all gems they use
meet their licensing needs. This tool depends on license information being available in the gemspec. This is an important enough
issue that even Bundler now generates gems with a default 'MIT' license.

If you need help choosing a license (sorry, I haven't checked your readme or looked for a license file),
github has created a license picker tool.

In case you're wondering how I found you and why I made this issue, it's because I'm collecting stats on gems (I was originally
looking for download data) and decided to collect license metadata,too, and make issues for gemspecs not specifying a license as a public service :).

I hope you'll consider specifying a license in your gemspec. If not, please just close the issue and let me know. In either case, I'll follow up. Thanks!

p.s. I've written a blog post about this project

Pass the name of the event as a final argument

It is possible to subscribe to all or multiple events with a single method using :with. It would make sense not to lose information on the subscriber side of the event name. This is useful in cross cutting subscribers like logging or analytics.

Currently I'm working around this with method_missing.

Would be happy to submit a PR if this is a sensible idea.

alias global and temporary listener methods to "subscribe" for consistancy

Allow:

Wisper.add_listener(listener) to be invoked as Wisper.subscribe(listeners)

Wisper.with_listeners(listeners) { } to be invoked as Wisper.subscribe(listeners) { }

Must retain backwards compatibility.

New method Wisper#subscribe will be something like:

def self.subscribe(*listeners, &block)
  options = listeners.last.is_a?(Hash) ? listeners.pop : {}
  if block_given?
    TemporaryListeners.with(listeners, block)
  else
    GlobalListeners.add(listeners, options)
  end
end

Temporary Listener for all events

Is there any possibility to listen to all events without explicitly defining them? So to say, to define a "wrapper" or a "proxy" method which is called if any event is emitted, but without a) defining a method for each event and b) loosing the information about the fired event?

I know I can use the "with" method to redirect all events to one method, but this way I gonna loose the (very important) information, which event was called originally. The other option I can think of (if overwriting "send" in my listener is not an option...) is writing my own broadcaster (like this https://github.com/krisleech/wisper/blob/master/lib/wisper/broadcasters/logger_broadcaster.rb) for it, but can I attach this broadcaster only for one temporary subscription like this:

Wisper.subscribe(MyPublicLoggingListener.new, broadcaster: :my_logging_broadcaster) do 
  # anything
end

class MyPublicLoggingListener
  def any_event_emitted(event)
    log(event)
  end
end

Allow listener to be subscribed to all instances of a class

class CreateDelivery
  include Wisper::Publisher
end

CreateDelivery.add_listener(StatsListener.new) 

This is like global subscription but scoped to instances of a single class.

Another option would be to add an option to global subscriptions allowing scoping to a class (or classes):

Wisper.add_listener(MyListener.new, :scope => CreateDelivery)

Unit testing for publishing?

What's the 'correct' way to verify if a model is publishing an event? I tried this:

class User
  include Wisper::Publisher

  after_create do |user|
    publish(:user_created, user)
  end
end

it "publishes a message on creation" do                                        
  allow(Wisper::Publisher).to receive(:publish)                                                                                                         
  user = create(:user)                                                                                                                                         
  expect(Wisper::Publisher).to have_received(:publish).with(:user_created, user)
end

But this fails with an unmet expectation - I think because publish is a private method?

Make dispatch of event to listener easier to override

So we can provide integration with Sidekiq, Celluloid and similar we need to allow dispatch of an event (i.e. public_send) inWisper::ObjectRegistration#broadcast to be overriden easily in another gem.

broadcast should be reduced to something like:

def broadcast(event, *args)
  broadcast_event(event, *args) if should_broadcast?(event)
end

This allows either broadcast or broadcast_event to be overriden to provide alternative dispatch of events in gems like wisper-async and (hopefully forthcoming) wisper-sidekiq.

`require': cannot load such file -- celluloid/autostart (LoadError)

Trying out master and for some reason celluloid isn't getting picked up as a dependency. I could probably add it to my Gemfile manually, but I wonder if it would work to make a wisper-celluloid or if celluloid was optional. It feels weird having a simple pub sub framework bundled celluloid with it.

Allow #broadcast method to return its own arguments

What

Instead of the last registration.broadcast results

def broadcast(event, *args)
  registrations.each do | registration |
    registration.broadcast(clean_event(event), self, *args)
  end
end

let it the method returns arguments of its own:

def broadcast(event, *args)
  registrations.each do | registration |
    registration.broadcast(clean_event(event), self, *args)
  end
  return event, *args
end

Why

I like using the method in my service objects

require 'wisper'

class MyService
  include Wisper::Publisher

  def run
    begin
      # do something
    rescue SomeError
      publish :some_error
    rescue AnotherError
      publish :another_error
    rescue
      publish :common_error
    else
      publish :success
    end
  end
end

This works fine in controllers

class MyController < ApplicationController

  def my_action
    service = MyService.new
    service.subscribe self
    service.run
  end

  def success
    # send some responce
  end
end

Here the controller is playing the role of some listener with a 'responce methods' defined.

But when I'm trying to use one service inside another one, I need to set special listener to catch published notifications. Instead I'd like in some cases to get an answer from MyService#run directly.

service = MyService.run

Yeah, I know this «breaks» the pattern, but can be useful in special cases.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.