event_store_client 1.0.17 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5eebe6985468ff2cd381dd397f0e30ff5459f3cc783f2f24ec3af8e489e52cd
4
- data.tar.gz: 2d6654fcf4aede64b32f6ae9a9b9390f09ed714917d3cb87157581791f4b97f1
3
+ metadata.gz: 301a92ed47fa04e52d45989042f82571e30eeba810c73b7c0f73af55a95304f0
4
+ data.tar.gz: 0a58c9b4dc19bb56a2f2082b06552de05f39e0983f5c7379a1e058cbe5c65332
5
5
  SHA512:
6
- metadata.gz: 7c08741b831569f05bc6016484ef4f22d36a371c63d57d76ad29fc9ed9ae1fed82e06abd9e511705c9585ef7d5e4868ad41de488822e4b9ade066272f26f4334
7
- data.tar.gz: d7141f9192166b232fbe73f47dfa994ea79f4b155b75ae436f13b2d979b1460341f2829bed97dd3e7388c7f0daf36fad4c08813b823ba90d9bebf10026972754
6
+ metadata.gz: f87b1b7516fdae3a51a23970485ad6d492a50b91550208eee10d3d01530cbda57641941c0dc7a9c2696f25c2f442b87bfc02dc70149fdea499dbb11e02bc8d73
7
+ data.tar.gz: 903f6f6b64725911be55f7d4730f988f1a4596b4f4c8de05c226a77fb6ac477f90063854c0ae2b0981abba5cc78928feebc3aff52c38f6b5863f12408f8bcc84
@@ -15,5 +15,7 @@ require 'event_store_client/deserialized_event'
15
15
 
16
16
  require 'event_store_client/subscription'
17
17
  require 'event_store_client/subscriptions'
18
+ require 'event_store_client/catch_up_subscription'
19
+ require 'event_store_client/catch_up_subscriptions'
18
20
  require 'event_store_client/broker'
19
21
  require 'event_store_client/client'
@@ -7,6 +7,7 @@ require 'event_store_client/adapters/grpc/commands/streams/delete'
7
7
  require 'event_store_client/adapters/grpc/commands/streams/link_to'
8
8
  require 'event_store_client/adapters/grpc/commands/streams/read'
9
9
  require 'event_store_client/adapters/grpc/commands/streams/read_all'
10
+ require 'event_store_client/adapters/grpc/commands/streams/subscribe'
10
11
  require 'event_store_client/adapters/grpc/commands/streams/tombstone'
11
12
 
12
13
  require 'event_store_client/adapters/grpc/commands/persistent_subscriptions/create'
@@ -90,6 +90,18 @@ module EventStoreClient
90
90
  config.error_handler&.call(e)
91
91
  end
92
92
 
93
+ # Subscribe to a stream
94
+ # @param options [Hash] additional options to the request
95
+ # @return - Nothing, it is a blocking operation, yields the given block with event instead
96
+ #
97
+ def subscribe(options = {})
98
+ Commands::Streams::Subscribe.new.call(options) do |event|
99
+ yield event if block_given?
100
+ end
101
+ rescue StandardError => e
102
+ config.error_handler&.call(e)
103
+ end
104
+
93
105
  private
94
106
 
95
107
  # Joins multiple streams into the new one under the given name
@@ -98,10 +110,7 @@ module EventStoreClient
98
110
  # @return Dry::Monads::Result::Success or Dry::Monads::Result::Failure
99
111
  #
100
112
  def join_streams(name, streams)
101
- res = Commands::Projections::Create.new.call(name, streams)
102
- return if res.success?
103
-
104
- Commands::Projections::Update.new.call(name, streams)
113
+ Commands::Projections::Create.new.call(name, streams)
105
114
  end
106
115
 
107
116
  # @api private
@@ -12,7 +12,7 @@ module EventStoreClient
12
12
  def self.inherited(klass)
13
13
  super
14
14
  klass.class_eval do
15
- include Dry::Monads[:try, :result]
15
+ include Dry::Monads[:result]
16
16
 
17
17
  def self.use_request(request_klass)
