event_store_client 1.4.9 → 2.0.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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -145
  3. data/docs/appending_events.md +155 -0
  4. data/docs/catch_up_subscriptions.md +253 -0
  5. data/docs/configuration.md +83 -0
  6. data/docs/deleting_streams.md +25 -0
  7. data/docs/encrypting_events.md +84 -0
  8. data/docs/linking_events.md +149 -0
  9. data/docs/reading_events.md +200 -0
  10. data/lib/event_store_client/adapters/grpc/client.rb +244 -105
  11. data/lib/event_store_client/adapters/grpc/cluster/gossip_discover.rb +131 -0
  12. data/lib/event_store_client/adapters/grpc/cluster/insecure_connection.rb +21 -0
  13. data/lib/event_store_client/adapters/grpc/cluster/member.rb +18 -0
  14. data/lib/event_store_client/adapters/grpc/cluster/queryless_discover.rb +25 -0
  15. data/lib/event_store_client/adapters/grpc/cluster/secure_connection.rb +71 -0
  16. data/lib/event_store_client/adapters/grpc/command_registrar.rb +7 -7
  17. data/lib/event_store_client/adapters/grpc/commands/command.rb +63 -25
  18. data/lib/event_store_client/adapters/grpc/commands/gossip/cluster_info.rb +24 -0
  19. data/lib/event_store_client/adapters/grpc/commands/streams/append.rb +43 -68
  20. data/lib/event_store_client/adapters/grpc/commands/streams/append_multiple.rb +44 -0
  21. data/lib/event_store_client/adapters/grpc/commands/streams/delete.rb +21 -17
  22. data/lib/event_store_client/adapters/grpc/commands/streams/hard_delete.rb +39 -0
  23. data/lib/event_store_client/adapters/grpc/commands/streams/link_to.rb +7 -52
  24. data/lib/event_store_client/adapters/grpc/commands/streams/link_to_multiple.rb +44 -0
  25. data/lib/event_store_client/adapters/grpc/commands/streams/read.rb +20 -85
  26. data/lib/event_store_client/adapters/grpc/commands/streams/read_paginated.rb +174 -0
  27. data/lib/event_store_client/adapters/grpc/commands/streams/subscribe.rb +31 -106
  28. data/lib/event_store_client/adapters/grpc/connection.rb +56 -36
  29. data/lib/event_store_client/adapters/grpc/discover.rb +75 -0
  30. data/lib/event_store_client/adapters/grpc/generated/cluster_pb.rb +106 -18
  31. data/lib/event_store_client/adapters/grpc/generated/cluster_services_pb.rb +12 -12
  32. data/lib/event_store_client/adapters/grpc/generated/code_pb.rb +34 -0
  33. data/lib/event_store_client/adapters/grpc/generated/gossip_pb.rb +3 -2
  34. data/lib/event_store_client/adapters/grpc/generated/gossip_services_pb.rb +3 -3
  35. data/lib/event_store_client/adapters/grpc/generated/monitoring_pb.rb +25 -0
  36. data/lib/event_store_client/adapters/grpc/generated/monitoring_services_pb.rb +26 -0
  37. data/lib/event_store_client/adapters/grpc/generated/operations_pb.rb +2 -1
  38. data/lib/event_store_client/adapters/grpc/generated/operations_services_pb.rb +8 -7
  39. data/lib/event_store_client/adapters/grpc/generated/persistent_pb.rb +199 -38
  40. data/lib/event_store_client/adapters/grpc/generated/persistent_services_pb.rb +7 -3
  41. data/lib/event_store_client/adapters/grpc/generated/projections_pb.rb +9 -26
  42. data/lib/event_store_client/adapters/grpc/generated/projections_services_pb.rb +4 -3
  43. data/lib/event_store_client/adapters/grpc/generated/serverfeatures_pb.rb +29 -0
  44. data/lib/event_store_client/adapters/grpc/generated/serverfeatures_services_pb.rb +26 -0
  45. data/lib/event_store_client/adapters/grpc/generated/shared_pb.rb +54 -12
  46. data/lib/event_store_client/adapters/grpc/generated/status_pb.rb +23 -0
  47. data/lib/event_store_client/adapters/grpc/generated/streams_pb.rb +104 -64
  48. data/lib/event_store_client/adapters/grpc/generated/streams_services_pb.rb +3 -2
  49. data/lib/event_store_client/adapters/grpc/generated/users_services_pb.rb +2 -2
  50. data/lib/event_store_client/adapters/grpc/options/streams/read_options.rb +78 -0
  51. data/lib/event_store_client/adapters/grpc/options/streams/write_options.rb +39 -0
  52. data/lib/event_store_client/adapters/grpc/shared/event_deserializer.rb +52 -0
  53. data/lib/event_store_client/adapters/grpc/shared/options/filter_options.rb +76 -0
  54. data/lib/event_store_client/adapters/grpc/shared/options/stream_options.rb +91 -0
  55. data/lib/event_store_client/adapters/grpc/shared/streams/process_response.rb +28 -0
  56. data/lib/event_store_client/adapters/grpc/shared/streams/process_responses.rb +33 -0
  57. data/lib/event_store_client/adapters/grpc.rb +28 -12
  58. data/lib/event_store_client/configuration.rb +39 -54
  59. data/lib/event_store_client/connection/url.rb +57 -0
  60. data/lib/event_store_client/connection/url_parser.rb +144 -0
  61. data/lib/event_store_client/data_decryptor.rb +2 -9
  62. data/lib/event_store_client/deserialized_event.rb +35 -10
  63. data/lib/event_store_client/encryption_metadata.rb +0 -1
  64. data/lib/event_store_client/event.rb +4 -2
  65. data/lib/event_store_client/extensions/options_extension.rb +87 -0
  66. data/lib/event_store_client/mapper/default.rb +12 -9
  67. data/lib/event_store_client/mapper/encrypted.rb +18 -17
  68. data/lib/event_store_client/types.rb +1 -1
  69. data/lib/event_store_client/utils.rb +30 -0
  70. data/lib/event_store_client/version.rb +1 -1
  71. data/lib/event_store_client.rb +8 -7
  72. metadata +74 -83
  73. data/lib/event_store_client/adapters/grpc/Protos/cluster.proto +0 -149
  74. data/lib/event_store_client/adapters/grpc/Protos/gossip.proto +0 -44
  75. data/lib/event_store_client/adapters/grpc/Protos/operations.proto +0 -45
  76. data/lib/event_store_client/adapters/grpc/Protos/persistent.proto +0 -180
  77. data/lib/event_store_client/adapters/grpc/Protos/projections.proto +0 -174
  78. data/lib/event_store_client/adapters/grpc/Protos/shared.proto +0 -22
  79. data/lib/event_store_client/adapters/grpc/Protos/streams.proto +0 -242
  80. data/lib/event_store_client/adapters/grpc/Protos/users.proto +0 -119
  81. data/lib/event_store_client/adapters/grpc/README.md +0 -16
  82. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/create.rb +0 -46
  83. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/delete.rb +0 -34
  84. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/read.rb +0 -77
  85. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/settings_schema.rb +0 -38
  86. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/update.rb +0 -48
  87. data/lib/event_store_client/adapters/grpc/commands/projections/create.rb +0 -48
  88. data/lib/event_store_client/adapters/grpc/commands/projections/delete.rb +0 -34
  89. data/lib/event_store_client/adapters/grpc/commands/projections/update.rb +0 -44
  90. data/lib/event_store_client/adapters/grpc/commands/streams/read_all.rb +0 -43
  91. data/lib/event_store_client/adapters/grpc/commands/streams/tombstone.rb +0 -35
  92. data/lib/event_store_client/adapters/http/README.md +0 -16
  93. data/lib/event_store_client/adapters/http/client.rb +0 -161
  94. data/lib/event_store_client/adapters/http/commands/command.rb +0 -27
  95. data/lib/event_store_client/adapters/http/commands/persistent_subscriptions/ack.rb +0 -15
  96. data/lib/event_store_client/adapters/http/commands/persistent_subscriptions/create.rb +0 -35
  97. data/lib/event_store_client/adapters/http/commands/persistent_subscriptions/read.rb +0 -60
  98. data/lib/event_store_client/adapters/http/commands/projections/create.rb +0 -33
  99. data/lib/event_store_client/adapters/http/commands/projections/update.rb +0 -31
  100. data/lib/event_store_client/adapters/http/commands/streams/append.rb +0 -49
  101. data/lib/event_store_client/adapters/http/commands/streams/delete.rb +0 -16
  102. data/lib/event_store_client/adapters/http/commands/streams/link_to.rb +0 -49
  103. data/lib/event_store_client/adapters/http/commands/streams/read.rb +0 -52
  104. data/lib/event_store_client/adapters/http/commands/streams/tombstone.rb +0 -17
  105. data/lib/event_store_client/adapters/http/connection.rb +0 -46
  106. data/lib/event_store_client/adapters/http/request_method.rb +0 -28
  107. data/lib/event_store_client/adapters/http.rb +0 -17
  108. data/lib/event_store_client/adapters/in_memory.rb +0 -144
  109. data/lib/event_store_client/broker.rb +0 -40
  110. data/lib/event_store_client/catch_up_subscription.rb +0 -42
  111. data/lib/event_store_client/catch_up_subscriptions.rb +0 -92
  112. data/lib/event_store_client/client.rb +0 -73
  113. data/lib/event_store_client/error_handler.rb +0 -10
  114. data/lib/event_store_client/subscription.rb +0 -23
  115. data/lib/event_store_client/subscriptions.rb +0 -38
  116. data/lib/event_store_client/value_objects/read_direction.rb +0 -43
