ably 1.1.2 → 1.1.6

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +27 -0
  3. data/CHANGELOG.md +67 -0
  4. data/COPYRIGHT +1 -0
  5. data/LICENSE +172 -11
  6. data/MAINTAINERS.md +1 -0
  7. data/README.md +11 -21
  8. data/SPEC.md +1020 -922
  9. data/ably.gemspec +4 -4
  10. data/lib/ably/auth.rb +12 -2
  11. data/lib/ably/exceptions.rb +2 -2
  12. data/lib/ably/modules/ably.rb +11 -1
  13. data/lib/ably/realtime/channel.rb +7 -11
  14. data/lib/ably/realtime/channel/channel_manager.rb +2 -2
  15. data/lib/ably/realtime/channel/channel_properties.rb +24 -0
  16. data/lib/ably/realtime/client.rb +9 -0
  17. data/lib/ably/realtime/connection.rb +5 -4
  18. data/lib/ably/realtime/connection/websocket_transport.rb +67 -1
  19. data/lib/ably/realtime/presence.rb +0 -14
  20. data/lib/ably/rest/channel.rb +27 -19
  21. data/lib/ably/rest/client.rb +31 -15
  22. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
  23. data/lib/ably/version.rb +1 -1
  24. data/spec/acceptance/realtime/auth_spec.rb +3 -3
  25. data/spec/acceptance/realtime/channel_spec.rb +10 -0
  26. data/spec/acceptance/realtime/client_spec.rb +72 -16
  27. data/spec/acceptance/realtime/connection_failures_spec.rb +26 -11
  28. data/spec/acceptance/realtime/connection_spec.rb +36 -17
  29. data/spec/acceptance/realtime/presence_history_spec.rb +0 -58
  30. data/spec/acceptance/realtime/presence_spec.rb +54 -0
  31. data/spec/acceptance/realtime/push_admin_spec.rb +3 -19
  32. data/spec/acceptance/rest/auth_spec.rb +6 -75
  33. data/spec/acceptance/rest/base_spec.rb +8 -4
  34. data/spec/acceptance/rest/channel_spec.rb +42 -4
  35. data/spec/acceptance/rest/client_spec.rb +121 -26
  36. data/spec/acceptance/rest/push_admin_spec.rb +3 -19
  37. data/spec/shared/client_initializer_behaviour.rb +131 -8
  38. data/spec/spec_helper.rb +1 -0
  39. data/spec/support/serialization_helper.rb +21 -0
  40. data/spec/support/test_app.rb +2 -2
  41. data/spec/unit/realtime/client_spec.rb +19 -6
  42. metadata +20 -15
  43. data/.travis.yml +0 -19
@@ -3,6 +3,9 @@ require 'json'
3
3
  require 'logger'
4
4
  require 'uri'
5
5
 
6
+ require 'typhoeus'
7
+ require 'typhoeus/adapters/faraday'
8
+
6
9
  require 'ably/rest/middleware/exceptions'
7
10
 
8
11
  module Ably
@@ -32,6 +35,13 @@ module Ably
32
35
 
33
36
  FALLBACK_RETRY_TIMEOUT = 10 * 60
34
37
 
38
+ # Faraday 1.0 introduced new error types, however we want to support Faraday <1 too which only used Faraday::ClientError
39
+ FARADAY_CLIENT_OR_SERVER_ERRORS = if defined?(Faraday::ParsingError)
40
+ [Faraday::ClientError, Faraday::ServerError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ParsingError]
41
+ else
42
+ Faraday::ClientError
43
+ end
44
+
35
45
  def_delegators :auth, :client_id, :auth_options
36
46
 
37
47
  # Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
@@ -174,16 +184,18 @@ module Ably
174
184
  @idempotent_rest_publishing = options.delete(:idempotent_rest_publishing) || Ably.major_minor_version_numeric > 1.1
175
185
 
176
186
 
