event_sourcing 0.0.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 +7 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/Gemfile +7 -0
- data/Guardfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +13 -0
- data/event_sourcing.gemspec +28 -0
- data/features/steps/whole_stack.rb +29 -0
- data/features/support/env.rb +5 -0
- data/features/support/logger.log +1 -0
- data/features/support/sample_app.rb +40 -0
- data/features/support/spinach.log +0 -0
- data/features/whole_stack.feature +6 -0
- data/lib/event_sourcing.rb +3 -0
- data/lib/event_sourcing/aggregate.rb +30 -0
- data/lib/event_sourcing/aggregate/actor.rb +29 -0
- data/lib/event_sourcing/aggregate/manager.rb +33 -0
- data/lib/event_sourcing/aggregate/manager/cache.rb +17 -0
- data/lib/event_sourcing/aggregate/manager/instance_of.rb +11 -0
- data/lib/event_sourcing/aggregate/manager/reference.rb +14 -0
- data/lib/event_sourcing/aggregate/message.rb +7 -0
- data/lib/event_sourcing/aggregate/wrapper.rb +18 -0
- data/lib/event_sourcing/application.rb +37 -0
- data/lib/event_sourcing/application/actor.rb +40 -0
- data/lib/event_sourcing/application/actor/reference.rb +23 -0
- data/lib/event_sourcing/command.rb +30 -0
- data/lib/event_sourcing/command/bus.rb +16 -0
- data/lib/event_sourcing/event.rb +29 -0
- data/lib/event_sourcing/event/bus.rb +29 -0
- data/lib/event_sourcing/event/bus/reference.rb +28 -0
- data/lib/event_sourcing/event/publisher.rb +47 -0
- data/lib/event_sourcing/event/publisher/reference.rb +11 -0
- data/lib/event_sourcing/event/store.rb +8 -0
- data/lib/event_sourcing/event/store/memory.rb +43 -0
- data/lib/event_sourcing/event/stream.rb +23 -0
- data/lib/event_sourcing/event/subscriber.rb +16 -0
- data/lib/event_sourcing/version.rb +3 -0
- data/spec/concurrent_logging.rb +6 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/actor_helpers.rb +11 -0
- data/spec/support/shared_examples/a_store_implementation.rb +70 -0
- data/spec/unit/aggregate/actor_spec.rb +59 -0
- data/spec/unit/aggregate/manager/cache_spec.rb +26 -0
- data/spec/unit/aggregate/manager/reference_spec.rb +14 -0
- data/spec/unit/aggregate/manager_spec.rb +32 -0
- data/spec/unit/aggregate/wrapper_spec.rb +22 -0
- data/spec/unit/aggregate_spec.rb +75 -0
- data/spec/unit/application/actor/reference_spec.rb +25 -0
- data/spec/unit/application/actor_spec.rb +36 -0
- data/spec/unit/application_spec.rb +41 -0
- data/spec/unit/command/bus_spec.rb +15 -0
- data/spec/unit/command_spec.rb +38 -0
- data/spec/unit/event/bus/reference_spec.rb +48 -0
- data/spec/unit/event/bus_spec.rb +41 -0
- data/spec/unit/event/publisher_spec.rb +28 -0
- data/spec/unit/event/store/memory_spec.rb +6 -0
- data/spec/unit/event/stream_spec.rb +30 -0
- data/spec/unit/event/subscriber_spec.rb +27 -0
- data/spec/unit/event_spec.rb +27 -0
- data/spec/unit_helper.rb +14 -0
- metadata +233 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
require "concurrent/actor"
|
2
|
+
require "event_sourcing/aggregate/manager"
|
3
|
+
require "event_sourcing/command/bus"
|
4
|
+
require "event_sourcing/event/bus"
|
5
|
+
|
6
|
+
module EventSourcing
|
7
|
+
module Application
|
8
|
+
class Actor < Concurrent::Actor::RestartingContext
|
9
|
+
require "event_sourcing/application/actor/reference"
|
10
|
+
|
11
|
+
def self.to_str #TODO Remove this. It's needed for specs passing under jruby O_o
|
12
|
+
to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_reference_class
|
16
|
+
Reference
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(event_store)
|
20
|
+
@event_store = event_store
|
21
|
+
@event_bus = EventSourcing::Event::Bus.spawn!(name: :event_bus, supervise: true, args: [@event_store])
|
22
|
+
@aggregate_manager = EventSourcing::Aggregate::Manager.spawn!(name: :aggregate_manager, supervise: true, args: [@event_bus])
|
23
|
+
@command_bus = EventSourcing::Command::Bus.spawn!(name: :command_bus, supervise: true, args: [@aggregate_manager])
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_message(message)
|
27
|
+
case message
|
28
|
+
when :get_command_bus
|
29
|
+
@command_bus
|
30
|
+
when :get_event_bus
|
31
|
+
@event_bus
|
32
|
+
when :get_aggregate_manager
|
33
|
+
@aggregate_manager
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Concurrent::Actor.i_know_it_is_experimental!
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "concurrent/actor"
|
2
|
+
require "event_sourcing/application/actor"
|
3
|
+
|
4
|
+
module EventSourcing
|
5
|
+
module Application
|
6
|
+
class Actor
|
7
|
+
class Reference < Concurrent::Actor::Reference
|
8
|
+
def execute_command(command)
|
9
|
+
command_bus.tell(command)
|
10
|
+
end
|
11
|
+
|
12
|
+
def terminate!
|
13
|
+
tell(:terminate!)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def command_bus
|
18
|
+
@command_bus ||= ask!(:get_command_bus)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module EventSourcing
|
2
|
+
class Command
|
3
|
+
private_class_method :new
|
4
|
+
|
5
|
+
def self.define(*fields, &block)
|
6
|
+
raise "Commands require an execution block" unless block_given?
|
7
|
+
|
8
|
+
Class.new(self) do
|
9
|
+
public_class_method :new
|
10
|
+
attr_reader(*fields)
|
11
|
+
|
12
|
+
define_method :initialize do |properties = {}|
|
13
|
+
missing_keys = fields - properties.keys
|
14
|
+
|
15
|
+
if missing_keys.any?
|
16
|
+
raise ArgumentError, "missing keyword: #{missing_keys.first}"
|
17
|
+
end
|
18
|
+
|
19
|
+
fields.each do |field|
|
20
|
+
instance_variable_set("@#{field}", properties[field])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
define_method :execute do |*args|
|
25
|
+
instance_exec(*args, &block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "concurrent/actor"
|
2
|
+
|
3
|
+
module EventSourcing
|
4
|
+
class Command
|
5
|
+
class Bus < Concurrent::Actor::RestartingContext
|
6
|
+
|
7
|
+
def initialize(aggregate_manager)
|
8
|
+
@aggregate_manager = aggregate_manager
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_message(command)
|
12
|
+
command.execute(@aggregate_manager)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module EventSourcing
|
2
|
+
class Event
|
3
|
+
|
4
|
+
private_class_method :new
|
5
|
+
|
6
|
+
def self.define(*fields)
|
7
|
+
Class.new(self) do
|
8
|
+
attr_reader(*fields)
|
9
|
+
public_class_method :new
|
10
|
+
|
11
|
+
define_method :initialize do |properties = {}|
|
12
|
+
missing_keys = fields - properties.keys
|
13
|
+
|
14
|
+
if missing_keys.any?
|
15
|
+
raise ArgumentError, "missing keyword: #{missing_keys.first}"
|
16
|
+
end
|
17
|
+
|
18
|
+
fields.each do |field|
|
19
|
+
instance_variable_set("@#{field}", properties[field])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
self.class.to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "concurrent/actor"
|
2
|
+
require "event_sourcing/event/publisher"
|
3
|
+
|
4
|
+
module EventSourcing
|
5
|
+
class Event
|
6
|
+
class Bus < Concurrent::Actor::RestartingContext
|
7
|
+
|
8
|
+
require "event_sourcing/event/bus/reference"
|
9
|
+
|
10
|
+
def initialize(event_store)
|
11
|
+
@store = event_store
|
12
|
+
@publisher = Publisher.spawn!(name: :event_publisher, supervise: true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_message(message)
|
16
|
+
case message
|
17
|
+
when :get_event_publisher
|
18
|
+
@publisher
|
19
|
+
when :get_event_store
|
20
|
+
@store
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_reference_class
|
25
|
+
Reference
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "concurrent/actor"
|
2
|
+
require "event_sourcing/event/bus"
|
3
|
+
|
4
|
+
module EventSourcing
|
5
|
+
class Event
|
6
|
+
class Bus
|
7
|
+
class Reference < Concurrent::Actor::Reference
|
8
|
+
|
9
|
+
def publish(events)
|
10
|
+
publisher.publish(events)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_stream(id)
|
14
|
+
store.get_stream(id)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def publisher
|
19
|
+
@publisher ||= ask!(:get_event_publisher)
|
20
|
+
end
|
21
|
+
|
22
|
+
def store
|
23
|
+
@store ||= ask!(:get_event_store)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "concurrent/actor"
|
2
|
+
|
3
|
+
module EventSourcing
|
4
|
+
class Event
|
5
|
+
class Publisher < Concurrent::Actor::RestartingContext
|
6
|
+
|
7
|
+
require "event_sourcing/event/publisher/reference"
|
8
|
+
|
9
|
+
@subscribers = {}
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_reader :subscribers
|
13
|
+
|
14
|
+
def subscribe(klass, event)
|
15
|
+
@subscribers[event.to_s] ||= []
|
16
|
+
@subscribers[event.to_s] << klass
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@subscribed_actors = {}
|
22
|
+
self.class.subscribers.each do |event,subscribers|
|
23
|
+
@subscribed_actors[event] = subscribers.map { |s| actor_for(s) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_message(event)
|
28
|
+
subscribers_for(event).each do |subscriber|
|
29
|
+
subscriber.tell(event) # TODO: Add support for some kind of ACK + recovery
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_reference_class
|
34
|
+
Reference
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def subscribers_for(event)
|
39
|
+
@subscribed_actors[event.to_s] || []
|
40
|
+
end
|
41
|
+
|
42
|
+
def actor_for(subscriber)
|
43
|
+
@actors ||= subscriber.spawn!(name: subscriber.to_s, supervise: true)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "event_sourcing/event"
|
2
|
+
require "event_sourcing/event/store"
|
3
|
+
require "event_sourcing/event/stream"
|
4
|
+
|
5
|
+
module EventSourcing
|
6
|
+
class Event
|
7
|
+
module Store
|
8
|
+
class Memory
|
9
|
+
def initialize
|
10
|
+
@events_with_stream_id = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def events
|
14
|
+
@events_with_stream_id.map { |e| e[:event] }
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_stream(id)
|
18
|
+
events = events_for(id)
|
19
|
+
EventSourcing::Event::Stream.new(id, events, events.count, self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def append(stream_id, expected_version, events)
|
23
|
+
raise EventSourcing::Event::Store::ConcurrencyError if get_stream(stream_id).version != expected_version
|
24
|
+
|
25
|
+
Array(events).tap do |events|
|
26
|
+
events.each do |event|
|
27
|
+
@events_with_stream_id.push(stream_id: stream_id, event: event)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method :<<, :append
|
34
|
+
alias_method :push, :append
|
35
|
+
|
36
|
+
protected
|
37
|
+
def events_for(stream_id)
|
38
|
+
@events_with_stream_id.select { |event| event[:stream_id] == stream_id }.map { |event| event[:event] }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module EventSourcing
|
2
|
+
class Event
|
3
|
+
class Stream
|
4
|
+
attr_reader :version
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(id, events, version, store)
|
8
|
+
@id = id
|
9
|
+
@events = events
|
10
|
+
@version = version
|
11
|
+
@store = store
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
@events.each(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def append(events)
|
19
|
+
@store.append(@id, @version, events)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "event_sourcing/event/publisher"
|
2
|
+
|
3
|
+
module EventSourcing
|
4
|
+
class Event
|
5
|
+
class Subscriber < Concurrent::Actor::RestartingContext #TODO: Should be a plain Context?
|
6
|
+
def self.subscribe_to(event, &block)
|
7
|
+
define_method "handle_#{event}", &block
|
8
|
+
Publisher.subscribe(self, event)
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_message(event)
|
12
|
+
send("handle_#{event}", event)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module ActorHelpers
|
2
|
+
def actor_reference(ref_class)
|
3
|
+
ref_class.new(instance_double("Concurrent::Actor::Core", is_a?: true, path: "Path for #{ref_class} core", context_class: ref_class))
|
4
|
+
end
|
5
|
+
|
6
|
+
def event_double(name)
|
7
|
+
instance_double("EventSourcing::Event", to_s: name)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec.configuration.include ActorHelpers
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require "event_sourcing/event"
|
2
|
+
|
3
|
+
RSpec.shared_examples "a store implementation" do
|
4
|
+
let(:event) { double("Some event") }
|
5
|
+
let(:another_event) { double("Anothe event") }
|
6
|
+
|
7
|
+
it "is empty when created" do
|
8
|
+
expect(subject.events).to be_empty
|
9
|
+
end
|
10
|
+
|
11
|
+
context "append" do
|
12
|
+
it "returns nil, since it's a command" do
|
13
|
+
expect(subject.append("stream-id", 0, event)).to be_nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it "does nothing if no events are passed to it" do
|
17
|
+
expect { subject.append("stream-id", 0, []) }.not_to change { subject.events.count }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when events have been appended" do
|
22
|
+
before do
|
23
|
+
subject.append("stream-id", 0, event)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "they can be retrieved later" do
|
27
|
+
expect(subject.events).to eq([event])
|
28
|
+
end
|
29
|
+
|
30
|
+
it "they can be retrieved by stream" do
|
31
|
+
expect(subject.get_stream("stream-id").to_a).to eq([event])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when multiple events are appended" do
|
36
|
+
let(:further_event) { double("Further event")}
|
37
|
+
|
38
|
+
before do
|
39
|
+
subject.append("stream-id", 0, [event, another_event])
|
40
|
+
subject.append("stream-id", 2, [further_event])
|
41
|
+
end
|
42
|
+
|
43
|
+
it "they can be retrieved later" do
|
44
|
+
expect(subject.events).to eq([event, another_event, further_event])
|
45
|
+
end
|
46
|
+
|
47
|
+
it "they can be retrieved by stream" do
|
48
|
+
expect(subject.get_stream("stream-id").to_a).to eq([event, another_event, further_event])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it "locks optimistically per stream" do
|
53
|
+
expect {
|
54
|
+
subject.append("stream-id", 0, event)
|
55
|
+
subject.append("stream-id", 0, another_event)
|
56
|
+
}.to raise_error(EventSourcing::Event::Store::ConcurrencyError)
|
57
|
+
end
|
58
|
+
|
59
|
+
context "with multiple streams" do
|
60
|
+
before do
|
61
|
+
subject.append("stream-id", 0, event)
|
62
|
+
subject.append("another-stream-id", 0, [event, another_event])
|
63
|
+
end
|
64
|
+
|
65
|
+
it "allows retrieval of events by stream id" do
|
66
|
+
expect(subject.get_stream("stream-id").version).to eq(1)
|
67
|
+
expect(subject.get_stream("another-stream-id").version).to eq(2)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|