18
18
  CommandRegistrar.register_request(self, request: request_klass)
@@ -33,11 +33,8 @@ module EventStoreClient
33
33
  }
34
34
  }
35
35
 
36
- res = Try do
37
- service.create(request.new(options: options), metadata: metadata)
38
- end
39
-
40
- res.error? ? res.to_result : Success()
36
+ service.create(request.new(options: options), metadata: metadata)
37
+ Success()
41
38
  rescue ::GRPC::Unknown => e
42
39
  Failure(:conflict) if e.message.include?('Conflict')
43
40
  end
@@ -30,9 +30,7 @@ module EventStoreClient
30
30
  name: name,
31
31
  emit_enabled: true
32
32
  }
33
-
34
33
  service.update(request.new(options: options), metadata: metadata)
35
-
36
34
  Success()
37
35
  rescue ::GRPC::AlreadyExists
38
36
  Failure(:conflict)
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grpc'
4
+ require 'event_store_client/adapters/grpc/generated/streams_pb.rb'
5
+ require 'event_store_client/adapters/grpc/generated/streams_services_pb.rb'
6
+
7
+ require 'event_store_client/configuration'
8
+ require 'event_store_client/adapters/grpc/commands/command'
9
+
10
+ module EventStoreClient
11
+ module GRPC
12
+ module Commands
13
+ module Streams
14
+ class Subscribe < Command
15
+ include Configuration
16
+
17
+ use_request EventStore::Client::Streams::ReadReq
18
+ use_service EventStore::Client::Streams::Streams::Stub
19
+
20
+ StreamNotFound = Class.new(StandardError)
21
+
22
+ def call(options = {})
23
+ opts = options_with_defaults(options)
24
+
25
+ service.read(request.new(options: opts), metadata: metadata).map do |res|
26
+ raise StreamNotFound if res.stream_not_found
27
+
28
+ yield prepared_response(res) if block_given?
29
+ end
30
+ rescue StreamNotFound
31
+ Failure(:not_found)
32
+ end
33
+
34
+ private
35
+
36
+ def prepared_response(res)
37
+ if res.event
38
+ event = res.event.event
39
+ [position(event), deserialize_event(event)] rescue event
40
+ elsif res.checkpoint
41
+ [position(res.checkpoint), nil]
42
+ elsif res.confirmation
43
+ res.confirmation
44
+ end
45
+ end
46
+
47
+ def position(event_or_checkpoint)
48
+ {
49
+ prepare_position: event_or_checkpoint.prepare_position,
50
+ commit_position: event_or_checkpoint.commit_position
51
+ }
52
+ end
53
+
54
+ def read_direction(direction)
55
+ EventStoreClient::ReadDirection.new(direction || 'forwards').to_sym
56
+ end
57
+
58
+ def options_with_defaults(options)
59
+ options[:without_system_events] = true unless options[:without_system_events] == false
60
+ opts = {
61
+ subscription: {},
62
+ read_direction: read_direction(options[:direction]),
63
+ resolve_links: options[:resolve_links] || true,
64
+ uuid_option: {
65
+ string: {}
66
+ }
67
+ }
68
+ if options[:stream]
69
+ opts[:stream] = {
70
+ stream_identifier: {
71
+ streamName: stream
72
+ }
73
+ }
74
+ else
75
+ opts[:all] = options[:all] || default_all_options
76
+ end
77
+ if options[:filter]
78
+ opts[:filter] = options[:filter]
79
+ elsif options[:without_system_events]
80
+ opts[:filter] = {
81
+ event_type: { regex: '^[^$].*' },
82
+ max: 32,
83
+ checkpointIntervalMultiplier: 1000
84
+ }
85
+ else
86
+ opts[:no_filter] = {}
87
+ end
88
+
89
+ options[:start] ||= 0
90
+
91
+ return opts unless options[:stream]
92
+
93
+ if options[:start].zero?
94
+ opts[:stream][:start] = {}
95
+ else
96
+ opts[:stream][:revision] = options[:start]
97
+ end
98
+ opts
99
+ end
100
+
101
+ def default_all_options
102
+ {
103
+ position: {
104
+ commit_position: 0,
105
+ prepare_position: 0
106
+ }
107
+ }
108
+ end
109
+
110
+ def deserialize_event(entry)
111
+ data = (entry.data.nil? || entry.data.empty?) ? '{}' : entry.data
112
+
113
+ metadata =
114
+ JSON.parse(entry.custom_metadata || '{}').merge(
115
+ entry.metadata.to_h || {}
116
+ ).to_json
117
+
118
+ event = EventStoreClient::Event.new(
119
+ id: entry.id.string,
120
+ title: "#{entry.stream_revision}@#{entry.stream_identifier.streamName}",
121
+ type: entry.metadata['type'],
122
+ data: data,
123
+ metadata: metadata
124
+ )
125
+
126
+ config.mapper.deserialize(event)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -6,7 +6,6 @@ require 'event_store_client/adapters/http/commands/persistent_subscriptions/crea
6
6
  require 'event_store_client/adapters/http/commands/persistent_subscriptions/read'
