- Notifications
You must be signed in to change notification settings - Fork0
nedap/wisper-compat
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A micro library providing Ruby objects with Publish-Subscribe capabilities
- 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 dependant on Rails.
Please also see theWiki for more additional information and articles.
For greenfield applications you might also be interested inWisperNext andMa.
Add this line to your application's Gemfile:
gem'wisper-compat','4.0.0'
Any class with theWisper::Publisher
module included can broadcast eventsto subscribed listeners. Listeners subscribe, at runtime, to the publisher.
classCancelOrderincludeWisper::Publisherdefcall(order_id)order=Order.find_by_id(order_id)# business logic...iforder.cancelled?broadcast(:cancel_order_successful,order.id)elsebroadcast(:cancel_order_failed,order.id)endendend
When a publisher broadcasts an event it can include any number of arguments.
Thebroadcast
method is also aliased aspublish
.
You can also includeWisper.publisher
instead ofWisper::Publisher
.
Any object can be subscribed as a listener.
cancel_order=CancelOrder.newcancel_order.subscribe(OrderNotifier.new)cancel_order.call(order_id)
The listener would need to implement a method for every event it wishes to receive.
classOrderNotifierdefcancel_order_successful(order_id)order=Order.find_by_id(order_id)# notify someone ...endend
Blocks can be subscribed to single events and can be chained.
cancel_order=CancelOrder.newcancel_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 usingon
by passingadditional events as arguments.
cancel_order=CancelOrder.newcancel_order.on(:cancel_order_successful){ |order_id| ...}.on(:cancel_order_failed,:cancel_order_invalid){ |order_id| ...}cancel_order.call(order_id)
Do notreturn
from inside a subscribed block, due to the wayRuby treats blocksthis will prevent any subsequent listeners having their events delivered.
cancel_order.subscribe(OrderNotifier.new,async:true)
Wisper has various adapters for asynchronous event handling, please refer towisper-celluloid,wisper-sidekiq,wisper-activejob,wisper-que orwisper-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:
classOrderNotifier# declare a class method if you are subscribing the listener class instead of its instance like:# cancel_order.subscribe(OrderNotifier)#defself.cancel_order_successful(order_id)order=Order.find_by_id(order_id)# notify someone ...endend
classCancelOrderController <ApplicationControllerdefcreatecancel_order=CancelOrder.newcancel_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_toorder_path(order_id)}cancel_order.on(:cancel_order_failed){ |order_id|renderaction::new}cancel_order.call(order_id)endend
If you wish to publish directly from ActiveRecord models you can broadcast events from callbacks:
classOrder <ActiveRecord::BaseincludeWisper::Publisherafter_commit:publish_creation_successful,on::createafter_validation:publish_creation_failed,on::createprivatedefpublish_creation_successfulbroadcast(:order_creation_successful,self)enddefpublish_creation_failedbroadcast(:order_creation_failed,self)iferrors.any?endend
There are more examples in theWiki.
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.
Global listeners are threadsafe. Subscribers will receive events published on all threads.
You might want to globally subscribe a listener to publishers with a certainclass.
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 theirsubclasses.
Alternatively you can also do exactly the same with a publisher class itself:
MyPublisher.subscribe(MyListener.new)
You can also globally subscribe listeners for the duration of a block.
Wisper.subscribe(MyListener.new,OtherListener.new)do# do stuffend
Any events broadcast within the block by any publisher will be sent to thelisteners.
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.
By default a listener will get notified of all events it can respond to. Youcan limit which events a listener is notified of by passing a string, symbol,array or regular expression toon
:
post_creator.subscribe(PusherListener.new,on::create_post_successful)
If you would prefer listeners to receive events with a prefix, for exampleon
, you can do so by passing a string or symbol toprefix:
.
post_creator.subscribe(PusherListener.new,prefix::on)
Ifpost_creator
were to broadcast the eventpost_created
the subscribedlisteners would receiveon_post_created
. You can also passtrue
which willuse the default prefix, "on".
By default the method called on the listener is the same as the eventbroadcast. However it can be mapped to a different method usingwith:
.
report_creator.subscribe(MailResponder.new,with::successful)
This is pretty useless unless used in conjunction withon:
, since all eventswill 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 toon:
each event will be mapped to the samemethod whenwith:
is specified. If you need to listen for select eventsand map each one to a different method subscribe the listener once foreach 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 suchalias successful create_report_successful
.
Testing matchers and stubs are in separate gems.
If you use global listeners in non-feature tests youmight want to clear themin a hook to prevent global subscriptions persisting between tests.
after{Wisper.clear}
TheWiki has more examples,articles and talks.
Got a specific question, try theWisper tag on StackOverflow.
bundle exec rspec
To run the specs on code changes tryentr:
ls **/*.rb | entr bundle exec rspec
Please read theContributing Guidelines.
(The MIT License)
Copyright (c) 2013 Kris Leech
Permission is hereby granted, free of charge, to any person obtaining a copy ofthis software and associated documentation files (the 'Software'), to deal inthe Software without restriction, including without limitation the rights touse, copy, modify, merge, publish, distribute, sublicense, and/or sell copiesof the Software, and to permit persons to whom the Software is furnished to doso, subject to the following conditions:
The above copyright notice and this permission notice shall be included in allcopies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHERLIABILITY, 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 THESOFTWARE.
About
A micro library providing Ruby objects with Publish-Subscribe capabilities