tcb 0.6.0 → 0.6.2
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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +70 -26
- data/lib/generators/tcb/domain/domain_generator.rb +1 -1
- data/lib/generators/tcb/event_store/event_store_generator.rb +1 -1
- data/lib/generators/tcb/install/install_generator.rb +1 -1
- data/lib/generators/tcb/install/templates/tcb.rb.tt +22 -14
- data/lib/tcb/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d004e21f0c4555988f0a5b84e40a4ae4710933b3e862ab964a6a4bc07cd02cf
|
|
4
|
+
data.tar.gz: 684c9a07870739ebec829c872f6c4c71d455b0919d369d0f9badd000fceb28d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71b0fe98490c7a0c1d995b7eaf7612976690306c55c1ccc904f4038089f11c3f6389e03afd9474fd1a058c28de4905fdfca811cb4e88fcf77119a361cde3ff3f
|
|
7
|
+
data.tar.gz: a433451a28f9fe1bd295db4fe40e4760069d4d4915ec8807f8f35f2791df6821dd86f1e24b7c57bbb470dc9b4d618f540e26862a61461e71af9c839bc881a661
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.2] - 2026-05-07
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `tcb:install` generator — `TCB.domain_modules=` moved into `Rails.application.config.to_prepare` block; bare initializer runs before Zeitwerk loads application constants, causing `NameError` when domain modules reference Rails classes such as `ApplicationJob` or `ApplicationRecord`
|
|
13
|
+
|
|
14
|
+
## [0.6.1] - 2026-05-06
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Rails generators now invoked with lowercase namespace: `rails g tcb:install`, `rails g tcb:domain`, `rails g tcb:event_store`
|
|
19
|
+
|
|
8
20
|
## [0.6.0] - 2026-04-24
|
|
9
21
|
|
|
10
22
|
### Added
|
data/README.md
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
# TCB
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Is your codebase using every form of coupling except the one architects actually recommend?
|
|
4
|
+
How much does a feature request cost you now?
|
|
5
|
+
|
|
6
|
+
TCB gives each concern its own place, a clean domain language, and a full record
|
|
7
|
+
of everything that happened, so you can understand and evolve your business logic
|
|
8
|
+
with confidence.
|
|
9
|
+
|
|
10
|
+
Imagine the following scenario. An order is placed. Stock gets reserved. The customer is notified.
|
|
11
|
+
Now imagine each piece in its own domain, reacting independently, easy to test in isolation,
|
|
12
|
+
simple to evolve as your business grows, and the full picture always visible.
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
PlaceOrder = Data.define(:order_id, :customer)
|
|
16
|
+
def Sales.place!(order_id:, customer:) = TCB.dispatch(PlaceOrder.new(order_id:, customer:))
|
|
4
17
|
|
|
5
|
-
|
|
18
|
+
correlation_id = Sales.place!(order_id: 42, customer: "Alice")
|
|
19
|
+
# Sales → OrderPlaced persisted, published
|
|
20
|
+
# Warehouse → StockReserved reacts automatically, same correlation
|
|
21
|
+
# Notifications → CustomerNotified reacts automatically, same correlation
|
|
22
|
+
|
|
23
|
+
TCB.read_correlation(correlation_id).to_a # => all three events, across all three domains, in order
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A lightweight, thread-safe event and command runtime for Domain-Driven Design on Rails.
|
|
27
|
+
Events, aggregates, and handlers are plain Ruby. No framework inheritance, no infrastructure
|
|
28
|
+
details leaking into your domain.
|
|
6
29
|
|
|
7
|
-
|
|
30
|
+
Rails can change. Your domains should change only when your business demands it.
|
|
31
|
+
Clean domain code pays compound interest. It's easier to reason about, easier to test,
|
|
32
|
+
and easier for AI agents to work with. TCB keeps your domain that way.
|
|
33
|
+
Rails takes care of the rest.
|
|
8
34
|
|
|
9
35
|
## Installation
|
|
10
36
|
|
|
@@ -46,11 +72,11 @@ bus.publish(UserRegistered.new(id: 1, email: "alice@example.com"))
|
|
|
46
72
|
|
|
47
73
|
### Execution model
|
|
48
74
|
|
|
49
|
-
TCB::EventBus uses a single background thread to process events. Publishing is non-blocking
|
|
75
|
+
TCB::EventBus uses a single background thread to process events. Publishing is non-blocking. The event is placed on a queue and control returns to the caller immediately. The dispatcher thread processes events in FIFO order. Handlers for a given event execute sequentially within the dispatcher thread.
|
|
50
76
|
|
|
51
77
|
This design favors determinism and simplicity: events are always processed in the order they were published, and handlers cannot race with each other.
|
|
52
78
|
|
|
53
|
-
For tests and simple use cases, `sync: true` executes handlers in the caller thread immediately
|
|
79
|
+
For tests and simple use cases, `sync: true` executes handlers in the caller thread immediately, no background thread, no queue:
|
|
54
80
|
|
|
55
81
|
```ruby
|
|
56
82
|
bus = TCB::EventBus.new(sync: true)
|
|
@@ -77,7 +103,7 @@ The event queue is unbounded by default. If handlers are slower than the rate of
|
|
|
77
103
|
TCB::EventBus.new(max_queue_size: 10_000)
|
|
78
104
|
```
|
|
79
105
|
|
|
80
|
-
When the queue is full, `publish` blocks until space is available. The right value depends on your event volume and handler latency
|
|
106
|
+
When the queue is full, `publish` blocks until space is available. The right value depends on your event volume and handler latency. Measure before deciding.
|
|
81
107
|
|
|
82
108
|
---
|
|
83
109
|
|
|
@@ -140,7 +166,7 @@ end
|
|
|
140
166
|
Event classes can come from anywhere. Cross-module reactions are the norm, not the exception. Each handler is isolated. Ine failure does not prevent others from executing.
|
|
141
167
|
|
|
142
168
|
Domain modules are declared once at the top level, before infrastructure is configured.
|
|
143
|
-
This is the only place TCB needs to know about your bounded contexts
|
|
169
|
+
This is the only place TCB needs to know about your bounded contexts. All reactions,
|
|
144
170
|
persistence rules, and handler mappings live inside each module itself.
|
|
145
171
|
|
|
146
172
|
```ruby
|
|
@@ -154,7 +180,7 @@ end
|
|
|
154
180
|
|
|
155
181
|
`TCB.domain_modules=` wires up subscriptions and command routing from all modules.
|
|
156
182
|
`TCB.configure` provides the infrastructure they run on. The two are intentionally
|
|
157
|
-
separate
|
|
183
|
+
separate. Domain modules don't change between environments, infrastructure does.
|
|
158
184
|
|
|
159
185
|
---
|
|
160
186
|
|
|
@@ -370,7 +396,7 @@ end
|
|
|
370
396
|
|
|
371
397
|
### Domain modules
|
|
372
398
|
|
|
373
|
-
Domain modules are the bounded contexts of your application. Declare them once, at the top level
|
|
399
|
+
Domain modules are the bounded contexts of your application. Declare them once, at the top level, before infrastructure is configured:
|
|
374
400
|
|
|
375
401
|
```ruby
|
|
376
402
|
# config/initializers/tcb.rb
|
|
@@ -425,7 +451,7 @@ Rails.application.config.after_initialize do
|
|
|
425
451
|
end
|
|
426
452
|
```
|
|
427
453
|
|
|
428
|
-
`sync: true` executes handlers in the caller thread
|
|
454
|
+
`sync: true` executes handlers in the caller thread. No background thread, no polling. `after_initialize` runs once at boot. Between tests, call `TCB.reset!` to get a fresh bus and store.
|
|
429
455
|
|
|
430
456
|
Each domain module gets its own database table. Domains stay isolated at the persistence level:
|
|
431
457
|
|
|
@@ -483,7 +509,7 @@ envelope.causation_id # UUID string, event_id of the triggering event; nil for
|
|
|
483
509
|
|
|
484
510
|
## Correlation and causation tracking
|
|
485
511
|
|
|
486
|
-
Every `TCB.dispatch` generates a `correlation_id` and returns it to the caller. All events produced within that dispatch chain share the same `correlation_id`, regardless of how deep the reactive chain goes. `causation_id` identifies the direct cause
|
|
512
|
+
Every `TCB.dispatch` generates a `correlation_id` and returns it to the caller. All events produced within that dispatch chain share the same `correlation_id`, regardless of how deep the reactive chain goes. `causation_id` identifies the direct cause, the `event_id` of the envelope that triggered the handler.
|
|
487
513
|
|
|
488
514
|
```
|
|
489
515
|
Sales.place!(order_id: 42, customer: "Alice")
|
|
@@ -529,7 +555,7 @@ TCB.read_correlation("req-abc").between(1.hour.ago, Time.now).to_a
|
|
|
529
555
|
|
|
530
556
|
Results are ordered by `occurred_at` across all domains. Each result is a `TCB::Envelope` with `correlation_id` and `causation_id` populated.
|
|
531
557
|
|
|
532
|
-
`across:` defaults to all configured domain modules that have persistence registrations. Domains without persistence
|
|
558
|
+
`across:` defaults to all configured domain modules that have persistence registrations. Domains without persistence, like `Notifications` in the example above, are excluded automatically.
|
|
533
559
|
|
|
534
560
|
---
|
|
535
561
|
|
|
@@ -550,7 +576,7 @@ TCB::EventStore::ActiveRecord.new
|
|
|
550
576
|
Generate migration and AR model:
|
|
551
577
|
|
|
552
578
|
```
|
|
553
|
-
bin/rails generate
|
|
579
|
+
bin/rails generate tcb:event_store orders
|
|
554
580
|
```
|
|
555
581
|
|
|
556
582
|
---
|
|
@@ -562,7 +588,7 @@ TCB includes generators to scaffold domain modules, command handlers, and migrat
|
|
|
562
588
|
### Install
|
|
563
589
|
|
|
564
590
|
```bash
|
|
565
|
-
rails generate
|
|
591
|
+
rails generate tcb:install
|
|
566
592
|
```
|
|
567
593
|
|
|
568
594
|
Creates `config/initializers/tcb.rb` with a minimal configuration template.
|
|
@@ -570,7 +596,7 @@ Creates `config/initializers/tcb.rb` with a minimal configuration template.
|
|
|
570
596
|
### Domain module with event store
|
|
571
597
|
|
|
572
598
|
```bash
|
|
573
|
-
rails generate
|
|
599
|
+
rails generate tcb:event_store orders place_order:order_id,customer cancel_order:order_id,reason
|
|
574
600
|
```
|
|
575
601
|
|
|
576
602
|
Generates:
|
|
@@ -582,7 +608,7 @@ Generates:
|
|
|
582
608
|
### Domain module without persistence (pub/sub only)
|
|
583
609
|
|
|
584
610
|
```bash
|
|
585
|
-
rails generate
|
|
611
|
+
rails generate tcb:domain notifications send_welcome_email:user_id,email send_verification_sms:user_id,phone
|
|
586
612
|
```
|
|
587
613
|
|
|
588
614
|
Generates:
|
|
@@ -598,17 +624,21 @@ Generates:
|
|
|
598
624
|
| `--skip-migration` | Skip migration (event_store only) |
|
|
599
625
|
| `--no-comments` | Generate without inline guidance comments |
|
|
600
626
|
|
|
601
|
-
After generating, add your module to config/initializers/tcb.rb. Domain modules don't change between environments
|
|
627
|
+
After generating, add your module to config/initializers/tcb.rb. Domain modules don't change between environments. Infrastructure does. Keeping them separate means your bounded contexts are declared once, while the bus and store are configured per environment:
|
|
602
628
|
|
|
603
629
|
```ruby
|
|
604
630
|
# config/initializers/tcb.rb
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
631
|
+
Rails.application.config.to_prepare do
|
|
632
|
+
TCB.domain_modules = [
|
|
633
|
+
Sales,
|
|
634
|
+
Warehouse,
|
|
635
|
+
Notifications
|
|
636
|
+
]
|
|
637
|
+
end
|
|
610
638
|
```
|
|
611
639
|
|
|
640
|
+
`to_prepare` runs after Zeitwerk loads all application constants. Use it instead of a bare initializer. Domain modules typically reference Rails classes (ApplicationJob, ApplicationRecord, etc.) that are not yet available at initializer time.
|
|
641
|
+
|
|
612
642
|
---
|
|
613
643
|
|
|
614
644
|
## Error Handling
|
|
@@ -645,7 +675,7 @@ bus.force_shutdown
|
|
|
645
675
|
|
|
646
676
|
Configure TCB once at boot in `config/environments/test.rb` (see Configuration above). Between tests, call `TCB.reset!` to get a fresh event bus and a clean event store.
|
|
647
677
|
|
|
648
|
-
`TCB.reset!` shuts down the current bus, clears the event store, and clears all subscriptions. The next test starts with a clean slate. Domain modules do not need to be re-declared
|
|
678
|
+
`TCB.reset!` shuts down the current bus, clears the event store, and clears all subscriptions. The next test starts with a clean slate. Domain modules do not need to be re-declared. They are set once at the top level and persist across resets.
|
|
649
679
|
|
|
650
680
|
### Synchronous mode
|
|
651
681
|
|
|
@@ -661,12 +691,20 @@ Rails.application.config.after_initialize do
|
|
|
661
691
|
end
|
|
662
692
|
```
|
|
663
693
|
|
|
694
|
+
`after_initialize` runs once at boot. Tests do not reload Rails, so `to_prepare` is unnecessary. Between tests, TCB.reset! shuts down the current bus and clears the store, but also wipes the configuration. Restore it in your test teardown so every test starts with a clean, fully configured bus.
|
|
695
|
+
|
|
664
696
|
### Minitest
|
|
665
697
|
|
|
666
698
|
```ruby
|
|
667
699
|
class OrdersTest < Minitest::Test
|
|
668
700
|
include TCB::MinitestHelpers
|
|
669
|
-
def teardown
|
|
701
|
+
def teardown
|
|
702
|
+
TCB.reset!
|
|
703
|
+
TCB.configure do |c|
|
|
704
|
+
c.event_bus = TCB::EventBus.new(sync: true)
|
|
705
|
+
c.event_store = TCB::EventStore::InMemory.new
|
|
706
|
+
end
|
|
707
|
+
end
|
|
670
708
|
|
|
671
709
|
def test_placing_order_publishes_event
|
|
672
710
|
assert_published(Orders::OrderPlaced) do
|
|
@@ -687,7 +725,7 @@ assert_published(Orders::OrderPlaced, within: 0.5) { Orders.place!(...) }
|
|
|
687
725
|
|
|
688
726
|
#### poll_assert
|
|
689
727
|
|
|
690
|
-
Only needed when using an async bus. With `TCB::EventBus.new(sync: true)`, handlers execute in the caller thread and results are available immediately
|
|
728
|
+
Only needed when using an async bus. With `TCB::EventBus.new(sync: true)`, handlers execute in the caller thread and results are available immediately. No polling required.
|
|
691
729
|
|
|
692
730
|
```ruby
|
|
693
731
|
poll_assert("reserve inventory called") { CALLED.include?(:reserve_inventory) }
|
|
@@ -699,7 +737,13 @@ poll_assert("payment processed", within: 2.0) { Payment.completed? }
|
|
|
699
737
|
```ruby
|
|
700
738
|
# spec/support/tcb.rb
|
|
701
739
|
RSpec.configure do |config|
|
|
702
|
-
config.after(:each)
|
|
740
|
+
config.after(:each) do
|
|
741
|
+
TCB.reset!
|
|
742
|
+
TCB.configure do |c|
|
|
743
|
+
c.event_bus = TCB::EventBus.new(sync: true)
|
|
744
|
+
c.event_store = TCB::EventStore::InMemory.new
|
|
745
|
+
end
|
|
746
|
+
end
|
|
703
747
|
end
|
|
704
748
|
```
|
|
705
749
|
|
|
@@ -5,7 +5,7 @@ require_relative "../shared/command_argument"
|
|
|
5
5
|
module TCB
|
|
6
6
|
module Generators
|
|
7
7
|
class DomainGenerator < Rails::Generators::Base
|
|
8
|
-
namespace "
|
|
8
|
+
namespace "tcb:domain"
|
|
9
9
|
source_root File.expand_path("templates", __dir__)
|
|
10
10
|
|
|
11
11
|
argument :module_name, type: :string
|
|
@@ -5,7 +5,7 @@ require_relative "../shared/command_argument"
|
|
|
5
5
|
module TCB
|
|
6
6
|
module Generators
|
|
7
7
|
class EventStoreGenerator < Rails::Generators::Base
|
|
8
|
-
namespace "
|
|
8
|
+
namespace "tcb:event_store"
|
|
9
9
|
source_root File.expand_path("templates", __dir__)
|
|
10
10
|
|
|
11
11
|
argument :module_name, type: :string
|
|
@@ -2,19 +2,27 @@
|
|
|
2
2
|
# Add your domain modules here after generating them:
|
|
3
3
|
# rails generate tcb:event_store orders place_order:order_id,customer
|
|
4
4
|
# rails generate tcb:domain notifications send_welcome_email:user_id,email
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# Infrastructure — how events are transported and stored
|
|
11
|
-
# Runs on every Rails reload in development
|
|
5
|
+
#
|
|
6
|
+
# to_prepare runs after Zeitwerk loads all application constants.
|
|
7
|
+
# Domain modules typically reference Rails classes (ApplicationJob, ApplicationRecord, etc.)
|
|
8
|
+
# that are not yet available at initializer time.
|
|
12
9
|
Rails.application.config.to_prepare do
|
|
13
|
-
TCB.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
-
c.event_store = TCB::EventStore::ActiveRecord.new
|
|
19
|
-
end
|
|
10
|
+
TCB.domain_modules = [
|
|
11
|
+
# Orders,
|
|
12
|
+
# Notifications,
|
|
13
|
+
]
|
|
20
14
|
end
|
|
15
|
+
|
|
16
|
+
# Infrastructure — how events are transported and stored
|
|
17
|
+
# Configure per environment in config/environments/*.rb so differences are explicit.
|
|
18
|
+
# Example for development:
|
|
19
|
+
#
|
|
20
|
+
# Rails.application.config.to_prepare do
|
|
21
|
+
# TCB.reset!
|
|
22
|
+
# TCB.configure do |c|
|
|
23
|
+
# c.event_bus = TCB::EventBus.new(handle_signals: false, shutdown_timeout: 10.0)
|
|
24
|
+
# c.event_store = TCB::EventStore::ActiveRecord.new
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# See README for production and test examples.
|
data/lib/tcb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tcb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ljubomir Marković
|
|
@@ -192,7 +192,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
192
192
|
- !ruby/object:Gem::Version
|
|
193
193
|
version: '0'
|
|
194
194
|
requirements: []
|
|
195
|
-
rubygems_version:
|
|
195
|
+
rubygems_version: 4.0.6
|
|
196
196
|
specification_version: 4
|
|
197
197
|
summary: Lightweight DDD runtime for Rails — events, commands, and aggregates
|
|
198
198
|
test_files: []
|