177
- if options[:fallback_hosts_use_default] && options[:fallback_jhosts]
178
- raise ArgumentError, "fallback_hosts_use_default cannot be set to trye when fallback_jhosts is also provided"
187
+ if options[:fallback_hosts_use_default] && options[:fallback_hosts]
188
+ raise ArgumentError, "fallback_hosts_use_default cannot be set to try when fallback_hosts is also provided"
179
189
  end
180
190
  @fallback_hosts = case
181
191
  when options.delete(:fallback_hosts_use_default)
182
192
  Ably::FALLBACK_HOSTS
183
193
  when options_fallback_hosts = options.delete(:fallback_hosts)
184
194
  options_fallback_hosts
185
- when environment || custom_host || options[:realtime_host] || custom_port || custom_tls_port
195
+ when custom_host || options[:realtime_host] || custom_port || custom_tls_port
186
196
  []
197
+ when environment
198
+ CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES.map { |host| "#{environment}#{host}" }
187
199
  else
188
200
  Ably::FALLBACK_HOSTS
189
201
  end
@@ -195,6 +207,8 @@ module Ably
195
207
  @http_defaults = HTTP_DEFAULTS.dup
196
208
  options.each do |key, val|
197
209
  if http_key = key[/^http_(.+)/, 1]
210
+ # Typhoeus converts decimal durations to milliseconds, so 0.0001 timeout is treated as 0 (no timeout)
211
+ val = 0.001 if val.kind_of?(Numeric) && (val > 0) && (val < 0.001)
198
212
  @http_defaults[http_key.to_sym] = val if val && @http_defaults.has_key?(http_key.to_sym)
199
213
  end
200
214
  end
@@ -336,14 +350,14 @@ module Ably
336
350
  #
337
351
  # @return [Ably::Models::HttpPaginatedResponse<>]
338
352
  def request(method, path, params = {}, body = nil, headers = {}, options = {})
339
- raise "Method #{method.to_s.upcase} not supported" unless [:get, :put, :post].include?(method.to_sym)
353
+ raise "Method #{method.to_s.upcase} not supported" unless %i(get put patch post delete).include?(method.to_sym)
340
354
 
341
355
  response = case method.to_sym
342
- when :get
356
+ when :get, :delete
343
357
  reauthorize_on_authorization_failure do
344
358
  send_request(method, path, params, headers: headers)
345
359
  end
346
- when :post
360
+ when :post, :patch, :put
347
361
  path_with_params = Addressable::URI.new
348
362
  path_with_params.query_values = params || {}
349
363
  query = path_with_params.query
@@ -559,12 +573,14 @@ module Ably
559
573
  request.options.context = {} if request.options.context.nil?
560
574
  request.options.context[:request_id] = request_id
561
575
  end
576
+ if options[:qs_params]
577
+ request.params.merge!(options[:qs_params])
578
+ end
562
579
  unless options[:send_auth_header] == false
563
580
  request.headers[:authorization] = auth.auth_header
564
- if options[:headers]
565
- options[:headers].map do |key, val|
566
- request.headers[key] = val
567
- end
581
+
582
+ options[:headers].to_h.merge(auth.extra_auth_headers).map do |key, val|
583
+ request.headers[key] = val
568
584
  end
569
585
  end
570
586
  end.tap do
@@ -578,7 +594,7 @@ module Ably
578
594
  end
579
595
  end
580
596
 
581
- rescue Faraday::TimeoutError, Faraday::ClientError, Ably::Exceptions::ServerError => error
597
+ rescue *([Faraday::TimeoutError, Ably::Exceptions::ServerError] + FARADAY_CLIENT_OR_SERVER_ERRORS) => error
582
598
  retry_sequence_id ||= SecureRandom.urlsafe_base64(4)
583
599
  time_passed = Time.now - requested_at
584
600
 
@@ -598,7 +614,7 @@ module Ably
598
614
  case error
599
615
  when Faraday::TimeoutError
600
616
  raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT, error, { request_id: request_id })
