ably 0.7.2 → 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/LICENSE.txt +1 -1
  2. data/README.md +107 -24
  3. data/SPEC.md +531 -398
  4. data/lib/ably/auth.rb +23 -15
  5. data/lib/ably/exceptions.rb +9 -0
  6. data/lib/ably/models/message.rb +17 -9
  7. data/lib/ably/models/paginated_resource.rb +12 -8
  8. data/lib/ably/models/presence_message.rb +18 -10
  9. data/lib/ably/models/protocol_message.rb +15 -4
  10. data/lib/ably/modules/async_wrapper.rb +4 -3
  11. data/lib/ably/modules/event_emitter.rb +31 -2
  12. data/lib/ably/modules/message_emitter.rb +77 -0
  13. data/lib/ably/modules/safe_deferrable.rb +71 -0
  14. data/lib/ably/modules/safe_yield.rb +41 -0
  15. data/lib/ably/modules/state_emitter.rb +28 -8
  16. data/lib/ably/realtime.rb +0 -5
  17. data/lib/ably/realtime/channel.rb +24 -29
  18. data/lib/ably/realtime/channel/channel_manager.rb +54 -11
  19. data/lib/ably/realtime/channel/channel_state_machine.rb +21 -6
  20. data/lib/ably/realtime/client.rb +7 -2
  21. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +29 -26
  22. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +4 -4
  23. data/lib/ably/realtime/connection.rb +41 -9
  24. data/lib/ably/realtime/connection/connection_manager.rb +72 -24
  25. data/lib/ably/realtime/connection/connection_state_machine.rb +26 -4
  26. data/lib/ably/realtime/connection/websocket_transport.rb +19 -6
  27. data/lib/ably/realtime/presence.rb +74 -208
  28. data/lib/ably/realtime/presence/members_map.rb +264 -0
  29. data/lib/ably/realtime/presence/presence_manager.rb +59 -0
  30. data/lib/ably/realtime/presence/presence_state_machine.rb +64 -0
  31. data/lib/ably/rest/channel.rb +1 -1
  32. data/lib/ably/rest/client.rb +6 -2
  33. data/lib/ably/rest/presence.rb +1 -1
  34. data/lib/ably/util/pub_sub.rb +3 -1
  35. data/lib/ably/util/safe_deferrable.rb +18 -0
  36. data/lib/ably/version.rb +1 -1
  37. data/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  38. data/spec/acceptance/realtime/channel_spec.rb +28 -6
  39. data/spec/acceptance/realtime/connection_failures_spec.rb +116 -46
  40. data/spec/acceptance/realtime/connection_spec.rb +55 -10
  41. data/spec/acceptance/realtime/message_spec.rb +32 -0
  42. data/spec/acceptance/realtime/presence_spec.rb +456 -96
  43. data/spec/acceptance/realtime/stats_spec.rb +2 -2
  44. data/spec/acceptance/realtime/time_spec.rb +2 -2
  45. data/spec/acceptance/rest/auth_spec.rb +75 -7
  46. data/spec/shared/client_initializer_behaviour.rb +8 -0
  47. data/spec/shared/safe_deferrable_behaviour.rb +71 -0
  48. data/spec/support/api_helper.rb +1 -1
  49. data/spec/support/event_machine_helper.rb +1 -1
  50. data/spec/support/test_app.rb +13 -7
  51. data/spec/unit/models/message_spec.rb +15 -14
  52. data/spec/unit/models/paginated_resource_spec.rb +4 -4
  53. data/spec/unit/models/presence_message_spec.rb +17 -17
  54. data/spec/unit/models/stat_spec.rb +4 -4
  55. data/spec/unit/modules/async_wrapper_spec.rb +28 -9
  56. data/spec/unit/modules/event_emitter_spec.rb +50 -0
  57. data/spec/unit/modules/state_emitter_spec.rb +76 -2
  58. data/spec/unit/realtime/channel_spec.rb +51 -20
  59. data/spec/unit/realtime/channels_spec.rb +3 -3
  60. data/spec/unit/realtime/connection_spec.rb +30 -0
  61. data/spec/unit/realtime/presence_spec.rb +52 -26
  62. data/spec/unit/realtime/safe_deferrable_spec.rb +12 -0
  63. metadata +85 -39
  64. checksums.yaml +0 -7
  65. data/.ruby-version.old +0 -1
@@ -101,7 +101,7 @@ module Ably
101
101
  # Retrieve the Ably service time
