event_sourcery 0.14.0 → 0.15.0
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.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +11 -0
 - data/README.md +3 -1
 - data/lib/event_sourcery/aggregate_root.rb +80 -1
 - data/lib/event_sourcery/config.rb +32 -5
 - data/lib/event_sourcery/errors.rb +8 -3
 - data/lib/event_sourcery/event.rb +61 -0
 - data/lib/event_sourcery/event_body_serializer.rb +16 -0
 - data/lib/event_sourcery/event_processing/error_handlers/constant_retry.rb +9 -2
 - data/lib/event_sourcery/event_processing/error_handlers/error_handler.rb +4 -1
 - data/lib/event_sourcery/event_processing/error_handlers/exponential_backoff_retry.rb +15 -6
 - data/lib/event_sourcery/event_processing/error_handlers/no_retry.rb +1 -0
 - data/lib/event_sourcery/event_processing/esp_process.rb +6 -1
 - data/lib/event_sourcery/event_processing/esp_runner.rb +2 -0
 - data/lib/event_sourcery/event_processing/event_stream_processor.rb +42 -1
 - data/lib/event_sourcery/event_processing/event_stream_processor_registry.rb +16 -0
 - data/lib/event_sourcery/event_store/poll_waiter.rb +10 -0
 - data/lib/event_sourcery/event_store/signal_handling_subscription_master.rb +7 -0
 - data/lib/event_sourcery/event_store/subscription.rb +15 -0
 - data/lib/event_sourcery/memory/config.rb +36 -0
 - data/lib/event_sourcery/memory/event_store.rb +59 -0
 - data/lib/event_sourcery/memory/projector.rb +25 -0
 - data/lib/event_sourcery/memory/tracker.rb +19 -0
 - data/lib/event_sourcery/repository.rb +28 -0
 - data/lib/event_sourcery/rspec/event_store_shared_examples.rb +59 -74
 - data/lib/event_sourcery/version.rb +2 -1
 - data/lib/event_sourcery.rb +28 -0
 - metadata +5 -3
 
| 
         @@ -5,22 +5,38 @@ module EventSourcery 
     | 
|
| 
       5 
5 
     | 
    
         
             
                    @processors = []
         
     | 
| 
       6 
6 
     | 
    
         
             
                  end
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
      
 8 
     | 
    
         
            +
                  # Register the class of the Event Stream Processor.
         
     | 
| 
      
 9 
     | 
    
         
            +
                  #
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # @param klass [Class] the class to register
         
     | 
| 
       8 
11 
     | 
    
         
             
                  def register(klass)
         
     | 
| 
       9 
12 
     | 
    
         
             
                    @processors << klass
         
     | 
| 
       10 
13 
     | 
    
         
             
                  end
         
     | 
| 
       11 
14 
     | 
    
         | 
| 
      
 15 
     | 
    
         
            +
                  # Find a registered processor by its name.
         
     | 
| 
      
 16 
     | 
    
         
            +
                  #
         
     | 
| 
      
 17 
     | 
    
         
            +
                  # @param processor_name [String] name of the processor you're looking for
         
     | 
| 
      
 18 
     | 
    
         
            +
                  #
         
     | 
| 
      
 19 
     | 
    
         
            +
                  # @return [ESProcess, nil] the found processor object or nil
         
     | 
| 
       12 
20 
     | 
    
         
             
                  def find(processor_name)
         
     | 
| 
       13 
21 
     | 
    
         
             
                    @processors.find do |processor|
         
     | 
| 
       14 
22 
     | 
    
         
             
                      processor.processor_name == processor_name
         
     | 
| 
       15 
23 
     | 
    
         
             
                    end
         
     | 
| 
       16 
24 
     | 
    
         
             
                  end
         
     | 
| 
       17 
25 
     | 
    
         | 
| 
      
 26 
     | 
    
         
            +
                  # Find a registered processor by its type.
         
     | 
| 
      
 27 
     | 
    
         
            +
                  #
         
     | 
| 
      
 28 
     | 
    
         
            +
                  # @param constant [String] name of the constant the processor has included
         
     | 
| 
      
 29 
     | 
    
         
            +
                  #
         
     | 
| 
      
 30 
     | 
    
         
            +
                  # @return [ESProcess, nil] the found processor object or nil
         
     | 
| 
       18 
31 
     | 
    
         
             
                  def by_type(constant)
         
     | 
| 
       19 
32 
     | 
    
         
             
                    @processors.select do |processor|
         
     | 
| 
       20 
33 
     | 
    
         
             
                      processor.included_modules.include?(constant)
         
     | 
| 
       21 
34 
     | 
    
         
             
                    end
         
     | 
| 
       22 
35 
     | 
    
         
             
                  end
         
     | 
| 
       23 
36 
     | 
    
         | 
| 
      
 37 
     | 
    
         
            +
                  # Returns an array of all the registered processors.
         
     | 
| 
      
 38 
     | 
    
         
            +
                  #
         
     | 
| 
      
 39 
     | 
    
         
            +
                  # @return [Array] of all the processors that are registered
         
     | 
| 
       24 
40 
     | 
    
         
             
                  def all
         
     | 
| 
       25 
41 
     | 
    
         
             
                    @processors
         
     | 
| 
       26 
42 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -1,10 +1,20 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module EventSourcery
         
     | 
| 
       2 
2 
     | 
    
         
             
              module EventStore
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
                # This class provides a basic poll waiter implementation that calls the provided block and sleeps for the specified interval, to be used by a {Subscription}.
         
     | 
| 
       3 
5 
     | 
    
         
             
                class PollWaiter
         
     | 
| 
      
 6 
     | 
    
         
            +
                  #
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # @param interval [Float] Optional. Will default to `0.5`
         
     | 
| 
       4 
8 
     | 
    
         
             
                  def initialize(interval: 0.5)
         
     | 
| 
       5 
9 
     | 
    
         
             
                    @interval = interval
         
     | 
| 
       6 
10 
     | 
    
         
             
                  end
         
     | 
| 
       7 
11 
     | 
    
         | 
| 
      
 12 
     | 
    
         
            +
                  # Start polling. Call the provided block and sleep. Repeat until `:stop` is thrown (usually via a subscription master).
         
     | 
| 
      
 13 
     | 
    
         
            +
                  #
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # @param block [Proc] code block to be called when polling
         
     | 
| 
      
 15 
     | 
    
         
            +
                  #
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # @see SignalHandlingSubscriptionMaster
         
     | 
| 
      
 17 
     | 
    
         
            +
                  # @see Subscription
         
     | 
| 
       8 
18 
     | 
    
         
             
                  def poll(&block)
         
     | 
| 
       9 
19 
     | 
    
         
             
                    catch(:stop) do
         
     | 
| 
       10 
20 
     | 
    
         
             
                      loop do
         
     | 
| 
         @@ -1,11 +1,18 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module EventSourcery
         
     | 
| 
       2 
2 
     | 
    
         
             
              module EventStore
         
     | 
| 
      
 3 
     | 
    
         
            +
                # Manages shutdown signals and facilitate graceful shutdowns of subscriptions.
         
     | 
| 
      
 4 
     | 
    
         
            +
                #
         
     | 
| 
      
 5 
     | 
    
         
            +
                # @see Subscription
         
     | 
| 
       3 
