ably-rest 0.9.3 → 1.0.0

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/ably-rest.gemspec +2 -1
  3. data/lib/submodules/ably-ruby/.travis.yml +6 -4
  4. data/lib/submodules/ably-ruby/CHANGELOG.md +52 -61
  5. data/lib/submodules/ably-ruby/README.md +10 -0
  6. data/lib/submodules/ably-ruby/SPEC.md +1473 -852
  7. data/lib/submodules/ably-ruby/ably.gemspec +2 -1
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +57 -25
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +34 -8
  10. data/lib/submodules/ably-ruby/lib/ably/logger.rb +10 -1
  11. data/lib/submodules/ably-ruby/lib/ably/models/auth_details.rb +42 -0
  12. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +18 -4
  13. data/lib/submodules/ably-ruby/lib/ably/models/connection_details.rb +6 -3
  14. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +1 -1
  16. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +12 -1
  17. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +101 -97
  18. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +13 -1
  19. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +7 -3
  21. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +17 -7
  22. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +29 -14
  23. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +7 -4
  24. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -4
  25. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +7 -3
  26. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +2 -0
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +79 -31
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +62 -26
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +154 -65
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +16 -3
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +108 -49
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +165 -59
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +67 -45
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +198 -36
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +30 -6
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  42. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +3 -3
  43. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +21 -8
  44. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +1 -3
  45. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +2 -2
  46. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  47. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +1 -1
  48. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +26 -0
  49. data/lib/submodules/ably-ruby/lib/ably/version.rb +2 -2
  50. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +416 -99
  51. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  52. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +1011 -160
  53. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +2 -2
  54. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  55. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +436 -97
  56. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +52 -23
  57. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  58. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1160 -105
  59. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +151 -22
  60. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +1 -1
  61. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +88 -27
  62. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +42 -15
  63. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +4 -4
  64. data/lib/submodules/ably-ruby/spec/rspec_config.rb +2 -1
  65. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +2 -2
  66. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +6 -2
  67. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +20 -4
  68. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +32 -1
  69. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +4 -11
  70. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +28 -2
  71. data/lib/submodules/ably-ruby/spec/unit/models/auth_details_spec.rb +49 -0
  72. data/lib/submodules/ably-ruby/spec/unit/models/channel_state_change_spec.rb +23 -3
  73. data/lib/submodules/ably-ruby/spec/unit/models/connection_details_spec.rb +12 -1
  74. data/lib/submodules/ably-ruby/spec/unit/models/connection_state_change_spec.rb +15 -4
  75. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +34 -2
  76. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +73 -2
  77. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +64 -6
  78. data/lib/submodules/ably-ruby/spec/unit/models/token_details_spec.rb +1 -1
  79. data/lib/submodules/ably-ruby/spec/unit/models/token_request_spec.rb +1 -1
  80. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -1
  81. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +69 -0
  82. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +149 -22
  83. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +9 -3
  84. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +1 -1
  85. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +8 -5
  86. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  87. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +4 -3
  88. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +3 -3
  90. metadata +7 -5
@@ -20,11 +20,12 @@ module Ably::Realtime
20
20
  state state_enum.to_sym, initial: index == 0
21
21
  end
22
22
 
23
- transition :from => :initialized, :to => [:attaching]
24
- transition :from => :attaching, :to => [:attached, :detaching, :failed]
25
- transition :from => :attached, :to => [:detaching, :detached, :failed]
26
- transition :from => :detaching, :to => [:detached, :attaching, :failed]
23
+ transition :from => :initialized, :to => [:attaching, :failed]
24
+ transition :from => :attaching, :to => [:attached, :detaching, :failed, :suspended]
25
+ transition :from => :attached, :to => [:attaching, :detaching, :detached, :failed, :suspended]
26
+ transition :from => :detaching, :to => [:detached, :attaching, :attached, :failed, :suspended]
27
27
  transition :from => :detached, :to => [:attaching, :attached, :failed]
28
+ transition :from => :suspended, :to => [:attaching, :detached, :failed]
28
29
  transition :from => :failed, :to => [:attaching]
29
30
 
30
31
  after_transition do |channel, transition|
@@ -41,31 +42,29 @@ module Ably::Realtime
41
42
 
