ably 0.8.3 → 0.8.4
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/CHANGELOG.md +13 -3
- data/lib/ably/auth.rb +1 -1
- data/lib/ably/exceptions.rb +3 -0
- data/lib/ably/models/channel_state_change.rb +41 -0
- data/lib/ably/models/connection_state_change.rb +43 -0
- data/lib/ably/models/message.rb +1 -1
- data/lib/ably/models/presence_message.rb +1 -1
- data/lib/ably/models/protocol_message.rb +2 -1
- data/lib/ably/modules/state_emitter.rb +4 -1
- data/lib/ably/modules/uses_state_machine.rb +28 -4
- data/lib/ably/realtime/channel.rb +11 -3
- data/lib/ably/realtime/channel/channel_manager.rb +24 -4
- data/lib/ably/realtime/channel/channel_state_machine.rb +20 -11
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +4 -4
- data/lib/ably/realtime/connection.rb +1 -0
- data/lib/ably/realtime/connection/connection_manager.rb +33 -21
- data/lib/ably/realtime/connection/connection_state_machine.rb +24 -16
- data/lib/ably/rest/channel.rb +3 -2
- data/lib/ably/util/crypto.rb +15 -0
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_spec.rb +155 -9
- data/spec/acceptance/realtime/client_spec.rb +2 -2
- data/spec/acceptance/realtime/connection_failures_spec.rb +8 -4
- data/spec/acceptance/realtime/connection_spec.rb +122 -11
- data/spec/acceptance/realtime/message_spec.rb +119 -3
- data/spec/acceptance/realtime/presence_spec.rb +34 -13
- data/spec/acceptance/rest/channel_spec.rb +9 -0
- data/spec/acceptance/rest/client_spec.rb +10 -0
- data/spec/unit/models/channel_state_change_spec.rb +44 -0
- data/spec/unit/models/connection_state_change_spec.rb +54 -0
- data/spec/unit/util/crypto_spec.rb +18 -0
- metadata +8 -2
| @@ -55,6 +55,7 @@ module Ably | |
| 55 55 | 
             
                  )
         | 
| 56 56 | 
             
                  include Ably::Modules::StateEmitter
         | 
| 57 57 | 
             
                  include Ably::Modules::UsesStateMachine
         | 
| 58 | 
            +
                  ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
         | 
| 58 59 |  | 
| 59 60 | 
             
                  # Expected format for a connection recover key
         | 
| 60 61 | 
             
                  RECOVER_REGEX = /^(?<recover>[\w-]+):(?<connection_serial>\-?\w+)$/
         | 
| @@ -53,7 +53,7 @@ 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::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
         | 
| 56 | 
            +
                      connection.transition_state_machine :failed, reason: Ably::Exceptions::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
         | 
| 57 57 | 
             
                      return
         | 
| 58 58 | 
             
                    end
         | 
| 59 59 |  | 
| @@ -80,7 +80,8 @@ module Ably::Realtime | |
| 80 80 | 
             
                  # @api private
         | 
| 81 81 | 
             
                  def connection_opening_failed(error)
         | 
| 82 82 | 
             
                    logger.warn "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}"
         | 
| 83 | 
            -
                     | 
| 83 | 
            +
                    next_state = get_next_retry_state_info
         | 
| 84 | 
            +
                    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)
         | 
| 84 85 | 
             
                  end
         | 
| 85 86 |  | 
| 86 87 | 
             
                  # Called whenever a new connection is made
         | 
| @@ -155,11 +156,10 @@ module Ably::Realtime | |
| 155 156 | 
             
                  # When a connection is disconnected whilst connecting, attempt reconnect and/or set state to :suspended or :failed
         | 
| 156 157 | 
             
                  #
         | 
| 157 158 | 
             
                  # @api private
         | 
