synapse-core 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. data/lib/synapse.rb +351 -0
  2. data/lib/synapse/command/command_bus.rb +45 -0
  3. data/lib/synapse/command/command_callback.rb +18 -0
  4. data/lib/synapse/command/command_filter.rb +17 -0
  5. data/lib/synapse/command/command_handler.rb +13 -0
  6. data/lib/synapse/command/dispatch_interceptor.rb +16 -0
  7. data/lib/synapse/command/duplication.rb +43 -0
  8. data/lib/synapse/command/errors.rb +27 -0
  9. data/lib/synapse/command/filters/validation.rb +32 -0
  10. data/lib/synapse/command/gateway.rb +34 -0
  11. data/lib/synapse/command/interceptor_chain.rb +31 -0
  12. data/lib/synapse/command/interceptors/serialization.rb +35 -0
  13. data/lib/synapse/command/message.rb +19 -0
  14. data/lib/synapse/command/rollback_policy.rb +22 -0
  15. data/lib/synapse/command/simple_command_bus.rb +138 -0
  16. data/lib/synapse/command/wiring.rb +47 -0
  17. data/lib/synapse/domain/aggregate_root.rb +121 -0
  18. data/lib/synapse/domain/event_container.rb +127 -0
  19. data/lib/synapse/domain/message.rb +82 -0
  20. data/lib/synapse/domain/message_builder.rb +34 -0
  21. data/lib/synapse/domain/stream.rb +108 -0
  22. data/lib/synapse/duplication.rb +60 -0
  23. data/lib/synapse/errors.rb +13 -0
  24. data/lib/synapse/event_bus/event_bus.rb +40 -0
  25. data/lib/synapse/event_bus/event_listener.rb +16 -0
  26. data/lib/synapse/event_bus/event_listener_proxy.rb +12 -0
  27. data/lib/synapse/event_bus/simple_event_bus.rb +69 -0
  28. data/lib/synapse/event_bus/wiring.rb +23 -0
  29. data/lib/synapse/event_sourcing/aggregate_factory.rb +69 -0
  30. data/lib/synapse/event_sourcing/aggregate_root.rb +104 -0
  31. data/lib/synapse/event_sourcing/conflict_resolver.rb +80 -0
  32. data/lib/synapse/event_sourcing/entity.rb +64 -0
  33. data/lib/synapse/event_sourcing/member.rb +72 -0
  34. data/lib/synapse/event_sourcing/repository.rb +119 -0
  35. data/lib/synapse/event_sourcing/snapshot/count_stream.rb +86 -0
  36. data/lib/synapse/event_sourcing/snapshot/count_trigger.rb +91 -0
  37. data/lib/synapse/event_sourcing/snapshot/taker.rb +73 -0
  38. data/lib/synapse/event_sourcing/storage_listener.rb +34 -0
  39. data/lib/synapse/event_sourcing/stream_decorator.rb +25 -0
  40. data/lib/synapse/event_store/errors.rb +16 -0
  41. data/lib/synapse/event_store/event_store.rb +43 -0
  42. data/lib/synapse/event_store/in_memory.rb +59 -0
  43. data/lib/synapse/event_store/mongo/cursor_event_stream.rb +63 -0
  44. data/lib/synapse/event_store/mongo/event_store.rb +86 -0
  45. data/lib/synapse/event_store/mongo/per_commit_strategy.rb +253 -0
  46. data/lib/synapse/event_store/mongo/per_event_strategy.rb +143 -0
  47. data/lib/synapse/event_store/mongo/storage_strategy.rb +113 -0
  48. data/lib/synapse/event_store/mongo/template.rb +73 -0
  49. data/lib/synapse/identifier.rb +23 -0
  50. data/lib/synapse/message.rb +101 -0
  51. data/lib/synapse/message_builder.rb +38 -0
  52. data/lib/synapse/process_manager/correlation.rb +32 -0
  53. data/lib/synapse/process_manager/correlation_resolver.rb +14 -0
  54. data/lib/synapse/process_manager/correlation_set.rb +58 -0
  55. data/lib/synapse/process_manager/process.rb +71 -0
  56. data/lib/synapse/repository/errors.rb +26 -0
  57. data/lib/synapse/repository/lock_manager.rb +40 -0
  58. data/lib/synapse/repository/locking.rb +97 -0
  59. data/lib/synapse/repository/pessimistic_lock_manager.rb +61 -0
  60. data/lib/synapse/repository/repository.rb +109 -0
  61. data/lib/synapse/serialization/converter.rb +39 -0
  62. data/lib/synapse/serialization/converter/chain.rb +45 -0
  63. data/lib/synapse/serialization/converter/factory.rb +68 -0
  64. data/lib/synapse/serialization/converter/identity.rb +29 -0
  65. data/lib/synapse/serialization/converter/json.rb +31 -0
  66. data/lib/synapse/serialization/converter/ox.rb +31 -0
  67. data/lib/synapse/serialization/errors.rb +12 -0
  68. data/lib/synapse/serialization/lazy_object.rb +61 -0
  69. data/lib/synapse/serialization/message/data.rb +25 -0
  70. data/lib/synapse/serialization/message/metadata.rb +13 -0
  71. data/lib/synapse/serialization/message/serialization_aware.rb +17 -0
  72. data/lib/synapse/serialization/message/serialization_aware_message.rb +66 -0
  73. data/lib/synapse/serialization/message/serialized_message.rb +201 -0
  74. data/lib/synapse/serialization/message/serialized_message_builder.rb +64 -0
  75. data/lib/synapse/serialization/message/serialized_object_cache.rb +50 -0
  76. data/lib/synapse/serialization/message/serializer.rb +47 -0
  77. data/lib/synapse/serialization/revision_resolver.rb +30 -0
  78. data/lib/synapse/serialization/serialized_object.rb +37 -0
  79. data/lib/synapse/serialization/serialized_type.rb +31 -0
  80. data/lib/synapse/serialization/serializer.rb +98 -0
  81. data/lib/synapse/serialization/serializer/marshal.rb +32 -0
  82. data/lib/synapse/serialization/serializer/oj.rb +34 -0
  83. data/lib/synapse/serialization/serializer/ox.rb +31 -0
  84. data/lib/synapse/uow/factory.rb +28 -0
  85. data/lib/synapse/uow/listener.rb +79 -0
  86. data/lib/synapse/uow/listener_collection.rb +93 -0
  87. data/lib/synapse/uow/nesting.rb +262 -0
  88. data/lib/synapse/uow/provider.rb +71 -0
  89. data/lib/synapse/uow/storage_listener.rb +14 -0
  90. data/lib/synapse/uow/transaction_manager.rb +27 -0
  91. data/lib/synapse/uow/uow.rb +178 -0
  92. data/lib/synapse/upcasting/chain.rb +78 -0
  93. data/lib/synapse/upcasting/context.rb +58 -0
  94. data/lib/synapse/upcasting/data.rb +30 -0
  95. data/lib/synapse/upcasting/single_upcaster.rb +57 -0
  96. data/lib/synapse/upcasting/upcaster.rb +55 -0
  97. data/lib/synapse/version.rb +3 -0
  98. data/lib/synapse/wiring/message_wiring.rb +41 -0
  99. data/lib/synapse/wiring/wire.rb +55 -0
  100. data/lib/synapse/wiring/wire_registry.rb +61 -0
  101. data/test/command/duplication_test.rb +54 -0
  102. data/test/command/gateway_test.rb +25 -0
  103. data/test/command/interceptor_chain_test.rb +26 -0
  104. data/test/command/serialization_test.rb +37 -0
  105. data/test/command/simple_command_bus_test.rb +141 -0
  106. data/test/command/validation_test.rb +42 -0
  107. data/test/command/wiring_test.rb +73 -0
  108. data/test/domain/aggregate_root_test.rb +57 -0
  109. data/test/domain/fixtures.rb +31 -0
  110. data/test/domain/message_test.rb +61 -0
  111. data/test/domain/stream_test.rb +35 -0
  112. data/test/duplication_test.rb +40 -0
  113. data/test/event_bus/wiring_test.rb +46 -0
  114. data/test/event_sourcing/aggregate_factory_test.rb +28 -0
  115. data/test/event_sourcing/aggregate_root_test.rb +76 -0
  116. data/test/event_sourcing/entity_test.rb +34 -0
  117. data/test/event_sourcing/fixtures.rb +85 -0
  118. data/test/event_sourcing/repository_test.rb +102 -0
  119. data/test/event_sourcing/snapshot/aggregate_taker_test.rb +39 -0
  120. data/test/event_sourcing/snapshot/deferred_taker_test.rb +19 -0
  121. data/test/event_sourcing/snapshot/integration_test.rb +65 -0
  122. data/test/event_sourcing/storage_listener_test.rb +77 -0
  123. data/test/event_store/in_memory_test.rb +47 -0
  124. data/test/process_manager/correlation_set_test.rb +49 -0
  125. data/test/process_manager/correlation_test.rb +24 -0
  126. data/test/process_manager/process_test.rb +52 -0
  127. data/test/repository/locking_test.rb +101 -0
  128. data/test/serialization/converter/factory_test.rb +33 -0
  129. data/test/serialization/converter/identity_test.rb +17 -0
  130. data/test/serialization/converter/json_test.rb +31 -0
  131. data/test/serialization/converter/ox_test.rb +40 -0
  132. data/test/serialization/fixtures.rb +17 -0
  133. data/test/serialization/lazy_object_test.rb +32 -0
  134. data/test/serialization/message/metadata_test.rb +19 -0
  135. data/test/serialization/message/serialization_aware_message_test.rb +88 -0
  136. data/test/serialization/message/serialized_message_builder_test.rb +41 -0
  137. data/test/serialization/message/serialized_message_test.rb +140 -0
  138. data/test/serialization/message/serializer_test.rb +50 -0
  139. data/test/serialization/revision_resolver_test.rb +12 -0
  140. data/test/serialization/serialized_object_test.rb +36 -0
  141. data/test/serialization/serialized_type_test.rb +27 -0
  142. data/test/serialization/serializer/marshal_test.rb +22 -0
  143. data/test/serialization/serializer/oj_test.rb +24 -0
  144. data/test/serialization/serializer/ox_test.rb +36 -0
  145. data/test/serialization/serializer_test.rb +20 -0
  146. data/test/test_helper.rb +19 -0
  147. data/test/uow/factory_test.rb +23 -0
  148. data/test/uow/outer_commit_listener_test.rb +50 -0
  149. data/test/uow/provider_test.rb +70 -0
  150. data/test/uow/uow_test.rb +337 -0
  151. data/test/upcasting/chain_test.rb +29 -0
  152. data/test/upcasting/fixtures.rb +66 -0
  153. data/test/wiring/wire_registry_test.rb +60 -0
  154. data/test/wiring/wire_test.rb +51 -0
  155. metadata +263 -0