42
43
  after_transition(to: [:detaching]) do |channel, current_transition|
43
44
  err = error_from_state_change(current_transition)
44
- channel.manager.detach err
45
+ channel.manager.detach err, current_transition.metadata.previous
45
46
  end
46
47
 
47
- after_transition(to: [:detached]) do |channel, current_transition|
48
+ after_transition(to: [:detached, :failed, :suspended]) do |channel, current_transition|
48
49
  err = error_from_state_change(current_transition)
49
- channel.manager.fail_messages_awaiting_ack err
50
- channel.manager.emit_error err if err
50
+ channel.manager.fail_queued_messages err
51
+ channel.manager.log_channel_error err if err
51
52
  end
52
53
 
53
- after_transition(to: [:failed]) do |channel, current_transition|
54
- err = error_from_state_change(current_transition)
55
- channel.manager.fail_messages_awaiting_ack err
56
- channel.manager.emit_error err if err
54
+ after_transition(to: [:suspended]) do |channel, current_transition|
55
+ channel.manager.start_attach_from_suspended_timer
57
56
  end
58
57
 
59
58
  # Transitions responsible for updating channel#error_reason
60
- before_transition(to: [:failed]) do |channel, current_transition|
59
+ before_transition(to: [:failed, :suspended]) do |channel, current_transition|
61
60
  err = error_from_state_change(current_transition)
62
- channel.set_failed_channel_error_reason err if err
61
+ channel.set_channel_error_reason err if err
63
62
  end
64
63
 
65
64
  before_transition(to: [:attached, :detached]) do |channel, current_transition|
66
65
  err = error_from_state_change(current_transition)
67
66
  if err
68
- channel.set_failed_channel_error_reason err
67
+ channel.set_channel_error_reason err
69
68
  else
70
69
  # Attached & Detached are "healthy" final states so reset the error reason
71
70
  channel.clear_error_reason
@@ -78,8 +78,10 @@ module Ably
78
78
  # @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
79
79
  # @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.
80
80
  #
81
- # @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
82
- # @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
81
+ # @option options [Integer] :channel_retry_timeout (15 seconds). When a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED
82
+ # @option options [Integer] :disconnected_retry_timeout (15 seconds). When the connection enters the DISCONNECTED state, after this delay in seconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
83
+ # @option options [Integer] :suspended_retry_timeout (30 seconds). When the connection enters the SUSPENDED state, after this delay in seconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically
84
+ # @option options [Boolean] :disable_websocket_heartbeats WebSocket heartbeats are more efficient than protocol level heartbeats, however they can be disabled for development purposes
83
85
  #
84
86
  # @return [Ably::Realtime::Client]
85
87
  #
@@ -91,7 +93,18 @@ module Ably
91
93
  # client = Ably::Realtime::Client.new(key: 'key.id:secret', client_id: 'john')
92
94
  #
93
95
  def initialize(options)
94
- @rest_client = Ably::Rest::Client.new(options)
96
+ raise ArgumentError, 'Options Hash is expected' if options.nil?
97
+
98
+ options = options.clone
99
+ if options.kind_of?(String)
100
+ options = if options.match(Ably::Auth::API_KEY_REGEX)
101
+ { key: options }
102
+ else
103
+ { token: options }
104
+ end
105
+ end
106
+
107
+ @rest_client = Ably::Rest::Client.new(options.merge(realtime_client: self))
95
108
  @auth = Ably::Realtime::Auth.new(self)
96
109
  @channels = Ably::Realtime::Channels.new(self)
97
110
  @connection = Ably::Realtime::Connection.new(self, options)
@@ -27,7 +27,7 @@ module Ably::Realtime
27
27
 
28
28
  def get_channel(channel_name)
29
29
  channels.fetch(channel_name) do
30
- logger.warn "Received channel message for non-existent channel"
30
+ logger.warn { "Received channel message for non-existent channel" }
31
31
  Ably::Realtime::Models::NilChannel.new
32
32
  end
33
33
  end
@@ -43,19 +43,13 @@ module Ably::Realtime
43
43
  raise ArgumentError, "Expected a ProtocolMessage. Received #{protocol_message}"
44
44
  end
45
45
 
