ably 0.8.15 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -4
  3. data/CHANGELOG.md +6 -2
  4. data/README.md +5 -1
  5. data/SPEC.md +1473 -852
  6. data/ably.gemspec +11 -8
  7. data/lib/ably/auth.rb +90 -53
  8. data/lib/ably/exceptions.rb +37 -8
  9. data/lib/ably/logger.rb +10 -1
  10. data/lib/ably/models/auth_details.rb +42 -0
  11. data/lib/ably/models/channel_state_change.rb +18 -4
  12. data/lib/ably/models/connection_details.rb +6 -3
  13. data/lib/ably/models/connection_state_change.rb +4 -3
  14. data/lib/ably/models/error_info.rb +1 -1
  15. data/lib/ably/models/message.rb +17 -1
  16. data/lib/ably/models/message_encoders/base.rb +103 -82
  17. data/lib/ably/models/message_encoders/base64.rb +1 -1
  18. data/lib/ably/models/presence_message.rb +16 -1
  19. data/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/ably/models/token_details.rb +11 -1
  21. data/lib/ably/models/token_request.rb +16 -6
  22. data/lib/ably/modules/async_wrapper.rb +7 -3
  23. data/lib/ably/modules/encodeable.rb +51 -12
  24. data/lib/ably/modules/enum.rb +17 -7
  25. data/lib/ably/modules/event_emitter.rb +29 -14
  26. data/lib/ably/modules/model_common.rb +13 -21
  27. data/lib/ably/modules/state_emitter.rb +7 -4
  28. data/lib/ably/modules/state_machine.rb +2 -4
  29. data/lib/ably/modules/uses_state_machine.rb +7 -3
  30. data/lib/ably/realtime.rb +2 -0
  31. data/lib/ably/realtime/auth.rb +102 -42
  32. data/lib/ably/realtime/channel.rb +68 -26
  33. data/lib/ably/realtime/channel/channel_manager.rb +154 -65
  34. data/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  35. data/lib/ably/realtime/client.rb +18 -3
  36. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  37. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  38. data/lib/ably/realtime/connection.rb +108 -49
  39. data/lib/ably/realtime/connection/connection_manager.rb +167 -61
  40. data/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  41. data/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  42. data/lib/ably/realtime/presence.rb +70 -45
  43. data/lib/ably/realtime/presence/members_map.rb +201 -36
  44. data/lib/ably/realtime/presence/presence_manager.rb +30 -6
  45. data/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  46. data/lib/ably/rest.rb +2 -2
  47. data/lib/ably/rest/channel.rb +5 -5
  48. data/lib/ably/rest/client.rb +31 -27
  49. data/lib/ably/rest/middleware/exceptions.rb +1 -3
  50. data/lib/ably/rest/middleware/logger.rb +2 -2
  51. data/lib/ably/rest/presence.rb +2 -2
  52. data/lib/ably/util/pub_sub.rb +1 -1
  53. data/lib/ably/util/safe_deferrable.rb +26 -0
  54. data/lib/ably/version.rb +2 -2
  55. data/spec/acceptance/realtime/auth_spec.rb +470 -111
  56. data/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  57. data/spec/acceptance/realtime/channel_spec.rb +1017 -168
  58. data/spec/acceptance/realtime/client_spec.rb +6 -6
  59. data/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  60. data/spec/acceptance/realtime/connection_spec.rb +424 -105
  61. data/spec/acceptance/realtime/message_spec.rb +52 -23
  62. data/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  63. data/spec/acceptance/realtime/presence_spec.rb +1110 -96
  64. data/spec/acceptance/rest/auth_spec.rb +222 -59
  65. data/spec/acceptance/rest/base_spec.rb +1 -1
  66. data/spec/acceptance/rest/channel_spec.rb +1 -2
  67. data/spec/acceptance/rest/client_spec.rb +104 -48
  68. data/spec/acceptance/rest/message_spec.rb +42 -15
  69. data/spec/acceptance/rest/presence_spec.rb +4 -11
  70. data/spec/rspec_config.rb +2 -1
  71. data/spec/shared/client_initializer_behaviour.rb +2 -2
  72. data/spec/shared/safe_deferrable_behaviour.rb +6 -2
  73. data/spec/spec_helper.rb +4 -2
  74. data/spec/support/debug_failure_helper.rb +20 -4
  75. data/spec/support/event_machine_helper.rb +32 -1
  76. data/spec/unit/auth_spec.rb +4 -11
  77. data/spec/unit/logger_spec.rb +28 -2
  78. data/spec/unit/models/auth_details_spec.rb +49 -0
  79. data/spec/unit/models/channel_state_change_spec.rb +23 -3
  80. data/spec/unit/models/connection_details_spec.rb +12 -1
  81. data/spec/unit/models/connection_state_change_spec.rb +15 -4
  82. data/spec/unit/models/message_encoders/base64_spec.rb +2 -1
  83. data/spec/unit/models/message_spec.rb +153 -0
  84. data/spec/unit/models/presence_message_spec.rb +192 -0
  85. data/spec/unit/models/protocol_message_spec.rb +64 -6
  86. data/spec/unit/models/token_details_spec.rb +75 -0
  87. data/spec/unit/models/token_request_spec.rb +74 -0
  88. data/spec/unit/modules/async_wrapper_spec.rb +2 -1
  89. data/spec/unit/modules/enum_spec.rb +69 -0
  90. data/spec/unit/modules/event_emitter_spec.rb +149 -22
  91. data/spec/unit/modules/state_emitter_spec.rb +9 -3
  92. data/spec/unit/realtime/client_spec.rb +1 -1
  93. data/spec/unit/realtime/connection_spec.rb +8 -5
  94. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  95. data/spec/unit/realtime/presence_spec.rb +4 -3
  96. data/spec/unit/rest/client_spec.rb +1 -1
  97. data/spec/unit/util/crypto_spec.rb +3 -3
  98. metadata +22 -19
