ably 0.6.2 → 0.7.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 (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