ably 0.8.15 → 1.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 (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