{"id":13878742,"url":"https://github.com/palkan/downstream","last_synced_at":"2025-10-24T16:32:28.404Z","repository":{"id":39903115,"uuid":"422118875","full_name":"palkan/downstream","owner":"palkan","description":"Straightforward way to implement communication between Rails Engines using the Publish-Subscribe pattern.","archived":false,"fork":false,"pushed_at":"2025-02-14T19:41:10.000Z","size":62,"stargazers_count":61,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-31T16:13:53.663Z","etag":null,"topics":["dip","event-driven-architecture","evilmartians","rails","rails-engines","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/palkan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-10-28T08:09:25.000Z","updated_at":"2025-03-19T03:44:09.000Z","dependencies_parsed_at":"2025-03-31T16:12:39.240Z","dependency_job_id":"488fe7a7-e1fa-43af-8a20-f9256bfa0703","html_url":"https://github.com/palkan/downstream","commit_stats":{"total_commits":22,"total_committers":4,"mean_commits":5.5,"dds":0.2727272727272727,"last_synced_commit":"48cce77796dc03a8f41d7460571d3e3e5abea6b2"},"previous_names":["bibendi/downstream"],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fdownstream","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fdownstream/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fdownstream/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fdownstream/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/palkan","download_url":"https://codeload.github.com/palkan/downstream/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247694875,"owners_count":20980733,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dip","event-driven-architecture","evilmartians","rails","rails-engines","ruby"],"created_at":"2024-08-06T08:01:58.500Z","updated_at":"2025-10-24T16:32:28.399Z","avatar_url":"https://github.com/palkan.png","language":"Ruby","readme":"[![Gem Version](https://badge.fury.io/rb/downstream.svg)](https://badge.fury.io/rb/downstream)\n[![Build Status](https://github.com/bibendi/downstream/workflows/Ruby/badge.svg?branch=master)](https://github.com/bibendi/downstream/actions?query=branch%3Amaster)\n\n# Downstream\n\nThis gem provides a straightforward way to implement communication between Rails Engines using the Publish-Subscribe pattern. The gem allows decreasing the coupling of 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.\n\nThe gem inspired by [`active_event_store`](https://github.com/palkan/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.\n\n\u003ca href=\"https://evilmartians.com/?utm_source=bibendi-downstream\"\u003e\n\u003cimg src=\"https://evilmartians.com/badges/sponsored-by-evil-martians.svg\" alt=\"Sponsored by Evil Martians\" width=\"236\" height=\"54\"\u003e\u003c/a\u003e\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"downstream\", \"~\u003e 1.0\"\n```\n\n## Usage\n\nDownstream provides a way more handy interface to build reactive apps. Each event has a strict schema described by a separate class. The gem has convenient tooling to write tests.\n\nDownstream supports various adapters for event handling. It can be configured in a Rails initializer `config/initializers/downstream.rb`:\n\n```ruby\nDownstream.configure do |config|\n  config.pubsub = :stateless # it's a default adapter\n  config.async_queue = :high_priority # nil by default\nend\n```\n\nFor now, it's implemented only one adapter. The `stateless` adapter is based on `ActiveSupport::Notifications`, and it doesn't store history events anywhere. All event invocations are synchronous. Adding asynchronous subscribers are on my road map.\n\n### Describe events\n\nEvents are represented by _event classes_, which describe events payloads and identifiers:\n\n```ruby\nclass ProfileCreated \u003c Downstream::Event\n  # (optional)\n  # Event identifier is used for streaming events to subscribers.\n  # By default, identifier is equal to underscored class name.\n  # You don't need to specify identifier manually, only for backward compatibility when\n  # class name is changed.\n  self.identifier = \"profile_created\"\n\n  # Add attributes accessors\n  attributes :user\nend\n```\n\nEach event has predefined (_reserved_) fields:\n- `event_id` – unique event id\n- `type` – event type (=identifier)\n\n**NOTE:** events should be in the past tense and describe what happened (e.g. \"ProfileCreated\", \"EventPublished\", etc.).\n\nEvents are stored in `app/events` folder.\n\nYou can also define events using the Data-interface:\n\n```ruby\nProfileCreated = Downstream::Event.define(:user)\n\n# or with an explicit identifier\nProfileCreated = Downstream::Event.define(:user) do\n  self.identifier = \"user.profile_created\"\nend\n```\n\nDate-events provide the same interface as regular events but use Data classes for keeping event payloads (`event.data`) and are frozen (as well as their derivatives, such as `event.to_h`).\n\n\u003e [!NOTE]\n\u003e Data-events are only available in Ruby 3.2+.\n\n### Publish events\n\nTo publish an event you must first create an instance of the event class and call `Downstream.publish` method:\n\n```ruby\nevent = ProfileCompleted.new(user: user)\n\n# then publish the event\nDownstream.publish(event)\n```\n\nThat's it! Your event has been stored and propagated.\n\n### Subscribe to events\n\nTo subscribe a handler to an event you must use `Downstream.subscribe` method.\n\nYou should do this in your app or engine initializer:\n\n```ruby\n# some/engine.rb\n\ninitializer \"my_engine.subscribe_to_events\" do\n  # To make sure event store is initialized use load hook\n  # `store` == `Downstream`\n  ActiveSupport.on_load \"downstream-events\" do |store|\n    store.subscribe MyEventHandler, to: ProfileCreated\n\n    # anonymous handler (could only be synchronous)\n    store.subscribe(to: ProfileCreated) do |event|\n      # do something\n    end\n\n    # you can omit event if your subscriber follows the convention\n    # for example, the following subscriber would subscribe to\n    # ProfileCreated event\n    store.subscribe OnProfileCreated::DoThat\n  end\nend\n```\n\n**NOTE:** event handler **must** be a callable object.\n\nAlthough subscriber could be any callable Ruby object, that have specific input format (event); thus we suggest putting subscribers under `app/subscribers/on_\u003cevent_type\u003e/\u003csubscriber.rb\u003e`, e.g. `app/subscribers/on_profile_created/create_chat_user.rb`).\n\nSometimes, you may be interested in using temporary subscriptions. For that, you can use this:\n\n```ruby\nsubscriber = -\u003e(event) { my_event_handler(event) }\nDownstream.subscribed(subscriber, to: ProfileCreated) do\n  some_invocation\nend\n```\n\nIf you want to handle events in a background job, you can pass the `async: true` option:\n\n```ruby\nstore.subscribe OnProfileCreated::DoThat, async: true\n```\n\nBy default, a job will be enqueued into `async_queue` name from the Downstream config. You can define your own queue name for a specific subscriber:\n\n```ruby\nstore.subscribe OnProfileCreated::DoThat, async: {queue: :low_priority}\n```\n\n**NOTE:** all subscribers are synchronous by default\n\n### Subscriber classes\n\nYou can also use subscriber objects based on `Downstream::Subscriber` class. For example:\n\n```ruby\nclass CRMSubscriber \u003c Downstream::Subscriber\n  def profile_created(event)\n    # handle \"profile_created\" event\n  end\n\n  def project_created(event)\n    # handle \"project_created\" event\n  end\nend\n\nstore.subscribe CRMSubscriber, async: true\n```\n\nThe subscriber object allows you to subscribe to multiple events at once: each public method is considered an event handler for the same named event.\n\n## Testing\n\nYou can test subscribers as normal Ruby objects.\n\nFirst, load testing helpers in the `spec_helper.rb`:\n\n```ruby\nrequire \"downstream/rspec\"\n```\n\nTo test that a given subscriber exists, you can do the following:\n\n```ruby\nit \"is subscribed to some event\" do\n  allow(MySubscriberService).to receive(:call)\n\n  event = MyEvent.new(some: \"data\")\n\n  Downstream.publish event\n\n  expect(MySubscriberService).to have_received(:call).with(event)\nend\n\n# for asynchronous subscriptions\nit \"is subscribed to some event\" do\n  event = MyEvent.new(some: \"data\")\n  expect { Downstream.publish event }\n    .to have_enqueued_async_subscriber_for(MySubscriberService)\n    .with(event)\nend\n```\n\nTo test publishing use `have_published_event` matcher:\n\n```ruby\nexpect { subject }.to have_published_event(ProfileCreated).with(user: user)\n```\n\n**NOTE:** `have_published_event` only supports block expectations.\n\n**NOTE 2** `with` modifier works like `have_attributes` matcher (not `contain_exactly`);\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpalkan%2Fdownstream","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpalkan%2Fdownstream","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpalkan%2Fdownstream/lists"}