ably 0.1.6 → 0.2.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +9 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +8 -1
  6. data/Rakefile +10 -0
  7. data/ably.gemspec +18 -18
  8. data/lib/ably.rb +6 -5
  9. data/lib/ably/auth.rb +11 -14
  10. data/lib/ably/exceptions.rb +18 -15
  11. data/lib/ably/logger.rb +102 -0
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/message.rb +19 -5
  14. data/lib/ably/models/message_encoders/base.rb +107 -0
  15. data/lib/ably/models/message_encoders/base64.rb +39 -0
  16. data/lib/ably/models/message_encoders/cipher.rb +80 -0
  17. data/lib/ably/models/message_encoders/json.rb +33 -0
  18. data/lib/ably/models/message_encoders/utf8.rb +33 -0
  19. data/lib/ably/models/paginated_resource.rb +23 -6
  20. data/lib/ably/models/presence_message.rb +19 -7
  21. data/lib/ably/models/protocol_message.rb +5 -4
  22. data/lib/ably/models/token.rb +2 -2
  23. data/lib/ably/modules/channels_collection.rb +0 -3
  24. data/lib/ably/modules/conversions.rb +3 -3
  25. data/lib/ably/modules/encodeable.rb +68 -0
  26. data/lib/ably/modules/event_emitter.rb +10 -4
  27. data/lib/ably/modules/event_machine_helpers.rb +6 -4
  28. data/lib/ably/modules/http_helpers.rb +7 -2
  29. data/lib/ably/modules/model_common.rb +2 -0
  30. data/lib/ably/modules/state_emitter.rb +10 -1
  31. data/lib/ably/realtime.rb +19 -12
  32. data/lib/ably/realtime/channel.rb +26 -13
  33. data/lib/ably/realtime/client.rb +31 -7
  34. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -3
  35. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +13 -4
  36. data/lib/ably/realtime/connection.rb +152 -46
  37. data/lib/ably/realtime/connection/connection_manager.rb +168 -0
  38. data/lib/ably/realtime/connection/connection_state_machine.rb +56 -33
  39. data/lib/ably/realtime/connection/websocket_transport.rb +56 -29
  40. data/lib/ably/{models → realtime/models}/nil_channel.rb +1 -1
  41. data/lib/ably/realtime/presence.rb +38 -13
  42. data/lib/ably/rest.rb +7 -5
  43. data/lib/ably/rest/channel.rb +24 -3
  44. data/lib/ably/rest/client.rb +56 -17
  45. data/lib/ably/rest/middleware/encoder.rb +49 -0
  46. data/lib/ably/rest/middleware/exceptions.rb +3 -2
  47. data/lib/ably/rest/middleware/logger.rb +37 -0
  48. data/lib/ably/rest/presence.rb +10 -2
  49. data/lib/ably/util/crypto.rb +57 -29
  50. data/lib/ably/util/pub_sub.rb +11 -0
  51. data/lib/ably/version.rb +1 -1
  52. data/spec/acceptance/realtime/channel_spec.rb +65 -7
  53. data/spec/acceptance/realtime/connection_spec.rb +123 -27
  54. data/spec/acceptance/realtime/message_spec.rb +319 -34
  55. data/spec/acceptance/realtime/presence_history_spec.rb +58 -0
  56. data/spec/acceptance/realtime/presence_spec.rb +160 -18
  57. data/spec/acceptance/rest/auth_spec.rb +93 -49
  58. data/spec/acceptance/rest/base_spec.rb +10 -10
  59. data/spec/acceptance/rest/channel_spec.rb +35 -19
  60. data/spec/acceptance/rest/channels_spec.rb +8 -8
  61. data/spec/acceptance/rest/message_spec.rb +224 -0
  62. data/spec/acceptance/rest/presence_spec.rb +159 -23
  63. data/spec/acceptance/rest/stats_spec.rb +5 -5
  64. data/spec/acceptance/rest/time_spec.rb +4 -4
  65. data/spec/integration/rest/auth.rb +1 -1
  66. data/spec/resources/crypto-data-128.json +56 -0
  67. data/spec/resources/crypto-data-256.json +56 -0
  68. data/spec/rspec_config.rb +39 -0
  69. data/spec/spec_helper.rb +4 -42
  70. data/spec/support/api_helper.rb +1 -1
  71. data/spec/support/event_machine_helper.rb +0 -5
  72. data/spec/support/protocol_msgbus_helper.rb +3 -3
  73. data/spec/support/test_app.rb +3 -3
  74. data/spec/unit/logger_spec.rb +135 -0
  75. data/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  76. data/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  77. data/spec/unit/models/message_encoders/json_spec.rb +135 -0
  78. data/spec/unit/models/message_encoders/utf8_spec.rb +100 -0
  79. data/spec/unit/models/message_spec.rb +16 -1
  80. data/spec/unit/models/paginated_resource_spec.rb +46 -0
  81. data/spec/unit/models/presence_message_spec.rb +18 -5
  82. data/spec/unit/models/token_spec.rb +1 -1
  83. data/spec/unit/modules/event_emitter_spec.rb +24 -10
  84. data/spec/unit/realtime/channel_spec.rb +3 -3
  85. data/spec/unit/realtime/channels_spec.rb +1 -1
  86. data/spec/unit/realtime/client_spec.rb +44 -2
  87. data/spec/unit/realtime/connection_spec.rb +2 -2
  88. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +4 -4
  89. data/spec/unit/realtime/presence_spec.rb +1 -1
  90. data/spec/unit/realtime/realtime_spec.rb +3 -3
  91. data/spec/unit/realtime/websocket_transport_spec.rb +24 -0
  92. data/spec/unit/rest/channels_spec.rb +1 -1
  93. data/spec/unit/rest/client_spec.rb +45 -10
  94. data/spec/unit/util/crypto_spec.rb +82 -0
  95. data/spec/unit/{modules → util}/pub_sub_spec.rb +13 -1
  96. metadata +43 -12
  97. data/spec/acceptance/crypto.rb +0 -63