@@ -4,7 +4,8 @@ module Ably::Modules
4
4
  # module, and the class is an {EventEmitter}. It then emits state changes.
5
5
  #
6
6
  # It also ensures the EventEmitter is configured to retrict permitted events to the
7
- # the available STATEs and :error.
7
+ # the available STATEs or EVENTs if defined i.e. if EVENTs includes an additional type such as
8
+ # :update, then it will support all EVENTs being emitted. EVENTs must be a superset of STATEs
8
9
  #
9
10
  # @note This module requires that the method #logger is defined.
10
11
  #
@@ -51,7 +52,7 @@ module Ably::Modules
51
52
  # @api private
52
53
  def state=(new_state, *args)
53
54
  if state != new_state
54
- logger.debug("#{self.class}: StateEmitter changed from #{state} => #{new_state}") if respond_to?(:logger, true)
55
+ logger.debug { "#{self.class}: StateEmitter changed from #{state} => #{new_state}" } if respond_to?(:logger, true)
55
56
  @state = STATE(new_state)
56
57
  emit @state, *args
57
58
  end
@@ -153,8 +154,10 @@ module Ably::Modules
153
154
 
154
155
  def self.included(klass)
155
156
  klass.configure_event_emitter coerce_into: Proc.new { |event|
156
- if event == :error
157
- :error
157
+ # Special case allows EVENT instead of STATE to be emitted
158
+ # Relies on the assumption that EVENT is a superset of STATE
159
+ if klass.const_defined?(:EVENT)
160
+ klass::EVENT(event)
158
161
  else
159
162
  klass::STATE(event)
160
163
  end
@@ -18,14 +18,12 @@ module Ably::Modules
18
18
 
19
19
  # Alternative to Statesman's #transition_to that:
20
20
  # * log state change failures to {Logger}
21
- # * raise an exception on the {Ably::Realtime::Channel}
22
21
  #
23
22
  # @return [void]
24
23
  def transition_state(state, *args)
25
- unless result = transition_to(state, *args)
24
+ unless result = transition_to(state.to_sym, *args)
26
25
  exception = exception_for_state_change_to(state)
27
- object.emit :error, exception
28
- logger.fatal "#{self.class}: #{exception.message}"
26
+ logger.fatal { "#{self.class}: #{exception.message}" }
29
27
  end
30
28
  result
31
29
  end
@@ -67,15 +67,19 @@ module Ably::Modules
67
67
 
68
68
  def log_state_machine_state_change
69
69
  if state_machine.previous_state
70
- logger.debug "#{self.class.name}: Transitioned from #{state_machine.previous_state} => #{state_machine.current_state}"
70
+ logger.debug { "#{self.class.name}: Transitioned from #{state_machine.previous_state} => #{state_machine.current_state}" }
71
71
  else
72
- logger.debug "#{self.class.name}: Transitioned to #{state_machine.current_state}"
72
+ logger.debug { "#{self.class.name}: Transitioned to #{state_machine.current_state}" }
73
73
  end
74
74
  end
75
75
 
76
76
  def emit_object(new_state, emit_params)
77
77
  if self.class.emits_klass
78
- self.class.emits_klass.new((emit_params || {}).merge(current: STATE(new_state), previous: STATE(state_machine.current_state)))
78
+ self.class.emits_klass.new((emit_params || {}).merge(
79
+ current: STATE(new_state),
80
+ previous: STATE(state_machine.current_state),
81
+ event: EVENT(new_state)
82
+ ))
79
83
  else
80
84
  emit_params
81
85
  end
data/lib/ably/realtime.rb CHANGED
@@ -11,6 +11,8 @@ require 'ably/realtime/client'
11
11
  require 'ably/realtime/connection'
12
12
  require 'ably/realtime/presence'
13
13
 
14
+ require 'ably/models/message_encoders/base'
15
+
14
16
  Dir.glob(File.expand_path("models/*.rb", File.dirname(__FILE__))).each do |file|
15
17
  require file
16
18
  end
@@ -44,18 +44,20 @@ module Ably
44
44
  def_delegators :auth_sync, :using_basic_auth?, :using_token_auth?
45
45
  def_delegators :auth_sync, :token_renewable?, :authentication_security_requirements_met?
46
46
  def_delegators :client, :logger
47
+ def_delegators :client, :connection
47
48
 
48
49
  def initialize(client)
49
50
  @client = client
50
51
  @auth_sync = client.rest_client.auth
51
52
  end
52
53
 
53
- # Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary.
54
+ # For new connections, ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary.
55
+ # If a connection is already established, the connection will be upgraded with a new token
54
56
  #
55
57
  # In the event that a new token request is made, the provided options are used
56
58
  #
57
- # @param (see Ably::Auth#authorise)
58
- # @option (see Ably::Auth#authorise)
59
+ # @param (see Ably::Auth#authorize)
60
+ # @option (see Ably::Auth#authorize)
59
61
  #
60
62
  # @return [Ably::Util::SafeDeferrable]
61
63
  # @yield [Ably::Models::TokenDetails]
@@ -63,33 +65,90 @@ module Ably
63
65
  # @example
64
66
  # # will issue a simple token request using basic auth
65
67
  # client = Ably::Rest::Client.new(key: 'key.id:secret')
66
- # client.auth.authorise do |token_details|
68
+ # client.auth.authorize do |token_details|
67
69
  # token_details #=> Ably::Models::TokenDetails
68
70
  # end
69
71
  #
70
- def authorise(token_params = nil, auth_options = nil, &success_callback)
71
- async_wrap(success_callback) do
72
- auth_sync.authorise(token_params, auth_options, &method(:upgrade_authentication_block).to_proc)
73
- end.tap do |deferrable|
74
- deferrable.errback do |error|
75
- client.connection.transition_state_machine :failed, reason: error if error.kind_of?(Ably::Exceptions::IncompatibleClientId)
72
+ def authorize(token_params = nil, auth_options = nil, &success_callback)
73
+ Ably::Util::SafeDeferrable.new(logger).tap do |authorize_method_deferrable|
74
+ # Wrap the sync authorize method and wait for the result from the deferrable
75
+ async_wrap do
76
+ authorize_sync(token_params, auth_options)
77
+ end.tap do |auth_operation|
78
+ # Authorize operation succeeded and we have a new token, now let's perform inline authentication
79
+ auth_operation.callback do |token|
80
+ case connection.state.to_sym
81
+ when :initialized, :disconnected, :suspended, :closed, :closing, :failed
82
+ connection.connect
83
+ when :connected
84
+ perform_inline_auth token
85
+ when :connecting
86
+ # Fail all current connection attempts and try again with the new token, see #RTC8b
87
+ connection.manager.release_and_establish_new_transport
88
+ else
89
+ logger.fatal { "Auth#authorize: unsupported state #{connection.state}" }
90
+ authorize_method_deferrable.fail Ably::Exceptions::InvalidState.new("Unsupported state #{connection.state} for Auth#authorize")
91
+ next
92
+ end
93
+
94
+ # Indicate success or failure based on response from realtime, see #RTC8b1
95
+ auth_deferrable_resolved = false
96
+
97
+ connection.unsafe_once(:connected, :update) do
98
+ auth_deferrable_resolved = true
99
+ authorize_method_deferrable.succeed token
100
+ end
101
+ connection.unsafe_once(:suspended, :closed, :failed) do |state_change|
102
+ auth_deferrable_resolved = true
103
+ authorize_method_deferrable.fail state_change.reason
104
+ end
105
+ end
106
+
107
+ # Authorize failed, likely due to auth_url or auth_callback failing
108
+ auth_operation.errback do |error|
109
+ client.connection.transition_state_machine :failed, reason: error if error.kind_of?(Ably::Exceptions::IncompatibleClientId)
110
+ authorize_method_deferrable.fail error
111
+ end
112
+ end
113
+
114
+ # Call the block provided to this method upon success of this deferrable
115
+ authorize_method_deferrable.callback do |token|
116
+ yield token if block_given?
76
117
  end
77
118
  end
78
119
  end
79
120
 
80
- # Synchronous version of {#authorise}. See {Ably::Auth#authorise} for method definition
81
- # @param (see Ably::Auth#authorise)
82
- # @option (see Ably::Auth#authorise)
121
+ # @deprecated Use {#authorize} instead
122
+ def authorise(*args, &block)
123
+ logger.warn { "Auth#authorise is deprecated and will be removed in 1.0. Please use Auth#authorize instead" }
124
+ authorize(*args, &block)
125
+ end
126
+
127
+ # Synchronous version of {#authorize}. See {Ably::Auth#authorize} for method definition
128
+ # Please note that authorize_sync will however not upgrade the current connection's token as this requires
129
+ # an synchronous operation to send the new authentication details to Ably over a realtime connection
130
+ #
131
+ # @param (see Ably::Auth#authorize)
132
+ # @option (see Ably::Auth#authorize)
83
133
  # @return [Ably::Models::TokenDetails]
84
134
  #
85
- def authorise_sync(token_params = nil, auth_options = nil)
86
- auth_sync.authorise(token_params, auth_options, &method(:upgrade_authentication_block).to_proc)
135
+ def authorize_sync(token_params = nil, auth_options = nil)
136
+ @authorization_in_flight = true
137
+ auth_sync.authorize(token_params, auth_options)
138
+ ensure
139
+ @authorization_in_flight = false
87
140
  end
88
141
 
89
- # def_delegator :auth_sync, :request_token, :request_token_sync
90
- # def_delegator :auth_sync, :create_token_request, :create_token_request_sync
91
- # def_delegator :auth_sync, :auth_header, :auth_header_sync
92
- # def_delegator :auth_sync, :auth_params, :auth_params_sync
142
+ # @api private
143
+ def authorization_in_flight?
144
+ @authorization_in_flight
145
+ end
146
+
147
+ # @deprecated Use {#authorize_sync} instead
148
+ def authorise_sync(*args)
149
+ logger.warn { "Auth#authorise_sync is deprecated and will be removed in 1.0. Please use Auth#authorize_sync instead" }
150
+ authorize_sync(*args)
151
+ end
93
152
 
94
153
  # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests
95
154
  #
@@ -113,8 +172,8 @@ module Ably
113
172
  end
114
173
 
115
174
  # Synchronous version of {#request_token}. See {Ably::Auth#request_token} for method definition
116
- # @param (see Ably::Auth#authorise)
117
- # @option (see Ably::Auth#authorise)
175
+ # @param (see Ably::Auth#authorize)
176
+ # @option (see Ably::Auth#authorize)
118
177
  # @return [Ably::Models::TokenDetails]
119
178
  #
120
179
  def request_token_sync(token_params = {}, auth_options = {})
@@ -140,8 +199,8 @@ module Ably
140
199
  end
141
200
 
142
201
  # Synchronous version of {#create_token_request}. See {Ably::Auth#create_token_request} for method definition
143
- # @param (see Ably::Auth#authorise)
144
- # @option (see Ably::Auth#authorise)
202
+ # @param (see Ably::Auth#authorize)
203
+ # @option (see Ably::Auth#authorize)
145
204
  # @return [Ably::Models::TokenRequest]
146
205
  #
147
206
  def create_token_request_sync(token_params = {}, auth_options = {})
@@ -149,7 +208,7 @@ module Ably
149
208
  end
150
209
 
151
210
  # Auth header string used in HTTP requests to Ably
152
- # Will reauthorise implicitly if required and capable
211
+ # Will reauthorize implicitly if required and capable
153
212
  #
154
213
  # @return [Ably::Util::SafeDeferrable]
155
214
  # @yield [String] HTTP authentication value used in HTTP_AUTHORIZATION header
@@ -168,13 +227,22 @@ module Ably
168
227
  end
169
228
 
170
229
  # Auth params used in URI endpoint for Realtime connections
171
- # Will reauthorise implicitly if required and capable
230
+ # Will reauthorize implicitly if required and capable
172
231
  #
173
232
  # @return [Ably::Util::SafeDeferrable]
174
233
  # @yield [Hash] Auth params for a new Realtime connection
175
234
  #
176
235
  def auth_params(&success_callback)
177
- async_wrap(success_callback) do
236
+ fail_callback = Proc.new do |error, deferrable|
237
+ logger.error { "Failed to authenticate: #{error}" }
238
+ if error.kind_of?(Ably::Exceptions::BaseAblyException)
239
+ # Use base exception if it exists carrying forward the status codes
240
+ deferrable.fail Ably::Exceptions::AuthenticationFailed.new(error.message, nil, nil, error)
241
+ else
242
+ deferrable.fail Ably::Exceptions::AuthenticationFailed.new(error.message, 500, 80019)
243
+ end
244
+ end
245
+ async_wrap(success_callback, fail_callback) do
178
246
  auth_params_sync
179
247
  end
180
248
  end
@@ -197,22 +265,14 @@ module Ably
197
265
  @client
198
266
  end
199
267
 
200
- # If authorise is called with true, this block is executed so that it
201
- # can perform the authentication upgrade
202
- def upgrade_authentication_block(new_token)
203
- # This block is called if the authorisation was forced
204
- if client.connection.connected? || client.connection.connecting?
205
- logger.debug "Realtime::Auth - authorise called with { force: true } so forcibly disconnecting transport to initiate auth upgrade"
206
- block = Proc.new do
207
- if client.connection.transport
208
- logger.debug "Realtime::Auth - current transport disconnected"
209
- client.connection.transport.disconnect
210
- else
211
- EventMachine.add_timer(0.1, &block)
212
- end
213
- end
214
- block.call
215
- end
268
+ # Sends an AUTH ProtocolMessage on the existing connection triggering
269
+ # an inline AUTH process, see #RTC8a
270
+ def perform_inline_auth(token)
271
+ logger.debug { "Performing inline AUTH with Ably using token #{token}" }
272
+ connection.send_protocol_message(
273
+ action: Ably::Models::ProtocolMessage::ACTION.Auth.to_i,
274
+ auth: { access_token: token.token }
275
+ )
216
276
  end
217
277
  end
218
278
  end
@@ -23,8 +23,6 @@ module Ably
23
23
  # Channel::STATE.Detached
24
24
  # Channel::STATE.Failed
25
25
  #
26
- # Channels emit errors - use +on(:error)+ to subscribe to errors
27
- #
28
26
  # @!attribute [r] state
29
27
  # @return {Ably::Realtime::Connection::STATE} channel state
30
28
  #
@@ -36,14 +34,24 @@ module Ably
36
34
  include Ably::Modules::MessageEmitter
37
35
  extend Ably::Modules::Enum
38
36
 
37
+ # ChannelState
38
+ # The permited states for this channel
39
39
  STATE = ruby_enum('STATE',
40
40
  :initialized,
41
41
  :attaching,
42
42
  :attached,
43
43
  :detaching,
44
44
  :detached,
45
+ :suspended,
45
46
  :failed
46
47
  )
48
+
49
+ # ChannelEvent
50
+ # The permitted channel events that are emitted for this channel
51
+ EVENT = ruby_enum('EVENT',
52
+ STATE.to_sym_arr + [:update]
53
+ )
54
+
47
55
  include Ably::Modules::StateEmitter
48
56
  include Ably::Modules::UsesStateMachine
49
57
  ensure_state_machine_emits 'Ably::Models::ChannelStateChange'
@@ -138,11 +146,14 @@ module Ably
138
146
  # end
139
147
  #
140
148
  def publish(name, data = nil, attributes = {}, &success_block)
141
- raise Ably::Exceptions::ChannelInactive.new('Cannot publish messages on a detached channel') if detached? || detaching?
142
- raise Ably::Exceptions::ChannelInactive.new('Cannot publish messages on a failed channel') if failed?
149
+ if detached? || detaching? || failed?
150
+ error = Ably::Exceptions::ChannelInactive.new("Cannot publish messages on a channel in state #{state}")
151
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
152
+ end
143
153
 
144
154
  if !connection.can_publish_messages?
145
- raise Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. Client is configured to disallow queueing of messages and connection is currently #{connection.state}")
155
+ error = Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. Client is configured to disallow queueing of messages and connection is currently #{connection.state}")
156
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
146
157
  end
147
158
 
148
159
  messages = if name.kind_of?(Enumerable)
@@ -192,10 +203,19 @@ module Ably
192
203
  #
193
204
  def attach(&success_block)
194
205
  if connection.closing? || connection.closed? || connection.suspended? || connection.failed?
195
- raise Ably::Exceptions::InvalidStateChange.new("Cannot ATTACH channel when the connection is in a closed, suspended or failed state. Connection state: #{connection.state}")
206
+ error = Ably::Exceptions::InvalidStateChange.new("Cannot ATTACH channel when the connection is in a closed, suspended or failed state. Connection state: #{connection.state}")
207
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
208
+ end
209
+
210
+ if !attached?
211
+ if detaching?
212
+ # Let the pending operation complete (#RTL4h)
213
+ once_state_changed { transition_state_machine :attaching if can_transition_to?(:attaching) }
214
+ else
215
+ transition_state_machine :attaching if can_transition_to?(:attaching)
216
+ end
196
217
  end
197
218
 
198
- transition_state_machine :attaching if can_transition_to?(:attaching)
199
219
  deferrable_for_state_change_to(STATE.Attached, &success_block)
200
220
  end
201
221
 
@@ -207,14 +227,24 @@ module Ably
207
227
  def detach(&success_block)
208
228
  if initialized?
209
229
  success_block.call if block_given?
210
- return Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
211
- EventMachine.next_tick { deferrable.succeed }
212
- end
230
+ return Ably::Util::SafeDeferrable.new_and_succeed_immediately(logger)
213
231
  end
214
232
 
215
- raise exception_for_state_change_to(:detaching) if failed?
233
+ if failed? || connection.closing? || connection.failed?
234
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:detaching))
235
+ end
236
+
237
+ if !detached?
238
+ if attaching?
239
+ # Let the pending operation complete (#RTL5i)
240
+ once_state_changed { transition_state_machine :detaching if can_transition_to?(:detaching) }
241
+ elsif can_transition_to?(:detaching)
242
+ transition_state_machine :detaching
243
+ else
244
+ transition_state_machine! :detached
245
+ end
246
+ end
216
247
 
