synapse-core 0.1.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.
- data/lib/synapse.rb +351 -0
- data/lib/synapse/command/command_bus.rb +45 -0
- data/lib/synapse/command/command_callback.rb +18 -0
- data/lib/synapse/command/command_filter.rb +17 -0
- data/lib/synapse/command/command_handler.rb +13 -0
- data/lib/synapse/command/dispatch_interceptor.rb +16 -0
- data/lib/synapse/command/duplication.rb +43 -0
- data/lib/synapse/command/errors.rb +27 -0
- data/lib/synapse/command/filters/validation.rb +32 -0
- data/lib/synapse/command/gateway.rb +34 -0
- data/lib/synapse/command/interceptor_chain.rb +31 -0
- data/lib/synapse/command/interceptors/serialization.rb +35 -0
- data/lib/synapse/command/message.rb +19 -0
- data/lib/synapse/command/rollback_policy.rb +22 -0
- data/lib/synapse/command/simple_command_bus.rb +138 -0
- data/lib/synapse/command/wiring.rb +47 -0
- data/lib/synapse/domain/aggregate_root.rb +121 -0
- data/lib/synapse/domain/event_container.rb +127 -0
- data/lib/synapse/domain/message.rb +82 -0
- data/lib/synapse/domain/message_builder.rb +34 -0
- data/lib/synapse/domain/stream.rb +108 -0
- data/lib/synapse/duplication.rb +60 -0
- data/lib/synapse/errors.rb +13 -0
- data/lib/synapse/event_bus/event_bus.rb +40 -0
- data/lib/synapse/event_bus/event_listener.rb +16 -0
- data/lib/synapse/event_bus/event_listener_proxy.rb +12 -0
- data/lib/synapse/event_bus/simple_event_bus.rb +69 -0
- data/lib/synapse/event_bus/wiring.rb +23 -0
- data/lib/synapse/event_sourcing/aggregate_factory.rb +69 -0
- data/lib/synapse/event_sourcing/aggregate_root.rb +104 -0
- data/lib/synapse/event_sourcing/conflict_resolver.rb +80 -0
- data/lib/synapse/event_sourcing/entity.rb +64 -0
- data/lib/synapse/event_sourcing/member.rb +72 -0
- data/lib/synapse/event_sourcing/repository.rb +119 -0
- data/lib/synapse/event_sourcing/snapshot/count_stream.rb +86 -0
- data/lib/synapse/event_sourcing/snapshot/count_trigger.rb +91 -0
- data/lib/synapse/event_sourcing/snapshot/taker.rb +73 -0
- data/lib/synapse/event_sourcing/storage_listener.rb +34 -0
- data/lib/synapse/event_sourcing/stream_decorator.rb +25 -0
- data/lib/synapse/event_store/errors.rb +16 -0
- data/lib/synapse/event_store/event_store.rb +43 -0
- data/lib/synapse/event_store/in_memory.rb +59 -0
- data/lib/synapse/event_store/mongo/cursor_event_stream.rb +63 -0
- data/lib/synapse/event_store/mongo/event_store.rb +86 -0
- data/lib/synapse/event_store/mongo/per_commit_strategy.rb +253 -0
- data/lib/synapse/event_store/mongo/per_event_strategy.rb +143 -0
- data/lib/synapse/event_store/mongo/storage_strategy.rb +113 -0
- data/lib/synapse/event_store/mongo/template.rb +73 -0
- data/lib/synapse/identifier.rb +23 -0
- data/lib/synapse/message.rb +101 -0
- data/lib/synapse/message_builder.rb +38 -0
- data/lib/synapse/process_manager/correlation.rb +32 -0
- data/lib/synapse/process_manager/correlation_resolver.rb +14 -0
- data/lib/synapse/process_manager/correlation_set.rb +58 -0
- data/lib/synapse/process_manager/process.rb +71 -0
- data/lib/synapse/repository/errors.rb +26 -0
- data/lib/synapse/repository/lock_manager.rb +40 -0
- data/lib/synapse/repository/locking.rb +97 -0
- data/lib/synapse/repository/pessimistic_lock_manager.rb +61 -0
- data/lib/synapse/repository/repository.rb +109 -0
- data/lib/synapse/serialization/converter.rb +39 -0
- data/lib/synapse/serialization/converter/chain.rb +45 -0
- data/lib/synapse/serialization/converter/factory.rb +68 -0
- data/lib/synapse/serialization/converter/identity.rb +29 -0
- data/lib/synapse/serialization/converter/json.rb +31 -0
- data/lib/synapse/serialization/converter/ox.rb +31 -0
- data/lib/synapse/serialization/errors.rb +12 -0
- data/lib/synapse/serialization/lazy_object.rb +61 -0
- data/lib/synapse/serialization/message/data.rb +25 -0
- data/lib/synapse/serialization/message/metadata.rb +13 -0
- data/lib/synapse/serialization/message/serialization_aware.rb +17 -0
- data/lib/synapse/serialization/message/serialization_aware_message.rb +66 -0
- data/lib/synapse/serialization/message/serialized_message.rb +201 -0
- data/lib/synapse/serialization/message/serialized_message_builder.rb +64 -0
- data/lib/synapse/serialization/message/serialized_object_cache.rb +50 -0
- data/lib/synapse/serialization/message/serializer.rb +47 -0
- data/lib/synapse/serialization/revision_resolver.rb +30 -0
- data/lib/synapse/serialization/serialized_object.rb +37 -0
- data/lib/synapse/serialization/serialized_type.rb +31 -0
- data/lib/synapse/serialization/serializer.rb +98 -0
- data/lib/synapse/serialization/serializer/marshal.rb +32 -0
- data/lib/synapse/serialization/serializer/oj.rb +34 -0
- data/lib/synapse/serialization/serializer/ox.rb +31 -0
- data/lib/synapse/uow/factory.rb +28 -0
- data/lib/synapse/uow/listener.rb +79 -0
- data/lib/synapse/uow/listener_collection.rb +93 -0
- data/lib/synapse/uow/nesting.rb +262 -0
- data/lib/synapse/uow/provider.rb +71 -0
- data/lib/synapse/uow/storage_listener.rb +14 -0
- data/lib/synapse/uow/transaction_manager.rb +27 -0
- data/lib/synapse/uow/uow.rb +178 -0
- data/lib/synapse/upcasting/chain.rb +78 -0
- data/lib/synapse/upcasting/context.rb +58 -0
- data/lib/synapse/upcasting/data.rb +30 -0
- data/lib/synapse/upcasting/single_upcaster.rb +57 -0
- data/lib/synapse/upcasting/upcaster.rb +55 -0
- data/lib/synapse/version.rb +3 -0
- data/lib/synapse/wiring/message_wiring.rb +41 -0
- data/lib/synapse/wiring/wire.rb +55 -0
- data/lib/synapse/wiring/wire_registry.rb +61 -0
- data/test/command/duplication_test.rb +54 -0
- data/test/command/gateway_test.rb +25 -0
- data/test/command/interceptor_chain_test.rb +26 -0
- data/test/command/serialization_test.rb +37 -0
- data/test/command/simple_command_bus_test.rb +141 -0
- data/test/command/validation_test.rb +42 -0
- data/test/command/wiring_test.rb +73 -0
- data/test/domain/aggregate_root_test.rb +57 -0
- data/test/domain/fixtures.rb +31 -0
- data/test/domain/message_test.rb +61 -0
- data/test/domain/stream_test.rb +35 -0
- data/test/duplication_test.rb +40 -0
- data/test/event_bus/wiring_test.rb +46 -0
- data/test/event_sourcing/aggregate_factory_test.rb +28 -0
- data/test/event_sourcing/aggregate_root_test.rb +76 -0
- data/test/event_sourcing/entity_test.rb +34 -0
- data/test/event_sourcing/fixtures.rb +85 -0
- data/test/event_sourcing/repository_test.rb +102 -0
- data/test/event_sourcing/snapshot/aggregate_taker_test.rb +39 -0
- data/test/event_sourcing/snapshot/deferred_taker_test.rb +19 -0
- data/test/event_sourcing/snapshot/integration_test.rb +65 -0
- data/test/event_sourcing/storage_listener_test.rb +77 -0
- data/test/event_store/in_memory_test.rb +47 -0
- data/test/process_manager/correlation_set_test.rb +49 -0
- data/test/process_manager/correlation_test.rb +24 -0
- data/test/process_manager/process_test.rb +52 -0
- data/test/repository/locking_test.rb +101 -0
- data/test/serialization/converter/factory_test.rb +33 -0
- data/test/serialization/converter/identity_test.rb +17 -0
- data/test/serialization/converter/json_test.rb +31 -0
- data/test/serialization/converter/ox_test.rb +40 -0
- data/test/serialization/fixtures.rb +17 -0
- data/test/serialization/lazy_object_test.rb +32 -0
- data/test/serialization/message/metadata_test.rb +19 -0
- data/test/serialization/message/serialization_aware_message_test.rb +88 -0
- data/test/serialization/message/serialized_message_builder_test.rb +41 -0
- data/test/serialization/message/serialized_message_test.rb +140 -0
- data/test/serialization/message/serializer_test.rb +50 -0
- data/test/serialization/revision_resolver_test.rb +12 -0
- data/test/serialization/serialized_object_test.rb +36 -0
- data/test/serialization/serialized_type_test.rb +27 -0
- data/test/serialization/serializer/marshal_test.rb +22 -0
- data/test/serialization/serializer/oj_test.rb +24 -0
- data/test/serialization/serializer/ox_test.rb +36 -0
- data/test/serialization/serializer_test.rb +20 -0
- data/test/test_helper.rb +19 -0
- data/test/uow/factory_test.rb +23 -0
- data/test/uow/outer_commit_listener_test.rb +50 -0
- data/test/uow/provider_test.rb +70 -0
- data/test/uow/uow_test.rb +337 -0
- data/test/upcasting/chain_test.rb +29 -0
- data/test/upcasting/fixtures.rb +66 -0
- data/test/wiring/wire_registry_test.rb +60 -0
- data/test/wiring/wire_test.rb +51 -0
- metadata +263 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module EventBus
|
|
3
|
+
# Represents a listener that can be notified of events from an event bus. Implementations are
|
|
4
|
+
# highly discouraged from throwing exceptions.
|
|
5
|
+
#
|
|
6
|
+
# @abstract
|
|
7
|
+
module EventListener
|
|
8
|
+
# Called when an event is published to the event bus
|
|
9
|
+
#
|
|
10
|
+
# @abstract
|
|
11
|
+
# @param [EventMessage] event
|
|
12
|
+
# @return [undefined]
|
|
13
|
+
def notify(event); end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module EventBus
|
|
3
|
+
# Implementation of an event bus that notifies any subscribed event listeners in the calling
|
|
4
|
+
# thread. Listeners are expected to implement asynchronous handing themselves, if desired.
|
|
5
|
+
class SimpleEventBus < EventBus
|
|
6
|
+
def initialize
|
|
7
|
+
@listeners = Set.new
|
|
8
|
+
@logger = Logging.logger.new self.class
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param [EventMessage...] events
|
|
12
|
+
# @return [undefined]
|
|
13
|
+
def publish(*events)
|
|
14
|
+
if @listeners.empty?
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
events.flatten!
|
|
19
|
+
events.each do |event|
|
|
20
|
+
@listeners.each do |listener|
|
|
21
|
+
if @logger.debug?
|
|
22
|
+
listener_type = actual_type listener
|
|
23
|
+
@logger.debug 'Dispatching event [%s] to listener [%s]' %
|
|
24
|
+
[event.payload_type, listener_type]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
listener.notify event
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param [EventListener] listener
|
|
33
|
+
# @return [undefined]
|
|
34
|
+
def subscribe(listener)
|
|
35
|
+
listener_type = actual_type listener
|
|
36
|
+
|
|
37
|
+
if @listeners.add? listener
|
|
38
|
+
@logger.debug 'Event listener [%s] subscribed' % listener_type
|
|
39
|
+
else
|
|
40
|
+
@logger.info 'Event listener [%s] not added, was already subscribed' % listener_type
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param [EventListener] listener
|
|
45
|
+
# @return [undefined]
|
|
46
|
+
def unsubscribe(listener)
|
|
47
|
+
listener_type = actual_type listener
|
|
48
|
+
|
|
49
|
+
if @listeners.delete? listener
|
|
50
|
+
@logger.debug 'Event listener [%s] unsubscribed' % listener_type
|
|
51
|
+
else
|
|
52
|
+
@logger.info 'Event listener [%s] not removed, was not subscribed' % listener_type
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# @param [EventListener] listener
|
|
59
|
+
# @return [Class]
|
|
60
|
+
def actual_type(listener)
|
|
61
|
+
if listener.respond_to? :proxied_type
|
|
62
|
+
listener.proxied_type
|
|
63
|
+
else
|
|
64
|
+
listener.class
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module EventBus
|
|
3
|
+
# Mixin for an event listener that wishes to use the wiring DSL
|
|
4
|
+
module WiringEventListener
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
include EventListener
|
|
7
|
+
include Wiring::MessageWiring
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
self.wire_registry = Wiring::WireRegistry.new true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param [EventMessage] event
|
|
14
|
+
# @return [undefined]
|
|
15
|
+
def notify(event)
|
|
16
|
+
wire = self.wire_registry.wire_for event.payload_type
|
|
17
|
+
if wire
|
|
18
|
+
invoke_wire event, wire
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module EventSourcing
|
|
3
|
+
# Represents a mechanism for creating aggregates to be initialized by an event stream
|
|
4
|
+
# @abstract
|
|
5
|
+
class AggregateFactory
|
|
6
|
+
# Instantiates an aggregate using the given aggregate identifier and first event
|
|
7
|
+
#
|
|
8
|
+
# The first event is either the event used to create the aggregate or the most recent
|
|
9
|
+
# snapshot event for the aggregate.
|
|
10
|
+
#
|
|
11
|
+
# @abstract
|
|
12
|
+
# @param [Object] aggregate_id
|
|
13
|
+
# @param [DomainEventMessage] first_event
|
|
14
|
+
# @return [AggregateRoot]
|
|
15
|
+
def create_aggregate(aggregate_id, first_event); end
|
|
16
|
+
|
|
17
|
+
# @abstract
|
|
18
|
+
# @return [Class] Type of aggregate being created by this factory
|
|
19
|
+
def aggregate_type; end
|
|
20
|
+
|
|
21
|
+
# @abstract
|
|
22
|
+
# @return [String] Type identifier used to store the aggregate in the event store
|
|
23
|
+
def type_identifier; end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Aggregate factory that uses a convention to create instances of aggregates
|
|
27
|
+
class GenericAggregateFactory < AggregateFactory
|
|
28
|
+
# @return [Class]
|
|
29
|
+
attr_reader :aggregate_type
|
|
30
|
+
|
|
31
|
+
# @return [String]
|
|
32
|
+
attr_reader :type_identifier
|
|
33
|
+
|
|
34
|
+
# @param [Class] aggregate_type
|
|
35
|
+
# @return [undefined]
|
|
36
|
+
def initialize(aggregate_type)
|
|
37
|
+
@aggregate_type = aggregate_type
|
|
38
|
+
@type_identifier = aggregate_type.to_s.demodulize
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param [Object] aggregate_id
|
|
42
|
+
# @param [DomainEventMessage] first_event
|
|
43
|
+
# @return [AggregateRoot]
|
|
44
|
+
def create_aggregate(aggregate_id, first_event)
|
|
45
|
+
payload = first_event.payload
|
|
46
|
+
|
|
47
|
+
if payload.is_a? AggregateRoot
|
|
48
|
+
aggregate = payload
|
|
49
|
+
else
|
|
50
|
+
aggregate = @aggregate_type.allocate
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
post_process aggregate
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
protected
|
|
57
|
+
|
|
58
|
+
# Performs any processing that must be done on an aggregate instance that was reconstructed
|
|
59
|
+
# from a snapshot event. Implementations may choose to modify the existing instance or return
|
|
60
|
+
# a new instance.
|
|
61
|
+
#
|
|
62
|
+
# @param [AggregateRoot] aggregate
|
|
63
|
+
# @return [AggregateRoot]
|
|
64
|
+
def post_process(aggregate)
|
|
65
|
+
aggregate
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module EventSourcing
|
|
3
|
+
# Mixin for the root entity of an aggregate that is initialized from a historical event stream
|
|
4
|
+
module AggregateRoot
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
include Domain::AggregateRoot
|
|
7
|
+
include Member
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
# Creates a new instance of this aggregate root without calling its initializer and
|
|
11
|
+
# initializes the state of the aggregate from the given event stream.
|
|
12
|
+
#
|
|
13
|
+
# @param [DomainEventStream] stream
|
|
14
|
+
# @return [AggregateRoot]
|
|
15
|
+
def new_from_stream(stream)
|
|
16
|
+
aggregate = allocate
|
|
17
|
+
aggregate.initialize_from_stream stream
|
|
18
|
+
aggregate
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Integer] The sequence number of the last committed event
|
|
23
|
+
def version
|
|
24
|
+
last_committed_sequence_number
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Initializes the state of this aggregate from the given domain event stream
|
|
28
|
+
#
|
|
29
|
+
# @raise [RuntimeError] If aggregate has already been initialized
|
|
30
|
+
# @param [DomainEventStream] stream
|
|
31
|
+
# @return [undefined]
|
|
32
|
+
def initialize_from_stream(stream)
|
|
33
|
+
if uncommitted_event_count > 0
|
|
34
|
+
raise 'Aggregate has already been initialized'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
pre_initialize
|
|
38
|
+
|
|
39
|
+
last_sequence_number = nil
|
|
40
|
+
|
|
41
|
+
stream.each do |event|
|
|
42
|
+
last_sequence_number = event.sequence_number
|
|
43
|
+
handle_recursively event
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
initialize_event_container last_sequence_number
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Called when a member of the aggregate publishes an event
|
|
50
|
+
#
|
|
51
|
+
# This is only meant to be invoked by entities that are members of this aggregate
|
|
52
|
+
#
|
|
53
|
+
# @api private
|
|
54
|
+
# @param [Object] payload
|
|
55
|
+
# @param [Hash] metadata
|
|
56
|
+
# @return [undefined]
|
|
57
|
+
def handle_member_event(payload, metadata = nil)
|
|
58
|
+
apply payload, metadata
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
protected
|
|
62
|
+
|
|
63
|
+
# Hook that is called before the aggregate is initialized
|
|
64
|
+
# @return [undefined]
|
|
65
|
+
def pre_initialize; end
|
|
66
|
+
|
|
67
|
+
# Creates an event with the given metadata and payload, publishes it using the event
|
|
68
|
+
# container, and finally handles it locally and recursively down the aggregate.
|
|
69
|
+
#
|
|
70
|
+
# @param [Object] payload
|
|
71
|
+
# @param [Hash] metadata
|
|
72
|
+
# @return [undefined]
|
|
73
|
+
def apply(payload, metadata = nil)
|
|
74
|
+
if id
|
|
75
|
+
event = publish_event payload, metadata
|
|
76
|
+
handle_recursively event
|
|
77
|
+
else
|
|
78
|
+
# This is a workaround for aggregates that set the aggregate identifier in an event handler
|
|
79
|
+
event = Domain::DomainEventMessage.build do |b|
|
|
80
|
+
b.metadata = metadata
|
|
81
|
+
b.payload = payload
|
|
82
|
+
b.sequence_number = 0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
handle_recursively event
|
|
86
|
+
publish_event payload, metadata
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Handles the event locally and then cascades to any registered child entities
|
|
91
|
+
#
|
|
92
|
+
# @param [DomainEventMessage] event
|
|
93
|
+
# @return [undefined]
|
|
94
|
+
def handle_recursively(event)
|
|
95
|
+
handle_event event
|
|
96
|
+
|
|
97
|
+
child_entities.each do |entity|
|
|
98
|
+
entity.aggregate_root = self
|
|
99
|
+
entity.handle_aggregate_event event
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module EventSourcing
|
|
3
|
+
# Represents a mechanism that is capable of detecting conflicts between applied changes
|
|
4
|
+
# to the aggregate and unseen changes made to the aggregate.
|
|
5
|
+
class ConflictResolver
|
|
6
|
+
# Checks the list of changes applied to the aggregate and compares it to the list of
|
|
7
|
+
# events already applied to the aggregate. If a conflict is detected, this should throw
|
|
8
|
+
# an exception. Otherwise, the changes will be applied.
|
|
9
|
+
#
|
|
10
|
+
# @raise [ConflictingModificationException] If any conflicts were detected
|
|
11
|
+
# @param [Array] applied_changes List of changes applied to the aggregate
|
|
12
|
+
# @param [Array] committed_changes List of events that were unexpected by the command handler
|
|
13
|
+
# @return [undefined]
|
|
14
|
+
def resolve_conflicts(applied_changes, committed_changes); end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Unit of work listener that detects if there is a conflict before an aggregate is committed
|
|
18
|
+
# If there is a potential conflict, a conflict resolver determines how to resolve the conflict.
|
|
19
|
+
class ConflictResolvingUnitOfWorkListener < UnitOfWork::UnitOfWorkListener
|
|
20
|
+
# @param [AggregateRoot] aggregate
|
|
21
|
+
# @param [Array] unseen_events
|
|
22
|
+
# @param [ConflictResolver] conflict_resolver
|
|
23
|
+
# @return [undefined]
|
|
24
|
+
def initialize(aggregate, unseen_events, conflict_resolver)
|
|
25
|
+
@aggregate = aggregate
|
|
26
|
+
@unseen_events = unseen_events
|
|
27
|
+
@conflict_resolver = conflict_resolver
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param [UnitOfWork] unit
|
|
31
|
+
# @param [Array<AggregateRoot>] aggregates
|
|
32
|
+
# @param [Hash<EventBus, Array>] events
|
|
33
|
+
# @return [undefined]
|
|
34
|
+
def on_prepare_commit(unit, aggregates, events)
|
|
35
|
+
if potential_conflicts?
|
|
36
|
+
@conflict_resolver.resolve_conflicts @aggregate.uncommitted_events.to_a, @unseen_events
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
def potential_conflicts?
|
|
44
|
+
@aggregate.uncommitted_event_count > 0 and
|
|
45
|
+
@aggregate.version and
|
|
46
|
+
@unseen_events.size > 0
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Event stream decorator that captures any events that have been applied after the expected
|
|
51
|
+
# version of an aggregate
|
|
52
|
+
class CapturingEventStream < Domain::DomainEventStream
|
|
53
|
+
extend Forwardable
|
|
54
|
+
|
|
55
|
+
# @param [DomainEventStream] delegate
|
|
56
|
+
# @param [Array] unseen_events
|
|
57
|
+
# @param [Integer] expected_version
|
|
58
|
+
# @return [undefined]
|
|
59
|
+
def initialize(delegate, unseen_events, expected_version)
|
|
60
|
+
@delegate = delegate
|
|
61
|
+
@unseen_events = unseen_events
|
|
62
|
+
@expected_version = expected_version
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [DomainEventMessage]
|
|
66
|
+
def next_event
|
|
67
|
+
event = @delegate.next_event
|
|
68
|
+
|
|
69
|
+
if @expected_version and event.sequence_number > @expected_version
|
|
70
|
+
@unseen_events.push event
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
event
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Delegators for domain event stream
|
|
77
|
+
def_delegators :@delegate, :end?, :peek
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module EventSourcing
|
|
3
|
+
# Mixin for an entity that is part of an event-sourced aggregate
|
|
4
|
+
#
|
|
5
|
+
# Instead managing its own published events, the entity relies on being registered to the
|
|
6
|
+
# aggregate root and using its event container. Events applied to child entities will be
|
|
7
|
+
# cascaded throughout the entire aggregate.
|
|
8
|
+
module Entity
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
include Member
|
|
11
|
+
|
|
12
|
+
# Handles an aggregate event locally and then cascades to any registered child entities
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
# @param [DomainEventMessage] event
|
|
16
|
+
# @return [undefined]
|
|
17
|
+
def handle_aggregate_event(event)
|
|
18
|
+
handle_recursively event
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Registers this entity to the aggregate root
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
# @raise [RuntimeError] If entity is registered to a different aggregate root
|
|
25
|
+
# @param [AggregateRoot] aggregate_root
|
|
26
|
+
# @return [undefined]
|
|
27
|
+
def aggregate_root=(aggregate_root)
|
|
28
|
+
if @aggregate_root and !@aggregate_root.equal? aggregate_root
|
|
29
|
+
raise 'Entity is registered to a different aggregate root'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@aggregate_root = aggregate_root
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
# Handles the event locally and then cascades to any registered child entities
|
|
38
|
+
#
|
|
39
|
+
# @param [DomainEventMessage] event
|
|
40
|
+
# @return [undefined]
|
|
41
|
+
def handle_recursively(event)
|
|
42
|
+
handle_event event
|
|
43
|
+
|
|
44
|
+
child_entities.each do |entity|
|
|
45
|
+
entity.aggregate_root = @aggregate_root
|
|
46
|
+
entity.handle_aggregate_event event
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Applies the given event to the aggregate and publishes it to the event container
|
|
51
|
+
#
|
|
52
|
+
# @param [Object] payload
|
|
53
|
+
# @param [Hash] metadata
|
|
54
|
+
# @return [undefined]
|
|
55
|
+
def apply(payload, metadata = nil)
|
|
56
|
+
unless @aggregate_root
|
|
57
|
+
raise 'Entity has not been registered to an aggregate root'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@aggregate_root.handle_member_event payload, metadata
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module EventSourcing
|
|
3
|
+
# Base mixin for a member of an aggregate which has its state mutated by events that are
|
|
4
|
+
# applied to the aggregate
|
|
5
|
+
module Member
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
# Registers an instance variable as a child entity
|
|
10
|
+
#
|
|
11
|
+
# @param [Symbol...] fields
|
|
12
|
+
# @return [undefined]
|
|
13
|
+
def child_entity(*fields)
|
|
14
|
+
fields.each do |field|
|
|
15
|
+
child_entities.add field.to_s
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns a set of symbols referring to child entities
|
|
20
|
+
# @return [Set]
|
|
21
|
+
def child_entities
|
|
22
|
+
@child_entities ||= Set.new
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
# Returns an array of the child entities of this aggregate member
|
|
29
|
+
# @return [Array]
|
|
30
|
+
def child_entities
|
|
31
|
+
entities = Array.new
|
|
32
|
+
|
|
33
|
+
self.class.child_entities.each do |field|
|
|
34
|
+
value = instance_variable_get '@' + field
|
|
35
|
+
|
|
36
|
+
if value.is_a? Entity
|
|
37
|
+
entities.push value
|
|
38
|
+
|
|
39
|
+
# Hashes
|
|
40
|
+
elsif value.is_a? Hash
|
|
41
|
+
entities.concat filter_entities value.each_key
|
|
42
|
+
entities.concat filter_entities value.each_value
|
|
43
|
+
|
|
44
|
+
# Sets, arrays
|
|
45
|
+
elsif value.is_a? Enumerable
|
|
46
|
+
entities.concat filter_entities value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
entities
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# If the event is relative to this member, its parameters will be used to change
|
|
54
|
+
# the state of this member
|
|
55
|
+
#
|
|
56
|
+
# @abstract
|
|
57
|
+
# @param [EventMessage] event
|
|
58
|
+
# @return [undefined]
|
|
59
|
+
def handle_event(event); end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# @param [Array] entities
|
|
64
|
+
# @return [Array]
|
|
65
|
+
def filter_entities(entities)
|
|
66
|
+
entities.select do |entity|
|
|
67
|
+
entity.is_a? Member
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|