ably 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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