@@ -8,6 +8,7 @@ module Ably::Realtime
8
8
  def initialize(client, connection)
9
9
  @client = client
10
10
  @connection = connection
11
+
11
12
  subscribe_to_incoming_protocol_messages
12
13
  end
13
14
 
@@ -21,7 +22,7 @@ module Ably::Realtime
21
22
  def get_channel(channel_name)
22
23
  channels.fetch(channel_name) do
23
24
  logger.warn "Received channel message for non-existent channel"
24
- Ably::Models::NilChannel.new
25
+ Ably::Realtime::Models::NilChannel.new
25
26
  end
26
27
  end
27
28
 
@@ -64,7 +65,9 @@ module Ably::Realtime
64
65
  when ACTION.Error
65
66
  logger.error "Error received: #{protocol_message.error}"
66
67
  if protocol_message.channel && !protocol_message.has_message_serial?
67
- get_channel(protocol_message.channel).change_state Ably::Realtime::Channel::STATE.Failed, protocol_message.error
68
+ dispatch_channel_error protocol_message
69
+ else
70
+ connection.transition_state_machine :failed, protocol_message.error
68
71
  end
69
72
 
70
73
  when ACTION.Attach
@@ -90,6 +93,14 @@ module Ably::Realtime
90
93
  end
91
94
  end
92
95
 
96
+ def dispatch_channel_error(protocol_message)
97
+ if !protocol_message.has_message_serial?
98
+ get_channel(protocol_message.channel).change_state Ably::Realtime::Channel::STATE.Failed, protocol_message.error
99
+ else
100
+ logger.fatal "Cannot process ProtocolMessage as not yet implemented: #{protocol_message}"
101
+ end
102
+ end
103
+
93
104
  def update_connection_id(protocol_message)
94
105
  if protocol_message.connection_id && (protocol_message.connection_id != connection.id)
95
106
  logger.debug "New connection ID set to #{protocol_message.connection_id}"
@@ -136,7 +147,7 @@ module Ably::Realtime
136
147
  end
137
148
 
138
149
  def subscribe_to_incoming_protocol_messages
139
- connection.__incoming_protocol_msgbus__.subscribe(:message) do |*args|
150
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |*args|
140
151
  dispatch_protocol_message *args
141
152
  end
142
153
  end
@@ -11,6 +11,7 @@ module Ably::Realtime
11
11
  def initialize(client, connection)
12
12
  @client = client
13
13
  @connection = connection
14
+
14
15
  subscribe_to_outgoing_protocol_message_queue
15
16
  setup_event_handlers
16
17
  end
@@ -34,19 +35,27 @@ module Ably::Realtime
34
35
  connection.__pending_message_queue__