601
- when Faraday::ClientError
617
+ when *FARADAY_CLIENT_OR_SERVER_ERRORS
602
618
  # request_id is also available in the request context
603
619
  raise Ably::Exceptions::ConnectionError.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error, { request_id: request_id })
604
620
  else
@@ -656,7 +672,7 @@ module Ably
656
672
  }
657
673
  end
658
674
 
659
- # Return a Faraday middleware stack to initiate the Faraday::Connection with
675
+ # Return a Faraday middleware stack to initiate the Faraday::RackBuilder with
660
676
  #
661
677
  # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
662
678
  def middleware
@@ -668,8 +684,8 @@ module Ably
668
684
 
669
685
  setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true
670
686
 
671
- # Set Faraday's HTTP adapter
672
- builder.adapter :excon
687
+ # Set Faraday's HTTP adapter with support for HTTP/2
688
+ builder.adapter :typhoeus, http_version: :httpv2_0
673
689
  end
674
690
  end
675
691
 
@@ -7,7 +7,10 @@ module Ably
7
7
  class FailIfUnsupportedMimeType < Faraday::Response::Middleware
8
8
  def on_complete(env)
9
9
  unless env.response_headers['Ably-Middleware-Parsed'] == true
10
- unless (500..599).include?(env.status)
10
+ # Ignore empty body with success status code for no body response
11
+ return if env.body.to_s.empty? && env.status == 204
12
+
13
+ unless (500..599).include?(env.status)
11
14
  raise Ably::Exceptions::InvalidResponseBody,
12
15
  "Content Type #{env.response_headers['Content-Type']} is not supported by this client library"
13
16
  end
data/lib/ably/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Ably
2
- VERSION = '1.1.2'
2
+ VERSION = '1.1.6'
3
3
  PROTOCOL_VERSION = '1.1'
4
4
 
5
5
  # Allow a variant to be configured for all instances of this client library
@@ -1058,7 +1058,7 @@ describe Ably::Realtime::Auth, :event_machine do
1058
1058
 
1059
1059
  it 'disconnected includes and invalid signature message' do
1060
1060
  client.connection.once(:disconnected) do |state_change|
1061
- expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil
1061
+ expect(state_change.reason.message.match(/signature verification failed/i)).to_not be_nil
1062
1062
  expect(state_change.reason.code).to eql(40144)
1063
1063
  stop_reactor
1064
1064
  end
@@ -1111,7 +1111,7 @@ describe Ably::Realtime::Auth, :event_machine do
1111
1111
 
1112
1112
  it 'authentication fails and reason for disconnection is invalid signature' do
1113
1113
  client.connection.once(:disconnected) do |state_change|
1114
- expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil
1114
+ expect(state_change.reason.message.match(/signature verification failed/i)).to_not be_nil
1115
1115
  expect(state_change.reason.code).to eql(40144)
1116
1116
  stop_reactor
1117
1117
  end
@@ -1143,7 +1143,7 @@ describe Ably::Realtime::Auth, :event_machine do
1143
1143
 
1144
1144
  it 'fails with an invalid signature error' do
1145
1145
  client.connection.once(:disconnected) do |state_change|
1146
- expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil
1146
+ expect(state_change.reason.message.match(/signature verification failed/i)).to_not be_nil
1147
1147
  expect(state_change.reason.code).to eql(40144)
1148
1148
  stop_reactor
1149
1149
  end
@@ -83,6 +83,16 @@ describe Ably::Realtime::Channel, :event_machine do
83
83
  end
84
84
  end
85
85
 
86
+ it 'sets attach_serial property after the attachment (#RTL15a)' do
87
+ expect(channel.properties.attach_serial).to be_nil
88
+
89
+ channel.attach
90
+ channel.on(:attached) do
91
+ expect(channel.properties.attach_serial).to_not be_nil
92
+ stop_reactor
93
+ end
94
+ end
95
+
86
96
  it 'sends an ATTACH and waits for an ATTACHED (#RTL4c)' do
87
97
  connection.once(:connected) do