217
- transition_state_machine :detaching if can_transition_to?(:detaching)
218
248
  deferrable_for_state_change_to(STATE.Detached, &success_block)
219
249
  end
220
250
 
@@ -244,7 +274,10 @@ module Ably
244
274
  #
245
275
  def history(options = {}, &callback)
246
276
  if options.delete(:until_attach)
247
- raise ArgumentError, 'option :until_attach is invalid as the channel is not attached' unless attached?
277
+ unless attached?
278
+ error = Ably::Exceptions::InvalidRequest.new('option :until_attach is invalid as the channel is not attached' )
279
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
280
+ end
248
281
  options[:from_serial] = attached_serial
249
282
  end
250
283
 
@@ -263,7 +296,7 @@ module Ably
263
296
  end
264
297
 
265
298
  # @api private
266
- def set_failed_channel_error_reason(error)
299
+ def set_channel_error_reason(error)
267
300
  @error_reason = error
268
301
  end
269
302
 
@@ -288,22 +321,26 @@ module Ably
288
321
  client.logger
289
322
  end
290
323
 
324
+ # Internal queue used for messages published that cannot yet be enqueued on the connection
325
+ # @api private
326
+ def __queue__
327
+ @queue
328
+ end
329
+
291
330
  # As we are using a state machine, do not allow change_state to be used
