ably 0.8.3 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -3
  3. data/lib/ably/auth.rb +1 -1
  4. data/lib/ably/exceptions.rb +3 -0
  5. data/lib/ably/models/channel_state_change.rb +41 -0
  6. data/lib/ably/models/connection_state_change.rb +43 -0
  7. data/lib/ably/models/message.rb +1 -1
  8. data/lib/ably/models/presence_message.rb +1 -1
  9. data/lib/ably/models/protocol_message.rb +2 -1
  10. data/lib/ably/modules/state_emitter.rb +4 -1
  11. data/lib/ably/modules/uses_state_machine.rb +28 -4
  12. data/lib/ably/realtime/channel.rb +11 -3
  13. data/lib/ably/realtime/channel/channel_manager.rb +24 -4
  14. data/lib/ably/realtime/channel/channel_state_machine.rb +20 -11
  15. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +4 -4
  16. data/lib/ably/realtime/connection.rb +1 -0
  17. data/lib/ably/realtime/connection/connection_manager.rb +33 -21
  18. data/lib/ably/realtime/connection/connection_state_machine.rb +24 -16
  19. data/lib/ably/rest/channel.rb +3 -2
  20. data/lib/ably/util/crypto.rb +15 -0
  21. data/lib/ably/version.rb +1 -1
  22. data/spec/acceptance/realtime/channel_spec.rb +155 -9
  23. data/spec/acceptance/realtime/client_spec.rb +2 -2
  24. data/spec/acceptance/realtime/connection_failures_spec.rb +8 -4
  25. data/spec/acceptance/realtime/connection_spec.rb +122 -11
  26. data/spec/acceptance/realtime/message_spec.rb +119 -3
  27. data/spec/acceptance/realtime/presence_spec.rb +34 -13
  28. data/spec/acceptance/rest/channel_spec.rb +9 -0
  29. data/spec/acceptance/rest/client_spec.rb +10 -0
  30. data/spec/unit/models/channel_state_change_spec.rb +44 -0
  31. data/spec/unit/models/connection_state_change_spec.rb +54 -0
  32. data/spec/unit/util/crypto_spec.rb +18 -0
  33. metadata +8 -2
@@ -55,6 +55,7 @@ module Ably
55
55
  )
56
56
  include Ably::Modules::StateEmitter
57
57
  include Ably::Modules::UsesStateMachine
58
+ ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
58
59
 
59
60
  # Expected format for a connection recover key
60
61
  RECOVER_REGEX = /^(?<recover>[\w-]+):(?<connection_serial>\-?\w+)$/
@@ -53,7 +53,7 @@ module Ably::Realtime
53
53
  end
54
54
 
55
55
  unless client.auth.authentication_security_requirements_met?
56
- connection.transition_state_machine :failed, Ably::Exceptions::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
56
+ connection.transition_state_machine :failed, reason: Ably::Exceptions::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
57
57
  return
58
58
  end
59
59
 
@@ -80,7 +80,8 @@ module Ably::Realtime
80
80
  # @api private
81
81
  def connection_opening_failed(error)
82
82
  logger.warn "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}"
83
- connection.transition_state_machine next_retry_state, Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, 80000)
83
+ next_state = get_next_retry_state_info
84
+ connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, 80000)
84
85
  end
85
86
 
86
87
  # Called whenever a new connection is made
@@ -155,11 +156,10 @@ module Ably::Realtime
155
156
  # When a connection is disconnected whilst connecting, attempt reconnect and/or set state to :suspended or :failed
156
157
  #
157
158
  # @api private
158
- def respond_to_transport_disconnected_when_connecting(current_transition)
159
+ def respond_to_transport_disconnected_when_connecting(error)
159
160
  return unless connection.disconnected? || connection.suspended? # do nothing if state has changed through an explicit request
160
161
  return unless retry_connection? # do not always reattempt connection or change state as client may be re-authorising
161
162
 
162
- error = current_transition.metadata
163
163
  if error.kind_of?(Ably::Models::ErrorInfo)