46
- unless [:nack, :error].include?(protocol_message.action)
47
- logger.debug "#{protocol_message.action} received: #{protocol_message}"
46
+ unless protocol_message.action.match_any?(:nack, :error)
47
+ logger.debug { "#{protocol_message.action} received: #{protocol_message}" }
48
48
  end
49
49
 
50
- if [:sync, :presence, :message].any? { |prevent_duplicate| protocol_message.action == prevent_duplicate }
50
+ if protocol_message.action.match_any?(:sync, :presence, :message)
51
51
  if connection.serial && protocol_message.has_connection_serial? && protocol_message.connection_serial <= connection.serial
52
- error_target = if protocol_message.channel
53
- get_channel(protocol_message.channel)
54
- else
55
- connection
56
- end
57
52
  error_message = "Protocol error, duplicate message received for serial #{protocol_message.connection_serial}"
58
- error_target.emit :error, Ably::Exceptions::ProtocolError.new(error_message, 400, 80013)
59
53
  logger.error error_message
60
54
  return
61
55
  end
@@ -69,15 +63,18 @@ module Ably::Realtime
69
63
  ack_pending_queue_for_message_serial(protocol_message) if protocol_message.has_message_serial?
70
64
 
71
65
  when ACTION.Nack
72
- logger.warn "NACK received: #{protocol_message}"
66
+ logger.warn { "NACK received: #{protocol_message}" }
73
67
  nack_pending_queue_for_message_serial(protocol_message) if protocol_message.has_message_serial?
74
68
 
75
69
  when ACTION.Connect
76
70
  when ACTION.Connected
77
- if connection.disconnected? || connection.closing? || connection.closed? || connection.failed?
78
- logger.debug "Incoming CONNECTED ProtocolMessage discarded as connection has moved on and is in state: #{connection.state}"
71
+ if connection.closing?
72
+ logger.debug { "Out-of-order incoming CONNECTED ProtocolMessage discarded as connection has moved on and is in state: #{connection.state}" }
73
+ elsif connection.disconnected? || connection.closing? || connection.closed? || connection.failed?
74
+ logger.warn { "Out-of-order incoming CONNECTED ProtocolMessage discarded as connection has moved on and is in state: #{connection.state}" }
79
75
  elsif connection.connected?
80
- logger.error "CONNECTED ProtocolMessage should not have been received when the connection is in the CONNECTED state"
76
+ logger.debug { "Updated CONNECTED ProtocolMessage received (whilst connected)" }
77
+ process_connected_update_message protocol_message
81
78
  else
82
79
  process_connected_message protocol_message
83
80
  end
@@ -90,7 +87,7 @@ module Ably::Realtime
90
87
  connection.transition_state_machine :closed unless connection.closed?
91
88
 
92
89
  when ACTION.Error
93
- if protocol_message.channel && !protocol_message.has_message_serial?
90
+ if protocol_message.channel
94
91
  dispatch_channel_error protocol_message
95
92
  else
96
93
  process_connection_error protocol_message
@@ -99,21 +96,22 @@ module Ably::Realtime
99
96
  when ACTION.Attach
100
97
  when ACTION.Attached
101
98
  get_channel(protocol_message.channel).tap do |channel|
102
- channel.transition_state_machine :attached, reason: protocol_message.error, protocol_message: protocol_message unless channel.attached?
99
+ if channel.attached?
100
+ channel.manager.duplicate_attached_received protocol_message
101
+ else
102
+ channel.transition_state_machine :attached, reason: protocol_message.error, resumed: protocol_message.has_channel_resumed_flag?, protocol_message: protocol_message
103
+ end
103
104
  end
104
105
 
105
106
  when ACTION.Detach
106
107
  when ACTION.Detached
107
108
  get_channel(protocol_message.channel).tap do |channel|
108
- channel.transition_state_machine :detached unless channel.detached?
109
+ channel.manager.detached_received protocol_message.error
109
110
  end
110
111
 
111
112
  when ACTION.Sync
112
113
  presence = get_channel(protocol_message.channel).presence
113
- protocol_message.presence.each do |presence_message|
114
- presence.__incoming_msgbus__.publish :sync, presence_message
115
- end
116
- presence.members.update_sync_serial protocol_message.channel_serial
114
+ presence.manager.sync_process_messages protocol_message.channel_serial, protocol_message.presence
117
115
 