35
36
  end
36
37
 
38
+ def current_transport_outgoing_message_bus
39
+ connection.transport.__outgoing_protocol_msgbus__
40
+ end
41
+
37
42
  def deliver_queued_protocol_messages
38
43
  condition = -> { can_send_messages? && messages_in_outgoing_queue? }
39
44
 
40
45
  non_blocking_loop_while(condition) do
41
46
  protocol_message = outgoing_queue.shift
42
- pending_queue << protocol_message if protocol_message.ack_required?
43
- connection.transport.send_object protocol_message
44
- client.logger.debug "Prot msg sent =>: #{protocol_message.action} #{protocol_message}"
47
+ current_transport_outgoing_message_bus.publish :protocol_message, protocol_message
48
+
49
+ if protocol_message.ack_required?
50
+ pending_queue << protocol_message
51
+ else
52
+ protocol_message.succeed protocol_message
53
+ end
45
54
  end
46
55
  end
47
56
 
48
57
  def subscribe_to_outgoing_protocol_message_queue
49
- connection.__outgoing_protocol_msgbus__.subscribe(:message) do |*args|
58
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |*args|
50
59
  deliver_queued_protocol_messages
51
60
  end
52
61
  end
@@ -23,6 +23,12 @@ module Ably
23
23
  # Connection::STATE.Closed
24
24
  # Connection::STATE.Failed
25
25
  #
26
+ # @example
27
+ # client = Ably::Realtime::Client.new('key.id:secret')
28
+ # client.connection.on(:connected) do
29
+ # puts "Connected with connection ID: #{client.connection.id}"
30
+ # end
31
+ #
26
32
  # @!attribute [r] state
27
33
  # @return {Ably::Realtime::Connection::STATE} connection state
28
34
  # @!attribute [r] id
@@ -31,6 +37,7 @@ module Ably
31
37
  # @return {Ably::Models::ErrorInfo} error information associated with a connection failure
32
38
  class Connection
33
39
  include Ably::Modules::EventEmitter
40
+ include Ably::Modules::Conversions
34
41
  extend Ably::Modules::Enum
35
42
 
36
43
  # Valid Connection states
@@ -52,6 +59,11 @@ module Ably
52
59
  # @return {Ably::Realtime::Connection::WebsocketTransport}
53
60
  attr_reader :transport
54
61
 
62
+ # @api private
63
+ # The connection manager responsible for creating, maintaining and closing the connection and underlying transport
64
+ # @return {Ably::Realtime::Connection::ConnectionManager}
65
+ attr_reader :manager
66
+
55
67
  # @api private
56
68
  # An internal queue used to manage unsent outgoing messages. You should never interface with this array directly
57
69
  # @return [Array]
@@ -62,11 +74,6 @@ module Ably
62
74
  # @return [Array]
63
75
  attr_reader :__pending_message_queue__
64
76
 
65
- # @api private
66
- # Timers used to manage connection state, for internal use by the client library
67
- # @return [Hash]
68
- attr_reader :timers
69
-
70
77
  # @api public
71
78
  def initialize(client)
72
79
  @client = client
@@ -75,9 +82,6 @@ module Ably
75
82
  @__outgoing_message_queue__ = []
76
83
  @__pending_message_queue__ = []
77
84
 
78
- @timers = Hash.new { |hash, key| hash[key] = [] }
79
- @timers[:initializer] << EventMachine::Timer.new(0.001) { connect }
80
-
81
85
  Client::IncomingMessageDispatcher.new client, self
82
86
  Client::OutgoingMessageDispatcher.new client, self
83
87
 
@@ -86,6 +90,7 @@ module Ably
86
90
  end
87
91
 
88
92
  @state_machine = ConnectionStateMachine.new(self)
93
+ @manager = ConnectionManager.new(self)
89
94
  @state = STATE(state_machine.current_state)
90
95
  end
91
96
 
@@ -95,13 +100,13 @@ module Ably
95
100
  #
96
101
  # @yield [Ably::Realtime::Connection] block is called as soon as this connection is in the Closed state
97
102
  #
98
- # @return <void>
103
+ # @return [void]
99
104
  def close(&block)
100
105
  if closed?
101
106
  block.call self
102
107
  else
103
108
  EventMachine.next_tick do
104
- state_machine.transition_to(:closed)
109
+ transition_state_machine(:closed)
105
110
  end
