ruby_event_store 0.30.0 → 0.31.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.
@@ -0,0 +1,37 @@
1
+ module RubyEventStore
2
+ class CorrelatedCommands
3
+
4
+ def initialize(event_store, command_bus)
5
+ @event_store = event_store
6
+ @command_bus = command_bus
7
+ end
8
+
9
+ class MiniEvent < Struct.new(:correlation_id, :message_id)
10
+ end
11
+
12
+ def call(command)
13
+ if (correlation_id = event_store.metadata[:correlation_id]) && (causation_id = event_store.metadata[:causation_id])
14
+ command.correlate_with(MiniEvent.new(
15
+ correlation_id,
16
+ causation_id,
17
+ )) if command.respond_to?(:correlate_with)
18
+ event_store.with_metadata(
19
+ causation_id: command.message_id,
20
+ ) do
21
+ command_bus.call(command)
22
+ end
23
+ else
24
+ event_store.with_metadata(
25
+ correlation_id: command.message_id,
26
+ causation_id: command.message_id,
27
+ ) do
28
+ command_bus.call(command)
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :event_store, :command_bus
36
+ end
37
+ end
@@ -1,22 +1,42 @@
1
1
  require 'securerandom'
2
2
 
3
3
  module RubyEventStore
4
+
5
+ # Data structure representing an event
4
6
  class Event
7
+ # Instantiates a new event
8
+ #
9
+ # @param event_id [String] event id
10
+ # @param data [Hash] event data which belong to your application domain
11
+ # @param metadata [Hash] event metadata which are technical and not
12
+ # part of your domain such as remote_ip, request_id, correlation_id,
13
+ # causation_id etc.
14
+ # @return [Event]
5
15
  def initialize(event_id: SecureRandom.uuid, metadata: nil, data: nil)
6
16
  @event_id = event_id.to_s
7
17
  @metadata = Metadata.new(metadata.to_h)
8
18
  @data = data.to_h
9
19
  end
20
+
10
21
  attr_reader :event_id, :metadata, :data
11
22
 
23
+ # Event id
24
+ # @return [String]
12
25
  def message_id
13
26
  event_id
14
27
  end
15
28
 
29
+ # Type of event. Used when matching with subscribed handlers.
30
+ # @return [String]
16
31
  def type
17
32
  self.class.name
18
33
  end
19
34
 
35
+ # Returns a hash representation of the event.
36
+ #
37
+ # Metadata is converted to hash as well
38
+ #
39
+ # @return [Hash] with :event_id, :metadata, :data, :type keys
20
40
  def to_h
21
41
  {
22
42
  event_id: event_id,
@@ -26,20 +46,43 @@ module RubyEventStore
26
46
  }
27
47
  end
28
48
 
49
+ # Timestamp from metadata
50
+ #
51
+ # @return [Time, nil]
29
52
  def timestamp
30
53
  metadata[:timestamp]
31
54
  end
32
55
 
56
+ # Two events are equal if:
57
+ # * they are of the same class
58
+ # * have identical event id
59
+ # * have identical data (verified with eql? method)
60
+ #
61
+ # @param other_event [Event, Object] object to compare
62
+ #
63
+ # Event equality ignores metadata!
64
+ # @return [TrueClass, FalseClass]
33
65
  def ==(other_event)
34
66
  other_event.instance_of?(self.class) &&
35
67
  other_event.event_id.eql?(event_id) &&
36
68
  other_event.data.eql?(data)
37
69
  end
38
70
 
71
+ # @private
39
72
  BIG_VALUE = 0b111111100100000010010010110011101011000101010101001100100110000
40
73
 
41
- # We don't use metadata because == does not use metadata
74
+ # Generates a Fixnum hash value for this object. This function
75
+ # have the property that a.eql?(b) implies a.hash == b.hash.
76
+ #
77
+ # The hash value is used along with eql? by the Hash class to
78
+ # determine if two objects reference the same hash key.
79
+ #
80
+ # This hash is based on
81
+ # * class
82
+ # * event_id
83
+ # * data
42
84
  def hash
85
+ # We don't use metadata because == does not use metadata
43
86
  [
44
87
  self.class,
45
88
  event_id,
@@ -47,22 +90,46 @@ module RubyEventStore
47
90
  ].hash ^ BIG_VALUE
48
91
  end
49
92
 