118
116
  when ACTION.Presence
119
117
  presence = get_channel(protocol_message.channel).presence
@@ -127,19 +125,23 @@ module Ably::Realtime
127
125
  channel.__incoming_msgbus__.publish :message, message
128
126
  end
129
127
 
128
+ when ACTION.Auth
129
+ client.auth.authorize
130
+
130
131
  else
131
132
  error = Ably::Exceptions::ProtocolError.new("Protocol Message Action #{protocol_message.action} is unsupported by this MessageDispatcher", 400, 80013)
132
- client.connection.emit :error, error
133
133
  logger.fatal error.message
134
134
  end
135
+
136
+ connection.set_connection_confirmed_alive
135
137
  end
136
138
 
137
139
  def dispatch_channel_error(protocol_message)
138
- logger.warn "Channel Error message received: #{protocol_message.error}"
140
+ logger.warn { "Channel Error message received: #{protocol_message.error}" }
139
141
  if !protocol_message.has_message_serial?
140
142
  get_channel(protocol_message.channel).transition_state_machine :failed, reason: protocol_message.error
141
143
  else
142
- logger.fatal "Cannot process ProtocolMessage as not yet implemented: #{protocol_message}"
144
+ logger.fatal { "Cannot process ProtocolMessage ERROR with message serial as not yet implemented: #{protocol_message}" }
143
145
  end
144
146
  end
145
147
 
@@ -149,11 +151,18 @@ module Ably::Realtime
149
151
 
150
152
  def process_connected_message(protocol_message)
151
153
  if client.auth.token_client_id_allowed?(protocol_message.connection_details.client_id)
152
- client.auth.configure_client_id protocol_message.connection_details.client_id
153
- client.connection.set_connection_details protocol_message.connection_details
154
154
  connection.transition_state_machine :connected, reason: protocol_message.error, protocol_message: protocol_message
155
155
  else
156
- 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)
156
+ 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}'")
157
+ connection.transition_state_machine :failed, reason: reason, protocol_message: protocol_message
158
+ end
159
+ end
160
+
161
+ def process_connected_update_message(protocol_message)
162
+ if client.auth.token_client_id_allowed?(protocol_message.connection_details.client_id)
163
+ connection.manager.connected_update protocol_message
164
+ else
165
+ reason = Ably::Exceptions::IncompatibleClientId.new("Client ID '#{protocol_message.connection_details.client_id}' in CONNECTED update specified by the server is incompatible with the library's configured client ID '#{client.client_id}'")
157
166
  connection.transition_state_machine :failed, reason: reason, protocol_message: protocol_message
158
167
  end
159
168
  end
@@ -178,14 +187,14 @@ module Ably::Realtime
178
187
 
179
188
  def ack_messages(messages)
180
189
  messages.each do |message|
181
- logger.debug "Calling ACK success callbacks for #{message.class.name} - #{message.to_json}"
190
+ logger.debug { "Calling ACK success callbacks for #{message.class.name} - #{message.to_json}" }
182
191
  message.succeed message
183
192
  end
184
193
  end
185
194
 
186
195
  def nack_messages(messages, protocol_message)
187
196
  messages.each do |message|
188
- logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}"
197
+ logger.debug { "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}" }
189
198
  message.fail protocol_message.error
190
199
  end
191
200
  end
@@ -74,7 +74,12 @@ module Ably::Realtime
74
74
 
75
75
  def setup_event_handlers
76
76
  connection.unsafe_on(:connected) do
77
- deliver_queued_protocol_messages
77
+ # Give connection manager enough time to prevent message delivery if necessary
78
+ # For example, if reconnecting and connection and channel state is lost,
79
+ # then the queued messages must be NACK'd
80
+ EventMachine.next_tick do
81
+ deliver_queued_protocol_messages
82
+ end
78
83
  end
79
84
  end
80
85
  end
@@ -1,3 +1,5 @@
1
+ require 'securerandom'
2
+
1
3
  module Ably
2
4
  module Realtime
3
5
  # The Connection class represents the connection associated with an Ably Realtime instance.
