ably 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version.old +1 -0
  4. data/.travis.yml +0 -2
  5. data/Rakefile +22 -4
  6. data/SPEC.md +1676 -0
  7. data/ably.gemspec +1 -1
  8. data/lib/ably.rb +0 -8
  9. data/lib/ably/auth.rb +54 -46
  10. data/lib/ably/exceptions.rb +19 -5
  11. data/lib/ably/logger.rb +1 -1
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
  14. data/lib/ably/models/message.rb +15 -12
  15. data/lib/ably/models/message_encoders/base.rb +6 -5
  16. data/lib/ably/models/message_encoders/base64.rb +1 -0
  17. data/lib/ably/models/message_encoders/cipher.rb +6 -3
  18. data/lib/ably/models/message_encoders/json.rb +1 -0
  19. data/lib/ably/models/message_encoders/utf8.rb +2 -9
  20. data/lib/ably/models/nil_logger.rb +20 -0
  21. data/lib/ably/models/paginated_resource.rb +5 -2
  22. data/lib/ably/models/presence_message.rb +21 -12
  23. data/lib/ably/models/protocol_message.rb +22 -6
  24. data/lib/ably/modules/ably.rb +11 -0
  25. data/lib/ably/modules/async_wrapper.rb +2 -0
  26. data/lib/ably/modules/conversions.rb +23 -3
  27. data/lib/ably/modules/encodeable.rb +2 -1
  28. data/lib/ably/modules/enum.rb +2 -0
  29. data/lib/ably/modules/event_emitter.rb +7 -1
  30. data/lib/ably/modules/event_machine_helpers.rb +2 -0
  31. data/lib/ably/modules/http_helpers.rb +2 -0
  32. data/lib/ably/modules/model_common.rb +12 -2
  33. data/lib/ably/modules/state_emitter.rb +76 -0
  34. data/lib/ably/modules/state_machine.rb +53 -0
  35. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  36. data/lib/ably/modules/uses_state_machine.rb +74 -0
  37. data/lib/ably/realtime.rb +4 -2
  38. data/lib/ably/realtime/channel.rb +51 -58
  39. data/lib/ably/realtime/channel/channel_manager.rb +91 -0
  40. data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
  41. data/lib/ably/realtime/client.rb +70 -26
  42. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
  43. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  44. data/lib/ably/realtime/connection.rb +135 -92
  45. data/lib/ably/realtime/connection/connection_manager.rb +216 -33
  46. data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
  47. data/lib/ably/realtime/models/nil_channel.rb +10 -1
  48. data/lib/ably/realtime/presence.rb +336 -92
  49. data/lib/ably/rest.rb +2 -2
  50. data/lib/ably/rest/channel.rb +13 -4
  51. data/lib/ably/rest/client.rb +138 -38
  52. data/lib/ably/rest/middleware/logger.rb +24 -3
  53. data/lib/ably/rest/presence.rb +12 -7
  54. data/lib/ably/version.rb +1 -1
  55. data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
  56. data/spec/acceptance/realtime/channel_spec.rb +461 -120
  57. data/spec/acceptance/realtime/client_spec.rb +119 -0
  58. data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
  59. data/spec/acceptance/realtime/connection_spec.rb +571 -97
  60. data/spec/acceptance/realtime/message_spec.rb +347 -333
  61. data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
  62. data/spec/acceptance/realtime/presence_spec.rb +769 -239
  63. data/spec/acceptance/realtime/stats_spec.rb +14 -22
  64. data/spec/acceptance/realtime/time_spec.rb +16 -20
  65. data/spec/acceptance/rest/auth_spec.rb +425 -364
  66. data/spec/acceptance/rest/base_spec.rb +108 -176
  67. data/spec/acceptance/rest/channel_spec.rb +89 -89
  68. data/spec/acceptance/rest/channels_spec.rb +30 -32
  69. data/spec/acceptance/rest/client_spec.rb +273 -0
  70. data/spec/acceptance/rest/encoders_spec.rb +185 -0
  71. data/spec/acceptance/rest/message_spec.rb +186 -163
  72. data/spec/acceptance/rest/presence_spec.rb +150 -111
  73. data/spec/acceptance/rest/stats_spec.rb +45 -40
  74. data/spec/acceptance/rest/time_spec.rb +8 -10
  75. data/spec/rspec_config.rb +10 -1
  76. data/spec/shared/client_initializer_behaviour.rb +212 -0
  77. data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
  78. data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
  79. data/spec/spec_helper.rb +9 -0
  80. data/spec/support/api_helper.rb +11 -0
  81. data/spec/support/event_machine_helper.rb +101 -3
  82. data/spec/support/markdown_spec_formatter.rb +90 -0
  83. data/spec/support/private_api_formatter.rb +36 -0
  84. data/spec/support/protocol_helper.rb +32 -0
  85. data/spec/support/random_helper.rb +15 -0
  86. data/spec/support/test_app.rb +4 -0
  87. data/spec/unit/auth_spec.rb +68 -0
  88. data/spec/unit/logger_spec.rb +77 -66
  89. data/spec/unit/models/error_info_spec.rb +1 -1
  90. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
  91. data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
  92. data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
  93. data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
  94. data/spec/unit/models/message_spec.rb +160 -15
  95. data/spec/unit/models/paginated_resource_spec.rb +29 -27
  96. data/spec/unit/models/presence_message_spec.rb +163 -20
  97. data/spec/unit/models/protocol_message_spec.rb +43 -8
  98. data/spec/unit/modules/async_wrapper_spec.rb +2 -3
  99. data/spec/unit/modules/conversions_spec.rb +1 -1
  100. data/spec/unit/modules/enum_spec.rb +2 -3
  101. data/spec/unit/modules/event_emitter_spec.rb +62 -5
  102. data/spec/unit/modules/state_emitter_spec.rb +283 -0
  103. data/spec/unit/realtime/channel_spec.rb +107 -2
  104. data/spec/unit/realtime/channels_spec.rb +1 -0
  105. data/spec/unit/realtime/client_spec.rb +8 -48
  106. data/spec/unit/realtime/connection_spec.rb +3 -3
  107. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
  108. data/spec/unit/realtime/presence_spec.rb +13 -4
  109. data/spec/unit/realtime/realtime_spec.rb +0 -11
  110. data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
  111. data/spec/unit/rest/channel_spec.rb +109 -0
  112. data/spec/unit/rest/channels_spec.rb +4 -3
  113. data/spec/unit/rest/client_spec.rb +30 -125
  114. data/spec/unit/rest/rest_spec.rb +10 -0
  115. data/spec/unit/util/crypto_spec.rb +10 -5
  116. data/spec/unit/util/pub_sub_spec.rb +5 -5
  117. metadata +44 -12
  118. data/spec/integration/modules/state_emitter_spec.rb +0 -80
  119. data/spec/integration/rest/auth.rb +0 -9