164
164
  renew_token_and_reconnect error if error.code == RESOLVABLE_ERROR_CODES.fetch(:token_expired)
165
165
  return
@@ -172,23 +172,22 @@ module Ably::Realtime
172
172
  return if connection_retry_for(:suspended)
173
173
 
174
174
  # Fallback if no other criteria met
175
- connection.transition_state_machine :failed, current_transition.metadata
175
+ connection.transition_state_machine :failed, reason: error
176
176
  end
177
177
 
178
178
  # When a connection is disconnected after connecting, attempt reconnect and/or set state to :suspended or :failed
179
179
  #
180
180
  # @api private
181
- def respond_to_transport_disconnected_whilst_connected(current_transition)
181
+ def respond_to_transport_disconnected_whilst_connected(error)
182
182
  logger.warn "ConnectionManager: Connection to #{connection.transport.url} was disconnected unexpectedly"
183
183
 
184
- error = current_transition.metadata
185
184
  if error.kind_of?(Ably::Models::ErrorInfo) && error.code != RESOLVABLE_ERROR_CODES.fetch(:token_expired)
186
185
  connection.emit :error, error
187
186
  logger.error "ConnectionManager: Error in Disconnected ProtocolMessage received from the server - #{error}"
188
187
  end
189
188
 
190
189
  destroy_transport
191
- respond_to_transport_disconnected_when_connecting current_transition
190
+ respond_to_transport_disconnected_when_connecting error
192
191
  end
193
192
 
194
193
  # {Ably::Models::ProtocolMessage ProtocolMessage Error} received from server.
@@ -198,13 +197,13 @@ module Ably::Realtime
198
197
  def error_received_from_server(error)
199
198
  case error.code
200
199
  when RESOLVABLE_ERROR_CODES.fetch(:token_expired)
201
- connection.transition_state_machine :disconnected
200
+ connection.transition_state_machine :disconnected, retry_in: 0
202
201
  connection.unsafe_once_or_if(:disconnected) do
203
202
  renew_token_and_reconnect error
204
203
  end
205
204
  else
206
205
  logger.error "ConnectionManager: Error #{error.class.name} code #{error.code} received from server '#{error.message}', transitioning to failed state"
207
- connection.transition_state_machine :failed, error
206
+ connection.transition_state_machine :failed, reason: error
208
207
  end
209
208
  end
210
209
 
@@ -249,12 +248,26 @@ module Ably::Realtime
249
248
  timers.fetch(key, []).each(&:cancel)
250
249
  end
251
250
 
252
- def next_retry_state
253
- if connection_retry_from_suspended_state? || time_passed_since_disconnected > CONNECT_RETRY_CONFIG.fetch(:disconnected).fetch(:max_time_in_state)
251
+ def get_next_retry_state_info
252
+ retry_state = if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
254
253
  :suspended
255
254
  else
256
255
  :disconnected
257
256
  end
257
+ {
258
+ state: retry_state,
259
+ pause: next_retry_pause(retry_state)
260
+ }
261
+ end
262
+
263
+ def next_retry_pause(retry_state)
264
+ return nil unless CONNECT_RETRY_CONFIG.fetch(retry_state)
265
+
266
+ if retries_for_state(retry_state, ignore_states: [:connecting]).empty?
267
+ 0
268
+ else
269
+ CONNECT_RETRY_CONFIG.fetch(retry_state).fetch(:retry_every)
270
+ end
258
271
  end
259
272
 
260
273
  def connection_retry_from_suspended_state?
@@ -348,13 +361,12 @@ module Ably::Realtime
348
361
  connection.transition_state_machine :closed
349
362
  elsif !connection.closed? && !connection.disconnected?
350
363
  exception = if reason
351
- Ably::Exceptions::ConnectionClosed.new(reason)
352
- end
353
- if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
354
- connection.transition_state_machine :suspended, exception
364
+ Ably::Exceptions::TransportClosed.new(reason, nil, 80003)
355
365
  else
356
- connection.transition_state_machine :disconnected, exception
366
+ Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, 80003)
357
367
  end