@@ -25,8 +27,6 @@ module Ably
25
27
  # Connection::STATE.Closed
26
28
  # Connection::STATE.Failed
27
29
  #
28
- # Connection emit errors - use `on(:error)` to subscribe to errors
29
- #
30
30
  # @example
31
31
  # client = Ably::Realtime::Client.new('key.id:secret')
32
32
  # client.connection.on(:connected) do
@@ -42,7 +42,8 @@ module Ably
42
42
  include Ably::Modules::SafeYield
43
43
  extend Ably::Modules::Enum
44
44
 
45
- # Valid Connection states
45
+ # ConnectionState
46
+ # The permited states for this connection
46
47
  STATE = ruby_enum('STATE',
47
48
  :initialized,
48
49
  :connecting,
@@ -53,6 +54,13 @@ module Ably
53
54
  :closed,
54
55
  :failed
55
56
  )
57
+
58
+ # ConnectionEvent
59
+ # The permitted connection events that are emitted for this connection
60
+ EVENT = ruby_enum('EVENT',
61
+ STATE.to_sym_arr + [:update]
62
+ )
63
+
56
64
  include Ably::Modules::StateEmitter
57
65
  include Ably::Modules::UsesStateMachine
58
66
  ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
@@ -62,11 +70,13 @@ module Ably
62
70
 
63
71
  # Defaults for automatic connection recovery and timeouts
64
72
  DEFAULTS = {
73
+ channel_retry_timeout: 15, # when a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED
65
74
  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
75
  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
76
  connection_state_ttl: 120, # the duration that Ably will persist the connection state when a Realtime client is abruptly disconnected
68
- max_connection_state_ttl: nil, # allow a max TTL to be passed in for CI test purposes thus overiding any connection_state_ttl sent from Ably
77
+ max_connection_state_ttl: nil, # allow a max TTL to be passed in, usually for CI test purposes thus overiding any connection_state_ttl sent from Ably
69
78
  realtime_request_timeout: 10, # default timeout when establishing a connection, or sending a HEARTBEAT, CONNECT, ATTACH, DETACH or CLOSE ProtocolMessage
79
+ websocket_heartbeats_disabled: false,
70
80
  }.freeze
71
81
 
72
82
  # A unique public identifier for this connection, used to identify this member in presence events and messages
@@ -122,9 +132,9 @@ module Ably
122
132
  # @api public
123
133
  def initialize(client, options)
124
134
  @client = client
125
- @client_serial = -1
126
135
  @__outgoing_message_queue__ = []
127
136
  @__pending_message_ack_queue__ = []
137
+ reset_client_serial
128
138
 
129
139
  @defaults = DEFAULTS.dup
130
140
  options.each do |key, val|
@@ -150,7 +160,9 @@ module Ably
150
160
  #
151
161
  def close(&success_block)
152
162
  unless closing? || closed?
153
- raise exception_for_state_change_to(:closing) unless can_transition_to?(:closing)
163
+ unless can_transition_to?(:closing)
164
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:closing))
165
+ end
154
166
  transition_state_machine :closing
155
167
  end
156
168
  deferrable_for_state_change_to(STATE.Closed, &success_block)
@@ -170,8 +182,11 @@ module Ably
170
182
  #
171
183
  def connect(&success_block)
172
184
  unless connecting? || connected?
173
- raise exception_for_state_change_to(:connecting) unless can_transition_to?(:connecting)
174
- transition_state_machine :connecting
185
+ unless can_transition_to?(:connecting)
186
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:connecting))
187
+ end
188
+ # If connect called in a suspended block, we want to ensure the other callbacks have finished their work first
189
+ EventMachine.next_tick { transition_state_machine :connecting if can_transition_to?(:connecting) }
175
190
  end
176
191
 
177
192
  Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
@@ -181,12 +196,12 @@ module Ably
181
196
  succeed_callback = deferrable.method(:succeed)
182
197
  fail_callback = deferrable.method(:fail)
183
198
 
184
- once(:connected) do
199
+ unsafe_once(:connected) do
185
200
  deferrable.succeed
186
201
  off(&fail_callback)
187
202
  end
188
203
 