@@ -2,18 +2,45 @@ module Ably::Realtime
2
2
  class Connection
3
3
  # ConnectionManager is responsible for all actions relating to underlying connection and transports,
4
4
  # such as opening, closing, attempting reconnects etc.
5
+ # Connection state changes are performed by this class and executed from {ConnectionStateMachine}
5
6
  #
6
7
  # This is a private class and should never be used directly by developers as the API is likely to change in future.
7
8
  #
8
9
  # @api private
9
10
  class ConnectionManager
10
- CONNECTION_FAILED = { retry_after: 0.5, max_retries: 2, code: 80000 }.freeze
11
+ # Configuration for automatic recovery of failed connection attempts
12
+ CONNECT_RETRY_CONFIG = {
13
+ disconnected: { retry_every: 0.5, max_time_in_state: 10 },
14
+ suspended: { retry_every: 5, max_time_in_state: 60 }
15
+ }.freeze
16
+
17
+ # Time to wait following a connection state request before it's considered a failure
18
+ TIMEOUTS = {
19
+ open: 15,
20
+ close: 10
21
+ }
22
+
23
+ # Error codes from the server that can potentially be resolved
24
+ RESOLVABLE_ERROR_CODES = {
25
+ token_expired: 40140
26
+ }
11
27
 
12
28
  def initialize(connection)
