ably-rest 0.8.3 → 0.8.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/lib/submodules/ably-ruby/CHANGELOG.md +10 -0
  4. data/lib/submodules/ably-ruby/README.md +1 -1
  5. data/lib/submodules/ably-ruby/Rakefile +1 -1
  6. data/lib/submodules/ably-ruby/lib/ably/auth.rb +24 -20
  7. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +3 -0
  8. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +41 -0
  9. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +43 -0
  10. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +1 -1
  11. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +1 -1
  12. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +2 -1
  13. data/lib/submodules/ably-ruby/lib/ably/models/token_details.rb +8 -6
  14. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +4 -0
  15. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +4 -1
  16. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +35 -4
  17. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +13 -13
  18. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +13 -5
  19. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +27 -7
  20. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +22 -12
  21. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +16 -10
  22. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -0
  23. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +5 -4
  24. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +42 -24
  25. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +25 -17
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +4 -4
  27. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +3 -2
  28. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +2 -2
  29. data/lib/submodules/ably-ruby/lib/ably/util/crypto.rb +15 -0
  30. data/lib/submodules/ably-ruby/lib/ably/version.rb +1 -1
  31. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +9 -9
  32. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  33. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +168 -21
  34. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channels_spec.rb +6 -2
  35. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +6 -5
  36. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +29 -19
  37. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +150 -35
  38. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +146 -23
  39. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +2 -2
  40. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +44 -24
  41. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +1 -1
  42. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +1 -1
  43. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +77 -46
  44. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +31 -3
  45. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +15 -5
  46. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +9 -7
  47. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +30 -4
  48. data/lib/submodules/ably-ruby/spec/support/protocol_helper.rb +9 -6
  49. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +1 -1
  50. data/lib/submodules/ably-ruby/spec/unit/models/channel_state_change_spec.rb +44 -0
  51. data/lib/submodules/ably-ruby/spec/unit/models/connection_state_change_spec.rb +54 -0
  52. data/lib/submodules/ably-ruby/spec/unit/models/token_details_spec.rb +8 -0
  53. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +1 -1
  54. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +18 -0
  55. metadata +6 -2
@@ -65,9 +65,9 @@ module Ably
65
65
  # token_details #=> Ably::Models::TokenDetails
66
66
  # end
67
67
  #
68
- def authorise(auth_options = {}, token_params = {}, &success_callback)
68
+ def authorise(token_params = {}, auth_options = {}, &success_callback)
69
69
  async_wrap(success_callback) do
70
- auth_sync.authorise(auth_options, token_params)
70
+ auth_sync.authorise(token_params, auth_options)
71
71
  end
72
72
  end
73
73
 
@@ -76,8 +76,8 @@ module Ably
76
76
  # @option (see Ably::Auth#authorise)
77
77
  # @return [Ably::Models::TokenDetails]
78
78
  #
79
- def authorise_sync(auth_options = {}, token_params = {})
80
- auth_sync.authorise(auth_options, token_params)
79
+ def authorise_sync(token_params = {}, auth_options = {})
80
+ auth_sync.authorise(token_params, auth_options)
81
81
  end
82
82
 
83
83
  # def_delegator :auth_sync, :request_token, :request_token_sync
@@ -100,9 +100,9 @@ module Ably
100
100
  # token_details #=> Ably::Models::TokenDetails
101
101
  # end
102
102
  #
103
- def request_token(auth_options = {}, token_params = {}, &success_callback)
103
+ def request_token(token_params = {}, auth_options = {}, &success_callback)
104
104
  async_wrap(success_callback) do
105
- request_token_sync(auth_options, token_params)
105
+ request_token_sync(token_params, auth_options)
106
106
  end
107
107
  end
108
108
 
@@ -111,8 +111,8 @@ module Ably
111
111
  # @option (see Ably::Auth#authorise)
112
112
  # @return [Ably::Models::TokenDetails]
113
113
  #
114
- def request_token_sync(auth_options = {}, token_params = {})
115
- auth_sync.request_token(auth_options, token_params)
114
+ def request_token_sync(token_params = {}, auth_options = {})
115
+ auth_sync.request_token(token_params, auth_options)
116
116
  end
117
117
 
118
118
  # Creates and signs a token request that can then subsequently be used by any client to request a token
@@ -124,12 +124,12 @@ module Ably
124
124
  # @yield [Models::TokenRequest]
