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
@@ -0,0 +1,68 @@
1
+ require 'ably/modules/state_machine'
2
+
3
+ module Ably::Realtime
4
+ class Channel
5
+ # Internal class to manage channel state for {Ably::Realtime::Channel}
6
+ #
7
+ # @api private
8
+ #
9
+ class ChannelStateMachine
10
+ include Ably::Modules::StateMachine
11
+
12
+ # States supported by this StateMachine match #{Channel::STATE}s
13
+ # :initialized
14
+ # :attaching
15
+ # :attached
16
+ # :detaching
17
+ # :detached
18
+ # :failed
19
+ Channel::STATE.each_with_index do |state_enum, index|
20
+ state state_enum.to_sym, initial: index == 0
21
+ end
22
+
23
+ transition :from => :initialized, :to => [:attaching]
24
+ transition :from => :attaching, :to => [:attached, :detaching, :failed]
25
+ transition :from => :attached, :to => [:detaching, :failed]
26
+ transition :from => :detaching, :to => [:detached, :attaching, :failed]
27
+ transition :from => :failed, :to => [:attaching]
28
+
29
+ after_transition do |channel, transition|
30
+ channel.synchronize_state_with_statemachine
31
+ end
32
+
33
+ after_transition(to: [:attaching]) do |channel|
34
+ channel.manager.attach
35
+ end
36
+
37
+ before_transition(to: [:attached]) do |channel, current_transition|
38
+ channel.manager.sync current_transition.metadata
39
+ end
40
+
41
+ after_transition(to: [:detaching]) do |channel|
42
+ channel.manager.detach
43
+ end
44
+
45
+ after_transition(to: [:failed]) do |channel, current_transition|
46
+ channel.manager.failed current_transition.metadata
47
+ end
48
+
49
+ # Transitions responsible for updating channel#error_reason
50
+ before_transition(to: [:failed]) do |channel, current_transition|
51
+ channel.set_failed_channel_error_reason current_transition.metadata
52
+ end
53
+
54
+ before_transition(to: [:attached, :detached]) do |channel, current_transition|
55
+ channel.set_failed_channel_error_reason nil
56
+ end
57
+
58
+ private
59
+ def channel
60
+ object
61
+ end
62
+
63
+ def logger
64
+ channel.logger
65
+ end
66
+ end
67
+ end
68
+ end
@@ -2,8 +2,6 @@ module Ably
2
2
  module Realtime
3
3
  # Client for the Ably Realtime API
4
4
  #
5
- # @!attribute [r] auth
6
- # (see Ably::Rest::Client#auth)
7
5
  # @!attribute [r] client_id
8
6
  # (see Ably::Rest::Client#client_id)
9
7
  # @!attribute [r] auth_options
@@ -12,28 +10,50 @@ module Ably
12
10
  # (see Ably::Rest::Client#environment)
13
11
  # @!attribute [r] channels
14
12
  # @return [Aby::Realtime::Channels] The collection of {Ably::Realtime::Channel}s that have been created
15
- # @!attribute [r] rest_client
16
- # @return [Ably::Rest::Client] The {Ably::Rest::Client REST client} instantiated with the same credentials and configuration that is used for all REST operations such as authentication
17
- # @!attribute [r] echo_messages
18
- # @return [Boolean] If false, suppresses messages originating from this connection being echoed back on the same connection. Defaults to true
19
13
  # @!attribute [r] encoders
20
14
  # (see Ably::Rest::Client#encoders)
21
15
  # @!attribute [r] protocol
22
16
  # (see Ably::Rest::Client#protocol)
23
17
  # @!attribute [r] protocol_binary?
24
18
  # (see Ably::Rest::Client#protocol_binary?)
19
+ #
25
20
  class Client
26
21
  include Ably::Modules::AsyncWrapper
27
22
  extend Forwardable
28
23
 
29
24
  DOMAIN = 'realtime.ably.io'
30
25
 
