ably-rest 0.8.5 → 0.8.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/SPEC.md +1380 -631
  4. data/ably-rest.gemspec +11 -5
  5. data/lib/submodules/ably-ruby/.travis.yml +1 -1
  6. data/lib/submodules/ably-ruby/CHANGELOG.md +42 -48
  7. data/lib/submodules/ably-ruby/ably.gemspec +7 -1
  8. data/lib/submodules/ably-ruby/lib/ably.rb +2 -0
  9. data/lib/submodules/ably-ruby/lib/ably/auth.rb +155 -47
  10. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +2 -0
  11. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +2 -3
  12. data/lib/submodules/ably-ruby/lib/ably/models/connection_details.rb +54 -0
  13. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +14 -4
  14. data/lib/submodules/ably-ruby/lib/ably/models/token_details.rb +13 -7
  15. data/lib/submodules/ably-ruby/lib/ably/models/token_request.rb +1 -2
  16. data/lib/submodules/ably-ruby/lib/ably/modules/ably.rb +3 -2
  17. data/lib/submodules/ably-ruby/lib/ably/modules/message_emitter.rb +1 -3
  18. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +2 -2
  19. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +6 -0
  20. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +15 -4
  21. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +2 -0
  22. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +10 -3
  23. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +11 -1
  24. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +62 -6
  25. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +58 -54
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +18 -5
  27. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +9 -1
  28. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +32 -14
  29. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  30. data/lib/submodules/ably-ruby/lib/ably/version.rb +1 -1
  31. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +251 -11
  32. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +12 -2
  33. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +316 -24
  34. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +93 -1
  35. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +177 -86
  36. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +284 -60
  37. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +45 -6
  38. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +4 -0
  39. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +181 -49
  40. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +13 -0
  41. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +222 -4
  42. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +132 -1
  43. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +129 -28
  44. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +7 -7
  45. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +10 -0
  46. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +41 -17
  47. data/lib/submodules/ably-ruby/spec/spec_helper.rb +1 -0
  48. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +16 -0
  49. data/lib/submodules/ably-ruby/spec/unit/models/connection_details_spec.rb +60 -0
  50. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +45 -0
  51. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +3 -1
  52. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +6 -5
  53. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +5 -1
  54. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +5 -1
  55. data/lib/submodules/ably-ruby/spec/unit/realtime/realtime_spec.rb +5 -1
  56. metadata +57 -13
@@ -106,5 +106,7 @@ module Ably
106
106
 
107
107
  # When a channel is detached / failed, certain operations are not permitted such as publishing messages
108
108
  class ChannelInactive < BaseAblyException; end
109
+
110
+ class IncompatibleClientId < BaseAblyException; end
109
111
  end
110
112
  end
@@ -3,9 +3,9 @@ module Ably::Models
3
3
  # when a state change occurs
4
4
  #
5
5
  # @!attribute [r] current
6
- # @return [Connection::STATE] Current connection state
6
+ # @return [Connection::STATE] Current channel state
7
7
  # @!attribute [r] previous
8
- # @return [Connection::STATE] Previous connection state
8
+ # @return [Connection::STATE] Previous channel state
9
9
  # @!attribute [r] reason
10
10
  # @return [Ably::Models::ErrorInfo] Object describing the reason for a state change when not initiated by the consumer of the client library
11
11
  #
@@ -20,7 +20,6 @@ module Ably::Models
20
20
  @hash_object = {
21
21
  current: hash_object.fetch(:current),
22
22
  previous: hash_object.fetch(:previous),
23
- retry_in: hash_object[:retry_in],
24
23
  reason: hash_object[:reason],
25
24
  protocol_message: hash_object[:protocol_message]
26
25
  }
