ably-rest 0.8.5 → 0.8.6

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 (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}"