31
- attr_reader :channels, :auth, :rest_client, :echo_messages
26
+ # The collection of {Ably::Realtime::Channel}s that have been created
27
+ # @return [Aby::Realtime::Channels]
28
+ attr_reader :channels
29
+
30
+ # (see Ably::Rest::Client#auth)
31
+ attr_reader :auth
32
+
33
+ # The {Ably::Rest::Client REST client} instantiated with the same credentials and configuration that is used for all REST operations such as authentication
34
+ # @return [Ably::Rest::Client]
35
+ attr_reader :rest_client
36
+
37
+ # When false the client suppresses messages originating from this connection being echoed back on the same connection. Defaults to true
38
+ # @return [Boolean]
39
+ attr_reader :echo_messages
40
+
41
+ # The custom realtime websocket host that is being used if it was provided with the option `:ws_host` when the {Client} was created
42
+ # @return [String,Nil]
43
+ attr_reader :custom_realtime_host
44
+
45
+ # When true, as soon as the client library is instantiated it will connect to Ably. If this attribute is false, a connection must be opened explicitly
46
+ # @return [Boolean]
47
+ attr_reader :connect_automatically
48
+
49
+ # 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
50
+ # @return [String,Nil]
51
+ attr_reader :recover
52
+
32
53
  def_delegators :auth, :client_id, :auth_options
33
54
  def_delegators :@rest_client, :encoders
34
- def_delegators :@rest_client, :environment, :use_tls?, :protocol, :protocol_binary?
55
+ def_delegators :@rest_client, :environment, :use_tls?, :protocol, :protocol_binary?, :custom_host
35
56
  def_delegators :@rest_client, :log_level
36
- def_delegators :@rest_client, :time, :stats
37
57
 
38
58
  # Creates a {Ably::Realtime::Client Realtime Client} and configures the {Ably::Auth} object for the connection.
39
59
  #
@@ -41,7 +61,8 @@ module Ably
41
61
  # @option options (see Ably::Rest::Client#initialize)
42
62
  # @option options [Boolean] :queue_messages If false, this disables the default behaviour whereby the library queues messages on a connection in the disconnected or connecting states
43
63
  # @option options [Boolean] :echo_messages If false, prevents messages originating from this connection being echoed back on the same connection
44
- # @option options [String] :recover This option allows a connection to inherit the state of a previous connection that may have existed under an different instance of the Realtime library.
64
+ # @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
65
+ # @option options [Boolean] :connect_automatically 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.
45
66
  #
46
67
  # @yield (see Ably::Rest::Client#initialize)
47
68
  # @yieldparam (see Ably::Rest::Client#initialize)
@@ -56,12 +77,16 @@ module Ably
56
77
  # # create a new client and configure a client ID used for presence
57
78
  # client = Ably::Realtime::Client.new(api_key: 'key.id:secret', client_id: 'john')
58
79
  #
59
- def initialize(options, &auth_block)
60
- @rest_client = Ably::Rest::Client.new(options, &auth_block)
61
- @auth = @rest_client.auth
62
- @channels = Ably::Realtime::Channels.new(self)
63
- @echo_messages = @rest_client.options.fetch(:echo_messages, true) == false ? false : true
64
- @custom_socket_host = @rest_client.options[:ws_host]
80
+ def initialize(options, &token_request_block)
81
+ @rest_client = Ably::Rest::Client.new(options, &token_request_block)
82
+ @auth = @rest_client.auth
83
+ @channels = Ably::Realtime::Channels.new(self)
84
+ @echo_messages = @rest_client.options.fetch(:echo_messages, true) == false ? false : true
85
+ @custom_realtime_host = @rest_client.options[:realtime_host] || @rest_client.options[:ws_host]
86
+ @connect_automatically = @rest_client.options.fetch(:connect_automatically, true) == false ? false : true
87
+ @recover = @rest_client.options[:recover]
88
+
89
+ raise ArgumentError, "Recovery key is invalid" if @recover && !@recover.match(Connection::RECOVER_REGEX)
65
90
  end
66
91
 
67
92
  # Return a {Ably::Realtime::Channel Realtime Channel} for the given name
@@ -103,10 +128,7 @@ module Ably
103
128
  # @!attribute [r] endpoint
104
129
  # @return [URI::Generic] Default Ably Realtime endpoint used for all requests
105
130
  def endpoint
