ruby_event_store 0.30.0 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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