@@ -1,136 +1,275 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Layout/LineLength, Metrics/ParameterLists
4
+
3
5
  module EventStoreClient
4
6
  module GRPC
5
7
  class Client
6
8
  include Configuration
7
- # Appends given events to the stream
8
- # @param [String] Stream name to append events to
9
- # @param [Array](each: EventStoreClient::DeserializedEvent) list of events to publish
10
- # @return Dry::Monads::Result::Success or Dry::Monads::Result::Failure
11
- #
12
- def append_to_stream(stream_name, events, options: {})
13
- Commands::Streams::Append.new.call(
14
- stream_name, events, options: options
15
- )
16
- end
17
9
 
18
- # Softly deletes the given stream
19
- # @param [String] Stream name to delete
20
- # @param options [Hash] additional options to the request
21
- # @return Dry::Monads::Result::Success or Dry::Monads::Result::Failure
22
- #
23
- def delete_stream(stream_name, options: {})
24
- Commands::Streams::Delete.new.call(
25
- stream_name, options: options
26
- )
10
+ # @param stream_name [String]
11
+ # @param events_or_event [EventStoreClient::DeserializedEvent, Array<EventStoreClient::DeserializedEvent>]
12
+ # @param options [Hash]
13
+ # @option options [Integer] :expected_revision provide your own revision number
14
+ # @option options [String] :expected_revision provide one of next values: 'any', 'no_stream'
15
+ # or 'stream_exists'
16
+ # @param credentials [Hash]
17
+ # @option credentials [String] :username override authentication username
18
+ # @option credentials [String] :password override authentication password
19
+ # @yield [EventStore::Client::Streams::AppendReq, EventStore::Client::Streams::AppendReq]
20
+ # yields options and proposed message option right before sending the request. You can
21
+ # extend it with your own options, not covered in the default implementation.
22
+ # Example:
23
+ # ```ruby
24
+ # append_to_stream('some-stream', event) do |req_opts, proposed_msg_opts|
25
+ # puts req_opts.options
26
+ # puts proposed_msg_opts.proposed_message
27
+ # end
28
+ # ```
29
+ # @return [Dry::Monads::Result::Success, Dry::Monads::Result::Failure, Array<Dry::Monads::Result::Success, Dry::Monads::Result::Failure>]
30
+ # Returns monads' Success/Failure in case whether request was performed.
31
+ def append_to_stream(stream_name, events_or_event, options: {}, credentials: {}, &blk)
32
+ if events_or_event.is_a?(Array)
33
+ Commands::Streams::AppendMultiple.new(**credentials).call(
34
+ stream_name, events_or_event, options: options
35
+ )
36
+ else
37
+ Commands::Streams::Append.new(**credentials).call(
38
+ stream_name, events_or_event, options: options, &blk
39
+ )
40
+ end
27
41
  end