13
29
  @connection = connection
30
+ @timers = Hash.new { |hash, key| hash[key] = [] }
31
+
32
+ connection.on(:closed) do
33
+ connection.reset_resume_info
34
+ end
35
+
36
+ connection.once(:connecting) do
37
+ close_connection_when_reactor_is_stopped
38
+ end
14
39
 
15
- @timers = Hash.new { |hash, key| hash[key] = [] }
16
- @timers[:initializer] << EventMachine::Timer.new(0.01) { connection.connect }
40
+ EventMachine.next_tick do
41
+ # Connect once Connection object is initialised
42
+ connection.connect if client.connect_automatically
43
+ end
17
44
  end
18
45
 
19
46
  # Creates and sets up a new {Ably::Realtime::Connection::WebsocketTransport} available on attribute #transport
@@ -25,20 +52,30 @@ module Ably::Realtime
25
52
  raise RuntimeError, 'Existing WebsocketTransport is connected, and must be closed first'
26
53
  end
27
54
 
55
+ unless client.auth.authentication_security_requirements_met?
56
+ connection.transition_state_machine :failed, Ably::Exceptions::InsecureRequestError.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
57
+ return
58
+ end
59
+
28
60
  logger.debug "ConnectionManager: Opening connection to #{connection.host}:#{connection.port}"
29
61
 
30
62
  connection.create_websocket_transport do |websocket_transport|
31
63
  subscribe_to_transport_events websocket_transport
32
64
  yield websocket_transport if block_given?
33
65
  end
66
+
67
+ logger.debug 'ConnectionManager: Setting up automatic connection timeout timer for #{TIMEOUTS.fetch(:open)}s'
68
+ create_timeout_timer_whilst_in_state(:connect, TIMEOUTS.fetch(:open)) do
69
+ connection_opening_failed Ably::Exceptions::ConnectionTimeoutError.new("Connection to Ably timed out after #{TIMEOUTS.fetch(:open)}s", nil, 80014)
70
+ end
34
71
  end
35
72
 
36
73
  # Called by the transport when a connection attempt fails
37
74
  #
38
75
  # @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)
76
+ def connection_opening_failed(error)
77
+ logger.warn "ConnectionManager: Connection to #{connection.host}:#{connection.port} failed; #{error.message}"
78
+ connection.transition_state_machine next_retry_state, Ably::Exceptions::ConnectionError.new("Connection failed; #{error.message}", nil, 80000)
42
79
  end
43
80
 
44
81
  # Ensures the underlying transport has been disconnected and all event emitter callbacks removed
@@ -67,43 +104,75 @@ module Ably::Realtime
67
104
  #
68
105
  # @api private
69
106
  def close_connection
70
- protocol_message = connection.send_protocol_message(action: Ably::Models::ProtocolMessage::ACTION.Close)
71
-
72
- unsubscribe_from_transport_events transport
107
+ connection.send_protocol_message(action: Ably::Models::ProtocolMessage::ACTION.Close)
73
108
 
74
- protocol_message.callback do
75
- destroy_transport
109
+ create_timeout_timer_whilst_in_state(:close, TIMEOUTS.fetch(:close)) do
110
+ force_close_connection if connection.closing?
76
111
  end
77
112
  end
78
113
 
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
114
+ # Close the underlying transport immediately and set the connection state to closed
81
115
  #
82
116
  # @api private
83
- def cancel_initialized_timers
84
- clear_timers :initializer
117
+ def force_close_connection
118
+ destroy_transport
119
+ connection.transition_state_machine :closed
85
120
  end