106
- URI::Generic.build(
107
- scheme: use_tls? ? "wss" : "ws",
108
- host: custom_socket_host || [environment, DOMAIN].compact.join('-')
109
- )
131
+ endpoint_for_host(custom_realtime_host || [environment, DOMAIN].compact.join('-'))
110
132
  end
111
133
 
112
134
  # @!attribute [r] connection
@@ -115,12 +137,6 @@ module Ably
115
137
  @connection ||= Connection.new(self)
116
138
  end
117
139
 
118
- # @!attribute [r] custom_socket_host
119
- # @return [String,NilClass] Returns the custom socket host that is being used if it was provided with the option :ws_host when the {Client} was created
120
- def custom_socket_host
121
- @custom_socket_host
122
- end
123
-
124
140
  # (see Ably::Rest::Client#register_encoder)
125
141
  def register_encoder(encoder)
126
142
  rest_client.register_encoder encoder
@@ -130,6 +146,34 @@ module Ably
130
146
  def logger
131
147
  @logger ||= Ably::Logger.new(self, log_level, rest_client.logger.custom_logger)
132
148
  end
149
+
150
+ # Disable connection recovery, typically used after a connection has been recovered
151
+ # @return [void]
152
+ # @api private
153
+ def disable_automatic_connection_recovery
154
+ @recover = nil
155
+ end
156
+
157
+ # @!attribute [r] fallback_endpoint
158
+ # @return [URI::Generic] Fallback endpoint used to connect to the realtime Ably service. Note, after each connection attempt, a new random {Ably::FALLBACK_HOSTS fallback host} is used
159
+ # @api private
160
+ def fallback_endpoint
161
+ unless @fallback_endpoints
162
+ @fallback_endpoints = Ably::FALLBACK_HOSTS.shuffle.map { |fallback_host| endpoint_for_host(fallback_host) }
163
+ end
164
+
165
+ fallback_endpoint_index = connection.manager.retry_count_for_state(:disconnected) + connection.manager.retry_count_for_state(:suspended)
166
+
167
+ @fallback_endpoints[fallback_endpoint_index % @fallback_endpoints.count]
168
+ end
169
+
170
+ private
171
+ def endpoint_for_host(host)
172
+ URI::Generic.build(
173
+ scheme: use_tls? ? 'wss' : 'ws',
174
+ host: host
175
+ )
176
+ end
133
177
  end
134
178
  end
135
179
  end
@@ -41,7 +41,7 @@ module Ably::Realtime
41
41
  logger.debug "#{protocol_message.action} received: #{protocol_message}"
42
42
  end
43
43
 
44
- update_connection_id protocol_message
44
+ update_connection_recovery_info protocol_message
45
45
 
46
46
  case protocol_message.action
47
47
  when ACTION.Heartbeat
@@ -57,35 +57,44 @@ module Ably::Realtime
57
57
  connection.transition_state_machine :connected
58
58
 
59
59
  when ACTION.Disconnect, ACTION.Disconnected
60
+ connection.transition_state_machine :disconnected, protocol_message.error
60
61
 
61
62
  when ACTION.Close
62
63
  when ACTION.Closed
63
64
  connection.transition_state_machine :closed
64
65
 
65
66
  when ACTION.Error
66
- logger.error "Error received: #{protocol_message.error}"
67
67
  if protocol_message.channel && !protocol_message.has_message_serial?
68
68
  dispatch_channel_error protocol_message
69
69
  else
70
- connection.transition_state_machine :failed, protocol_message.error
70
+ process_connection_error protocol_message
71
71
  end
72
72
 
73
73
  when ACTION.Attach
74
74
  when ACTION.Attached
75
- get_channel(protocol_message.channel).change_state Ably::Realtime::Channel::STATE.Attached
75
+ get_channel(protocol_message.channel).transition_state_machine :attached, protocol_message
76
76
 
77
77
  when ACTION.Detach
78
78
  when ACTION.Detached
79
- get_channel(protocol_message.channel).change_state Ably::Realtime::Channel::STATE.Detached
79
+ get_channel(protocol_message.channel).transition_state_machine :detached
80
+
81
+ when ACTION.Sync
82
+ presence = get_channel(protocol_message.channel).presence
83
+ protocol_message.presence.each do |presence_message|
84
+ presence.__incoming_msgbus__.publish :sync, presence_message
85
+ end
86
+ presence.update_sync_serial protocol_message.channel_serial
80
87
 