28
42
 
29
- # Completely removes the given stream
30
- # @param [String] Stream name to delete
31
- # @param options [Hash] additional options to the request
32
- # @return Dry::Monads::Result::Success or Dry::Monads::Result::Failure
43
+ # @param stream_name [String]
44
+ # @param skip_deserialization [Boolean]
45
+ # @param skip_decryption [Boolean]
46
+ # @param options [Hash] request options
47
+ # @option options [String] :direction read direction - 'Forwards' or 'Backwards'
48
+ # @option options [Integer, Symbol] :from_revision. If number is provided - it is threaded
49
+ # as starting revision number. Alternatively you can provide :start or :end value to
50
+ # define a stream revision. **Use this option when stream name is a normal stream name**
51
+ # @option options [Hash, Symbol] :from_position. If hash is provided - you should supply
52
+ # it with :commit_position and/or :prepare_position keys. Alternatively you can provide
53
+ # :start or :end value to define a stream position. **Use this option when stream name
54
+ # is "$all"**
55
+ # @option options [Integer] :max_count max number of events to return in one response
56
+ # @option options [Boolean] :resolve_link_tos When using projections to create new events you
57
+ # can set whether the generated events are pointers to existing events. Setting this value
58
+ # to true tells EventStoreDB to return the event as well as the event linking to it.
59
+ # @option options [Hash] :filter provide it to filter events when reading from $all. You can
60
+ # either filter by stream name or filter by event type. Filtering can be done by using
61
+ # Regexp or by a string.
62
+ # Examples:
63
+ # ```ruby
64
+ # # Include events only from streams which names start from 'some-stream-1' and
65
+ # # 'some-stream-2'
66
+ # { filter: { stream_identifier: { prefix: ['some-stream-1', 'some-stream-2'] } } }
33
67
  #