86
121
 
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
122
+ # When a connection is disconnected whilst connecting, attempt reconnect and/or set state to :suspended or :failed
89
123
  #
90
124
  # @api private
91
- def cancel_connection_retry_timers
92
- clear_timers :connection_retry_timers
125
+ def respond_to_transport_disconnected_when_connecting(current_transition)
126
+ return unless connection.disconnected? || connection.suspended? # do nothing if state has changed through an explicit request
127
+ return unless retry_connection? # do not always reattempt connection or change state as client may be re-authorising
128
+
129
+ unless connection_retry_from_suspended_state?
130
+ return if connection_retry_for(:disconnected, ignore_states: [:connecting])
131
+ end
132
+
133
+ return if connection_retry_for(:suspended, ignore_states: [:connecting])
134
+
135
+ # Fallback if no other criteria met
136
+ connection.transition_state_machine :failed, current_transition.metadata
93
137
  end
94
138
 
95
- # When a connection is disconnected try and reconnect or set the connection state to :failed
139
+ # When a connection is disconnected after connecting, attempt reconnect and/or set state to :suspended or :failed
96
140
  #
97
141
  # @api private
98
- def respond_to_transport_disconnected(current_transition)
99
- error_code = current_transition && current_transition.metadata && current_transition.metadata.code
142
+ def respond_to_transport_disconnected_whilst_connected(current_transition)
143
+ logger.warn "ConnectionManager: Connection to #{connection.transport.url} was disconnected unexpectedly"
100
144
 
101
- if connection.previous_state == :connecting && error_code == CONNECTION_FAILED[:code]
102
- return if retry_connection_failed
145
+ if current_transition.metadata.kind_of?(Ably::Models::ErrorInfo)
146
+ connection.trigger :error, current_transition.metadata
147
+ logger.error "ConnectionManager: Error received when disconnected within ProtocolMessage - #{current_transition.metadata}"
103
148
  end
104
149
 
105
- # Fallback if no other criteria met
106
- connection.transition_state_machine :failed, current_transition.metadata
150
+ destroy_transport
151
+ respond_to_transport_disconnected_when_connecting current_transition
152
+ end
153
+
154
+ # {Ably::Models::ProtocolMessage ProtocolMessage Error} received from server.
155
+ # Some error states can be resolved by the client library.
156
+ #
157
+ # @api private
158
+ def error_received_from_server(error)
159
+ case error.code
160
+ when RESOLVABLE_ERROR_CODES.fetch(:token_expired)
161
+ connection.transition_state_machine :disconnected
162
+ connection.once_or_if(:disconnected) do
163
+ renew_token_and_reconnect error
164
+ end
165
+ else
166
+ logger.error "ConnectionManager: Error #{error.class.name} code #{error.code} received from server '#{error.message}', transitioning to failed state"
167
+ connection.transition_state_machine :failed, error
168
+ end
169
+ end
170
+
171
+ # Number of consecutive attempts for provided state
172
+ # @return [Integer]
173
+ # @api private
174
+ def retry_count_for_state(state)
175
+ retries_for_state(state, ignore_states: [:connecting]).count
107
176
  end
108
177
 
109
178
  private
@@ -121,20 +190,79 @@ module Ably::Realtime
121
190
  connection.client
122
191
  end
123
192
 
193
+ # Create a timer that will execute in timeout_in seconds.
194
+ # If the connection state changes however, cancel the timer
195
+ def create_timeout_timer_whilst_in_state(timer_id, timeout_in, &block)
196
+ raise 'Block required for timer' unless block_given?
197
+
198
+ timers[timer_id] << EventMachine::Timer.new(timeout_in) do
199
+ block.call
200
+ end
201
+ connection.once_state_changed { clear_timers timer_id }
202
+ end
203
+
124
204
  def clear_timers(key)
125
205
  timers.fetch(key, []).each(&:cancel)
126
206
  end