6 
     | 
    
         
             
                class SignalHandlingSubscriptionMaster
         
     | 
| 
       4 
7 
     | 
    
         
             
                  def initialize
         
     | 
| 
       5 
8 
     | 
    
         
             
                    @shutdown_requested = false
         
     | 
| 
       6 
9 
     | 
    
         
             
                    setup_graceful_shutdown
         
     | 
| 
       7 
10 
     | 
    
         
             
                  end
         
     | 
| 
       8 
11 
     | 
    
         | 
| 
      
 12 
     | 
    
         
            +
                  # If a shutdown has been requested through a `TERM` or `INT` signal, this will throw a `:stop`
         
     | 
| 
      
 13 
     | 
    
         
            +
                  # (generally) causing a Subscription to stop listening for events.
         
     | 
| 
      
 14 
     | 
    
         
            +
                  #
         
     | 
| 
      
 15 
     | 
    
         
            +
                  # @see Subscription#start
         
     | 
| 
       9 
16 
     | 
    
         
             
                  def shutdown_if_requested
         
     | 
| 
       10 
17 
     | 
    
         
             
                    throw :stop if @shutdown_requested
         
     | 
| 
       11 
18 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -1,6 +1,17 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module EventSourcery
         
     | 
| 
       2 
2 
     | 
    
         
             
              module EventStore
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
                # This allows Event Stream Processors (ESPs) to subscribe to an event store, and be notified when new events are
         
     | 
| 
      
 5 
     | 
    
         
            +
                # added.
         
     | 
| 
       3 
6 
     | 
    
         
             
                class Subscription
         
     | 
| 
      
 7 
     | 
    
         
            +
                  #
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # @param event_store Event store to source events from
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # @param poll_waiter Poll waiter instance used (such as {EventStore::PollWaiter}) for polling the event store
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # @param from_event_id [Integer] Start reading events from this event ID
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # @param event_types [Array] Optional. If specified, only subscribe to given event types.
         
     | 
| 
      
 12 
     | 
    
         
            +
                  # @param on_new_events [Proc] Code block to be executed when new events are received
         
     | 
| 
      
 13 
     | 
    
         
            +
                  # @param subscription_master A subscription master instance (such as {EventStore::SignalHandlingSubscriptionMaster}) which orchestrates a graceful shutdown of the subscription, if one is requested.
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # @param events_table_name [Symbol] Optional. Defaults to `:events`
         
     | 
| 
       4 
15 
     | 
    
         
             
                  def initialize(event_store:,
         
     | 
| 
       5 
16 
     | 
    
         
             
                                 poll_waiter:,
         
     | 
| 
       6 
17 
     | 
    
         
             
                                 from_event_id:,
         
     | 
| 
         @@ -17,6 +28,10 @@ module EventSourcery 
     | 
|
| 
       17 
28 
     | 
    
         
             
                    @current_event_id = from_event_id - 1
         
     | 
| 
       18 
29 
     | 
    
         
             
                  end
         
     | 
| 
       19 
30 
     | 
    
         | 
| 
      
 31 
     | 
    
         
            +
                  # Start listening for new events. This method will continue to listen for new events until a shutdown is requested
         
     | 
| 
      
 32 
     | 
    
         
            +
                  # through the subscription_master provided.
         
     | 
| 
      
 33 
     | 
    
         
            +
                  #
         
     | 
| 
      
 34 
     | 
    
         
            +
                  # @see EventStore::SignalHandlingSubscriptionMaster
         
     | 
| 
       20 
35 
     | 
    
         
             
                  def start
         
     | 
| 
       21 
36 
     | 
    
         
             
                    catch(:stop) do
         
     | 
| 
       22 
37 
     | 
    
         
             
                      @poll_waiter.poll do
         
     | 
| 
         @@ -0,0 +1,36 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module EventSourcery
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Memory
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Config
         
     | 
| 
      
 4 
     | 
    
         
            +
                  attr_accessor :event_tracker,
         
     | 
| 
      
 5 
     | 
    
         
            +
                                :event_store,
         
     | 
| 
      
 6 
     | 
    
         
            +
                                :event_source,
         
     | 
| 
      
 7 
     | 
    
         
            +
                                :event_sink
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  def initialize
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @event_tracker = Memory::Tracker.new
         
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  def event_store
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @event_store ||= EventStore.new
         
     | 
| 
      
 15 
     | 
    
         
            +
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  def event_source
         
     | 
| 
      
 18 
     | 
    
         
            +
                    @event_source ||= ::EventSourcery::EventStore::EventSource.new(event_store)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  def event_sink
         
     | 
| 
      
 22 
     | 
    
         
            +
                    @event_sink ||= ::EventSourcery::EventStore::EventSink.new(event_store)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def self.configure
         
     | 
| 
      
 28 
     | 
    
         
            +
                  yield config
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def self.config
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @config ||= Config.new
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              end
         
     | 
| 
      
 36 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -1,13 +1,26 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module EventSourcery
         
     | 
| 
       2 
2 
     | 
    
         
             
              module Memory
         
     | 
| 
      
 3 
     | 
    
         
            +
                # In-memory event store.
         
     | 
| 
      
 4 
     | 
    
         
            +
                #
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Note: This is not persisted and is generally used for testing.
         
     | 
| 
       3 
6 
     | 
    
         
             
                class EventStore
         
     | 
| 
       4 
7 
     | 
    
         
             
                  include EventSourcery::EventStore::EachByRange
         
     | 
| 
       5 
8 
     | 
    
         | 
| 
      
 9 
     | 
    
         
            +
                  #
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # @param events [Array] Optional. Collection of events
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # @param event_builder Optional. Event builder instance. Will default to {Config#event_builder}
         
     | 
| 
       6 
12 
     | 
    
         
             
                  def initialize(events = [], event_builder: EventSourcery.config.event_builder)
         
     | 
| 
       7 
13 
     | 
    
         
             
                    @events = events
         
     | 
| 
       8 
14 
     | 
    
         
             
                    @event_builder = event_builder
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @listeners = []
         
     | 
| 
       9 
16 
     | 
    
         
             
                  end
         
     | 
| 
       10 
17 
     | 
    
         | 
| 
      
 18 
     | 
    
         
            +
                  # Store given events to the in-memory store
         
     | 
| 
      
 19 
     | 
    
         
            +
                  #
         
     | 
| 
      
 20 
     | 
    
         
            +
                  # @param event_or_events Event(s) to be stored
         
     | 
| 
      
 21 
     | 
    
         
            +
                  # @param expected_version [Optional] Expected version for the aggregate. This is the version the caller of this method expect the aggregate to be in. If it's different from the expected version a {EventSourcery::ConcurrencyError} will be raised. Defaults to nil.
         
     | 
| 
      
 22 
     | 
    
         
            +
                  # @raise EventSourcery::ConcurrencyError
         
     | 
| 
      
 23 
     | 
    
         
            +
                  # @return Boolean
         
     | 
| 
       11 
24 
     | 
    
         
             
                  def sink(event_or_events, expected_version: nil)
         
     | 
| 
       12 
25 
     | 
    
         
             
                    events = Array(event_or_events)
         
     | 
| 
       13 
26 
     | 
    
         
             
                    ensure_one_aggregate(events)
         
     | 
| 
         @@ -30,9 +43,17 @@ module EventSourcery 
     | 
|
| 
       30 
43 
     | 
    
         
             
                      )
         
     | 