@@ -0,0 +1,54 @@
1
+ module Ably::Models
2
+ # Convert connection details attributes to a {ConnectionDetails} object
3
+ #
4
+ # @param attributes (see #initialize)
5
+ #
6
+ # @return [ConnectionDetails]
7
+ def self.ConnectionDetails(attributes)
8
+ case attributes
9
+ when ConnectionDetails
10
+ return attributes
11
+ else
12
+ ConnectionDetails.new(attributes || {})
13
+ end
14
+ end
15
+
16
+ # ConnectionDetails are optionally passed to the client library in the +CONNECTED+ {Ably::Models::ProtocolMessage#connectionDetails} attribute
17
+ # to inform the client about any constraints it should adhere to and provide additional metadata about the connection.
18
+ # For example, if a request is made to publish a message that exceeds the +maxMessageSize+, the client library can reject
19
+ # the message immediately, without communicating with the Ably service
20
+ #
21
+ class ConnectionDetails
22
+ include Ably::Modules::ModelCommon
23
+
24
+ # @param attributes [Hash]
25
+ # @option attributes [String] :client_id contains the client ID assigned to the connection
26
+ # @option attributes [String] :connection_key the connection secret key string that is used to resume a connection and its state
27
+ # @option attributes [Integer] :max_message_size maximum individual message size in bytes
28
+ # @option attributes [Integer] :max_frame_size maximum size for a single frame of data sent to Ably. This restriction applies to a {Ably::Models::ProtocolMessage} sent over a realtime connection, or the total body size for a REST request
29
+ # @option attributes [Integer] :max_inbound_rate maximum allowable number of requests per second from a client
30
+ # @option attributes [Integer] :connection_state_ttl duration in seconds that Ably will persist the connection state when a Realtime client is abruptly disconnected
31
+ #
32
+ def initialize(attributes = {})
33
+ @hash_object = IdiomaticRubyWrapper(attributes.clone)
34
+ hash[:connection_state_ttl] = (hash[:connection_state_ttl].to_f / 1000).round if hash[:connection_state_ttl]
35
+ hash.freeze
36
+ end
37
+
38
+ %w(client_id connection_key max_message_size max_frame_size max_inbound_rate connection_state_ttl).each do |attribute|
39
+ define_method attribute do
40
+ hash[attribute.to_sym]
41
+ end
42
+ end
43
+
44
+ def has_client_id?
45
+ hash.has_key?(:client_id)
46
+ end
47
+
48
+ # @!attribute [r] hash
49
+ # @return [Hash] Access the token details Hash object ruby'fied to use symbolized keys
50
+ def hash
51
+ @hash_object
52
+ end
53
+ end
54
+ end
@@ -10,12 +10,12 @@ module Ably::Models
10
10
  # @return [ACTION] Protocol Message action {Ably::Modules::Enum} from list of {ACTION}. Returns nil if action is unsupported by protocol
11
11
  # @!attribute [r] count
12
12
  # @return [Integer] The count field is used for ACK and NACK actions. See {http://docs.ably.io/client-lib-development-guide/protocol/#message-acknowledgement message acknowledgement protocol}
13
- # @!attribute [r] error_info
13
+ # @!attribute [r] error
14
14
  # @return [ErrorInfo] Contains error information
15
15
  # @!attribute [r] channel
16
16
  # @return [String] Channel name for messages
17
17
  # @!attribute [r] channel_serial
18
- # @return [String] Contains a serial number for a message on the current channel
18
+ # @return [String] Contains a serial number for a message on the current channelƒ
19
19
  # @!attribute [r] connection_id
20
20
  # @return [String] Contains a string public identifier for the connection
21
21
  # @!attribute [r] connection_key
@@ -88,12 +88,18 @@ module Ably::Models
88
88
  @hash_object.freeze
89
89
  end
90
90
 
91
- %w(id channel channel_serial connection_id connection_key).each do |attribute|
91
+ %w(id channel channel_serial connection_id).each do |attribute|
92
92
  define_method attribute do
93
93
  hash[attribute.to_sym]
94
94
  end
95
95
  end
96
96
 
97
+ def connection_key
98
+ # connection_key in connection details takes precedence over connection_key on the ProtocolMessage
99
+ # connection_key in the ProtocolMessage will be deprecated in future protocol versions > 0.8
100
+ connection_details.connection_key || hash[:connection_key]
101
+ end
102
+
97
103
  def id!
