ably 0.8.2 → 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +185 -0
  4. data/LICENSE +15 -0
  5. data/README.md +8 -4
  6. data/SPEC.md +999 -531
  7. data/ably.gemspec +1 -1
  8. data/lib/ably.rb +1 -1
  9. data/lib/ably/auth.rb +114 -87
  10. data/lib/ably/exceptions.rb +40 -14
  11. data/lib/ably/models/message.rb +3 -5
  12. data/lib/ably/models/paginated_result.rb +3 -12
  13. data/lib/ably/models/presence_message.rb +8 -2
  14. data/lib/ably/models/protocol_message.rb +15 -3
  15. data/lib/ably/models/stat.rb +1 -1
  16. data/lib/ably/models/token_details.rb +1 -1
  17. data/lib/ably/modules/channels_collection.rb +7 -1
  18. data/lib/ably/modules/conversions.rb +1 -1
  19. data/lib/ably/modules/encodeable.rb +6 -3
  20. data/lib/ably/modules/message_pack.rb +2 -2
  21. data/lib/ably/modules/model_common.rb +1 -1
  22. data/lib/ably/modules/state_machine.rb +2 -2
  23. data/lib/ably/realtime.rb +1 -0
  24. data/lib/ably/realtime/auth.rb +191 -0
  25. data/lib/ably/realtime/channel.rb +97 -25
  26. data/lib/ably/realtime/channel/channel_manager.rb +11 -3
  27. data/lib/ably/realtime/client.rb +22 -6
  28. data/lib/ably/realtime/connection.rb +74 -41
  29. data/lib/ably/realtime/connection/connection_manager.rb +48 -33
  30. data/lib/ably/realtime/presence.rb +17 -3
  31. data/lib/ably/rest/channel.rb +43 -16
  32. data/lib/ably/rest/client.rb +57 -26
  33. data/lib/ably/rest/middleware/exceptions.rb +3 -1
  34. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -2
  35. data/lib/ably/rest/presence.rb +1 -0
  36. data/lib/ably/version.rb +1 -1
  37. data/spec/acceptance/realtime/auth_spec.rb +242 -0
  38. data/spec/acceptance/realtime/channel_spec.rb +277 -5
  39. data/spec/acceptance/realtime/channels_spec.rb +64 -0
  40. data/spec/acceptance/realtime/client_spec.rb +26 -5
  41. data/spec/acceptance/realtime/connection_failures_spec.rb +23 -6
  42. data/spec/acceptance/realtime/connection_spec.rb +167 -16
  43. data/spec/acceptance/realtime/message_spec.rb +9 -8
  44. data/spec/acceptance/realtime/presence_history_spec.rb +1 -0
  45. data/spec/acceptance/realtime/presence_spec.rb +121 -10
  46. data/spec/acceptance/realtime/stats_spec.rb +13 -1
  47. data/spec/acceptance/rest/auth_spec.rb +161 -79
  48. data/spec/acceptance/rest/base_spec.rb +3 -3
  49. data/spec/acceptance/rest/channel_spec.rb +142 -15
  50. data/spec/acceptance/rest/channels_spec.rb +23 -0
  51. data/spec/acceptance/rest/client_spec.rb +180 -18
  52. data/spec/acceptance/rest/message_spec.rb +8 -8
  53. data/spec/acceptance/rest/presence_spec.rb +136 -25
  54. data/spec/acceptance/rest/stats_spec.rb +60 -4
  55. data/spec/shared/client_initializer_behaviour.rb +54 -3
  56. data/spec/unit/auth_spec.rb +7 -6
  57. data/spec/unit/models/message_spec.rb +1 -9
  58. data/spec/unit/models/paginated_result_spec.rb +1 -18
  59. data/spec/unit/models/presence_message_spec.rb +1 -1
  60. data/spec/unit/models/protocol_message_spec.rb +21 -1
  61. data/spec/unit/realtime/channel_spec.rb +10 -3
  62. data/spec/unit/realtime/channels_spec.rb +27 -8
  63. data/spec/unit/rest/channel_spec.rb +0 -8
  64. data/spec/unit/rest/client_spec.rb +7 -7
  65. metadata +13 -7
  66. data/LICENSE.txt +0 -22
@@ -26,7 +26,7 @@ module Ably::Realtime
26
26
 
27
27
  # Commence attachment
28
28
  def detach(error = nil)
