ably 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +42 -48
- data/SPEC.md +1099 -640
- data/ably.gemspec +10 -4
- data/lib/ably/auth.rb +155 -47
- data/lib/ably/exceptions.rb +2 -0
- data/lib/ably/models/channel_state_change.rb +2 -3
- data/lib/ably/models/connection_details.rb +54 -0
- data/lib/ably/models/protocol_message.rb +14 -4
- data/lib/ably/models/token_details.rb +13 -7
- data/lib/ably/models/token_request.rb +1 -2
- data/lib/ably/modules/ably.rb +3 -2
- data/lib/ably/modules/message_emitter.rb +1 -3
- data/lib/ably/modules/state_emitter.rb +2 -2
- data/lib/ably/realtime/auth.rb +6 -0
- data/lib/ably/realtime/channel/channel_manager.rb +2 -0
- data/lib/ably/realtime/channel.rb +15 -4
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +11 -1
- data/lib/ably/realtime/client.rb +10 -3
- data/lib/ably/realtime/connection/connection_manager.rb +58 -54
- data/lib/ably/realtime/connection.rb +62 -6
- data/lib/ably/realtime/presence.rb +18 -5
- data/lib/ably/rest/channel.rb +9 -1
- data/lib/ably/rest/client.rb +32 -14
- data/lib/ably/rest/presence.rb +1 -1
- data/lib/ably/version.rb +1 -1
- data/lib/ably.rb +2 -0
- data/spec/acceptance/realtime/auth_spec.rb +251 -11
- data/spec/acceptance/realtime/channel_history_spec.rb +12 -2
- data/spec/acceptance/realtime/channel_spec.rb +316 -24
- data/spec/acceptance/realtime/client_spec.rb +93 -1
- data/spec/acceptance/realtime/connection_failures_spec.rb +177 -86
- data/spec/acceptance/realtime/connection_spec.rb +284 -60
- data/spec/acceptance/realtime/message_spec.rb +45 -6
- data/spec/acceptance/realtime/presence_history_spec.rb +4 -0
- data/spec/acceptance/realtime/presence_spec.rb +181 -49
- data/spec/acceptance/realtime/time_spec.rb +13 -0
- data/spec/acceptance/rest/auth_spec.rb +222 -4
- data/spec/acceptance/rest/channel_spec.rb +132 -1
- data/spec/acceptance/rest/client_spec.rb +129 -28
- data/spec/acceptance/rest/presence_spec.rb +7 -7
- data/spec/acceptance/rest/time_spec.rb +10 -0
- data/spec/shared/client_initializer_behaviour.rb +41 -17
- data/spec/spec_helper.rb +1 -0
- data/spec/support/debug_failure_helper.rb +16 -0
- data/spec/unit/models/connection_details_spec.rb +60 -0
- data/spec/unit/models/protocol_message_spec.rb +45 -0
- data/spec/unit/modules/event_emitter_spec.rb +3 -1
- data/spec/unit/realtime/channel_spec.rb +6 -5
- data/spec/unit/realtime/client_spec.rb +5 -1
- data/spec/unit/realtime/connection_spec.rb +5 -1
- data/spec/unit/realtime/realtime_spec.rb +5 -1
- metadata +54 -7
@@ -73,7 +73,7 @@ module Ably::Realtime
|
|
73
73
|
elsif connection.connected?
|
74
74
|
logger.error "CONNECTED ProtocolMessage should not have been received when the connection is in the CONNECTED state"
|
75
75
|
else
|
76
|
-
|
76
|
+
process_connected_message protocol_message
|
77
77
|
end
|
78
78
|
|
79
79
|
when ACTION.Disconnect, ACTION.Disconnected
|
@@ -141,6 +141,16 @@ module Ably::Realtime
|
|
141
141
|
connection.manager.error_received_from_server protocol_message.error
|
142
142
|
end
|
143
143
|
|
144
|
+
def process_connected_message(protocol_message)
|
145
|
+
if client.auth.token_client_id_allowed?(protocol_message.connection_details.client_id)
|
146
|
+
client.auth.configure_client_id protocol_message.connection_details.client_id
|
147
|
+
connection.transition_state_machine :connected, reason: protocol_message.error, protocol_message: protocol_message
|
148
|
+
else
|
149
|
+
reason = Ably::Exceptions::IncompatibleClientId.new("Client ID '#{protocol_message.connection_details.client_id}' specified by the server is incompatible with the library's configured client ID '#{client.client_id}'", 400, 40012)
|
150
|
+
connection.transition_state_machine :failed, reason: reason, protocol_message: protocol_message
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
144
154
|
def update_connection_recovery_info(protocol_message)
|
145
155
|
connection.update_connection_serial protocol_message.connection_serial if protocol_message.has_connection_serial?
|
146
156
|
end
|
data/lib/ably/realtime/client.rb
CHANGED
@@ -30,6 +30,10 @@ module Ably
|
|
30
30
|
# (see Ably::Rest::Client#auth)
|
31
31
|
attr_reader :auth
|
32
32
|
|
33
|
+
# The underlying connection for this client
|
34
|
+
# @return [Aby::Realtime::Connection]
|
35
|
+
attr_reader :connection
|
36
|
+
|
33
37
|
# The {Ably::Rest::Client REST client} instantiated with the same credentials and configuration that is used for all REST operations such as authentication
|
34
38
|
# @return [Ably::Rest::Client]
|
35
39
|
attr_reader :rest_client
|
@@ -69,6 +73,9 @@ module Ably
|
|
69
73
|
# @option options [String] :recover When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery
|
70
74
|
# @option options [Boolean] :auto_connect By default as soon as the client library is instantiated it will connect to Ably. You can optionally set this to false and explicitly connect.
|
71
75
|
#
|
76
|
+
# @option options [Integer] :disconnected_retry_timeout (15 seconds). 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
|
77
|
+
# @option options [Integer] :suspended_retry_timeout (30 seconds). 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
|
78
|
+
#
|
72
79
|
# @return [Ably::Realtime::Client]
|
73
80
|
#
|
74
81
|
# @example
|
@@ -82,6 +89,7 @@ module Ably
|
|
82
89
|
@rest_client = Ably::Rest::Client.new(options)
|
83
90
|
@auth = Ably::Realtime::Auth.new(self)
|
84
91
|
@channels = Ably::Realtime::Channels.new(self)
|
92
|
+
@connection = Ably::Realtime::Connection.new(self, options)
|
85
93
|
@echo_messages = @rest_client.options.fetch(:echo_messages, true) == false ? false : true
|
86
94
|
@queue_messages = @rest_client.options.fetch(:queue_messages, true) == false ? false : true
|
87
95
|
@custom_realtime_host = @rest_client.options[:realtime_host] || @rest_client.options[:ws_host]
|
@@ -142,10 +150,9 @@ module Ably
|
|
142
150
|
endpoint_for_host(custom_realtime_host || [environment, DOMAIN].compact.join('-'))
|
143
151
|
end
|
144
152
|
|
145
|
-
|
146
|
-
# @return [Aby::Realtime::Connection] The underlying connection for this client
|
153
|
+
|
147
154
|
def connection
|
148
|
-
@connection
|
155
|
+
@connection
|
149
156
|
end
|
150
157
|
|
151
158
|
# (see Ably::Rest::Client#register_encoder)
|
@@ -8,18 +8,6 @@ module Ably::Realtime
|
|
8
8
|
#
|
9
9
|
# @api private
|
10
10
|
class ConnectionManager
|
11
|
-
# Configuration for automatic recovery of failed connection attempts
|
12
|
-
CONNECT_RETRY_CONFIG = {
|
13
|
-
disconnected: { retry_every: 15, max_time_in_state: 120 },
|
14
|
-
suspended: { retry_every: 120, max_time_in_state: Float::INFINITY }
|
15
|
-
}.freeze
|
16
|
-
|
17
|
-
# Time to wait following a connection state request before it's considered a failure
|
18
|
-
TIMEOUTS = {
|
19
|
-
open: 15,
|
20
|
-
close: 10
|
21
|
-
}
|
22
|
-
|
23
11
|
# Error codes from the server that can potentially be resolved
|
24
12
|
RESOLVABLE_ERROR_CODES = {
|
25
13
|
token_expired: 40140
|
@@ -69,9 +57,9 @@ module Ably::Realtime
|
|
69
57
|
end
|
70
58
|
end
|
71
59
|
|
72
|
-
logger.debug "ConnectionManager: Setting up automatic connection timeout timer for #{
|
73
|
-
create_timeout_timer_whilst_in_state(:connect,
|
74
|
-
connection_opening_failed Ably::Exceptions::ConnectionTimeout.new("Connection to Ably timed out after #{
|
60
|
+
logger.debug "ConnectionManager: Setting up automatic connection timeout timer for #{realtime_request_timeout}s"
|
61
|
+
create_timeout_timer_whilst_in_state(:connect, realtime_request_timeout) do
|
62
|
+
connection_opening_failed Ably::Exceptions::ConnectionTimeout.new("Connection to Ably timed out after #{realtime_request_timeout}s", nil, 80014)
|
75
63
|
end
|
76
64
|
end
|
77
65
|
|
@@ -79,6 +67,11 @@ module Ably::Realtime
|
|
79
67
|
#
|
80
68
|
# @api private
|
81
69
|
def connection_opening_failed(error)
|
70
|
+
if error.kind_of?(Ably::Exceptions::IncompatibleClientId)
|
71
|
+
client.connection.transition_state_machine :failed, reason: error
|
72
|
+
return
|
73
|
+
end
|
74
|
+
|
82
75
|
logger.warn "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}"
|
83
76
|
next_state = get_next_retry_state_info
|
84
77
|
connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, 80000)
|
@@ -89,18 +82,17 @@ module Ably::Realtime
|
|
89
82
|
# @api private
|
90
83
|
def connected(protocol_message)
|
91
84
|
if connection.key
|
92
|
-
if
|
85
|
+
if protocol_message.connection_id == connection.id
|
93
86
|
logger.debug "ConnectionManager: Connection resumed successfully - ID #{connection.id} and key #{connection.key}"
|
94
87
|
EventMachine.next_tick { connection.resumed }
|
95
88
|
else
|
96
89
|
logger.debug "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connect ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}"
|
97
90
|
detach_attached_channels protocol_message.error
|
98
|
-
connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
|
99
91
|
end
|
100
92
|
else
|
101
93
|
logger.debug "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}"
|
102
|
-
connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
|
103
94
|
end
|
95
|
+
connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
|
104
96
|
end
|
105
97
|
|
106
98
|
# Ensures the underlying transport has been disconnected and all event emitter callbacks removed
|
@@ -131,7 +123,7 @@ module Ably::Realtime
|
|
131
123
|
def close_connection
|
132
124
|
connection.send_protocol_message(action: Ably::Models::ProtocolMessage::ACTION.Close)
|
133
125
|
|
134
|
-
create_timeout_timer_whilst_in_state(:close,
|
126
|
+
create_timeout_timer_whilst_in_state(:close, realtime_request_timeout) do
|
135
127
|
force_close_connection if connection.closing?
|
136
128
|
end
|
137
129
|
end
|
@@ -158,19 +150,23 @@ module Ably::Realtime
|
|
158
150
|
# @api private
|
159
151
|
def respond_to_transport_disconnected_when_connecting(error)
|
160
152
|
return unless connection.disconnected? || connection.suspended? # do nothing if state has changed through an explicit request
|
161
|
-
return unless
|
153
|
+
return unless can_retry_connection? # do not always reattempt connection or change state as client may be re-authorising
|
162
154
|
|
163
155
|
if error.kind_of?(Ably::Models::ErrorInfo)
|
164
|
-
|
165
|
-
|
156
|
+
if error.code == RESOLVABLE_ERROR_CODES.fetch(:token_expired)
|
157
|
+
next_state = get_next_retry_state_info
|
158
|
+
logger.debug "ConnectionManager: Transport disconnected because of token expiry, pausing #{next_state.fetch(:pause)}s before reattempting to connect"
|
159
|
+
EventMachine.add_timer(next_state.fetch(:pause)) { renew_token_and_reconnect error }
|
160
|
+
return
|
161
|
+
end
|
166
162
|
end
|
167
163
|
|
168
|
-
|
164
|
+
if connection.state == :suspended
|
165
|
+
return if connection_retry_for(:suspended)
|
166
|
+
elsif connection.state == :disconnected
|
169
167
|
return if connection_retry_for(:disconnected)
|
170
168
|
end
|
171
169
|
|
172
|
-
return if connection_retry_for(:suspended)
|
173
|
-
|
174
170
|
# Fallback if no other criteria met
|
175
171
|
connection.transition_state_machine :failed, reason: error
|
176
172
|
end
|
@@ -179,7 +175,11 @@ module Ably::Realtime
|
|
179
175
|
#
|
180
176
|
# @api private
|
181
177
|
def respond_to_transport_disconnected_whilst_connected(error)
|
182
|
-
|
178
|
+
unless connection.disconnected? || connection.suspended?
|
179
|
+
logger.warn "ConnectionManager: Connection #{"to #{connection.transport.url}" if connection.transport} was disconnected unexpectedly"
|
180
|
+
else
|
181
|
+
logger.debug "ConnectionManager: Transport disconnected whilst connection in #{connection.state} state"
|
182
|
+
end
|
183
183
|
|
184
184
|
if error.kind_of?(Ably::Models::ErrorInfo) && error.code != RESOLVABLE_ERROR_CODES.fetch(:token_expired)
|
185
185
|
connection.emit :error, error
|
@@ -197,10 +197,8 @@ module Ably::Realtime
|
|
197
197
|
def error_received_from_server(error)
|
198
198
|
case error.code
|
199
199
|
when RESOLVABLE_ERROR_CODES.fetch(:token_expired)
|
200
|
-
|
201
|
-
connection.
|
202
|
-
renew_token_and_reconnect error
|
203
|
-
end
|
200
|
+
next_state = get_next_retry_state_info
|
201
|
+
connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: error
|
204
202
|
else
|
205
203
|
logger.error "ConnectionManager: Error #{error.class.name} code #{error.code} received from server '#{error.message}', transitioning to failed state"
|
206
204
|
connection.transition_state_machine :failed, reason: error
|
@@ -233,10 +231,16 @@ module Ably::Realtime
|
|
233
231
|
client.channels
|
234
232
|
end
|
235
233
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
234
|
+
def realtime_request_timeout
|
235
|
+
connection.defaults.fetch(:realtime_request_timeout)
|
236
|
+
end
|
237
|
+
|
238
|
+
def retry_timeout_for(state)
|
239
|
+
connection.defaults.fetch("#{state}_retry_timeout".to_sym) { raise ArgumentError.new("#{state} does not have a configured retry timeout") }
|
240
|
+
end
|
241
|
+
|
242
|
+
def state_has_retry_timeout?(state)
|
243
|
+
connection.defaults.has_key?("#{state}_retry_timeout".to_sym)
|
240
244
|
end
|
241
245
|
|
242
246
|
# Create a timer that will execute in timeout_in seconds.
|
@@ -267,12 +271,12 @@ module Ably::Realtime
|
|
267
271
|
end
|
268
272
|
|
269
273
|
def next_retry_pause(retry_state)
|
270
|
-
return nil unless
|
274
|
+
return nil unless state_has_retry_timeout?(retry_state)
|
271
275
|
|
272
276
|
if retries_for_state(retry_state, ignore_states: [:connecting]).empty?
|
273
277
|
0
|
274
278
|
else
|
275
|
-
|
279
|
+
retry_timeout_for(retry_state)
|
276
280
|
end
|
277
281
|
end
|
278
282
|
|
@@ -284,20 +288,18 @@ module Ably::Realtime
|
|
284
288
|
time_spent_attempting_state(:disconnected, ignore_states: [:connecting])
|
285
289
|
end
|
286
290
|
|
287
|
-
# Reattempt a connection with a delay based on the
|
291
|
+
# Reattempt a connection with a delay based on the configured retry timeout for +from_state+
|
288
292
|
#
|
289
293
|
# @return [Boolean] True if a connection attempt has been set up, false if no further connection attempts can be made for this state
|
290
294
|
#
|
291
295
|
def connection_retry_for(from_state)
|
292
|
-
retry_params = CONNECT_RETRY_CONFIG.fetch(from_state)
|
293
|
-
|
294
296
|
if can_reattempt_connect_for_state?(from_state)
|
295
|
-
if retries_for_state(from_state, ignore_states: [:connecting]).empty?
|
296
|
-
logger.debug "ConnectionManager: Will attempt reconnect immediately as no previous reconnect attempts made in
|
297
|
+
if connection.state == :disconnected && retries_for_state(from_state, ignore_states: [:connecting]).empty?
|
298
|
+
logger.debug "ConnectionManager: Will attempt reconnect immediately as no previous reconnect attempts made in state #{from_state}"
|
297
299
|
EventMachine.next_tick { connection.connect }
|
298
300
|
else
|
299
|
-
logger.debug "ConnectionManager: Pausing for #{
|
300
|
-
create_timeout_timer_whilst_in_state(
|
301
|
+
logger.debug "ConnectionManager: Pausing for #{retry_timeout_for(from_state)}s before attempting to reconnect"
|
302
|
+
create_timeout_timer_whilst_in_state(from_state, retry_timeout_for(from_state)) do
|
301
303
|
connection.connect if connection.state == from_state
|
302
304
|
end
|
303
305
|
end
|
@@ -309,8 +311,14 @@ module Ably::Realtime
|
|
309
311
|
# For example, if the state is disconnected, and has been in a cycle of disconnected > connect > disconnected
|
310
312
|
# so long as the time in this cycle of states is less than max_time_in_state, this will return true
|
311
313
|
def can_reattempt_connect_for_state?(state)
|
312
|
-
|
313
|
-
|
314
|
+
case state
|
315
|
+
when :disconnected
|
316
|
+
time_spent_attempting_state(:disconnected, ignore_states: [:connecting]) < connection.defaults.fetch(:connection_state_ttl)
|
317
|
+
when :suspended
|
318
|
+
true # suspended state remains indefinitely
|
319
|
+
else
|
320
|
+
raise ArgumentError, "Connections in state '#{state}' cannot be reattempted"
|
321
|
+
end
|
314
322
|
end
|
315
323
|
|
316
324
|
# Returns a float representing the amount of time passed since the first consecutive attempt of this state
|
@@ -380,23 +388,18 @@ module Ably::Realtime
|
|
380
388
|
def renew_token_and_reconnect(error)
|
381
389
|
if client.auth.token_renewable?
|
382
390
|
if @renewing_token
|
383
|
-
|
391
|
+
logger.error 'ConnectionManager: Attempting to renew token whilst another token renewal is underway. Aborting current renew token request'
|
384
392
|
return
|
385
393
|
end
|
386
394
|
|
387
395
|
@renewing_token = true
|
388
396
|
logger.info "ConnectionManager: Token has expired and is renewable, renewing token now"
|
389
397
|
|
390
|
-
client.auth.authorise.tap do |authorise_deferrable|
|
398
|
+
client.auth.authorise({}, force: true).tap do |authorise_deferrable|
|
391
399
|
authorise_deferrable.callback do |token_details|
|
392
400
|
logger.info 'ConnectionManager: Token renewed succesfully following expiration'
|
393
401
|
|
394
|
-
|
395
|
-
@renewing_token = false
|
396
|
-
connection.off &state_changed_callback
|
397
|
-
end
|
398
|
-
|
399
|
-
connection.unsafe_once :connected, :closed, :failed, &state_changed_callback
|
402
|
+
connection.once_state_changed { @renewing_token = false }
|
400
403
|
|
401
404
|
if token_details && !token_details.expired?
|
402
405
|
connection.connect
|
@@ -406,6 +409,7 @@ module Ably::Realtime
|
|
406
409
|
end
|
407
410
|
|
408
411
|
authorise_deferrable.errback do |auth_error|
|
412
|
+
@renewing_token = false
|
409
413
|
logger.error "ConnectionManager: Error authorising following token expiry: #{auth_error}"
|
410
414
|
connection.transition_state_machine :failed, reason: auth_error
|
411
415
|
end
|
@@ -428,7 +432,7 @@ module Ably::Realtime
|
|
428
432
|
end
|
429
433
|
end
|
430
434
|
|
431
|
-
def
|
435
|
+
def can_retry_connection?
|
432
436
|
!@renewing_token
|
433
437
|
end
|
434
438
|
|
@@ -60,6 +60,14 @@ module Ably
|
|
60
60
|
# Expected format for a connection recover key
|
61
61
|
RECOVER_REGEX = /^(?<recover>[\w-]+):(?<connection_serial>\-?\w+)$/
|
62
62
|
|
63
|
+
# Defaults for automatic connection recovery and timeouts
|
64
|
+
DEFAULTS = {
|
65
|
+
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
|
66
|
+
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
|
67
|
+
connection_state_ttl: 60, # the duration that Ably will persist the connection state when a Realtime client is abruptly disconnected
|
68
|
+
realtime_request_timeout: 10 # default timeout when establishing a connection, or sending a HEARTBEAT, CONNECT, ATTACH, DETACH or CLOSE ProtocolMessage
|
69
|
+
}.freeze
|
70
|
+
|
63
71
|
# A unique public identifier for this connection, used to identify this member in presence events and messages
|
64
72
|
# @return [String]
|
65
73
|
attr_reader :id
|
@@ -90,23 +98,35 @@ module Ably
|
|
90
98
|
# @api private
|
91
99
|
attr_reader :manager
|
92
100
|
|
93
|
-
# An internal queue used to manage unsent outgoing messages.
|
101
|
+
# An internal queue used to manage unsent outgoing messages. You should never interface with this array directly
|
94
102
|
# @return [Array]
|
95
103
|
# @api private
|
96
104
|
attr_reader :__outgoing_message_queue__
|
97
105
|
|
98
|
-
# An internal queue used to manage sent messages.
|
106
|
+
# An internal queue used to manage sent messages. You should never interface with this array directly
|
99
107
|
# @return [Array]
|
100
108
|
# @api private
|
101
109
|
attr_reader :__pending_message_ack_queue__
|
102
110
|
|
111
|
+
# Configured recovery and timeout defaults for this {Connection}.
|
112
|
+
# See the configurable options in {Ably::Realtime::Client#initialize}.
|
113
|
+
# The defaults are immutable
|
114
|
+
# @return [Hash]
|
115
|
+
attr_reader :defaults
|
116
|
+
|
103
117
|
# @api public
|
104
|
-
def initialize(client)
|
118
|
+
def initialize(client, options)
|
105
119
|
@client = client
|
106
120
|
@client_serial = -1
|
107
121
|
@__outgoing_message_queue__ = []
|
108
122
|
@__pending_message_ack_queue__ = []
|
109
123
|
|
124
|
+
@defaults = DEFAULTS.dup
|
125
|
+
options.each do |key, val|
|
126
|
+
@defaults[key] = val if DEFAULTS.has_key?(key)
|
127
|
+
end if options.kind_of?(Hash)
|
128
|
+
@defaults.freeze
|
129
|
+
|
110
130
|
Client::IncomingMessageDispatcher.new client, self
|
111
131
|
Client::OutgoingMessageDispatcher.new client, self
|
112
132
|
|
@@ -133,6 +153,11 @@ module Ably
|
|
133
153
|
|
134
154
|
# Causes the library to attempt connection. If it was previously explicitly
|
135
155
|
# closed by the user, or was closed as a result of an unrecoverable error, a new connection will be opened.
|
156
|
+
# Succeeds when connection is established i.e. state is @Connected@
|
157
|
+
# Fails when state becomes either @Closing@, @Closed@ or @Failed@
|
158
|
+
#
|
159
|
+
# Note that if the connection remains in the disconnected ans suspended states indefinitely,
|
160
|
+
# the Deferrable or block provided may never be called
|
136
161
|
#
|
137
162
|
# @yield block is called as soon as this connection is in the Connected state
|
138
163
|
#
|
@@ -143,14 +168,32 @@ module Ably
|
|
143
168
|
raise exception_for_state_change_to(:connecting) unless can_transition_to?(:connecting)
|
144
169
|
transition_state_machine :connecting
|
145
170
|
end
|
146
|
-
|
171
|
+
|
172
|
+
Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
|
173
|
+
deferrable.callback do
|
174
|
+
yield if block_given?
|
175
|
+
end
|
176
|
+
succeed_callback = deferrable.method(:succeed)
|
177
|
+
fail_callback = deferrable.method(:fail)
|
178
|
+
|
179
|
+
once(:connected) do
|
180
|
+
deferrable.succeed
|
181
|
+
off &fail_callback
|
182
|
+
end
|
183
|
+
|
184
|
+
once(:failed, :closed, :closing) do
|
185
|
+
deferrable.fail
|
186
|
+
off &succeed_callback
|
187
|
+
end
|
188
|
+
end
|
147
189
|
end
|
148
190
|
|
149
191
|
# Sends a ping to Ably and yields the provided block when a heartbeat ping request is echoed from the server.
|
150
192
|
# This can be useful for measuring true roundtrip client to Ably server latency for a simple message, or checking that an underlying transport is responding currently.
|
151
193
|
# The elapsed milliseconds is passed as an argument to the block and represents the time taken to echo a ping heartbeat once the connection is in the `:connected` state.
|
152
194
|
#
|
153
|
-
# @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 milliseconds
|
195
|
+
# @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 milliseconds.
|
196
|
+
# If the ping is not received within an acceptable timeframe, the block will be called with +nil+ as he first argument
|
154
197
|
#
|
155
198
|
# @example
|
156
199
|
# client = Ably::Rest::Client.new(key: 'key.id:secret')
|
@@ -165,9 +208,12 @@ module Ably
|
|
165
208
|
raise RuntimeError, 'Cannot send a ping when connection is in a closed or failed state' if closed? || failed?
|
166
209
|
|
167
210
|
started = nil
|
211
|
+
finished = false
|
168
212
|
|
169
213
|
wait_for_ping = Proc.new do |protocol_message|
|
214
|
+
next if finished
|
170
215
|
if protocol_message.action == Ably::Models::ProtocolMessage::ACTION.Heartbeat
|
216
|
+
finished = true
|
171
217
|
__incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
|
172
218
|
time_passed = (Time.now.to_f * 1000 - started.to_f * 1000).to_i
|
173
219
|
safe_yield block, time_passed if block_given?
|
@@ -175,10 +221,19 @@ module Ably
|
|
175
221
|
end
|
176
222
|
|
177
223
|
once_or_if(STATE.Connected) do
|
224
|
+
next if finished
|
178
225
|
started = Time.now
|
179
226
|
send_protocol_message action: Ably::Models::ProtocolMessage::ACTION.Heartbeat.to_i
|
180
227
|
__incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping
|
181
228
|
end
|
229
|
+
|
230
|
+
EventMachine.add_timer(defaults.fetch(:realtime_request_timeout)) do
|
231
|
+
next if finished
|
232
|
+
finished = true
|
233
|
+
__incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
|
234
|
+
logger.warn "Ping timed out after #{defaults.fetch(:realtime_request_timeout)}s"
|
235
|
+
safe_yield block, nil if block_given?
|
236
|
+
end
|
182
237
|
end
|
183
238
|
|
184
239
|
# @yield [Boolean] True if an internet connection check appears to be up following an HTTP request to a reliable CDN
|
@@ -327,11 +382,12 @@ module Ably
|
|
327
382
|
client.auth.auth_params.tap do |auth_deferrable|
|
328
383
|
auth_deferrable.callback do |auth_params|
|
329
384
|
url_params = auth_params.merge(
|
330
|
-
timestamp: as_since_epoch(Time.now),
|
331
385
|
format: client.protocol,
|
332
386
|
echo: client.echo_messages
|
333
387
|
)
|
334
388
|
|
389
|
+
url_params['clientId'] = client.auth.client_id if client.auth.has_client_id?
|
390
|
+
|
335
391
|
if connection_resumable?
|
336
392
|
url_params.merge! resume: key, connection_serial: serial
|
337
393
|
logger.debug "Resuming connection key #{key} with serial #{serial}"
|
@@ -74,8 +74,8 @@ module Ably::Realtime
|
|
74
74
|
data = options.fetch(:data, nil)
|
75
75
|
deferrable = create_deferrable
|
76
76
|
|
77
|
+
ensure_supported_client_id client_id
|
77
78
|
ensure_supported_payload data unless data.nil?
|
78
|
-
raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
|
79
79
|
|
80
80
|
@data = data
|
81
81
|
@client_id = client_id
|
@@ -120,8 +120,8 @@ module Ably::Realtime
|
|
120
120
|
#
|
121
121
|
def enter_client(client_id, options = {}, &success_block)
|
122
122
|
raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
|
123
|
+
ensure_supported_client_id client_id
|
123
124
|
ensure_supported_payload options[:data] if options.has_key?(:data)
|
124
|
-
raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
|
125
125
|
|
126
126
|
send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Enter, client_id, options, &success_block)
|
127
127
|
end
|
@@ -139,6 +139,7 @@ module Ably::Realtime
|
|
139
139
|
data = options.fetch(:data, self.data) # nil value defaults leave data to existing value
|
140
140
|
deferrable = create_deferrable
|
141
141
|
|
142
|
+
ensure_supported_client_id client_id
|
142
143
|
ensure_supported_payload data unless data.nil?
|
143
144
|
raise Ably::Exceptions::Standard.new('Unable to leave presence channel that is not entered', 400, 91002) unless able_to_leave?
|
144
145
|
|
@@ -178,8 +179,8 @@ module Ably::Realtime
|
|
178
179
|
#
|
179
180
|
def leave_client(client_id, options = {}, &success_block)
|
180
181
|
raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
|
182
|
+
ensure_supported_client_id client_id
|
181
183
|
ensure_supported_payload options[:data] if options.has_key?(:data)
|
182
|
-
raise Ably::Exceptions::Standard.new('Unable to leave presence channel without a client_id', 400, 91000) unless client_id
|
183
184
|
|
184
185
|
send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Leave, client_id, options, &success_block)
|
185
186
|
end
|
@@ -198,8 +199,8 @@ module Ably::Realtime
|
|
198
199
|
data = options.fetch(:data, nil)
|
199
200
|
deferrable = create_deferrable
|
200
201
|
|
202
|
+
ensure_supported_client_id client_id
|
201
203
|
ensure_supported_payload data unless data.nil?
|
202
|
-
raise Ably::Exceptions::Standard.new('Unable to update presence channel without a client_id', 400, 91000) unless client_id
|
203
204
|
|
204
205
|
@data = data
|
205
206
|
|
@@ -230,8 +231,8 @@ module Ably::Realtime
|
|
230
231
|
#
|
231
232
|
def update_client(client_id, options = {}, &success_block)
|
232
233
|
raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
|
234
|
+
ensure_supported_client_id client_id
|
233
235
|
ensure_supported_payload options[:data] if options.has_key?(:data)
|
234
|
-
raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
|
235
236
|
|
236
237
|
send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Update, client_id, options, &success_block)
|
237
238
|
end
|
@@ -387,6 +388,18 @@ module Ably::Realtime
|
|
387
388
|
deferrable
|
388
389
|
end
|
389
390
|
|
391
|
+
def ensure_supported_client_id(check_client_id)
|
392
|
+
unless check_client_id
|
393
|
+
raise Ably::Exceptions::IncompatibleClientId.new('Unable to enter/update/leave presence channel without a client_id', 400, 40012)
|
394
|
+
end
|
395
|
+
if check_client_id == '*'
|
396
|
+
raise Ably::Exceptions::IncompatibleClientId.new('Unable to enter/update/leave presence channel with the reserved wildcard client_id', 400, 40012)
|
397
|
+
end
|
398
|
+
unless client.auth.can_assume_client_id?(check_client_id)
|
399
|
+
raise Ably::Exceptions::IncompatibleClientId.new("Cannot enter with provided client_id '#{check_client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'", 400, 40012)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
390
403
|
def send_protocol_message_and_transition_state_to(action, options = {}, &success_block)
|
391
404
|
deferrable = options.fetch(:deferrable) { raise ArgumentError, 'option :deferrable is required' }
|
392
405
|
client_id = options.fetch(:client_id) { raise ArgumentError, 'option :client_id is required' }
|
data/lib/ably/rest/channel.rb
CHANGED
@@ -67,6 +67,14 @@ module Ably
|
|
67
67
|
payload = messages.map do |message|
|
68
68
|
Ably::Models::Message(message.dup).tap do |message|
|
69
69
|
message.encode self
|
70
|
+
|
71
|
+
next if message.client_id.nil?
|
72
|
+
if message.client_id == '*'
|
73
|
+
raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages', 400, 40012)
|
74
|
+
end
|
75
|
+
unless client.auth.can_assume_client_id?(message.client_id)
|
76
|
+
raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{message.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'", 400, 40012)
|
77
|
+
end
|
70
78
|
end.as_json
|
71
79
|
end
|
72
80
|
|
@@ -123,7 +131,7 @@ module Ably
|
|
123
131
|
|
124
132
|
private
|
125
133
|
def base_path
|
126
|
-
"/channels/#{
|
134
|
+
"/channels/#{Addressable::URI.encode(name)}"
|
127
135
|
end
|
128
136
|
|
129
137
|
def decode_message(message)
|