81
88
  when ACTION.Presence
82
- protocol_message.presence.each do |presence|
83
- get_channel(protocol_message.channel).presence.__incoming_msgbus__.publish :presence, presence
89
+ presence = get_channel(protocol_message.channel).presence
90
+ protocol_message.presence.each do |presence_message|
91
+ presence.__incoming_msgbus__.publish :presence, presence_message
84
92
  end
85
93
 
86
94
  when ACTION.Message
95
+ channel = get_channel(protocol_message.channel)
87
96
  protocol_message.messages.each do |message|
88
- get_channel(protocol_message.channel).__incoming_msgbus__.publish :message, message
97
+ channel.__incoming_msgbus__.publish :message, message
89
98
  end
90
99
 
91
100
  else
@@ -94,17 +103,26 @@ module Ably::Realtime
94
103
  end
95
104
 
96
105
  def dispatch_channel_error(protocol_message)
106
+ logger.warn "Channel Error message received: #{protocol_message.error}"
97
107
  if !protocol_message.has_message_serial?
98
- get_channel(protocol_message.channel).change_state Ably::Realtime::Channel::STATE.Failed, protocol_message.error
108
+ get_channel(protocol_message.channel).transition_state_machine :failed, protocol_message.error
99
109
  else
100
110
  logger.fatal "Cannot process ProtocolMessage as not yet implemented: #{protocol_message}"
101
111
  end
102
112
  end
103
113
 
104
- def update_connection_id(protocol_message)
105
- if protocol_message.connection_id && (protocol_message.connection_id != connection.id)
106
- logger.debug "New connection ID set to #{protocol_message.connection_id}"
107
- connection.update_connection_id protocol_message.connection_id
114
+ def process_connection_error(protocol_message)
115
+ connection.manager.error_received_from_server protocol_message.error
116
+ end
117
+
118
+ def update_connection_recovery_info(protocol_message)
119
+ if protocol_message.connection_key && (protocol_message.connection_key != connection.key)
120
+ logger.debug "New connection ID set to #{protocol_message.connection_id} with connection key #{protocol_message.connection_key}"
121
+ connection.update_connection_id_and_key protocol_message.connection_id, protocol_message.connection_key
122
+ end
123
+
124
+ if protocol_message.has_connection_serial?
125
+ connection.update_connection_serial protocol_message.connection_serial
108
126
  end
109
127
  end
110
128
 
@@ -20,7 +20,7 @@ module Ably::Realtime
20
20
  attr_reader :client, :connection
21
21
 
22
22
  def can_send_messages?
23
- connection.connected?
23
+ connection.connected? || connection.closing?
24
24
  end
25
25
 
26
26
  def messages_in_outgoing_queue?
@@ -23,6 +23,8 @@ module Ably
23
23
  # Connection::STATE.Closed
24
24
  # Connection::STATE.Failed
25
25
  #
26
+ # Connection emit errors - use `on(:error)` to subscribe to errors
27
+ #
26
28
  # @example
27
29
  # client = Ably::Realtime::Client.new('key.id:secret')
28
30
  # client.connection.on(:connected) do
@@ -30,11 +32,8 @@ module Ably
30
32
  # end
31
33
  #
32
34
  # @!attribute [r] state
33
- # @return {Ably::Realtime::Connection::STATE} connection state
34
- # @!attribute [r] id
35
- # @return {String} the assigned connection ID
36
- # @!attribute [r] error_reason
37
- # @return {Ably::Models::ErrorInfo} error information associated with a connection failure
35
+ # @return [Ably::Realtime::Connection::STATE] connection state
36
+ #
38
37
  class Connection
39
38
  include Ably::Modules::EventEmitter
40
39
  include Ably::Modules::Conversions
@@ -47,37 +46,59 @@ module Ably
47
46
  :connected,
48
47
  :disconnected,
49
48
  :suspended,
49
+ :closing,
50
50
  :closed,
51
51
  :failed
52
52
  )
53
53
  include Ably::Modules::StateEmitter
54
+ include Ably::Modules::UsesStateMachine
54
55
 