29
- if connection.closed? || connection.connecting?
29
+ if connection.closed? || connection.connecting? || connection.suspended?
30
30
  channel.transition_state_machine :detached, error
31
31
  elsif can_transition_to?(:detached)
32
32
  send_detach_protocol_message
@@ -59,7 +59,7 @@ module Ably::Realtime
59
59
  def fail_messages_awaiting_ack(error)
60
60
  # Allow a short time for other queued operations to complete before failing all messages
61
61
  EventMachine.add_timer(0.1) do
62
- error = Ably::Exceptions::MessageDeliveryError.new('Channel is no longer in a state suitable to deliver this message to the server') unless error
62
+ error = Ably::Exceptions::MessageDeliveryFailed.new("Channel cannot publish messages whilst state is '#{channel.state}'") unless error
63
63
  fail_messages_in_queue connection.__pending_message_ack_queue__, error
64
64
  fail_messages_in_queue connection.__outgoing_message_queue__, error
65
65
  end
@@ -123,8 +123,16 @@ module Ably::Realtime
123
123
  channel.transition_state_machine :detaching if can_transition_to?(:detaching)
124
124
  end
125
125
 
126
+ connection.unsafe_on(:suspended) do |error|
127
+ if can_transition_to?(:detaching)
128
+ channel.transition_state_machine :detaching, Ably::Exceptions::ConnectionSuspended.new('Connection suspended', nil, 80002, error)
129
+ end
130
+ end
131
+
126
132
  connection.unsafe_on(:failed) do |error|
127
- channel.transition_state_machine :failed, error if can_transition_to?(:failed)
133
+ if can_transition_to?(:failed)
134
+ channel.transition_state_machine :failed, Ably::Exceptions::ConnectionFailed.new('Connection failed', nil, 80002, error)
135
+ end
128
136
  end
129
137
  end
130
138
 
@@ -34,10 +34,14 @@ module Ably
34
34
  # @return [Ably::Rest::Client]
35
35
  attr_reader :rest_client
36
36
 
37
- # When false the client suppresses messages originating from this connection being echoed back on the same connection. Defaults to true
37
+ # When false the client suppresses messages originating from this connection being echoed back on the same connection. Defaults to true
38
38
  # @return [Boolean]
39
39
  attr_reader :echo_messages
40
40
 
41
+ # If false, this disables the default behaviour whereby the library queues messages on a connection in the disconnected or connecting states. Defaults to true
42
+ # @return [Boolean]
43
+ attr_reader :queue_messages
44
+
41
45
  # The custom realtime websocket host that is being used if it was provided with the option `:ws_host` when the {Client} was created
42
46
  # @return [String,Nil]
43
47
  attr_reader :custom_realtime_host
@@ -52,7 +56,8 @@ module Ably
52
56
 
53
57
  def_delegators :auth, :client_id, :auth_options
54
58
  def_delegators :@rest_client, :encoders
55
- def_delegators :@rest_client, :environment, :use_tls?, :protocol, :protocol_binary?, :custom_host
59
+ def_delegators :@rest_client, :use_tls?, :protocol, :protocol_binary?
60
+ def_delegators :@rest_client, :environment, :custom_host, :custom_port, :custom_tls_port
56
61
  def_delegators :@rest_client, :log_level
57
62
 
58
63
  # Creates a {Ably::Realtime::Client Realtime Client} and configures the {Ably::Auth} object for the connection.
@@ -75,9 +80,10 @@ module Ably
75
80
  #
76
81
  def initialize(options)
77
82
  @rest_client = Ably::Rest::Client.new(options)
78
- @auth = @rest_client.auth
83
+ @auth = Ably::Realtime::Auth.new(self)
79
84
  @channels = Ably::Realtime::Channels.new(self)
80
85
  @echo_messages = @rest_client.options.fetch(:echo_messages, true) == false ? false : true
86
+ @queue_messages = @rest_client.options.fetch(:queue_messages, true) == false ? false : true
81
87
  @custom_realtime_host = @rest_client.options[:realtime_host] || @rest_client.options[:ws_host]
82
88
  @auto_connect = @rest_client.options.fetch(:auto_connect, true) == false ? false : true
83
89
  @recover = @rest_client.options[:recover]
@@ -174,12 +180,22 @@ module Ably
174
180
 
175
181
  private
176
182
  def endpoint_for_host(host)