98
104
  raise RuntimeError, 'ProtocolMessage #id is nil' unless id
99
105
  id
@@ -106,7 +112,7 @@ module Ably::Models
106
112
  end
107
113
 
108
114
  def error
109
- @error_info ||= ErrorInfo.new(hash[:error]) if hash[:error]
115
+ @error ||= ErrorInfo.new(hash[:error]) if hash[:error]
110
116
  end
111
117
 
112
118
  def timestamp
@@ -181,6 +187,10 @@ module Ably::Models
181
187
  flags & 1 == 1
182
188
  end
183
189
 
190
+ def connection_details
191
+ @connection_details ||= Ably::Models::ConnectionDetails(hash[:connection_details])
192
+ end
193
+
184
194
  # Indicates this protocol message will generate an ACK response when sent
185
195
  # Examples of protocol messages required ACK include :message and :presence
186
196
  def ack_required?
@@ -1,8 +1,7 @@
1
1
  module Ably::Models
2
2
  # Convert token details argument to a {TokenDetails} object
3
3
  #
4
- # @param attributes [TokenDetails,Hash] A {TokenDetails} object or Hash of token and meta data attributes
5
- # @option attributes (see TokenDetails#initialize)
4
+ # @param attributes (see #initialize)
6
5
  #
7
6
  # @return [TokenDetails]
8
7
  def self.TokenDetails(attributes)
@@ -27,10 +26,6 @@ module Ably::Models
27
26
  # For example, if buffer is 10s, the token can no longer be used for new requests 9s before it expires
28
27
  TOKEN_EXPIRY_BUFFER = 15
29
28
 
30
- def initialize(attributes)
31
- @hash_object = IdiomaticRubyWrapper(attributes.clone.freeze)
32
- end
33
-
34
29
  # @param attributes
35
30
  # @option attributes [String] :token token used to authenticate requests
36
31
  # @option attributes [String] :key_name API key name used to create this token
@@ -76,7 +71,7 @@ module Ably::Models
76
71
  # @!attribute [r] capability
77
72
  # @return [Hash] Capabilities assigned to this token
78
73
  def capability
79
- JSON.parse(hash.fetch(:capability)) if hash.fetch(:capability)
74
+ JSON.parse(hash.fetch(:capability)) if hash.has_key?(:capability)
80
75
  end
81
76
 
82
77
  # @!attribute [r] client_id
@@ -94,10 +89,21 @@ module Ably::Models
94
89
  expires < Time.now + TOKEN_EXPIRY_BUFFER
95
90
  end
96
91
 
92
+ # True if the TokenDetails was created from an opaque string i.e. no metadata exists for this token
93
+ # @return [Boolean]
94
+ # @api private
95
+ def from_token_string?
96
+ hash.keys == [:token]
97
+ end
98
+
97
99
  # @!attribute [r] hash
98
100
  # @return [Hash] Access the token details Hash object ruby'fied to use symbolized keys
99
101
  def hash
100
102
  @hash_object
101
103
  end
104
+
105
+ def to_s
106
+ "<TokenDetails token=#{token} client_id=#{client_id} key_name=#{key_name} issued=#{issued} expires=#{expires} capability=#{capability} expired?=#{expired?}>"
107
+ end
102
108
  end
103
109
  end
@@ -1,8 +1,7 @@
1
1
  module Ably::Models
2
2
  # Convert token request argument to a {TokenRequest} object
3
3
  #
4
- # @param attributes [TokenRequest,Hash] A {TokenRequest} object or Hash of attributes to create a new token request
5
- # @option attributes (see TokenRequest#initialize)
4
+ # @param attributes (see #initialize)
6
5
  #
7
6
  # @return [TokenRequest]
8
7
  def self.TokenRequest(attributes)
@@ -7,9 +7,10 @@ module Ably
7
7
  # Fallback hosts to use when a connection to rest/realtime.ably.io is not possible due to