| 
       31 
44 
     | 
    
         
             
                    end
         
     | 
| 
       32 
45 
     | 
    
         | 
| 
      
 46 
     | 
    
         
            +
                    project_events(events)
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
       33 
48 
     | 
    
         
             
                    true
         
     | 
| 
       34 
49 
     | 
    
         
             
                  end
         
     | 
| 
       35 
50 
     | 
    
         | 
| 
      
 51 
     | 
    
         
            +
                  # Retrieve a subset of events
         
     | 
| 
      
 52 
     | 
    
         
            +
                  #
         
     | 
| 
      
 53 
     | 
    
         
            +
                  # @param id Starting from event ID
         
     | 
| 
      
 54 
     | 
    
         
            +
                  # @param event_types [Array] Optional. If supplied, only retrieve events of given type(s).
         
     | 
| 
      
 55 
     | 
    
         
            +
                  # @param limit [Integer] Optional. Number of events to retrieve (starting from the given event ID).
         
     | 
| 
      
 56 
     | 
    
         
            +
                  # @return Array
         
     | 
| 
       36 
57 
     | 
    
         
             
                  def get_next_from(id, event_types: nil, limit: 1000)
         
     | 
| 
       37 
58 
     | 
    
         
             
                    events = if event_types.nil?
         
     | 
| 
       38 
59 
     | 
    
         
             
                      @events
         
     | 
| 
         @@ -43,6 +64,10 @@ module EventSourcery 
     | 
|
| 
       43 
64 
     | 
    
         
             
                    events.select { |event| event.id >= id }.first(limit)
         
     | 
| 
       44 
65 
     | 
    
         
             
                  end
         
     | 
| 
       45 
66 
     | 
    
         | 
| 
      
 67 
     | 
    
         
            +
                  # Retrieve the latest event ID
         
     | 
| 
      
 68 
     | 
    
         
            +
                  #
         
     | 
| 
      
 69 
     | 
    
         
            +
                  # @param event_types [Array] Optional. If supplied, only retrieve events of given type(s).
         
     | 
| 
      
 70 
     | 
    
         
            +
                  # @return Integer
         
     | 
| 
       46 
71 
     | 
    
         
             
                  def latest_event_id(event_types: nil)
         
     | 
| 
       47 
72 
     | 
    
         
             
                    events = if event_types.nil?
         
     | 
| 
       48 
73 
     | 
    
         
             
                      @events
         
     | 
| 
         @@ -53,24 +78,58 @@ module EventSourcery 
     | 
|
| 
       53 
78 
     | 
    
         
             
                    events.empty? ? 0 : events.last.id
         
     | 
| 
       54 
79 
     | 
    
         
             
                  end
         
     | 
| 
       55 
80 
     | 
    
         | 
| 
      
 81 
     | 
    
         
            +
                  # Get all events for the given aggregate
         
     | 
| 
      
 82 
     | 
    
         
            +
                  #
         
     | 
| 
      
 83 
     | 
    
         
            +
                  # @param id [String] Aggregate ID (UUID as String)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  # @return Array
         
     | 
| 
       56 
85 
     | 
    
         
             
                  def get_events_for_aggregate_id(id)
         
     | 
| 
       57 
86 
     | 
    
         
             
                    stringified_id = id.to_str
         
     | 
| 
       58 
87 
     | 
    
         
             
                    @events.select { |event| event.aggregate_id == stringified_id }
         
     | 
| 
       59 
88 
     | 
    
         
             
                  end
         
     | 
| 
       60 
89 
     | 
    
         | 
| 
      
 90 
     | 
    
         
            +
                  # Next version for the aggregate
         
     | 
| 
      
 91 
     | 
    
         
            +
                  #
         
     | 
| 
      
 92 
     | 
    
         
            +
                  # @param aggregate_id [String] Aggregate ID (UUID as String)
         
     | 
| 
      
 93 
     | 
    
         
            +
                  # @return Integer
         
     | 
| 
       61 
94 
     | 
    
         
             
                  def next_version(aggregate_id)
         
     | 
| 
       62 
95 
     | 
    
         
             
                    version_for(aggregate_id) + 1
         
     | 
| 
       63 
96 
     | 
    
         
             
                  end
         
     | 
| 
       64 
97 
     | 
    
         | 
| 
      
 98 
     | 
    
         
            +
                  # Current version for the aggregate
         
     | 
| 
      
 99 
     | 
    
         
            +
                  #
         
     | 
| 
      
 100 
     | 
    
         
            +
                  # @param aggregate_id [String] Aggregate ID (UUID as String)
         
     | 
| 
      
 101 
     | 
    
         
            +
                  # @return Integer
         
     | 
| 
       65 
102 
     | 
    
         
             
                  def version_for(aggregate_id)
         
     | 
| 
       66 
103 
     | 
    
         
             
                    get_events_for_aggregate_id(aggregate_id).count
         
     | 
| 
       67 
104 
     | 
    
         
             
                  end
         
     | 
| 
       68 
105 
     | 
    
         | 
| 
      
 106 
     | 
    
         
            +
                  # Ensure all events have the same aggregate
         
     | 
| 
      
 107 
     | 
    
         
            +
                  #
         
     | 
| 
      
 108 
     | 
    
         
            +
                  # @param events [Array] Collection of events
         
     | 
| 
      
 109 
     | 
    
         
            +
                  # @raise AtomicWriteToMultipleAggregatesNotSupported
         
     | 
| 
       69 
110 
     | 
    
         
             
                  def ensure_one_aggregate(events)
         
     | 
| 
       70 
111 
     | 
    
         
             
                    unless events.map(&:aggregate_id).uniq.one?
         
     | 
| 
       71 
112 
     | 
    
         
             
                      raise AtomicWriteToMultipleAggregatesNotSupported
         
     | 
| 
       72 
113 
     | 
    
         
             
                    end
         
     | 
| 
       73 
114 
     | 
    
         
             
                  end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                  # Adds a listener or listeners to the memory store.
         
     | 
| 
      
 117 
     | 
    
         
            +
                  # the #process(event) method will execute whenever an event is emitted
         
     | 
| 
      
 118 
     | 
    
         
            +
                  #
         
     | 
| 
      
 119 
     | 
    
         
            +
                  # @param listener A single listener or an array of listeners
         
     | 
| 
      
 120 
     | 
    
         
            +
                  def add_listeners(listeners)
         
     | 
| 
      
 121 
     | 
    
         
            +
                    @listeners.concat(Array(listeners))
         
     | 
| 
      
 122 
     | 
    
         
            +
                  end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                  private
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                  def project_events(events)
         
     | 
| 
      
 127 
     | 
    
         
            +
                    events.each do |event|
         
     | 
| 
      
 128 
     | 
    
         
            +
                      @listeners.each do |listener|
         
     | 
| 
      
 129 
     | 
    
         
            +
                        listener.process(event)
         
     | 
| 
      
 130 
     | 
    
         
            +
                      end
         
     | 
| 
      
 131 
     | 
    
         
            +
                    end
         
     | 
| 
      
 132 
     | 
    
         
            +
                  end
         
     | 
| 
       74 
133 
     | 
    
         
             
                end
         
     | 