55
- attr_reader :id, :error_reason, :client
56
+ # Expected format for a connection recover key
57
+ RECOVER_REGEX = /^(?<recover>[\w-]+):(?<connection_serial>\-?\w+)$/
58
+
59
+ # A unique public identifier for this connection, used to identify this member in presence events and messages
60
+ # @return [String]
61
+ attr_reader :id
62
+
63
+ # A unique private connection key used to recover this connection, assigned by Ably
64
+ # @return [String]
65
+ attr_reader :key
66
+
67
+ # The serial number of the last message to be received on this connection, used to recover or resume a connection
68
+ # @return [Integer]
69
+ attr_reader :serial
70
+
71
+ # When a connection failure occurs this attribute contains the Ably Exception
72
+ # @return [Ably::Models::ErrorInfo,Ably::Exceptions::BaseAblyException]
73
+ attr_reader :error_reason
74
+
75
+ # {Ably::Realtime::Client} associated with this connection
76
+ # @return [Ably::Realtime::Client]
77
+ attr_reader :client
56
78
 
57
- # @api private
58
79
  # Underlying socket transport used for this connection, for internal use by the client library
59
- # @return {Ably::Realtime::Connection::WebsocketTransport}
80
+ # @return [Ably::Realtime::Connection::WebsocketTransport]
81
+ # @api private
60
82
  attr_reader :transport
61
83
 
84
+ # The Connection manager responsible for creating, maintaining and closing the connection and underlying transport
85
+ # @return [Ably::Realtime::Connection::ConnectionManager]
62
86
  # @api private
63
- # The connection manager responsible for creating, maintaining and closing the connection and underlying transport
64
- # @return {Ably::Realtime::Connection::ConnectionManager}
65
87
  attr_reader :manager
66
88
 
67
- # @api private
68
89
  # An internal queue used to manage unsent outgoing messages. You should never interface with this array directly
69
90
  # @return [Array]
91
+ # @api private
70
92
  attr_reader :__outgoing_message_queue__
71
93
 
72
- # @api private
73
94
  # An internal queue used to manage sent messages. You should never interface with this array directly
74
95
  # @return [Array]
96
+ # @api private
75
97
  attr_reader :__pending_message_queue__
76
98
 
77
99
  # @api public
78
100
  def initialize(client)
79
101
  @client = client
80
-
81
102
  @serial = -1
82
103
  @__outgoing_message_queue__ = []
83
104
  @__pending_message_queue__ = []
@@ -85,13 +106,9 @@ module Ably
85
106
  Client::IncomingMessageDispatcher.new client, self
86
107
  Client::OutgoingMessageDispatcher.new client, self
87
108
 
88
- EventMachine.next_tick do
89
- trigger STATE.Initialized
90
- end
91
-
92
- @state_machine = ConnectionStateMachine.new(self)
93
- @manager = ConnectionManager.new(self)
94
- @state = STATE(state_machine.current_state)
109
+ @state_machine = ConnectionStateMachine.new(self)
110
+ @state = STATE(state_machine.current_state)
111
+ @manager = ConnectionManager.new(self)
95
112
  end
96
113
 
97
114
  # Causes the connection to close, entering the closed state, from any state except
@@ -100,31 +117,29 @@ module Ably
100
117
  #
101
118
  # @yield [Ably::Realtime::Connection] block is called as soon as this connection is in the Closed state
102
119
  #
103
- # @return [void]
104
- def close(&block)
105
- if closed?
106
- block.call self
107
- else
108
- EventMachine.next_tick do
109
- transition_state_machine(:closed)
110
- end
111
- once(STATE.Closed) { block.call self } if block_given?
120
+ # @return [EventMachine::Deferrable]
121
+ #
122
+ def close(&success_block)
123
+ unless closing? || closed?
124
+ raise exception_for_state_change_to(:closing) unless can_transition_to?(:closing)
125
+ transition_state_machine :closing
112
126
  end
127
+ deferrable_for_state_change_to(STATE.Closed, &success_block)
113
128
  end
114
129
 
115
- # Causes the library to re-attempt connection, if it was previously explicitly
116
- # closed by the user, or was closed as a result of an unrecoverable error.
130
+ # Causes the library to attempt connection. If it was previously explicitly
131
+ # closed by the user, or was closed as a result of an unrecoverable error, a new connection will be opened.
117
132
  #