127
207
 
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
208
+ def next_retry_state
209
+ if connection_retry_from_suspended_state? || time_passed_since_disconnected > CONNECT_RETRY_CONFIG.fetch(:disconnected).fetch(:max_time_in_state)
210
+ :suspended
211
+ else
212
+ :disconnected
213
+ end
214
+ end
215
+
216
+ def connection_retry_from_suspended_state?
217
+ !retries_for_state(:suspended, ignore_states: [:connecting]).empty?
218
+ end
219
+
220
+ def time_passed_since_disconnected
221
+ time_spent_attempting_state(:disconnected, ignore_states: [:connecting])
222
+ end
223
+
224
+ # Reattempt a connection with a delay based on the CONNECT_RETRY_CONFIG for `from_state`
225
+ #
226
+ # @return [Boolean] True if a connection attempt has been set up, false if no further connection attempts can be made for this state
227
+ #
228
+ def connection_retry_for(from_state, options = {})
229
+ retry_params = CONNECT_RETRY_CONFIG.fetch(from_state)
230
+
231
+ if time_spent_attempting_state(from_state, options) <= retry_params.fetch(:max_time_in_state)
232
+ logger.debug "ConnectionManager: Pausing for #{retry_params.fetch(:retry_every)}s before attempting to reconnect"
233
+ create_timeout_timer_whilst_in_state(:reconnect, retry_params.fetch(:retry_every)) do
234
+ connection.connect if connection.state == from_state
133
235
  end
236
+ true
134
237
  end
135
238
  end
136
239
 
137
- def retries_for_state(state, ignore_states: [])
240
+ # Returns a float representing the amount of time passed since the first consecutive attempt of this state
241
+ #
242
+ # @param (see #retries_for_state)
243
+ # @return [Float] time passed in seconds
244
+ #
245
+ def time_spent_attempting_state(state, options)
246
+ states = retries_for_state(state, options)
247
+ if states.empty?
248
+ 0
249
+ else
250
+ Time.now.to_f - states.last[:transitioned_at].to_f
251
+ end.to_f
252
+ end
253
+
254
+ # Checks the state change history for the current connection and returns all matching consecutive states.
255
+ # This is useful to determine the number of retries of a particular state on a connection.
256
+ #
257
+ # @param state [Symbol]
258
+ # @param options [Hash]
259
+ # @option options [Array<Symbol>] :ignore_states states that should be ignored when determining consecutive historical retries for `state`.
260
+ # For example, when working out :connecting attempts, :disconnect state changes should be ignored as they are a side effect of a failed :connecting
261
+ #
262
+ # @return [Array<Hash>] Array of consecutive state attempts matching `state` in order of transitioned_at desc
263
+ #
264
+ def retries_for_state(state, options)
265
+ ignore_states = options.fetch(:ignore_states, [])
138
266
  allowed_states = Array(state) + Array(ignore_states)
139
267
 
140
268
  connection.state_history.reverse.take_while do |transition|
@@ -150,7 +278,52 @@ module Ably::Realtime
150
278
  end
151
279
 
152
280
  transport.on(:disconnected) do
153
- connection.transition_state_machine :disconnected
281
+ if connection.closing?
282
+ connection.transition_state_machine :closed
283
+ elsif !connection.closed? && !connection.disconnected?
284
+ connection.transition_state_machine :disconnected
285
+ end
286
+ end
287
+ end
288
+
289
+ def renew_token_and_reconnect(error)
290
+ if client.auth.token_renewable?
291
+ if @renewing_token
292
+ connection.transition_state_machine :failed, error
293
+ return
294
+ end
295
+
296
+ @renewing_token = true
297
+ logger.warn "ConnectionManager: Token has expired and is renewable, renewing token now"
298
+
299
+ operation = proc do
300
+ begin
301
+ client.auth.authorise
302
+ rescue StandardError => auth_error
303
+ connection.transition_state_machine :failed, auth_error
304
+ nil
305
+ end
306
+ end
307
+
308
+ callback = proc do |token|
309
+ state_changed_callback = proc do
310
+ @renewing_token = false
311
+ connection.off &state_changed_callback
312
+ end
313
+
314
+ connection.once :connected, :closed, :failed, &state_changed_callback
315
+
316
+ if token && !token.expired?
317
+ reconnect_transport
318
+ else
319
+ connection.transition_state_machine :failed, error unless connection.failed?
320
+ end
321
+ end
322
+
323
+ EventMachine.defer operation, callback
324
+ else
325
+ logger.warn "ConnectionManager: Token has expired and is not renewable"
326
+ connection.transition_state_machine :failed, error
154
327
  end