368
+ next_state = get_next_retry_state_info
369
+ connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: exception
358
370
  end
359
371
  end
360
372
  end
@@ -362,7 +374,7 @@ module Ably::Realtime
362
374
  def renew_token_and_reconnect(error)
363
375
  if client.auth.token_renewable?
364
376
  if @renewing_token
365
- connection.transition_state_machine :failed, error
377
+ connection.transition_state_machine :failed, reason: error
366
378
  return
367
379
  end
368
380
 
@@ -383,18 +395,18 @@ module Ably::Realtime
383
395
  if token_details && !token_details.expired?
384
396
  connection.connect
385
397
  else
386
- connection.transition_state_machine :failed, error unless connection.failed?
398
+ connection.transition_state_machine :failed, reason: error unless connection.failed?
387
399
  end
388
400
  end
389
401
 
390
402
  authorise_deferrable.errback do |auth_error|
391
403
  logger.error "ConnectionManager: Error authorising following token expiry: #{auth_error}"
392
- connection.transition_state_machine :failed, auth_error
404
+ connection.transition_state_machine :failed, reason: auth_error
393
405
  end
394
406
  end
395
407
  else
396
408
  logger.error "ConnectionManager: Token has expired and is not renewable - #{error}"
397
- connection.transition_state_machine :failed, error
409
+ connection.transition_state_machine :failed, reason: error
398
410
  end
399
411
  end
400
412
 
@@ -41,23 +41,25 @@ module Ably::Realtime
41
41
  end
42
42
 
43
43
  before_transition(to: [:connected]) do |connection, current_transition|
44
- connection.manager.connected current_transition.metadata
44
+ connection.manager.connected current_transition.metadata.protocol_message
45
45
  end
46
46
 
47
47
  after_transition(to: [:connected]) do |connection, current_transition|
48
- protocol_message = current_transition.metadata
49
- if is_error_type?(protocol_message.error)
50
- connection.logger.warn "ConnectionManager: Connected with error - #{protocol_message.error.message}"
51
- connection.emit :error, protocol_message.error
48
+ error = current_transition.metadata.reason
49
+ if is_error_type?(error)
50
+ connection.logger.warn "ConnectionManager: Connected with error - #{error.message}"
51
+ connection.emit :error, error
52
52
  end
53
53
  end
54
54
 
55
55
  after_transition(to: [:disconnected, :suspended], from: [:connecting]) do |connection, current_transition|
56
- connection.manager.respond_to_transport_disconnected_when_connecting current_transition
56
+ err = error_from_state_change(current_transition)
57
+ connection.manager.respond_to_transport_disconnected_when_connecting err
57
58
  end
58
59
 
59
60
  after_transition(to: [:disconnected], from: [:connected]) do |connection, current_transition|
60
- connection.manager.respond_to_transport_disconnected_whilst_connected current_transition
61
+ err = error_from_state_change(current_transition)
62
+ connection.manager.respond_to_transport_disconnected_whilst_connected err
61
63
  end
62
64
 
63
65
  after_transition(to: [:disconnected, :suspended]) do |connection|
@@ -65,7 +67,8 @@ module Ably::Realtime
65
67
  end
66
68
 
67
69
  before_transition(to: [:failed]) do |connection, current_transition|
68
- connection.manager.fail current_transition.metadata
70
+ err = error_from_state_change(current_transition)
71
+ connection.manager.fail err
69
72
  end
70
73
 
71
74
  after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
@@ -82,24 +85,29 @@ module Ably::Realtime
82
85
 
83
86
  # Transitions responsible for updating connection#error_reason
84
87
  before_transition(to: [:disconnected, :suspended, :failed]) do |connection, current_transition|
85
- connection.set_failed_connection_error_reason current_transition.metadata
88
+ err = error_from_state_change(current_transition)
89
+ connection.set_failed_connection_error_reason err
86
90
  end
87
91
 
88
92
  before_transition(to: [:connected, :closed]) do |connection, current_transition|
