synapse-core 0.5.4 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/synapse/command/async_command_bus.rb +3 -5
- data/lib/synapse/common.rb +0 -1
- data/lib/synapse/configuration/component/command_bus/async_command_bus.rb +3 -2
- data/lib/synapse/configuration/component/{mixin → shared}/thread_pool.rb +10 -8
- data/lib/synapse/configuration/dependent.rb +2 -2
- data/lib/synapse/configuration/ext.rb +58 -29
- data/lib/synapse/configuration.rb +1 -1
- data/lib/synapse/domain/aggregate_root.rb +43 -19
- data/lib/synapse/domain/errors.rb +9 -0
- data/lib/synapse/domain/event_container.rb +3 -3
- data/lib/synapse/domain/message.rb +2 -2
- data/lib/synapse/domain/message_builder.rb +2 -2
- data/lib/synapse/domain/simple_stream.rb +39 -0
- data/lib/synapse/domain/stream.rb +2 -41
- data/lib/synapse/domain.rb +3 -0
- data/lib/synapse/event_sourcing/aggregate_root.rb +102 -2
- data/lib/synapse/event_sourcing/caching.rb +1 -1
- data/lib/synapse/event_sourcing/entity.rb +2 -2
- data/lib/synapse/event_sourcing/member.rb +3 -13
- data/lib/synapse/event_sourcing/repository.rb +9 -2
- data/lib/synapse/event_sourcing/snapshot/taker.rb +2 -2
- data/lib/synapse/mapping/mapping.rb +2 -2
- data/lib/synapse/process_manager/simple_process_manager.rb +1 -0
- data/lib/synapse/rails/injection_helper.rb +23 -0
- data/lib/synapse/railtie.rb +17 -0
- data/lib/synapse/repository/lock_manager.rb +3 -3
- data/lib/synapse/uow/listener.rb +2 -2
- data/lib/synapse/uow/listener_collection.rb +7 -51
- data/lib/synapse/version.rb +1 -1
- data/lib/synapse-core.rb +1 -0
- data/lib/synapse.rb +3 -0
- data/test/command/async_command_bus_test.rb +8 -11
- data/test/configuration/component/command_bus/async_command_bus_test.rb +7 -6
- data/test/configuration/component/serialization/serializer_test.rb +2 -2
- data/test/configuration/fixtures/dependent.rb +1 -1
- data/test/rails/injection_helper_test.rb +27 -0
- data/test/serialization/converter/ox_test.rb +2 -2
- data/test/serialization/serializer/oj_test.rb +1 -1
- data/test/serialization/serializer/ox_test.rb +1 -1
- data/test/test_helper.rb +8 -9
- data/test/uow/uow_test.rb +2 -2
- metadata +221 -222
- data/lib/synapse/common/concurrency/executor.rb +0 -13
- data/test/support/countdown_latch.rb +0 -18
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'thread/pool'
|
2
|
-
|
3
1
|
module Synapse
|
4
2
|
module Command
|
5
3
|
# Command bus that uses a thread pool to asynchronously execute commands, invoking the given
|
@@ -7,8 +5,8 @@ module Synapse
|
|
7
5
|
#
|
8
6
|
# @todo Look into non-blocking circular buffers or LMAX Disruptor
|
9
7
|
class AsynchronousCommandBus < SimpleCommandBus
|
10
|
-
# Pool of
|
11
|
-
# @return [
|
8
|
+
# Pool of worker threads that dispatch commands from a queue
|
9
|
+
# @return [Contender::Pool::ThreadPoolExecutor]
|
12
10
|
attr_accessor :thread_pool
|
13
11
|
|
14
12
|
# @api public
|
@@ -16,7 +14,7 @@ module Synapse
|
|
16
14
|
# @param [CommandCallback] callback
|
17
15
|
# @return [undefined]
|
18
16
|
def dispatch_with_callback(command, callback)
|
19
|
-
@thread_pool.
|
17
|
+
@thread_pool.execute do
|
20
18
|
super command, callback
|
21
19
|
end
|
22
20
|
end
|
data/lib/synapse/common.rb
CHANGED
@@ -3,7 +3,6 @@ require 'synapse/common/identifier'
|
|
3
3
|
require 'synapse/common/message'
|
4
4
|
require 'synapse/common/message_builder'
|
5
5
|
|
6
|
-
require 'synapse/common/concurrency/executor'
|
7
6
|
require 'synapse/common/concurrency/identifier_lock'
|
8
7
|
require 'synapse/common/concurrency/public_lock'
|
9
8
|
|
@@ -3,13 +3,14 @@ module Synapse
|
|
3
3
|
# Definition builder used to build an asynchronous command bus
|
4
4
|
#
|
5
5
|
# @see SimpleCommandBusDefinitionBuilder For additional options
|
6
|
+
# @see Contender::Pool::ThreadPoolExecutor For pool options
|
6
7
|
#
|
7
8
|
# @example The minimum possible effort to build an asynchronous command bus
|
8
9
|
# async_command_bus
|
9
10
|
#
|
10
11
|
# @example Create an asynchronous command bus with a custom thread count
|
11
12
|
# async_command_bus do
|
12
|
-
#
|
13
|
+
# use_pool_options size: 2
|
13
14
|
# end
|
14
15
|
class AsynchronousCommandBusDefinitionBuilder < SimpleCommandBusDefinitionBuilder
|
15
16
|
include ThreadPoolDefinitionBuilder
|
@@ -19,7 +20,7 @@ module Synapse
|
|
19
20
|
# @return [undefined]
|
20
21
|
def populate_defaults
|
21
22
|
super
|
22
|
-
|
23
|
+
use_pool_options size: 4
|
23
24
|
end
|
24
25
|
|
25
26
|
# @param [UnitOfWorkFactory] unit_factory
|
@@ -1,25 +1,27 @@
|
|
1
1
|
module Synapse
|
2
2
|
module Configuration
|
3
3
|
# Mixin for a definition builder that creates a service that is backed by a thread pool
|
4
|
+
# @see Contender::Pool::ThreadPoolExecutor For pool options
|
4
5
|
module ThreadPoolDefinitionBuilder
|
5
6
|
extend ActiveSupport::Concern
|
6
7
|
|
7
|
-
# Sets the
|
8
|
+
# Sets the options for the thread pool
|
8
9
|
#
|
9
|
-
# @param [
|
10
|
-
# @param [Integer] max_threads
|
10
|
+
# @param [Hash] pool_options
|
11
11
|
# @return [undefined]
|
12
|
-
def
|
13
|
-
@
|
14
|
-
@max_threads = max_threads
|
12
|
+
def use_pool_options(pool_options)
|
13
|
+
@pool_options = pool_options
|
15
14
|
end
|
16
15
|
|
17
16
|
protected
|
18
17
|
|
19
18
|
# Creates a thread pool with the configured options
|
20
|
-
# @return [
|
19
|
+
# @return [Contender::Pool::ThreadPoolExecutor]
|
21
20
|
def create_thread_pool
|
22
|
-
|
21
|
+
pool = Contender::Pool::ThreadPoolExecutor.new @pool_options
|
22
|
+
pool.start
|
23
|
+
|
24
|
+
pool
|
23
25
|
end
|
24
26
|
end # ThreadPoolDefinitionBuilder
|
25
27
|
end # Configuration
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Synapse
|
2
2
|
module Configuration
|
3
|
-
# Mixin for an object defines its dependencies so that they can be auto-injected by the
|
3
|
+
# Mixin for an object defines its dependencies so that they can be auto-injected by the
|
4
4
|
# service container after it has been instantiated
|
5
5
|
#
|
6
6
|
# Note that this only supports setter injection
|
@@ -20,7 +20,7 @@ module Synapse
|
|
20
20
|
def depends_on(service, *args)
|
21
21
|
options = args.extract_options!
|
22
22
|
|
23
|
-
attribute = options[:
|
23
|
+
attribute = options[:as] || service
|
24
24
|
attr_accessor attribute
|
25
25
|
|
26
26
|
self.dependencies[service] = attribute
|
@@ -1,35 +1,64 @@
|
|
1
1
|
module Synapse
|
2
|
-
|
3
|
-
|
2
|
+
class << self
|
3
|
+
# @return [Configuration::Container]
|
4
|
+
attr_accessor :container
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
# @return [Configuration::ContainerBuilder]
|
7
|
+
attr_accessor :container_builder
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
9
|
+
# Initializes the service container and the container builder
|
10
|
+
#
|
11
|
+
# The given block is executed in the container of the container builder. Factory blocks are
|
12
|
+
# always deferred until the service is needed to build another service or is manually
|
13
|
+
# requested from the container.
|
14
|
+
#
|
15
|
+
# This method can be called multiple times without losing the state of the container.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# Synapse.build do
|
19
|
+
# definition :account_projection do
|
20
|
+
# tag :event_listener, :projection
|
21
|
+
# use_factory do
|
22
|
+
# Bank::Projections::AccountProjection.new
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @see Configuration::ContainerBuilder#build_with
|
28
|
+
# @param [Proc] block
|
29
|
+
# @return [undefined]
|
30
|
+
def build(&block)
|
31
|
+
self.container ||= Configuration::Container.new
|
32
|
+
self.container_builder ||= Configuration::ContainerBuilder.new self.container
|
32
33
|
|
33
|
-
|
34
|
+
self.container_builder.build_with(&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Initializes the service container and the container builder and applies simple defaults for
|
38
|
+
# getting started with a single node setup
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# Synapse.build_with_defaults do
|
42
|
+
# # Setup things like command handlers, event listeners and repositories
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# @see Configuration::ContainerBuilder#build_with
|
46
|
+
# @param [Proc] block
|
47
|
+
# @return [undefined]
|
48
|
+
def build_with_defaults(&block)
|
49
|
+
build do
|
50
|
+
converter_factory
|
51
|
+
serializer
|
52
|
+
upcaster_chain
|
53
|
+
|
54
|
+
unit_factory
|
55
|
+
simple_command_bus
|
56
|
+
gateway
|
57
|
+
|
58
|
+
simple_event_bus
|
59
|
+
end
|
60
|
+
|
61
|
+
build &block
|
62
|
+
end
|
34
63
|
end
|
35
64
|
end
|
@@ -5,7 +5,7 @@ require 'synapse/configuration/definition_builder'
|
|
5
5
|
require 'synapse/configuration/dependent'
|
6
6
|
require 'synapse/configuration/ext'
|
7
7
|
|
8
|
-
require 'synapse/configuration/component/
|
8
|
+
require 'synapse/configuration/component/shared/thread_pool'
|
9
9
|
require 'synapse/configuration/component/command_bus'
|
10
10
|
require 'synapse/configuration/component/event_bus'
|
11
11
|
# Has to be loaded before event sourcing
|
@@ -1,6 +1,22 @@
|
|
1
1
|
module Synapse
|
2
2
|
module Domain
|
3
|
-
# Mixin module for a basic aggregate root
|
3
|
+
# Mixin module for a basic aggregate root that is not event-sourced
|
4
|
+
#
|
5
|
+
# The persistence mechanism is left up to the aggregate root that uses this mixin. Any sort of
|
6
|
+
# ORM can be used to persist aggregates.
|
7
|
+
#
|
8
|
+
# If optimistic locking is used, the ORM must increment the version field before saving.
|
9
|
+
#
|
10
|
+
# class Order
|
11
|
+
# include Synapse::Domain::AggregateRoot
|
12
|
+
# include MongoMapper::Document
|
13
|
+
#
|
14
|
+
# key :version, Integer
|
15
|
+
#
|
16
|
+
# before_save { self.version += 1 }
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @see Repository::SimpleRepository
|
4
20
|
module AggregateRoot
|
5
21
|
# @return [Boolean] True if this aggregate has been marked for deletion
|
6
22
|
attr_reader :deleted
|
@@ -14,6 +30,8 @@ module Synapse
|
|
14
30
|
attr_reader :version
|
15
31
|
|
16
32
|
# Marks this aggregate as committed by a repository
|
33
|
+
#
|
34
|
+
# @api public
|
17
35
|
# @return [undefined]
|
18
36
|
def mark_committed
|
19
37
|
if @event_container
|
@@ -23,6 +41,8 @@ module Synapse
|
|
23
41
|
end
|
24
42
|
|
25
43
|
# Returns the number of uncommitted events published by this aggregate
|
44
|
+
#
|
45
|
+
# @api public
|
26
46
|
# @return [Integer]
|
27
47
|
def uncommitted_event_count
|
28
48
|
unless @event_container
|
@@ -33,6 +53,8 @@ module Synapse
|
|
33
53
|
end
|
34
54
|
|
35
55
|
# Returns a domain event strema containing any uncommitted events published by this aggregate
|
56
|
+
#
|
57
|
+
# @api public
|
36
58
|
# @return [DomainEventStream]
|
37
59
|
def uncommitted_events
|
38
60
|
unless @event_container
|
@@ -47,7 +69,8 @@ module Synapse
|
|
47
69
|
# If an event registration listener is added after events have already been registered, it
|
48
70
|
# will still get a change to process the uncommitted events in this aggregate.
|
49
71
|
#
|
50
|
-
# @
|
72
|
+
# @api public
|
73
|
+
# @param [Proc] listener
|
51
74
|
# @return [undefined]
|
52
75
|
def add_registration_listener(&listener)
|
53
76
|
event_container.add_registration_listener listener
|
@@ -57,10 +80,13 @@ module Synapse
|
|
57
80
|
|
58
81
|
# Publishes a domain event with the given payload and optional metadata
|
59
82
|
#
|
83
|
+
# Before any events are published, the aggregate identifier must be set.
|
84
|
+
#
|
60
85
|
# @api public
|
86
|
+
# @raise [AggregateIdentifierNotInitializedError] If identifier not set
|
61
87
|
# @param [Object] payload Payload of the message; the actual event object
|
62
88
|
# @param [Hash] metadata Metadata associated with the event
|
63
|
-
# @return [DomainEventMessage] The event that will be
|
89
|
+
# @return [DomainEventMessage] The event that will be published
|
64
90
|
def publish_event(payload, metadata = nil)
|
65
91
|
event_container.register_event payload, metadata
|
66
92
|
end
|
@@ -73,18 +99,9 @@ module Synapse
|
|
73
99
|
@deleted = true
|
74
100
|
end
|
75
101
|
|
76
|
-
# Initializes the event container with the given sequence number
|
77
|
-
#
|
78
|
-
# @param [Integer] last_sequence_number
|
79
|
-
# The sequence number of the last committed event for this aggregate
|
80
|
-
#
|
81
|
-
# @return [undefined]
|
82
|
-
def initialize_event_container(last_sequence_number)
|
83
|
-
event_container.initialize_sequence_number last_sequence_number
|
84
|
-
@last_sequence_number = last_sequence_number >= 0 ? last_sequence_number : nil
|
85
|
-
end
|
86
|
-
|
87
102
|
# Returns the sequence number of the last committed event
|
103
|
+
#
|
104
|
+
# @api public
|
88
105
|
# @return [Integer]
|
89
106
|
def last_committed_sequence_number
|
90
107
|
unless @event_container
|
@@ -96,6 +113,16 @@ module Synapse
|
|
96
113
|
|
97
114
|
private
|
98
115
|
|
116
|
+
# Initializes the event container with the given sequence number
|
117
|
+
#
|
118
|
+
# @param [Integer] last_sequence_number
|
119
|
+
# The sequence number of the last committed event for this aggregate
|
120
|
+
# @return [undefined]
|
121
|
+
def initialize_event_container(last_sequence_number)
|
122
|
+
event_container.initialize_sequence_number last_sequence_number
|
123
|
+
@last_sequence_number = last_sequence_number >= 0 ? last_sequence_number : nil
|
124
|
+
end
|
125
|
+
|
99
126
|
# Initializes the uncommitted event container for this aggregate, if not already
|
100
127
|
#
|
101
128
|
# @raise [AggregateIdentifierNotInitializedError] If identifier not set
|
@@ -112,9 +139,6 @@ module Synapse
|
|
112
139
|
|
113
140
|
@event_container
|
114
141
|
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# Raised when an event is published but the aggregate identifier is not set
|
118
|
-
class AggregateIdentifierNotInitializedError < NonTransientError; end
|
119
|
-
end
|
142
|
+
end # AggregateRoot
|
143
|
+
end # Domain
|
120
144
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Synapse
|
2
|
+
module Domain
|
3
|
+
# Raised when an event is published but the aggregate identifier is not set
|
4
|
+
class AggregateIdentifierNotInitializedError < NonTransientError; end
|
5
|
+
|
6
|
+
# Raised when the end of a domain event stream has been reached
|
7
|
+
class EndOfStreamError < NonTransientError; end
|
8
|
+
end
|
9
|
+
end
|
@@ -56,7 +56,7 @@ module Synapse
|
|
56
56
|
# If the listener is added after events have already registered with the container, it will
|
57
57
|
# be called with a backlog of events to process.
|
58
58
|
#
|
59
|
-
# @param [
|
59
|
+
# @param [Proc] listener
|
60
60
|
# @return [undefined]
|
61
61
|
def add_registration_listener(listener)
|
62
62
|
@listeners.push listener
|
@@ -122,6 +122,6 @@ module Synapse
|
|
122
122
|
def next_sequence_number
|
123
123
|
last_sequence_number ? last_sequence_number.next : 0
|
124
124
|
end
|
125
|
-
end
|
126
|
-
end
|
125
|
+
end # EventContainer
|
126
|
+
end # Domain
|
127
127
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Synapse
|
2
|
+
module Domain
|
3
|
+
# Implementation of a domain event stream that holds a stream of events in memory
|
4
|
+
class SimpleDomainEventStream < DomainEventStream
|
5
|
+
def initialize(*events)
|
6
|
+
@events = events.flatten
|
7
|
+
@next_index = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns true if the end of the stream has been reached
|
11
|
+
# @return [Boolean]
|
12
|
+
def end?
|
13
|
+
@next_index >= @events.size
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the next event in the stream and moves the stream's pointer forward
|
17
|
+
#
|
18
|
+
# @raise [EndOfStreamError] If the end of the stream has been reached
|
19
|
+
# @return [DomainEventMessage]
|
20
|
+
def next_event
|
21
|
+
assert_valid
|
22
|
+
|
23
|
+
event = @events.at @next_index
|
24
|
+
@next_index += 1
|
25
|
+
|
26
|
+
event
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the next event in the stream without moving the stream's pointer forward
|
30
|
+
#
|
31
|
+
# @raise [EndOfStreamError] If the end of the stream has been reached
|
32
|
+
# @return [DomainEventMessage]
|
33
|
+
def peek
|
34
|
+
assert_valid
|
35
|
+
@events.at @next_index
|
36
|
+
end
|
37
|
+
end # SimpleDomainEventStream
|
38
|
+
end # Domain
|
39
|
+
end
|
@@ -64,45 +64,6 @@ module Synapse
|
|
64
64
|
raise EndOfStreamError
|
65
65
|
end
|
66
66
|
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# Raised when the end of a domain event stream has been reached
|
70
|
-
class EndOfStreamError < NonTransientError; end
|
71
|
-
|
72
|
-
# Implementation of a domain event stream that holds a stream of events in memory
|
73
|
-
class SimpleDomainEventStream < DomainEventStream
|
74
|
-
def initialize(*events)
|
75
|
-
@events = events.flatten
|
76
|
-
@next_index = 0
|
77
|
-
end
|
78
|
-
|
79
|
-
# Returns true if the end of the stream has been reached
|
80
|
-
# @return [Boolean]
|
81
|
-
def end?
|
82
|
-
@next_index >= @events.size
|
83
|
-
end
|
84
|
-
|
85
|
-
# Returns the next event in the stream and moves the stream's pointer forward
|
86
|
-
#
|
87
|
-
# @raise [EndOfStreamError] If the end of the stream has been reached
|
88
|
-
# @return [DomainEventMessage]
|
89
|
-
def next_event
|
90
|
-
assert_valid
|
91
|
-
|
92
|
-
event = @events.at @next_index
|
93
|
-
@next_index += 1
|
94
|
-
|
95
|
-
event
|
96
|
-
end
|
97
|
-
|
98
|
-
# Returns the next event in the stream without moving the stream's pointer forward
|
99
|
-
#
|
100
|
-
# @raise [EndOfStreamError] If the end of the stream has been reached
|
101
|
-
# @return [DomainEventMessage]
|
102
|
-
def peek
|
103
|
-
assert_valid
|
104
|
-
@events.at @next_index
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
67
|
+
end # DomainEventStream
|
68
|
+
end # Domain
|
108
69
|
end
|
data/lib/synapse/domain.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
require 'synapse/domain/errors'
|
2
|
+
|
1
3
|
require 'synapse/domain/aggregate_root'
|
2
4
|
require 'synapse/domain/event_container'
|
3
5
|
require 'synapse/domain/message'
|
4
6
|
require 'synapse/domain/message_builder'
|
5
7
|
require 'synapse/domain/stream'
|
8
|
+
require 'synapse/domain/simple_stream'
|
@@ -1,6 +1,106 @@
|
|
1
1
|
module Synapse
|
2
2
|
module EventSourcing
|
3
3
|
# Mixin for the root entity of an aggregate that is initialized from a historical event stream
|
4
|
+
#
|
5
|
+
# = Handling events
|
6
|
+
#
|
7
|
+
# For ease of use, the event mapping DSL is included in event-sourced aggregates. This includes
|
8
|
+
# both the aggregate root and any entities that are a member of the aggregate.
|
9
|
+
#
|
10
|
+
# This mapping DSL can be used to map handlers for events that have been applied to the
|
11
|
+
# aggregate
|
12
|
+
#
|
13
|
+
# class Order
|
14
|
+
# include Synapse::EventSourcing::AggregateRoot
|
15
|
+
#
|
16
|
+
# def add_item(sku, quantity, unit_price)
|
17
|
+
# # Do business logic validation here
|
18
|
+
# apply(new ItemsAddedToOrder(id, sku, quantity, unit_price))
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# map_event ItemsAddedToOrder do |event|
|
22
|
+
# @value = @value + (event.quantity * event.unit_price)
|
23
|
+
# @line_items.add(OrderLineItem.new(sku, quantity, unit_price))
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# Events applied to the aggregate root are cascaded down to any child entities of the aggregate
|
28
|
+
# root. Events applied to a child entity are actually applied to the aggregate root's event
|
29
|
+
# container, and are then cascaded down to the child entities.
|
30
|
+
#
|
31
|
+
# = Initialization
|
32
|
+
#
|
33
|
+
# An aggregate can be initialized in three ways:
|
34
|
+
#
|
35
|
+
# - Created by the caller using new, usually to create a new aggregate
|
36
|
+
# - Allocated, then initialized from an event stream
|
37
|
+
# - Deserialized from a snapshot
|
38
|
+
#
|
39
|
+
# Because of the different ways the aggregate can be created, it is necessary to separate out
|
40
|
+
# the logic needed to create data structures required by the aggregate to function.
|
41
|
+
#
|
42
|
+
# Ruby does not support method overloading, so there is a hook provided to do any logic
|
43
|
+
# necessary to instantiate the aggregate, such as creating collections.
|
44
|
+
#
|
45
|
+
# class Order
|
46
|
+
# include Synapse::EventSourcing::AggregateRoot
|
47
|
+
#
|
48
|
+
# def initialize(id, max_value)
|
49
|
+
# pre_initialize
|
50
|
+
# apply(OrderCreated.new(id, max_value))
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# protected
|
54
|
+
#
|
55
|
+
# def pre_initialize
|
56
|
+
# @line_items = Set.new
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# Instead of using the pre-initialization hook, you can also do lazy initialization.
|
61
|
+
#
|
62
|
+
# class Order
|
63
|
+
# include Synapse::EventSourcing::AggregateRoot
|
64
|
+
#
|
65
|
+
# def line_items
|
66
|
+
# @line_items ||= Set.new
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# = Snapshots
|
71
|
+
#
|
72
|
+
# When the event stream for an aggregate becomes large, it can put a drain on the application
|
73
|
+
# to have to load the entire stream of an aggregate just to perform a single operation upon
|
74
|
+
# it.
|
75
|
+
#
|
76
|
+
# To solve this issue, Synapse supports snapshotting aggregates. The built-in way to snapshot
|
77
|
+
# is simply to serialize the aggregate root, since it contains entire state of the aggregate.
|
78
|
+
#
|
79
|
+
# When using a marshalling serializer, like the built-in Ruby marshaller or ones like Oj and
|
80
|
+
# Ox, no work needs to be done to prepare the aggregate for serialization. However, when using
|
81
|
+
# the attribute serializer, you have to treat snapshots as mementos.
|
82
|
+
#
|
83
|
+
# class Order
|
84
|
+
# include Synapse::EventSourcing::AggregateRoot
|
85
|
+
#
|
86
|
+
# def attributes
|
87
|
+
# line_items = @line_items.map { |li| li.attributes }
|
88
|
+
# { id: @id, value: @value }
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# # Note that this is called after #allocate
|
92
|
+
# def attributes=(attributes)
|
93
|
+
# @id = attributes[:id]
|
94
|
+
# @value = attributes[:value]
|
95
|
+
# # Yeah, it's pretty ugly to support this
|
96
|
+
# @line_items = attributes[:line_items].map do |line_item|
|
97
|
+
# OrderLineItem.allocate.tap { |li| li.attributes = line_item }
|
98
|
+
# end
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# It would be nice to have something like XStream to make serialization easier, but as far as
|
103
|
+
# I can tell, there's nothing even close to it for Ruby.
|
4
104
|
module AggregateRoot
|
5
105
|
extend ActiveSupport::Concern
|
6
106
|
include Domain::AggregateRoot
|
@@ -119,6 +219,6 @@ module Synapse
|
|
119
219
|
entity.handle_aggregate_event event
|
120
220
|
end
|
121
221
|
end
|
122
|
-
end
|
123
|
-
end
|
222
|
+
end # AggregateRoot
|
223
|
+
end # EventSourcing
|
124
224
|
end
|
@@ -28,7 +28,7 @@ module Synapse
|
|
28
28
|
if aggregate.nil?
|
29
29
|
aggregate = super aggregate_id, expected_version
|
30
30
|
elsif aggregate.deleted?
|
31
|
-
raise AggregateDeletedError
|
31
|
+
raise AggregateDeletedError.new type_identifier, aggregate_id
|
32
32
|
end
|
33
33
|
|
34
34
|
register_listener CacheClearingUnitOfWorkListener.new aggregate_id, @cache
|
@@ -2,17 +2,16 @@ module Synapse
|
|
2
2
|
module EventSourcing
|
3
3
|
# Base mixin for a member of an aggregate which has its state mutated by events that are
|
4
4
|
# applied to the aggregate
|
5
|
+
#
|
6
|
+
# @see AggregateRoot
|
7
|
+
# @see Entity
|
5
8
|
module Member
|
6
9
|
extend ActiveSupport::Concern
|
7
10
|
|
8
11
|
included do
|
9
|
-
# @return [Mapper::Mapping]
|
10
|
-
class_attribute :command_mapper
|
11
|
-
|
12
12
|
# @return [Mapper::Mapping]
|
13
13
|
class_attribute :event_mapper
|
14
14
|
|
15
|
-
self.command_mapper = Mapping::Mapper.new false
|
16
15
|
self.event_mapper = Mapping::Mapper.new true
|
17
16
|
end
|
18
17
|
|
@@ -33,15 +32,6 @@ module Synapse
|
|
33
32
|
@child_entities ||= Set.new
|
34
33
|
end
|
35
34
|
|
36
|
-
# @see Mapper#map
|
37
|
-
# @param [Class] type
|
38
|
-
# @param [Object...] args
|
39
|
-
# @param [Proc] block
|
40
|
-
# @return [undefined]
|
41
|
-
def map_command(type, *args, &block)
|
42
|
-
commnad_mapper.map type, *args, &block
|
43
|
-
end
|
44
|
-
|
45
35
|
# @see Mapper#map
|
46
36
|
# @param [Class] type
|
47
37
|
# @param [Object...] args
|