@@ -0,0 +1,43 @@
1
+ module Synapse
2
+ module Command
3
+ # Filter that prevents duplicate commands from reaching the command handlers
4
+ class DuplicationFilter < CommandFilter
5
+ # @param [DuplicationRecorder] recorder
6
+ # @return [undefined]
7
+ def initialize(recorder)
8
+ @recorder = recorder
9
+ end
10
+
11
+ # @param [CommandMessage] command
12
+ # @return [CommandMessage] The command to dispatch on the bus
13
+ def filter(command)
14
+ @recorder.record command
15
+ command
16
+ end
17
+ end
18
+
19
+ # Interceptor that removes commands from the duplication recorder if their execution results
20
+ # in a transient error (like concurrency error) being raised. This way, the same command can
21
+ # be retried by the client or command gateway
22
+ class DuplicationCleanupInterceptor < DispatchInterceptor
23
+ # @param [DuplicationRecorder] recorder
24
+ # @return [undefined]
25
+ def initialize(recorder)
26
+ @recorder = recorder
27
+ end
28
+
29
+ # @param [CommandMessage] command
30
+ # @param [UnitOfWork] unit The current unit of work for this command dispatch
31
+ # @param [InterceptorChain] chain
32
+ # @return [Object] The result of the execution of the command
33
+ def intercept(command, unit, chain)
34
+ begin
35
+ chain.proceed command
36
+ rescue TransientError
37
+ @recorder.forget command
38
+ raise
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ module Synapse
2
+ module Command
3
+ # Raised when an error is raised during the handling of a command
4
+ class CommandExecutionError < SynapseError
5
+ # @return [Exception]
6
+ attr_reader :cause
7
+
8
+ # @param [Exception] cause
9
+ # @return [undefined]
10
+ def initialize(cause)
11
+ @cause = cause
12
+ set_backtrace cause.backtrace
13
+ end
14
+
15
+ # @return [String]
16
+ def inspect
17
+ @cause.inspect
18
+ end
19
+ end
20
+
21
+ # Raised when a command is refused because of structural validation errors
22
+ class CommandValidationError < NonTransientError; end
23
+
24
+ # Raised when a dispatched command has no handler subscribed to it
25
+ class NoHandlerError < NonTransientError; end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ module Synapse
2
+ module Command
3
+ class ActiveModelValidationFilter < CommandFilter
4
+ # @raise [ActiveModelValidationError] If command doesn't pass validation
5
+ # @param [CommandMessage] command
6
+ # @return [CommandMessage] The command to dispatch on the bus
7
+ def filter(command)
8
+ payload = command.payload
9
+
10
+ if payload.respond_to? :valid?
11
+ unless payload.valid?
12
+ raise ActiveModelValidationError, payload.errors
13
+ end
14
+ end
15
+
16
+ command
17
+ end
18
+ end
19
+
20
+ # Raised when a command with ActiveModel doesn't pass validation
21
+ class ActiveModelValidationError < CommandValidationError
22
+ # @return [ActiveModel::Errors]
23
+ attr_reader :errors
24
+
25
+ # @param [ActiveModel::Errors] errors
26
+ # @return [undefined]
27
+ def initialize(errors)
28
+ @errors = errors
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ module Synapse
2
+ module Command
3
+ # Simplified interface to the command bus
4
+ class CommandGateway
5
+ # @param [CommandBus] command_bus
6
+ # @return [undefined]
7
+ def initialize(command_bus)
8
+ @command_bus = command_bus
9
+ end
10
+
11
+ # Fire and forget method of sending a command to the command bus
12
+ #
13
+ # @param [Object] command
14
+ # @return [undefined]
15
+ def send(command)
16
+ @command_bus.dispatch(as_command_message(command))
17
+ end
18
+
19
+ private
20
+
21
+ # @param [Object] command
22
+ # @return [CommandMessage]
23
+ def as_command_message(command)
24
+ unless command.is_a? CommandMessage
25
+ command = CommandMessage.build do |builder|
26
+ builder.payload = command
27
+ end
28
+ end
29
+
30
+ command
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ module Synapse
2
+ module Command
3
+ # Represents a mechanism for controlling the flow through a chain of interceptors that
4
+ # eventually ends in the invocation of the command handler
5
+ #
6
+ # Interceptors can either continue the dispatch of a command by calling {#proceed} or choose
7
+ # to block the dispatch by not calling {#proceed} at all. Interceptors can also replace the
8
+ # command message that will be passed on in the chain.
9
+ class InterceptorChain
10
+ # @param [UnitOfWork] unit The current unit of work for this command dispatch
11
+ # @param [Array] interceptors
12
+ # @param [CommandHandler] handler
13
+ # @return [undefined]
14
+ def initialize(unit, interceptors, handler)
15
+ @unit = unit
16
+ @interceptors = interceptors.each
17
+ @handler = handler
18
+ end
19
+
20
+ # @param [CommandMessage] command The command to proceed with
21
+ # @return [Object] The result of the execution of the command
22
+ def proceed(command)
23
+ begin
24
+ @interceptors.next.intercept command, @unit, self
25
+ rescue StopIteration
26
+ @handler.handle command, @unit
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ module Synapse
2
+ module Command
3
+ # Interceptor that registers a unit of work listener that wraps each event message in a
4
+ # serialization-aware message. This provides optimization in cases where storage (in an event
5
+ # store) and publication (on the event bus) use the same serialization mechansim.
6
+ class SerializationOptimizingInterceptor < DispatchInterceptor
7
+ def initialize
8
+ @listener = SerializationOptimizingListener.new
9
+ end
10
+
11
+ # @param [CommandMessage] command
12
+ # @param [UnitOfWork] unit The current unit of work for this command dispatch
13
+ # @param [InterceptorChain] chain
14
+ # @return [Object] The result of the execution of the command
15
+ def intercept(command, unit, chain)
16
+ unit.register_listener @listener
17
+ chain.proceed command
18
+ end
19
+ end
20
+
21
+ # @api private
22
+ class SerializationOptimizingListener < UnitOfWork::UnitOfWorkListener
23
+ # @param [UnitOfWork] unit
24
+ # @param [EventMessage] event
25
+ # @return [EventMessage]
26
+ def on_event_registered(unit, event)
27
+ if event.is_a? Domain::DomainEventMessage
28
+ Serialization::SerializationAwareDomainEventMessage.decorate event
29
+ else
30
+ Serialization::SerializationAwareEventMessage.decorate event
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+ module Synapse
2
+ module Command
3
+ # Represents an intent to change application state
4
+ class CommandMessage < Message
5
+ # @return [Class]
6
+ def self.builder
7
+ CommandMessageBuilder
8
+ end
9
+ end
10
+
11
+ # Message builder capable of producing CommandMessage instances
12
+ class CommandMessageBuilder < MessageBuilder
13
+ # @return [CommandMessage]
14
+ def build
15
+ CommandMessage.new @id, @metadata, @payload
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ module Synapse
2
+ module Command
3
+ # Represents a mechanism for determining whether or not to rollback the unit of work for a
4
+ # command dispatch in case an exception occurs during the dispatch
5
+ class RollbackPolicy
6
+ # Returns true if the unit of work should be rolled back
7
+ #
8
+ # @param [Exception] exception
9
+ # @return [Boolean]
10
+ def should_rollback(exception); end
11
+ end
12
+
13
+ # Implementation of a rollback policy that performs a rollback on any exception
14
+ class RollbackOnAnyExceptionPolicy < RollbackPolicy
15
+ # @param [Exception] exception
16
+ # @return [Boolean]
17
+ def should_rollback(exception)
18
+ true
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,138 @@
1
+ module Synapse
2
+ module Command
3
+ # Implementation of a command bus that dispatches commands in the calling thread
4
+ # @todo Thread safety?
5
+ class SimpleCommandBus < CommandBus
6
+ # @return [RollbackPolicy]
7
+ attr_accessor :rollback_policy
8
+
9
+ # @return [Array]
10
+ attr_reader :filters
11
+
12
+ # @return [Array]
13
+ attr_reader :interceptors
14
+
15
+ # @param [UnitOfWorkFactory] unit_factory
16
+ # @return [undefined]
17
+ def initialize(unit_factory)
18
+ @handlers = Hash.new
19
+ @filters = Array.new
20
+ @interceptors = Array.new
21
+ @logger = Logging.logger.new self.class
22
+ @rollback_policy = RollbackOnAnyExceptionPolicy.new
23
+ @unit_factory = unit_factory
24
+ end
25
+
26
+ # @param [CommandMessage] command
27
+ # @return [undefined]
28
+ def dispatch(command)
29
+ dispatch_with_callback command, CommandCallback.new
30
+ end
31
+
32
+ # @param [CommandMessage] command
33
+ # @param [CommandCallback] callback
34
+ # @return [undefined]
35
+ def dispatch_with_callback(command, callback)
36
+ begin
37
+ result = perform_dispatch command
38
+ callback.on_success result
39
+ rescue => exception
40
+ backtrace = exception.backtrace.join $/
41
+ @logger.error 'Exception occured while dispatching command [%s] [%s]: %s %s' %
42
+ [command.payload_type, command.id, exception.inspect, backtrace]
43
+
44
+ callback.on_failure exception
45
+ end
46
+ end
47
+
48
+ # @param [Class] command_type
49
+ # @param [CommandHandler] handler
50
+ # @return [undefined]
51
+ def subscribe(command_type, handler)
52
+ if @handlers.has_key? command_type
53
+ current_handler = @handlers.fetch command_type
54
+ @logger.info 'Command handler [%s] is being replaced by [%s] for command type [%s]' %
55
+ [current_handler.class, handler.class, command_type]
56
+ else
57
+ @logger.debug 'Command handler [%s] subscribed to command type [%s]' %
58
+ [handler.class, command_type]
59
+ end
60
+
61
+ @handlers.store command_type, handler
62
+ end
63
+
64
+ # @param [Class] command_type
65
+ # @param [CommandHandler] handler
66
+ # @return [undefined]
67
+ def unsubscribe(command_type, handler)
68
+ if @handlers.has_key? command_type
69
+ current_handler = @handlers.fetch command_type
70
+ if current_handler.equal? handler
71
+ @handlers.delete command_type
72
+
73
+ @logger.debug 'Command handler [%s] unsubscribed from command type [%s]' %
74
+ [handler.class, command_type]
75
+ else
76
+ @logger.info 'Command type [%s] subscribed to handler [%s] not [%s]' %
77
+ [command_type, current_handler.class, handler.class]
78
+ end
79
+ else
80
+ @logger.info 'Command type [%s] not subscribed to any handler' % command_type
81
+ end
82
+ end
83
+
84
+ protected
85
+
86
+ # @raise [CommandExecutionError]
87
+ # If an error occurs during the handling of the command
88
+ # @raise [NoHandlerError]
89
+ # If no handler is subscribed that is capable of handling the command
90
+ # @param [CommandMessage] command
91
+ # @return [Object] The result from the command handler
92
+ def perform_dispatch(command)
93
+ @filters.each do |filter|
94
+ command = filter.filter command
95
+ end
96
+
97
+ handler = handler_for command
98
+ unit = @unit_factory.create
99
+
100
+ chain = InterceptorChain.new unit, @interceptors, handler
101
+
102
+ begin
103
+ @logger.debug 'Dispatching command [%s] [%s] to handler [%s]' %
104
+ [command.id, command.payload_type, handler.class]
105
+
106
+ result = chain.proceed command
107
+ rescue => exception
108
+ if @rollback_policy.should_rollback exception
109
+ @logger.debug 'Unit of work is being rolled back due to rollback policy'
110
+ unit.rollback exception
111
+ else
112
+ @logger.info 'Unit of work is being committed due to rollback policy'
113
+ unit.commit
114
+ end
115
+
116
+ raise CommandExecutionError, exception
117
+ end
118
+
119
+ unit.commit
120
+
121
+ result
122
+ end
123
+
124
+ # @raise [NoHandlerError]
125
+ # @param [CommandMessage] command
126
+ # @return [CommandHandler]
127
+ def handler_for(command)
128
+ type = command.payload_type
129
+
130
+ begin
131
+ @handlers.fetch type
132
+ rescue KeyError
133
+ raise NoHandlerError, 'No handler subscribed for command [%s]' % type
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,47 @@
1
+ module Synapse
2
+ module Command
3
+ # Mixin for a command handler that wishes to use the wiring DSL
4
+ module WiringCommandHandler
5
+ extend ActiveSupport::Concern
6
+ include CommandHandler
7
+ include Wiring::MessageWiring
8
+
9
+ included do
10
+ self.wire_registry = Wiring::WireRegistry.new false
11
+ end
12
+
13
+ # @param [CommandMessage] command
14
+ # @param [UnitOfWork] current_unit Current unit of work
15
+ # @return [Object] The result of handling the given command
16
+ def handle(command, current_unit)
17
+ wire = self.wire_registry.wire_for command.payload_type
18
+
19
+ unless wire
20
+ raise ArgumentError, 'Not capable of handling [%s] commands' % command.payload_type
21
+ end
22
+
23
+ invoke_wire command, wire
24
+ end
25
+
26
+ # Subscribes this handler to the given command bus for any types that have been wired
27
+ #
28
+ # @param [CommandBus] command_bus
29
+ # @return [undefined]
30
+ def subscribe(command_bus)
31
+ self.wire_registry.each_type do |type|
32
+ command_bus.subscribe type, self
33
+ end
34
+ end
35
+
36
+ # Unsubscribes this handler from the given command bus for any types that have been wired
37
+ #
38
+ # @param [CommandBus] command_bus
39
+ # @return [undefined]
40
+ def unsubscribe(command_bus)
41
+ self.wire_registry.each_type do |type|
42
+ command_bus.unsubscribe type, self
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,121 @@
1
+ module Synapse
2
+ module Domain
3
+ # Mixin module for a basic aggregate root
4
+ module AggregateRoot
5
+ # @return [Boolean] True if this aggregate has been marked for deletion
6
+ attr_reader :deleted
7
+
8
+ alias deleted? deleted
9
+
10
+ # @return [Object] The identifier of this aggregate
11
+ attr_reader :id
12
+
13
+ # @return [Integer] The version of this aggregate
14
+ attr_reader :version
15
+
16
+ # Adds a listener that will be notified when this aggregate registers an event to be published
17
+ #
18
+ # If an event registration listener is added after events have already been registered, it
19
+ # will still get a change to process the uncommitted events in this aggregate.
20
+ #
21
+ # @param [#call] listener
22
+ # @return [undefined]
23
+ def add_registration_listener(&listener)
24
+ event_container.add_registration_listener listener
25
+ end
26
+
27
+ # Marks this aggregate as committed by a repository
28
+ # @return [undefined]
29
+ def mark_committed
30
+ if @event_container
31
+ @last_sequence_number = @event_container.last_sequence_number
32
+ @event_container.mark_committed
33
+ end
34
+ end
35
+
36
+ # Returns the number of uncommitted events published by this aggregate
37
+ # @return [Integer]
38
+ def uncommitted_event_count
39
+ unless @event_container
40
+ return 0
41
+ end
42
+
43
+ @event_container.size
44
+ end
45
+
46
+ # Returns a domain event strema containing any uncommitted events published by this aggregate
47
+ # @return [DomainEventStream]
48
+ def uncommitted_events
49
+ unless @event_container
50
+ return SimpleDomainEventStream.new
51
+ end
52
+
53
+ @event_container.to_stream
54
+ end
55
+
56
+ protected
57
+
58
+ # Publishes a domain event with the given payload and metadata (optional)
59
+ #
60
+ # @param [Object] payload
61
+ # Payload of the message; the actual event object
62
+ #
63
+ # @param [Hash] metadata
64
+ # Metadata associated with the event
65
+ #
66
+ # @return [DomainEventMessage] The event that will be committed
67
+ def publish_event(payload, metadata = nil)
68
+ event_container.register_event payload, metadata
69
+ end
70
+
71
+ # Initializes the event container with the given sequence number
72
+ #
73
+ # @param [Integer] last_sequence_number
74
+ # The sequence number of the last committed event for this aggregate
75
+ #
76
+ # @return [undefined]
77
+ def initialize_event_container(last_sequence_number)
78
+ event_container.initialize_sequence_number last_sequence_number
79
+ @last_sequence_number = last_sequence_number >= 0 ? last_sequence_number : nil
80
+ end
81
+
82
+ # Returns the sequence number of the last committed event
83
+ # @return [Integer]
84
+ def last_committed_sequence_number
85
+ unless @event_container
86
+ return @last_sequence_number
87
+ end
88
+
89
+ @event_container.last_committed_sequence_number
90
+ end
91
+
92
+ # Marks this aggregate for deletion by its repository
93
+ # @return [undefined]
94
+ def mark_deleted
95
+ @deleted = true
96
+ end
97
+
98
+ private
99
+
100
+ # Initializes the uncommitted event container for this aggregate, if not already
101
+ #
102
+ # @raise [AggregateIdentifierNotInitializedError] If identifier not set
103
+ # @return [EventContainer]
104
+ def event_container
105
+ unless @event_container
106
+ unless @id
107
+ raise AggregateIdentifierNotInitializedError
108
+ end
109
+
110
+ @event_container = EventContainer.new @id
111
+ @event_container.initialize_sequence_number @last_sequence_number
112
+ end
113
+
114
+ @event_container
115
+ end
116
+ end
117
+
118
+ # Raised when an event is published but the aggregate identifier is not set
119
+ class AggregateIdentifierNotInitializedError < NonTransientError; end
120
+ end
121
+ end