88
98
  attach_count = 0
@@ -87,22 +87,6 @@ describe Ably::Realtime::Client, :event_machine do
87
87
  end
88
88
  end
89
89
  end
90
-
91
- context 'with client_id' do
92
- let(:client_options) do
93
- default_options.merge(client_id: random_str)
94
- end
95
- it 'connects using token auth' do
96
- run_reactor do
97
- connection.on(:connected) do
98
- expect(connection.state).to eq(:connected)
99
- expect(auth_params[:access_token]).to_not be_nil
100
- expect(auth_params[:key]).to be_nil
101
- stop_reactor
102
- end
103
- end
104
- end
105
- end
106
90
  end
107
91
  end
108
92
 
@@ -249,6 +233,8 @@ describe Ably::Realtime::Client, :event_machine do
249
233
 
250
234
  context '#request (#RSC19*)' do
251
235
  let(:client_options) { default_options.merge(key: api_key) }
236
+ let(:device_id) { random_str }
237
+ let(:endpoint) { subject.rest_client.endpoint }
252
238
 
253
239
  context 'get' do
254
240
  it 'returns an HttpPaginatedResponse object' do
@@ -297,6 +283,76 @@ describe Ably::Realtime::Client, :event_machine do
297
283
  end
298
284
  end
299
285
  end
286
+
287
+
288
+ context 'post', :webmock do
289
+ before do
290
+ stub_request(:delete, "#{endpoint}/push/deviceRegistrations/#{device_id}/resetUpdateToken").
291
+ to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
292
+ end
293
+
294
+ it 'supports post' do
295
+ subject.request(:delete, "push/deviceRegistrations/#{device_id}/resetUpdateToken").callback do |response|
296
+ expect(response).to be_success
297
+ stop_reactor
298
+ end
299
+ end
300
+ end
301
+
302
+ context 'delete', :webmock do
303
+ before do
304
+ stub_request(:delete, "#{endpoint}/push/channelSubscriptions?deviceId=#{device_id}").
305
+ to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
306
+ end
307
+
308
+ it 'supports delete' do
309
+ subject.request(:delete, "/push/channelSubscriptions", { deviceId: device_id}).callback do |response|
310
+ expect(response).to be_success
311
+ stop_reactor
312
+ end
313
+ end
314
+ end
315
+
316
+ context 'patch', :webmock do
317
+ let(:body_params) { { 'metadata' => { 'key' => 'value' } } }
318
+
319
+ before do
320
+ stub_request(:patch, "#{endpoint}/push/deviceRegistrations/#{device_id}")
321
+ .with(body: serialize_body(body_params, protocol))
322
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
323
+ end
324
+
325
+ it 'supports patch' do
326
+ subject.request(:patch, "/push/deviceRegistrations/#{device_id}", {}, body_params).callback do |response|
327
+ expect(response).to be_success
328
+ stop_reactor
329
+ end
330
+ end
331
+ end
332
+
333
+ context 'put', :webmock do
334
+ let(:body_params) do
335
+ {
336
+ 'id' => random_str,
337
+ 'platform' => 'ios',
338
+ 'formFactor' => 'phone',
339
+ 'metadata' => { 'key' => 'value' }
340
+ }
341
+ end
342
+
343
+ before do
344
+ stub_request(:put, "#{endpoint}/push/deviceRegistrations/#{device_id}")
345
+ .with(body: serialize_body(body_params, protocol))
346
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
347
+ end
348
+
349
+ it 'supports put' do
350
+ subject.request(:put, "/push/deviceRegistrations/#{device_id}", {}, body_params).callback do |response|
351
+ expect(response).to be_success
352
+ stop_reactor
353
+ end
354
+ end
355
+ end
300
356
  end
301
357
 
302
358
  context '#publish (#TBC)' do
@@ -156,10 +156,20 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
156
156
 
157
157
  stub_request(:get, auth_url).
158
158
  to_return do |request|