7
7
 
8
8
  require 'event_store_client/adapters/http/commands/projections/create'
9
- require 'event_store_client/adapters/http/commands/projections/update'
10
9
 
11
10
  require 'event_store_client/adapters/http/commands/streams/append'
12
11
  require 'event_store_client/adapters/http/commands/streams/delete'
@@ -137,10 +137,7 @@ module EventStoreClient
137
137
  # @return Dry::Monads::Result::Success or Dry::Monads::Result::Failure
138
138
  #
139
139
  def join_streams(name, streams, options: {})
140
- res = Commands::Projections::Create.new(connection).call(name, streams, options: options)
141
- return res if res.success?
142
-
143
- Commands::Projections::Update.new(connection).call(name, streams, options: options)
140
+ Commands::Projections::Create.new(connection).call(name, streams, options: options)
144
141
  end
145
142
 
146
143
  # @api private
@@ -16,15 +16,12 @@ module EventStoreClient
16
16
  })
17
17
  STRING
18
18
 
19
-
20
- res = connection.call(
19
+ connection.call(
21
20
  :post,
22
21
  "/projections/continuous?name=#{name}&type=js&enabled=yes&emit=true&trackemittedstreams=true", # rubocop:disable Metrics/LineLength
23
22
  body: data,
24
23
  headers: {}
25
24
  )
26
-
27
- (200...300).cover?(res.status) ? Success() : Failure(res)
28
25
  end
29
26
  end
30
27
  end
@@ -6,15 +6,13 @@ module EventStoreClient
6
6
 
7
7
  # Distributes known subscriptions to multiple threads
8
8
  # @param [EventStoreClient::Subscriptions]
9
- # @param wait [Boolean] (Optional) Controls if broker shold block
9
+ # @param wait [Boolean] (Optional) Controls if broker should block
10
10
  # main app process (useful for debugging)
11
11
  #
12
12
  def call(subscriptions, wait: false)
13
13
  subscriptions.each do |subscription|
14
14
  threads << Thread.new do
15
- connection.listen(subscription, options: { interval: 1, count: 10 }) do |event|
16
- subscription.subscriber.call(event)
17
- end
15
+ subscriptions.listen(subscription)
18
16
  end
19
17
  end
20
18
  threads.each(&:join) if wait
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ class CatchUpSubscription
5
+ attr_reader :subscriber, :filter
6
+ attr_accessor :position
7
+
8
+ def options
9
+ {
10
+ filter: @filter,
11
+ without_system_events: @without_system_events,
12
+ all: {
13
+ position: {
14
+ commit_position: position[:commit_position],
15
+ prepare_position: position[:prepare_position]
16
+ }
17
+ }
18
+ }.compact
19
+ end
20
+
21
+ def name
22
+ self.class.name(subscriber)
23
+ end
24
+
25
+ def self.name(subscriber)
26
+ subscriber.class.to_s
27
+ end
28
+
29
+ private
30
+
31
+ def initialize(subscriber, filter: nil, position: nil)
32
+ @filter = filter
33
+ @subscriber = subscriber
34
+ @position = position
35
+ @position ||= {
36
+ commit_position: 0,
37
+ prepare_position: 0
38
+ }
39
+ @without_system_events = true
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ class CatchUpSubscriptions
5
+ FILTER_DEFAULT_MAX = 32
6
+ FILTER_DEFAULT_CHECKPOINT_INTERVAL_MULTIPLIER = 10000
7
+
8
+ def create_or_load(subscriber, filter: {})
9
+ filter_options = prepare_filter_options(filter)
10
+ position = subscription_store.load_all_position(CatchUpSubscription.name(subscriber))
11
+
12
+ subscription = CatchUpSubscription.new(subscriber, position: position, filter: filter_options)
13
+ subscription_store.add(subscription) unless position
14
+
15
+ subscriptions << subscription unless @subscriptions.find { |s| s.name == subscription.name }
16
+ subscription
17
+ end
18
+
19
+ def each
20
+ subscriptions.each { |subscription| yield(subscription) }
21
+ end
22
+
23
+ def listen(subscription)
24
+ connection.subscribe(subscription.options) do |event_data|
25
+ next if recorded_event?(event_data)
26
+ next if confirmation?(event_data)
27
+
28
+ new_position = event_data[0]
29
+ event = event_data[1]
30
+
31
+ old_position = subscription.position
32
+ subscription.position = new_position
33
+ subscription_store.update_position(subscription)
34
+ next unless event
35
+
36
+ subscription.subscriber.call(event)
37
+ rescue StandardError
38
+ subscription.position = old_position
39
+ subscription_store.update_position(subscription)
40
+ raise
41
+ end
42
+ end
43
+
44
+ def clean_unused
45
+ subscription_store.clean_unused(subscriptions.map(&:name))
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :connection, :subscriptions, :subscription_store
51
+
52
+ def initialize(connection:, subscription_store:)
53
+ @connection = connection
54
+ @subscription_store = subscription_store
55
+ @subscriptions = []
56
+ end
57
+
58
+ def confirmation?(event_data)
59
+ event_data.is_a? EventStore::Client::Streams::ReadResp::SubscriptionConfirmation
60
+ end
61
+
62
+ def recorded_event?(event_data)
63
+ event_data.is_a? EventStore::Client::Streams::ReadResp::ReadEvent::RecordedEvent
64
+ end
65
+
66
+ def prepare_filter_options(filter)
67
+ return if filter.nil? || filter.empty?
68
+
69
+ {
70
+ event_type: filter[:event_type],
71
+ stream_identifier: filter[:stream_identifier],
72
+ max: FILTER_DEFAULT_MAX,
73
+ checkpointIntervalMultiplier: FILTER_DEFAULT_CHECKPOINT_INTERVAL_MULTIPLIER
74
+ }.compact
75
+ end
76
+ end
77
+ end
@@ -27,6 +27,12 @@ module EventStoreClient
27
27
  @subscriptions.create(subscriber, to, options: options)
28
28
  end
29
29
 
30
+ def subscribe_to_all(subscriber, filter=nil)
31
+ raise NoCallMethodOnSubscriber unless subscriber.respond_to?(:call)
32
+
33
+ @subscriptions.create_or_load(subscriber, filter: filter)
34
+ end
35
+
30
36
  def listen(wait: false)
31
37
  broker.call(@subscriptions, wait: wait)
32
38
  end
@@ -50,9 +56,10 @@ module EventStoreClient
50
56
 
51
57
  def initialize
52
58
  @threads = []
53
- @connection ||= EventStoreClient.adapter
54
- @error_handler ||= config.error_handler
55
- @broker ||= Broker.new(connection: connection)
59
+ @connection = EventStoreClient.adapter
60
+ @error_handler = config.error_handler
61
+ @broker = Broker.new(connection: connection)
62
+ @subscriptions = config.subscriptions_repo
56
63
  @subscriptions ||= Subscriptions.new(connection: connection, service: config.service_name)
57
64
  end
58
65
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'dry-configurable'
4
4
  require 'event_store_client/error_handler'
5
+
5
6
  module EventStoreClient
6
7
  extend Dry::Configurable
7
8
 
@@ -27,6 +28,8 @@ module EventStoreClient
27
28
 
28
29
  setting :mapper, Mapper::Default.new
29
30
 
31
+ setting :subscriptions_repo
32
+
30
33
  def self.configure
31
34
  yield(config) if block_given?
32
35
  end
@@ -19,6 +19,12 @@ module EventStoreClient
19
19
  end
20
20
  end
21
21
 
22
+ def listen(subscription)
23
+ connection.listen(subscription, options: { interval: 1, count: 10 }) do |event|
24
+ subscription.subscriber.call(event)
25
+ end
26
+ end
27
+
22
28
  private
23
29
 
24
30
  attr_reader :connection, :subscriptions, :service
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EventStoreClient
4
- VERSION = '1.0.17'
4
+ VERSION = '1.1.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event_store_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.17
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Wilgosz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-03 00:00:00.000000000 Z
11
+ date: 2021-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-configurable
@@ -173,6 +173,7 @@ files:
173
173
  - lib/event_store_client/adapters/grpc/commands/streams/link_to.rb
174
174
  - lib/event_store_client/adapters/grpc/commands/streams/read.rb
175
175
  - lib/event_store_client/adapters/grpc/commands/streams/read_all.rb
176
+ - lib/event_store_client/adapters/grpc/commands/streams/subscribe.rb
176
177
  - lib/event_store_client/adapters/grpc/commands/streams/tombstone.rb
177
178
  - lib/event_store_client/adapters/grpc/connection.rb
178
179
  - lib/event_store_client/adapters/grpc/generated/cluster_pb.rb
@@ -198,7 +199,6 @@ files:
198
199
  - lib/event_store_client/adapters/http/commands/persistent_subscriptions/create.rb
199
200
  - lib/event_store_client/adapters/http/commands/persistent_subscriptions/read.rb
200
201
  - lib/event_store_client/adapters/http/commands/projections/create.rb
201
- - lib/event_store_client/adapters/http/commands/projections/update.rb
202
202
  - lib/event_store_client/adapters/http/commands/streams/append.rb
203
203
  - lib/event_store_client/adapters/http/commands/streams/delete.rb
204
204
  - lib/event_store_client/adapters/http/commands/streams/link_to.rb
@@ -208,6 +208,8 @@ files:
208
208
  - lib/event_store_client/adapters/http/request_method.rb
209
209
  - lib/event_store_client/adapters/in_memory.rb
210
210
  - lib/event_store_client/broker.rb
211
+ - lib/event_store_client/catch_up_subscription.rb
212
+ - lib/event_store_client/catch_up_subscriptions.rb
211
213
  - lib/event_store_client/client.rb
212
214
  - lib/event_store_client/configuration.rb
213
215
  - lib/event_store_client/data_decryptor.rb
@@ -245,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
245
247
  - !ruby/object:Gem::Version
246
248
  version: '0'
247
249
  requirements: []
248
- rubygems_version: 3.1.4
250
+ rubygems_version: 3.0.4
249
251
  signing_key:
250
252
  specification_version: 4
251
253
  summary: Ruby integration for https://eventstore.org
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module EventStoreClient
4
- module HTTP
5
- module Commands
6
- module Projections
7
- class Update < Command
8
- def call(name, streams, options: {})
9
- data =
10
- <<~STRING
11
- fromStreams(#{streams})
12
- .when({
13
- $any: function(s,e) {
14
- linkTo("#{name}", e)
15
- }
16
- })
17
- STRING
18
- res = connection.call(
19
- :put,
20
- "/projection/#{name}/query?type=js&enabled=yes&emit=true&trackemittedstreams=true", # rubocop:disable Metrics/LineLength
21
- body: data,
22
- headers: {}
23
- )
24
-
25
- (200...300).cover?(res.status) ? Success() : Failure(res)
26
- end
27
- end
28
- end
29
- end
30
- end
31
- end