nti_event_bus 0.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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +102 -0
- data/Rakefile +12 -0
- data/lib/nti_event_bus/configuration.rb +35 -0
- data/lib/nti_event_bus/dispatchers.rb +21 -0
- data/lib/nti_event_bus/dsl.rb +62 -0
- data/lib/nti_event_bus/handle_event_job.rb +21 -0
- data/lib/nti_event_bus/handler_base.rb +46 -0
- data/lib/nti_event_bus/railtie.rb +21 -0
- data/lib/nti_event_bus/registry.rb +30 -0
- data/lib/nti_event_bus/version.rb +5 -0
- data/lib/nti_event_bus.rb +78 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 20a81903201667f24f670449fdcd72d0c5dc7e2ff317fc992a32d129c585eb60
|
|
4
|
+
data.tar.gz: 91b184e622a256994b980f6ed0fae575768144ee195b8dcf97ab1477dc86fa60
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1f711d6690c639c15e71cab839ec930ef5d0e86e01e290f537757386ead3dd330ee88e91826f8ea9b54788c311b146c0de296f3f55271823369a510dfd8440a3
|
|
7
|
+
data.tar.gz: 2e50543c96df318dc59523ee3f03a97bfcb6eea141d26ec919988428ce6c4f4fa91b746032bb55d7393ee728efd50b961f380f76d1ec18c750946083ca114442
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-07-07
|
|
4
|
+
|
|
5
|
+
- Initial release. Extracted from the Tindahang Tapat OnPrem application.
|
|
6
|
+
- `NtiEventBus.publish` fans a `domain.action` event out to subscribed handler classes.
|
|
7
|
+
- `NtiEventBus::DSL` (`draw` / `on_event` / `perform`) declares subscriptions loaded from event files.
|
|
8
|
+
- `NtiEventBus::Registry` — deep-frozen after `finalize!` for lock-free reads.
|
|
9
|
+
- `NtiEventBus::HandlerBase` — base class for handlers (indifferent-access frozen payload, `require_keys`).
|
|
10
|
+
- `NtiEventBus::HandleEventJob` — ActiveJob-backed asynchronous dispatch on a configurable queue.
|
|
11
|
+
- `NtiEventBus::Dispatchers::ActiveJob` (default) and `NtiEventBus::Dispatchers::Inline`.
|
|
12
|
+
- `NtiEventBus::Railtie` — sets event-file path defaults and rebuilds the registry on `to_prepare`.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nueca Technologies Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# NTI Event Bus
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nueca-tech/nti_event_bus/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
A small, production-grade **publish/subscribe event bus** for Ruby & Rails.
|
|
6
|
+
|
|
7
|
+
Declare `domain.action` subscriptions in a compact DSL, `publish` events, and fan them out to handler classes **asynchronously** via ActiveJob. The subscription registry is built once at boot and **deep-frozen**, so publishing is a lock-free hash lookup with no per-call allocation and no long-lived, unbounded state — no thread pools, no notification subscribers, nothing to leak.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
publisher → NtiEventBus.publish("order.created", payload)
|
|
11
|
+
→ registry.handlers_for("order.created") # frozen O(1) lookup
|
|
12
|
+
→ dispatcher.call(handler_name, payload)
|
|
13
|
+
→ NtiEventBus::HandleEventJob (ActiveJob, async)
|
|
14
|
+
→ Handler.new(payload).call
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'nti_event_bus'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Rails usage
|
|
24
|
+
|
|
25
|
+
On Rails the bundled `NtiEventBus::Railtie` wires everything up automatically:
|
|
26
|
+
|
|
27
|
+
- Defaults `configuration.root_events_file` to `config/events.rb` and `configuration.events_dir` to
|
|
28
|
+
`config/events/`.
|
|
29
|
+
- Rebuilds the registry on every `to_prepare` (once in production; per code-reload in development, so edits to event files are picked up and old handler references are released).
|
|
30
|
+
|
|
31
|
+
Define the root events file and one or more drawn files:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# config/events.rb
|
|
35
|
+
draw :internal_events
|
|
36
|
+
|
|
37
|
+
# config/events/internal_events.rb
|
|
38
|
+
on_event 'order.created' do
|
|
39
|
+
perform Orders::Handlers::Created
|
|
40
|
+
perform Notifications::Handlers::OrderCreated
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Publish from anywhere:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
NtiEventBus.publish('order.created', { order_id: order.id })
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Write handlers by subclassing `HandlerBase`:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
module Orders::Handlers
|
|
54
|
+
class Created < NtiEventBus::HandlerBase
|
|
55
|
+
def call
|
|
56
|
+
order_id = payload.fetch(:order_id) # payload is an indifferent-access, frozen hash
|
|
57
|
+
# ...
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
NtiEventBus.configure do |config|
|
|
67
|
+
config.root_events_file = Rails.root.join('config/events.rb').to_s # defaulted by the Railtie
|
|
68
|
+
config.events_dir = Rails.root.join('config/events').to_s # defaulted by the Railtie
|
|
69
|
+
config.queue_name = :events # ActiveJob queue (default)
|
|
70
|
+
config.dispatcher = NtiEventBus::Dispatchers::ActiveJob.new # default; async
|
|
71
|
+
config.logger = Rails.logger # optional
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Dispatchers
|
|
76
|
+
|
|
77
|
+
- `NtiEventBus::Dispatchers::ActiveJob` (default) — enqueues `HandleEventJob` on `config.queue_name`.
|
|
78
|
+
- `NtiEventBus::Dispatchers::Inline` — runs the handler synchronously in-process (tests / non-ActiveJob hosts).
|
|
79
|
+
- Any object responding to `#call(handler_name, payload)` may be used.
|
|
80
|
+
|
|
81
|
+
## Non-Rails usage
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
NtiEventBus.configure do |config|
|
|
85
|
+
config.root_events_file = '/path/to/events.rb'
|
|
86
|
+
config.events_dir = '/path/to/events'
|
|
87
|
+
end
|
|
88
|
+
NtiEventBus.setup! # build the registry
|
|
89
|
+
NtiEventBus.publish('order.created', { order_id: 1 })
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Thread-safety & reloads
|
|
93
|
+
|
|
94
|
+
`setup!` builds a fresh `Registry`, deep-freezes it, and atomically swaps it in under a mutex. Readers always see a fully-frozen registry (the previous one or the new one), so `publish` never locks and never observes a half-built registry.
|
|
95
|
+
|
|
96
|
+
## Event naming
|
|
97
|
+
|
|
98
|
+
Event names must be non-empty strings in `domain.action` form (they must contain a `.`). This is enforced at subscription time by the DSL.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NtiEventBus
|
|
4
|
+
# Runtime configuration. On Rails the Railtie fills in path defaults; hosts may override any of it.
|
|
5
|
+
class Configuration
|
|
6
|
+
# Absolute path to the root events file that is evaluated by `setup!` (e.g. config/events.rb).
|
|
7
|
+
attr_accessor :root_events_file
|
|
8
|
+
# Absolute path to the directory that `draw` resolves nested event files from (e.g. config/events).
|
|
9
|
+
attr_accessor :events_dir
|
|
10
|
+
# ActiveJob queue used by the default dispatcher.
|
|
11
|
+
attr_accessor :queue_name
|
|
12
|
+
# Optional logger; when set, receives debug lines from the bus.
|
|
13
|
+
attr_accessor :logger
|
|
14
|
+
|
|
15
|
+
attr_writer :dispatcher
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@queue_name = :events
|
|
19
|
+
@root_events_file = nil
|
|
20
|
+
@events_dir = nil
|
|
21
|
+
@logger = nil
|
|
22
|
+
@dispatcher = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Object responding to `#call(handler_name, payload)`. Defaults to asynchronous ActiveJob dispatch.
|
|
26
|
+
def dispatcher
|
|
27
|
+
@dispatcher ||= Dispatchers::ActiveJob.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def root_events_file!
|
|
31
|
+
root_events_file ||
|
|
32
|
+
raise(NotConfiguredError, 'NtiEventBus.configuration.root_events_file must be set')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NtiEventBus
|
|
4
|
+
# Strategies for turning a resolved (handler_name, payload) pair into actual handler execution.
|
|
5
|
+
# A dispatcher is any object responding to `#call(handler_name, payload)`.
|
|
6
|
+
module Dispatchers
|
|
7
|
+
# Default. Enqueues handler execution asynchronously via ActiveJob (Solid Queue, Sidekiq, ...).
|
|
8
|
+
class ActiveJob
|
|
9
|
+
def call(handler_name, payload)
|
|
10
|
+
NtiEventBus::HandleEventJob.perform_later(handler_name, payload)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Runs the handler synchronously, in-process. Useful for tests or hosts without a job backend.
|
|
15
|
+
class Inline
|
|
16
|
+
def call(handler_name, payload)
|
|
17
|
+
NtiEventBus::HandleEventJob.perform_now(handler_name, payload)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NtiEventBus
|
|
4
|
+
# Evaluation context for event-definition files.
|
|
5
|
+
#
|
|
6
|
+
# A root file is `instance_eval`ed against a DSL instance; it may `draw` additional files from the
|
|
7
|
+
# configured `events_dir`, and declares subscriptions with `on_event` / `perform`:
|
|
8
|
+
#
|
|
9
|
+
# on_event 'order.created' do
|
|
10
|
+
# perform Orders::Handlers::Created
|
|
11
|
+
# end
|
|
12
|
+
class DSL
|
|
13
|
+
def initialize(registry, events_dir)
|
|
14
|
+
@registry = registry
|
|
15
|
+
@events_dir = events_dir
|
|
16
|
+
@current_event_name = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_event(event_name, &block)
|
|
20
|
+
raise ArgumentError, 'Nested `on_event` not allowed' if @current_event_name
|
|
21
|
+
raise ArgumentError, 'Block required for `on_event`' unless block
|
|
22
|
+
|
|
23
|
+
event_name = normalize_event!(event_name)
|
|
24
|
+
|
|
25
|
+
@current_event_name = event_name
|
|
26
|
+
instance_eval(&block)
|
|
27
|
+
ensure
|
|
28
|
+
@current_event_name = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def perform(handlers)
|
|
32
|
+
raise ArgumentError, 'perform must be inside `on_event`' unless @current_event_name
|
|
33
|
+
|
|
34
|
+
Array(handlers).each do |handler|
|
|
35
|
+
@registry.add(@current_event_name, handler)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def draw(name)
|
|
40
|
+
raise NotConfiguredError, 'NtiEventBus.configuration.events_dir must be set' unless @events_dir
|
|
41
|
+
|
|
42
|
+
path = File.join(@events_dir.to_s, "#{name}.rb")
|
|
43
|
+
raise LoadError, "Event file not found: #{path}" unless File.exist?(path)
|
|
44
|
+
|
|
45
|
+
instance_eval(File.read(path), path.to_s)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def normalize_event!(event_name)
|
|
51
|
+
raise ArgumentError, 'event_name must be a String' unless event_name.is_a?(String)
|
|
52
|
+
|
|
53
|
+
event_name = event_name.strip
|
|
54
|
+
raise ArgumentError, 'event_name cannot be empty' if event_name.empty?
|
|
55
|
+
|
|
56
|
+
# enforce naming convention: domain.action
|
|
57
|
+
raise ArgumentError, "event_name must follow 'domain.action' format" unless event_name.include?('.')
|
|
58
|
+
|
|
59
|
+
event_name.freeze
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_job'
|
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
|
5
|
+
|
|
6
|
+
module NtiEventBus
|
|
7
|
+
# ActiveJob that runs a single handler for a published event.
|
|
8
|
+
#
|
|
9
|
+
# The handler is passed by name (a String) rather than as a class so the argument serializes
|
|
10
|
+
# cleanly and is re-resolved to the current class version at run time (reload-safe). The queue is
|
|
11
|
+
# read from configuration at enqueue time so hosts can route event work to a dedicated queue.
|
|
12
|
+
class HandleEventJob < ::ActiveJob::Base
|
|
13
|
+
queue_as { NtiEventBus.configuration.queue_name }
|
|
14
|
+
|
|
15
|
+
def perform(handler_name, payload)
|
|
16
|
+
handler = handler_name.constantize
|
|
17
|
+
|
|
18
|
+
handler.new(payload).call
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
|
4
|
+
|
|
5
|
+
module NtiEventBus
|
|
6
|
+
# Base class for all event handlers.
|
|
7
|
+
#
|
|
8
|
+
# A handler lives inside the *subscribing* module and is responsible for:
|
|
9
|
+
# 1. Extracting what it needs from the raw event payload
|
|
10
|
+
# 2. Doing any lookups / transformations
|
|
11
|
+
# 3. Calling the module's Main interactor (or service) with normalized params
|
|
12
|
+
#
|
|
13
|
+
# Subclasses must implement #call.
|
|
14
|
+
#
|
|
15
|
+
# Example:
|
|
16
|
+
# class CreateAuditLog::Handlers::OrderVoided < NtiEventBus::HandlerBase
|
|
17
|
+
# def call
|
|
18
|
+
# CreateAuditLog::Main.call(
|
|
19
|
+
# cashier_id: payload.fetch(:cashier_id),
|
|
20
|
+
# ...
|
|
21
|
+
# )
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
class HandlerBase
|
|
25
|
+
attr_reader :payload
|
|
26
|
+
|
|
27
|
+
def initialize(payload)
|
|
28
|
+
@payload = payload.with_indifferent_access.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call
|
|
32
|
+
raise NotImplementedError, "#{self.class}#call must be implemented"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Convenience — fetch multiple required keys at once.
|
|
38
|
+
# Raises KeyError with a clear message if any are missing.
|
|
39
|
+
#
|
|
40
|
+
# cashier_id, order_id = require_keys(:cashier_id, :order_id)
|
|
41
|
+
#
|
|
42
|
+
def require_keys(*keys)
|
|
43
|
+
keys.map { |key| payload.fetch(key) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module NtiEventBus
|
|
6
|
+
# Rails integration: sets sensible event-file path defaults and (re)builds the registry whenever the
|
|
7
|
+
# framework prepares the app. `to_prepare` runs once during boot in production and on every code
|
|
8
|
+
# reload in development, so event-file edits are picked up and stale handler references are released.
|
|
9
|
+
class Railtie < ::Rails::Railtie
|
|
10
|
+
initializer 'nti_event_bus.set_defaults' do
|
|
11
|
+
NtiEventBus.configure do |config|
|
|
12
|
+
config.root_events_file ||= Rails.root.join('config/events.rb').to_s
|
|
13
|
+
config.events_dir ||= Rails.root.join('config/events').to_s
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
config.to_prepare do
|
|
18
|
+
NtiEventBus.setup!
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NtiEventBus
|
|
4
|
+
# Maps event names to the ordered, de-duplicated list of handler classes subscribed to them.
|
|
5
|
+
#
|
|
6
|
+
# Built once during `NtiEventBus.setup!` and then deep-frozen via `#finalize!`, which makes reads
|
|
7
|
+
# (`#handlers_for`) lock-free and allocation-free on the hot path.
|
|
8
|
+
class Registry
|
|
9
|
+
EMPTY = [].freeze
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@handlers = Hash.new { |hash, key| hash[key] = [] }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add(event_name, handler)
|
|
16
|
+
list = @handlers[event_name]
|
|
17
|
+
list << handler unless list.include?(handler)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def finalize!
|
|
21
|
+
@handlers.each_value(&:freeze)
|
|
22
|
+
@handlers.freeze
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def handlers_for(event_name)
|
|
27
|
+
@handlers.fetch(event_name, EMPTY)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
|
5
|
+
|
|
6
|
+
require_relative 'nti_event_bus/version'
|
|
7
|
+
require_relative 'nti_event_bus/configuration'
|
|
8
|
+
require_relative 'nti_event_bus/registry'
|
|
9
|
+
require_relative 'nti_event_bus/dsl'
|
|
10
|
+
require_relative 'nti_event_bus/handler_base'
|
|
11
|
+
require_relative 'nti_event_bus/dispatchers'
|
|
12
|
+
require_relative 'nti_event_bus/handle_event_job'
|
|
13
|
+
require_relative 'nti_event_bus/railtie' if defined?(Rails::Railtie)
|
|
14
|
+
|
|
15
|
+
# Publish/subscribe event bus.
|
|
16
|
+
#
|
|
17
|
+
# NtiEventBus.publish('order.created', { order_id: 1 })
|
|
18
|
+
#
|
|
19
|
+
# Handlers are declared in event files (see NtiEventBus::DSL) and executed asynchronously via a
|
|
20
|
+
# configurable dispatcher (see NtiEventBus::Dispatchers). The subscription registry is built once by
|
|
21
|
+
# `setup!`, deep-frozen, and read lock-free on the publish path.
|
|
22
|
+
module NtiEventBus
|
|
23
|
+
class Error < StandardError; end
|
|
24
|
+
|
|
25
|
+
# Raised when the bus is used before `setup!` has run or when required configuration is missing.
|
|
26
|
+
class NotConfiguredError < Error; end
|
|
27
|
+
|
|
28
|
+
# Guards registry (re)builds; reads of the frozen registry never take this lock.
|
|
29
|
+
SETUP_MUTEX = Mutex.new
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def configure
|
|
33
|
+
yield(configuration) if block_given?
|
|
34
|
+
configuration
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def configuration
|
|
38
|
+
@configuration ||= Configuration.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Resets configuration to defaults. Intended for test suites.
|
|
42
|
+
def reset_configuration!
|
|
43
|
+
@configuration = Configuration.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Build the subscription registry from the configured event files and atomically install it.
|
|
47
|
+
#
|
|
48
|
+
# A fresh Registry is built, deep-frozen, then swapped in under SETUP_MUTEX. Readers always see a
|
|
49
|
+
# fully-frozen registry (the previous one or the new one) — never a half-built one.
|
|
50
|
+
def setup!
|
|
51
|
+
SETUP_MUTEX.synchronize do
|
|
52
|
+
registry = Registry.new
|
|
53
|
+
path = configuration.root_events_file!
|
|
54
|
+
DSL.new(registry, configuration.events_dir).instance_eval(File.read(path), path.to_s)
|
|
55
|
+
registry.finalize!
|
|
56
|
+
|
|
57
|
+
configuration.logger&.debug { "[NtiEventBus] registry built from #{path}" }
|
|
58
|
+
@registry = registry
|
|
59
|
+
end
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def registry
|
|
64
|
+
@registry || raise(NotConfiguredError, 'NtiEventBus.setup! has not been run')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Fan an event out to every subscribed handler via the configured dispatcher.
|
|
68
|
+
# Returns nil. Unknown events (no handlers) are a no-op.
|
|
69
|
+
def publish(event_name, payload)
|
|
70
|
+
handlers = registry.handlers_for(event_name)
|
|
71
|
+
return if handlers.empty?
|
|
72
|
+
|
|
73
|
+
dispatcher = configuration.dispatcher
|
|
74
|
+
handlers.each { |handler| dispatcher.call(handler.name, payload) }
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nti_event_bus
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Den Meralpis
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activejob
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activesupport
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
description: NtiEventBus is a lightweight publish/subscribe event bus. Declare `domain.action`
|
|
41
|
+
subscriptions in a small DSL, publish events, and fan them out to handler classes
|
|
42
|
+
asynchronously via ActiveJob. The subscription registry is built once and deep-frozen
|
|
43
|
+
for lock-free reads, so publishing adds no locking and no unbounded state.
|
|
44
|
+
email:
|
|
45
|
+
- denmark@nueca.com.ph
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE.txt
|
|
52
|
+
- README.md
|
|
53
|
+
- Rakefile
|
|
54
|
+
- lib/nti_event_bus.rb
|
|
55
|
+
- lib/nti_event_bus/configuration.rb
|
|
56
|
+
- lib/nti_event_bus/dispatchers.rb
|
|
57
|
+
- lib/nti_event_bus/dsl.rb
|
|
58
|
+
- lib/nti_event_bus/handle_event_job.rb
|
|
59
|
+
- lib/nti_event_bus/handler_base.rb
|
|
60
|
+
- lib/nti_event_bus/railtie.rb
|
|
61
|
+
- lib/nti_event_bus/registry.rb
|
|
62
|
+
- lib/nti_event_bus/version.rb
|
|
63
|
+
homepage: https://github.com/nueca-tech/nti_event_bus
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata:
|
|
67
|
+
allowed_push_host: https://rubygems.org
|
|
68
|
+
homepage_uri: https://github.com/nueca-tech/nti_event_bus
|
|
69
|
+
source_code_uri: https://github.com/nueca-tech/nti_event_bus
|
|
70
|
+
changelog_uri: https://github.com/nueca-tech/nti_event_bus/blob/main/CHANGELOG.md
|
|
71
|
+
rubygems_mfa_required: 'true'
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: 3.4.0
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
requirements: []
|
|
86
|
+
rubygems_version: 4.0.10
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: A production-grade event bus with a Rails DSL and asynchronous ActiveJob
|
|
89
|
+
dispatch.
|
|
90
|
+
test_files: []
|