93
+ # Reads correlation_id from metadata.
94
+ # {http://railseventstore.org/docs/correlation_causation/ Find out more}
95
+ #
96
+ # @return [String, nil]
50
97
  def correlation_id
51
98
  metadata[:correlation_id]
52
99
  end
53
100
 
101
+ # Sets correlation_id in metadata.
102
+ # {http://railseventstore.org/docs/correlation_causation/ Find out more}
103
+ #
104
+ # @param val [String]
105
+ # @return [String]
54
106
  def correlation_id=(val)
55
107
  metadata[:correlation_id] = val
56
108
  end
57
109
 
110
+ # Reads causation_id from metadata.
111
+ # {http://railseventstore.org/docs/correlation_causation/ Find out more}
112
+ #
113
+ # @return [String, nil]
58
114
  def causation_id
59
115
  metadata[:causation_id]
60
116
  end
61
117
 
118
+ # Sets causation_id= in metadata.
119
+ # {http://railseventstore.org/docs/correlation_causation/ Find out more}
120
+ #
121
+ # @param val [String]
122
+ # @return [String]
62
123
  def causation_id=(val)
63
124
  metadata[:causation_id]= val
64
125
  end
65
126
 
127
+ # Sets correlation_id and causation_id in metadata based
128
+ # on correlation_id and message_id of the provided message.
129
+ # {http://railseventstore.org/docs/correlation_causation/ Find out more}
130
+ #
131
+ # @param other_message [Event, Proto, command] message to correlate with. Most likely an event or a command. Must respond to correlation_id and message_id.
132
+ # @return [String] set causation_id
66
133
  def correlate_with(other_message)
67
134
  self.correlation_id = other_message.correlation_id || other_message.message_id
68
135
  self.causation_id = other_message.message_id
@@ -0,0 +1,55 @@
1
+ module RubyEventStore
2
+ class LinkByMetadata
3
+
4
+ def initialize(event_store:, key:, prefix: nil)
5
+ @event_store = event_store
6
+ @key = key
7
+ @prefix = prefix || ["$by", key, nil].join("_")
8
+ end
9
+
10
+ def call(event)
11
+ return unless event.metadata.has_key?(@key)
12
+
13
+ @event_store.link_to_stream(
14
+ [event.message_id],
15
+ stream_name: "#{@prefix}#{event.metadata.fetch(@key)}"
16
+ )
17
+ end
18
+
19
+ end
20
+
21
+ class LinkByCorrelationId < LinkByMetadata
22
+ def initialize(event_store:, prefix: nil)
23
+ super(
24
+ event_store: event_store,
25
+ prefix: prefix,
26
+ key: :correlation_id,
27
+ )
28
+ end
29
+ end
30
+
31
+ class LinkByCausationId < LinkByMetadata
32
+ def initialize(event_store:, prefix: nil)
33
+ super(
34
+ event_store: event_store,
35
+ prefix: prefix,
36
+ key: :causation_id,
37
+ )
38
+ end
39
+ end
40
+
41
+ class LinkByEventType
42
+ def initialize(event_store:, prefix: nil)
43
+ @event_store = event_store
44
+ @prefix = prefix || "$by_type_"
45
+ end
46
+
47
+ def call(event)
48
+ @event_store.link_to_stream(
49
+ [event.message_id],
50
+ stream_name: "#{@prefix}#{event.type}"
51
+ )
52
+ end
53
+ end
54
+
55
+ end
@@ -9,7 +9,6 @@ module RubyEventStore
9
9
  def serialized_record_to_event(record)
10
10
  record
11
11
  end
12
-
13
12
  end
14
13
  end
15
14
  end
@@ -55,19 +55,27 @@ module RubyEventStore
55
55
 
56
56
  def serialized_record_to_event(record)
57
57
  event_type = events_class_remapping.fetch(record.event_type) { record.event_type }
58
- data = Google::Protobuf::DescriptorPool.generated_pool.lookup(event_type).msgclass.decode(record.data)
59
58
  Proto.new(
60
59
  event_id: record.event_id,
61
- data: data,
62
- ).tap do |p|
63
- ProtobufNestedStruct::HashMapStringValue.load(record.metadata).each_with_object(p.metadata) {|(k, v), meta| meta[k.to_sym] = v}
64
- end
60
+ data: load_data(event_type, record.data),
61
+ metadata: load_metadata(record.metadata)
62
+ )
65
63
  end
