ably-rest 0.7.3 → 0.7.5

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 (69) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +1 -0
  3. data/SPEC.md +480 -472
  4. data/lib/ably-rest.rb +1 -1
  5. data/lib/submodules/ably-ruby/LICENSE.txt +1 -1
  6. data/lib/submodules/ably-ruby/README.md +107 -24
  7. data/lib/submodules/ably-ruby/SPEC.md +531 -398
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +24 -16
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +9 -0
  10. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +17 -9
  11. data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +12 -8
  12. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +18 -10
  13. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +15 -4
  14. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +31 -2
  16. data/lib/submodules/ably-ruby/lib/ably/modules/message_emitter.rb +77 -0
  17. data/lib/submodules/ably-ruby/lib/ably/modules/safe_deferrable.rb +71 -0
  18. data/lib/submodules/ably-ruby/lib/ably/modules/safe_yield.rb +41 -0
  19. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +28 -8
  20. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +0 -5
  21. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +24 -29
  22. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +54 -11
  23. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +21 -6
  24. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +7 -2
  25. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +29 -26
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +4 -4
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +41 -9
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +72 -24
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +26 -4
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -6
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +74 -208
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +264 -0
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +59 -0
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +64 -0
  35. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +1 -1
  36. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +6 -2
  37. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  38. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +3 -1
  39. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +18 -0
  40. data/lib/submodules/ably-ruby/lib/ably/version.rb +1 -1
  41. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  42. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +28 -6
  43. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +116 -46
  44. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +55 -10
  45. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +32 -0
  46. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +456 -96
  47. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +2 -2
  48. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +2 -2
  49. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +96 -7
  50. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +8 -0
  51. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +71 -0
  52. data/lib/submodules/ably-ruby/spec/support/api_helper.rb +1 -1
  53. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +1 -1
  54. data/lib/submodules/ably-ruby/spec/support/test_app.rb +13 -7
  55. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +15 -14
  56. data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +4 -4
  57. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +17 -17
  58. data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +4 -4
  59. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +28 -9
  60. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +50 -0
  61. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +76 -2
  62. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +51 -20
  63. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +3 -3
  64. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +30 -0
  65. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +52 -26
  66. data/lib/submodules/ably-ruby/spec/unit/realtime/safe_deferrable_spec.rb +12 -0
  67. data/spec/spec_helper.rb +5 -0
  68. metadata +12 -4
  69. data/lib/submodules/ably-ruby/.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