118
133
  # @yield [Ably::Realtime::Connection] block is called as soon as this connection is in the Connected state
119
134
  #
120
- # @return [void]
121
- def connect(&block)
122
- if connected?
123
- block.call self
124
- else
125
- transition_state_machine(:connecting) unless connecting?
126
- once(STATE.Connected) { block.call self } if block_given?
135
+ # @return [EventMachine::Deferrable]
136
+ #
137
+ def connect(&success_block)
138
+ unless connecting? || connected?
139
+ raise exception_for_state_change_to(:connecting) unless can_transition_to?(:connecting)
140
+ transition_state_machine :connecting
127
141
  end
142
+ deferrable_for_state_change_to(STATE.Connected, &success_block)
128
143
  end
129
144
 
130
145
  # Sends a ping to Ably and yields the provided block when a heartbeat ping request is echoed from the server.
@@ -139,7 +154,10 @@ module Ably
139
154
  # puts "Ping took #{ms_elapsed}ms"
140
155
  # end
141
156
  #
157
+ # @return [void]
158
+ #
142
159
  def ping(&block)
160
+ raise RuntimeError, 'Cannot send a ping when connection is not open' if initialized?
143
161
  raise RuntimeError, 'Cannot send a ping when connection is in a closed or failed state' if closed? || failed?
144
162
 
145
163
  started = nil
@@ -152,42 +170,40 @@ module Ably
152
170
  end
153
171
  end
154
172
 
155
- once(STATE.Connected) do
173
+ once_or_if(STATE.Connected) do
156
174
  started = Time.now
157
175
  send_protocol_message action: Ably::Models::ProtocolMessage::ACTION.Heartbeat.to_i
158
176
  __incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping
159
177
  end
160
178
  end
161
179
 
162
- # Reconfigure the current connection ID
163
- # @return [void]
164
- # @api private
165
- def update_connection_id(connection_id)
166
- @id = connection_id
180
+ # @!attribute [r] recovery_key
181
+ # @return [String] recovery key that can be used by another client to recover this connection with the :recover option
182
+ def recovery_key
183
+ "#{key}:#{serial}" if connection_resumable?
167
184
  end
168
185
 
169
- # Call #transition_to on {Ably::Realtime::Connection::ConnectionStateMachine}
170
- #
171
- # @return [Boolean] true if new_state can be transitioned to by state machine
186
+ # Configure the current connection ID and connection key
187
+ # @return [void]
172
188
  # @api private
173
- def transition_state_machine(new_state, emit_object = nil)
174
- state_machine.transition_to(new_state, emit_object)
189
+ def update_connection_id_and_key(connection_id, connection_key)
190
+ @id = connection_id
191
+ @key = connection_key
175
192
  end
176
193
 
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
- #
194
+ # Store last received connection serial so that the connection can be resumed from the last known point-in-time
180
195
  # @return [void]
181
196
  # @api private
182
- def transition_state_machine!(new_state, emit_object = nil)
183
- state_machine.transition_to!(new_state, emit_object)
197
+ def update_connection_serial(connection_serial)
198
+ @serial = connection_serial
184
199
  end
185
200
 
186
- # Provides an internal method for the {Ably::Realtime::Connection} state to match the {Ably::Realtime::Connection::ConnectionStateMachine}'s state
201
+ # Disable automatic resume of a connection
202
+ # @return [void]
187
203
  # @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
204
+ def reset_resume_info
205
+ @key = nil
206
+ @serial = nil
191
207
  end
192
208
 
193
209
  # @!attribute [r] __outgoing_protocol_msgbus__
@@ -205,9 +221,13 @@ module Ably
205
221
  end
206
222
 
207
223
  # @!attribute [r] host
208
- # @return [String] The default host name used for this connection
224
+ # @return [String] The host name used for this connection, for network connection failures a {Ably::FALLBACK_HOSTS fallback host} is used to route around networking or intermittent problems
209
225
  def host
210
- client.endpoint.host
226
+ if can_use_fallback_hosts?
227
+ client.fallback_endpoint.host
228
+ else
229
+ client.endpoint.host
230
+ end
211
231
  end