155
328
  end
156
329
 
@@ -160,6 +333,16 @@ module Ably::Realtime
160
333
  logger.debug "ConnectionManager: Unsubscribed from all events from current transport"
161
334
  end
162
335
 
336
+ def close_connection_when_reactor_is_stopped
337
+ EventMachine.add_shutdown_hook do
338
+ connection.close unless connection.closed? || connection.failed?
339
+ end
340
+ end
341
+
342
+ def retry_connection?
343
+ !@renewing_token
344
+ end
345
+
163
346
  def logger
164
347
  connection.logger
165
348
  end
@@ -1,40 +1,10 @@
1
- require 'statesman'
1
+ require 'ably/modules/state_machine'
2
2
 
3
3
  module Ably::Realtime
4
4
  class Connection
5
- module StatesmanMonkeyPatch
6
- # Override Statesman's #before_transition to support :from arrays
7
- # This can be removed once https://github.com/gocardless/statesman/issues/95 is solved
8
- def before_transition(options = nil, &block)
9
- arrayify_transition(options) do |options_without_from_array|
10
- super *options_without_from_array, &block
11
- end
12
- end
13
-
14
- def after_transition(options = nil, &block)
15
- arrayify_transition(options) do |options_without_from_array|
16
- super *options_without_from_array, &block
17
- end
18
- end
19
-
20
- private
21
- def arrayify_transition(options, &block)
22
- if options.nil?
23
- yield []
24
- elsif options.fetch(:from, nil).kind_of?(Array)
25
- options[:from].each do |from_state|
26
- yield [options.merge(from: from_state)]
27
- end
28
- else
29
- yield [options]
30
- end
31
- end
32
- end
33
-
34
- # Internal class to manage connection state, recovery and state transitions for an {Ably::Realtime::Connection}
5
+ # Internal class to manage connection state, recovery and state transitions for {Ably::Realtime::Connection}
35
6
  class ConnectionStateMachine
36
- include Statesman::Machine
37
- extend StatesmanMonkeyPatch
7
+ include Ably::Modules::StateMachine
38
8
 
39
9
  # States supported by this StateMachine match #{Connection::STATE}s
40
10
  # :initialized
@@ -42,17 +12,19 @@ module Ably::Realtime
42
12
  # :connected
43
13
  # :disconnected
44
14
  # :suspended
15
+ # :closing
45
16
  # :closed
46
17
  # :failed
47
18
  Connection::STATE.each_with_index do |state_enum, index|
48
19
  state state_enum.to_sym, initial: index == 0
49
20
  end
50
21
 
51
- transition :from => :initialized, :to => [:connecting, :closed]
52
- transition :from => :connecting, :to => [:connected, :failed, :closed, :disconnected]
53
- transition :from => :connected, :to => [:disconnected, :suspended, :closed, :failed]
54
- transition :from => :disconnected, :to => [:connecting, :closed, :failed]
55
- transition :from => :suspended, :to => [:connecting, :closed, :failed]
22
+ transition :from => :initialized, :to => [:connecting, :closing]
23
+ transition :from => :connecting, :to => [:connected, :failed, :closing, :disconnected, :suspended]
24
+ transition :from => :connected, :to => [:disconnected, :suspended, :closing, :failed]
25
+ transition :from => :disconnected, :to => [:connecting, :closing, :suspended, :failed]
26
+ transition :from => :suspended, :to => [:connecting, :closing, :failed]
27
+ transition :from => :closing, :to => [:closed]
56
28
  transition :from => :closed, :to => [:connecting]