189
- once(:failed, :closed, :closing) do
204
+ unsafe_once(:failed, :closed, :closing) do
190
205
  deferrable.fail
191
206
  off(&succeed_callback)
192
207
  end
@@ -206,38 +221,53 @@ module Ably
206
221
  # puts "Ping took #{elapsed_s}s"
207
222
  # end
208
223
  #
209
- # @return [void]
224
+ # @return [Ably::Util::SafeDeferrable]
210
225
  #
211
226
  def ping(&block)
212
- raise RuntimeError, 'Cannot send a ping when connection is not open' if initialized?
213
- raise RuntimeError, 'Cannot send a ping when connection is in a closed or failed state' if closed? || failed?
227
+ if initialized? || suspended? || closing? || closed? || failed?
228
+ error = Ably::Models::ErrorInfo.new(message: "Cannot send a ping when the connection is #{state}", code: 80003)
229
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
230
+ end
214
231
 
215
- started = nil
216
- finished = false
232
+ Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
233
+ started = nil
234
+ finished = false
235
+ ping_id = SecureRandom.hex(16)
236
+ heartbeat_action = Ably::Models::ProtocolMessage::ACTION.Heartbeat
237
+
238
+ wait_for_ping = Proc.new do |protocol_message|
239
+ next if finished
240
+ if protocol_message.action == heartbeat_action && protocol_message.id == ping_id
241
+ finished = true
242
+ __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
243
+ time_passed = Time.now.to_f - started.to_f
244
+ deferrable.succeed time_passed
245
+ safe_yield block, time_passed if block_given?
246
+ end
247
+ end
217
248
 
218
- wait_for_ping = Proc.new do |protocol_message|
219
- next if finished
220
- if protocol_message.action == Ably::Models::ProtocolMessage::ACTION.Heartbeat
221
- finished = true
222
- __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
223
- time_passed = Time.now.to_f - started.to_f
224
- safe_yield block, time_passed if block_given?
249
+ once_or_if(STATE.Connected) do
250
+ next if finished
251
+ started = Time.now
252
+ send_protocol_message action: heartbeat_action.to_i, id: ping_id
253
+ __incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping
225
254
  end
226
- end
227
255
 
228
- once_or_if(STATE.Connected) do
229
- next if finished
230
- started = Time.now
231
- send_protocol_message action: Ably::Models::ProtocolMessage::ACTION.Heartbeat.to_i
232
- __incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping
233
- end
256
+ once_or_if([:suspended, :closing, :closed, :failed]) do
257
+ next if finished
258
+ finished = true
259
+ deferrable.fail Ably::Models::ErrorInfo.new(message: "Ping failed as connection has changed state to #{state}", code: 80003)
260
+ end
234
261
 
235
- EventMachine.add_timer(defaults.fetch(:realtime_request_timeout)) do
236
- next if finished
237
- finished = true
238
- __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
239
- logger.warn "Ping timed out after #{defaults.fetch(:realtime_request_timeout)}s"
240
- safe_yield block, nil if block_given?
262
+ EventMachine.add_timer(defaults.fetch(:realtime_request_timeout)) do
263
+ next if finished
264
+ finished = true
265
+ __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
266
+ error_msg = "Ping timed out after #{defaults.fetch(:realtime_request_timeout)}s"
267
+ logger.warn { error_msg }
268
+ deferrable.fail Ably::Models::ErrorInfo.new(message: error_msg, code: 50003)
269
+ safe_yield block, nil if block_given?
270
+ end
241
271
  end
242
272
  end
243
273
 
@@ -364,7 +394,7 @@ module Ably
364
394
  Ably::Models::ProtocolMessage.new(protocol_message, logger: logger).tap do |message|
365
395
  add_message_to_outgoing_queue message
366
396
  notify_message_dispatcher_of_new_message message
367
- logger.debug("Connection: Prot msg queued =>: #{message.action} #{message}")
397
+ logger.debug { "Connection: Prot msg queued =>: #{message.action} #{message}" }
368
398
  end
369
399
  end
370
400
  end
@@ -387,21 +417,24 @@ module Ably
387
417
  client.auth.auth_params.tap do |auth_deferrable|
388
418
  auth_deferrable.callback do |auth_params|
