koine-event_manager 2.1.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfb4bada53604e54aa8118063adccfcc996e137c387ba967521a1af7c99828e1
4
- data.tar.gz: b043883d06200820bba474a3191db5d4f6816c67be5f9f02a3a2c7c7852740bc
3
+ metadata.gz: 48dfe1c350d523d6dc0b09cec81848a225fa41cf02edd07fbe3c902f4e9ba826
4
+ data.tar.gz: c5d594bac4dc7e8ed338d485e5e5a2fcd4558f35533cd496d4497783644a543f
5
5
  SHA512:
6
- metadata.gz: 1b996259bb8fc3c71633fb6a314d0fe433825d4bfeedd6b444db9b2e8f59c4a9e27c5b521b951ca6b1b76071f603f28b3a2b5c48a6b93210ba0de4a985f2c22f
7
- data.tar.gz: 3ad407c03c10a251c2dd2450095cc9714af03d194282075e98ba4bfae0610bb350004c057445142cbeeac78cfae305ff2f77d8a6d4f398b6b714f6b8ae6761a0
6
+ metadata.gz: a465c356d1944acb9cb2c2aea7f4f269877a253dce7fce66798cb9cd6560c80c196c0151f7fbba0c746e6816bbca3038ac31f6e0a1bc272d42696f52b6af7755
7
+ data.tar.gz: 87700e2f619c41c382cace295d90b3d76afba14c3af5174769117b78196796199ed6921faa4b6e97ece79bb1889589b6b48089d9dc811d71d82081347230fcd1
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master, update ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4']
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Ruby ${{ matrix.ruby-version }}
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: ${{ matrix.ruby-version }}
23
+ bundler-cache: true
24
+
25
+ - name: Run tests
26
+ run: bundle exec rspec
27
+
28
+ - name: Upload coverage to Coveralls
29
+ if: matrix.ruby-version == '3.4'
30
+ uses: coverallsapp/github-action@v2
31
+ with:
32
+ github-token: ${{ secrets.GITHUB_TOKEN }}
33
+ path-to-lcov: ./coverage/lcov.info
34
+ continue-on-error: true
35
+
36
+ rubocop:
37
+ runs-on: ubuntu-latest
38
+
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+
42
+ - name: Set up Ruby
43
+ uses: ruby/setup-ruby@v1
44
+ with:
45
+ ruby-version: '3.4'
46
+ bundler-cache: true
47
+
48
+ - name: Run RuboCop
49
+ run: bundle exec rubocop
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  .rspec_status
11
+ /.claude
data/.rubocop.yml CHANGED
@@ -1,13 +1,17 @@
1
- require: rubocop-rspec
2
-
3
1
  # https://github.com/rubocop-hq/rubocop/blob/36404b40cbdac743b596c8520877c4ca3eee5691/manual/cops.md
4
2
 
5
3
  AllCops:
6
- TargetRubyVersion: 2.3
4
+ TargetRubyVersion: 3.0
7
5
  Exclude:
8
6
  - config/environment.rb
9
7
  - apps/web/application.rb
10
- Layout/AlignParameters:
8
+ - vendor/**/*
9
+ - node_modules/**/*
10
+ - bin/**/*
11
+ NewCops: enable
12
+ SuggestExtensions: false
13
+
14
+ Layout/ParameterAlignment:
11
15
  Enabled: false
12
16
 
13
17
  Layout/ClosingParenthesisIndentation:
@@ -16,10 +20,10 @@ Layout/ClosingParenthesisIndentation:
16
20
  Layout/FirstParameterIndentation:
17
21
  Enabled: false
18
22
 
19
- Layout/IndentHash:
23
+ Layout/FirstHashElementIndentation:
20
24
  Enabled: false
21
25
 
22
- Layout/IndentArray:
26
+ Layout/FirstArrayElementIndentation:
23
27
  Enabled: false
24
28
 
25
29
  Layout/MultilineMethodCallIndentation:
@@ -39,7 +43,7 @@ Metrics/BlockLength:
39
43
  Exclude:
40
44
  - 'spec/**/*_spec.rb'
41
45
 
42
- Metrics/LineLength:
46
+ Layout/LineLength:
43
47
  Max: 100
44
48
 
45
49
  Style/CommentedKeyword:
@@ -59,23 +63,3 @@ Style/TrailingCommaInArrayLiteral:
59
63
 
60
64
  Style/TrailingCommaInHashLiteral:
61
65
  EnforcedStyleForMultiline: comma
62
-
63
- # rubocop-rspec cops
64
-
65
- Capybara/FeatureMethods:
66
- Enabled: false
67
-
68
- FactoryBot/CreateList:
69
- Enabled: false
70
-
71
- RSpec/ExampleLength:
72
- Enabled: false
73
-
74
- RSpec/MessageSpies:
75
- Enabled: false
76
-
77
- RSpec/MultipleExpectations:
78
- Enabled: false
79
-
80
- RSpec/NestedGroups:
81
- Enabled: false
data/Gemfile CHANGED
@@ -6,3 +6,13 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
7
  # Specify your gem's dependencies in koine-event_manager.gemspec
8
8
  gemspec
9
+
10
+ group :development, :test do
11
+ gem 'coveralls', require: false
12
+ gem 'rake', '~> 13.0'
13
+ gem 'rspec', '~> 3.13'
14
+ gem 'rubocop', '~> 1.0', require: false
15
+ gem 'rubocop-rake', '~> 0.6', require: false
16
+ gem 'rubocop-rspec', '~> 3.0', require: false
17
+ gem 'simplecov', require: false
18
+ end
data/README.md CHANGED
@@ -1,12 +1,25 @@
1
1
  # Koine::EventManager
2
2
 
3
- Event manager for ruby.
3
+ A simple and lightweight event management library for Ruby that enables event-driven architecture through a publish-subscribe pattern.
4
4
 