66
64
 
67
65
  private
68
66
 
69
67
  attr_reader :event_id_getter, :events_class_remapping
70
68
 
69
+ def load_metadata(protobuf_metadata)
70
+ ProtobufNestedStruct::HashMapStringValue.load(protobuf_metadata).each_with_object({}) do |(k, v), meta|
71
+ meta[k.to_sym] = v
72
+ end
73
+ end
74
+
75
+ def load_data(event_type, protobuf_data)
76
+ Google::Protobuf::DescriptorPool.generated_pool.lookup(event_type).msgclass.decode(protobuf_data)
77
+ end
78
+
71
79
  def require_optional_dependency
72
80
  require 'protobuf_nested_struct'
73
81
  rescue LoadError
@@ -1,73 +1,45 @@
1
- require 'concurrent'
2
-
3
1
  module RubyEventStore
4
2
  module PubSub
5
3
  class Broker
6
- DEFAULT_DISPATCHER = Dispatcher.new
7
-
8
- def initialize(dispatcher: DEFAULT_DISPATCHER)
9
- @subscribers = Hash.new {|hsh, key| hsh[key] = [] }
10
- @global_subscribers = []
11
-
12
- @thread_global_subscribers = Concurrent::ThreadLocalVar.new([])
13
- @thread_subscribers = Concurrent::ThreadLocalVar.new do
14
- Hash.new {|hsh, key| hsh[key] = [] }
15
- end
16
-
4
+ def initialize(subscriptions:, dispatcher:)
5
+ @subscriptions = subscriptions
17
6
  @dispatcher = dispatcher
18
7
  end
19
8
 
20
- def add_subscriber(subscriber, event_types)
21
- verify_subscriber(subscriber)
22
- subscribe(subscriber, event_types)
9
+ def call(event, serialized_event)
10
+ subscribers = subscriptions.all_for(event.type)
11
+ subscribers.each do |subscriber|
12
+ dispatcher.call(subscriber, event, serialized_event)
13
+ end
23
14
  end
24
15
 
25
- def add_global_subscriber(subscriber)
26
- verify_subscriber(subscriber)
27
- global_subscribers << subscriber
28
-
29
- ->() { global_subscribers.delete(subscriber) }
16
+ def add_subscription(subscriber, event_types)
17
+ verify_subscription(subscriber)
18
+ subscriptions.add_subscription(subscriber, event_types)
30
19
  end
31
20
 
32
- def add_thread_global_subscriber(subscriber)
33
- verify_subscriber(subscriber)
34
- thread_global_subscribers.value += [subscriber]
35
-
36
- ->() { thread_global_subscribers.value -= [subscriber] }
21
+ def add_global_subscription(subscriber)
22
+ verify_subscription(subscriber)
23
+ subscriptions.add_global_subscription(subscriber)
37
24
  end
38
25
 
39
- def add_thread_subscriber(subscriber, event_types)
40
- verify_subscriber(subscriber)
41
- event_types.each{ |type| thread_subscribers.value[type.to_s] << subscriber }
42
- ->() {event_types.each{ |type| thread_subscribers.value.fetch(type.to_s).delete(subscriber) } }
26
+ def add_thread_subscription(subscriber, event_types)
27
+ verify_subscription(subscriber)
28
+ subscriptions.add_thread_subscription(subscriber, event_types)
43
29
  end
44
30
 
45
- def notify_subscribers(event)
46
- all_subscribers_for(event.type).each do |subscriber|
47
- dispatcher.call(subscriber, event)
48
- end
31
+ def add_thread_global_subscription(subscriber)
32
+ verify_subscription(subscriber)
33
+ subscriptions.add_thread_global_subscription(subscriber)
49
34
  end
50
35
 
51
36
  private
37
+ attr_reader :subscriptions, :dispatcher
52
38
 
53
- def verify_subscriber(subscriber)
54
- raise SubscriberNotExist if subscriber.nil?
39
+ def verify_subscription(subscriber)
40
+ raise SubscriberNotExist, "subscriber must be first argument or block" unless subscriber
55
41
  dispatcher.verify(subscriber)
56
42
  end