34
- def tombstone_stream(stream_name, options: {})
35
- Commands::Streams::Tombstone.new.call(stream_name, options: options)
36
- end
37
-
38
- # Reads a page of events from the given stream
39
- # @param [String] Stream name to read events from
40
- # @param options [Hash] additional options to the request
41
- # @return Dry::Monads::Result::Success with returned events or Dry::Monads::Result::Failure
68
+ # # Include events only from streams which names end with digit
69
+ # { filter: { stream_identifier: { regex: /\d$/.to_s } } }
42
70
  #
43
- def read(stream_name, options: {})
44
- Commands::Streams::Read.new.call(stream_name, options: options)
45
- end
46
-
47
- # Reads all events from the given stream
48
- # @param [String] Stream name to read events from
49
- # @param options [Hash] additional options to the request
50
- # @return Dry::Monads::Result::Success with returned events or Dry::Monads::Result::Failure
71
+ # # Include events which start from 'some-event-1' and 'some-event-2'
72
+ # { filter: { event_type: { prefix: ['some-event-1', 'some-event-2'] } } }
51
73
  #
52
- def read_all_from_stream(stream_name, options: {})
53
- Commands::Streams::ReadAll.new.call(stream_name, options: options)
74
+ # # Include events which names end with digit
75
+ # { filter: { event_type: { regex: /\d$/.to_s } } }
76
+ # ```
77
+ # @param credentials [Hash]
78
+ # @option credentials [String] :username override authentication username
79
+ # @option credentials [String] :password override authentication password
80
+ # @yield [EventStore::Client::Streams::ReadReq::Options] yields request options right
81
+ # before sending the request. You can extend it with your own options, not covered in
82
+ # the default implementation.
83
+ # Example:
84
+ # ```ruby
85
+ # read('$all') do |opts|
86
+ # opts.filter = EventStore::Client::Streams::ReadReq::Options::FilterOptions.new(
87
+ # { stream_identifier: { prefix: ['as'] }, count: EventStore::Client::Empty.new }
88
+ # )
89
+ # end
90
+ # ```
91
+ # @return [Dry::Monads::Success, Dry::Monads::Failure]
92
+ def read(stream_name, options: {}, skip_deserialization: config.skip_deserialization,
93
+ skip_decryption: config.skip_decryption, credentials: {}, &blk)
94
+ Commands::Streams::Read.new(**credentials).call(
95
+ stream_name,
96
+ options: options,
97
+ skip_deserialization: skip_deserialization,
98
+ skip_decryption: skip_decryption,
99
+ &blk
100
+ )
54
101
  end
55
102
 