125
125
  #
126
126
  # @example
127
- # client.auth.create_token_request(id: 'asd.asd', ttl: 3600) do |token_request|
127
+ # client.auth.create_token_request({ ttl: 3600 }, id: 'asd.asd') do |token_request|
128
128
  # token_request #=> Ably::Models::TokenRequest
129
129
  # end
130
- def create_token_request(auth_options = {}, token_params = {}, &success_callback)
130
+ def create_token_request(token_params = {}, auth_options = {}, &success_callback)
131
131
  async_wrap(success_callback) do
132
- create_token_request_sync(auth_options, token_params)
132
+ create_token_request_sync(token_params, auth_options)
133
133
  end
134
134
  end
135
135
 
@@ -138,8 +138,8 @@ module Ably
138
138
  # @option (see Ably::Auth#authorise)
139
139
  # @return [Ably::Models::TokenRequest]
140
140
  #
141
- def create_token_request_sync(auth_options = {}, token_params = {})
142
- auth_sync.create_token_request(auth_options, token_params)
141
+ def create_token_request_sync(token_params = {}, auth_options = {})
142
+ auth_sync.create_token_request(token_params, auth_options)
143
143
  end
144
144
 
145
145
  # Auth header string used in HTTP requests to Ably
@@ -46,6 +46,7 @@ module Ably
46
46
  )
47
47
  include Ably::Modules::StateEmitter
48
48
  include Ably::Modules::UsesStateMachine
49
+ ensure_state_machine_emits 'Ably::Models::ChannelStateChange'
49
50
 
50
51
  # Max number of messages to bundle in a single ProtocolMessage
51
52
  MAX_PROTOCOL_MESSAGE_BATCH_SIZE = 50
@@ -106,6 +107,7 @@ module Ably
106
107
  #
107
108
  # @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
108
109
  # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
110
+ # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
109
111
  #
110
112
  # @yield [Ably::Models::Message,Array<Ably::Models::Message>] On success, will call the block with the {Ably::Models::Message} if a single message is publishde, or an Array of {Ably::Models::Message} when multiple messages are published
111
113
  # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
@@ -132,11 +134,11 @@ module Ably
132
134
  # puts "#{message.name} event received with #{message.data}"
133
135
  # end
134
136
  #
135
- # channel.publish('click', 'body').errback do |message, error|
137
+ # channel.publish('click', 'body').errback do |error, message|
136
138
  # puts "#{message.name} was not received, error #{error.message}"
137
139
  # end
138
140
  #
139
- def publish(name, data = nil, &success_block)
141
+ def publish(name, data = nil, attributes = {}, &success_block)
140
142
  raise Ably::Exceptions::ChannelInactive.new('Cannot publish messages on a detached channel') if detached? || detaching?
141
143
  raise Ably::Exceptions::ChannelInactive.new('Cannot publish messages on a failed channel') if failed?
142
144
 
@@ -149,11 +151,11 @@ module Ably
149
151
  else
150
152
  ensure_utf_8 :name, name, allow_nil: true
151
153
  ensure_supported_payload data
152
- [{ name: name, data: data }]
154
+ [{ name: name, data: data }.merge(attributes)]
153
155
  end
154
156
 
155
157
  queue_messages(messages).tap do |deferrable|
156
- deferrable.callback &success_block if block_given?
158
+ deferrable.callback(&success_block) if block_given?
157
159
  end
158
160
  end
159
161
 
@@ -190,7 +192,9 @@ module Ably
190
192
  # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callback
191
193
  #
192
194
  def attach(&success_block)
193
- raise Ably::Exceptions::InvalidStateChange.new("Cannot ATTACH channel when the connection is in a closed, suspended or failed state. Connection state: #{connection.state}") if connection.closing? || connection.closed? || connection.suspended? || connection.failed?
195
+ if connection.closing? || connection.closed? || connection.suspended? || connection.failed?
196
+ raise Ably::Exceptions::InvalidStateChange.new("Cannot ATTACH channel when the connection is in a closed, suspended or failed state. Connection state: #{connection.state}")
197
+ end
194
198
 
195
199
  transition_state_machine :attaching if can_transition_to?(:attaching)
196
200
  deferrable_for_state_change_to(STATE.Attached, &success_block)
@@ -285,6 +289,10 @@ module Ably
285
289
  client.logger