177
- URI::Generic.build(
183
+ port = if use_tls?
184
+ custom_tls_port
185
+ else
186
+ custom_port
187
+ end
188
+
189
+ raise ArgumentError, "Custom port must be an Integer or nil" if port && !port.kind_of?(Integer)
190
+
191
+ options = {
178
192
  scheme: use_tls? ? 'wss' : 'ws',
179
193
  host: host
180
- )
194
+ }
195
+ options.merge!(port: port) if port
196
+
197
+ URI::Generic.build(options)
181
198
  end
182
199
  end
183
200
  end
184
201
  end
185
-
@@ -184,23 +184,30 @@ module Ably
184
184
  # @return [EventMachine::Deferrable]
185
185
  # @api private
186
186
  def internet_up?
187
+ url = "http#{'s' if client.use_tls?}:#{Ably::INTERNET_CHECK.fetch(:url)}"
187
188
  EventMachine::DefaultDeferrable.new.tap do |deferrable|
188
- EventMachine::HttpRequest.new("http#{'s' if client.use_tls?}:#{Ably::INTERNET_CHECK.fetch(:url)}").get.tap do |http|
189
+ EventMachine::HttpRequest.new(url).get.tap do |http|
189
190
  http.errback do
190
191
  yield false if block_given?
191
- deferrable.fail
192
+ deferrable.fail "Unable to connect to #{url}"
192
193
  end
193
194
  http.callback do
194
- result = http.response_header.status == 200 && http.response.strip == Ably::INTERNET_CHECK.fetch(:ok_text)
195
- yield result if block_given?
196
- deferrable.succeed
195
+ EventMachine.next_tick do
196
+ result = http.response_header.status == 200 && http.response.strip == Ably::INTERNET_CHECK.fetch(:ok_text)
197
+ yield result if block_given?
198
+ if result
199
+ deferrable.succeed
200
+ else
201
+ deferrable.fail "Unexpected response from #{url} (#{http.response_header.status})"
202
+ end
203
+ end
197
204
  end
198
205
  end
199
206
  end
200
207
  end
201
208
 
202
209
  # @!attribute [r] recovery_key
203
- # @return [String] recovery key that can be used by another client to recover this connection with the :recover option
210
+ # @return [String] recovery key that can be used by another client to recover this connection with the :recover option
204
211
  def recovery_key
205
212
  "#{key}:#{serial}" if connection_resumable?
206
213
  end
@@ -275,7 +282,7 @@ module Ably
275
282
  # @!attribute [r] port
276
283
  # @return [Integer] The default port used for this connection
277
284
  def port
278
- client.use_tls? ? 443 : 80
285
+ client.use_tls? ? client.custom_tls_port || 443 : client.custom_port || 80
279
286
  end
280
287
 
281
288
  # @!attribute [r] logger
@@ -311,48 +318,51 @@ module Ably
311
318
  __outgoing_protocol_msgbus__.publish :protocol_message, protocol_message
312
319
  end
313
320
 
321
+ # @return [EventMachine::Deferrable]
314
322
  # @api private
315
323
  def create_websocket_transport
316
- raise ArgumentError, 'Block required' unless block_given?
324
+ EventMachine::DefaultDeferrable.new.tap do |websocket_deferrable|
325
+ # Getting auth params can be blocking so uses a Deferrable
326
+ client.auth.auth_params.tap do |auth_deferrable|
327
+ auth_deferrable.callback do |auth_params|
328
+ url_params = auth_params.merge(
329
+ timestamp: as_since_epoch(Time.now),
330
+ format: client.protocol,
331
+ echo: client.echo_messages
332
+ )
333
+
334
+ if connection_resumable?
335
+ url_params.merge! resume: key, connection_serial: serial
336
+ logger.debug "Resuming connection key #{key} with serial #{serial}"
337
+ elsif connection_recoverable?
338
+ url_params.merge! recover: connection_recover_parts[:recover], connection_serial: connection_recover_parts[:connection_serial]
339
+ logger.debug "Recovering connection with key #{client.recover}"
340
+ once(:connected, :closed, :failed) do
341
+ client.disable_automatic_connection_recovery
342
+ end
343
+ end
317
344
 