102
102
  #
103
103
  # @yield [Time] The time as reported by the Ably service
104
- # @return [EventMachine::Deferrable]
104
+ # @return [Ably::Util::SafeDeferrable]
105
105
  #
106
106
  def time(&success_callback)
107
107
  async_wrap(success_callback) do
@@ -116,7 +116,7 @@ module Ably
116
116
  #
117
117
  # @yield [Ably::Models::PaginatedResource<Ably::Models::Stat>] An Array of Stats
118
118
  #
119
- # @return [EventMachine::Deferrable]
119
+ # @return [Ably::Util::SafeDeferrable]
120
120
  #
121
121
  def stats(options = {}, &success_callback)
122
122
  async_wrap(success_callback) do
@@ -129,6 +129,11 @@ module Ably
129
129
  connection.close(&block)
130
130
  end
131
131
 
132
+ # (see Ably::Realtime::Connection#connect)
133
+ def connect(&block)
134
+ connection.connect(&block)
135
+ end
136
+
132
137
  # @!attribute [r] endpoint
133
138
  # @return [URI::Generic] Default Ably Realtime endpoint used for all requests
134
139
  def endpoint
@@ -41,6 +41,20 @@ module Ably::Realtime
41
41
  logger.debug "#{protocol_message.action} received: #{protocol_message}"
42
42
  end
43
43
 
44
+ if [:sync, :presence, :message].any? { |prevent_duplicate| protocol_message.action == prevent_duplicate }
45
+ if connection.serial && protocol_message.has_connection_serial? && protocol_message.connection_serial <= connection.serial
46
+ error_target = if protocol_message.channel
47
+ get_channel(protocol_message.channel)
48
+ else
49
+ connection
50
+ end
51
+ error_message = "Protocol error, duplicate message received for serial #{protocol_message.connection_serial}"
52
+ error_target.trigger :error, Ably::Exceptions::ProtocolError.new(error_message, 400, 80013)
53
+ logger.error error_message
54
+ return
55
+ end
56
+ end
57
+
44
58
  update_connection_recovery_info protocol_message
45
59
 
46
60
  case protocol_message.action
@@ -54,14 +68,14 @@ module Ably::Realtime
54
68
 
55
69
  when ACTION.Connect
56
70
  when ACTION.Connected
57
- connection.transition_state_machine :connected, protocol_message.error
71
+ connection.transition_state_machine :connected, protocol_message unless connection.connected?
58
72
 
59
73
  when ACTION.Disconnect, ACTION.Disconnected
60
- connection.transition_state_machine :disconnected, protocol_message.error
74
+ connection.transition_state_machine :disconnected, protocol_message.error unless connection.disconnected?
61
75
 
62
76
  when ACTION.Close
63
77
  when ACTION.Closed
64
- connection.transition_state_machine :closed
78
+ connection.transition_state_machine :closed unless connection.closed?
65
79
 
66
80
  when ACTION.Error
67
81
  if protocol_message.channel && !protocol_message.has_message_serial?
@@ -72,18 +86,22 @@ module Ably::Realtime
72
86
 
73
87
  when ACTION.Attach
74
88
  when ACTION.Attached
75
- get_channel(protocol_message.channel).transition_state_machine :attached, protocol_message
89
+ get_channel(protocol_message.channel).tap do |channel|
90
+ channel.transition_state_machine :attached, protocol_message unless channel.attached?
91
+ end
76
92
 
77
93
  when ACTION.Detach
78
94
  when ACTION.Detached
79
- get_channel(protocol_message.channel).transition_state_machine :detached
95
+ get_channel(protocol_message.channel).tap do |channel|
96
+ channel.transition_state_machine :detached unless channel.detached?
97
+ end
80
98
 
81
99
  when ACTION.Sync
82
100
  presence = get_channel(protocol_message.channel).presence
83
101
  protocol_message.presence.each do |presence_message|
84
102
  presence.__incoming_msgbus__.publish :sync, presence_message
85
103
  end
86
- presence.update_sync_serial protocol_message.channel_serial
104
+ presence.members.update_sync_serial protocol_message.channel_serial
87
105
 
88
106
  when ACTION.Presence
89
107
  presence = get_channel(protocol_message.channel).presence
@@ -98,7 +116,9 @@ module Ably::Realtime
98
116
  end
99
117
 
100
118
  else