106
111
  once(STATE.Closed) { block.call self } if block_given?
107
112
  end
@@ -112,46 +117,107 @@ module Ably
112
117
  #
113
118
  # @yield [Ably::Realtime::Connection] block is called as soon as this connection is in the Connected state
114
119
  #
115
- # @return <void>
120
+ # @return [void]
116
121
  def connect(&block)
117
122
  if connected?
118
123
  block.call self
119
124
  else
120
- state_machine.transition_to(:connecting)
125
+ transition_state_machine(:connecting) unless connecting?
121
126
  once(STATE.Connected) { block.call self } if block_given?
122
127
  end
123
128
  end
124
129
 
130
+ # Sends a ping to Ably and yields the provided block when a heartbeat ping request is echoed from the server.
131
+ # 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.
132
+ # 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.
133
+ #
134
+ # @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
135
+ #
136
+ # @example
137
+ # client = Ably::Rest::Client.new(api_key: 'key.id:secret')
138
+ # client.connection.ping do |ms_elapsed|
139
+ # puts "Ping took #{ms_elapsed}ms"
140
+ # end
141
+ #
142
+ def ping(&block)
143
+ raise RuntimeError, 'Cannot send a ping when connection is in a closed or failed state' if closed? || failed?
144
+
145
+ started = nil
146
+
147
+ wait_for_ping = Proc.new do |protocol_message|
148
+ if protocol_message.action == Ably::Models::ProtocolMessage::ACTION.Heartbeat
149
+ __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
150
+ time_passed = (Time.now.to_f * 1000 - started.to_f * 1000).to_i
151
+ block.call time_passed if block_given?
152
+ end
153
+ end
154
+
155
+ once(STATE.Connected) do
156
+ started = Time.now
157
+ send_protocol_message action: Ably::Models::ProtocolMessage::ACTION.Heartbeat.to_i
158
+ __incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping
159
+ end
160
+ end
161
+
125
162
  # Reconfigure the current connection ID
126
- # @return <void>
163
+ # @return [void]
127
164
  # @api private
128
165
  def update_connection_id(connection_id)
129
166
  @id = connection_id
130
167
  end
131
168
 
132
- # Send #transition_to to connection state machine
133
- # @return [Boolean] true if new_state can be transitioned_to by state machine
169
+ # Call #transition_to on {Ably::Realtime::Connection::ConnectionStateMachine}
170
+ #
171
+ # @return [Boolean] true if new_state can be transitioned to by state machine
134
172
  # @api private
135
- def transition_state_machine(new_state)
136
- state_machine.transition_to(new_state)
173
+ def transition_state_machine(new_state, emit_object = nil)
174
+ state_machine.transition_to(new_state, emit_object)
175
+ end
176
+
177
+ # Call #transition_to! on {Ably::Realtime::Connection::ConnectionStateMachine}.
178
+ # An exception wil be raised if new_state cannot be transitioned to by state machine
179
+ #
180
+ # @return [void]
181
+ # @api private
182
+ def transition_state_machine!(new_state, emit_object = nil)
183
+ state_machine.transition_to!(new_state, emit_object)
184
+ end
185
+
186
+ # Provides an internal method for the {Ably::Realtime::Connection} state to match the {Ably::Realtime::Connection::ConnectionStateMachine}'s state
187
+ # @api private
188
+ def synchronize_state_with_statemachine(*args)
189
+ log_state_machine_state_change
190
+ change_state state_machine.current_state, state_machine.last_transition.metadata
137
191
  end
138
192
 
139
193
  # @!attribute [r] __outgoing_protocol_msgbus__
140
- # @return [Ably::Util::PubSub] Client library internal outgoing message bus
194
+ # @return [Ably::Util::PubSub] Client library internal outgoing protocol message bus
141
195
  # @api private
142
196
  def __outgoing_protocol_msgbus__
143
197
  @__outgoing_protocol_msgbus__ ||= create_pub_sub_message_bus
144
198
  end
145
199
 
146
200
  # @!attribute [r] __incoming_protocol_msgbus__
147
- # @return [Ably::Util::PubSub] Client library internal incoming message bus
201
+ # @return [Ably::Util::PubSub] Client library internal incoming protocol message bus
148
202
  # @api private
149
203
  def __incoming_protocol_msgbus__
150
204
  @__incoming_protocol_msgbus__ ||= create_pub_sub_message_bus