159
- sleep Ably::Rest::Client::HTTP_DEFAULTS.fetch(:request_timeout)
160
- { status: [500, "Internal Server Error"] }
161
- end.then.
162
- to_return(:status => 201, :body => token_response.to_json, :headers => { 'Content-Type' => 'application/json' })
159
+ sleep Ably::Rest::Client::HTTP_DEFAULTS.fetch(:request_timeout)
160
+ { status: [500, "Internal Server Error"] }
161
+ end.then.
162
+ to_return(:status => 201, :body => token_response.to_json, :headers => { 'Content-Type' => 'application/json' })
163
+
164
+ stub_request(:get, 'https://internet-up.ably-realtime.com/is-the-internet-up.txt')
165
+ .with(
166
+ headers: {
167
+ 'Accept-Encoding' => 'gzip, compressed',
168
+ 'Connection' => 'close',
169
+ 'Host' => 'internet-up.ably-realtime.com',
170
+ 'User-Agent' => 'EventMachine HttpClient'
171
+ }
172
+ ).to_return(status: 200, body: 'yes\n', headers: { 'Content-Type' => 'text/plain' })
163
173
  end
164
174
 
165
175
  specify 'the connection moves to the disconnected state and tries again, returning again to the disconnected state (#RSA4c, #RSA4c1, #RSA4c2)' do
@@ -1423,14 +1433,19 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
1423
1433
  let(:expected_host) { "#{environment}-#{Ably::Realtime::Client::DOMAIN}" }
1424
1434
  let(:client_options) { timeout_options.merge(environment: environment) }
1425
1435
 
1426
- it 'does not use a fallback host by default' do
1427
- expect(connection).to receive(:create_transport).exactly(retry_count_for_all_states).times do |host|
1428
- expect(host).to eql(expected_host)
1429
- raise EventMachine::ConnectionError
1430
- end
1436
+ context ':fallback_hosts_use_default is unset' do
1437
+ let(:max_time_in_state_for_tests) { 8 }
1438
+ let(:expected_hosts) { Ably::CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES.map { |suffix| "#{environment}#{suffix}" } + [expected_host] }
1439
+ let(:fallback_hosts_used) { Array.new }
1440
+
1441
+ it 'uses fallback hosts by default' do
1442
+ allow(connection).to receive(:create_transport) do |host|
1443
+ fallback_hosts_used << host
1444
+ raise EventMachine::ConnectionError
1445
+ end
1431
1446
 
1432
- connection.once(:suspended) do
1433
1447
  connection.once(:suspended) do
1448
+ expect(fallback_hosts_used.uniq).to match_array(expected_hosts)
1434
1449
  stop_reactor
1435
1450
  end
1436
1451
  end
@@ -1508,7 +1523,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
1508
1523
  end
1509
1524
 
1510
1525
  context 'with production environment' do
1511
- let(:custom_hosts) { %w(A.ably-realtime.com B.ably-realtime.com) }
1526
+ let(:custom_hosts) { %w(a.ably-realtime.com b.ably-realtime.com) }
1512
1527
  before do
1513
1528
  stub_const 'Ably::FALLBACK_HOSTS', custom_hosts
1514
1529
  end
@@ -77,18 +77,6 @@ describe Ably::Realtime::Connection, :event_machine do
77
77
  end
78
78
  end
79
79
  end
80
-
81
- context 'with implicit authorisation' do
82
- let(:client_options) { default_options.merge(client_id: 'force_token_auth') }
83
-
84
- it 'uses the token created by the implicit authorisation' do
85
- expect(client.rest_client.auth).to receive(:request_token).once.and_call_original
86
-
87
- connection.once(:connected) do
88
- stop_reactor
89
- end
90
- end
91
- end
92
80
  end
93
81
 
94
82
  context 'that expire' do
@@ -134,7 +122,7 @@ describe Ably::Realtime::Connection, :event_machine do
134
122
  end
135
123
  end
136
124
 
137
- context 'with immediately expired token' do
125
+ context 'with immediately expired token and no fallback hosts' do
138
126
  let(:ttl) { 0.001 }
