• Stars
    star
    180
  • Rank 213,042 (Top 5 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 4 years ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Rails Event Store in a more Rails way

Gem Version Build

Active Event Store

Active Event Store is a wrapper over Rails Event Store which adds conventions and transparent Rails integration.

Motivation

Why creating a wrapper and not using Rails Event Store itself?

RES is an awesome project but, in our opinion, it lacks Rails simplicity and elegance (=conventions and less boilerplate). It's an advanced tool for advanced developers. We've been using it in multiple projects in a similar way, and decided to extract our approach into this gem (originally private).

Secondly, we wanted to have a store implementation independent API that would allow us to adapterize the actual event store in the future (something like ActiveEventStore.store_engine = :rails_event_store or ActiveEventStore.store_engine = :hanami_events).

Sponsored by Evil Martians

Installation

Add the gem to your project:

# Gemfile
gem "active_event_store", "~> 1.0"

Setup database according to the Rails Event Store docs:

rails generate rails_event_store_active_record:migration
rails db:migrate

Requirements

  • Ruby (MRI) >= 2.6
  • Rails >= 6.0
  • RailsEventStore >= 2.1

Usage

Describe events

Events are represented by event classes, which describe events payloads and identifiers:

class ProfileCompleted < ActiveEventStore::Event
  # (optional) event identifier is used for transmitting events
  # to subscribers.
  #
  # By default, identifier is equal to `name.underscore.gsub('/', '.')`.
  #
  # You don't need to specify identifier manually, only for backward compatibility when
  # class name is changed.
  self.identifier = "profile_completed"

  # Add attributes accessors
  attributes :user_id

  # Sync attributes only available for sync subscribers
  # (so you can add some optional non-JSON serializable data here)
  # For example, we can also add `user` record to the event to avoid
  # reloading in sync subscribers
  sync_attributes :user
end

NOTE: we use JSON to serialize events, thus only the simple field types (numbers, strings, booleans) are supported.

Each event has predefined (reserved) fields:

  • event_id – unique event id
  • type – event type (=identifier)
  • metadata

We suggest to use a naming convention for event classes, for example, using the past tense and describe what happened (e.g. "ProfileCreated", "EventPublished", etc.).

We recommend to keep event definitions in the app/events folder.

Events registration

Since we use abstract identifiers instead of class names, we need a way to tell our mapper how to infer an event class from its type.

In most cases, we register events automatically when they're published or when a subscription is created.

You can also register events manually:

# by passing an event class
ActiveEventStore.mapper.register_event MyEventClass

# or more precisely (in that case `event.type` must be equal to "my_event")
ActiveEventStore.mapper.register "my_event", MyEventClass

Publish events

To publish an event you must first create an instance of the event class and call ActiveEventStore.publish method:

event = ProfileCompleted.new(user_id: user.id)

# or with metadata
event = ProfileCompleted.new(user_id: user.id, metadata: {ip: request.remote_ip})

# then publish the event
ActiveEventStore.publish(event)

That's it! Your event has been stored and propagated to the subscribers.

Subscribe to events

To subscribe a handler to an event you must use ActiveEventStore.subscribe method.

You can do this in your app or engine initializer:

# some/engine.rb

# To make sure event store has been initialized use the load hook
# `store` == `ActiveEventStore`
ActiveSupport.on_load :active_event_store do |store|
  # async subscriber – invoked from background job, enqueued after the current transaction commits
  # NOTE: all subscribers are asynchronous by default
  store.subscribe MyEventHandler, to: ProfileCreated

  # sync subscriber – invoked right "within" `publish` method
  store.subscribe MyEventHandler, to: ProfileCreated, sync: true

  # anonymous handler (could only be synchronous)
  store.subscribe(to: ProfileCreated, sync: true) do |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

Subscribers could be any callable Ruby objects that accept a single argument (event) as its input or classes that inherit from Class and have #call as an instance method.

We suggest putting subscribers to the app/subscribers folder using the following convention: app/subscribers/on_<event_type>/<subscriber.rb>, e.g. app/subscribers/on_profile_created/create_chat_user.rb.

NOTE: Active Job must be loaded to use async subscribers (i.e., require "active_job/railtie" or require "rails/all" in your config/application.rb).

NOTE: Subscribers that inherit from Class and implement call as a class method will not be instantiated.

Testing

You can test subscribers as normal Ruby objects.

NOTE To test using minitest include the ActiveEventStore::TestHelpers module in your tests.

To test that a given subscriber exists, you can use the have_enqueued_async_subscriber_for matcher:

# for asynchronous subscriptions (rspec)
it "is subscribed to some event" do
  event = MyEvent.new(some: "data")
  expect { ActiveEventStore.publish event }
    .to have_enqueued_async_subscriber_for(MySubscriberService)
    .with(event)
end

# for asynchronous subscriptions (minitest)
def test_is_subscribed_to_some_event
  event = MyEvent.new(some: "data")

  assert_async_event_subscriber_enqueued(MySubscriberService, event: event) do
    ActiveEventStore.publish event
  end
end

NOTE Async event subscribers are queued only after the current transaction has committed so when using assert_enqued_async_subcriber in rails make sure to have self.use_transactional_fixtures = false at the top of your test class.

NOTE: You must have rspec-rails gem in your bundle to use have_enqueued_async_subscriber_for matcher.

For synchronous subscribers using have_received is enough:

it "is subscribed to some event" do
  allow(MySubscriberService).to receive(:call)

  event = MyEvent.new(some: "data")

  ActiveEventStore.publish event

  expect(MySubscriberService).to have_received(:call).with(event)
end

To test event publishing, use have_published_event matcher:

# rspec
expect { subject }.to have_published_event(ProfileCreated).with(user_id: user.id)

# minitest
assert_event_published(ProfileCreated, with: {user_id: user.id}) { subject }

NOTE: have_published_event and assert_event_published only supports block expectations.

NOTE 2 with modifier works like have_attributes matcher (not contain_exactly); you can only specify serializable attributes in with (i.e. sync attributes are not supported, 'cause they are not persistent).

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/active_event_store.

License

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

More Repositories

1

logidze

Database changes log for Rails
Ruby
1,596
star
2

action_policy

Authorization framework for Ruby/Rails applications
Ruby
1,404
star
3

isolator

Detect non-atomic interactions within DB transactions
Ruby
850
star
4

anyway_config

Configuration library for Ruby gems and applications
Ruby
735
star
5

active_delivery

Ruby framework for keeping all types of notifications (mailers, push notifications, whatever) in one place
Ruby
595
star
6

n_plus_one_control

RSpec and Minitest matchers to prevent N+1 queries problem
Ruby
556
star
7

store_attribute

ActiveRecord extension which adds typecasting to store accessors
Ruby
376
star
8

view_component-contrib

A collection of extension and developer tools for ViewComponent
Ruby
345
star
9

litecable

Lightweight Action Cable implementation (Rails-free)
Ruby
298
star
10

acli

Action Cable command-line client
Ruby
230
star
11

action-cable-testing

Action Cable testing utils
Ruby
212
star
12

rubanok

Parameters-based transformation DSL
Ruby
205
star
13

action_policy-graphql

Action Policy integration for GraphQL
Ruby
151
star
14

engems

Rails component-based architecture on top of engines and gems (showroom)
Ruby
136
star
15

influxer

InfluxDB ActiveRecord-style
Ruby
118
star
16

abstract_notifier

ActionMailer-like interface for any type of notifications
Ruby
116
star
17

turbo-music-drive

Exploring Turbo future features while building a music library app
Ruby
102
star
18

wsdirector

All the world's a server, and all the men and women merely clients
Ruby
102
star
19

gem-check

GemCheck: Writing Better Ruby Gems Checklist
CSS
95
star
20

pgrel

ActiveRecord extension for querying hstore and jsonb
Ruby
93
star
21

rbytes

Ruby Bytes helps you build, deploy and install Ruby and Rails application templates
Ruby
68
star
22

faqueue

Researching background jobs fairness
Ruby
66
star
23

downstream

Straightforward way to implement communication between Rails Engines using the Publish-Subscribe pattern.
Ruby
51
star
24

newgem

Custom script to generate new gems
Ruby
35
star
25

turbo-view-transitions

View Transitions API for Turbo
TypeScript
33
star
26

influx_udp

Erlang InfluxDB UDP writer
Erlang
31
star
27

ruby-dip

Docker-based development environment for hacking Ruby MRI
Dockerfile
30
star
28

erlgrpc

GRPC client for Erlang
Erlang
25
star
29

as3_p2plocal

as3 lib for local p2p connections (serverless rtmfp)
ActionScript
25
star
30

rails-intest-views

Generate view templates dynamically in Rails tests
Ruby
21
star
31

docsify-namespaced

Docsify plugin to work with namespaces
JavaScript
12
star
32

sharelatex-vagrant-ansible

Vagrant + Ansible configuration for ShareLatex
Shell
12
star
33

slog-spy

Slog handler to temporary deliver formatted verbose logs to an arbitrary target
Go
12
star
34

docs-example

Playground for dealing with documentation engines
7
star
35

ruby-russia-2020

RubyRussia 2020 "Frontendless Rails" workshop demo app
Ruby
6
star
36

engine-cable-app

Experimenting with Action Cable and engines
Ruby
6
star
37

rails-on-wasm-playground

A minimal Rails app to showcase Rails on Wasm features
Ruby
5
star
38

palkan

It's me
4
star
39

ruby-compatibility-examples

Collections of reproduction cases for TruffleRuby vs. MRI (in)compatibility
Ruby
3
star
40

erffmpeg

Erlang wrapper for some ffmpeg
C
3
star
41

th-dummy

TH Dummy
Ruby
2
star
42

ulitos

Erlang utils modules
Erlang
2
star
43

meetings

Good old Teachbase Meetings client
ActionScript
2
star
44

rebar_templates

Custom rebar templates
Erlang
1
star
45

macos-setup

Shell
1
star
46

bitrix-orm

Bitrix kinda ORM for IBlockElements and CUser.
PHP
1
star
47

anycable-elements

Web Components for AnyCable console
JavaScript
1
star
48

adventofcode2018

https://adventofcode.com
Rust
1
star
49

tb_utils

ActionScript 3 library
ActionScript
1
star