151
205
  end
152
206
 
207
+ # @!attribute [r] host
208
+ # @return [String] The default host name used for this connection
209
+ def host
210
+ client.endpoint.host
211
+ end
212
+
213
+ # @!attribute [r] port
214
+ # @return [Integer] The default port used for this connection
215
+ def port
216
+ client.use_tls? ? 443 : 80
217
+ end
218
+
153
219
  # @!attribute [r] logger
154
- # @return [Logger] The Logger configured for this client when the client was instantiated.
220
+ # @return [Logger] The {Ably::Logger} for this client.
155
221
  # Configure the log_level with the `:log_level` option, refer to {Ably::Realtime::Client#initialize}
156
222
  def logger
157
223
  client.logger
@@ -161,67 +227,99 @@ module Ably
161
227
  # ready to be sent
162
228
  #
163
229
  # @param [Ably::Models::ProtocolMessage] protocol_message
164
- # @return <void>
230
+ # @return [void]
165
231
  # @api private
166
232
  def send_protocol_message(protocol_message)
167
233
  add_message_serial_if_ack_required_to(protocol_message) do
168
- Models::ProtocolMessage.new(protocol_message).tap do |protocol_message|
234
+ Ably::Models::ProtocolMessage.new(protocol_message).tap do |protocol_message|
169
235
  add_message_to_outgoing_queue protocol_message
170
236
  notify_message_dispatcher_of_new_message protocol_message
171
- logger.debug("Prot msg queued =>: #{protocol_message.action} #{protocol_message}")
237
+ logger.debug("Connection: Prot msg queued =>: #{protocol_message.action} #{protocol_message}")
172
238
  end
173
239
  end
174
240
  end
175
241
 
242
+ # @api private
176
243
  def add_message_to_outgoing_queue(protocol_message)
177
244
  __outgoing_message_queue__ << protocol_message
178
245
  end
179
246
 
247
+ # @api private
180
248
  def notify_message_dispatcher_of_new_message(protocol_message)
181
- __outgoing_protocol_msgbus__.publish :message, protocol_message
249
+ __outgoing_protocol_msgbus__.publish :protocol_message, protocol_message
182
250
  end
183
251
 
184
- # Creates and sets up a new {WebSocketTransport} available on attribute #transport
185
- # @yield [Ably::Realtime::Connection::WebsocketTransport] block is called with new websocket transport
252
+ # @!attribute [r] previous_state
253
+ # @return [Ably::Realtime::Connection::STATE,nil] The previous state for this connection
186
254
  # @api private
187
- def setup_transport(&block)
188
- if transport && !transport.ready_for_release?
189
- raise RuntimeError, "Existing WebsocketTransport is connected, and must be closed first"
255
+ def previous_state
256
+ if state_machine.previous_state
257
+ STATE(state_machine.previous_state)
190
258
  end
259
+ end
191
260
 
192
- @transport = EventMachine.connect(connection_host, connection_port, WebsocketTransport, self) do |websocket|
193
- yield websocket
261
+ # @!attribute [r] state_history
262
+ # @return [Array<Hash>] All previous states including the current state in date ascending order with Hash properties :state, :metadata, :transitioned_at
263
+ # @api private
264
+ def state_history
265
+ state_machine.history.map do |transition|
266
+ {
267
+ state: STATE(transition.to_state),
268
+ metadata: transition.metadata,
269
+ transitioned_at: transition.created_at
270
+ }
194
271
  end
195
272
  end
196
273
 
197
- # Reconnect the {Ably::Realtime::Connection::WebsocketTransport} following a disconnection
198
274
  # @api private
199
- def reconnect_transport
200
- raise RuntimeError, "WebsocketTransport is not set up" if !transport
201
- raise RuntimeError, "WebsocketTransport is not disconnected so cannot be reconnected" if !transport.disconnected?
202
-
203
- transport.reconnect(connection_host, connection_port)
204
- end
275
+ def create_websocket_transport(&block)
276
+ operation = proc do
277
+ URI(client.endpoint).tap do |endpoint|
278
+ endpoint.query = URI.encode_www_form(client.auth.auth_params.merge(
279
+ timestamp: as_since_epoch(Time.now),
280
+ format: client.protocol,
281
+ echo: client.echo_messages
282
+ ))
283
+ end.to_s
284
+ end
205
285
 
