event_store_client 1.4.9 → 2.0.0

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