101
- raise ArgumentError, "Protocol Message Action #{protocol_message.action} is unsupported by this MessageDispatcher"
119
+ error = Ably::Exceptions::ProtocolError.new("Protocol Message Action #{protocol_message.action} is unsupported by this MessageDispatcher", 400, 80013)
120
+ client.connection.trigger :error, error
121
+ logger.fatal error.message
102
122
  end
103
123
  end
104
124
 
@@ -116,24 +136,7 @@ module Ably::Realtime
116
136
  end
117
137
 
118
138
  def update_connection_recovery_info(protocol_message)
119
- if protocol_message.connection_key && (protocol_message.connection_key != connection.key)
120
- logger.debug "New connection ID set to #{protocol_message.connection_id} with connection key #{protocol_message.connection_key}"
121
- detach_attached_channels protocol_message.error if protocol_message.error
122
- connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
123
- end
124
-
125
- if protocol_message.has_connection_serial?
126
- connection.update_connection_serial protocol_message.connection_serial
127
- end
128
- end
129
-
130
- def detach_attached_channels(error)
131
- channels.select do |channel|
132
- channel.attached? || channel.attaching?
133
- end.each do |channel|
134
- logger.warn "Detaching channel '#{channel.name}': #{error}"
135
- channel.manager.suspend error
136
- end
139
+ connection.update_connection_serial protocol_message.connection_serial if protocol_message.has_connection_serial?
137
140
  end
138
141
 
139
142
  def ack_pending_queue_for_message_serial(ack_protocol_message)
@@ -166,7 +169,7 @@ module Ably::Realtime
166
169
 
167
170
  def drop_pending_queue_from_ack(ack_protocol_message)
168
171
  message_serial_up_to = ack_protocol_message.message_serial + ack_protocol_message.count - 1
169
- connection.__pending_message_queue__.drop_while do |protocol_message|
172
+ connection.__pending_message_ack_queue__.drop_while do |protocol_message|
170
173
  if protocol_message.message_serial <= message_serial_up_to
171
174
  yield protocol_message
172
175
  true
@@ -31,8 +31,8 @@ module Ably::Realtime
31
31
  connection.__outgoing_message_queue__
32
32
  end
33
33
 
34
- def pending_queue
35
- connection.__pending_message_queue__
34
+ def pending_ack_queue
35
+ connection.__pending_message_ack_queue__
36
36
  end
37
37
 
38
38
  def current_transport_outgoing_message_bus
@@ -47,7 +47,7 @@ module Ably::Realtime
47
47
  current_transport_outgoing_message_bus.publish :protocol_message, protocol_message
48
48
 
49
49
  if protocol_message.ack_required?
50
- pending_queue << protocol_message
50
+ pending_ack_queue << protocol_message
51
51
  else
52
52
  protocol_message.succeed protocol_message
53
53
  end
@@ -61,7 +61,7 @@ module Ably::Realtime
61
61
  end
62
62
 
63
63
  def setup_event_handlers
64
- connection.on(:connected) do
64
+ connection.unsafe_on(:connected) do
65
65
  deliver_queued_protocol_messages
66
66
  end
67
67
  end
@@ -37,6 +37,7 @@ module Ably
37
37
  class Connection
38
38
  include Ably::Modules::EventEmitter
39
39
  include Ably::Modules::Conversions
40
+ include Ably::Modules::SafeYield
40
41
  extend Ably::Modules::Enum
41
42
 
42
43
  # Valid Connection states
@@ -94,14 +95,14 @@ module Ably
94
95
  # An internal queue used to manage sent messages. You should never interface with this array directly
95
96
  # @return [Array]
96
97
  # @api private
97
- attr_reader :__pending_message_queue__
98
+ attr_reader :__pending_message_ack_queue__
98
99
 
99
100
  # @api public
100
101
  def initialize(client)
101
- @client = client
102
- @client_serial = -1
103
- @__outgoing_message_queue__ = []
104
- @__pending_message_queue__ = []
102
+ @client = client
103
+ @client_serial = -1
104
+ @__outgoing_message_queue__ = []
105
+ @__pending_message_ack_queue__ = []
105
106
 
106
107
  Client::IncomingMessageDispatcher.new client, self
107
108
  Client::OutgoingMessageDispatcher.new client, self
@@ -156,7 +157,7 @@ module Ably
156
157
  #
157
158
  # @return [void]
158
159
  #