212
232
 
213
233
  # @!attribute [r] port
@@ -249,37 +269,28 @@ module Ably
249
269
  __outgoing_protocol_msgbus__.publish :protocol_message, protocol_message
250
270
  end
251
271
 
252
- # @!attribute [r] previous_state
253
- # @return [Ably::Realtime::Connection::STATE,nil] The previous state for this connection
254
- # @api private
255
- def previous_state
256
- if state_machine.previous_state
257
- STATE(state_machine.previous_state)
258
- end
259
- end
260
-
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
- }
271
- end
272
- end
273
-
274
272
  # @api private
275
273
  def create_websocket_transport(&block)
276
274
  operation = proc do
277
275
  URI(client.endpoint).tap do |endpoint|
278
- endpoint.query = URI.encode_www_form(client.auth.auth_params.merge(
276
+ url_params = client.auth.auth_params.merge(
279
277
  timestamp: as_since_epoch(Time.now),
280
278
  format: client.protocol,
281
279
  echo: client.echo_messages
282
- ))
280
+ )
281
+
282
+ if connection_resumable?
283
+ url_params.merge! resume: key, connection_serial: serial
284
+ logger.debug "Resuming connection key #{key} with serial #{serial}"
285
+ elsif connection_recoverable?
286
+ url_params.merge! recover: connection_recover_parts[:recover], connection_serial: connection_recover_parts[:connection_serial]
287
+ logger.debug "Recovering connection with key #{client.recover}"
288
+ once(:connected, :closed, :failed) do
289
+ client.disable_automatic_connection_recovery
290
+ end
291
+ end
292
+
293
+ endpoint.query = URI.encode_www_form(url_params)
283
294
  end.to_s
284
295
  end
285
296
 
@@ -289,7 +300,7 @@ module Ably
289
300
  yield websocket_transport if block_given?
290
301
  end
291
302
  rescue EventMachine::ConnectionError => error
292
- manager.connection_failed error
303
+ manager.connection_opening_failed error
293
304
  end
294
305
  end
295
306
 
@@ -302,13 +313,16 @@ module Ably
302
313
  @transport = nil
303
314
  end
304
315
 
316
+ # @api private
317
+ def set_failed_connection_error_reason(error)
318
+ @error_reason = error
319
+ end
320
+
305
321
  # As we are using a state machine, do not allow change_state to be used
306
322
  # #transition_state_machine must be used instead
307
323
  private :change_state
308
324
 
309
325
  private
310
- attr_reader :serial, :state_machine
311
-
312
326
  def create_pub_sub_message_bus
313
327
  Ably::Util::PubSub.new(
314
328
  coerce_into: Proc.new do |event|
@@ -335,13 +349,42 @@ module Ably
335
349
  raise e
336
350
  end
337
351
 
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}"
352
+ # Simply wait until the next EventMachine tick to ensure Connection initialization is complete
353
+ def when_initialized(&block)
354
+ EventMachine.next_tick { yield }
355
+ end
356
+
357
+ def connection_resumable?
358
+ !key.nil? && !serial.nil?
359
+ end
360
+
361
+ def connection_recoverable?
362
+ connection_recover_parts
363
+ end
364
+
365
+ def connection_recover_parts
366
+ client.recover.to_s.match(RECOVER_REGEX)
367
+ end
368
+
369
+ def can_use_fallback_hosts?
370
+ if client.environment.nil? && client.custom_realtime_host.nil?
371
+ if connecting? && previous_state
372
+ use_fallback_if_disconnected? || use_fallback_if_suspended?
373
+ end
343
374
  end
344
375
  end
376
+
377
+ def use_fallback_if_disconnected?
378
+ second_reconnect_attempt_for(:disconnected, 1)
379
+ end
380
+
381
+ def use_fallback_if_suspended?
382
+ second_reconnect_attempt_for(:suspended, 2) # on first suspended state use default Ably host again
383
+ end
384
+
385
+ def second_reconnect_attempt_for(state, first_attempt_count)
386
+ previous_state == state && manager.retry_count_for_state(state) >= first_attempt_count
387
+ end
345
388
  end
346
389
  end
347
390
  end