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.
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