206
- private
207
- attr_reader :manager, :serial, :state_machine
286
+ callback = proc do |url|
287
+ begin
288
+ @transport = EventMachine.connect(host, port, WebsocketTransport, self, url) do |websocket_transport|
289
+ yield websocket_transport if block_given?
290
+ end
291
+ rescue EventMachine::ConnectionError => error
292
+ manager.connection_failed error
293
+ end
294
+ end
208
295
 
209
- def connection_host
210
- client.endpoint.host
296
+ # client.auth.auth_params is a blocking call, so defer this into a thread
297
+ EventMachine.defer operation, callback
211
298
  end
212
299
 
213
- def connection_port
214
- client.use_tls? ? 443 : 80
300
+ # @api private
301
+ def release_websocket_transport
302
+ @transport = nil
215
303
  end
216
304
 
305
+ # As we are using a state machine, do not allow change_state to be used
306
+ # #transition_state_machine must be used instead
307
+ private :change_state
308
+
309
+ private
310
+ attr_reader :serial, :state_machine
311
+
217
312
  def create_pub_sub_message_bus
218
313
  Ably::Util::PubSub.new(
219
- coerce_into: Proc.new { |event| Models::ProtocolMessage::ACTION(event) }
314
+ coerce_into: Proc.new do |event|
315
+ raise KeyError, "Expected :protocol_message, :#{event} is disallowed" unless event == :protocol_message
316
+ :protocol_message
317
+ end
220
318
  )
221
319
  end
222
320
 
223
321
  def add_message_serial_if_ack_required_to(protocol_message)
224
- if Models::ProtocolMessage.ack_required?(protocol_message[:action])
322
+ if Ably::Models::ProtocolMessage.ack_required?(protocol_message[:action])
225
323
  add_message_serial_to(protocol_message) { yield }
226
324
  else
227
325
  yield
@@ -236,6 +334,14 @@ module Ably
236
334
  @serial -= 1
237
335
  raise e
238
336
  end
337
+
338
+ def log_state_machine_state_change
339
+ if state_machine.previous_state
340
+ logger.debug "ConnectionStateMachine: Transitioned from #{state_machine.previous_state} => #{state_machine.current_state}"
341
+ else
342
+ logger.debug "ConnectionStateMachine: Transitioned to #{state_machine.current_state}"
343
+ end
344
+ end
239
345
  end
240
346
  end
241
347
  end