389
419
  url_params = auth_params.merge(
390
- format: client.protocol,
391
- echo: client.echo_messages,
392
- v: Ably::PROTOCOL_VERSION,
393
- lib: client.rest_client.lib_version_id,
420
+ format: client.protocol,
421
+ echo: client.echo_messages,
422
+ v: Ably::PROTOCOL_VERSION,
423
+ lib: client.rest_client.lib_version_id,
394
424
  )
395
425
 
426
+ # Use native websocket heartbeats if possible
427
+ url_params['heartbeats'] = 'false' unless defaults.fetch(:websocket_heartbeats_disabled)
428
+
396
429
  url_params['clientId'] = client.auth.client_id if client.auth.has_client_id?
397
430
 
398
431
  if connection_resumable?
399
432
  url_params.merge! resume: key, connection_serial: serial
400
- logger.debug "Resuming connection key #{key} with serial #{serial}"
433
+ logger.debug { "Resuming connection key #{key} with serial #{serial}" }
401
434
  elsif connection_recoverable?
402
- url_params.merge! recover: connection_recover_parts[:recover], connection_serial: connection_recover_parts[:connection_serial]
403
- logger.debug "Recovering connection with key #{client.recover}"
404
- once(:connected, :closed, :failed) do
435
+ url_params.merge! recover: connection_recover_parts[:recover], connectionSerial: connection_recover_parts[:connection_serial]
436
+ logger.debug { "Recovering connection with key #{client.recover}" }
437
+ unsafe_once(:connected, :closed, :failed) do
405
438
  client.disable_automatic_connection_recovery
406
439
  end
407
440
  end
@@ -412,7 +445,7 @@ module Ably
412
445
 
413
446
  determine_host do |host|
414
447
  begin
415
- logger.debug "Connection: Opening socket connection to #{host}:#{port}/#{url.path}?#{url.query}"
448
+ logger.debug { "Connection: Opening socket connection to #{host}:#{port}/#{url.path}?#{url.query}" }
416
449
  @transport = create_transport(host, port, url) do |websocket_transport|
417
450
  websocket_deferrable.succeed websocket_transport
418
451
  end
@@ -451,7 +484,7 @@ module Ably
451
484
 
452
485
  # Executes registered callbacks for a successful connection resume event
453
486
  # @api private
454
- def resumed
487
+ def trigger_resumed
455
488
  resume_callbacks.each(&:call)
456
489
  end
457
490
 
@@ -490,6 +523,33 @@ module Ably
490
523
  @connection_state_ttl = val
491
524
  end
492
525
 
526
+ # @api private
527
+ def heartbeat_interval
528
+ # See RTN23a
529
+ (details && details.max_idle_interval).to_i +
530
+ defaults.fetch(:realtime_request_timeout)
531
+ end
532
+
533
+ # Resets the client serial (msgSerial) sent to Ably for each new {Ably::Models::ProtocolMessage}
534
+ # (see #client_serial)
535
+ # @api private
536
+ def reset_client_serial
537
+ @client_serial = -1
538
+ end
539
+
540
+ # When a hearbeat or any other message from Ably is received
541
+ # we know it's alive, see #RTN23
542
+ # @api private
543
+ def set_connection_confirmed_alive
544
+ @last_liveness_event = Time.now
545
+ manager.reset_liveness_timer
546
+ end
547
+
548
+ # @api private
549
+ def time_since_connection_confirmed_alive?
550
+ Time.now.to_i - @last_liveness_event.to_i
551
+ end
552
+
493
553
  # As we are using a state machine, do not allow change_state to be used
494
554
  # #transition_state_machine must be used instead
495
555
  private :change_state
@@ -500,9 +560,8 @@ module Ably
500
560
  # Note that this is different to the connection serial that contains the last known serial number
501
561
  # received from the server.
502
562
  #
503
- # A client serial number therefore does not guarantee a message has been received, only sent.
504
- # A connection serial guarantees the server has received the message and is thus used for connection
505
- # recovery and resumes.
563
+ # A message serial number does not guarantee a message has been received, only sent.
564
+ # A connection serial guarantees the server has received the message and is thus used for connection recovery and resumes.
506
565
  # @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent
507
566
  def client_serial
508
567
  @client_serial