| 
       75 
134 
     | 
    
         
             
              end
         
     | 
| 
       76 
135 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,25 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module EventSourcery
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Memory
         
     | 
| 
      
 3 
     | 
    
         
            +
                module Projector
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
                  def self.included(base)
         
     | 
| 
      
 6 
     | 
    
         
            +
                    base.include(EventSourcery::EventProcessing::EventStreamProcessor)
         
     | 
| 
      
 7 
     | 
    
         
            +
                    base.include(InstanceMethods)
         
     | 
| 
      
 8 
     | 
    
         
            +
                    base.class_eval do
         
     | 
| 
      
 9 
     | 
    
         
            +
                      alias_method :project, :process
         
     | 
| 
      
 10 
     | 
    
         
            +
                      class << self
         
     | 
| 
      
 11 
     | 
    
         
            +
                        alias_method :project, :process
         
     | 
| 
      
 12 
     | 
    
         
            +
                        alias_method :projects_events, :processes_events
         
     | 
| 
      
 13 
     | 
    
         
            +
                        alias_method :projector_name, :processor_name
         
     | 
| 
      
 14 
     | 
    
         
            +
                      end
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  module InstanceMethods
         
     | 
| 
      
 19 
     | 
    
         
            +
                    def initialize(tracker: EventSourcery::Memory.config.event_tracker)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      @tracker = tracker
         
     | 
| 
      
 21 
     | 
    
         
            +
                    end
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -1,24 +1,43 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module EventSourcery
         
     | 
| 
       2 
2 
     | 
    
         
             
              module Memory
         
     | 
| 
      
 3 
     | 
    
         
            +
                # Being able to know where you're at when reading an event stream
         
     | 
| 
      
 4 
     | 
    
         
            +
                # is important. In here are mechanisms to do so.
         
     | 
| 
       3 
5 
     | 
    
         
             
                class Tracker
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # Tracking where you're in an event stream at via an in memory hash.
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # Note: This is not persisted and is generally used for testing.
         
     | 
| 
       4 
8 
     | 
    
         
             
                  def initialize
         
     | 
| 
       5 
9 
     | 
    
         
             
                    @state = Hash.new(0)
         
     | 
| 
       6 
10 
     | 
    
         
             
                  end
         
     | 
| 
       7 
11 
     | 
    
         | 
| 
      
 12 
     | 
    
         
            +
                  # Register a new processor to track or
         
     | 
| 
      
 13 
     | 
    
         
            +
                  # reset an existing tracker's last processed event id.
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # Will start from 0.
         
     | 
| 
      
 15 
     | 
    
         
            +
                  #
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # @param processor_name [String] the name of the processor to track
         
     | 
| 
       8 
17 
     | 
    
         
             
                  def setup(processor_name)
         
     | 
| 
       9 
18 
     | 
    
         
             
                    @state[processor_name.to_s] = 0
         
     | 
| 
       10 
19 
     | 
    
         
             
                  end
         
     | 
| 
       11 
20 
     | 
    
         | 
| 
      
 21 
     | 
    
         
            +
                  # Update the given processor name to the given event id number.
         
     | 
| 
      
 22 
     | 
    
         
            +
                  #
         
     | 
| 
      
 23 
     | 
    
         
            +
                  # @param processor_name [String] the name of the processor to update
         
     | 
| 
      
 24 
     | 
    
         
            +
                  # @param event_id [Int] the number of the event to update
         
     | 
| 
       12 
25 
     | 
    
         
             
                  def processed_event(processor_name, event_id)
         
     | 
| 
       13 
26 
     | 
    
         
             
                    @state[processor_name.to_s] = event_id
         
     | 
| 
       14 
27 
     | 
    
         
             
                  end
         
     | 
| 
       15 
28 
     | 
    
         | 
| 
       16 
29 
     | 
    
         
             
                  alias :reset_last_processed_event_id :setup
         
     | 
| 
       17 
30 
     | 
    
         | 
| 
      
 31 
     | 
    
         
            +
                  # Find the last processed event id for a given processor name.
         
     | 
| 
      
 32 
     | 
    
         
            +
                  #
         
     | 
| 
      
 33 
     | 
    
         
            +
                  # @return [Int] the last event id that the given processor has processed
         
     | 
| 
       18 
34 
     | 
    
         
             
                  def last_processed_event_id(processor_name)
         
     | 
| 
       19 
35 
     | 
    
         
             
                    @state[processor_name.to_s]
         
     | 
| 
       20 
36 
     | 
    
         
             
                  end
         
     | 
| 
       21 
37 
     | 
    
         | 
| 
      
 38 
     | 
    
         
            +
                  # Returns an array of all the processors that are being tracked.
         
     | 
| 
      
 39 
     | 
    
         
            +
                  #
         
     | 
| 
      
 40 
     | 
    
         
            +
                  # @return [Array] an array of names of the tracked processors
         
     | 
| 
       22 
41 
     | 
    
         
             
                  def tracked_processors
         
     | 
| 
       23 
42 
     | 
    
         
             
                    @state.keys
         
     | 
| 
       24 
43 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -1,20 +1,48 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module EventSourcery
         
     | 
| 
      
 2 
     | 
    
         
            +
              # This class provides a set of methods to help load and save aggregate instances.
         
     | 
