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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.ruby-version.old +1 -0
- data/.travis.yml +0 -2
- data/Rakefile +22 -4
- data/SPEC.md +1676 -0
- data/ably.gemspec +1 -1
- data/lib/ably.rb +0 -8
- data/lib/ably/auth.rb +54 -46
- data/lib/ably/exceptions.rb +19 -5
- data/lib/ably/logger.rb +1 -1
- data/lib/ably/models/error_info.rb +1 -1
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
- data/lib/ably/models/message.rb +15 -12
- data/lib/ably/models/message_encoders/base.rb +6 -5
- data/lib/ably/models/message_encoders/base64.rb +1 -0
- data/lib/ably/models/message_encoders/cipher.rb +6 -3
- data/lib/ably/models/message_encoders/json.rb +1 -0
- data/lib/ably/models/message_encoders/utf8.rb +2 -9
- data/lib/ably/models/nil_logger.rb +20 -0
- data/lib/ably/models/paginated_resource.rb +5 -2
- data/lib/ably/models/presence_message.rb +21 -12
- data/lib/ably/models/protocol_message.rb +22 -6
- data/lib/ably/modules/ably.rb +11 -0
- data/lib/ably/modules/async_wrapper.rb +2 -0
- data/lib/ably/modules/conversions.rb +23 -3
- data/lib/ably/modules/encodeable.rb +2 -1
- data/lib/ably/modules/enum.rb +2 -0
- data/lib/ably/modules/event_emitter.rb +7 -1
- data/lib/ably/modules/event_machine_helpers.rb +2 -0
- data/lib/ably/modules/http_helpers.rb +2 -0
- data/lib/ably/modules/model_common.rb +12 -2
- data/lib/ably/modules/state_emitter.rb +76 -0
- data/lib/ably/modules/state_machine.rb +53 -0
- data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
- data/lib/ably/modules/uses_state_machine.rb +74 -0
- data/lib/ably/realtime.rb +4 -2
- data/lib/ably/realtime/channel.rb +51 -58
- data/lib/ably/realtime/channel/channel_manager.rb +91 -0
- data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
- data/lib/ably/realtime/client.rb +70 -26
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
- data/lib/ably/realtime/connection.rb +135 -92
- data/lib/ably/realtime/connection/connection_manager.rb +216 -33
- data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
- data/lib/ably/realtime/models/nil_channel.rb +10 -1
- data/lib/ably/realtime/presence.rb +336 -92
- data/lib/ably/rest.rb +2 -2
- data/lib/ably/rest/channel.rb +13 -4
- data/lib/ably/rest/client.rb +138 -38
- data/lib/ably/rest/middleware/logger.rb +24 -3
- data/lib/ably/rest/presence.rb +12 -7
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
- data/spec/acceptance/realtime/channel_spec.rb +461 -120
- data/spec/acceptance/realtime/client_spec.rb +119 -0
- data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
- data/spec/acceptance/realtime/connection_spec.rb +571 -97
- data/spec/acceptance/realtime/message_spec.rb +347 -333
- data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
- data/spec/acceptance/realtime/presence_spec.rb +769 -239
- data/spec/acceptance/realtime/stats_spec.rb +14 -22
- data/spec/acceptance/realtime/time_spec.rb +16 -20
- data/spec/acceptance/rest/auth_spec.rb +425 -364
- data/spec/acceptance/rest/base_spec.rb +108 -176
- data/spec/acceptance/rest/channel_spec.rb +89 -89
- data/spec/acceptance/rest/channels_spec.rb +30 -32
- data/spec/acceptance/rest/client_spec.rb +273 -0
- data/spec/acceptance/rest/encoders_spec.rb +185 -0
- data/spec/acceptance/rest/message_spec.rb +186 -163
- data/spec/acceptance/rest/presence_spec.rb +150 -111
- data/spec/acceptance/rest/stats_spec.rb +45 -40
- data/spec/acceptance/rest/time_spec.rb +8 -10
- data/spec/rspec_config.rb +10 -1
- data/spec/shared/client_initializer_behaviour.rb +212 -0
- data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
- data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/support/api_helper.rb +11 -0
- data/spec/support/event_machine_helper.rb +101 -3
- data/spec/support/markdown_spec_formatter.rb +90 -0
- data/spec/support/private_api_formatter.rb +36 -0
- data/spec/support/protocol_helper.rb +32 -0
- data/spec/support/random_helper.rb +15 -0
- data/spec/support/test_app.rb +4 -0
- data/spec/unit/auth_spec.rb +68 -0
- data/spec/unit/logger_spec.rb +77 -66
- data/spec/unit/models/error_info_spec.rb +1 -1
- data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
- data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
- data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
- data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
- data/spec/unit/models/message_spec.rb +160 -15
- data/spec/unit/models/paginated_resource_spec.rb +29 -27
- data/spec/unit/models/presence_message_spec.rb +163 -20
- data/spec/unit/models/protocol_message_spec.rb +43 -8
- data/spec/unit/modules/async_wrapper_spec.rb +2 -3
- data/spec/unit/modules/conversions_spec.rb +1 -1
- data/spec/unit/modules/enum_spec.rb +2 -3
- data/spec/unit/modules/event_emitter_spec.rb +62 -5
- data/spec/unit/modules/state_emitter_spec.rb +283 -0
- data/spec/unit/realtime/channel_spec.rb +107 -2
- data/spec/unit/realtime/channels_spec.rb +1 -0
- data/spec/unit/realtime/client_spec.rb +8 -48
- data/spec/unit/realtime/connection_spec.rb +3 -3
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
- data/spec/unit/realtime/presence_spec.rb +13 -4
- data/spec/unit/realtime/realtime_spec.rb +0 -11
- data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
- data/spec/unit/rest/channel_spec.rb +109 -0
- data/spec/unit/rest/channels_spec.rb +4 -3
- data/spec/unit/rest/client_spec.rb +30 -125
- data/spec/unit/rest/rest_spec.rb +10 -0
- data/spec/unit/util/crypto_spec.rb +10 -5
- data/spec/unit/util/pub_sub_spec.rb +5 -5
- metadata +44 -12
- data/spec/integration/modules/state_emitter_spec.rb +0 -80
- 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
|
data/lib/ably/realtime/client.rb
CHANGED
@@ -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
|
-
|
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
|
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, &
|
60
|
-
@rest_client
|
61
|
-
@auth
|
62
|
-
@channels
|
63
|
-
@echo_messages
|
64
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
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).
|
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).
|
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.
|
83
|
-
|
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
|
-
|
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).
|
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
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
|
@@ -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
|
34
|
-
#
|
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
|
-
|
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
|
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
|
-
|
89
|
-
|
90
|
-
|
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 [
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
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 [
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
transition_state_machine
|
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
|
-
|
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
|
-
#
|
163
|
-
#
|
164
|
-
|
165
|
-
|
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
|
-
#
|
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
|
174
|
-
|
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
|
-
#
|
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
|
183
|
-
|
197
|
+
def update_connection_serial(connection_serial)
|
198
|
+
@serial = connection_serial
|
184
199
|
end
|
185
200
|
|
186
|
-
#
|
201
|
+
# Disable automatic resume of a connection
|
202
|
+
# @return [void]
|
187
203
|
# @api private
|
188
|
-
def
|
189
|
-
|
190
|
-
|
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
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
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
|