5
- [![Build Status](https://travis-ci.org/mjacobus/koine-event-manager.svg)](https://travis-ci.org/mjacobus/koine-event-manager)
5
+ [![CI](https://github.com/mjacobus/koine-event-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/mjacobus/koine-event-manager/actions/workflows/ci.yml)
6
6
  [![Coverage Status](https://coveralls.io/repos/github/mjacobus/koine-event-manager/badge.svg?branch=master)](https://coveralls.io/github/mjacobus/koine-event-manager?branch=master)
7
7
  [![Code Climate](https://codeclimate.com/github/mjacobus/koine-event-manager/badges/gpa.svg)](https://codeclimate.com/github/mjacobus/koine-event-manager)
8
8
  [![Gem Version](https://badge.fury.io/rb/koine-event_manager.svg)](https://badge.fury.io/rb/koine-event_manager)
9
9
 
10
+ ## Features
11
+
12
+ - **Simple API** - Easy to understand and use
13
+ - **Block-based listeners** - Quick inline event handlers
14
+ - **Object-based subscribers** - Reusable event handling objects
15
+ - **Event inheritance** - Listen to parent event classes and receive child events
16
+ - **No dependencies** - Pure Ruby implementation
17
+ - **Thread-safe operations** - Safe for concurrent use
18
+
19
+ ## Requirements
20
+
21
+ - Ruby >= 3.0
22
+
10
23
  ## Installation
11
24
 
12
25
  Add this line to your application's Gemfile:
@@ -17,7 +30,7 @@ gem 'koine-event_manager'
17
30
 
18
31
  And then execute:
19
32
 
20
- $ bundle
33
+ $ bundle install
21
34
 
22
35
  Or install it yourself as:
23
36
 
@@ -25,35 +38,64 @@ Or install it yourself as:
25
38
 
26
39
  ## Usage
27
40
 
28
- Here follows a simple example
41
+ ### Basic Example
29
42
 
30
43
  ```ruby
44
+ require 'koine/event_manager'
45
+
46
+ # Define your event
47
+ class UserSignedIn
48
+ attr_reader :user
49
+
50
+ def initialize(user)
51
+ @user = user
52
+ end
53
+ end
54
+
55
+ # Create the event manager
31
56
  event_manager = Koine::EventManager::EventManager.new
32
57
 
58
+ # Register a listener
33
59
  event_manager.listen_to(UserSignedIn) do |event|
60
+ puts "Welcome, #{event.user.name}!"
34
61
  WelcomeEmail.new(event.user).send
35
62
  end
63
+
64
+ # Trigger the event
65
+ user = User.find(123)
66
+ event_manager.trigger(UserSignedIn.new(user))
36
67
  ```
37
68
 
38
- In your controller, or service:
69
+ ### Block-based Listeners
70
+
71
+ Use block-based listeners for simple, inline event handlers:
39
72
 
40
73
  ```ruby
41
- class UserController < ApplicationController
42
- def create
43
- @user = User.create(user_params)
74
+ event_manager = Koine::EventManager::EventManager.new
44
75
 
45
- event_manager.trigger(UserSignedIn.new(@user))
76
+ event_manager.listen_to(UserSignedIn) do |event|
77
+ WelcomeEmail.new(event.user).send
78
+ end
46
79
 
47
- respond_with(@user)
48
- end
80
+ event_manager.listen_to(UserRemovedAccount) do |event|
81
+ CleanupJob.perform_later(event.user.id)
49
82
  end
83
+
84
+ # Trigger events
85
+ event_manager.trigger(UserSignedIn.new(user))
50
86
  ```
51
87
 
52
- You can also create event listeners:
88
+ **When to use:** Quick, one-off event handlers that don't need to be reused.
89
+
90
+ ### Event Listener Classes
91
+
92
+ For better organization, create reusable listener classes:
53
93
 
54
94
  ```ruby
55
95
  class UserListener < Koine::EventManager::EventListener
56
- def initializer
96
+ def initialize
97
+ super
98
+
57
99
  listen_to(UserSignedIn) do |event|
58
100
  WelcomeEmail.new(event.user).send
59
101
  end
@@ -63,53 +105,241 @@ class UserListener < Koine::EventManager::EventListener
63
105
  end
64
106
  end
65
107
  end
66
- ```
67
108
 
68
- And attach to the event manager:
69
-
70
- ```ruby
109
+ # Attach the listener to the event manager
71
110
  event_manager = Koine::EventManager::EventManager.new
72
111
  event_manager.attach_listener(UserListener.new)
73
112
 
74
- # and of course later, you can detach listeners, if you want
113
+ # Trigger events
114
+ event_manager.trigger(UserSignedIn.new(some_user))
115
+
116
+ # Later, you can detach listeners if needed
75
117
  event_manager.detach_listener(event_manager.listeners.last)
76
118
  ```
77
119
 
78
- And trigger the event on the event manager
79
-
80
- ```ruby
81
- event_manager.trigger(UserSignedIn.new(some_user))
82
- ```
120
+ **When to use:** Related event handlers that should be grouped together and potentially attached/detached as a unit.
83
121
 
84
122
  ### Subscribers
85
123
 
86
- You can add and remove subscribers as follows:
124
+ Subscribers are objects that implement a `publish` method. They provide a more object-oriented approach to event handling:
87
125
 
88
126
  ```ruby
89
- class MySubscriber
127
+ class NotificationSubscriber
90
128
  def publish(event)
91
- # do something
129
+ case event
130
+ when UserSignedIn
131
+ send_welcome_notification(event.user)
132
+ when UserRemovedAccount
133
+ send_goodbye_notification(event.user)
134
+ end
92
135
  end
93
- end
94
136
 
95
- subscriber = MySubscriber.new
137
+ private
138
+
139
+ def send_welcome_notification(user)
140
+ # Send notification logic
141
+ end
142
+
143
+ def send_goodbye_notification(user)
144
+ # Send notification logic
145
+ end
146
+ end
96
147
 
148
+ # Subscribe to specific events
149
+ subscriber = NotificationSubscriber.new
97
150
  event_manager.subscribe(subscriber, to: UserSignedIn)
151
+ event_manager.subscribe(subscriber, to: UserRemovedAccount)
152
+
153
+ # Trigger events - subscriber.publish(event) will be called
154
+ event_manager.trigger(UserSignedIn.new(user))
155
+
156
+ # Unsubscribe when no longer needed
98
157
  event_manager.unsubscribe(subscriber, from: UserSignedIn)
99
158
  ```
100
159
 
160
+ **When to use:** Complex event handling logic that needs to be encapsulated in a class with state and multiple methods.
161
+
162
+ ### Event Inheritance
163
+
164
+ The event manager supports event inheritance. If you listen to a parent event class, you'll also receive events from child classes:
165
+
166
+ ```ruby
167
+ class UserEvent
168
+ attr_reader :user
169
+
170
+ def initialize(user)
171
+ @user = user
172
+ end
173
+ end
174
+
175
+ class UserSignedIn < UserEvent
176
+ end
177
+
178
+ class UserSignedOut < UserEvent
179
+ end
180
+
181
+ # Listen to the parent class
182
+ event_manager.listen_to(UserEvent) do |event|
183
+ puts "User event occurred for #{event.user.name}"
184
+ end
185
+
186
+ # Both of these will trigger the listener above
187
+ event_manager.trigger(UserSignedIn.new(user))
188
+ event_manager.trigger(UserSignedOut.new(user))
189
+ ```
190
+
191
+ ### Using in Rails
192
+
193
+ Here's a complete example of how to use the event manager in a Rails application:
194
+
195
+ ```ruby
196
+ # app/events/user_signed_in.rb
197
+ class UserSignedIn
198
+ attr_reader :user, :ip_address
199
+
200
+ def initialize(user, ip_address: nil)
201
+ @user = user
202
+ @ip_address = ip_address
203
+ end
204
+ end
205
+
206
+ # app/listeners/user_listener.rb
207
+ class UserListener < Koine::EventManager::EventListener
208
+ def initialize
209
+ super
210
+
211
+ listen_to(UserSignedIn) do |event|
212
+ WelcomeMailer.welcome_email(event.user).deliver_later
213
+ TrackingService.track_login(event.user, event.ip_address)
214
+ end
215
+ end
216
+ end
217
+
218
+ # config/initializers/event_manager.rb
219
+ Rails.application.config.event_manager = Koine::EventManager::EventManager.new
220
+ Rails.application.config.event_manager.attach_listener(UserListener.new)
221
+
222
+ # app/controllers/application_controller.rb
223
+ class ApplicationController < ActionController::Base
224
+ def event_manager
225
+ Rails.application.config.event_manager
226
+ end
227
+ end
228
+
229
+ # app/controllers/sessions_controller.rb
230
+ class SessionsController < ApplicationController
231
+ def create
232
+ @user = User.find_by(email: params[:email])
233
+
234
+ if @user&.authenticate(params[:password])
235
+ session[:user_id] = @user.id
236
+ event_manager.trigger(UserSignedIn.new(@user, ip_address: request.remote_ip))
237
+ redirect_to root_path, notice: 'Signed in successfully'
238
+ else
239
+ render :new, alert: 'Invalid credentials'
240
+ end
241
+ end
242
+ end
243
+ ```
244
+
245
+ ### Error Isolation
246
+
247
+ By default, an exception raised by a listener or subscriber propagates and aborts
248
+ the dispatch. Pass an `on_error` callback to isolate failures so the remaining
249
+ listeners still run:
250
+
251
+ ```ruby
252
+ event_manager = Koine::EventManager::EventManager.new(
253
+ on_error: ->(error, event:, listener:) { Bugsnag.notify(error) }
254
+ )
255
+ ```
256
+
257
+ The callback is invoked as `on_error.call(error, event:, listener:)`, so it **must
258
+ accept the `event:` and `listener:` keyword arguments** (the failing event and the
259
+ listener/subscriber that raised). Use them to attribute the failure, or ignore them
260
+ with a splat:
261
+
262
+ ```ruby
263
+ on_error: ->(error, **) { Bugsnag.notify(error) }
264
+ ```
265
+
266
+ Without `on_error` the behavior is unchanged (errors re-raise).
267
+
268
+ ### Post-commit Dispatch (Rails)
269
+
270
+ `require "koine/event_manager/rails"` adds `TransactionalEventManager`, a drop-in
271
+ subclass that defers dispatch until the current database transaction commits (or
272
+ runs immediately when none is open), via `ActiveRecord.after_all_transactions_commit`.
273
+ This keeps listeners from reacting to changes that a rollback would undo.
274
+
275
+ ```ruby
276
+ require "koine/event_manager/rails"
277
+
278
+ bus = Koine::EventManager::TransactionalEventManager.new(
279
+ on_error: ->(error, **) { Bugsnag.notify(error) }
280
+ )
281
+ ```
282
+
283
+ Requires ActiveRecord >= 7.2; the core library remains dependency-free, so only load
284
+ this file from within a Rails/ActiveRecord process.
285
+
286
+ Subscribers (and block listeners) are deferred by default. A reaction that must run
287
+ **inside** the transaction — e.g. to write a related record atomically, or to raise
288
+ and roll the whole operation back — can opt out with `after_commit: false`:
289
+
290
+ ```ruby
291
+ bus.subscribe(AuditWriter.new, to: ProposalCreated) # post-commit, isolated
292
+ bus.subscribe(InventoryReserver.new, to: ProposalCreated, after_commit: false) # in-transaction
293
+ bus.listen_to(ProposalCreated, after_commit: false) { |e| ... } # in-transaction block
294
+ ```
295
+
296
+ Synchronous (`after_commit: false`) listeners run inline and their errors **propagate**
297
+ (bypassing `on_error`), so a failed integrity reaction aborts the surrounding
298
+ transaction. Deferred listeners stay isolated by `on_error`.
299
+
300
+ ## API Reference
301
+
302
+ ### EventManager
303
+
304
+ - `EventManager.new(on_error: nil)` - Optionally isolate listener errors (see Error Isolation)
305
+ - `listen_to(event_class, &block)` - Register a block to handle events
306
+ - `trigger(event)` - Dispatch an event to all listeners and subscribers
307
+ - `publish(event)` - Alias for `trigger` (pub/sub vocabulary)
308
+ - `subscribe(subscriber, to: event_type)` - Add a subscriber for an event type
309
+ - `unsubscribe(subscriber, from: event_type)` - Remove a subscriber
310
+ - `attach_listener(listener)` - Attach an EventListener instance
311
+ - `detach_listener(listener)` - Remove a listener
312
+ - `listeners` - Get array of attached listeners
313
+
314
+ ### EventListener
315
+
316
+ - `listen_to(event_type, &block)` - Register a block handler for an event type
317
+ - `subscribe(subscriber, to: event_type)` - Register a subscriber object
318
+ - `unsubscribe(subscriber, from: event_type)` - Unregister a subscriber
319
+ - `trigger(event_object)` - Process the event through all listeners and subscribers
320
+
101
321
  ## Development
102
322
 
103
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
323
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
104
324
 
105
325
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
106
326
 
107
- ## Contributing
327
+ ### Running Tests
108
328
 
109
- Bug reports and pull requests are welcome on GitHub at https://github.com/mjacobus/koine-event-manager. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
329
+ ```bash
330
+ bundle exec rspec
331
+ ```
332
+
333
+ ### Running Linter
110
334
 
335
+ ```bash
336
+ bundle exec rubocop
337
+ ```
338
+
339
+ ## Contributing
340
+
341
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mjacobus/koine-event-manager. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
111
342
 
112
343
  ## License
113
344
 
114
345
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
115
-
data/Rakefile CHANGED
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
4
5
 
5
- # rubocop:disable Lint/HandleExceptions
6
- begin
7
- require 'rspec/core/rake_task'
8
- RSpec::Core::RakeTask.new(:spec)
9
- task default: :spec
10
- rescue LoadError
11
- end
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -4,7 +4,6 @@ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'koine/event_manager/version'
6
6
 
7
- # rubocop:disable Metrics/BlockLength
8
7
  Gem::Specification.new do |spec|
9
8
  spec.name = 'koine-event_manager'
10
9
  spec.version = Koine::EventManager::VERSION
@@ -15,11 +14,13 @@ Gem::Specification.new do |spec|
15
14
  spec.description = 'Event Manager for ruby'
16
15
  spec.homepage = 'https://github.com/mjacobus/koine-event-manager'
17
16
  spec.license = 'MIT'
17
+ spec.required_ruby_version = '>= 3.0'
18
18
 
19
19
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
20
20
  # to allow pushing to a single host or delete this section to allow pushing to any host.
21
21
  if spec.respond_to?(:metadata)
22
22
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
24
  else
24
25
  raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
25
26
  end
@@ -32,12 +33,4 @@ Gem::Specification.new do |spec|
32
33
  spec.bindir = 'exe'
33
34
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
35
  spec.require_paths = ['lib']
35
-
36
- spec.add_development_dependency 'bundler', '~> 1.16'
37
- spec.add_development_dependency 'coveralls'
38
- spec.add_development_dependency 'rake', '~> 10.0'
39
- spec.add_development_dependency 'rspec', '~> 3.0'
40
- spec.add_development_dependency 'rubocop', '0.58'
41
- spec.add_development_dependency 'rubocop-rspec', '~>1.30'
42
- spec.add_development_dependency 'simplecov'
43
36
  end
@@ -3,13 +3,15 @@
3
3
  module Koine
4
4
  module EventManager
5
5
  class EventListener
6
- def initialize
6
+ def initialize(on_error: nil)
7
7
  @listeners = {}
8
8
  @subscribers = {}
9
+ @on_error = on_error
9
10
  end
10
11
 
11
12
  def listen_to(event_type, &block)
12
13
  raise ArgumentError, 'block not given' unless block_given?
14
+
13
15
  add_listener(event_type, &block)
14
16
  end
15
17
 
@@ -29,13 +31,13 @@ module Koine
29
31
 
30
32
  def trigger(event_object)
31
33
  listeners_for(event_object.class).each do |block|
32
- block.call(event_object)
34
+ dispatch(event_object, block) { block.call(event_object) }
33
35
  end
34
36
 
35
37
  subscribers.each do |subscriber, events|
36
38
  events.each do |event|
37
39
  if event_object.class.ancestors.map(&:to_s).include?(event.to_s)
38
- subscriber.publish(event_object)
40
+ dispatch(event_object, subscriber) { subscriber.publish(event_object) }
39
41
  end
40
42
  end
41
43
  end
@@ -49,13 +51,20 @@ module Koine
49
51
 
50
52
  private
51
53
 
54
+ def dispatch(event, listener)
55
+ yield
56
+ rescue StandardError => e
57
+ raise if @on_error.nil?
58
+
59
+ @on_error.call(e, event: event, listener: listener)
60
+ end
61
+
52
62
  def add_listener(event_type, &block)
53
63
  listeners[event_type.to_s] ||= []
54
64
  listeners[event_type.to_s] << block
55
65
  end
56
66
 
57
- attr_reader :listeners
58
- attr_reader :subscribers
67
+ attr_reader :listeners, :subscribers
59
68
  end
60
69
  end
61
70
  end
@@ -3,8 +3,13 @@
3
3
  module Koine
4
4
  module EventManager
5
5
  class EventManager
6
- def initialize
7
- @internal_listener = EventListener.new
6
+ def initialize(on_error: nil)
7
+ @on_error = on_error
8
+ @internal_listener = EventListener.new(on_error: on_error)
9
+ end
10
+
11
+ def publish(event)
12
+ trigger(event)
8
13
  end
9
14
 
10
15
  def listen_to(event, &block)
@@ -31,13 +36,23 @@ module Koine
31
36
  @internal_listener.trigger(event)
32
37
 
33
38
  listeners.each do |listener|
34
- listener.trigger(event)
39
+ dispatch(event, listener) { listener.trigger(event) }
35
40
  end
36
41
  end
37
42
 
38
43
  def listeners
39
44
  @listeners ||= []
40
45
  end
46
+
47
+ private
48
+
49
+ def dispatch(event, listener)
50
+ yield
51
+ rescue StandardError => e
52
+ raise if @on_error.nil?
53
+
54
+ @on_error.call(e, event: event, listener: listener)
55
+ end
41
56
  end
42
57
  end
43
58
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'koine/event_manager'
4
+
5
+ module Koine
6
+ module EventManager
7
+ # Dispatches listeners after the current DB transaction commits (or runs
8
+ # immediately when none is open), so they never react to changes a rollback
9
+ # would undo. Requires ActiveRecord >= 7.2.
10
+ #
11
+ # Individual subscribers/listeners may opt into synchronous, in-transaction
12
+ # dispatch with `after_commit: false`. Those run inline and their errors
13
+ # propagate (bypassing `on_error` isolation), so an integrity reaction can
14
+ # abort the surrounding transaction. Post-commit listeners stay isolated.
15
+ class TransactionalEventManager < EventManager
16
+ alias deferred_dispatch trigger
17
+
18
+ def initialize(on_error: nil)
19
+ super
20
+ # In-transaction listeners: no on_error isolation, so a failure propagates
21
+ # and can roll the surrounding transaction back.
22
+ @synchronous = EventListener.new
23
+ end
24
+
25
+ def subscribe(subscriber, to:, after_commit: true)
26
+ return @synchronous.subscribe(subscriber, to: to) unless after_commit
27
+
28
+ super(subscriber, to: to)
29
+ end
30
+
31
+ def listen_to(event, after_commit: true, &block)
32
+ return @synchronous.listen_to(event, &block) unless after_commit
33
+
34
+ super(event, &block)
35
+ end
36
+
37
+ def unsubscribe(subscriber, from:)
38
+ @synchronous.unsubscribe(subscriber, from: from)
39
+ super
40
+ end
41
+
42
+ def trigger(event)
43
+ @synchronous.trigger(event)
44
+
45
+ unless active_record_available?
46
+ raise LoadError, 'TransactionalEventManager requires ActiveRecord >= 7.2 ' \
47
+ '(after_all_transactions_commit). Load Rails before using it.'
48
+ end
49
+
50
+ ::ActiveRecord.after_all_transactions_commit do
51
+ deferred_dispatch(event)
52
+ end
53
+ end
54
+
55
+ private :deferred_dispatch
56
+
57
+ private
58
+
59
+ def active_record_available?
60
+ defined?(::ActiveRecord) &&
61
+ ::ActiveRecord.respond_to?(:after_all_transactions_commit)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Koine
4
4
  module EventManager
5
- VERSION = '2.1.1'
5
+ VERSION = '3.1.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,113 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: koine-event_manager
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcelo Jacobus
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2018-11-26 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.16'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.16'
27
- - !ruby/object:Gem::Dependency
28
- name: coveralls
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '10.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '10.0'
55
- - !ruby/object:Gem::Dependency
56
- name: rspec
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '3.0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '3.0'
69
- - !ruby/object:Gem::Dependency
70
- name: rubocop
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - '='
74
- - !ruby/object:Gem::Version
75
- version: '0.58'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - '='
81
- - !ruby/object:Gem::Version
82
- version: '0.58'
83
- - !ruby/object:Gem::Dependency
84
- name: rubocop-rspec
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '1.30'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '1.30'
97
- - !ruby/object:Gem::Dependency
98
- name: simplecov
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
111
12
  description: Event Manager for ruby
112
13
  email:
113
14
  - marcelo.jacobus@gmail.com
@@ -115,11 +16,11 @@ executables: []
115
16
  extensions: []
116
17
  extra_rdoc_files: []
117
18
  files:
19
+ - ".github/workflows/ci.yml"
118
20
  - ".gitignore"
119
21
  - ".rspec"
120
22
  - ".rubocop.yml"
121
23
  - ".scrutinizer.yml"
122
- - ".travis.yml"
123
24
  - CODE_OF_CONDUCT.md
124
25
  - Gemfile
125
26
  - LICENSE.txt
@@ -131,13 +32,14 @@ files:
131
32
  - lib/koine/event_manager.rb
132
33
  - lib/koine/event_manager/event_listener.rb
133
34
  - lib/koine/event_manager/event_manager.rb
35
+ - lib/koine/event_manager/rails.rb
134
36
  - lib/koine/event_manager/version.rb
135
37
  homepage: https://github.com/mjacobus/koine-event-manager
136
38
  licenses:
137
39
  - MIT
138
40
  metadata:
139
41
  allowed_push_host: https://rubygems.org
140
- post_install_message:
42
+ rubygems_mfa_required: 'true'
141
43
  rdoc_options: []
142
44
  require_paths:
143
45
  - lib
@@ -145,16 +47,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
145
47
  requirements:
146
48
  - - ">="
147
49
  - !ruby/object:Gem::Version
148
- version: '0'
50
+ version: '3.0'
149
51
  required_rubygems_version: !ruby/object:Gem::Requirement
150
52
  requirements:
151
53
  - - ">="
152
54
  - !ruby/object:Gem::Version
153
55
  version: '0'
154
56
  requirements: []
155
- rubyforge_project:
156
- rubygems_version: 2.7.6
157
- signing_key:
57
+ rubygems_version: 3.6.7
158
58
  specification_version: 4
159
59
  summary: Event Manager for ruby
160
60
  test_files: []
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- language: ruby
2
- cache: bundle
3
- script:
4
- - COVERALLS=1 bundle exec rake
5
- rvm:
6
- - 2.4.4
7
- before_install: gem install bundler -v 1.16.3