| 
      
 3 
     | 
    
         
            +
              #
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Refer to {https://github.com/envato/event_sourcery_todo_app/blob/31e200f4a2a65be5d847a66a20e23a334d43086b/app/commands/todo/amend.rb#L26 EventSourceryTodoApp}
         
     | 
| 
      
 5 
     | 
    
         
            +
              # for a more complete example.
         
     | 
| 
      
 6 
     | 
    
         
            +
              # @example
         
     | 
| 
      
 7 
     | 
    
         
            +
              #  repository = EventSourcery::Repository.new(
         
     | 
| 
      
 8 
     | 
    
         
            +
              #    event_source: EventSourceryTodoApp.event_source,
         
     | 
| 
      
 9 
     | 
    
         
            +
              #    event_sink: EventSourceryTodoApp.event_sink,
         
     | 
| 
      
 10 
     | 
    
         
            +
              #  )
         
     | 
| 
      
 11 
     | 
    
         
            +
              #
         
     | 
| 
      
 12 
     | 
    
         
            +
              #  aggregate = repository.load(Aggregates::Todo, command.aggregate_id)
         
     | 
| 
      
 13 
     | 
    
         
            +
              #  aggregate.amend(command.payload)
         
     | 
| 
      
 14 
     | 
    
         
            +
              #  repository.save(aggregate)
         
     | 
| 
       2 
15 
     | 
    
         
             
              class Repository
         
     | 
| 
      
 16 
     | 
    
         
            +
                # Create a new instance of the repository and load an aggregate instance
         
     | 
| 
      
 17 
     | 
    
         
            +
                #
         
     | 
| 
      
 18 
     | 
    
         
            +
                # @param aggregate_class Aggregate type
         
     | 
| 
      
 19 
     | 
    
         
            +
                # @param aggregate_id [Integer] ID of the aggregate instance to be loaded
         
     | 
| 
      
 20 
     | 
    
         
            +
                # @param event_source event source to be used for loading the events for the aggregate
         
     | 
| 
      
 21 
     | 
    
         
            +
                # @param event_sink event sink to be used for saving any new events for the aggregate
         
     | 
| 
       3 
22 
     | 
    
         
             
                def self.load(aggregate_class, aggregate_id, event_source:, event_sink:)
         
     | 
| 
       4 
23 
     | 
    
         
             
                  new(event_source: event_source, event_sink: event_sink)
         
     | 
| 
       5 
24 
     | 
    
         
             
                    .load(aggregate_class, aggregate_id)
         
     | 
| 
       6 
25 
     | 
    
         
             
                end
         
     | 
| 
       7 
26 
     | 
    
         | 
| 
      
 27 
     | 
    
         
            +
                # @param event_source event source to be used for loading the events for the aggregate
         
     | 
| 
      
 28 
     | 
    
         
            +
                # @param event_sink event sink to be used for saving any new events for the aggregate
         
     | 
| 
       8 
29 
     | 
    
         
             
                def initialize(event_source:, event_sink:)
         
     | 
| 
       9 
30 
     | 
    
         
             
                  @event_source = event_source
         
     | 
| 
       10 
31 
     | 
    
         
             
                  @event_sink = event_sink
         
     | 
| 
       11 
32 
     | 
    
         
             
                end
         
     | 
| 
       12 
33 
     | 
    
         | 
| 
      
 34 
     | 
    
         
            +
                # Load an aggregate instance
         
     | 
| 
      
 35 
     | 
    
         
            +
                #
         
     | 
| 
      
 36 
     | 
    
         
            +
                # @param aggregate_class Aggregate type
         
     | 
| 
      
 37 
     | 
    
         
            +
                # @param aggregate_id [Integer] ID of the aggregate instance to be loaded
         
     | 
| 
       13 
38 
     | 
    
         
             
                def load(aggregate_class, aggregate_id)
         
     | 
| 
       14 
39 
     | 
    
         
             
                  events = event_source.get_events_for_aggregate_id(aggregate_id)
         
     | 
| 
       15 
40 
     | 
    
         
             
                  aggregate_class.new(aggregate_id, events)
         
     | 
| 
       16 
41 
     | 
    
         
             
                end
         
     | 
| 
       17 
42 
     | 
    
         | 
| 
      
 43 
     | 
    
         
            +
                # Save any new events/changes in the provided aggregate to the event sink
         
     | 
| 
      
 44 
     | 
    
         
            +
                #
         
     | 
| 
      
 45 
     | 
    
         
            +
                # @param aggregate An aggregate instance to be saved
         
     | 
| 
       18 
46 
     | 
    
         
             
                def save(aggregate)
         
     | 
| 
       19 
47 
     | 
    
         
             
                  new_events = aggregate.changes
         
     | 
| 
       20 
48 
     | 
    
         
             
                  if new_events.any?
         
     | 
| 
         @@ -1,25 +1,18 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            RSpec.shared_examples 'an event store' do
         
     | 
| 
       2 
     | 
    
         
            -
               
     | 
| 
      
 2 
     | 
    
         
            +
              TestEvent2 = Class.new(EventSourcery::Event)
         
     | 
| 
      
 3 
     | 
    
         
            +
              UserSignedUp = Class.new(EventSourcery::Event)
         
     | 
| 
      
 4 
     | 
    
         
            +
              ItemRejected = Class.new(EventSourcery::Event)
         
     | 
| 
      
 5 
     | 
    
         
            +
              Type1 = Class.new(EventSourcery::Event)
         
     | 
| 
      
 6 
     | 
    
         
            +
              Type2 = Class.new(EventSourcery::Event)
         
     | 
| 
      
 7 
     | 
    
         
            +
              BillingDetailsProvided = Class.new(EventSourcery::Event)
         
     | 
| 
       3 
8 
     | 
    
         | 
| 
       4 
     | 
    
         
            -
               
     | 
| 
       5 
     | 
    
         
            -
                            id: nil, version: 1, created_at: nil, uuid: SecureRandom.uuid,
         
     | 
| 
       6 
     | 
    
         
            -
                            correlation_id: SecureRandom.uuid, causation_id: SecureRandom.uuid)
         
     | 
| 
       7 
     | 
    
         
            -
                EventSourcery::Event.new(id: id,
         
     | 
| 
       8 
     | 
    
         
            -
                                         aggregate_id: aggregate_id,
         
     | 
| 
       9 
     | 
    
         
            -
                                         type: type,
         
     | 
| 
       10 
     | 
    
         
            -
                                         body: body,
         
     | 
| 
       11 
     | 
    
         
            -
                                         version: version,
         
     | 
| 
       12 
     | 
    
         
            -
                                         created_at: created_at,
         
     | 
| 
       13 
     | 
    
         
            -
                                         uuid: uuid,
         
     | 
| 
       14 
     | 
    
         
            -
                                         correlation_id: correlation_id,
         
     | 
| 
       15 
     | 
    
         
            -
                                         causation_id: causation_id)
         
     | 
| 
       16 
     | 
    
         
            -
              end
         
     | 
| 
      
 9 
     | 
    
         
            +
              let(:aggregate_id) { SecureRandom.uuid }
         
     | 
| 
       17 
10 
     | 
    
         | 
| 
       18 
11 
     | 
    
         
             
              describe '#sink' do
         
     | 
| 
       19 
12 
     | 
    
         
             
                it 'assigns auto incrementing event IDs' do
         
     | 
| 
       20 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       21 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       22 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
      
 13 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))
         
     | 
| 
      
 14 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))
         
     | 
| 
      
 15 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))
         
     | 
| 
       23 
16 
     | 
    
         
             
                  events = event_store.get_next_from(1)
         
     | 
| 
       24 
17 
     | 
    
         
             
                  expect(events.count).to eq 3
         
     | 
| 
       25 
18 
     | 
    
         
             
                  expect(events.map(&:id)).to eq [1, 2, 3]
         
     | 
| 
         @@ -27,40 +20,40 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       27 
20 
     | 
    
         | 
| 
       28 
21 
     | 
    
         
             
                it 'assigns UUIDs' do
         
     | 
| 
       29 
22 
     | 
    
         
             
                  uuid = SecureRandom.uuid
         
     | 
| 
       30 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
      
 23 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid, uuid: uuid))
         
     | 
| 
       31 
24 
     | 
    
         
             
                  event = event_store.get_next_from(1).first
         
     | 
| 
       32 
25 
     | 
    
         
             
                  expect(event.uuid).to eq uuid
         
     | 
| 
       33 
26 
     | 
    
         
             
                end
         
     | 
| 
       34 
27 
     | 
    
         | 
| 
       35 
28 
     | 
    
         
             
                it 'returns true' do
         
     | 
| 
       36 
     | 
    
         
            -
                  expect(event_store.sink( 
     | 
| 
      
 29 
     | 
    
         
            +
                  expect(event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))).to eq true
         
     | 
| 
       37 
30 
     | 
    
         
             
                end
         
     | 
| 
       38 
31 
     | 
    
         | 
| 
       39 
32 
     | 
    
         
             
                it 'serializes the event body' do
         
     | 
| 
       40 
33 
     | 
    
         
             
                  time = Time.now
         
     | 
| 
       41 
     | 
    
         
            -
                  event =  
     | 
| 
      
 34 
     | 
    
         
            +
                  event = ItemAdded.new(aggregate_id: SecureRandom.uuid, body: { 'time' => time })
         
     | 
