event_sourcing 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|