89
- error = if current_transition.metadata.kind_of?(Ably::Models::ProtocolMessage)
90
- current_transition.metadata.error
91
- else
92
- current_transition.metadata
93
- end
93
+ err = error_from_state_change(current_transition)
94
94
 
95
- if is_error_type?(error)
96
- connection.set_failed_connection_error_reason error
95
+ if err
96
+ connection.set_failed_connection_error_reason err
97
97
  else
98
98
  # Connected & Closed are "healthy" final states so reset the error reason
99
99
  connection.clear_error_reason
100
100
  end
101
101
  end
102
102
 
103
+ def self.error_from_state_change(current_transition)
104
+ # ConnectionStateChange object is always passed in current_transition metadata object
105
+ connection_state_change = current_transition.metadata
106
+ # Reason attribute contains errors
107
+ err = connection_state_change && connection_state_change.reason
108
+ err if is_error_type?(err)
109
+ end
110
+
103
111
  private
104
112
  def connection
105
113
  object
@@ -34,6 +34,7 @@ module Ably
34
34
  #
35
35
  # @param name [String, Array<Ably::Models::Message|Hash>, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs
36
36
  # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
37
+ # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
37
38
  # @return [Boolean] true if the message was published, otherwise false
38
39
  #
39
40
  # @example
@@ -54,13 +55,13 @@ module Ably
54
55
  # ]
55
56
  # channel.publish messages
56
57
  #
57
- def publish(name, data = nil)
58
+ def publish(name, data = nil, attributes = {})
58
59
  messages = if name.kind_of?(Enumerable)
59
60
  name
60
61
  else
61
62
  ensure_utf_8 :name, name, allow_nil: true
62
63
  ensure_supported_payload data
63
- [{ name: name, data: data }]
64
+ [{ name: name, data: data }.merge(attributes)]
64
65
  end
65
66
 
66
67
  payload = messages.map do |message|
@@ -37,6 +37,21 @@ module Ably::Util
37
37
  @options = DEFAULTS.merge(options).freeze
38
38
  end
39
39
 
40
+ # Obtain a default CipherParams. This uses default algorithm, mode and
41
+ # padding and key length. A key and IV are generated using the default
42
+ # system SecureRandom; the key may be obtained from the returned CipherParams
43
+ # for out-of-band distribution to other clients.
44
+ #
45
+ # @return [Hash] CipherParam options Hash with attributes :key, :algorithn, :mode, :key_length
46
+ #
47
+ def self.get_default_params(key = nil)
48
+ params = DEFAULTS.merge(key: key)
49
+ params[:key_length] = key.unpack('b*').first.length if params[:key]
50
+ cipher_type = "#{params[:algorithm]}-#{params[:key_length]}-#{params[:mode]}"
51
+ params[:key] = OpenSSL::Cipher.new(cipher_type.upcase).random_key unless params[:key]
52
+ params
53
+ end
54
+
40
55
  # Encrypt payload using configured Cipher
41
56
  #
42
57
  # @param [String] payload the payload to be encrypted
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = '0.8.3'
2
+ VERSION = '0.8.4'
3
3
  end
@@ -92,7 +92,7 @@ describe Ably::Realtime::Channel, :event_machine do
92
92
 
93
93
  it 'reattaches' do
94
94
  channel.attach do
95
- channel.transition_state_machine :failed, RuntimeError.new
95
+ channel.transition_state_machine :failed, reason: RuntimeError.new
96
96
  expect(channel).to be_failed
97
97
  channel.attach do
98
98
  expect(channel).to be_attached
@@ -154,9 +154,9 @@ describe Ably::Realtime::Channel, :event_machine do
154
154
 
155
155
  it 'emits failed event' do
156
156
  restricted_channel.attach
157
- restricted_channel.on(:failed) do |error|
157
+ restricted_channel.on(:failed) do |connection_state|
158
158
  expect(restricted_channel.state).to eq(:failed)
159
- expect(error.status).to eq(401)
159
+ expect(connection_state.reason.status).to eq(401)
160
160
  stop_reactor
161
161
  end