8
8
  # network failures either at the client, between the client and Ably, within an Ably data center, or at the IO domain registrar
9
9
  #
10
- FALLBACK_HOSTS = %w(A.ably-realtime.com B.ably-realtime.com C.ably-realtime.com D.ably-realtime.com E.ably-realtime.com)
10
+ FALLBACK_HOSTS = %w(A.ably-realtime.com B.ably-realtime.com C.ably-realtime.com D.ably-realtime.com E.ably-realtime.com).freeze
11
+
11
12
  INTERNET_CHECK = {
12
13
  url: '//internet-up.ably-realtime.com/is-the-internet-up.txt',
13
14
  ok_text: 'yes'
14
- }
15
+ }.freeze
15
16
  end
@@ -50,10 +50,8 @@ module Ably::Modules
50
50
  #
51
51
  # @api private
52
52
  def emit_message(name, payload)
53
- raise 'Event name is required' unless name
54
-
55
53
  message_emitter_subscriptions[:all].each { |cb| cb.call(payload) }
56
- message_emitter_subscriptions[name].each { |cb| cb.call(payload) }
54
+ message_emitter_subscriptions[name].each { |cb| cb.call(payload) } if name
57
55
  end
58
56
 
59
57
  private
@@ -71,11 +71,11 @@ module Ably::Modules
71
71
  # @yield block is called if the state is matched immediately or once when the state is reached
72
72
  #
73
73
  # @return [void]
74
- def once_or_if(target_states, options = {})
74
+ def once_or_if(target_states, options = {}, &block)
75
75
  raise ArgumentError, 'Block required' unless block_given?
76
76
 
77
77
  if Array(target_states).any? { |target_state| state == target_state }
78
- yield
78
+ safe_yield block
79
79
  else
80
80
  failure_block = options.fetch(:else, nil)
81
81
  failure_wrapper = nil
@@ -37,6 +37,8 @@ module Ably
37
37
  include Ably::Modules::AsyncWrapper
38
38
 
39
39
  def_delegators :auth_sync, :client_id
40
+ def_delegators :auth_sync, :token_client_id_allowed?, :configure_client_id, :client_id_validated?
41
+ def_delegators :auth_sync, :can_assume_client_id?, :has_client_id?
40
42
  def_delegators :auth_sync, :current_token_details, :token
41
43
  def_delegators :auth_sync, :key, :key_name, :key_secret, :options, :auth_options, :token_params
42
44
  def_delegators :auth_sync, :using_basic_auth?, :using_token_auth?
@@ -68,6 +70,10 @@ module Ably
68
70
  def authorise(token_params = {}, auth_options = {}, &success_callback)
69
71
  async_wrap(success_callback) do
70
72
  auth_sync.authorise(token_params, auth_options)
73
+ end.tap do |deferrable|
74
+ deferrable.errback do |error|
75
+ client.connection.transition_state_machine :failed, reason: error if error.kind_of?(Ably::Exceptions::IncompatibleClientId)
76
+ end
71
77
  end
72
78
  end
73
79
 
@@ -309,9 +309,20 @@ module Ably
309
309
 
310
310
  # Queue messages and process queue if channel is attached.
311
311
  # If channel is not yet attached, attempt to attach it before the message queue is processed.
312
- # @returns [Ably::Util::SafeDeferrable]
312
+ # @return [Ably::Util::SafeDeferrable]
313
313
  def queue_messages(raw_messages)