318
- blocking_operation = proc do
319
- URI(client.endpoint).tap do |endpoint|
320
- url_params = client.auth.auth_params.merge(
321
- timestamp: as_since_epoch(Time.now),
322
- format: client.protocol,
323
- echo: client.echo_messages
324
- )
325
-
326
- if connection_resumable?
327
- url_params.merge! resume: key, connection_serial: serial
328
- logger.debug "Resuming connection key #{key} with serial #{serial}"
329
- elsif connection_recoverable?
330
- url_params.merge! recover: connection_recover_parts[:recover], connection_serial: connection_recover_parts[:connection_serial]
331
- logger.debug "Recovering connection with key #{client.recover}"
332
- once(:connected, :closed, :failed) do
333
- client.disable_automatic_connection_recovery
345
+ url = URI(client.endpoint).tap do |endpoint|
346
+ endpoint.query = URI.encode_www_form(url_params)
347
+ end.to_s
348
+
349
+ determine_host do |host|
350
+ begin
351
+ logger.debug "Connection: Opening socket connection to #{host}:#{port} and URL '#{url}'"
352
+ @transport = EventMachine.connect(host, port, WebsocketTransport, self, url) do |websocket_transport|
353
+ websocket_deferrable.succeed websocket_transport
354
+ end
355
+ rescue EventMachine::ConnectionError => error
356
+ websocket_deferrable.fail error
357
+ end
334
358
  end
335
359
  end
336
360
 
337
- endpoint.query = URI.encode_www_form(url_params)
338
- end.to_s
339
- end
340
-
341
- callback = proc do |url|
342
- determine_host do |host|
343
- begin
344
- logger.debug "Connection: Opening socket connection to #{host}:#{port} and URL '#{url}'"
345
- @transport = EventMachine.connect(host, port, WebsocketTransport, self, url) do |websocket_transport|
346
- yield websocket_transport if block_given?
347
- end
348
- rescue EventMachine::ConnectionError => error
349
- manager.connection_opening_failed error
361
+ auth_deferrable.errback do |error|
362
+ websocket_deferrable.fail error
350
363
  end
351
364
  end
352
365
  end
353
-
354
- # client.auth.auth_params is a blocking call, so defer this into a thread
355
- EventMachine.defer blocking_operation, callback
356
366
  end
357
367
 
358
368
  # @api private
@@ -388,6 +398,13 @@ module Ably
388
398
  resume_callbacks.delete(callback)
389
399
  end
390
400
 
401
+ # Returns false if messages cannot be published as a result of message queueing being disabled
402
+ # @api private
403
+ def can_publish_messages?
404
+ connected? ||
405
+ ( (initialized? || connecting? || disconnected?) && client.queue_messages )
406
+ end
407
+
391
408
  # As we are using a state machine, do not allow change_state to be used
392
409
  # #transition_state_machine must be used instead
393
410
  private :change_state
@@ -451,8 +468,24 @@ module Ably
451
468
  client.recover.to_s.match(RECOVER_REGEX)
452
469
  end
453
470
 
471
+ def production?
472
+ client.environment.nil? || client.environment == :production
473
+ end
474
+
475
+ def custom_port?
476
+ if client.use_tls?
477
+ !!client.custom_tls_port
478
+ else
479
+ !!client.custom_port
480
+ end
481
+ end
482
+
483
+ def custom_host?
484
+ !!client.custom_realtime_host
485
+ end
486
+
454
487
  def can_use_fallback_hosts?
455
- if client.environment.nil? && client.custom_realtime_host.nil?
488
+ if production? && !custom_port? && !custom_host?
456
489
  if connecting? && previous_state
457
490
  use_fallback_if_disconnected? || use_fallback_if_suspended?
458
491
  end
@@ -53,20 +53,25 @@ module Ably::Realtime
53
53
  end
54
54
 
55
55
  unless client.auth.authentication_security_requirements_met?
56
- connection.transition_state_machine :failed, Ably::Exceptions::InsecureRequestError.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
56
+ connection.transition_state_machine :failed, Ably::Exceptions::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
57
57
  return
58
58
  end
59
59
 
60
60
  logger.debug 'ConnectionManager: Opening a websocket transport connection'
61
61
 
62
- connection.create_websocket_transport do |websocket_transport|
63
- subscribe_to_transport_events websocket_transport
64
- yield websocket_transport if block_given?
62
+ connection.create_websocket_transport.tap do |socket_deferrable|
63
+ socket_deferrable.callback do |websocket_transport|
64
+ subscribe_to_transport_events websocket_transport
65
+ yield websocket_transport if block_given?
66
+ end
67
+ socket_deferrable.errback do |error|
68
+ connection_opening_failed error
69
+ end
65
70
  end
66
71
 
67
72
  logger.debug "ConnectionManager: Setting up automatic connection timeout timer for #{TIMEOUTS.fetch(:open)}s"