286
290
  end
287
291
 
292
+ # As we are using a state machine, do not allow change_state to be used
293
+ # #transition_state_machine must be used instead
294
+ private :change_state
295
+
288
296
  private
289
297
  attr_reader :queue
290
298
 
@@ -27,7 +27,7 @@ module Ably::Realtime
27
27
  # Commence attachment
28
28
  def detach(error = nil)
29
29
  if connection.closed? || connection.connecting? || connection.suspended?
30
- channel.transition_state_machine :detached, error
30
+ channel.transition_state_machine :detached, reason: error
31
31
  elsif can_transition_to?(:detached)
32
32
  send_detach_protocol_message
33
33
  end
@@ -51,7 +51,7 @@ module Ably::Realtime
51
51
 
52
52
  # Detach a channel as a result of an error
53
53
  def suspend(error)
54
- channel.transition_state_machine! :detaching, error
54
+ channel.transition_state_machine! :detaching, reason: error
55
55
  end
56
56
 
57
57
  # When a channel is no longer attached or has failed,
@@ -77,10 +77,10 @@ module Ably::Realtime
77
77
  def nack_messages(protocol_message, error)
78
78
  (protocol_message.messages + protocol_message.presence).each do |message|
79
79
  logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}"
80
- message.fail message, error
80
+ message.fail error
81
81
  end
82
82
  logger.debug "Calling NACK failure callbacks for #{protocol_message.class.name} - #{protocol_message.to_json}"
83
- protocol_message.fail protocol_message, error
83
+ protocol_message.fail error
84
84
  end
85
85
 
86
86
  def drop_pending_queue_from_ack(ack_protocol_message)
@@ -118,6 +118,22 @@ module Ably::Realtime
118
118
  )
119
119
  end
120
120
 
121
+ # Any message sent before an ACK/NACK was received on the previous transport
122
+ # needs to be resent to the Ably service so that a subsequent ACK/NACK is received.
123
+ # It is up to Ably to ensure that duplicate messages are not retransmitted on the channel
124
+ # base on the serial numbers
125
+ #
126
+ # @api private
127
+ def resend_pending_message_ack_queue
128
+ connection.__pending_message_ack_queue__.delete_if do |protocol_message|
129
+ if protocol_message.channel == channel.name
130
+ connection.__outgoing_message_queue__ << protocol_message
131
+ connection.__outgoing_protocol_msgbus__.publish :protocol_message
132
+ true
133
+ end
134
+ end
135
+ end
136
+
121
137
  def setup_connection_event_handlers
122
138
  connection.unsafe_on(:closed) do
123
139
  channel.transition_state_machine :detaching if can_transition_to?(:detaching)
@@ -125,15 +141,19 @@ module Ably::Realtime
125
141
 
126
142
  connection.unsafe_on(:suspended) do |error|
127
143
  if can_transition_to?(:detaching)
128
- channel.transition_state_machine :detaching, Ably::Exceptions::ConnectionSuspended.new('Connection suspended', nil, 80002, error)
144
+ channel.transition_state_machine :detaching, reason: Ably::Exceptions::ConnectionSuspended.new('Connection suspended', nil, 80002, error)
129
145
  end
130
146
  end
131
147
 
132
148
  connection.unsafe_on(:failed) do |error|
133
- if can_transition_to?(:failed)
134
- channel.transition_state_machine :failed, Ably::Exceptions::ConnectionFailed.new('Connection failed', nil, 80002, error)
149
+ if can_transition_to?(:failed) && !channel.detached?
150
+ channel.transition_state_machine :failed, reason: Ably::Exceptions::ConnectionFailed.new('Connection failed', nil, 80002, error)
135
151
  end
136
152
  end
153
+
154
+ connection.unsafe_on(:connected) do |error|
155
+ resend_pending_message_ack_queue
156
+ end
137
157
  end
138
158
 
139
159
  def logger
@@ -22,8 +22,9 @@ module Ably::Realtime
22
22
 
23
23
  transition :from => :initialized, :to => [:attaching]
24
24
  transition :from => :attaching, :to => [:attached, :detaching, :failed]
25
- transition :from => :attached, :to => [:detaching, :failed]
25
+ transition :from => :attached, :to => [:detaching, :detached, :failed]
26
26
  transition :from => :detaching, :to => [:detached, :attaching, :failed]