314
- messages = Array(raw_messages).map { |msg| create_message(msg) }
314
+ messages = Array(raw_messages).map do |raw_msg|
315
+ create_message(raw_msg).tap do |message|
316
+ next if message.client_id.nil?
317
+ if message.client_id == '*'
318
+ raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages', 400, 40012)
319
+ end
320
+ unless client.auth.can_assume_client_id?(message.client_id)
321
+ raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{message.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'", 400, 40012)
322
+ end
323
+ end
324
+ end
325
+
315
326
  queue.push *messages
316
327
 
317
328
  if attached?
@@ -340,12 +351,12 @@ module Ably
340
351
  Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
341
352
  messages.each do |message|
342
353
  message.callback do
343
- return if failed
354
+ next if failed
344
355
  actual_deliveries += 1
345
356
  deferrable.succeed messages if actual_deliveries == expected_deliveries
346
357
  end
347
358
  message.errback do |error|
348
- return if failed
359
+ next if failed
349
360
  failed = true
350
361
  deferrable.fail error, message
351
362
  end
@@ -123,6 +123,8 @@ module Ably::Realtime
123
123
  # It is up to Ably to ensure that duplicate messages are not retransmitted on the channel
124
124
  # base on the serial numbers
125
125
  #
126
+ # TODO: Move this into the Connection class, it does not belong in a Channel class
127
+ #
126
128
  # @api private
127
129
  def resend_pending_message_ack_queue
128
130
  connection.__pending_message_ack_queue__.delete_if do |protocol_message|
@@ -30,6 +30,10 @@ module Ably
30
30
  # (see Ably::Rest::Client#auth)
31
31
  attr_reader :auth
32
32
 
33
+ # The underlying connection for this client
34
+ # @return [Aby::Realtime::Connection]
35
+ attr_reader :connection
36
+
33
37
  # The {Ably::Rest::Client REST client} instantiated with the same credentials and configuration that is used for all REST operations such as authentication
34
38
  # @return [Ably::Rest::Client]
35
39
  attr_reader :rest_client
@@ -69,6 +73,9 @@ module Ably
69
73
  # @option options [String] :recover When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery
70
74
  # @option options [Boolean] :auto_connect By default as soon as the client library is instantiated it will connect to Ably. You can optionally set this to false and explicitly connect.
71
75
  #
76
+ # @option options [Integer] :disconnected_retry_timeout (15 seconds). When the connection enters the DISCONNECTED state, after this delay in milliseconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
77
+ # @option options [Integer] :suspended_retry_timeout (30 seconds). When the connection enters the SUSPENDED state, after this delay in milliseconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically
78
+ #
72
79
  # @return [Ably::Realtime::Client]
73
80
  #
74
81
  # @example
@@ -82,6 +89,7 @@ module Ably
82
89
  @rest_client = Ably::Rest::Client.new(options)
83
90
  @auth = Ably::Realtime::Auth.new(self)
84
91
  @channels = Ably::Realtime::Channels.new(self)
92
+ @connection = Ably::Realtime::Connection.new(self, options)
85
93
  @echo_messages = @rest_client.options.fetch(:echo_messages, true) == false ? false : true
86
94
  @queue_messages = @rest_client.options.fetch(:queue_messages, true) == false ? false : true
87
95
  @custom_realtime_host = @rest_client.options[:realtime_host] || @rest_client.options[:ws_host]
@@ -142,10 +150,9 @@ module Ably
142
150
  endpoint_for_host(custom_realtime_host || [environment, DOMAIN].compact.join('-'))
143
151
  end
144
152
 
145
- # @!attribute [r] connection
146
- # @return [Aby::Realtime::Connection] The underlying connection for this client
153
+
147
154
  def connection
148
- @connection ||= Connection.new(self)
155
+ @connection
149
156
  end
150
157
 
151
158
  # (see Ably::Rest::Client#register_encoder)
@@ -73,7 +73,7 @@ module Ably::Realtime
73
73
  elsif connection.connected?
74
74
  logger.error "CONNECTED ProtocolMessage should not have been received when the connection is in the CONNECTED state"
75
75
  else
76
- connection.transition_state_machine :connected, reason: protocol_message.error, protocol_message: protocol_message
76
+ process_connected_message protocol_message
77
77
  end
78
78
 
79
79
  when ACTION.Disconnect, ACTION.Disconnected
@@ -141,6 +141,16 @@ module Ably::Realtime
141
141
  connection.manager.error_received_from_server protocol_message.error
142
142
  end
143
143
 
144
+ def process_connected_message(protocol_message)
145
+ if client.auth.token_client_id_allowed?(protocol_message.connection_details.client_id)
146
+ client.auth.configure_client_id protocol_message.connection_details.client_id
147
+ connection.transition_state_machine :connected, reason: protocol_message.error, protocol_message: protocol_message
148
+ else
149
+ reason = Ably::Exceptions::IncompatibleClientId.new("Client ID '#{protocol_message.connection_details.client_id}' specified by the server is incompatible with the library's configured client ID '#{client.client_id}'", 400, 40012)
150
+ connection.transition_state_machine :failed, reason: reason, protocol_message: protocol_message
151
+ end
152
+ end
153
+
144
154
  def update_connection_recovery_info(protocol_message)
145
155
  connection.update_connection_serial protocol_message.connection_serial if protocol_message.has_connection_serial?
146
156
  end
@@ -60,6 +60,14 @@ module Ably
60
60
  # Expected format for a connection recover key
61
61
  RECOVER_REGEX = /^(?<recover>[\w-]+):(?<connection_serial>\-?\w+)$/
62
62
 
63
+ # Defaults for automatic connection recovery and timeouts
64
+ DEFAULTS = {
65
+ disconnected_retry_timeout: 15, # when the connection enters the DISCONNECTED state, after this delay in milliseconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
66
+ suspended_retry_timeout: 30, # when the connection enters the SUSPENDED state, after this delay in milliseconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically
67
+ connection_state_ttl: 60, # the duration that Ably will persist the connection state when a Realtime client is abruptly disconnected
68
+ realtime_request_timeout: 10 # default timeout when establishing a connection, or sending a HEARTBEAT, CONNECT, ATTACH, DETACH or CLOSE ProtocolMessage
69
+ }.freeze
70
+
63
71
  # A unique public identifier for this connection, used to identify this member in presence events and messages
64
72
  # @return [String]
65
73
  attr_reader :id
@@ -90,23 +98,35 @@ module Ably
90
98
  # @api private
91
99
  attr_reader :manager
92
100
 
93
- # An internal queue used to manage unsent outgoing messages. You should never interface with this array directly
101
+ # An internal queue used to manage unsent outgoing messages. You should never interface with this array directly
94
102
  # @return [Array]
95
103
  # @api private
96
104
  attr_reader :__outgoing_message_queue__
97
105
 
98
- # An internal queue used to manage sent messages. You should never interface with this array directly
106
+ # An internal queue used to manage sent messages. You should never interface with this array directly
99
107
  # @return [Array]
100
108
  # @api private
101
109
  attr_reader :__pending_message_ack_queue__
102
110
 
111
+ # Configured recovery and timeout defaults for this {Connection}.
112
+ # See the configurable options in {Ably::Realtime::Client#initialize}.
113
+ # The defaults are immutable
114
+ # @return [Hash]
115
+ attr_reader :defaults
116
+
103
117
  # @api public
104
- def initialize(client)
118
+ def initialize(client, options)
105
119
  @client = client
106
120
  @client_serial = -1
107
121
  @__outgoing_message_queue__ = []
108
122
  @__pending_message_ack_queue__ = []
109
123
 
124
+ @defaults = DEFAULTS.dup
125
+ options.each do |key, val|
126
+ @defaults[key] = val if DEFAULTS.has_key?(key)
127
+ end if options.kind_of?(Hash)
128
+ @defaults.freeze
129
+
110
130
  Client::IncomingMessageDispatcher.new client, self
111
131
  Client::OutgoingMessageDispatcher.new client, self
112
132
 
@@ -133,6 +153,11 @@ module Ably
133
153
 
134
154
  # Causes the library to attempt connection. If it was previously explicitly
135
155
  # closed by the user, or was closed as a result of an unrecoverable error, a new connection will be opened.
156
+ # Succeeds when connection is established i.e. state is @Connected@
157
+ # Fails when state becomes either @Closing@, @Closed@ or @Failed@
158
+ #
159
+ # Note that if the connection remains in the disconnected ans suspended states indefinitely,
160
+ # the Deferrable or block provided may never be called
136
161
  #
137
162
  # @yield block is called as soon as this connection is in the Connected state
138
163
  #
@@ -143,14 +168,32 @@ module Ably
143
168
  raise exception_for_state_change_to(:connecting) unless can_transition_to?(:connecting)
144
169
  transition_state_machine :connecting
145
170
  end
146
- deferrable_for_state_change_to(STATE.Connected, &success_block)
171
+
172
+ Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
173
+ deferrable.callback do
174
+ yield if block_given?
175
+ end
176
+ succeed_callback = deferrable.method(:succeed)
177
+ fail_callback = deferrable.method(:fail)
178
+
179
+ once(:connected) do
180
+ deferrable.succeed
181
+ off &fail_callback
182
+ end
183
+
184
+ once(:failed, :closed, :closing) do
185
+ deferrable.fail
186
+ off &succeed_callback
187
+ end
188
+ end
147
189
  end
148
190
 
149
191
  # Sends a ping to Ably and yields the provided block when a heartbeat ping request is echoed from the server.
150
192
  # This can be useful for measuring true roundtrip client to Ably server latency for a simple message, or checking that an underlying transport is responding currently.
151
193
  # The elapsed milliseconds is passed as an argument to the block and represents the time taken to echo a ping heartbeat once the connection is in the `:connected` state.
152
194
  #
153
- # @yield [Integer] if a block is passed to this method, then this block will be called once the ping heartbeat is received with the time elapsed in milliseconds
195
+ # @yield [Integer] if a block is passed to this method, then this block will be called once the ping heartbeat is received with the time elapsed in milliseconds.
196
+ # If the ping is not received within an acceptable timeframe, the block will be called with +nil+ as he first argument
154
197
  #
155
198
  # @example
156
199
  # client = Ably::Rest::Client.new(key: 'key.id:secret')
@@ -165,9 +208,12 @@ module Ably
165
208
  raise RuntimeError, 'Cannot send a ping when connection is in a closed or failed state' if closed? || failed?
166
209
 
167
210
  started = nil
211
+ finished = false
168
212
 
169
213
  wait_for_ping = Proc.new do |protocol_message|
214
+ next if finished
170
215
  if protocol_message.action == Ably::Models::ProtocolMessage::ACTION.Heartbeat
216
+ finished = true
171
217
  __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
172
218
  time_passed = (Time.now.to_f * 1000 - started.to_f * 1000).to_i
173
219
  safe_yield block, time_passed if block_given?
@@ -175,10 +221,19 @@ module Ably
175
221
  end
176
222
 
177
223
  once_or_if(STATE.Connected) do
224
+ next if finished
178
225
  started = Time.now
179
226
  send_protocol_message action: Ably::Models::ProtocolMessage::ACTION.Heartbeat.to_i
180
227
  __incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping
181
228
  end
229
+
230
+ EventMachine.add_timer(defaults.fetch(:realtime_request_timeout)) do
231
+ next if finished
232
+ finished = true
233
+ __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
234
+ logger.warn "Ping timed out after #{defaults.fetch(:realtime_request_timeout)}s"
235
+ safe_yield block, nil if block_given?
236
+ end
182
237
  end
183
238
 
184
239
  # @yield [Boolean] True if an internet connection check appears to be up following an HTTP request to a reliable CDN
@@ -327,11 +382,12 @@ module Ably
327
382
  client.auth.auth_params.tap do |auth_deferrable|
328
383
  auth_deferrable.callback do |auth_params|
329
384
  url_params = auth_params.merge(
330
- timestamp: as_since_epoch(Time.now),
331
385
  format: client.protocol,
332
386
  echo: client.echo_messages
333
387
  )
334
388
 
389
+ url_params['clientId'] = client.auth.client_id if client.auth.has_client_id?
390
+
335
391
  if connection_resumable?
336
392
  url_params.merge! resume: key, connection_serial: serial
337
393
  logger.debug "Resuming connection key #{key} with serial #{serial}"