ably-pubsub-core 0.0.1.pre
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 +7 -0
- data/lib/ably/agent.rb +8 -0
- data/lib/ably/auth.rb +791 -0
- data/lib/ably/exceptions.rb +169 -0
- data/lib/ably/logger.rb +119 -0
- data/lib/ably/models/auth_details.rb +45 -0
- data/lib/ably/models/channel_details.rb +69 -0
- data/lib/ably/models/channel_metrics.rb +107 -0
- data/lib/ably/models/channel_occupancy.rb +50 -0
- data/lib/ably/models/channel_options.rb +120 -0
- data/lib/ably/models/channel_state_change.rb +84 -0
- data/lib/ably/models/channel_status.rb +63 -0
- data/lib/ably/models/cipher_params.rb +137 -0
- data/lib/ably/models/connection_details.rb +149 -0
- data/lib/ably/models/connection_state_change.rb +83 -0
- data/lib/ably/models/delta_extras.rb +28 -0
- data/lib/ably/models/device_details.rb +126 -0
- data/lib/ably/models/device_push_details.rb +94 -0
- data/lib/ably/models/error_info.rb +104 -0
- data/lib/ably/models/http_paginated_response.rb +112 -0
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
- data/lib/ably/models/message.rb +223 -0
- data/lib/ably/models/message_encoders/base.rb +135 -0
- data/lib/ably/models/message_encoders/base64.rb +40 -0
- data/lib/ably/models/message_encoders/cipher.rb +84 -0
- data/lib/ably/models/message_encoders/json.rb +34 -0
- data/lib/ably/models/message_encoders/utf8.rb +26 -0
- data/lib/ably/models/nil_logger.rb +20 -0
- data/lib/ably/models/paginated_result.rb +201 -0
- data/lib/ably/models/presence_message.rb +244 -0
- data/lib/ably/models/protocol_message.rb +288 -0
- data/lib/ably/models/push_channel_subscription.rb +114 -0
- data/lib/ably/models/stats.rb +242 -0
- data/lib/ably/models/stats_types.rb +107 -0
- data/lib/ably/models/token_details.rb +148 -0
- data/lib/ably/models/token_request.rb +180 -0
- data/lib/ably/modules/ably.rb +26 -0
- data/lib/ably/modules/async_wrapper.rb +69 -0
- data/lib/ably/modules/channels_collection.rb +105 -0
- data/lib/ably/modules/conversions.rb +153 -0
- data/lib/ably/modules/encodeable.rb +111 -0
- data/lib/ably/modules/enum.rb +230 -0
- data/lib/ably/modules/event_emitter.rb +200 -0
- data/lib/ably/modules/event_machine_helpers.rb +26 -0
- data/lib/ably/modules/exception_codes.rb +128 -0
- data/lib/ably/modules/http_helpers.rb +41 -0
- data/lib/ably/modules/message_emitter.rb +78 -0
- data/lib/ably/modules/message_pack.rb +14 -0
- data/lib/ably/modules/model_common.rb +84 -0
- data/lib/ably/modules/safe_deferrable.rb +83 -0
- data/lib/ably/modules/safe_yield.rb +41 -0
- data/lib/ably/modules/state_emitter.rb +181 -0
- data/lib/ably/modules/state_machine.rb +59 -0
- data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
- data/lib/ably/modules/uses_state_machine.rb +111 -0
- data/lib/ably/realtime/auth.rb +283 -0
- data/lib/ably/realtime/channel/channel_manager.rb +272 -0
- data/lib/ably/realtime/channel/channel_properties.rb +40 -0
- data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
- data/lib/ably/realtime/channel/publisher.rb +82 -0
- data/lib/ably/realtime/channel/push_channel.rb +69 -0
- data/lib/ably/realtime/channel.rb +440 -0
- data/lib/ably/realtime/channels.rb +71 -0
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
- data/lib/ably/realtime/client.rb +363 -0
- data/lib/ably/realtime/connection/connection_manager.rb +594 -0
- data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
- data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
- data/lib/ably/realtime/connection.rb +738 -0
- data/lib/ably/realtime/models/nil_channel.rb +30 -0
- data/lib/ably/realtime/presence/members_map.rb +395 -0
- data/lib/ably/realtime/presence/presence_manager.rb +78 -0
- data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
- data/lib/ably/realtime/presence.rb +502 -0
- data/lib/ably/realtime/push/admin.rb +75 -0
- data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
- data/lib/ably/realtime/push/device_registrations.rb +105 -0
- data/lib/ably/realtime/push.rb +25 -0
- data/lib/ably/realtime/recovery_key_context.rb +36 -0
- data/lib/ably/realtime.rb +60 -0
- data/lib/ably/rest/channel/push_channel.rb +59 -0
- data/lib/ably/rest/channel.rb +194 -0
- data/lib/ably/rest/channels.rb +47 -0
- data/lib/ably/rest/client.rb +727 -0
- data/lib/ably/rest/middleware/encoder.rb +49 -0
- data/lib/ably/rest/middleware/exceptions.rb +53 -0
- data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
- data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
- data/lib/ably/rest/middleware/logger.rb +58 -0
- data/lib/ably/rest/middleware/parse_json.rb +27 -0
- data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
- data/lib/ably/rest/presence.rb +113 -0
- data/lib/ably/rest/push/admin.rb +58 -0
- data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
- data/lib/ably/rest/push/device_registrations.rb +114 -0
- data/lib/ably/rest/push.rb +25 -0
- data/lib/ably/rest.rb +40 -0
- data/lib/ably/util/ably_extensions.rb +29 -0
- data/lib/ably/util/crypto.rb +134 -0
- data/lib/ably/util/pub_sub.rb +45 -0
- data/lib/ably/util/safe_deferrable.rb +44 -0
- data/lib/ably/version.rb +7 -0
- data/lib/ably.rb +16 -0
- metadata +497 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
3
|
+
module Ably
|
|
4
|
+
module Realtime
|
|
5
|
+
# Enables the management of a connection to Ably.
|
|
6
|
+
#
|
|
7
|
+
class Connection
|
|
8
|
+
include Ably::Modules::EventEmitter
|
|
9
|
+
include Ably::Modules::Conversions
|
|
10
|
+
include Ably::Modules::SafeYield
|
|
11
|
+
extend Ably::Modules::Enum
|
|
12
|
+
using Ably::Util::AblyExtensions
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# The current {Ably::Realtime::Connection::STATE} of the connection.
|
|
16
|
+
# Describes the realtime [Connection]{@link Connection} object states.
|
|
17
|
+
#
|
|
18
|
+
# @spec RTN4d
|
|
19
|
+
#
|
|
20
|
+
# INITIALIZED A connection with this state has been initialized but no connection has yet been attempted.
|
|
21
|
+
# CONNECTING A connection attempt has been initiated. The connecting state is entered as soon as the library
|
|
22
|
+
# has completed initialization, and is reentered each time connection is re-attempted following disconnection.
|
|
23
|
+
# CONNECTED A connection exists and is active.
|
|
24
|
+
# DISCONNECTED A temporary failure condition. No current connection exists because there is no network connectivity
|
|
25
|
+
# or no host is available. The disconnected state is entered if an established connection is dropped,
|
|
26
|
+
# or if a connection attempt was unsuccessful. In the disconnected state the library will periodically
|
|
27
|
+
# attempt to open a new connection (approximately every 15 seconds), anticipating that the connection
|
|
28
|
+
# will be re-established soon and thus connection and channel continuity will be possible.
|
|
29
|
+
# In this state, developers can continue to publish messages as they are automatically placed
|
|
30
|
+
# in a local queue, to be sent as soon as a connection is reestablished. Messages published by
|
|
31
|
+
# other clients while this client is disconnected will be delivered to it upon reconnection,
|
|
32
|
+
# so long as the connection was resumed within 2 minutes. After 2 minutes have elapsed, recovery
|
|
33
|
+
# is no longer possible and the connection will move to the SUSPENDED state.
|
|
34
|
+
# SUSPENDED A long term failure condition. No current connection exists because there is no network connectivity
|
|
35
|
+
# or no host is available. The suspended state is entered after a failed connection attempt if
|
|
36
|
+
# there has then been no connection for a period of two minutes. In the suspended state, the library
|
|
37
|
+
# will periodically attempt to open a new connection every 30 seconds. Developers are unable to
|
|
38
|
+
# publish messages in this state. A new connection attempt can also be triggered by an explicit
|
|
39
|
+
# call to {Ably::Realtime::Connection#connect}. Once the connection has been re-established,
|
|
40
|
+
# channels will be automatically re-attached. The client has been disconnected for too long for them
|
|
41
|
+
# to resume from where they left off, so if it wants to catch up on messages published by other clients
|
|
42
|
+
# while it was disconnected, it needs to use the History API.
|
|
43
|
+
# CLOSING An explicit request by the developer to close the connection has been sent to the Ably service.
|
|
44
|
+
# If a reply is not received from Ably within a short period of time, the connection is forcibly
|
|
45
|
+
# terminated and the connection state becomes CLOSED.
|
|
46
|
+
# CLOSED The connection has been explicitly closed by the client. In the closed state, no reconnection attempts
|
|
47
|
+
# are made automatically by the library, and clients may not publish messages. No connection state is
|
|
48
|
+
# preserved by the service or by the library. A new connection attempt can be triggered by an explicit
|
|
49
|
+
# call to {Ably::Realtime::Connection#connect}, which results in a new connection.
|
|
50
|
+
# FAILED This state is entered if the client library encounters a failure condition that it cannot recover from.
|
|
51
|
+
# This may be a fatal connection error received from the Ably service, for example an attempt to connect
|
|
52
|
+
# with an incorrect API key, or a local terminal error, for example the token in use has expired
|
|
53
|
+
# and the library does not have any way to renew it. In the failed state, no reconnection attempts
|
|
54
|
+
# are made automatically by the library, and clients may not publish messages. A new connection attempt
|
|
55
|
+
# can be triggered by an explicit call to {Ably::Realtime::Connection#connect}.
|
|
56
|
+
#
|
|
57
|
+
# @return [Ably::Realtime::Connection::STATE]
|
|
58
|
+
#
|
|
59
|
+
STATE = ruby_enum('STATE',
|
|
60
|
+
:initialized,
|
|
61
|
+
:connecting,
|
|
62
|
+
:connected,
|
|
63
|
+
:disconnected,
|
|
64
|
+
:suspended,
|
|
65
|
+
:closing,
|
|
66
|
+
:closed,
|
|
67
|
+
:failed
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Describes the events emitted by a {Ably::Realtime::Connection} object. An event is either an UPDATE or a {Ably::Realtime::Connection::STATE}.
|
|
71
|
+
#
|
|
72
|
+
# UPDATE RTN4h An event for changes to connection conditions for which the {Ably::Realtime::Connection::STATE} does not change.
|
|
73
|
+
#
|
|
74
|
+
EVENT = ruby_enum('EVENT',
|
|
75
|
+
STATE.to_sym_arr + [:update]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
include Ably::Modules::StateEmitter
|
|
79
|
+
include Ably::Modules::UsesStateMachine
|
|
80
|
+
ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
|
|
81
|
+
|
|
82
|
+
# Defaults for automatic connection recovery and timeouts
|
|
83
|
+
DEFAULTS = {
|
|
84
|
+
channel_retry_timeout: 15, # when a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED
|
|
85
|
+
disconnected_retry_timeout: 15, # when the connection enters the DISCONNECTED state, after this delay in milliseconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
|
|
86
|
+
suspended_retry_timeout: 30, # when the connection enters the SUSPENDED state, after this delay in milliseconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically
|
|
87
|
+
connection_state_ttl: 120, # the duration that Ably will persist the connection state when a Realtime client is abruptly disconnected
|
|
88
|
+
max_connection_state_ttl: nil, # allow a max TTL to be passed in, usually for CI test purposes thus overiding any connection_state_ttl sent from Ably
|
|
89
|
+
realtime_request_timeout: 10, # default timeout when establishing a connection, or sending a HEARTBEAT, CONNECT, ATTACH, DETACH or CLOSE ProtocolMessage
|
|
90
|
+
websocket_heartbeats_disabled: false,
|
|
91
|
+
}.freeze
|
|
92
|
+
|
|
93
|
+
# Max number of messages to bundle in a single ProtocolMessage
|
|
94
|
+
MAX_PROTOCOL_MESSAGE_BATCH_SIZE = 50
|
|
95
|
+
|
|
96
|
+
# A unique public identifier for this connection, used to identify this member.
|
|
97
|
+
#
|
|
98
|
+
# @spec RTN8
|
|
99
|
+
#
|
|
100
|
+
# @return [String]
|
|
101
|
+
#
|
|
102
|
+
attr_reader :id
|
|
103
|
+
|
|
104
|
+
# A unique private connection key used to recover or resume a connection, assigned by Ably.
|
|
105
|
+
# When recovering a connection explicitly, the recoveryKey is used in the recover client options as it contains
|
|
106
|
+
# both the key and the last message serial. This private connection key can also be used by other REST clients
|
|
107
|
+
# to publish on behalf of this client. See the publishing over REST on behalf of a realtime client docs for more info.
|
|
108
|
+
#
|
|
109
|
+
# @spec RTN9
|
|
110
|
+
#
|
|
111
|
+
# @return [String]
|
|
112
|
+
#
|
|
113
|
+
attr_reader :key
|
|
114
|
+
|
|
115
|
+
# An {Ably::Models::ErrorInfo} object describing the last error received if a connection failure occurs.
|
|
116
|
+
#
|
|
117
|
+
# @spec RTN14a
|
|
118
|
+
#
|
|
119
|
+
# @return [Ably::Models::ErrorInfo,Ably::Exceptions::BaseAblyException]
|
|
120
|
+
#
|
|
121
|
+
attr_reader :error_reason
|
|
122
|
+
|
|
123
|
+
# Connection details of the currently established connection
|
|
124
|
+
# @return [Ably::Models::ConnectionDetails]
|
|
125
|
+
attr_reader :details
|
|
126
|
+
|
|
127
|
+
# {Ably::Realtime::Client} associated with this connection
|
|
128
|
+
# @return [Ably::Realtime::Client]
|
|
129
|
+
attr_reader :client
|
|
130
|
+
|
|
131
|
+
# Underlying socket transport used for this connection, for internal use by the client library
|
|
132
|
+
# @return [Ably::Realtime::Connection::WebsocketTransport]
|
|
133
|
+
# @api private
|
|
134
|
+
attr_reader :transport
|
|
135
|
+
|
|
136
|
+
# The Connection manager responsible for creating, maintaining and closing the connection and underlying transport
|
|
137
|
+
# @return [Ably::Realtime::Connection::ConnectionManager]
|
|
138
|
+
# @api private
|
|
139
|
+
attr_reader :manager
|
|
140
|
+
|
|
141
|
+
# An internal queue used to manage unsent outgoing messages. You should never interface with this array directly
|
|
142
|
+
# @return [Array]
|
|
143
|
+
# @api private
|
|
144
|
+
attr_reader :__outgoing_message_queue__
|
|
145
|
+
|
|
146
|
+
# An internal queue used to manage sent messages. You should never interface with this array directly
|
|
147
|
+
# @return [Array]
|
|
148
|
+
# @api private
|
|
149
|
+
attr_reader :__pending_message_ack_queue__
|
|
150
|
+
|
|
151
|
+
# Configured recovery and timeout defaults for this {Connection}.
|
|
152
|
+
# See the configurable options in {Ably::Realtime::Client#initialize}.
|
|
153
|
+
# The defaults are immutable
|
|
154
|
+
# @return [Hash]
|
|
155
|
+
attr_reader :defaults
|
|
156
|
+
|
|
157
|
+
# @api public
|
|
158
|
+
def initialize(client, options)
|
|
159
|
+
@client = client
|
|
160
|
+
@__outgoing_message_queue__ = []
|
|
161
|
+
@__pending_message_ack_queue__ = []
|
|
162
|
+
|
|
163
|
+
@defaults = DEFAULTS.dup
|
|
164
|
+
options.each do |key, val|
|
|
165
|
+
@defaults[key] = val if DEFAULTS.has_key?(key)
|
|
166
|
+
end if options.kind_of?(Hash)
|
|
167
|
+
@defaults.freeze
|
|
168
|
+
|
|
169
|
+
Client::IncomingMessageDispatcher.new client, self
|
|
170
|
+
Client::OutgoingMessageDispatcher.new client, self
|
|
171
|
+
|
|
172
|
+
@state_machine = ConnectionStateMachine.new(self)
|
|
173
|
+
@state = STATE(state_machine.current_state)
|
|
174
|
+
@manager = ConnectionManager.new(self)
|
|
175
|
+
|
|
176
|
+
@current_host = client.endpoint.host
|
|
177
|
+
|
|
178
|
+
reset_client_msg_serial
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Causes the connection to close, entering the {Ably::Realtime::Connection::STATE} CLOSING state.
|
|
182
|
+
# Once closed, the library does not attempt to re-establish the connection without an explicit call to
|
|
183
|
+
# {Ably::Realtime::Connection#connect}.
|
|
184
|
+
#
|
|
185
|
+
# @spec RTN12
|
|
186
|
+
#
|
|
187
|
+
# @yield block is called as soon as this connection is in the Closed state
|
|
188
|
+
#
|
|
189
|
+
# @return [EventMachine::Deferrable]
|
|
190
|
+
#
|
|
191
|
+
def close(&success_block)
|
|
192
|
+
unless closing? || closed?
|
|
193
|
+
unless can_transition_to?(:closing)
|
|
194
|
+
return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:closing))
|
|
195
|
+
end
|
|
196
|
+
transition_state_machine :closing
|
|
197
|
+
end
|
|
198
|
+
deferrable_for_state_change_to(STATE.Closed, &success_block)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Explicitly calling connect() is unnecessary unless the autoConnect attribute of
|
|
202
|
+
# the ClientOptions object is false. Unless already connected or connecting,
|
|
203
|
+
# this method causes the connection to open, entering the {Ably::Realtime::Connection::STATE} CONNECTING state.
|
|
204
|
+
#
|
|
205
|
+
# @spec RTC1b, RTN3, RTN11
|
|
206
|
+
#
|
|
207
|
+
# @yield block is called as soon as this connection is in the Connected state
|
|
208
|
+
#
|
|
209
|
+
# @return [EventMachine::Deferrable]
|
|
210
|
+
#
|
|
211
|
+
def connect(&success_block)
|
|
212
|
+
unless connecting? || connected?
|
|
213
|
+
unless can_transition_to?(:connecting)
|
|
214
|
+
return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:connecting))
|
|
215
|
+
end
|
|
216
|
+
# If connect called in a suspended block, we want to ensure the other callbacks have finished their work first
|
|
217
|
+
EventMachine.next_tick { transition_state_machine :connecting if can_transition_to?(:connecting) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
|
|
221
|
+
deferrable.callback do
|
|
222
|
+
yield if block_given?
|
|
223
|
+
end
|
|
224
|
+
succeed_callback = deferrable.method(:succeed)
|
|
225
|
+
fail_callback = deferrable.method(:fail)
|
|
226
|
+
|
|
227
|
+
unsafe_once(:connected) do
|
|
228
|
+
deferrable.succeed
|
|
229
|
+
off(&fail_callback)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
unsafe_once(:failed, :closed, :closing) do
|
|
233
|
+
deferrable.fail
|
|
234
|
+
off(&succeed_callback)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# When connected, sends a heartbeat ping to the Ably server and executes the callback with any error
|
|
240
|
+
# and the response time in milliseconds when a heartbeat ping request is echoed from the server.
|
|
241
|
+
# This can be useful for measuring true round-trip latency to the connected Ably server.
|
|
242
|
+
#
|
|
243
|
+
# @spec RTN13
|
|
244
|
+
#
|
|
245
|
+
# @yield [Integer] if a block is passed to this method, then this block will be called once the ping heartbeat is received with the time elapsed in seconds.
|
|
246
|
+
# If the ping is not received within an acceptable timeframe, the block will be called with +nil+ as he first argument
|
|
247
|
+
#
|
|
248
|
+
# @example
|
|
249
|
+
# client = Ably::Rest::Client.new(key: 'key.id:secret')
|
|
250
|
+
# client.connection.ping do |elapsed_s|
|
|
251
|
+
# puts "Ping took #{elapsed_s}s"
|
|
252
|
+
# end
|
|
253
|
+
#
|
|
254
|
+
# @return [Ably::Util::SafeDeferrable]
|
|
255
|
+
#
|
|
256
|
+
def ping(&block)
|
|
257
|
+
if initialized? || suspended? || closing? || closed? || failed?
|
|
258
|
+
error = Ably::Models::ErrorInfo.new(message: "Cannot send a ping when the connection is #{state}", code: Ably::Exceptions::Codes::DISCONNECTED)
|
|
259
|
+
return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
|
|
263
|
+
started = nil
|
|
264
|
+
finished = false
|
|
265
|
+
ping_id = SecureRandom.hex(16)
|
|
266
|
+
heartbeat_action = Ably::Models::ProtocolMessage::ACTION.Heartbeat
|
|
267
|
+
|
|
268
|
+
wait_for_ping = lambda do |protocol_message|
|
|
269
|
+
next if finished
|
|
270
|
+
if protocol_message.action == heartbeat_action && protocol_message.id == ping_id
|
|
271
|
+
finished = true
|
|
272
|
+
__incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
|
|
273
|
+
time_passed = Time.now.to_f - started.to_f
|
|
274
|
+
deferrable.succeed time_passed
|
|
275
|
+
safe_yield block, time_passed if block_given?
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
once_or_if(STATE.Connected) do
|
|
280
|
+
next if finished
|
|
281
|
+
started = Time.now
|
|
282
|
+
send_protocol_message action: heartbeat_action.to_i, id: ping_id
|
|
283
|
+
__incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
once_or_if([:suspended, :closing, :closed, :failed]) do
|
|
287
|
+
next if finished
|
|
288
|
+
finished = true
|
|
289
|
+
deferrable.fail Ably::Models::ErrorInfo.new(message: "Ping failed as connection has changed state to #{state}", code: Ably::Exceptions::Codes::DISCONNECTED)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
EventMachine.add_timer(defaults.fetch(:realtime_request_timeout)) do
|
|
293
|
+
next if finished
|
|
294
|
+
finished = true
|
|
295
|
+
__incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
|
|
296
|
+
error_msg = "Ping timed out after #{defaults.fetch(:realtime_request_timeout)}s"
|
|
297
|
+
logger.warn { error_msg }
|
|
298
|
+
deferrable.fail Ably::Models::ErrorInfo.new(message: error_msg, code: Ably::Exceptions::Codes::TIMEOUT_ERROR)
|
|
299
|
+
safe_yield block, nil if block_given?
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# @yield [Boolean] True if an internet connection check appears to be up following an HTTP request to a reliable CDN
|
|
305
|
+
# @return [EventMachine::Deferrable]
|
|
306
|
+
# @api private
|
|
307
|
+
def internet_up?
|
|
308
|
+
url = "http#{'s' if client.use_tls?}:#{Ably::INTERNET_CHECK.fetch(:url)}"
|
|
309
|
+
EventMachine::DefaultDeferrable.new.tap do |deferrable|
|
|
310
|
+
EventMachine::AblyHttpRequest::HttpRequest.new(url, tls: { verify_peer: true }).get.tap do |http|
|
|
311
|
+
http.errback do
|
|
312
|
+
yield false if block_given?
|
|
313
|
+
deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unable to connect to #{url}", nil, Ably::Exceptions::Codes::CONNECTION_FAILED)
|
|
314
|
+
end
|
|
315
|
+
http.callback do
|
|
316
|
+
EventMachine.next_tick do
|
|
317
|
+
result = http.response_header.status == 200 && http.response.strip == Ably::INTERNET_CHECK.fetch(:ok_text)
|
|
318
|
+
yield result if block_given?
|
|
319
|
+
if result
|
|
320
|
+
deferrable.succeed
|
|
321
|
+
else
|
|
322
|
+
deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unexpected response from #{url} (#{http.response_header.status})", 400, Ably::Exceptions::Codes::BAD_REQUEST)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# The recovery key string can be used by another client to recover this connection's state in the
|
|
331
|
+
# recover client options property. See connection state recover options for more information.
|
|
332
|
+
#
|
|
333
|
+
# @spec RTN16b, RTN16c
|
|
334
|
+
#
|
|
335
|
+
# @deprecated Use {#create_recovery_key} instead
|
|
336
|
+
#
|
|
337
|
+
def recovery_key
|
|
338
|
+
logger.warn "[DEPRECATION] recovery_key is deprecated, use create_recovery_key method instead"
|
|
339
|
+
create_recovery_key
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# The recovery key string can be used by another client to recover this connection's state in the recover client
|
|
343
|
+
# options property. See connection state recover options for more information.
|
|
344
|
+
#
|
|
345
|
+
# @spec RTN16g, RTN16c
|
|
346
|
+
#
|
|
347
|
+
# @return [String] a json string which incorporates the @connectionKey@, the current @msgSerial@ and collection
|
|
348
|
+
# of pairs of channel @name@ and current @channelSerial@ for every currently attached channel
|
|
349
|
+
def create_recovery_key
|
|
350
|
+
if key.nil_or_empty? || state == :closing || state == :closed || state == :failed || state == :suspended
|
|
351
|
+
return nil #RTN16g2
|
|
352
|
+
end
|
|
353
|
+
RecoveryKeyContext.new(key, client_msg_serial, client.channels.get_channel_serials).to_json
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Following a new connection being made, the connection ID, connection key
|
|
357
|
+
# need to match the details provided by the server.
|
|
358
|
+
#
|
|
359
|
+
# @return [void]
|
|
360
|
+
# @api private
|
|
361
|
+
def configure_new(connection_id, connection_key)
|
|
362
|
+
@id = connection_id
|
|
363
|
+
@key = connection_key
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Disable automatic resume of a connection
|
|
367
|
+
# @return [void]
|
|
368
|
+
# @api private
|
|
369
|
+
def reset_resume_info
|
|
370
|
+
@key = nil
|
|
371
|
+
@id = nil
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# @!attribute [r] __outgoing_protocol_msgbus__
|
|
375
|
+
# @return [Ably::Util::PubSub] Client library internal outgoing protocol message bus
|
|
376
|
+
# @api private
|
|
377
|
+
def __outgoing_protocol_msgbus__
|
|
378
|
+
@__outgoing_protocol_msgbus__ ||= create_pub_sub_message_bus
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# @!attribute [r] __incoming_protocol_msgbus__
|
|
382
|
+
# @return [Ably::Util::PubSub] Client library internal incoming protocol message bus
|
|
383
|
+
# @api private
|
|
384
|
+
def __incoming_protocol_msgbus__
|
|
385
|
+
@__incoming_protocol_msgbus__ ||= create_pub_sub_message_bus
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Determines the correct host name to use for the next connection attempt and updates current_host
|
|
389
|
+
# @yield [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 if an Internet connection is available
|
|
390
|
+
# @api private
|
|
391
|
+
def determine_host
|
|
392
|
+
raise ArgumentError, 'Block required' unless block_given?
|
|
393
|
+
|
|
394
|
+
if should_use_fallback_hosts?
|
|
395
|
+
internet_up? do |internet_is_up_result|
|
|
396
|
+
@current_host = if internet_is_up_result
|
|
397
|
+
client.fallback_endpoint.host
|
|
398
|
+
else
|
|
399
|
+
client.endpoint.host
|
|
400
|
+
end
|
|
401
|
+
yield current_host
|
|
402
|
+
end
|
|
403
|
+
else
|
|
404
|
+
@current_host = client.endpoint.host
|
|
405
|
+
yield current_host
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# @return [String] The current host that is configured following a call to method {#determine_host}
|
|
410
|
+
# @api private
|
|
411
|
+
attr_reader :current_host
|
|
412
|
+
|
|
413
|
+
# @!attribute [r] port
|
|
414
|
+
# @return [Integer] The default port used for this connection
|
|
415
|
+
def port
|
|
416
|
+
client.use_tls? ? client.custom_tls_port || 443 : client.custom_port || 80
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# @!attribute [r] logger
|
|
420
|
+
# @return [Logger] The {Ably::Logger} for this client.
|
|
421
|
+
# Configure the log_level with the `:log_level` option, refer to {Ably::Realtime::Client#initialize}
|
|
422
|
+
def logger
|
|
423
|
+
client.logger
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Add protocol message to the outgoing message queue and notify the dispatcher that a message is
|
|
427
|
+
# ready to be sent
|
|
428
|
+
#
|
|
429
|
+
# @param [Ably::Models::ProtocolMessage] protocol_message
|
|
430
|
+
# @return [void]
|
|
431
|
+
# @api private
|
|
432
|
+
def send_protocol_message(protocol_message)
|
|
433
|
+
add_message_serial_if_ack_required_to(protocol_message) do
|
|
434
|
+
message = Ably::Models::ProtocolMessage.new(protocol_message, logger: logger)
|
|
435
|
+
add_message_to_outgoing_queue(message)
|
|
436
|
+
notify_message_dispatcher_of_new_message message
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def send_protocol_message_immediately(protocol_message)
|
|
441
|
+
message = Ably::Models::ProtocolMessage.new(protocol_message, logger: logger)
|
|
442
|
+
add_message_to_outgoing_queue(message, true)
|
|
443
|
+
notify_message_dispatcher_of_new_message message
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# @api private
|
|
447
|
+
def add_message_to_outgoing_queue(protocol_message, send_immediately = false)
|
|
448
|
+
if send_immediately
|
|
449
|
+
# Adding msg at the top of the queue to get processed immediately while connection is CONNECTED
|
|
450
|
+
__outgoing_message_queue__.prepend(protocol_message)
|
|
451
|
+
logger.debug { "Connection: protocol msg pushed at the top =>: #{protocol_message.action} #{protocol_message}" }
|
|
452
|
+
else
|
|
453
|
+
__outgoing_message_queue__ << protocol_message
|
|
454
|
+
logger.debug { "Connection: protocol msg queued =>: #{protocol_message.action} #{protocol_message}" }
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# @api private
|
|
459
|
+
def notify_message_dispatcher_of_new_message(protocol_message)
|
|
460
|
+
__outgoing_protocol_msgbus__.publish :protocol_message, protocol_message
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# @return [EventMachine::Deferrable]
|
|
464
|
+
# @api private
|
|
465
|
+
def create_websocket_transport
|
|
466
|
+
EventMachine::DefaultDeferrable.new.tap do |websocket_deferrable|
|
|
467
|
+
# Getting auth params can be blocking so uses a Deferrable
|
|
468
|
+
client.auth.auth_params.tap do |auth_deferrable|
|
|
469
|
+
auth_deferrable.callback do |auth_params|
|
|
470
|
+
url_params = auth_params.merge(
|
|
471
|
+
'format' => client.protocol,
|
|
472
|
+
'echo' => client.echo_messages,
|
|
473
|
+
'v' => Ably::PROTOCOL_VERSION, # RSC7a
|
|
474
|
+
'agent' => client.rest_client.agent
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Use native websocket heartbeats if possible, but allow Ably protocol heartbeats
|
|
478
|
+
url_params['heartbeats'] = if defaults.fetch(:websocket_heartbeats_disabled)
|
|
479
|
+
'true'
|
|
480
|
+
else
|
|
481
|
+
'false'
|
|
482
|
+
end
|
|
483
|
+
# RSA7e1
|
|
484
|
+
url_params['clientId'] = client.auth.client_id_for_request_sync if client.auth.client_id_for_request_sync
|
|
485
|
+
url_params.merge!(client.transport_params)
|
|
486
|
+
|
|
487
|
+
if !key.nil_or_empty? and connection_state_available?
|
|
488
|
+
url_params.merge! resume: key
|
|
489
|
+
logger.debug { "Resuming connection with key #{key}" }
|
|
490
|
+
elsif !client.recover.nil_or_empty?
|
|
491
|
+
recovery_context = RecoveryKeyContext.from_json(client.recover, logger)
|
|
492
|
+
unless recovery_context.nil?
|
|
493
|
+
key = recovery_context.connection_key
|
|
494
|
+
logger.debug { "Recovering connection with key #{key}" }
|
|
495
|
+
url_params.merge! recover: key
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
url = URI(client.endpoint).tap do |endpoint|
|
|
500
|
+
endpoint.query = URI.encode_www_form(url_params)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
determine_host do |host|
|
|
504
|
+
# Ensure the hostname matches the fallback host name
|
|
505
|
+
url.hostname = host
|
|
506
|
+
url.port = port
|
|
507
|
+
|
|
508
|
+
begin
|
|
509
|
+
logger.debug { "Connection: Opening socket connection to #{host}:#{port}/#{url.path}?#{url.query}" }
|
|
510
|
+
@transport = create_transport(host, port, url) do |websocket_transport|
|
|
511
|
+
websocket_deferrable.succeed websocket_transport
|
|
512
|
+
end
|
|
513
|
+
rescue EventMachine::ConnectionError => error
|
|
514
|
+
websocket_deferrable.fail error
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
auth_deferrable.errback do |error|
|
|
520
|
+
websocket_deferrable.fail error
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# @api private
|
|
527
|
+
def release_websocket_transport
|
|
528
|
+
@transport = nil
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# @api private
|
|
532
|
+
def set_failed_connection_error_reason(error)
|
|
533
|
+
@error_reason = error
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# @api private
|
|
537
|
+
def clear_error_reason
|
|
538
|
+
@error_reason = nil
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# @api private
|
|
542
|
+
def set_connection_details(connection_details)
|
|
543
|
+
@details = connection_details
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Returns false if messages cannot be published as a result of message queueing being disabled
|
|
547
|
+
# @api private
|
|
548
|
+
def can_publish_messages?
|
|
549
|
+
connected? ||
|
|
550
|
+
( (initialized? || connecting? || disconnected?) && client.queue_messages )
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# @api private
|
|
554
|
+
def create_transport(host, port, url, &block)
|
|
555
|
+
logger.debug { "Connection: EventMachine connecting to #{host}:#{port} with URL: #{url}" }
|
|
556
|
+
EventMachine.connect(host, port, WebsocketTransport, self, url.to_s, &block)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# @api private
|
|
560
|
+
def connection_state_ttl
|
|
561
|
+
defaults[:max_connection_state_ttl] || # undocumented max TTL configuration
|
|
562
|
+
(details && details.connection_state_ttl) ||
|
|
563
|
+
defaults.fetch(:connection_state_ttl)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def connection_state_ttl=(val)
|
|
567
|
+
@connection_state_ttl = val
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# @api private
|
|
571
|
+
def heartbeat_interval
|
|
572
|
+
# See RTN23a
|
|
573
|
+
(details && details.max_idle_interval).to_i +
|
|
574
|
+
defaults.fetch(:realtime_request_timeout)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# Resets the client message serial (msgSerial) sent to Ably for each new {Ably::Models::ProtocolMessage}
|
|
578
|
+
# (see #client_msg_serial)
|
|
579
|
+
# @api private
|
|
580
|
+
def reset_client_msg_serial
|
|
581
|
+
@client_msg_serial = -1
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Sets the client message serial from recover clientOption.
|
|
585
|
+
# @api private
|
|
586
|
+
def set_msg_serial_from_recover=(value)
|
|
587
|
+
@client_msg_serial = value
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# When a hearbeat or any other message from Ably is received
|
|
591
|
+
# we know it's alive, see #RTN23
|
|
592
|
+
# @api private
|
|
593
|
+
def set_connection_confirmed_alive
|
|
594
|
+
@last_liveness_event = Time.now
|
|
595
|
+
manager.reset_liveness_timer
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# @api private
|
|
599
|
+
def time_since_connection_confirmed_alive?
|
|
600
|
+
Time.now.to_i - @last_liveness_event.to_i
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# As we are using a state machine, do not allow change_state to be used
|
|
604
|
+
# #transition_state_machine must be used instead
|
|
605
|
+
private :change_state
|
|
606
|
+
|
|
607
|
+
private
|
|
608
|
+
|
|
609
|
+
# The client message serial (msgSerial) is incremented for every message that is published that requires an ACK.
|
|
610
|
+
# A message serial number does not guarantee a message has been received, only sent.
|
|
611
|
+
# @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent
|
|
612
|
+
def client_msg_serial
|
|
613
|
+
@client_msg_serial
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def create_pub_sub_message_bus
|
|
617
|
+
Ably::Util::PubSub.new(
|
|
618
|
+
coerce_into: lambda do |event|
|
|
619
|
+
raise KeyError, "Expected :protocol_message, :#{event} is disallowed" unless event == :protocol_message
|
|
620
|
+
:protocol_message
|
|
621
|
+
end
|
|
622
|
+
)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def add_message_serial_if_ack_required_to(protocol_message)
|
|
626
|
+
if Ably::Models::ProtocolMessage.ack_required?(protocol_message[:action])
|
|
627
|
+
add_message_serial_to(protocol_message) { yield }
|
|
628
|
+
else
|
|
629
|
+
yield
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def add_message_serial_to(protocol_message)
|
|
634
|
+
@client_msg_serial += 1
|
|
635
|
+
protocol_message[:msgSerial] = client_msg_serial
|
|
636
|
+
yield
|
|
637
|
+
rescue StandardError => e
|
|
638
|
+
@client_msg_serial -= 1
|
|
639
|
+
raise e
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# Simply wait until the next EventMachine tick to ensure Connection initialization is complete
|
|
643
|
+
def when_initialized
|
|
644
|
+
EventMachine.next_tick { yield }
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def connection_state_available?
|
|
648
|
+
return true if connected?
|
|
649
|
+
|
|
650
|
+
return false if time_since_connection_confirmed_alive? > connection_state_ttl + details.max_idle_interval
|
|
651
|
+
|
|
652
|
+
connected_last = state_history.reverse.find { |connected| connected.fetch(:state) == :connected }
|
|
653
|
+
if connected_last.nil?
|
|
654
|
+
false
|
|
655
|
+
else
|
|
656
|
+
true
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def production?
|
|
661
|
+
client.environment.nil? || client.environment == :production
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def custom_port?
|
|
665
|
+
if client.use_tls?
|
|
666
|
+
!!client.custom_tls_port
|
|
667
|
+
else
|
|
668
|
+
!!client.custom_port
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def custom_host?
|
|
673
|
+
!!client.custom_realtime_host
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def should_use_fallback_hosts?
|
|
677
|
+
if client.fallback_hosts && !client.fallback_hosts.empty?
|
|
678
|
+
if connecting? && previous_state && !disconnected_from_connected_state?
|
|
679
|
+
use_fallback_if_disconnected? || use_fallback_if_suspended?
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def disconnected_from_connected_state?
|
|
685
|
+
most_recent_state_changes = state_history.last(3).first(2) # Ignore current state
|
|
686
|
+
|
|
687
|
+
# A valid connection was disconnected
|
|
688
|
+
most_recent_state_changes.last.fetch(:state) == Connection::STATE.Disconnected &&
|
|
689
|
+
most_recent_state_changes.first.fetch(:state) == Connection::STATE.Connected
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def use_fallback_if_disconnected?
|
|
693
|
+
unless second_reconnect_attempt_for(:disconnected, 1)
|
|
694
|
+
return false
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
does_error_necessitate_fallback(reason_for_last_time_in(:disconnected))
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def use_fallback_if_suspended?
|
|
701
|
+
unless second_reconnect_attempt_for(:suspended, 2) # on first suspended state use default Ably host again
|
|
702
|
+
return false
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
does_error_necessitate_fallback(reason_for_last_time_in(:suspended))
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def second_reconnect_attempt_for(state, first_attempt_count)
|
|
709
|
+
previous_state == state && manager.retry_count_for_state(state) >= first_attempt_count
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Provides a partial implementation of RTN17f's logic for whether an error necessitates a fallback host.
|
|
713
|
+
def does_error_necessitate_fallback(error)
|
|
714
|
+
return false unless error
|
|
715
|
+
|
|
716
|
+
# For now we just explicitly exclude token errors. TODO: implement properly in https://github.com/ably/ably-ruby/issues/444
|
|
717
|
+
|
|
718
|
+
if error.respond_to?(:status_code) && error.status_code == 401 && error.respond_to?(:code) && Ably::Exceptions::TOKEN_EXPIRED_CODE.include?(error.code)
|
|
719
|
+
return false
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
true
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Returns the error associated with the last state change to the given state (e.g. :disconnected).
|
|
726
|
+
def reason_for_last_time_in(state)
|
|
727
|
+
history_item = state_history.reverse.find do |history_item|
|
|
728
|
+
history_item.fetch(:state) == state
|
|
729
|
+
end.fetch(:metadata).reason
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
require 'ably/realtime/connection/connection_manager'
|
|
736
|
+
require 'ably/realtime/connection/connection_state_machine'
|
|
737
|
+
require 'ably/realtime/connection/websocket_transport'
|
|
738
|
+
require 'ably/realtime/recovery_key_context'
|