event_store_client 1.0.17 → 1.1.0

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