| 158 | 
            -
                  def respond_to_transport_disconnected_when_connecting( | 
| 159 | 
            +
                  def respond_to_transport_disconnected_when_connecting(error)
         | 
| 159 160 | 
             
                    return unless connection.disconnected? || connection.suspended? # do nothing if state has changed through an explicit request
         | 
| 160 161 | 
             
                    return unless retry_connection? # do not always reattempt connection or change state as client may be re-authorising
         | 
| 161 162 |  | 
| 162 | 
            -
                    error = current_transition.metadata
         | 
| 163 163 | 
             
                    if error.kind_of?(Ably::Models::ErrorInfo)
         | 
| 164 164 | 
             
                      renew_token_and_reconnect error if error.code == RESOLVABLE_ERROR_CODES.fetch(:token_expired)
         | 
| 165 165 | 
             
                      return
         | 
| @@ -172,23 +172,22 @@ module Ably::Realtime | |
| 172 172 | 
             
                    return if connection_retry_for(:suspended)
         | 
| 173 173 |  | 
| 174 174 | 
             
                    # Fallback if no other criteria met
         | 
| 175 | 
            -
                    connection.transition_state_machine :failed,  | 
| 175 | 
            +
                    connection.transition_state_machine :failed, reason: error
         | 
| 176 176 | 
             
                  end
         | 
| 177 177 |  | 
| 178 178 | 
             
                  # When a connection is disconnected after connecting, attempt reconnect and/or set state to :suspended or :failed
         | 
| 179 179 | 
             
                  #
         | 
| 180 180 | 
             
                  # @api private
         | 
| 181 | 
            -
                  def respond_to_transport_disconnected_whilst_connected( | 
| 181 | 
            +
                  def respond_to_transport_disconnected_whilst_connected(error)
         | 
| 182 182 | 
             
                    logger.warn "ConnectionManager: Connection to #{connection.transport.url} was disconnected unexpectedly"
         | 
| 183 183 |  | 
| 184 | 
            -
                    error = current_transition.metadata
         | 
| 185 184 | 
             
                    if error.kind_of?(Ably::Models::ErrorInfo) && error.code != RESOLVABLE_ERROR_CODES.fetch(:token_expired)
         | 
| 186 185 | 
             
                      connection.emit :error, error
         | 
| 187 186 | 
             
                      logger.error "ConnectionManager: Error in Disconnected ProtocolMessage received from the server - #{error}"
         | 
| 188 187 | 
             
                    end
         | 
| 189 188 |  | 
| 190 189 | 
             
                    destroy_transport
         | 
| 191 | 
            -
                    respond_to_transport_disconnected_when_connecting  | 
| 190 | 
            +
                    respond_to_transport_disconnected_when_connecting error
         | 
| 192 191 | 
             
                  end
         | 
| 193 192 |  | 
| 194 193 | 
             
                  # {Ably::Models::ProtocolMessage ProtocolMessage Error} received from server.
         | 
| @@ -198,13 +197,13 @@ module Ably::Realtime | |
| 198 197 | 
             
                  def error_received_from_server(error)
         | 
| 199 198 | 
             
                    case error.code
         | 
| 200 199 | 
             
                    when RESOLVABLE_ERROR_CODES.fetch(:token_expired)
         | 
| 201 | 
            -
                      connection.transition_state_machine :disconnected
         | 
| 200 | 
            +
                      connection.transition_state_machine :disconnected, retry_in: 0
         | 
| 202 201 | 
             
                      connection.unsafe_once_or_if(:disconnected) do
         | 
| 203 202 | 
             
                        renew_token_and_reconnect error
         | 
| 204 203 | 
             
                      end
         | 
| 205 204 | 
             
                    else
         | 
| 206 205 | 
             
                      logger.error "ConnectionManager: Error #{error.class.name} code #{error.code} received from server '#{error.message}', transitioning to failed state"
         | 
| 207 | 
            -
                      connection.transition_state_machine :failed, error
         | 
| 206 | 
            +
                      connection.transition_state_machine :failed, reason: error
         | 
| 208 207 | 
             
                    end
         | 
| 209 208 | 
             
                  end
         | 
| 210 209 |  | 
| @@ -249,12 +248,26 @@ module Ably::Realtime | |
| 249 248 | 
             
                    timers.fetch(key, []).each(&:cancel)
         | 
| 250 249 | 
             
                  end
         | 
| 251 250 |  | 
| 252 | 
            -
                  def  | 
| 253 | 
            -
                    if connection_retry_from_suspended_state? ||  | 
| 251 | 
            +
                  def get_next_retry_state_info
         | 
| 252 | 
            +
                    retry_state = if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
         | 
| 254 253 | 
             
                      :suspended
         | 
| 255 254 | 
             
                    else
         | 
| 256 255 | 
             
                      :disconnected
         | 
| 257 256 | 
             
                    end
         | 
| 257 | 
            +
                    {
         | 
| 258 | 
            +
                      state: retry_state,
         | 
| 259 | 
            +
                      pause: next_retry_pause(retry_state)
         | 
| 260 | 
            +
                    }
         | 
| 261 | 
            +
                  end
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                  def next_retry_pause(retry_state)
         | 
| 264 | 
            +
                    return nil unless CONNECT_RETRY_CONFIG.fetch(retry_state)
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                    if retries_for_state(retry_state, ignore_states: [:connecting]).empty?
         | 
| 267 | 
            +
                      0
         | 
| 268 | 
            +
                    else
         | 
| 269 | 
            +
                      CONNECT_RETRY_CONFIG.fetch(retry_state).fetch(:retry_every)
         | 
| 270 | 
            +
                    end
         | 
| 258 271 | 
             
                  end
         | 
| 259 272 |  | 
| 260 273 | 
             
                  def connection_retry_from_suspended_state?
         | 
| @@ -348,13 +361,12 @@ module Ably::Realtime | |
| 348 361 | 
             
                        connection.transition_state_machine :closed
         | 
| 349 362 | 
             
                      elsif !connection.closed? && !connection.disconnected?
         | 
| 350 363 | 
             
                        exception = if reason
         | 
| 351 | 
            -
                          Ably::Exceptions:: | 
| 352 | 
            -
                        end
         | 
| 353 | 
            -
                        if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
         | 
| 354 | 
            -
                          connection.transition_state_machine :suspended, exception
         | 
| 364 | 
            +
                          Ably::Exceptions::TransportClosed.new(reason, nil, 80003)
         | 
| 355 365 | 
             
                        else
         | 
| 356 | 
            -
                           | 
| 366 | 
            +
                          Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, 80003)
         | 
| 357 367 | 
             
                        end
         | 
| 368 | 
            +
                        next_state = get_next_retry_state_info
         | 
| 369 | 
            +
                        connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: exception
         | 
| 358 370 | 
             
                      end
         | 
| 359 371 | 
             
                    end
         | 
| 360 372 | 
             
                  end
         | 
| @@ -362,7 +374,7 @@ module Ably::Realtime | |
| 362 374 | 
             
                  def renew_token_and_reconnect(error)
         | 
| 363 375 | 
             
                    if client.auth.token_renewable?
         | 
| 364 376 | 
             
                      if @renewing_token
         | 
| 365 | 
            -
                        connection.transition_state_machine :failed, error
         | 
| 377 | 
            +
                        connection.transition_state_machine :failed, reason: error
         | 
| 366 378 | 
             
                        return
         | 
| 367 379 | 
             
                      end
         | 
| 368 380 |  | 
| @@ -383,18 +395,18 @@ module Ably::Realtime | |
| 383 395 | 
             
                          if token_details && !token_details.expired?
         | 
| 384 396 | 
             
                            connection.connect
         | 
| 385 397 | 
             
                          else
         | 
| 386 | 
            -
                            connection.transition_state_machine :failed, error unless connection.failed?
         | 
| 398 | 
            +
                            connection.transition_state_machine :failed, reason: error unless connection.failed?
         | 
| 387 399 | 
             
                          end
         | 
| 388 400 | 
             
                        end
         | 
| 389 401 |  | 
| 390 402 | 
             
                        authorise_deferrable.errback do |auth_error|
         | 
| 391 403 | 
             
                          logger.error "ConnectionManager: Error authorising following token expiry: #{auth_error}"
         | 
| 392 | 
            -
                          connection.transition_state_machine :failed, auth_error
         | 
| 404 | 
            +
                          connection.transition_state_machine :failed, reason: auth_error
         | 
| 393 405 | 
             
                        end
         | 
| 394 406 | 
             
                      end
         | 
| 395 407 | 
             
                    else
         | 
| 396 408 | 
             
                      logger.error "ConnectionManager: Token has expired and is not renewable - #{error}"
         | 
| 397 | 
            -
                      connection.transition_state_machine :failed, error
         | 
| 409 | 
            +
                      connection.transition_state_machine :failed, reason: error
         | 
| 398 410 | 
             
                    end
         | 
| 399 411 | 
             
                  end
         | 
| 400 412 |  | 
| @@ -41,23 +41,25 @@ module Ably::Realtime | |
| 41 41 | 
             
                  end
         | 
| 42 42 |  | 
| 43 43 | 
             
                  before_transition(to: [:connected]) do |connection, current_transition|
         | 
| 44 | 
            -
                    connection.manager.connected current_transition.metadata
         | 
| 44 | 
            +
                    connection.manager.connected current_transition.metadata.protocol_message
         | 
| 45 45 | 
             
                  end
         | 
| 46 46 |  | 
| 47 47 | 
             
                  after_transition(to: [:connected]) do |connection, current_transition|
         | 
| 48 | 
            -
                     | 
| 49 | 
            -
                    if is_error_type?( | 
| 50 | 
            -
                      connection.logger.warn "ConnectionManager: Connected with error - #{ | 
| 51 | 
            -
                      connection.emit :error,  | 
| 48 | 
            +
                    error = current_transition.metadata.reason
         | 
| 49 | 
            +
                    if is_error_type?(error)
         | 
| 50 | 
            +
                      connection.logger.warn "ConnectionManager: Connected with error - #{error.message}"
         | 
| 51 | 
            +
                      connection.emit :error, error
         | 
| 52 52 | 
             
                    end
         | 
| 53 53 | 
             
                  end
         | 
| 54 54 |  | 
| 55 55 | 
             
                  after_transition(to: [:disconnected, :suspended], from: [:connecting]) do |connection, current_transition|
         | 
| 56 | 
            -
                     | 
| 56 | 
            +
                    err = error_from_state_change(current_transition)
         | 
| 57 | 
            +
                    connection.manager.respond_to_transport_disconnected_when_connecting err
         | 
| 57 58 | 
             
                  end
         | 
| 58 59 |  | 
| 59 60 | 
             
                  after_transition(to: [:disconnected], from: [:connected]) do |connection, current_transition|
         | 
| 60 | 
            -
                     | 
| 61 | 
            +
                    err = error_from_state_change(current_transition)
         | 
| 62 | 
            +
                    connection.manager.respond_to_transport_disconnected_whilst_connected err
         | 
| 61 63 | 
             
                  end
         | 
| 62 64 |  | 
| 63 65 | 
             
                  after_transition(to: [:disconnected, :suspended]) do |connection|
         | 
| @@ -65,7 +67,8 @@ module Ably::Realtime | |
| 65 67 | 
             
                  end
         | 
| 66 68 |  | 
| 67 69 | 
             
                  before_transition(to: [:failed]) do |connection, current_transition|
         | 
| 68 | 
            -
                     | 
| 70 | 
            +
                    err = error_from_state_change(current_transition)
         | 
| 71 | 
            +
                    connection.manager.fail err
         | 
| 69 72 | 
             
                  end
         | 
| 70 73 |  | 
| 71 74 | 
             
                  after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
         | 
| @@ -82,24 +85,29 @@ module Ably::Realtime | |
| 82 85 |  | 
| 83 86 | 
             
                  # Transitions responsible for updating connection#error_reason
         | 
| 84 87 | 
             
                  before_transition(to: [:disconnected, :suspended, :failed]) do |connection, current_transition|
         | 
| 85 | 
            -
                     | 
| 88 | 
            +
                    err = error_from_state_change(current_transition)
         | 
| 89 | 
            +
                    connection.set_failed_connection_error_reason err
         | 
| 86 90 | 
             
                  end
         | 
| 87 91 |  | 
| 88 92 | 
             
                  before_transition(to: [:connected, :closed]) do |connection, current_transition|
         | 
| 89 | 
            -
                     | 
| 90 | 
            -
                      current_transition.metadata.error
         | 
| 91 | 
            -
                    else
         | 
| 92 | 
            -
                      current_transition.metadata
         | 
| 93 | 
            -
                    end
         | 
| 93 | 
            +
                    err = error_from_state_change(current_transition)
         | 
| 94 94 |  | 
| 95 | 
            -
                    if  | 
| 96 | 
            -
                      connection.set_failed_connection_error_reason  | 
| 95 | 
            +
                    if err
         | 
| 96 | 
            +
                      connection.set_failed_connection_error_reason err
         | 
| 97 97 | 
             
                    else
         | 
| 98 98 | 
             
                      # Connected & Closed are "healthy" final states so reset the error reason
         | 
| 99 99 | 
             
                      connection.clear_error_reason
         | 
| 100 100 | 
             
                    end
         | 
| 101 101 | 
             
                  end
         | 
| 102 102 |  | 
| 103 | 
            +
                  def self.error_from_state_change(current_transition)
         | 
| 104 | 
            +
                    # ConnectionStateChange object is always passed in current_transition metadata object
         | 
| 105 | 
            +
                    connection_state_change = current_transition.metadata
         | 
| 106 | 
            +
                    # Reason attribute contains errors
         | 
| 107 | 
            +
                    err = connection_state_change && connection_state_change.reason
         | 
| 108 | 
            +
                    err if is_error_type?(err)
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
             | 
| 103 111 | 
             
                  private
         | 
| 104 112 | 
             
                  def connection
         | 
| 105 113 | 
             
                    object
         | 
    
        data/lib/ably/rest/channel.rb
    CHANGED
    
    | @@ -34,6 +34,7 @@ module Ably | |
| 34 34 | 
             
                  #
         | 
| 35 35 | 
             
                  # @param name [String, Array<Ably::Models::Message|Hash>, nil]   The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs
         | 
| 36 36 | 
             
                  # @param data [String, ByteArray, nil]   The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
         | 
| 37 | 
            +
                  # @param attributes [Hash, nil]   Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
         | 
| 37 38 | 
             
                  # @return [Boolean]  true if the message was published, otherwise false
         | 
| 38 39 | 
             
                  #
         | 
| 39 40 | 
             
                  # @example
         | 
| @@ -54,13 +55,13 @@ module Ably | |
| 54 55 | 
             
                  #   ]
         | 
| 55 56 | 
             
                  #   channel.publish messages
         | 
| 56 57 | 
             
                  #
         | 
| 57 | 
            -
                  def publish(name, data = nil)
         | 
| 58 | 
            +
                  def publish(name, data = nil, attributes = {})
         | 
| 58 59 | 
             
                    messages = if name.kind_of?(Enumerable)
         | 
| 59 60 | 
             
                      name
         | 
| 60 61 | 
             
                    else
         | 
| 61 62 | 
             
                      ensure_utf_8 :name, name, allow_nil: true
         | 
| 62 63 | 
             
                      ensure_supported_payload data
         | 
| 63 | 
            -
                      [{ name: name, data: data }]
         | 
| 64 | 
            +
                      [{ name: name, data: data }.merge(attributes)]
         | 
| 64 65 | 
             
                    end
         | 
| 65 66 |  | 
| 66 67 | 
             
                    payload = messages.map do |message|
         | 
    
        data/lib/ably/util/crypto.rb
    CHANGED
    
    | @@ -37,6 +37,21 @@ module Ably::Util | |
| 37 37 | 
             
                  @options = DEFAULTS.merge(options).freeze
         | 
| 38 38 | 
             
                end
         | 
| 39 39 |  | 
| 40 | 
            +
                # Obtain a default CipherParams. This uses default algorithm, mode and
         | 
| 41 | 
            +
                # padding and key length. A key and IV are generated using the default
         | 
| 42 | 
            +
                # system SecureRandom; the key may be obtained from the returned CipherParams
         | 
| 43 | 
            +
                # for out-of-band distribution to other clients.
         | 
| 44 | 
            +
                #
         | 
| 45 | 
            +
                # @return [Hash]   CipherParam options Hash with attributes :key, :algorithn, :mode, :key_length
         | 
| 46 | 
            +
                #
         | 
| 47 | 
            +
                def self.get_default_params(key = nil)
         | 
| 48 | 
            +
                  params = DEFAULTS.merge(key: key)
         | 
| 49 | 
            +
                  params[:key_length] = key.unpack('b*').first.length if params[:key]
         | 
| 50 | 
            +
                  cipher_type = "#{params[:algorithm]}-#{params[:key_length]}-#{params[:mode]}"
         | 
| 51 | 
            +
                  params[:key] = OpenSSL::Cipher.new(cipher_type.upcase).random_key unless params[:key]
         | 
| 52 | 
            +
                  params
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 40 55 | 
             
                # Encrypt payload using configured Cipher
         | 
| 41 56 | 
             
                #
         | 
| 42 57 | 
             
                # @param [String] payload           the payload to be encrypted
         | 
    
        data/lib/ably/version.rb
    CHANGED
    
    
| @@ -92,7 +92,7 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 92 92 |  | 
| 93 93 | 
             
                    it 'reattaches' do
         | 
| 94 94 | 
             
                      channel.attach do
         | 
| 95 | 
            -
                        channel.transition_state_machine :failed, RuntimeError.new
         | 
| 95 | 
            +
                        channel.transition_state_machine :failed, reason: RuntimeError.new
         | 
| 96 96 | 
             
                        expect(channel).to be_failed
         | 
| 97 97 | 
             
                        channel.attach do
         | 
| 98 98 | 
             
                          expect(channel).to be_attached
         | 
| @@ -154,9 +154,9 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 154 154 |  | 
| 155 155 | 
             
                    it 'emits failed event' do
         | 
| 156 156 | 
             
                      restricted_channel.attach
         | 
| 157 | 
            -
                      restricted_channel.on(:failed) do | | 
| 157 | 
            +
                      restricted_channel.on(:failed) do |connection_state|
         | 
| 158 158 | 
             
                        expect(restricted_channel.state).to eq(:failed)
         | 
| 159 | 
            -
                        expect( | 
| 159 | 
            +
                        expect(connection_state.reason.status).to eq(401)
         | 
| 160 160 | 
             
                        stop_reactor
         | 
| 161 161 | 
             
                      end
         | 
| 162 162 | 
             
                    end
         | 
| @@ -262,7 +262,7 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 262 262 |  | 
| 263 263 | 
             
                    it 'raises an exception' do
         | 
| 264 264 | 
             
                      channel.attach do
         | 
| 265 | 
            -
                        channel.transition_state_machine :failed, RuntimeError.new
         | 
| 265 | 
            +
                        channel.transition_state_machine :failed, reason: RuntimeError.new
         | 
| 266 266 | 
             
                        expect(channel).to be_failed
         | 
| 267 267 | 
             
                        expect { channel.detach }.to raise_error Ably::Exceptions::InvalidStateChange
         | 
| 268 268 | 
             
                        stop_reactor
         | 
| @@ -433,6 +433,19 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 433 433 | 
             
                        end
         | 
| 434 434 | 
             
                      end
         | 
| 435 435 | 
             
                    end
         | 
| 436 | 
            +
             | 
| 437 | 
            +
                    context 'and additional attributes' do
         | 
| 438 | 
            +
                      let(:client_id) { random_str }
         | 
| 439 | 
            +
             | 
| 440 | 
            +
                      it 'publishes the message with the attributes and return true indicating success' do
         | 
| 441 | 
            +
                        channel.publish(name, data, client_id: client_id) do
         | 
| 442 | 
            +
                          channel.history do |page|
         | 
| 443 | 
            +
                            expect(page.items.first.client_id).to eql(client_id)
         | 
| 444 | 
            +
                            stop_reactor
         | 
| 445 | 
            +
                          end
         | 
| 446 | 
            +
                        end
         | 
| 447 | 
            +
                      end
         | 
| 448 | 
            +
                    end
         | 
| 436 449 | 
             
                  end
         | 
| 437 450 |  | 
| 438 451 | 
             
                  context 'with an array of Hash objects with :name and :data attributes' do
         | 
| @@ -666,7 +679,8 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 666 679 | 
             
                    context 'an :attached channel' do
         | 
| 667 680 | 
             
                      it 'transitions state to :failed' do
         | 
| 668 681 | 
             
                        channel.attach do
         | 
| 669 | 
            -
                          channel.on(:failed) do | | 
| 682 | 
            +
                          channel.on(:failed) do |connection_state_change|
         | 
| 683 | 
            +
                            error = connection_state_change.reason
         | 
| 670 684 | 
             
                            expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
         | 
| 671 685 | 
             
                            expect(error.code).to eql(80002)
         | 
| 672 686 | 
             
                            stop_reactor
         | 
| @@ -688,7 +702,8 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 688 702 |  | 
| 689 703 | 
             
                      it 'updates the channel error_reason' do
         | 
| 690 704 | 
             
                        channel.attach do
         | 
| 691 | 
            -
                          channel.on(:failed) do | | 
| 705 | 
            +
                          channel.on(:failed) do |connection_state_change|
         | 
| 706 | 
            +
                            error = connection_state_change.reason
         | 
| 692 707 | 
             
                            expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
         | 
| 693 708 | 
             
                            expect(error.code).to eql(80002)
         | 
| 694 709 | 
             
                            stop_reactor
         | 
| @@ -734,7 +749,7 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 734 749 | 
             
                            fake_error connection_error
         | 
| 735 750 | 
             
                          end
         | 
| 736 751 |  | 
| 737 | 
            -
                          channel.transition_state_machine :failed, original_error
         | 
| 752 | 
            +
                          channel.transition_state_machine :failed, reason: original_error
         | 
| 738 753 | 
             
                        end
         | 
| 739 754 | 
             
                      end
         | 
| 740 755 | 
             
                    end
         | 
| @@ -783,8 +798,8 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 783 798 | 
             
                    end
         | 
| 784 799 |  | 
| 785 800 | 
             
                    context 'a :failed channel' do
         | 
| 786 | 
            -
                      let(:original_error) { RuntimeError.new }
         | 
| 787 801 | 
             
                      let(:client_options)   { default_options.merge(log_level: :fatal) }
         | 
| 802 | 
            +
                      let(:original_error) { Ably::Models::ErrorInfo.new(message: 'Error') }
         | 
| 788 803 |  | 
| 789 804 | 
             
                      it 'remains in the :failed state and retains the error_reason' do
         | 
| 790 805 | 
             
                        channel.attach do
         | 
| @@ -801,7 +816,7 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 801 816 | 
             
                            client.connection.close
         | 
| 802 817 | 
             
                          end
         | 
| 803 818 |  | 
| 804 | 
            -
                          channel.transition_state_machine :failed, original_error
         | 
| 819 | 
            +
                          channel.transition_state_machine :failed, reason: original_error
         | 
| 805 820 | 
             
                        end
         | 
| 806 821 | 
             
                      end
         | 
| 807 822 | 
             
                    end
         | 
| @@ -830,6 +845,75 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 830 845 | 
             
                      end
         | 
| 831 846 | 
             
                    end
         | 
| 832 847 | 
             
                  end
         | 
| 848 | 
            +
             | 
| 849 | 
            +
                  context ':suspended' do
         | 
| 850 | 
            +
                    context 'an :attached channel' do
         | 
| 851 | 
            +
                      let(:client_options) { default_options.merge(log_level: :fatal) }
         | 
| 852 | 
            +
             | 
| 853 | 
            +
                      it 'transitions state to :detached' do
         | 
| 854 | 
            +
                        channel.attach do
         | 
| 855 | 
            +
                          channel.on(:detached) do
         | 
| 856 | 
            +
                            stop_reactor
         | 
| 857 | 
            +
                          end
         | 
| 858 | 
            +
                          client.connection.transition_state_machine :suspended
         | 
| 859 | 
            +
                        end
         | 
| 860 | 
            +
                      end
         | 
| 861 | 
            +
                    end
         | 
| 862 | 
            +
             | 
| 863 | 
            +
                    context 'a :detached channel' do
         | 
| 864 | 
            +
                      it 'remains in the :detached state' do
         | 
| 865 | 
            +
                        channel.attach do
         | 
| 866 | 
            +
                          channel.detach do
         | 
| 867 | 
            +
                            channel.on(:detached) { raise 'Detached state should not have been reached' }
         | 
| 868 | 
            +
                            channel.on(:error)    { raise 'Error should not have been emitted' }
         | 
| 869 | 
            +
             | 
| 870 | 
            +
                            EventMachine.add_timer(1) do
         | 
| 871 | 
            +
                              expect(channel).to be_detached
         | 
| 872 | 
            +
                              stop_reactor
         | 
| 873 | 
            +
                            end
         | 
| 874 | 
            +
             | 
| 875 | 
            +
                            client.connection.transition_state_machine :suspended
         | 
| 876 | 
            +
                          end
         | 
| 877 | 
            +
                        end
         | 
| 878 | 
            +
                      end
         | 
| 879 | 
            +
                    end
         | 
| 880 | 
            +
             | 
| 881 | 
            +
                    context 'a :failed channel' do
         | 
| 882 | 
            +
                      let(:original_error) { RuntimeError.new }
         | 
| 883 | 
            +
                      let(:client_options)   { default_options.merge(log_level: :fatal) }
         | 
| 884 | 
            +
             | 
| 885 | 
            +
                      it 'remains in the :failed state and retains the error_reason' do
         | 
| 886 | 
            +
                        channel.attach do
         | 
| 887 | 
            +
                          channel.once(:error) do
         | 
| 888 | 
            +
                            channel.on(:detached) { raise 'Detached state should not have been reached' }
         | 
| 889 | 
            +
                            channel.on(:error)    { raise 'Error should not have been emitted' }
         | 
| 890 | 
            +
             | 
| 891 | 
            +
                            EventMachine.add_timer(1) do
         | 
| 892 | 
            +
                              expect(channel).to be_failed
         | 
| 893 | 
            +
                              expect(channel.error_reason).to eql(original_error)
         | 
| 894 | 
            +
                              stop_reactor
         | 
| 895 | 
            +
                            end
         | 
| 896 | 
            +
             | 
| 897 | 
            +
                            client.connection.transition_state_machine :suspended
         | 
| 898 | 
            +
                          end
         | 
| 899 | 
            +
             | 
| 900 | 
            +
                          channel.transition_state_machine :failed, reason: original_error
         | 
| 901 | 
            +
                        end
         | 
| 902 | 
            +
                      end
         | 
| 903 | 
            +
                    end
         | 
| 904 | 
            +
             | 
| 905 | 
            +
                    context 'a channel ATTACH request when connection SUSPENDED' do
         | 
| 906 | 
            +
                      it 'raises an exception' do
         | 
| 907 | 
            +
                        client.connect do
         | 
| 908 | 
            +
                          client.connection.once(:suspended) do
         | 
| 909 | 
            +
                            expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
         | 
| 910 | 
            +
                            stop_reactor
         | 
| 911 | 
            +
                          end
         | 
| 912 | 
            +
                          client.connection.transition_state_machine :suspended
         | 
| 913 | 
            +
                        end
         | 
| 914 | 
            +
                      end
         | 
| 915 | 
            +
                    end
         | 
| 916 | 
            +
                  end
         | 
| 833 917 | 
             
                end
         | 
| 834 918 |  | 
| 835 919 | 
             
                describe '#presence' do
         | 
| @@ -838,5 +922,67 @@ describe Ably::Realtime::Channel, :event_machine do | |
| 838 922 | 
             
                    stop_reactor
         | 
| 839 923 | 
             
                  end
         | 
| 840 924 | 
             
                end
         | 
| 925 | 
            +
             | 
| 926 | 
            +
                context 'channel state change' do
         | 
| 927 | 
            +
                  it 'emits a ChannelStateChange object' do
         | 
| 928 | 
            +
                    channel.on(:attached) do |channel_state_change|
         | 
| 929 | 
            +
                      expect(channel_state_change).to be_a(Ably::Models::ChannelStateChange)
         | 
| 930 | 
            +
                      stop_reactor
         | 
| 931 | 
            +
                    end
         | 
| 932 | 
            +
                    channel.attach
         | 
| 933 | 
            +
                  end
         | 
| 934 | 
            +
             | 
| 935 | 
            +
                  context 'ChannelStateChange object' do
         | 
| 936 | 
            +
                    it 'has current state' do
         | 
| 937 | 
            +
                      channel.on(:attached) do |channel_state_change|
         | 
| 938 | 
            +
                        expect(channel_state_change.current).to eq(:attached)
         | 
| 939 | 
            +
                        stop_reactor
         | 
| 940 | 
            +
                      end
         | 
| 941 | 
            +
                      channel.attach
         | 
| 942 | 
            +
                    end
         | 
| 943 | 
            +
             | 
| 944 | 
            +
                    it 'has a previous state' do
         | 
| 945 | 
            +
                      channel.on(:attached) do |channel_state_change|
         | 
| 946 | 
            +
                        expect(channel_state_change.previous).to eq(:attaching)
         | 
| 947 | 
            +
                        stop_reactor
         | 
| 948 | 
            +
                      end
         | 
| 949 | 
            +
                      channel.attach
         | 
| 950 | 
            +
                    end
         | 
| 951 | 
            +
             | 
| 952 | 
            +
                    it 'contains a private API protocol_message attribute that is used for special state change events', :api_private do
         | 
| 953 | 
            +
                      channel.on(:attached) do |channel_state_change|
         | 
| 954 | 
            +
                        expect(channel_state_change.protocol_message).to be_a(Ably::Models::ProtocolMessage)
         | 
| 955 | 
            +
                        expect(channel_state_change.reason).to be_nil
         | 
| 956 | 
            +
                        stop_reactor
         | 
| 957 | 
            +
                      end
         | 
| 958 | 
            +
                      channel.attach
         | 
| 959 | 
            +
                    end
         | 
| 960 | 
            +
             | 
| 961 | 
            +
                    it 'has an empty reason when there is no error' do
         | 
| 962 | 
            +
                      channel.on(:detached) do |channel_state_change|
         | 
| 963 | 
            +
                        expect(channel_state_change.reason).to be_nil
         | 
| 964 | 
            +
                        stop_reactor
         | 
| 965 | 
            +
                      end
         | 
| 966 | 
            +
                      channel.attach do
         | 
| 967 | 
            +
                        channel.detach
         | 
| 968 | 
            +
                      end
         | 
| 969 | 
            +
                    end
         | 
| 970 | 
            +
             | 
| 971 | 
            +
                    context 'on failure' do
         | 
| 972 | 
            +
                      let(:client_options) { default_options.merge(log_level: :none) }
         | 
| 973 | 
            +
             | 
| 974 | 
            +
                      it 'has a reason Error object when there is an error on the channel' do
         | 
| 975 | 
            +
                        channel.on(:failed) do |channel_state_change|
         | 
| 976 | 
            +
                          expect(channel_state_change.reason).to be_a(Ably::Exceptions::BaseAblyException)
         | 
| 977 | 
            +
                          stop_reactor
         | 
| 978 | 
            +
                        end
         | 
| 979 | 
            +
                        channel.attach do
         | 
| 980 | 
            +
                          error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
         | 
| 981 | 
            +
                          client.connection.manager.error_received_from_server error
         | 
| 982 | 
            +
                        end
         | 
| 983 | 
            +
                      end
         | 
| 984 | 
            +
                    end
         | 
| 985 | 
            +
                  end
         | 
| 986 | 
            +
                end
         | 
| 841 987 | 
             
              end
         | 
| 842 988 | 
             
            end
         |