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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ably/agent.rb +8 -0
  3. data/lib/ably/auth.rb +791 -0
  4. data/lib/ably/exceptions.rb +169 -0
  5. data/lib/ably/logger.rb +119 -0
  6. data/lib/ably/models/auth_details.rb +45 -0
  7. data/lib/ably/models/channel_details.rb +69 -0
  8. data/lib/ably/models/channel_metrics.rb +107 -0
  9. data/lib/ably/models/channel_occupancy.rb +50 -0
  10. data/lib/ably/models/channel_options.rb +120 -0
  11. data/lib/ably/models/channel_state_change.rb +84 -0
  12. data/lib/ably/models/channel_status.rb +63 -0
  13. data/lib/ably/models/cipher_params.rb +137 -0
  14. data/lib/ably/models/connection_details.rb +149 -0
  15. data/lib/ably/models/connection_state_change.rb +83 -0
  16. data/lib/ably/models/delta_extras.rb +28 -0
  17. data/lib/ably/models/device_details.rb +126 -0
  18. data/lib/ably/models/device_push_details.rb +94 -0
  19. data/lib/ably/models/error_info.rb +104 -0
  20. data/lib/ably/models/http_paginated_response.rb +112 -0
  21. data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
  22. data/lib/ably/models/message.rb +223 -0
  23. data/lib/ably/models/message_encoders/base.rb +135 -0
  24. data/lib/ably/models/message_encoders/base64.rb +40 -0
  25. data/lib/ably/models/message_encoders/cipher.rb +84 -0
  26. data/lib/ably/models/message_encoders/json.rb +34 -0
  27. data/lib/ably/models/message_encoders/utf8.rb +26 -0
  28. data/lib/ably/models/nil_logger.rb +20 -0
  29. data/lib/ably/models/paginated_result.rb +201 -0
  30. data/lib/ably/models/presence_message.rb +244 -0
  31. data/lib/ably/models/protocol_message.rb +288 -0
  32. data/lib/ably/models/push_channel_subscription.rb +114 -0
  33. data/lib/ably/models/stats.rb +242 -0
  34. data/lib/ably/models/stats_types.rb +107 -0
  35. data/lib/ably/models/token_details.rb +148 -0
  36. data/lib/ably/models/token_request.rb +180 -0
  37. data/lib/ably/modules/ably.rb +26 -0
  38. data/lib/ably/modules/async_wrapper.rb +69 -0
  39. data/lib/ably/modules/channels_collection.rb +105 -0
  40. data/lib/ably/modules/conversions.rb +153 -0
  41. data/lib/ably/modules/encodeable.rb +111 -0
  42. data/lib/ably/modules/enum.rb +230 -0
  43. data/lib/ably/modules/event_emitter.rb +200 -0
  44. data/lib/ably/modules/event_machine_helpers.rb +26 -0
  45. data/lib/ably/modules/exception_codes.rb +128 -0
  46. data/lib/ably/modules/http_helpers.rb +41 -0
  47. data/lib/ably/modules/message_emitter.rb +78 -0
  48. data/lib/ably/modules/message_pack.rb +14 -0
  49. data/lib/ably/modules/model_common.rb +84 -0
  50. data/lib/ably/modules/safe_deferrable.rb +83 -0
  51. data/lib/ably/modules/safe_yield.rb +41 -0
  52. data/lib/ably/modules/state_emitter.rb +181 -0
  53. data/lib/ably/modules/state_machine.rb +59 -0
  54. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  55. data/lib/ably/modules/uses_state_machine.rb +111 -0
  56. data/lib/ably/realtime/auth.rb +283 -0
  57. data/lib/ably/realtime/channel/channel_manager.rb +272 -0
  58. data/lib/ably/realtime/channel/channel_properties.rb +40 -0
  59. data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
  60. data/lib/ably/realtime/channel/publisher.rb +82 -0
  61. data/lib/ably/realtime/channel/push_channel.rb +69 -0
  62. data/lib/ably/realtime/channel.rb +440 -0
  63. data/lib/ably/realtime/channels.rb +71 -0
  64. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
  65. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
  66. data/lib/ably/realtime/client.rb +363 -0
  67. data/lib/ably/realtime/connection/connection_manager.rb +594 -0
  68. data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
  69. data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
  70. data/lib/ably/realtime/connection.rb +738 -0
  71. data/lib/ably/realtime/models/nil_channel.rb +30 -0
  72. data/lib/ably/realtime/presence/members_map.rb +395 -0
  73. data/lib/ably/realtime/presence/presence_manager.rb +78 -0
  74. data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
  75. data/lib/ably/realtime/presence.rb +502 -0
  76. data/lib/ably/realtime/push/admin.rb +75 -0
  77. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  78. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  79. data/lib/ably/realtime/push.rb +25 -0
  80. data/lib/ably/realtime/recovery_key_context.rb +36 -0
  81. data/lib/ably/realtime.rb +60 -0
  82. data/lib/ably/rest/channel/push_channel.rb +59 -0
  83. data/lib/ably/rest/channel.rb +194 -0
  84. data/lib/ably/rest/channels.rb +47 -0
  85. data/lib/ably/rest/client.rb +727 -0
  86. data/lib/ably/rest/middleware/encoder.rb +49 -0
  87. data/lib/ably/rest/middleware/exceptions.rb +53 -0
  88. data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  89. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
  90. data/lib/ably/rest/middleware/logger.rb +58 -0
  91. data/lib/ably/rest/middleware/parse_json.rb +27 -0
  92. data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
  93. data/lib/ably/rest/presence.rb +113 -0
  94. data/lib/ably/rest/push/admin.rb +58 -0
  95. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  96. data/lib/ably/rest/push/device_registrations.rb +114 -0
  97. data/lib/ably/rest/push.rb +25 -0
  98. data/lib/ably/rest.rb +40 -0
  99. data/lib/ably/util/ably_extensions.rb +29 -0
  100. data/lib/ably/util/crypto.rb +134 -0
  101. data/lib/ably/util/pub_sub.rb +45 -0
  102. data/lib/ably/util/safe_deferrable.rb +44 -0
  103. data/lib/ably/version.rb +7 -0
  104. data/lib/ably.rb +16 -0
  105. 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'