| 
       42 
35 
     | 
    
         
             
                  expect(event_store.sink(event)).to eq true
         
     | 
| 
       43 
36 
     | 
    
         
             
                  expect(event_store.get_next_from(1, limit: 1).first.body).to eq('time' => time.iso8601)
         
     | 
| 
       44 
37 
     | 
    
         
             
                end
         
     | 
| 
       45 
38 
     | 
    
         | 
| 
       46 
39 
     | 
    
         
             
                it 'saves the causation_id' do
         
     | 
| 
       47 
40 
     | 
    
         
             
                  causation_id = SecureRandom.uuid
         
     | 
| 
       48 
     | 
    
         
            -
                  event =  
     | 
| 
      
 41 
     | 
    
         
            +
                  event = ItemAdded.new(aggregate_id: SecureRandom.uuid, causation_id: causation_id)
         
     | 
| 
       49 
42 
     | 
    
         
             
                  event_store.sink(event)
         
     | 
| 
       50 
43 
     | 
    
         
             
                  expect(event_store.get_next_from(1, limit: 1).first.causation_id).to eq(causation_id)
         
     | 
| 
       51 
44 
     | 
    
         
             
                end
         
     | 
| 
       52 
45 
     | 
    
         | 
| 
       53 
46 
     | 
    
         
             
                it 'saves the correlation_id' do
         
     | 
| 
       54 
47 
     | 
    
         
             
                  correlation_id = SecureRandom.uuid
         
     | 
| 
       55 
     | 
    
         
            -
                  event =  
     | 
| 
      
 48 
     | 
    
         
            +
                  event = ItemAdded.new(aggregate_id: SecureRandom.uuid, correlation_id: correlation_id)
         
     | 
| 
       56 
49 
     | 
    
         
             
                  event_store.sink(event)
         
     | 
| 
       57 
50 
     | 
    
         
             
                  expect(event_store.get_next_from(1, limit: 1).first.correlation_id).to eq(correlation_id)
         
     | 
| 
       58 
51 
     | 
    
         
             
                end
         
     | 
| 
       59 
52 
     | 
    
         | 
| 
       60 
53 
     | 
    
         
             
                it 'writes multiple events' do
         
     | 
| 
       61 
     | 
    
         
            -
                  event_store.sink([ 
     | 
| 
       62 
     | 
    
         
            -
                                     
     | 
| 
       63 
     | 
    
         
            -
                                     
     | 
| 
      
 54 
     | 
    
         
            +
                  event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: {e: 1}),
         
     | 
| 
      
 55 
     | 
    
         
            +
                                    ItemAdded.new(aggregate_id: aggregate_id, body: {e: 2}),
         
     | 
| 
      
 56 
     | 
    
         
            +
                                    ItemAdded.new(aggregate_id: aggregate_id, body: {e: 3})])
         
     | 
| 
       64 
57 
     | 
    
         
             
                  events = event_store.get_next_from(1)
         
     | 
| 
       65 
58 
     | 
    
         
             
                  expect(events.count).to eq 3
         
     | 
| 
       66 
59 
     | 
    
         
             
                  expect(events.map(&:id)).to eq [1, 2, 3]
         
     | 
| 
         @@ -69,11 +62,11 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       69 
62 
     | 
    
         
             
                end
         
     | 
| 
       70 
63 
     | 
    
         | 
| 
       71 
64 
     | 
    
         
             
                it 'sets the correct aggregates version' do
         
     | 
| 
       72 
     | 
    
         
            -
                  event_store.sink([ 
     | 
| 
       73 
     | 
    
         
            -
                                     
     | 
| 
      
 65 
     | 
    
         
            +
                  event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: {e: 1}),
         
     | 
| 
      
 66 
     | 
    
         
            +
                                    ItemAdded.new(aggregate_id: aggregate_id, body: {e: 2})])
         
     | 
| 
       74 
67 
     | 
    
         
             
                  # this will throw a unique constrain error if the aggregate version was not set correctly ^
         
     | 
| 
       75 
     | 
    
         
            -
                  event_store.sink([ 
     | 
| 
       76 
     | 
    
         
            -
                                     
     | 
| 
      
 68 
     | 
    
         
            +
                  event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: {e: 1}),
         
     | 
| 
      
 69 
     | 
    
         
            +
                                    ItemAdded.new(aggregate_id: aggregate_id, body: {e: 2})])
         
     | 
| 
       77 
70 
     | 
    
         
             
                  events = event_store.get_next_from(1)
         
     | 
| 
       78 
71 
     | 
    
         
             
                  expect(events.count).to eq 4
         
     | 
| 
       79 
72 
     | 
    
         
             
                  expect(events.map(&:id)).to eq [1, 2, 3, 4]
         
     | 
| 
         @@ -81,41 +74,37 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       81 
74 
     | 
    
         | 
| 
       82 
75 
     | 
    
         
             
                context 'with no existing aggregate stream' do
         
     | 
| 
       83 
76 
     | 
    
         
             
                  it 'saves an event' do
         
     | 
| 
       84 
     | 
    
         
            -
                    event =  
     | 
| 
       85 
     | 
    
         
            -
                                      type: :test_event_2,
         
     | 
| 
       86 
     | 
    
         
            -
                                      body: { 'my' => 'data' })
         
     | 
| 
      
 77 
     | 
    
         
            +
                    event = TestEvent2.new(aggregate_id: aggregate_id, body: { 'my' => 'data' })
         
     | 
| 
       87 
78 
     | 
    
         
             
                    event_store.sink(event)
         
     | 
| 
       88 
79 
     | 
    
         
             
                    events = event_store.get_next_from(1)
         
     | 
| 
       89 
80 
     | 
    
         
             
                    expect(events.count).to eq 1
         
     | 
| 
       90 
81 
     | 
    
         
             
                    expect(events.first.id).to eq 1
         
     | 
| 
       91 
82 
     | 
    
         
             
                    expect(events.first.aggregate_id).to eq aggregate_id
         
     | 
| 
       92 
     | 
    
         
            -
                    expect(events.first.type).to eq ' 
     | 
| 
      
 83 
     | 
    
         
            +
                    expect(events.first.type).to eq 'test_event2'
         
     | 
| 
       93 
84 
     | 
    
         
             
                    expect(events.first.body).to eq({ 'my' => 'data' }) # should we symbolize keys when hydrating events?
         
     | 
| 
       94 
85 
     | 
    
         
             
                  end
         
     | 
| 
       95 
86 
     | 
    
         
             
                end
         
     | 
| 
       96 
87 
     | 
    
         | 
| 
       97 
88 
     | 
    
         
             
                context 'with an existing aggregate stream' do
         
     | 
| 
       98 
89 
     | 
    
         
             
                  before do
         
     | 
| 
       99 
     | 
    
         
            -
                    event_store.sink( 
     | 
| 
      
 90 
     | 
    
         
            +
                    event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
         
     | 
| 
       100 
91 
     | 
    
         
             
                  end
         
     | 
| 
       101 
92 
     | 
    
         | 
| 
       102 
93 
     | 
    
         
             
                  it 'saves an event' do
         
     | 
| 
       103 
     | 
    
         
            -
                    event =  
     | 
| 
       104 
     | 
    
         
            -
                                     type: :test_event_2,
         
     | 
| 
       105 
     | 
    
         
            -
                                     body: { 'my' => 'data' })
         
     | 
