ably-pubsub-core 0.0.1.pre → 2.0.0

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