68
73
  create_timeout_timer_whilst_in_state(:connect, TIMEOUTS.fetch(:open)) do
69
- connection_opening_failed Ably::Exceptions::ConnectionTimeoutError.new("Connection to Ably timed out after #{TIMEOUTS.fetch(:open)}s", nil, 80014)
74
+ connection_opening_failed Ably::Exceptions::ConnectionTimeout.new("Connection to Ably timed out after #{TIMEOUTS.fetch(:open)}s", nil, 80014)
70
75
  end
71
76
  end
72
77
 
@@ -161,10 +166,10 @@ module Ably::Realtime
161
166
  end
162
167
 
163
168
  unless connection_retry_from_suspended_state?
164
- return if connection_retry_for(:disconnected, ignore_states: [:connecting])
169
+ return if connection_retry_for(:disconnected)
165
170
  end
166
171
 
167
- return if connection_retry_for(:suspended, ignore_states: [:connecting])
172
+ return if connection_retry_for(:suspended)
168
173
 
169
174
  # Fallback if no other criteria met
170
175
  connection.transition_state_machine :failed, current_transition.metadata
@@ -264,10 +269,10 @@ module Ably::Realtime
264
269
  #
265
270
  # @return [Boolean] True if a connection attempt has been set up, false if no further connection attempts can be made for this state
266
271
  #
267
- def connection_retry_for(from_state, options = {})
272
+ def connection_retry_for(from_state)
268
273
  retry_params = CONNECT_RETRY_CONFIG.fetch(from_state)
269
274
 
270
- if time_spent_attempting_state(from_state, options) < retry_params.fetch(:max_time_in_state)
275
+ if can_reattempt_connect_for_state?(from_state)
271
276
  if retries_for_state(from_state, ignore_states: [:connecting]).empty?
272
277
  logger.debug "ConnectionManager: Will attempt reconnect immediately as no previous reconnect attempts made in this state"
273
278
  EventMachine.next_tick { connection.connect }
@@ -281,6 +286,14 @@ module Ably::Realtime
281
286
  end
282
287
  end
283
288
 
289
+ # True if the client library has not exceeded the configured max_time_in_state for the current State
290
+ # For example, if the state is disconnected, and has been in a cycle of disconnected > connect > disconnected
291
+ # so long as the time in this cycle of states is less than max_time_in_state, this will return true
292
+ def can_reattempt_connect_for_state?(state)
293
+ retry_params = CONNECT_RETRY_CONFIG.fetch(state)
294
+ time_spent_attempting_state(state, ignore_states: [:connecting]) < retry_params.fetch(:max_time_in_state)
295
+ end
296
+
284
297
  # Returns a float representing the amount of time passed since the first consecutive attempt of this state
285
298
  #
286
299
  # @param (see #retries_for_state)
@@ -335,9 +348,13 @@ module Ably::Realtime
335
348
  connection.transition_state_machine :closed
336
349
  elsif !connection.closed? && !connection.disconnected?
337
350
  exception = if reason
338
- Ably::Exceptions::ConnectionClosedError.new(reason)
351
+ Ably::Exceptions::ConnectionClosed.new(reason)
352
+ end
353
+ if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
354
+ connection.transition_state_machine :suspended, exception
355
+ else
356
+ connection.transition_state_machine :disconnected, exception
339
357
  end
340
- connection.transition_state_machine :disconnected, exception
341
358
  end
342
359
  end
343
360
  end
@@ -350,35 +367,33 @@ module Ably::Realtime
350
367
  end
351
368
 
352
369
  @renewing_token = true
353
- logger.warn "ConnectionManager: Token has expired and is renewable, renewing token now"
370
+ logger.info "ConnectionManager: Token has expired and is renewable, renewing token now"
354
371
 
355
- operation = proc do
356
- begin
357
- client.auth.authorise
358
- rescue StandardError => auth_error
359
- connection.transition_state_machine :failed, auth_error
360
- nil
361
- end
362
- end
372
+ client.auth.authorise.tap do |authorise_deferrable|
373
+ authorise_deferrable.callback do |token_details|
374
+ logger.info 'ConnectionManager: Token renewed succesfully following expiration'
363
375
 
364
- callback = proc do |token|
365
- state_changed_callback = proc do
366
- @renewing_token = false
367
- connection.off &state_changed_callback
368
- end
376
+ state_changed_callback = proc do
377
+ @renewing_token = false
378
+ connection.off &state_changed_callback
379
+ end
369
380
 
