Downstream
This gem provides a straightforward way to implement communication between Rails Engines using the Publish-Subscribe pattern. The gem allows decreasing decoupling engines with events. An event is a recorded object in the system that reflects an action that the engine performs, and the params that lead to its creation.
The gem inspired by active_event_store
, and initially based on its codebase. Having said that, it does not store in a database all happened events which ensures simplicity and performance.
Installation
Add this line to your application's Gemfile:
gem "downstream", "~> 1.0"
Usage
Under the hood it's a wrapper for ActiveSupport::Notifications
. But it provides a way more handy interface to build reactive apps. Each event has a strict schema described by a separate class. Also, the gem has convenient tooling to write tests.
Describe events
Events are represented by event classes, which describe events payloads and identifiers:
class ProfileCreated < Downstream::Event
# (optional)
# Event identifier is used for streaming events to subscribers.
# By default, identifier is equal to underscored class name.
# You don't need to specify identifier manually, only for backward compatibility when
# class name is changed.
self.identifier = "profile_created"
# Add attributes accessors
attributes :user
end
Each event has predefined (reserved) fields:
event_id
– unique event idtype
– event type (=identifier)
NOTE: events should be in the past tense and describe what happened (e.g. "ProfileCreated", "EventPublished", etc.).
Events are stored in app/events
folder.
Publish events
To publish an event you must first create an instance of the event class and call Downstream.publish
method:
event = ProfileCompleted.new(user: user)
# then publish the event
Downstream.publish(event)
That's it! Your event has been stored and propagated.
Subscribe to events
To subscribe a handler to an event you must use Downstream.subscribe
method.
You should do this in your app or engine initializer:
# some/engine.rb
initializer "my_engine.subscribe_to_events" do
# To make sure event store is initialized use load hook
# `store` == `Downstream`
ActiveSupport.on_load "downstream-events" do |store|
store.subscribe MyEventHandler, to: ProfileCreated
# anonymous handler (could only be synchronous)
store.subscribe(to: ProfileCreated) do |name, event|
# do something
end
# you can omit event if your subscriber follows the convention
# for example, the following subscriber would subscribe to
# ProfileCreated event
store.subscribe OnProfileCreated::DoThat
end
end
NOTE: event handler must be a callable object.
Although subscriber could be any callable Ruby object, that have specific input format (event); thus we suggest putting subscribers under app/subscribers/on_<event_type>/<subscriber.rb>
, e.g. app/subscribers/on_profile_created/create_chat_user.rb
).
Testing
You can test subscribers as normal Ruby objects.
To test that a given subscriber exists, you can do the following:
it "is subscribed to some event" do
allow(MySubscriberService).to receive(:call)
event = MyEvent.new(some: "data")
Downstream.publish event
expect(MySubscriberService).to have_received(:call).with(event)
end
To test publishing use have_published_event
matcher:
expect { subject }.to have_published_event(ProfileCreated).with(user: user)
NOTE: have_published_event
only supports block expectations.
NOTE 2 with
modifier works like have_attributes
matcher (not contain_exactly
);