159
- def ping
160
+ def ping(&block)
160
161
  raise RuntimeError, 'Cannot send a ping when connection is not open' if initialized?
161
162
  raise RuntimeError, 'Cannot send a ping when connection is in a closed or failed state' if closed? || failed?
162
163
 
@@ -166,7 +167,7 @@ module Ably
166
167
  if protocol_message.action == Ably::Models::ProtocolMessage::ACTION.Heartbeat
167
168
  __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
168
169
  time_passed = (Time.now.to_f * 1000 - started.to_f * 1000).to_i
169
- yield time_passed if block_given?
170
+ safe_yield block, time_passed if block_given?
170
171
  end
171
172
  end
172
173
 
@@ -202,7 +203,7 @@ module Ably
202
203
  "#{key}:#{serial}" if connection_resumable?
203
204
  end
204
205
 
205
- # Following a new connection being made, resumed or recovered, the connection ID, connection key
206
+ # Following a new connection being made, the connection ID, connection key
206
207
  # and message serial need to match the details provided by the server.
207
208
  #
208
209
  # @return [void]
@@ -290,7 +291,7 @@ module Ably
290
291
  # @api private
291
292
  def send_protocol_message(protocol_message)
292
293
  add_message_serial_if_ack_required_to(protocol_message) do
293
- Ably::Models::ProtocolMessage.new(protocol_message).tap do |protocol_message|
294
+ Ably::Models::ProtocolMessage.new(protocol_message, logger: logger).tap do |protocol_message|
294
295
  add_message_to_outgoing_queue protocol_message
295
296
  notify_message_dispatcher_of_new_message protocol_message
296
297
  logger.debug("Connection: Prot msg queued =>: #{protocol_message.action} #{protocol_message}")
@@ -362,6 +363,29 @@ module Ably
362
363
  @error_reason = error
363
364
  end
364
365
 
366
+ # @api private
367
+ def clear_error_reason
368
+ @error_reason = nil
369
+ end
370
+
371
+ # Triggers registered callbacks for a successful connection resume event
372
+ # @api private
373
+ def resumed
374
+ resume_callbacks.each(&:call)
375
+ end
376
+
377
+ # Provides a simple hook to inject a callback when a connection is successfully resumed
378
+ # @api private
379
+ def on_resume(&callback)
380
+ resume_callbacks << callback
381
+ end
382
+
383
+ # Remove a registered connection resume callback
384
+ # @api private
385
+ def off_resume(&callback)
386
+ resume_callbacks.delete(callback)
387
+ end
388
+
365
389
  # As we are using a state machine, do not allow change_state to be used
366
390
  # #transition_state_machine must be used instead
367
391
  private :change_state
@@ -378,6 +402,10 @@ module Ably
378
402
  # @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent
379
403
  attr_reader :client_serial
380
404
 
405
+ def resume_callbacks
406
+ @resume_callbacks ||= []
407
+ end
408
+
381
409
  def create_pub_sub_message_bus
382
410
  Ably::Util::PubSub.new(
383
411
  coerce_into: Proc.new do |event|
@@ -443,3 +471,7 @@ module Ably
443
471
  end
444
472
  end
445
473
  end
474
+
475
+ require 'ably/realtime/connection/connection_manager'
476
+ require 'ably/realtime/connection/connection_state_machine'
477
+ require 'ably/realtime/connection/websocket_transport'
@@ -29,11 +29,11 @@ module Ably::Realtime
29
29
  @connection = connection
30
30
  @timers = Hash.new { |hash, key| hash[key] = [] }
31
31
 
32
- connection.on(:closed) do
32
+ connection.unsafe_on(:closed) do
33
33
  connection.reset_resume_info
34
34
  end
35
35
 
36
- connection.once(:connecting) do
36
+ connection.unsafe_once(:connecting) do
37
37
  close_connection_when_reactor_is_stopped
38
38
  end
39
39
 
@@ -75,15 +75,26 @@ module Ably::Realtime
75
75
  # @api private
76
76
  def connection_opening_failed(error)
77
77
  logger.warn "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}"
78
- connection.transition_state_machine next_retry_state, Ably::Exceptions::ConnectionError.new("Connection failed; #{error.message}", nil, 80000)
78
+ connection.transition_state_machine next_retry_state, Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, 80000)
79
79
  end
80
80
 
81
- # Called whenever a new connection message is received with an error
81
+ # Called whenever a new connection is made
82
82
  #