292
331
  # #transition_state_machine must be used instead
293
332
  private :change_state
294
333
 
295
334
  private
296
- def queue
297
- @queue
298
- end
299
-
300
335
  def setup_event_handlers
301
336
  __incoming_msgbus__.subscribe(:message) do |message|
302
- message.decode self
337
+ message.decode(client.encoders, options) do |encode_error, error_message|
338
+ client.logger.error error_message
339
+ end
303
340
  emit_message message.name, message
304
341
  end
305
342
 
306
- on(STATE.Attached) do
343
+ unsafe_on(STATE.Attached) do
307
344
  process_queue
308
345
  end
309
346
  end
@@ -316,15 +353,18 @@ module Ably
316
353
  create_message(raw_msg).tap do |message|
317
354
  next if message.client_id.nil?
318
355
  if message.client_id == '*'
319
- raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages', 400, 40012)
356
+ raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages')
357
+ end
358
+ if message.client_id && !message.client_id.kind_of?(String)
359
+ raise Ably::Exceptions::IncompatibleClientId.new('client_id must be a String when publishing messages')
320
360
  end
321
361
  unless client.auth.can_assume_client_id?(message.client_id)
322
- 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)
362
+ 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}'")
323
363
  end
324
364
  end
325
365
  end
326
366
 
327
- queue.push(*messages)
367
+ __queue__.push(*messages)
328
368
 
329
369
  if attached?
330
370
  process_queue
@@ -366,14 +406,14 @@ module Ably
366
406
  end
367
407
 
368
408
  def messages_in_queue?
369
- !queue.empty?
409
+ !__queue__.empty?
370
410
  end
371
411
 
372
412
  # Move messages from Channel Queue into Outgoing Connection Queue
373
413
  def process_queue
374
414
  condition = -> { attached? && messages_in_queue? }
375
415
  non_blocking_loop_while(condition) do
376
- send_messages_within_protocol_message queue.shift(MAX_PROTOCOL_MESSAGE_BATCH_SIZE)
416
+ send_messages_within_protocol_message __queue__.shift(MAX_PROTOCOL_MESSAGE_BATCH_SIZE)
377
417
  end
378
418
  end
379
419
 
@@ -387,7 +427,9 @@ module Ably
387
427
 
388
428
  def create_message(message)
389
429
  Ably::Models::Message(message.dup).tap do |msg|
390
- msg.encode self
430
+ msg.encode(client.encoders, options) do |encode_error, error_message|
431
+ client.logger.error error_message
432
+ end
391
433
  end
392
434
  end
393
435