57
29
  transition :from => :failed, :to => [:connecting]
58
30
 
@@ -68,60 +40,45 @@ module Ably::Realtime
68
40
  connection.manager.reconnect_transport
69
41
  end
70
42
 
71
- before_transition(to: [:connected]) do |connection|
72
- connection.manager.cancel_connection_retry_timers
43
+ after_transition(to: [:disconnected, :suspended], from: [:connecting]) do |connection, current_transition|
44
+ connection.manager.respond_to_transport_disconnected_when_connecting current_transition
45
+ end
46
+
47
+ after_transition(to: [:disconnected], from: [:connected]) do |connection, current_transition|
48
+ connection.manager.respond_to_transport_disconnected_whilst_connected current_transition
73
49
  end
74
50
 
75
- after_transition(to: [:disconnected], from: [:connecting]) do |connection, current_transition|
76
- connection.manager.respond_to_transport_disconnected current_transition
51
+ after_transition(to: [:disconnected, :suspended]) do |connection|
52
+ connection.manager.destroy_transport # never reuse a transport if the connection has failed
77
53
  end
78
54
 
79
- after_transition(to: [:failed]) do |connection|
55
+ after_transition(to: [:failed]) do |connection, current_transition|
56
+ connection.logger.fatal "ConnectionStateMachine: Connection failed - #{current_transition.metadata}"
80
57
  connection.manager.destroy_transport
81
58
  end
82
59
 
83
- before_transition(to: [:closed], from: [:initialized]) do |connection|
84
- connection.manager.cancel_initialized_timers
60
+ after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
61
+ connection.manager.force_close_connection
85
62
  end
86
63
 
87
- before_transition(to: [:closed], from: [:connecting, :connected, :disconnected, :suspended]) do |connection|
64
+ after_transition(to: [:closing], from: [:connecting, :connected]) do |connection|
88
65
  connection.manager.close_connection
89
66
  end
90
67
 
91
- # Override Statesman's #transition_to so that:
92
- # * log state change failures to {Logger}
93
- def transition_to(state, *args)
94
- unless result = super(state, *args)
95
- logger.fatal "ConnectionStateMachine: Unable to transition from #{current_state} => #{state}"
96
- end
97
- result
68
+ before_transition(to: [:closed], from: [:closing]) do |connection|
69
+ connection.manager.destroy_transport
98
70
  end
99
71
 
100
- def previous_transition
101
- history[-2]
72
+ # Transitions responsible for updating connection#error_reason
73
+ before_transition(to: [:disconnected, :suspended, :failed]) do |connection, current_transition|
74
+ connection.set_failed_connection_error_reason current_transition.metadata
102
75
  end
103
76
 
104
- def previous_state
105
- previous_transition.to_state if previous_transition
77
+ before_transition(to: [:connected, :closed]) do |connection, current_transition|
78
+ connection.set_failed_connection_error_reason nil
106
79
  end
107
80
 
108
81
  private
109
- # TODO: Implement once CLOSED ProtocolMessage is sent back from Ably in response to a CLOSE message
110
- #
111
- # FORCE_CONNECTION_CLOSED_TIMEOUT = 5
112
- #
113
- # def force_closed_unless_server_acknowledge_closed
114
- # timeouts[:close_connection] << EventMachine::Timer.new(FORCE_CONNECTION_CLOSED_TIMEOUT) do
115
- # transition_to :closed
116
- # end
117
- # end
118
- #
119
- # def clear_force_closed_timeouts
120
- # timeouts[:close_connection].each do |timeout|
121
- # timeout.cancel
122
- # end.clear
123
- # end
124
-
125
82
  def connection
126
83
  object
127
84
  end