83
83
  # @api private
84
- def connected_with_error(error)
85
- logger.warn "ConnectionManager: Connected with error; #{error.message}"
86
- connection.trigger :error, error
84
+ def connected(protocol_message)
85
+ if connection.key
86
+ if protocol_message.connection_key == connection.key
87
+ logger.debug "ConnectionManager: Connection resumed successfully - ID #{connection.id} and key #{connection.key}"
88
+ EventMachine.next_tick { connection.resumed }
89
+ else
90
+ logger.debug "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connect ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}"
91
+ detach_attached_channels protocol_message.error
92
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
93
+ end
94
+ else
95
+ logger.debug "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}"
96
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
97
+ end
87
98
  end
88
99
 
89
100
  # Ensures the underlying transport has been disconnected and all event emitter callbacks removed
@@ -133,7 +144,7 @@ module Ably::Realtime
133
144
  def fail(error)
134
145
  connection.logger.fatal "ConnectionManager: Connection failed - #{error}"
135
146
  connection.manager.destroy_transport
136
- connection.once(:failed) { connection.trigger :error, error }
147
+ connection.unsafe_once(:failed) { connection.trigger :error, error }
137
148
  end
138
149
 
139
150
  # When a connection is disconnected whilst connecting, attempt reconnect and/or set state to :suspended or :failed
@@ -143,6 +154,12 @@ module Ably::Realtime
143
154
  return unless connection.disconnected? || connection.suspended? # do nothing if state has changed through an explicit request
144
155
  return unless retry_connection? # do not always reattempt connection or change state as client may be re-authorising
145
156
 
157
+ error = current_transition.metadata
158
+ if error.kind_of?(Ably::Models::ErrorInfo)
159
+ renew_token_and_reconnect error if error.code == RESOLVABLE_ERROR_CODES.fetch(:token_expired)
160
+ return
161
+ end
162
+
146
163
  unless connection_retry_from_suspended_state?
147
164
  return if connection_retry_for(:disconnected, ignore_states: [:connecting])
148
165
  end
@@ -159,9 +176,10 @@ module Ably::Realtime
159
176
  def respond_to_transport_disconnected_whilst_connected(current_transition)
160
177
  logger.warn "ConnectionManager: Connection to #{connection.transport.url} was disconnected unexpectedly"
161
178
 
162
- if current_transition.metadata.kind_of?(Ably::Models::ErrorInfo)
163
- connection.trigger :error, current_transition.metadata
164
- logger.error "ConnectionManager: Error received when disconnected within ProtocolMessage - #{current_transition.metadata}"
179
+ error = current_transition.metadata
180
+ if error.kind_of?(Ably::Models::ErrorInfo) && error.code != RESOLVABLE_ERROR_CODES.fetch(:token_expired)
181
+ connection.trigger :error, error
182
+ logger.error "ConnectionManager: Error in Disconnected ProtocolMessage received from the server - #{error}"
165
183
  end
166
184
 
167
185
  destroy_transport
@@ -176,7 +194,7 @@ module Ably::Realtime
176
194
  case error.code
177
195
  when RESOLVABLE_ERROR_CODES.fetch(:token_expired)
178
196
  connection.transition_state_machine :disconnected
179
- connection.once_or_if(:disconnected) do
197
+ connection.unsafe_once_or_if(:disconnected) do
180
198
  renew_token_and_reconnect error
181
199
  end
182
200
  else
@@ -207,6 +225,10 @@ module Ably::Realtime
207
225
  connection.client
208
226
  end
209
227
 
228
+ def channels
229
+ client.channels
230
+ end
231
+
210
232
  # Create a timer that will execute in timeout_in seconds.
211
233
  # If the connection state changes however, cancel the timer
212
234
  def create_timeout_timer_whilst_in_state(timer_id, timeout_in)
@@ -215,7 +237,7 @@ module Ably::Realtime
215
237
  timers[timer_id] << EventMachine::Timer.new(timeout_in) do
216
238
  yield
217
239
  end
218
- connection.once_state_changed { clear_timers timer_id }
240
+ connection.unsafe_once_state_changed { clear_timers timer_id }
219
241
  end
220
242
 
221
243
  def clear_timers(key)
@@ -245,10 +267,15 @@ module Ably::Realtime
245
267
  def connection_retry_for(from_state, options = {})
246
268
  retry_params = CONNECT_RETRY_CONFIG.fetch(from_state)