| 
      
 94 
     | 
    
         
            +
                    event = TestEvent2.new(aggregate_id: aggregate_id, body: { 'my' => 'data' })
         
     | 
| 
       106 
95 
     | 
    
         
             
                    event_store.sink(event)
         
     | 
| 
       107 
96 
     | 
    
         
             
                    events = event_store.get_next_from(1)
         
     | 
| 
       108 
97 
     | 
    
         
             
                    expect(events.count).to eq 2
         
     | 
| 
       109 
98 
     | 
    
         
             
                    expect(events.last.id).to eq 2
         
     | 
| 
       110 
99 
     | 
    
         
             
                    expect(events.last.aggregate_id).to eq aggregate_id
         
     | 
| 
       111 
     | 
    
         
            -
                    expect(events.last.type).to eq : 
     | 
| 
      
 100 
     | 
    
         
            +
                    expect(events.last.type).to eq :test_event2.to_s # shouldn't you get back what you put in, a symbol?
         
     | 
| 
       112 
101 
     | 
    
         
             
                    expect(events.last.body).to eq({ 'my' => 'data' }) # should we symbolize keys when hydrating events?
         
     | 
| 
       113 
102 
     | 
    
         
             
                  end
         
     | 
| 
       114 
103 
     | 
    
         
             
                end
         
     | 
| 
       115 
104 
     | 
    
         | 
| 
       116 
105 
     | 
    
         
             
                it 'correctly inserts created at times when inserting multiple events atomically' do
         
     | 
| 
       117 
106 
     | 
    
         
             
                  time = Time.parse('2016-10-14T00:00:00.646191Z')
         
     | 
| 
       118 
     | 
    
         
            -
                  event_store.sink([ 
     | 
| 
      
 107 
     | 
    
         
            +
                  event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, created_at: nil), ItemAdded.new(aggregate_id: aggregate_id, created_at: time)])
         
     | 
| 
       119 
108 
     | 
    
         
             
                  created_ats = event_store.get_next_from(0).map(&:created_at)
         
     | 
| 
       120 
109 
     | 
    
         
             
                  expect(created_ats.map(&:class)).to eq [Time, Time]
         
     | 
| 
       121 
110 
     | 
    
         
             
                  expect(created_ats.last).to eq time
         
     | 
| 
         @@ -123,22 +112,22 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       123 
112 
     | 
    
         | 
| 
       124 
113 
     | 
    
         
             
                it 'raises an error if the events given are for more than one aggregate' do
         
     | 
| 
       125 
114 
     | 
    
         
             
                  expect {
         
     | 
| 
       126 
     | 
    
         
            -
                    event_store.sink([ 
     | 
| 
      
 115 
     | 
    
         
            +
                    event_store.sink([ItemAdded.new(aggregate_id: aggregate_id), ItemAdded.new(aggregate_id: SecureRandom.uuid)])
         
     | 
| 
       127 
116 
     | 
    
         
             
                  }.to raise_error(EventSourcery::AtomicWriteToMultipleAggregatesNotSupported)
         
     | 
| 
       128 
117 
     | 
    
         
             
                end
         
     | 
| 
       129 
118 
     | 
    
         
             
              end
         
     | 
| 
       130 
119 
     | 
    
         | 
| 
       131 
120 
     | 
    
         
             
              describe '#get_next_from' do
         
     | 
| 
       132 
121 
     | 
    
         
             
                it 'gets a subset of events' do
         
     | 
| 
       133 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       134 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
      
 122 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
         
     | 
| 
      
 123 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
         
     | 
| 
       135 
124 
     | 
    
         
             
                  expect(event_store.get_next_from(1, limit: 1).map(&:id)).to eq [1]
         
     | 
| 
       136 
125 
     | 
    
         
             
                  expect(event_store.get_next_from(2, limit: 1).map(&:id)).to eq [2]
         
     | 
| 
       137 
126 
     | 
    
         
             
                  expect(event_store.get_next_from(1, limit: 2).map(&:id)).to eq [1, 2]
         
     | 
| 
       138 
127 
     | 
    
         
             
                end
         
     | 
| 
       139 
128 
     | 
    
         | 
| 
       140 
129 
     | 
    
         
             
                it 'returns the event as expected' do
         
     | 
| 
       141 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
      
 130 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: aggregate_id, body: { 'my' => 'data' }))
         
     | 
| 
       142 
131 
     | 
    
         
             
                  event = event_store.get_next_from(1, limit: 1).first
         
     | 
| 
       143 
132 
     | 
    
         
             
                  expect(event.aggregate_id).to eq aggregate_id
         
     | 
| 
       144 
133 
     | 
    
         
             
                  expect(event.type).to eq 'item_added'
         
     | 
| 
         @@ -147,11 +136,11 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       147 
136 
     | 
    
         
             
                end
         
     | 
| 
       148 
137 
     | 
    
         | 
| 
       149 
138 
     | 
    
         
             
                it 'filters by event type' do
         
     | 
| 
       150 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       151 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       152 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       153 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       154 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
      
 139 
     | 
    
         
            +
                  event_store.sink(UserSignedUp.new(aggregate_id: aggregate_id))
         
     | 
| 
      
 140 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
         
     | 
| 
      
 141 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
         
     | 
| 
      
 142 
     | 
    
         
            +
                  event_store.sink(ItemRejected.new(aggregate_id: aggregate_id))
         
     | 
| 
      
 143 
     | 
    
         
            +
                  event_store.sink(UserSignedUp.new(aggregate_id: aggregate_id))
         
     | 
| 
       155 
144 
     | 
    
         
             
                  events = event_store.get_next_from(1, event_types: ['user_signed_up'])
         
     | 
| 
       156 
145 
     | 
    
         
             
                  expect(events.count).to eq 2
         
     | 
| 
       157 
146 
     | 
    
         
             
                  expect(events.map(&:id)).to eq [1, 5]
         
     | 
| 
         @@ -160,8 +149,8 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       160 
149 
     | 
    
         | 
| 
       161 
150 
     | 
    
         
             
              describe '#latest_event_id' do
         
     | 
| 
       162 
151 
     | 
    
         
             
                it 'returns the latest event id' do
         
     | 
| 
       163 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       164 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
      
 152 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
         
     | 
| 
      
 153 
     | 
    
         
            +
                  event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
         
     | 
| 
       165 
154 
     | 
    
         
             
                  expect(event_store.latest_event_id).to eq 2
         
     | 
| 
       166 
155 
     | 
    
         
             
                end
         
     | 
| 
       167 
156 
     | 
    
         | 
| 
         @@ -173,13 +162,13 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       173 
162 
     | 
    
         | 
| 
       174 
163 
     | 
    
         
             
                context 'with event type filtering' do
         
     | 
| 
       175 
164 
     | 
    
         
             
                  it 'gets the latest event ID for a set of event types' do
         
     | 