139
127
  let(:auth_requests) { [] }
140
128
  let(:token_callback) do
@@ -143,7 +131,7 @@ describe Ably::Realtime::Connection, :event_machine do
143
131
  Ably::Rest::Client.new(default_options).auth.request_token(ttl: ttl).token
144
132
  end
145
133
  end
146
- let(:client_options) { default_options.merge(auth_callback: token_callback) }
134
+ let(:client_options) { default_options.merge(auth_callback: token_callback, fallback_hosts: []) }
147
135
 
148
136
  it 'renews the token on connect, and makes one immediate subsequent attempt to obtain a new token (#RSA4b)' do
149
137
  started_at = Time.now.to_f
@@ -158,7 +146,7 @@ describe Ably::Realtime::Connection, :event_machine do
158
146
  end
159
147
 
160
148
  context 'when disconnected_retry_timeout is 0.5 seconds' do
161
- let(:client_options) { default_options.merge(disconnected_retry_timeout: 0.5, auth_callback: token_callback) }
149
+ let(:client_options) { default_options.merge(disconnected_retry_timeout: 0.5, auth_callback: token_callback, fallback_hosts: []) }
162
150
 
163
151
  it 'renews the token on connect, and continues to attempt renew based on the retry schedule' do
164
152
  disconnect_count = 0
@@ -184,7 +172,7 @@ describe Ably::Realtime::Connection, :event_machine do
184
172
  end
185
173
 
186
174
  context 'using implicit token auth' do
187
- let(:client_options) { default_options.merge(use_token_auth: true, default_token_params: { ttl: ttl }) }
175
+ let(:client_options) { default_options.merge(use_token_auth: true, default_token_params: { ttl: ttl }, fallback_hosts: []) }
188
176
 
189
177
  before do
190
178
  stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', -10 # ensure client lib thinks token is still valid
@@ -453,7 +441,9 @@ describe Ably::Realtime::Connection, :event_machine do
453
441
  end
454
442
  end
455
443
 
456
- context '#connect' do
444
+ context '#connect with no fallbacks' do
445
+ let(:client_options) { default_options.merge(fallback_hosts: []) }
446
+
457
447
  it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
458
448
  expect(connection.connect).to be_a(Ably::Util::SafeDeferrable)
459
449
  stop_reactor
@@ -1179,6 +1169,7 @@ describe Ably::Realtime::Connection, :event_machine do
1179
1169
  host: 'this.host.does.not.exist.com'
1180
1170
  )
1181
1171
  )
1172
+ allow(client).to receive(:fallback_hosts).and_return([])
1182
1173
 
1183
1174
  connection.transition_state_machine! :disconnected
1184
1175
  end
@@ -1879,5 +1870,33 @@ describe Ably::Realtime::Connection, :event_machine do
1879
1870
  end
1880
1871
  end
1881
1872
  end
1873
+
1874
+ context 'transport_params (#RTC1f)' do
1875
+ let(:client_options) { default_options.merge(transport_params: { 'extra_param' => 'extra_param' }) }
1876
+
1877
+ it 'pases transport_params to query' do
1878
+ expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1879
+ uri = URI.parse(url)
1880
+ expect(CGI::parse(uri.query)['extra_param'][0]).to eq('extra_param')
1881
+ stop_reactor
1882
+ end
1883
+
1884
+ client
1885
+ end
1886
+
1887
+ context 'when changing default param' do
1888
+ let(:client_options) { default_options.merge(transport_params: { v: '1.0' }) }
1889
+
1890
+ it 'overrides default param (#RTC1f1)' do
1891
+ expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1892
+ uri = URI.parse(url)
1893
+ expect(CGI::parse(uri.query)['v'][0]).to eq('1.0')
1894
+ stop_reactor
1895
+ end
1896
+
1897
+ client
1898
+ end
1899
+ end
1900
+ end
1882
1901
  end
1883
1902
  end