370
- connection.unsafe_once :connected, :closed, :failed, &state_changed_callback
381
+ connection.unsafe_once :connected, :closed, :failed, &state_changed_callback
371
382
 
372
- if token && !token.expired?
373
- connection.connect
374
- else
375
- connection.transition_state_machine :failed, error unless connection.failed?
383
+ if token_details && !token_details.expired?
384
+ connection.connect
385
+ else
386
+ connection.transition_state_machine :failed, error unless connection.failed?
387
+ end
376
388
  end
377
- end
378
389
 
379
- EventMachine.defer operation, callback
390
+ authorise_deferrable.errback do |auth_error|
391
+ logger.error "ConnectionManager: Error authorising following token expiry: #{auth_error}"
392
+ connection.transition_state_machine :failed, auth_error
393
+ end
394
+ end
380
395
  else
381
- logger.error "ConnectionManager: Token has expired and is not renewable"
396
+ logger.error "ConnectionManager: Token has expired and is not renewable - #{error}"
382
397
  connection.transition_state_machine :failed, error
383
398
  end
384
399
  end
@@ -82,6 +82,7 @@ module Ably::Realtime
82
82
 
83
83
  return deferrable_succeed(deferrable, &success_block) if state == STATE.Entered
84
84
 
85
+ ensure_presence_publishable_on_connection
85
86
  ensure_channel_attached(deferrable) do
86
87
  if entering?
87
88
  once_or_if(STATE.Entered, else: proc { |args| deferrable_fail deferrable, *args }) do
@@ -145,6 +146,7 @@ module Ably::Realtime
145
146
 
146
147
  return deferrable_succeed(deferrable, &success_block) if state == STATE.Left
147
148
 
149
+ ensure_presence_publishable_on_connection
148
150
  ensure_channel_attached(deferrable) do
149
151
  if leaving?
150
152
  once_or_if(STATE.Left, else: proc { |error|deferrable_fail deferrable, *args }) do
@@ -201,6 +203,7 @@ module Ably::Realtime
201
203
 
202
204
  @data = data
203
205
 
206
+ ensure_presence_publishable_on_connection
204
207
  ensure_channel_attached(deferrable) do
205
208
  send_protocol_message_and_transition_state_to(
206
209
  Ably::Models::PresenceMessage::ACTION.Update,
@@ -369,6 +372,12 @@ module Ably::Realtime
369
372
  end
370
373
  end
371
374
 
375
+ def ensure_presence_publishable_on_connection
376
+ if !connection.can_publish_messages?
377
+ raise Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. Client is configured to disallow queueing of messages and connection is currently #{connection.state}")
378
+ end
379
+ end
380
+
372
381
  def ensure_channel_attached(deferrable = nil)
373
382
  if channel.attached?
374
383
  yield
@@ -413,11 +422,12 @@ module Ably::Realtime
413
422
  safe_yield block, self, *args if block_given?
414
423
  EventMachine.next_tick { deferrable.fail self, *args } # allow errback to be added to the returned Deferrable
415
424
  deferrable
416
- end
425
+ end
417
426
 
418
427
  def send_presence_action_for_client(action, client_id, options = {}, &success_block)
419
- deferrable = create_deferrable
428
+ ensure_presence_publishable_on_connection
420
429
 
430
+ deferrable = create_deferrable
421
431
  ensure_channel_attached(deferrable) do
422
432
  send_presence_protocol_message(action, client_id, options).tap do |protocol_message|
423
433
  protocol_message.callback { |message| deferrable_succeed deferrable, &success_block }
@@ -428,7 +438,7 @@ module Ably::Realtime
428
438
 
429
439
  def attach_channel_then
430
440
  if channel.detached? || channel.failed?
431
- raise Ably::Exceptions::IncompatibleStateForOperation.new("Operation is not allowed when channel is in #{channel.state}", 400, 91001)
441
+ raise Ably::Exceptions::InvalidStateChange.new("Operation is not allowed when channel is in #{channel.state}", 400, 91001)
432
442
  else
433
443
  channel.unsafe_once(Channel::STATE.Attached) { yield }
434
444
  channel.attach
@@ -439,6 +449,10 @@ module Ably::Realtime
439
449
  channel.client
440
450
  end
441
451
 
452
+ def connection
453
+ client.connection
454
+ end
455
+
442
456
  def rest_presence
443
457
  client.rest_client.channel(channel.name).presence
444
458
  end