| 
       176 
     | 
    
         
            -
                    event_store.sink( 
     | 
| 
       177 
     | 
    
         
            -
                    event_store.sink( 
     | 
| 
       178 
     | 
    
         
            -
                    event_store.sink( 
     | 
| 
      
 165 
     | 
    
         
            +
                    event_store.sink(Type1.new(aggregate_id: aggregate_id))
         
     | 
| 
      
 166 
     | 
    
         
            +
                    event_store.sink(Type1.new(aggregate_id: aggregate_id))
         
     | 
| 
      
 167 
     | 
    
         
            +
                    event_store.sink(Type2.new(aggregate_id: aggregate_id))
         
     | 
| 
       179 
168 
     | 
    
         | 
| 
       180 
     | 
    
         
            -
                    expect(event_store.latest_event_id(event_types: [' 
     | 
| 
       181 
     | 
    
         
            -
                    expect(event_store.latest_event_id(event_types: [' 
     | 
| 
       182 
     | 
    
         
            -
                    expect(event_store.latest_event_id(event_types: [' 
     | 
| 
      
 169 
     | 
    
         
            +
                    expect(event_store.latest_event_id(event_types: ['type1'])).to eq 2
         
     | 
| 
      
 170 
     | 
    
         
            +
                    expect(event_store.latest_event_id(event_types: ['type2'])).to eq 3
         
     | 
| 
      
 171 
     | 
    
         
            +
                    expect(event_store.latest_event_id(event_types: ['type1', 'type2'])).to eq 3
         
     | 
| 
       183 
172 
     | 
    
         
             
                  end
         
     | 
| 
       184 
173 
     | 
    
         
             
                end
         
     | 
| 
       185 
174 
     | 
    
         
             
              end
         
     | 
| 
         @@ -187,9 +176,9 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       187 
176 
     | 
    
         
             
              describe '#get_events_for_aggregate_id' do
         
     | 
| 
       188 
177 
     | 
    
         
             
                RSpec.shared_examples 'gets events for a specific aggregate id' do
         
     | 
| 
       189 
178 
     | 
    
         
             
                  before do
         
     | 
| 
       190 
     | 
    
         
            -
                    event_store.sink( 
     | 
| 
       191 
     | 
    
         
            -
                    event_store.sink( 
     | 
| 
       192 
     | 
    
         
            -
                    event_store.sink( 
     | 
| 
      
 179 
     | 
    
         
            +
                    event_store.sink(ItemAdded.new(aggregate_id: aggregate_id, body: { 'my' => 'body' }))
         
     | 
| 
      
 180 
     | 
    
         
            +
                    event_store.sink(ItemAdded.new(aggregate_id: double(to_str: aggregate_id)))
         
     | 
| 
      
 181 
     | 
    
         
            +
                    event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))
         
     | 
| 
       193 
182 
     | 
    
         
             
                  end
         
     | 
| 
       194 
183 
     | 
    
         | 
| 
       195 
184 
     | 
    
         
             
                  subject(:events) { event_store.get_events_for_aggregate_id(uuid) }
         
     | 
| 
         @@ -219,9 +208,7 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       219 
208 
     | 
    
         
             
              describe '#each_by_range' do
         
     | 
| 
       220 
209 
     | 
    
         
             
                before do
         
     | 
| 
       221 
210 
     | 
    
         
             
                  (1..21).each do |i|
         
     | 
| 
       222 
     | 
    
         
            -
                    event_store.sink( 
     | 
| 
       223 
     | 
    
         
            -
                                               type: 'item_added',
         
     | 
| 
       224 
     | 
    
         
            -
                                               body: {}))
         
     | 
| 
      
 211 
     | 
    
         
            +
                    event_store.sink(ItemAdded.new(aggregate_id: aggregate_id, body: {}))
         
     | 
| 
       225 
212 
     | 
    
         
             
                  end
         
     | 
| 
       226 
213 
     | 
    
         
             
                end
         
     | 
| 
       227 
214 
     | 
    
         | 
| 
         @@ -266,14 +253,14 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       266 
253 
     | 
    
         
             
              end
         
     | 
| 
       267 
254 
     | 
    
         | 
| 
       268 
255 
     | 
    
         
             
              def save_event(expected_version: nil)
         
     | 
| 
       269 
     | 
    
         
            -
                event_store.sink( 
     | 
| 
       270 
     | 
    
         
            -
             
     | 
| 
       271 
     | 
    
         
            -
             
     | 
| 
       272 
     | 
    
         
            -
             
     | 
| 
      
 256 
     | 
    
         
            +
                event_store.sink(
         
     | 
| 
      
 257 
     | 
    
         
            +
                  BillingDetailsProvided.new(aggregate_id: aggregate_id, body: { my_event: 'data' }),
         
     | 
| 
      
 258 
     | 
    
         
            +
                  expected_version: expected_version,
         
     | 
| 
      
 259 
     | 
    
         
            +
                )
         
     | 
| 
       273 
260 
     | 
    
         
             
              end
         
     | 
| 
       274 
261 
     | 
    
         | 
| 
       275 
262 
     | 
    
         
             
              def add_event
         
     | 
| 
       276 
     | 
    
         
            -
                event_store.sink( 
     | 
| 
      
 263 
     | 
    
         
            +
                event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
         
     | 
| 
       277 
264 
     | 
    
         
             
              end
         
     | 
| 
       278 
265 
     | 
    
         | 
| 
       279 
266 
     | 
    
         
             
              def last_event
         
     | 
| 
         @@ -335,17 +322,15 @@ RSpec.shared_examples 'an event store' do 
     | 
|
| 
       335 
322 
     | 
    
         | 
| 
       336 
323 
     | 
    
         
             
                it 'allows overriding the created_at timestamp for events' do
         
     | 
| 
       337 
324 
     | 
    
         
             
                  time = Time.parse('2016-10-14T00:00:00.646191Z')
         
     | 
| 
       338 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       339 
     | 
    
         
            -
             
     | 
| 
       340 
     | 
    
         
            -
             
     | 
| 
       341 
     | 
    
         
            -
                                             created_at: time))
         
     | 
| 
      
 325 
     | 
    
         
            +
                  event_store.sink(BillingDetailsProvided.new(aggregate_id: aggregate_id,
         
     | 
| 
      
 326 
     | 
    
         
            +
                                                              body: { my_event: 'data' },
         
     | 
| 
      
 327 
     | 
    
         
            +
                                                              created_at: time))
         
     | 
| 
       342 
328 
     | 
    
         
             
                  expect(last_event.created_at).to eq time
         
     | 
| 
       343 
329 
     | 
    
         
             
                end
         
     | 
| 
       344 
330 
     | 
    
         | 
| 
       345 
331 
     | 
    
         
             
                it "sets a created_at time when one isn't provided in the event" do
         
     | 
| 
       346 
     | 
    
         
            -
                  event_store.sink( 
     | 
| 
       347 
     | 
    
         
            -
             
     | 
| 
       348 
     | 
    
         
            -
                                             body: { my_event: 'data' }))
         
     | 
| 
      
 332 
     | 
    
         
            +
                  event_store.sink(BillingDetailsProvided.new(aggregate_id: aggregate_id,
         
     | 
| 
      
 333 
     | 
    
         
            +
                                                              body: { my_event: 'data' }))
         
     | 
| 
       349 
334 
     | 
    
         
             
                  expect(last_event.created_at).to be_instance_of(Time)
         
     | 
| 
       350 
335 
     | 
    
         
             
                end
         
     | 
| 
       351 
336 
     | 
    
         
             
              end
         
     |