@@ -0,0 +1,168 @@
1
+ module Ably::Realtime
2
+ class Connection
3
+ # ConnectionManager is responsible for all actions relating to underlying connection and transports,
4
+ # such as opening, closing, attempting reconnects etc.
5
+ #
6
+ # This is a private class and should never be used directly by developers as the API is likely to change in future.
7
+ #
8
+ # @api private
9
+ class ConnectionManager
10
+ CONNECTION_FAILED = { retry_after: 0.5, max_retries: 2, code: 80000 }.freeze
11
+
12
+ def initialize(connection)
13
+ @connection = connection
14
+
15
+ @timers = Hash.new { |hash, key| hash[key] = [] }
16
+ @timers[:initializer] << EventMachine::Timer.new(0.01) { connection.connect }
17
+ end
18
+
19
+ # Creates and sets up a new {Ably::Realtime::Connection::WebsocketTransport} available on attribute #transport
20
+ #
21
+ # @yield [Ably::Realtime::Connection::WebsocketTransport] block is called with new websocket transport
22
+ # @api private
23
+ def setup_transport(&block)
24
+ if transport && !transport.ready_for_release?
25
+ raise RuntimeError, 'Existing WebsocketTransport is connected, and must be closed first'
26
+ end
27
+
28
+ logger.debug "ConnectionManager: Opening connection to #{connection.host}:#{connection.port}"
29
+
30
+ connection.create_websocket_transport do |websocket_transport|
31
+ subscribe_to_transport_events websocket_transport
32
+ yield websocket_transport if block_given?
33
+ end
34
+ end
35
+
36
+ # Called by the transport when a connection attempt fails
37
+ #
38
+ # @api private
39
+ def connection_failed(error)
40
+ logger.info "ConnectionManager: Connection to #{connection.host}:#{connection.port} failed; #{error.message}"
41
+ connection.transition_state_machine :disconnected, Ably::Models::ErrorInfo.new(message: "Connection failed; #{error.message}", code: 80000)
42
+ end
43
+
44
+ # Ensures the underlying transport has been disconnected and all event emitter callbacks removed
45
+ #
46
+ # @api private
47
+ def destroy_transport
48
+ if transport
49
+ unsubscribe_from_transport_events transport
50
+ transport.close_connection
51
+ connection.release_websocket_transport
52
+ end
53
+ end
54
+
55
+ # Reconnect the {Ably::Realtime::Connection::WebsocketTransport} if possible, otherwise set up a new transport
56
+ #
57
+ # @api private
58
+ def reconnect_transport
59
+ if !transport || transport.disconnected?
60
+ setup_transport
61
+ else
62
+ transport.reconnect connection.host, connection.port
63
+ end
64
+ end
65
+
66
+ # Send a Close {Ably::Models::ProtocolMessage} to the server and release the transport
67
+ #
68
+ # @api private
69
+ def close_connection
70
+ protocol_message = connection.send_protocol_message(action: Ably::Models::ProtocolMessage::ACTION.Close)
71
+
72
+ unsubscribe_from_transport_events transport
73
+
74
+ protocol_message.callback do
75
+ destroy_transport
76
+ end
77
+ end
78
+
79
+ # Remove all timers set up as part of the initialize process.
80
+ # Typically called by StateMachine when connection is closed and can no longer process the timers
81
+ #
82
+ # @api private
83
+ def cancel_initialized_timers
84
+ clear_timers :initializer
85
+ end
86
+
87
+ # Remove all timers related to connection attempt retries following a disconnect or suspended connection state.
88
+ # Typically called by StateMachine when connection is opened to ensure no further connection attempts are made
89
+ #
90
+ # @api private
91
+ def cancel_connection_retry_timers
92
+ clear_timers :connection_retry_timers
93
+ end
94
+
95
+ # When a connection is disconnected try and reconnect or set the connection state to :failed
96
+ #
97
+ # @api private
98
+ def respond_to_transport_disconnected(current_transition)
99
+ error_code = current_transition && current_transition.metadata && current_transition.metadata.code
100
+
101
+ if connection.previous_state == :connecting && error_code == CONNECTION_FAILED[:code]
102
+ return if retry_connection_failed
103
+ end
104
+
105
+ # Fallback if no other criteria met
106
+ connection.transition_state_machine :failed, current_transition.metadata
107
+ end
108
+
109
+ private
110
+ attr_reader :connection
111
+
112
+ # Timers used to manage connection state, for internal use by the client library
113
+ # @return [Hash]
114
+ attr_reader :timers
115
+
116
+ def transport
117
+ connection.transport
118
+ end
119
+
120
+ def client
121
+ connection.client
122
+ end
123
+
124
+ def clear_timers(key)
125
+ timers.fetch(key, []).each(&:cancel)
126
+ end
127
+
128
+ def retry_connection_failed
129
+ if retries_for_state(:disconnected, ignore_states: [:connecting]).count < CONNECTION_FAILED[:max_retries]
130
+ logger.debug "ConnectionManager: Pausing for #{CONNECTION_FAILED[:retry_after]}s before attempting to reconnect"
131
+ @timers[:connection_retry_timers] << EventMachine::Timer.new(CONNECTION_FAILED[:retry_after]) do
132
+ connection.connect
133
+ end
134
+ end
135
+ end
136
+
137
+ def retries_for_state(state, ignore_states: [])
138
+ allowed_states = Array(state) + Array(ignore_states)
139
+
140
+ connection.state_history.reverse.take_while do |transition|
141
+ allowed_states.include?(transition[:state].to_sym)
142
+ end.select do |transition|
143
+ transition[:state] == state
144
+ end
145
+ end
146
+
147
+ def subscribe_to_transport_events(transport)
148
+ transport.__incoming_protocol_msgbus__.on(:protocol_message) do |protocol_message|
149
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
150
+ end
151
+
152
+ transport.on(:disconnected) do
153
+ connection.transition_state_machine :disconnected
154
+ end
155
+ end
156
+
157
+ def unsubscribe_from_transport_events(transport)
158
+ transport.__incoming_protocol_msgbus__.unsubscribe
159
+ transport.off
160
+ logger.debug "ConnectionManager: Unsubscribed from all events from current transport"
161
+ end
162
+
163
+ def logger
164
+ connection.logger
165
+ end
166
+ end
167
+ end
168
+ end