162
162
  end
@@ -262,7 +262,7 @@ describe Ably::Realtime::Channel, :event_machine do
262
262
 
263
263
  it 'raises an exception' do
264
264
  channel.attach do
265
- channel.transition_state_machine :failed, RuntimeError.new
265
+ channel.transition_state_machine :failed, reason: RuntimeError.new
266
266
  expect(channel).to be_failed
267
267
  expect { channel.detach }.to raise_error Ably::Exceptions::InvalidStateChange
268
268
  stop_reactor
@@ -433,6 +433,19 @@ describe Ably::Realtime::Channel, :event_machine do
433
433
  end
434
434
  end
435
435
  end
436
+
437
+ context 'and additional attributes' do
438
+ let(:client_id) { random_str }
439
+
440
+ it 'publishes the message with the attributes and return true indicating success' do
441
+ channel.publish(name, data, client_id: client_id) do
442
+ channel.history do |page|
443
+ expect(page.items.first.client_id).to eql(client_id)
444
+ stop_reactor
445
+ end
446
+ end
447
+ end
448
+ end
436
449
  end
437
450
 
438
451
  context 'with an array of Hash objects with :name and :data attributes' do
@@ -666,7 +679,8 @@ describe Ably::Realtime::Channel, :event_machine do
666
679
  context 'an :attached channel' do
667
680
  it 'transitions state to :failed' do
668
681
  channel.attach do
669
- channel.on(:failed) do |error|
682
+ channel.on(:failed) do |connection_state_change|
683
+ error = connection_state_change.reason
670
684
  expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
671
685
  expect(error.code).to eql(80002)
672
686
  stop_reactor
@@ -688,7 +702,8 @@ describe Ably::Realtime::Channel, :event_machine do
688
702
 
689
703
  it 'updates the channel error_reason' do
690
704
  channel.attach do
691
- channel.on(:failed) do |error|
705
+ channel.on(:failed) do |connection_state_change|
706
+ error = connection_state_change.reason
692
707
  expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
693
708
  expect(error.code).to eql(80002)
694
709
  stop_reactor
@@ -734,7 +749,7 @@ describe Ably::Realtime::Channel, :event_machine do
734
749
  fake_error connection_error
735
750
  end
736
751
 
737
- channel.transition_state_machine :failed, original_error
752
+ channel.transition_state_machine :failed, reason: original_error
738
753
  end
739
754
  end
740
755
  end
@@ -783,8 +798,8 @@ describe Ably::Realtime::Channel, :event_machine do
783
798
  end
784
799
 
785
800
  context 'a :failed channel' do
786
- let(:original_error) { RuntimeError.new }
787
801
  let(:client_options) { default_options.merge(log_level: :fatal) }
802
+ let(:original_error) { Ably::Models::ErrorInfo.new(message: 'Error') }
788
803
 
789
804
  it 'remains in the :failed state and retains the error_reason' do
790
805
  channel.attach do
@@ -801,7 +816,7 @@ describe Ably::Realtime::Channel, :event_machine do
801
816
  client.connection.close
802
817
  end
803
818
 
804
- channel.transition_state_machine :failed, original_error
819
+ channel.transition_state_machine :failed, reason: original_error
805
820
  end
806
821
  end
807
822
  end
@@ -830,6 +845,75 @@ describe Ably::Realtime::Channel, :event_machine do
830
845
  end
831
846
  end
832
847
  end