27
+ transition :from => :detached, :to => [:attaching, :attached, :failed]
27
28
  transition :from => :failed, :to => [:attaching]
28
29
 
29
30
  after_transition do |channel, transition|
@@ -35,39 +36,48 @@ module Ably::Realtime
35
36
  end
36
37
 
37
38
  before_transition(to: [:attached]) do |channel, current_transition|
38
- channel.manager.attached current_transition.metadata
39
+ channel.manager.attached current_transition.metadata.protocol_message
39
40
  end
40
41
 
41
42
  after_transition(to: [:detaching]) do |channel, current_transition|
42
- channel.manager.detach current_transition.metadata
43
+ err = error_from_state_change(current_transition)
44
+ channel.manager.detach err
43
45
  end
44
46
 
45
47
  after_transition(to: [:detached]) do |channel, current_transition|
46
- channel.manager.fail_messages_awaiting_ack nil_unless_error(current_transition.metadata)
47
- channel.manager.emit_error current_transition.metadata if is_error_type?(current_transition.metadata)
48
+ err = error_from_state_change(current_transition)
49
+ channel.manager.fail_messages_awaiting_ack err
50
+ channel.manager.emit_error err if err
48
51
  end
49
52
 
50
53
  after_transition(to: [:failed]) do |channel, current_transition|
51
- channel.manager.fail_messages_awaiting_ack nil_unless_error(current_transition.metadata)
52
- channel.manager.emit_error current_transition.metadata if is_error_type?(current_transition.metadata)
54
+ err = error_from_state_change(current_transition)
55
+ channel.manager.fail_messages_awaiting_ack err
56
+ channel.manager.emit_error err if err
53
57
  end
54
58
 
55
59
  # Transitions responsible for updating channel#error_reason
56
60
  before_transition(to: [:failed]) do |channel, current_transition|
57
- channel.set_failed_channel_error_reason current_transition.metadata if is_error_type?(current_transition.metadata)
61
+ err = error_from_state_change(current_transition)
62
+ channel.set_failed_channel_error_reason err if err
58
63
  end
59
64
 
60
65
  before_transition(to: [:attached, :detached]) do |channel, current_transition|
61
- if is_error_type?(current_transition.metadata)
62
- channel.set_failed_channel_error_reason current_transition.metadata
66
+ err = error_from_state_change(current_transition)
67
+ if err
68
+ channel.set_failed_channel_error_reason err
63
69
  else
64
70
  # Attached & Detached are "healthy" final states so reset the error reason
65
71
  channel.clear_error_reason
66
72
  end
67
73
  end
68
74
 
69
- def self.nil_unless_error(error_object)
70
- error_object if is_error_type?(error_object)
75
+ def self.error_from_state_change(current_transition)
76
+ # ChannelStateChange object is always passed in current_transition metadata object
77
+ connection_state_change = current_transition.metadata
78
+ # Reason attribute contains errors
79
+ err = connection_state_change && connection_state_change.reason
80
+ err if is_error_type?(err)
71
81
  end
72
82
 
73
83
  private
@@ -68,10 +68,16 @@ module Ably::Realtime
68
68
 
69
69
  when ACTION.Connect
70
70
  when ACTION.Connected
71
- connection.transition_state_machine :connected, protocol_message unless connection.connected?
71
+ if connection.disconnected? || connection.closing? || connection.closed? || connection.failed?
72
+ logger.debug "Incoming CONNECTED ProtocolMessage discarded as connection has moved on and is in state: #{connection.state}"
73
+ elsif connection.connected?
74
+ logger.error "CONNECTED ProtocolMessage should not have been received when the connection is in the CONNECTED state"
75
+ else
76
+ connection.transition_state_machine :connected, reason: protocol_message.error, protocol_message: protocol_message
77
+ end
72
78
 
73
79
  when ACTION.Disconnect, ACTION.Disconnected
74
- connection.transition_state_machine :disconnected, protocol_message.error unless connection.disconnected?
80
+ connection.transition_state_machine :disconnected, reason: protocol_message.error unless connection.disconnected?
75
81
 
76
82
  when ACTION.Close
77
83
  when ACTION.Closed
@@ -87,7 +93,7 @@ module Ably::Realtime
87
93
  when ACTION.Attach
88
94
  when ACTION.Attached
89
95
  get_channel(protocol_message.channel).tap do |channel|