247
269
 
248
- if time_spent_attempting_state(from_state, options) <= retry_params.fetch(:max_time_in_state)
249
- logger.debug "ConnectionManager: Pausing for #{retry_params.fetch(:retry_every)}s before attempting to reconnect"
250
- create_timeout_timer_whilst_in_state(:reconnect, retry_params.fetch(:retry_every)) do
251
- connection.connect if connection.state == from_state
270
+ if time_spent_attempting_state(from_state, options) < retry_params.fetch(:max_time_in_state)
271
+ if retries_for_state(from_state, ignore_states: [:connecting]).empty?
272
+ logger.debug "ConnectionManager: Will attempt reconnect immediately as no previous reconnect attempts made in this state"
273
+ EventMachine.next_tick { connection.connect }
274
+ else
275
+ logger.debug "ConnectionManager: Pausing for #{retry_params.fetch(:retry_every)}s before attempting to reconnect"
276
+ create_timeout_timer_whilst_in_state(:reconnect, retry_params.fetch(:retry_every)) do
277
+ connection.connect if connection.state == from_state
278
+ end
252
279
  end
253
280
  true
254
281
  end
@@ -282,7 +309,16 @@ module Ably::Realtime
282
309
  ignore_states = options.fetch(:ignore_states, [])
283
310
  allowed_states = Array(state) + Array(ignore_states)
284
311
 
285
- connection.state_history.reverse.take_while do |transition|
312
+ state_history_ordered = connection.state_history.reverse
313
+ last_state = state_history_ordered.first
314
+
315
+ # If this method is called after the transition has been persisted to memory,
316
+ # then we need to ignore the current transition when reviewing the number of retries
317
+ if last_state[:state].to_sym == state && last_state.fetch(:transitioned_at).to_f > Time.now.to_f - 0.1
318
+ state_history_ordered.shift
319
+ end
320
+
321
+ state_history_ordered.take_while do |transition|
286
322
  allowed_states.include?(transition[:state].to_sym)
287
323
  end.select do |transition|
288
324
  transition[:state] == state
@@ -290,15 +326,18 @@ module Ably::Realtime
290
326
  end
291
327
 
292
328
  def subscribe_to_transport_events(transport)
293
- transport.__incoming_protocol_msgbus__.on(:protocol_message) do |protocol_message|
329
+ transport.__incoming_protocol_msgbus__.unsafe_on(:protocol_message) do |protocol_message|
294
330
  connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
295
331
  end
296
332
 
297
- transport.on(:disconnected) do
333
+ transport.unsafe_on(:disconnected) do |reason|
298
334
  if connection.closing?
299
335
  connection.transition_state_machine :closed
300
336
  elsif !connection.closed? && !connection.disconnected?
301
- connection.transition_state_machine :disconnected
337
+ exception = if reason
338
+ Ably::Exceptions::ConnectionClosedError.new(reason)
339
+ end
340
+ connection.transition_state_machine :disconnected, exception
302
341
  end
303
342
  end
304
343
  end
@@ -328,10 +367,10 @@ module Ably::Realtime
328
367
  connection.off &state_changed_callback
329
368
  end
330
369
 
331
- connection.once :connected, :closed, :failed, &state_changed_callback
370
+ connection.unsafe_once :connected, :closed, :failed, &state_changed_callback
332
371
 
333
372
  if token && !token.expired?
334
- reconnect_transport
373
+ connection.connect
335
374
  else
336
375
  connection.transition_state_machine :failed, error unless connection.failed?
337
376
  end
@@ -339,7 +378,7 @@ module Ably::Realtime
339
378
 
340
379
  EventMachine.defer operation, callback
341
380
  else
342
- logger.warn "ConnectionManager: Token has expired and is not renewable"
381
+ logger.error "ConnectionManager: Token has expired and is not renewable"
343
382
  connection.transition_state_machine :failed, error
344
383
  end
345
384
  end
@@ -360,6 +399,15 @@ module Ably::Realtime
360
399
  !@renewing_token
361
400
  end
362
401
 
402
+ def detach_attached_channels(error)
403
+ channels.select do |channel|
404
+ channel.attached? || channel.attaching?
405
+ end.each do |channel|
406
+ logger.warn "Force detaching channel '#{channel.name}': #{error}"
407
+ channel.manager.suspend error
408
+ end
409
+ end
410
+
363
411
  def logger
364
412
  connection.logger
365
413
  end