848
+
849
+ context ':suspended' do
850
+ context 'an :attached channel' do
851
+ let(:client_options) { default_options.merge(log_level: :fatal) }
852
+
853
+ it 'transitions state to :detached' do
854
+ channel.attach do
855
+ channel.on(:detached) do
856
+ stop_reactor
857
+ end
858
+ client.connection.transition_state_machine :suspended
859
+ end
860
+ end
861
+ end
862
+
863
+ context 'a :detached channel' do
864
+ it 'remains in the :detached state' do
865
+ channel.attach do
866
+ channel.detach do
867
+ channel.on(:detached) { raise 'Detached state should not have been reached' }
868
+ channel.on(:error) { raise 'Error should not have been emitted' }
869
+
870
+ EventMachine.add_timer(1) do
871
+ expect(channel).to be_detached
872
+ stop_reactor
873
+ end
874
+
875
+ client.connection.transition_state_machine :suspended
876
+ end
877
+ end
878
+ end
879
+ end
880
+
881
+ context 'a :failed channel' do
882
+ let(:original_error) { RuntimeError.new }
883
+ let(:client_options) { default_options.merge(log_level: :fatal) }
884
+
885
+ it 'remains in the :failed state and retains the error_reason' do
886
+ channel.attach do
887
+ channel.once(:error) do
888
+ channel.on(:detached) { raise 'Detached state should not have been reached' }
889
+ channel.on(:error) { raise 'Error should not have been emitted' }
890
+
891
+ EventMachine.add_timer(1) do
892
+ expect(channel).to be_failed
893
+ expect(channel.error_reason).to eql(original_error)
894
+ stop_reactor
895
+ end
896
+
897
+ client.connection.transition_state_machine :suspended
898
+ end
899
+
900
+ channel.transition_state_machine :failed, reason: original_error
901
+ end
902
+ end
903
+ end
904
+
905
+ context 'a channel ATTACH request when connection SUSPENDED' do
906
+ it 'raises an exception' do
907
+ client.connect do
908
+ client.connection.once(:suspended) do
909
+ expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
910
+ stop_reactor
911
+ end
912
+ client.connection.transition_state_machine :suspended
913
+ end
914
+ end
915
+ end
916
+ end
833
917
  end
834
918
 
835
919
  describe '#presence' do
@@ -838,5 +922,67 @@ describe Ably::Realtime::Channel, :event_machine do
838
922
  stop_reactor
839
923
  end
840
924
  end
925
+
926
+ context 'channel state change' do
927
+ it 'emits a ChannelStateChange object' do
928
+ channel.on(:attached) do |channel_state_change|
929
+ expect(channel_state_change).to be_a(Ably::Models::ChannelStateChange)
930
+ stop_reactor
931
+ end
932
+ channel.attach
933
+ end
934
+
935
+ context 'ChannelStateChange object' do
936
+ it 'has current state' do
937
+ channel.on(:attached) do |channel_state_change|
938
+ expect(channel_state_change.current).to eq(:attached)
939
+ stop_reactor
940
+ end
941
+ channel.attach
942
+ end
943
+
944
+ it 'has a previous state' do
945
+ channel.on(:attached) do |channel_state_change|
946
+ expect(channel_state_change.previous).to eq(:attaching)
947
+ stop_reactor
948
+ end
949
+ channel.attach
950
+ end
951
+
952
+ it 'contains a private API protocol_message attribute that is used for special state change events', :api_private do
953
+ channel.on(:attached) do |channel_state_change|
954
+ expect(channel_state_change.protocol_message).to be_a(Ably::Models::ProtocolMessage)
955
+ expect(channel_state_change.reason).to be_nil
956
+ stop_reactor
957
+ end
958
+ channel.attach
959
+ end
960
+
961
+ it 'has an empty reason when there is no error' do
962
+ channel.on(:detached) do |channel_state_change|
963
+ expect(channel_state_change.reason).to be_nil
964
+ stop_reactor
965
+ end
966
+ channel.attach do
967
+ channel.detach
968
+ end
969
+ end
970
+
971
+ context 'on failure' do
972
+ let(:client_options) { default_options.merge(log_level: :none) }
973
+
974
+ it 'has a reason Error object when there is an error on the channel' do
975
+ channel.on(:failed) do |channel_state_change|
976
+ expect(channel_state_change.reason).to be_a(Ably::Exceptions::BaseAblyException)
977
+ stop_reactor
978
+ end
979
+ channel.attach do
980
+ error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
981
+ client.connection.manager.error_received_from_server error
982
+ end
983
+ end
984
+ end
985
+ end
986
+ end
841
987
  end
842
988
  end