56
- # Creates the subscription for the given stream
57
- # @param [EventStoreClient::Subscription] subscription to observe
58
- # @param options [Hash] additional options to the request
59
- # @return Dry::Monads::Result::Success or Dry::Monads::Result::Failure
60
- #
61
- def subscribe_to_stream(subscription, options: {})
62
- join_streams(subscription.name, subscription.observed_streams)
63
- Commands::PersistentSubscriptions::Create.new.call(
64
- subscription.stream,
65
- subscription.name,
66
- options: options
103
+ # @see {#read} for available params
104
+ # @return [Enumerator] enumerator will yield Dry::Monads::Success or Dry::Monads::Failure on
105
+ # each iteration
106
+ def read_paginated(stream_name, options: {}, credentials: {},
107
+ skip_deserialization: config.skip_deserialization,
108
+ skip_decryption: config.skip_decryption, &blk)
109
+ Commands::Streams::ReadPaginated.new(**credentials).call(
110
+ stream_name,
111
+ options: options,
112
+ skip_deserialization: skip_deserialization,
113
+ skip_decryption: skip_decryption,
114
+ &blk
67
115
  )
68
116
  end
69
117
 
70
- # Links given events with the given stream
71
- # @param [String] Stream name to link events to
72
- # @param [Array](each: EventStoreClient::DeserializedEvent) a list of events to link
73
- # @param expected_version [Integer] expected number of events in the stream
74
- # @return Dry::Monads::Result::Success or Dry::Monads::Result::Failure
75
- #
76
- def link_to(stream_name, events, options: {})
77
- Commands::Streams::LinkTo.new.call(stream_name, events, options: options)
118
+ # Refs https://developers.eventstore.com/server/v5/streams.html#hard-delete
119
+ # @param stream_name [String]
120
+ # @param options [Hash]
121
+ # @option options [Integer, String] :expected_revision provide your own revision number.
122
+ # Alternatively you can provide one of next values: 'any', 'no_stream' or 'stream_exists'.
123
+ # @param credentials [Hash]
124
+ # @option credentials [String] :username override authentication username
125
+ # @option credentials [String] :password override authentication password
126
+ # @yield [EventStore::Client::Streams::TombstoneReq::Options] yields request options right
127
+ # before sending the request. You can override them in your own way.
128
+ # Example:
129
+ # ```ruby
130
+ # delete_stream('stream_name') do |opts|
131
+ # opts.stream_identifier.stream_name = 'overridden-stream-name'
132
+ # end
133
+ # ```
134
+ # @return [Dry::Monads::Success, Dry::Monads::Failure]
135
+ def hard_delete_stream(stream_name, options: {}, credentials: {}, &blk)
136
+ Commands::Streams::HardDelete.new(**credentials).call(stream_name, options: options, &blk)
78
137
  end
79
138
 
80
- # Runs the persistent subscription indeinitely
81
- # @param [EventStoreClient::Subscription] subscription to observe
82
- # @param options [Hash] additional options to the request
83
- # @return - Nothing, it is a blocking operation, yields the given block with event instead
84
- #
85
- def listen(subscription, options: {})
86
- consume_feed(subscription, options: options) do |event|
87
- begin
88
- yield event if block_given?
89
- rescue StandardError => e
90
- config.error_handler&.call(e)
91
- end
92
- end
139
+ # Refs https://developers.eventstore.com/server/v5/streams.html#soft-delete-and-truncatebefore
140
+ # @param stream_name [String]
141
+ # @param options [Hash]
142
+ # @option options [Integer, String] :expected_revision provide your own revision number.
143
+ # Alternatively you can provide one of next values: 'any', 'no_stream' or 'stream_exists'.
144
+ # @param credentials [Hash]
145
+ # @option credentials [String] :username override authentication username
146
+ # @option credentials [String] :password override authentication password
147
+ # @yield [EventStore::Client::Streams::DeleteReq::Options] yields request options right
148
+ # before sending the request. You can override them in your own way.
149
+ # Example:
150
+ # ```ruby
151
+ # delete_stream('stream_name') do |opts|
152
+ # opts.stream_identifier.stream_name = 'overridden-stream-name'
153
+ # end
154
+ # ```
155
+ # @return [Dry::Monads::Success, Dry::Monads::Failure]
156
+ def delete_stream(stream_name, options: {}, credentials: {}, &blk)
157
+ Commands::Streams::Delete.new(**credentials).call(stream_name, options: options, &blk)
93
158
  end
94
159
 
95
- # Subscribe to a stream
96
- # @param options [Hash] additional options to the request
97
- # @return - Nothing, it is a blocking operation, yields the given block with event instead
160
+ # Subscribe to the given stream and listens for events. Note, that it will block execution of
161
+ # current stack. If you want to do it asynchronous - consider putting it out of current
162
+ # thread.
163
+ # @param stream_name [String]
164
+ # @param handler [#call] whenever new event arrives - #call method of your handler will be
165
+ # called with the response passed into it
166
+ # @param skip_deserialization [Boolean]
167
+ # @param skip_decryption [Boolean]
168
+ # @param options [Hash] request options
169
+ # @option options [String] :direction read direction - 'Forwards' or 'Backwards'
170
+ # @option options [Integer, Symbol] :from_revision. If number is provided - it is threaded
171
+ # as starting revision number. Alternatively you can provide :start or :end value to
172
+ # define a stream revision. **Use this option when stream name is a normal stream name**
173
+ # @option options [Hash, Symbol] :from_position. If hash is provided - you should supply
174
+ # it with :commit_position and/or :prepare_position keys. Alternatively you can provide
175
+ # :start or :end value to define a stream position. **Use this option when stream name
176
+ # is "$all"**
177
+ # @option options [Boolean] :resolve_link_tos When using projections to create new events you
178
+ # can set whether the generated events are pointers to existing events. Setting this value
179
+ # to true tells EventStoreDB to return the event as well as the event linking to it.
180
+ # @option options [Hash] :filter provide it to filter events when subscribing to $all. You can
181
+ # either filter by stream name or filter by event type. Filtering can be done by using
182
+ # Regexp or by a string.
183
+ # Examples:
184
+ # ```ruby
185
+ # # Include events only from streams which names start from 'some-stream-1' and
186
+ # # 'some-stream-2'
187
+ # { filter: { stream_identifier: { prefix: ['some-stream-1', 'some-stream-2'] } } }
98
188
  #
99
- def subscribe(options = {})
100
- Commands::Streams::Subscribe.new.call(options) do |event|
101
- yield event if block_given?
102
- end
103
- rescue StandardError => e
104
- config.error_handler&.call(e)
105
- end
106
-
107
- private
108
-
109
- # Joins multiple streams into the new one under the given name
110
- # @param [String] Name of the stream containing the ones to join
111
- # @param [Array] (each: String) list of streams to join together
112
- # @return Dry::Monads::Result::Success or Dry::Monads::Result::Failure
189
+ # # Include events only from streams which names end with digit
190
+ # { filter: { stream_identifier: { regex: /\d$/.to_s } } }
191
+ #
192
+ # # Include events which start from 'some-event-1' and 'some-event-2'
193
+ # { filter: { event_type: { prefix: ['some-event-1', 'some-event-2'] } } }
113
194
  #
114
- def join_streams(name, streams)
115
- res = Commands::Projections::Create.new.call(name, streams)
116
- return if res.success?
195
+ # # Include events which names end with digit
196
+ # { filter: { event_type: { regex: /\d$/.to_s } } }
197
+ # ```
198
+ # @param credentials [Hash]
199
+ # @option credentials [String] :username override authentication username
200
+ # @option credentials [String] :password override authentication password
201
+ # @yield [EventStore::Client::Streams::ReadReq::Options] yields request options right
202
+ # before sending the request. You can extend it with your own options, not covered in
203
+ # the default implementation.
204
+ # Example:
205
+ # ```ruby
206
+ # subscribe_to_stream('$all', handler: proc { |response| puts response }) do |opts|
207
+ # opts.filter = EventStore::Client::Streams::ReadReq::Options::FilterOptions.new(
208
+ # { stream_identifier: { prefix: ['as'] }, max: 100 }
209
+ # )
210
+ # end
211
+ # ```
212
+ # @return [Dry::Monads::Success, Dry::Monads::Failure]
213
+ def subscribe_to_stream(stream_name, handler:, options: {}, credentials: {},
214
+ skip_deserialization: config.skip_deserialization,
215
+ skip_decryption: config.skip_decryption, &blk)
216
+ Commands::Streams::Subscribe.new(**credentials).call(
217
+ stream_name,
218
+ handler: handler,
219
+ options: options,
220
+ skip_deserialization: skip_deserialization,
221
+ skip_decryption: skip_decryption,
222
+ &blk
223
+ )
224
+ end
117
225
 
118
- Commands::Projections::Update.new.call(name, streams)
226
+ # This method acts the same as #subscribe_to_stream with the only exception that it subscribes
227
+ # to $all stream
228
+ # @see #subscribe_to_stream
229
+ def subscribe_to_all(handler:, options: {}, credentials: {},
230
+ skip_deserialization: config.skip_deserialization,
231
+ skip_decryption: config.skip_decryption, &blk)
232
+ Commands::Streams::Subscribe.new(**credentials).call(
233
+ '$all',
234
+ handler: handler,
235
+ options: options,
236
+ skip_deserialization: skip_deserialization,
237
+ skip_decryption: skip_decryption,
238
+ &blk
239
+ )
119
240
  end
120
241
 
121
- # @api private
122
- # Consumes the new events from the subscription
123
- # @param [EventStoreClient::Subscription] subscription to observe
124
- # @param options [Hash] additional options to the request
125
- # @return Dry::Monads::Result::Success or Dry::Monads::Result::Failure
126
- #
127
- def consume_feed(subscription, options: {})
128
- Commands::PersistentSubscriptions::Read.new.call(
129
- subscription.stream, subscription.name, options: options
130
- ) do |event|
131
- yield event if block_given?
242
+ # Links event from one stream into another stream. You can later access it by providing
243
+ # :resolve_link_tos option when reading from a stream. If you provide an event that does not
244
+ # present in EventStore database yet - its data will not be appended properly to the stream,
245
+ # thus, making it look as a malformed event.
246
+ # @see #append_to_stream for available params and returned value
247
+ def link_to(stream_name, events_or_event, options: {}, credentials: {}, &blk)
248
+ if events_or_event.is_a?(Array)
249
+ Commands::Streams::LinkToMultiple.new(**credentials).call(
250
+ stream_name,
251
+ events_or_event,
252
+ options: options,
253
+ &blk
254
+ )
255
+ else
256
+ Commands::Streams::LinkTo.new(**credentials).call(
257
+ stream_name,
258
+ events_or_event,
259
+ options: options,
260
+ &blk
261
+ )
132
262
  end
133
263
  end
264
+
265
+ # @param credentials [Hash]
266
+ # @option credentials [String] :username
267
+ # @option credentials [String] :password
268
+ # @return [Dry::Monads::Success, Dry::Monads::Failure]
269
+ def cluster_info(credentials: {})
270
+ Commands::Gossip::ClusterInfo.new(**credentials).call
271
+ end
134
272
  end
135
273
  end
136
274
  end
275
+ # rubocop:enable Layout/LineLength, Metrics/ParameterLists
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
4
+
5
+ require 'event_store_client/adapters/grpc/generated/shared_pb'
6
+ require 'event_store_client/adapters/grpc/generated/gossip_pb'
7
+ require 'event_store_client/adapters/grpc/generated/gossip_services_pb'
8
+
9
+ module EventStoreClient
10
+ module GRPC
11
+ module Cluster
12
+ class GossipDiscover
13
+ include Configuration
14
+
15
+ DiscoverError = Class.new(StandardError)
16
+
17
+ # Order is important - it plays role of states priority as well
18
+ READ_ONLY_STATES = %i[ReadOnlyReplica PreReadOnlyReplica ReadOnlyLeaderless].freeze
19
+ ALLOWED_NODE_STATES =
20
+ (%i[Leader Follower] + READ_ONLY_STATES).freeze
21
+
22
+ # @param nodes [Array<EventStoreClient::Connection::Url::Node>]
23
+ # @param failed_member [EventStoreClient::GRPC::Cluster::Member, nil]
24
+ # @return [EventStoreClient::GRPC::Cluster::Member]
25
+ # @raise [EventStoreClient::GRPC::Cluster::GossipDiscover::DiscoverError]
26
+ def call(nodes, failed_member:)
27
+ nodes = sort_nodes(nodes, failed_member)
28
+
29
+ attempts = config.eventstore_url.max_discover_attempts
30
+ attempts.times do
31
+ nodes.each do |node|
32
+ suitable_member = suitable_member_of_node(node)
33
+ next unless suitable_member
34
+
35
+ return suitable_member
36
+ end
37
+
38
+ sleep(config.eventstore_url.discover_interval / 1000.0)
39
+ end
40
+
41
+ raise DiscoverError, "Failed to discover suitable host after #{attempts} attempts."
42
+ end
43
+
44
+ private
45
+
46
+ # @return [Array<Symbol>]
47
+ def ordered_states
48
+ @ordered_states ||=
49
+ # Move preferred state to the first place
50
+ case config.eventstore_url.node_preference
51
+ when :Leader, :Follower
52
+ [config.eventstore_url.node_preference] +
53
+ (ALLOWED_NODE_STATES - [config.eventstore_url.node_preference])
54
+ else
55
+ READ_ONLY_STATES + (ALLOWED_NODE_STATES - READ_ONLY_STATES)
56
+ end
57
+ end
58
+
59
+ # @param node [EventStoreClient::Connection::Url::Node]
60
+ # @return [Array<EventStoreClient::GRPC::Cluster::Member>, nil]
61
+ def node_members(node)
62
+ connection = Connection.new(
63
+ host: node.host,
64
+ port: node.port,
65
+ timeout: config.eventstore_url.gossip_timeout
66
+ )
67
+ members =
68
+ connection.
69
+ call(EventStore::Client::Gossip::Gossip::Stub).
70
+ read(EventStore::Client::Empty.new).
71
+ members
72
+ members.map do |member|
73
+ Member.new(
74
+ host: member.http_end_point.address,
75
+ port: member.http_end_point.port,
76
+ state: member.state,
77
+ active: member.is_alive,
78
+ instance_id: Utils.uuid_to_str(member.instance_id)
79
+ )
80
+ end
81
+ rescue ::GRPC::BadStatus, Errno::ECONNREFUSED
82
+ config.logger&.debug("Failed to get cluster list from #{node.host}:#{node.port} node.")
83
+ nil
84
+ end
85
+
86
+ # Pick a suitable member based on its status and node preference
87
+ # @return [Array<EventStoreClient::GRPC::Cluster::Member>]
88
+ # @return [EventStoreClient::GRPC::Cluster::Member]
89
+ def detect_suitable_member(members)
90
+ members = members.select(&:active)
91
+ members = members.select { |member| ALLOWED_NODE_STATES.include?(member.state) }
92
+ members = members.sort_by { |member| ordered_states.index(member.state) }
93
+ members.first
94
+ end
95
+
96
+ # Put failed node to the end of the list
97
+ # @param nodes [Array<EventStoreClient::Connection::Url::Node>]
98
+ # @param failed_member [EventStoreClient::GRPC::Cluster::Member, nil]
99
+ # @return [Array<EventStoreClient::Connection::Url::Node>]
100
+ def sort_nodes(nodes, failed_member)
101
+ return nodes unless failed_member
102
+
103
+ nodes.sort_by do |node|
104
+ failed_member.host == node.host && failed_member.port == node.port ? 1 : 0
105
+ end
106
+ end
107
+
108
+ # Searches a suitable member among members of the given node
109
+ # @param node [EventStoreClient::Connection::Url::Node]
110
+ # @return [EventStoreClient::GRPC::Cluster::Member, nil]
111
+ def suitable_member_of_node(node)
112
+ config.logger&.debug(
113
+ "Starting to discover #{node.host}:#{node.port} node for candidates."
114
+ )
115
+ members = node_members(node)
116
+ return unless members
117
+
118
+ suitable_member = detect_suitable_member(members)
119
+ return unless suitable_member
120
+
121
+ config.logger&.debug(
122
+ "Found suitable member: #{suitable_member.host}:#{suitable_member.port} with " \
123
+ "role \"#{suitable_member.state}\"."
124
+ )
125
+ suitable_member
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module GRPC
5
+ module Cluster
6
+ class InsecureConnection < Connection
7
+ # @param stub_class GRPC request stub class. E.g. EventStore::Client::Gossip::Gossip::Stub
8
+ # @return instance of the given stub_class class
9
+ def call(stub_class)
10
+ config.logger&.debug('Using insecure connection.')
11
+ stub_class.new(
12
+ "#{host}:#{port}",
13
+ :this_channel_is_insecure,
14
+ channel_args: channel_args,
15
+ timeout: (timeout / 1000.0 if timeout)
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module GRPC
5
+ module Cluster
6
+ class Member
7
+ include Extensions::OptionsExtension
8
+
9
+ option(:host) # String
10
+ option(:port) # Integer
11
+ option(:active) # boolean
12
+ option(:instance_id) # string
13
+ option(:state) # symbol
14
+ option(:failed_endpoint) { false } # boolean
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module GRPC
5
+ module Cluster
6
+ class QuerylessDiscover
7
+ include Configuration
8
+
9
+ NoHostError = Class.new(StandardError)
10
+
11
+ # @param nodes [EventStoreClient::Connection::Url::Node]
12
+ # @return [EventStoreClient::GRPC::Cluster::Member]
13
+ def call(nodes)
14
+ raise NoHostError, 'No host setup' if nodes.empty?
15
+
16
+ Member.new(host: nodes.first.host, port: nodes.first.port).tap do |member|
17
+ config.logger&.debug(
18
+ "Picking #{member.host}:#{member.port} member without cluster discovery."
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ module GRPC
5
+ module Cluster
6
+ class SecureConnection < Connection
7
+ CertificateLookupError = Class.new(StandardError)
8
+
9
+ # @param stub_class GRPC request stub class. E.g. EventStore::Client::Gossip::Gossip::Stub
10
+ # @return instance of the given stub_class class
11
+ def call(stub_class)
12
+ config.logger&.debug("Using secure connection with credentials #{username}:#{password}.")
13
+ stub_class.new(
14
+ "#{host}:#{port}",
15
+ channel_credentials,
16
+ channel_args: channel_args,
17
+ timeout: (timeout / 1000.0 if timeout)
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ # @return [GRPC::Core::ChannelCredentials]
24
+ def channel_credentials
25
+ certificate =
26
+ if config.eventstore_url.tls_ca_file
27
+ config.logger&.debug('Picking certificate from tlsCAFile option.')
28
+ File.read(config.eventstore_url.tls_ca_file)
29
+ else
30
+ config.logger&.debug('Resolving certificate from current member.')
31
+ cert.to_s
32
+ end
33
+
34
+ ::GRPC::Core::ChannelCredentials.new(certificate)
35
+ end
36
+
37
+ # rubocop:disable Metrics/AbcSize
38
+
39
+ # @return [String, nil] returns the X.509 certificates the server presented
40
+ # @raise [EventStoreClient::GRPC::Cluster::SecureConnection::CertificateLookupError]
41
+ def cert
42
+ retries = 0
43
+
44
+ begin
45
+ Net::HTTP.start(host, port, use_ssl: true, verify_mode: verify_mode, &:peer_cert)
46
+ rescue SocketError => e
47
+ attempts = config.eventstore_url.ca_lookup_attempts
48
+ sleep config.eventstore_url.ca_lookup_interval / 1000.0
49
+ retries += 1
50
+ if retries <= attempts
51
+ config.logger&.debug("Failed to lookup certificate. Reason: #{e.class}. Retying.")
52
+ retry
53
+ end
54
+ raise(
55
+ CertificateLookupError,
56
+ "Failed to get X.509 certificate after #{attempts} attempts."
57
+ )
58
+ end
59
+ end
60
+ # rubocop:enable Metrics/AbcSize
61
+
62
+ # @return [Integer] SSL verify mode
63
+ def verify_mode
64
+ return OpenSSL::SSL::VERIFY_PEER if config.eventstore_url.tls_verify_cert
65
+
66
+ OpenSSL::SSL::VERIFY_NONE
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end