90
- channel.transition_state_machine :attached, protocol_message unless channel.attached?
96
+ channel.transition_state_machine :attached, reason: protocol_message.error, protocol_message: protocol_message unless channel.attached?
91
97
  end
92
98
 
93
99
  when ACTION.Detach
@@ -125,7 +131,7 @@ module Ably::Realtime
125
131
  def dispatch_channel_error(protocol_message)
126
132
  logger.warn "Channel Error message received: #{protocol_message.error}"
127
133
  if !protocol_message.has_message_serial?
128
- get_channel(protocol_message.channel).transition_state_machine :failed, protocol_message.error
134
+ get_channel(protocol_message.channel).transition_state_machine :failed, reason: protocol_message.error
129
135
  else
130
136
  logger.fatal "Cannot process ProtocolMessage as not yet implemented: #{protocol_message}"
131
137
  end
@@ -163,17 +169,17 @@ module Ably::Realtime
163
169
  def nack_messages(messages, protocol_message)
164
170
  messages.each do |message|
165
171
  logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}"
166
- message.fail message, protocol_message.error
172
+ message.fail protocol_message.error
167
173
  end
168
174
  end
169
175
 
170
176
  def drop_pending_queue_from_ack(ack_protocol_message)
171
177
  message_serial_up_to = ack_protocol_message.message_serial + ack_protocol_message.count - 1
172
- connection.__pending_message_ack_queue__.drop_while do |protocol_message|
173
- if protocol_message.message_serial <= message_serial_up_to
174
- yield protocol_message
175
- true
176
- end
178
+
179
+ while !connection.__pending_message_ack_queue__.empty?
180
+ next_message = connection.__pending_message_ack_queue__.first
181
+ return if next_message.message_serial > message_serial_up_to
182
+ yield connection.__pending_message_ack_queue__.shift
177
183
  end
178
184
  end
179
185
 
@@ -44,6 +44,12 @@ module Ably::Realtime
44
44
 
45
45
  non_blocking_loop_while(condition) do
46
46
  protocol_message = outgoing_queue.shift
47
+
48
+ if (!connection.transport)
49
+ protocol_message.fail Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, 80003)
50
+ next
51
+ end
52
+
47
53
  current_transport_outgoing_message_bus.publish :protocol_message, protocol_message
48
54
 
49
55
  if protocol_message.ack_required?
@@ -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+)$/
@@ -118,7 +119,7 @@ module Ably
118
119
  # the failed state. Once closed, the library will not attempt to re-establish the
119
120
  # connection without a call to {Connection#connect}.
120
121
  #
121
- # @yield [Ably::Realtime::Connection] block is called as soon as this connection is in the Closed state
122
+ # @yield block is called as soon as this connection is in the Closed state
122
123
  #
123
124
  # @return [EventMachine::Deferrable]
124
125
  #
@@ -133,7 +134,7 @@ module Ably
133
134
  # Causes the library to attempt connection. If it was previously explicitly
134
135
  # closed by the user, or was closed as a result of an unrecoverable error, a new connection will be opened.
135
136
  #
136
- # @yield [Ably::Realtime::Connection] block is called as soon as this connection is in the Connected state
137
+ # @yield block is called as soon as this connection is in the Connected state
137
138
  #
138
139
  # @return [EventMachine::Deferrable]
139
140
  #
@@ -189,7 +190,7 @@ module Ably
189
190
  EventMachine::HttpRequest.new(url).get.tap do |http|
190
191
  http.errback do
191
192
  yield false if block_given?
192
- deferrable.fail "Unable to connect to #{url}"
193
+ deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unable to connect to #{url}", nil, 80000)
193
194
  end
194
195
  http.callback do
195
196
  EventMachine.next_tick do
@@ -198,7 +199,7 @@ module Ably
198
199
  if result
199
200
  deferrable.succeed
200
201
  else
201
- deferrable.fail "Unexpected response from #{url} (#{http.response_header.status})"
202
+ deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unexpected response from #{url} (#{http.response_header.status})", 400, 40000)
202
203
  end
203
204
  end
204
205
  end
@@ -39,7 +39,7 @@ module Ably::Realtime
39
39
 
40
40
  EventMachine.next_tick do
41
41
  # Connect once Connection object is initialised
42
- connection.connect if client.auto_connect
42
+ connection.connect if client.auto_connect && connection.can_transition_to?(:connecting)
43
43
  end