57
-
58
- def subscribe(subscriber, event_types)
59
- event_types.each{ |type| subscribers[type.to_s] << subscriber }
60
- ->() {event_types.each{ |type| subscribers.fetch(type.to_s).delete(subscriber) } }
61
- end
62
-
63
- def all_subscribers_for(event_type)
64
- subscribers[event_type] +
65
- global_subscribers +
66
- thread_global_subscribers.value +
67
- thread_subscribers.value[event_type]
68
- end
69
-
70
- attr_reader :dispatcher, :subscribers, :global_subscribers, :thread_global_subscribers, :thread_subscribers
71
43
  end
72
44
  end
73
45
  end
@@ -2,7 +2,7 @@ module RubyEventStore
2
2
  module PubSub
3
3
 
4
4
  class Dispatcher
5
- def call(subscriber, event)
5
+ def call(subscriber, event, _)
6
6
  subscriber = subscriber.new if Class === subscriber
7
7
  subscriber.call(event)
8
8
  end
@@ -0,0 +1,110 @@
1
+ require 'concurrent'
2
+
3
+ module RubyEventStore
4
+ module PubSub
5
+ class Subscriptions
6
+ def initialize
7
+ @local = LocalSubscriptions.new
8
+ @global = GlobalSubscriptions.new
9
+ @thread = ThreadSubscriptions.new
10
+ end
11
+
12
+ def add_subscription(subscriber, event_types)
13
+ local.add(subscriber, event_types)
14
+ end
15
+
16
+ def add_global_subscription(subscriber)
17
+ global.add(subscriber)
18
+ end
19
+
20
+ def add_thread_subscription(subscriber, event_types)
21
+ thread.local.add(subscriber, event_types)
22
+ end
23
+
24
+ def add_thread_global_subscription(subscriber)
25
+ thread.global.add(subscriber)
26
+ end
27
+
28
+ def all_for(event_type)
29
+ [local, global, thread].map{|r| r.all_for(event_type)}.reduce(&:+)
30
+ end
31
+
32
+ private
33
+ attr_reader :local, :global, :thread
34
+
35
+ class ThreadSubscriptions
36
+ def initialize
37
+ @local = ThreadLocalSubscriptions.new
38
+ @global = ThreadGlobalSubscriptions.new
39
+ end
40
+ attr_reader :local, :global
41
+
42
+ def all_for(event_type)
43
+ [global, local].map{|r| r.all_for(event_type)}.reduce(&:+)
44
+ end
45
+ end
46
+
47
+ class LocalSubscriptions
48
+ def initialize
49
+ @subscriptions = Hash.new {|hsh, key| hsh[key] = [] }
50
+ end
51
+
52
+ def add(subscription, event_types)
53
+ event_types.each{ |type| @subscriptions[type.to_s] << subscription }
54
+ ->() {event_types.each{ |type| @subscriptions.fetch(type.to_s).delete(subscription) } }
55
+ end
56
+
57
+ def all_for(event_type)
58
+ @subscriptions[event_type]
59
+ end
60
+ end
61
+
62
+ class GlobalSubscriptions
63
+ def initialize
64
+ @subscriptions = []
65
+ end
66
+
67
+ def add(subscription)
68
+ @subscriptions << subscription
69
+ ->() { @subscriptions.delete(subscription) }
70
+ end
71
+
72
+ def all_for(_event_type)
73
+ @subscriptions
74
+ end
75
+ end
76
+
77
+ class ThreadLocalSubscriptions
78
+ def initialize
79
+ @subscriptions = Concurrent::ThreadLocalVar.new do
80
+ Hash.new {|hsh, key| hsh[key] = [] }
81
+ end
82
+ end
83
+
84
+ def add(subscription, event_types)
85
+ event_types.each{ |type| @subscriptions.value[type.to_s] << subscription }
86
+ ->() {event_types.each{ |type| @subscriptions.value.fetch(type.to_s).delete(subscription) } }
87
+ end
88
+
89
+ def all_for(event_type)
90
+ @subscriptions.value[event_type]
91
+ end
92
+ end
93
+
94
+ class ThreadGlobalSubscriptions
95
+ def initialize
96
+ @subscriptions = Concurrent::ThreadLocalVar.new([])
97
+ end
98
+
99
+ def add(subscription)
100
+ @subscriptions.value += [subscription]
101
+ ->() { @subscriptions.value -= [subscription] }
102
+ end
103
+
104
+ def all_for(_event_type)
105
+ @subscriptions.value
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end