44
44
  end
45
45
 
@@ -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
@@ -88,7 +89,7 @@ module Ably::Realtime
88
89
  # @api private
89
90
  def connected(protocol_message)
90
91
  if connection.key
91
- if protocol_message.connection_key == connection.key
92
+ if connection_key_shared(protocol_message.connection_key) == connection_key_shared(connection.key)
92
93
  logger.debug "ConnectionManager: Connection resumed successfully - ID #{connection.id} and key #{connection.key}"
93
94
  EventMachine.next_tick { connection.resumed }
94
95
  else
@@ -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)
182
- logger.warn "ConnectionManager: Connection to #{connection.transport.url} was disconnected unexpectedly"
181
+ def respond_to_transport_disconnected_whilst_connected(error)
182
+ logger.warn "ConnectionManager: Connection #{"to #{connection.transport.url}" if connection.transport} 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
 
@@ -234,6 +233,12 @@ module Ably::Realtime
234
233
  client.channels
235
234
  end
236
235
 
236
+ # Connection key left part is consistent between connection resumes
237
+ # i.e. wVIsgTHAB1UvXh7z-1991d8586 becomes wVIsgTHAB1UvXh7z-1990d8586 after a resume
238
+ def connection_key_shared(connection_key)
239
+ (connection_key || '')[/^\w{5,}-/, 0]
240
+ end
241
+
237
242
  # Create a timer that will execute in timeout_in seconds.
238
243
  # If the connection state changes however, cancel the timer
239
244
  def create_timeout_timer_whilst_in_state(timer_id, timeout_in)
@@ -249,12 +254,26 @@ module Ably::Realtime
249
254
  timers.fetch(key, []).each(&:cancel)
250
255
  end
251
256
 
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)
257
+ def get_next_retry_state_info
258
+ retry_state = if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
254
259
  :suspended
255
260
  else
256
261
  :disconnected
257
262
  end
263
+ {
264
+ state: retry_state,
265
+ pause: next_retry_pause(retry_state)
266
+ }
267
+ end
268
+
269
+ def next_retry_pause(retry_state)
270
+ return nil unless CONNECT_RETRY_CONFIG.fetch(retry_state)
271
+
272
+ if retries_for_state(retry_state, ignore_states: [:connecting]).empty?
273
+ 0
274
+ else
275
+ CONNECT_RETRY_CONFIG.fetch(retry_state).fetch(:retry_every)
276
+ end
258
277
  end
259
278
 
260
279
  def connection_retry_from_suspended_state?
@@ -348,13 +367,12 @@ module Ably::Realtime
348
367
  connection.transition_state_machine :closed
349
368
  elsif !connection.closed? && !connection.disconnected?
350
369
  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
370
+ Ably::Exceptions::TransportClosed.new(reason, nil, 80003)
355
371
  else
356
- connection.transition_state_machine :disconnected, exception
372
+ Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, 80003)
357
373
  end
374
+ next_state = get_next_retry_state_info
375
+ connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: exception
358
376
  end
359
377
  end
360
378
  end
@@ -362,7 +380,7 @@ module Ably::Realtime
362
380
  def renew_token_and_reconnect(error)
363
381
  if client.auth.token_renewable?
364
382
  if @renewing_token
365
- connection.transition_state_machine :failed, error
383
+ connection.transition_state_machine :failed, reason: error
366
384
  return
367
385
  end
368
386
 
@@ -383,18 +401,18 @@ module Ably::Realtime
383
401
  if token_details && !token_details.expired?
384
402
  connection.connect
385
403
  else
386
- connection.transition_state_machine :failed, error unless connection.failed?
404
+ connection.transition_state_machine :failed, reason: error unless connection.failed?
387
405
  end
388
406
  end
389
407
 
390
408
  authorise_deferrable.errback do |auth_error|
391
409
  logger.error "ConnectionManager: Error authorising following token expiry: #{auth_error}"
392
- connection.transition_state_machine :failed, auth_error
410
+ connection.transition_state_machine :failed, reason: auth_error
393
411
  end
394
412
  end
395
413
  else
396
414
  logger.error "ConnectionManager: Token has expired and is not renewable - #{error}"
397
- connection.transition_state_machine :failed, error
415
+ connection.transition_state_machine :failed, reason: error
398
416
  end
399
417
  end
400
418