ably-rest 0.9.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/ably-rest.gemspec +2 -1
  3. data/lib/submodules/ably-ruby/.travis.yml +6 -4
  4. data/lib/submodules/ably-ruby/CHANGELOG.md +52 -61
  5. data/lib/submodules/ably-ruby/README.md +10 -0
  6. data/lib/submodules/ably-ruby/SPEC.md +1473 -852
  7. data/lib/submodules/ably-ruby/ably.gemspec +2 -1
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +57 -25
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +34 -8
  10. data/lib/submodules/ably-ruby/lib/ably/logger.rb +10 -1
  11. data/lib/submodules/ably-ruby/lib/ably/models/auth_details.rb +42 -0
  12. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +18 -4
  13. data/lib/submodules/ably-ruby/lib/ably/models/connection_details.rb +6 -3
  14. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +1 -1
  16. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +12 -1
  17. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +101 -97
  18. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +13 -1
  19. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +7 -3
  21. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +17 -7
  22. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +29 -14
  23. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +7 -4
  24. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -4
  25. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +7 -3
  26. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +2 -0
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +79 -31
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +62 -26
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +154 -65
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +16 -3
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +108 -49
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +165 -59
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +67 -45
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +198 -36
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +30 -6
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  42. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +3 -3
  43. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +21 -8
  44. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +1 -3
  45. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +2 -2
  46. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  47. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +1 -1
  48. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +26 -0
  49. data/lib/submodules/ably-ruby/lib/ably/version.rb +2 -2
  50. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +416 -99
  51. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  52. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +1011 -160
  53. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +2 -2
  54. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  55. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +436 -97
  56. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +52 -23
  57. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  58. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1160 -105
  59. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +151 -22
  60. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +1 -1
  61. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +88 -27
  62. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +42 -15
  63. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +4 -4
  64. data/lib/submodules/ably-ruby/spec/rspec_config.rb +2 -1
  65. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +2 -2
  66. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +6 -2
  67. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +20 -4
  68. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +32 -1
  69. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +4 -11
  70. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +28 -2
  71. data/lib/submodules/ably-ruby/spec/unit/models/auth_details_spec.rb +49 -0
  72. data/lib/submodules/ably-ruby/spec/unit/models/channel_state_change_spec.rb +23 -3
  73. data/lib/submodules/ably-ruby/spec/unit/models/connection_details_spec.rb +12 -1
  74. data/lib/submodules/ably-ruby/spec/unit/models/connection_state_change_spec.rb +15 -4
  75. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +34 -2
  76. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +73 -2
  77. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +64 -6
  78. data/lib/submodules/ably-ruby/spec/unit/models/token_details_spec.rb +1 -1
  79. data/lib/submodules/ably-ruby/spec/unit/models/token_request_spec.rb +1 -1
  80. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -1
  81. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +69 -0
  82. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +149 -22
  83. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +9 -3
  84. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +1 -1
  85. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +8 -5
  86. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  87. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +4 -3
  88. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +3 -3
  90. metadata +7 -5
@@ -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
@@ -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,13 +44,15 @@ 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
  #
@@ -68,41 +70,86 @@ module Ably
68
70
  # end
69
71
  #
70
72
  def authorize(token_params = nil, auth_options = nil, &success_callback)
71
- async_wrap(success_callback) do
72
- auth_sync.authorize(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)
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
121
  # @deprecated Use {#authorize} instead
81
122
  def authorise(*args, &block)
82
- logger.warn "Auth#authorise is deprecated and will be removed in 1.0. Please use Auth#authorize instead"
123
+ logger.warn { "Auth#authorise is deprecated and will be removed in 1.0. Please use Auth#authorize instead" }
83
124
  authorize(*args, &block)
84
125
  end
85
126
 
86
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
+ #
87
131
  # @param (see Ably::Auth#authorize)
88
132
  # @option (see Ably::Auth#authorize)
89
133
  # @return [Ably::Models::TokenDetails]
90
134
  #
91
135
  def authorize_sync(token_params = nil, auth_options = nil)
92
- auth_sync.authorize(token_params, auth_options, &method(:upgrade_authentication_block).to_proc)
136
+ @authorization_in_flight = true
137
+ auth_sync.authorize(token_params, auth_options)
138
+ ensure
139
+ @authorization_in_flight = false
140
+ end
141
+
142
+ # @api private
143
+ def authorization_in_flight?
144
+ @authorization_in_flight
93
145
  end
94
146
 
95
147
  # @deprecated Use {#authorize_sync} instead
96
148
  def authorise_sync(*args)
97
- logger.warn "Auth#authorise_sync is deprecated and will be removed in 1.0. Please use Auth#authorize_sync instead"
149
+ logger.warn { "Auth#authorise_sync is deprecated and will be removed in 1.0. Please use Auth#authorize_sync instead" }
98
150
  authorize_sync(*args)
99
151
  end
100
152
 
101
- # def_delegator :auth_sync, :request_token, :request_token_sync
102
- # def_delegator :auth_sync, :create_token_request, :create_token_request_sync
103
- # def_delegator :auth_sync, :auth_header, :auth_header_sync
104
- # def_delegator :auth_sync, :auth_params, :auth_params_sync
105
-
106
153
  # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests
107
154
  #
108
155
  # @param (see Ably::Auth#request_token)
@@ -186,7 +233,16 @@ module Ably
186
233
  # @yield [Hash] Auth params for a new Realtime connection
187
234
  #
188
235
  def auth_params(&success_callback)
189
- 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
190
246
  auth_params_sync
191
247
  end
192
248
  end
@@ -209,22 +265,14 @@ module Ably
209
265
  @client
210
266
  end
211
267
 
212
- # If authorize is called with true, this block is executed so that it
213
- # can perform the authentication upgrade
214
- def upgrade_authentication_block(new_token)
215
- # This block is called if the authorisation was forced
216
- if client.connection.connected? || client.connection.connecting?
217
- logger.debug "Realtime::Auth - authorize was called so forcibly disconnecting transport to initiate auth upgrade"
218
- block = Proc.new do
219
- if client.connection.transport
220
- logger.debug "Realtime::Auth - current transport disconnected"
221
- client.connection.transport.disconnect
222
- else
223
- EventMachine.add_timer(0.1, &block)
224
- end
225
- end
226
- block.call
227
- 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
+ )
228
276
  end
229
277
  end
230
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)
231
+ end
232
+
233
+ if failed? || connection.closing? || connection.failed?
234
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:detaching))
213
235
  end
214
236
 
215
- raise exception_for_state_change_to(:detaching) if failed?
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,25 +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
337
  message.decode(client.encoders, options) do |encode_error, error_message|
303
338
  client.logger.error error_message
304
- emit :error, encode_error
305
339
  end
306
340
  emit_message message.name, message
307
341
  end
308
342
 
309
- on(STATE.Attached) do
343
+ unsafe_on(STATE.Attached) do
310
344
  process_queue
311
345
  end
312
346
  end
@@ -319,15 +353,18 @@ module Ably
319
353
  create_message(raw_msg).tap do |message|
320
354
  next if message.client_id.nil?
321
355
  if message.client_id == '*'
322
- 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')
323
360
  end
324
361
  unless client.auth.can_assume_client_id?(message.client_id)
325
- 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}'")
326
363
  end
327
364
  end
328
365
  end
329
366
 
330
- queue.push(*messages)
367
+ __queue__.push(*messages)
331
368
 
332
369
  if attached?
333
370
  process_queue
@@ -369,14 +406,14 @@ module Ably
369
406
  end
370
407
 
371
408
  def messages_in_queue?
372
- !queue.empty?
409
+ !__queue__.empty?
373
410
  end
374
411
 
375
412
  # Move messages from Channel Queue into Outgoing Connection Queue
376
413
  def process_queue
377
414
  condition = -> { attached? && messages_in_queue? }
378
415
  non_blocking_loop_while(condition) do
379
- 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)
380
417
  end
381
418
  end
382
419
 
@@ -392,7 +429,6 @@ module Ably
392
429
  Ably::Models::Message(message.dup).tap do |msg|
393
430
  msg.encode(client.encoders, options) do |encode_error, error_message|
394
431
  client.logger.error error_message
395
- emit :error, encode_error
396
432
  end
397
433
  end
398
434
  end
@@ -12,8 +12,6 @@ module Ably::Realtime
12
12
  def initialize(channel, connection)
13
13
  @channel = channel
14
14
  @connection = connection
15
-
16
- setup_connection_event_handlers
17
15
  end
18
16
 
19
17
  # Commence attachment
@@ -25,64 +23,130 @@ module Ably::Realtime
25
23
  end
26
24
 
27
25
  # Commence attachment
28
- def detach(error = nil)
26
+ def detach(error, previous_state)
29
27
  if connection.closed? || connection.connecting? || connection.suspended?
30
28
  channel.transition_state_machine :detached, reason: error
31
29
  elsif can_transition_to?(:detached)
32
- send_detach_protocol_message
30
+ send_detach_protocol_message previous_state
33
31
  end
34
32
  end
35
33
 
36
34
  # Channel is attached, notify presence if sync is expected
37
35
  def attached(attached_protocol_message)
38
- if attached_protocol_message.has_presence_flag?
39
- channel.presence.manager.sync_expected
40
- else
41
- channel.presence.manager.sync_not_expected
36
+ # If no attached ProtocolMessage then this attached request was triggered by the client
37
+ # library, such as returning to attached whne detach has failed
38
+ if attached_protocol_message
39
+ update_presence_sync_state_following_attached attached_protocol_message
40
+ channel.set_attached_serial attached_protocol_message.channel_serial
42
41
  end
43
- channel.set_attached_serial attached_protocol_message.channel_serial
44
42
  end
45
43
 
46
44
  # An error has occurred on the channel
47
- def emit_error(error)
48
- logger.error "ChannelManager: Channel '#{channel.name}' error: #{error}"
49
- channel.emit :error, error
45
+ def log_channel_error(error)
46
+ logger.error { "ChannelManager: Channel '#{channel.name}' error: #{error}" }
50
47
  end
51
48
 
52
- # Detach a channel as a result of an error
53
- def suspend(error)
54
- channel.transition_state_machine! :detaching, reason: error
49
+ # Request channel to be reattached by sending an attach protocol message
50
+ # @param [Hash] options
51
+ # @option options [Ably::Models::ErrorInfo] :reason
52
+ def request_reattach(options = {})
53
+ reason = options[:reason]
54
+ send_attach_protocol_message
55
+ logger.debug { "Explicit channel reattach request sent to Ably due to #{reason}" }
56
+ channel.set_channel_error_reason(reason) if reason
57
+ channel.transition_state_machine! :attaching, reason: reason unless channel.attaching?
55
58
  end
56
59
 
57
- # When a channel is no longer attached or has failed,
58
- # all messages awaiting an ACK response should fail immediately
59
- def fail_messages_awaiting_ack(error)
60
- # Allow a short time for other queued operations to complete before failing all messages
61
- EventMachine.add_timer(0.1) do
62
- error = Ably::Exceptions::MessageDeliveryFailed.new("Channel cannot publish messages whilst state is '#{channel.state}'") unless error
60
+ def duplicate_attached_received(protocol_message)
61
+ if protocol_message.error
62
+ channel.set_channel_error_reason protocol_message.error
63
+ log_channel_error protocol_message.error
64
+ end
65
+
66
+ if protocol_message.has_channel_resumed_flag?
67
+ logger.debug { "ChannelManager: Additional resumed ATTACHED message received for #{channel.state} channel '#{channel.name}'" }
68
+ else
69
+ channel.emit :update, Ably::Models::ChannelStateChange.new(
70
+ current: channel.state,
71
+ previous: channel.state,
72
+ event: Ably::Realtime::Channel::EVENT(:update),
73
+ reason: protocol_message.error,
74
+ resumed: false,
75
+ )
76
+ update_presence_sync_state_following_attached protocol_message
77
+ end
78
+
79
+ channel.set_attached_serial protocol_message.channel_serial
80
+ end
81
+
82
+ # Handle DETACED messages, see #RTL13 for server-initated detaches
83
+ def detached_received(reason)
84
+ case channel.state.to_sym
85
+ when :detaching
86
+ channel.transition_state_machine :detached, reason: reason
87
+ when :attached, :suspended
88
+ channel.transition_state_machine :attaching, reason: reason
89
+ else
90
+ logger.debug { "ChannelManager: DETACHED ProtocolMessage received, but no action to take as not DETACHING, ATTACHED OR SUSPENDED" }
91
+ end
92
+ end
93
+
94
+ # When continuity on the connection is interrupted or channel becomes suspended (implying loss of continuity)
95
+ # then all messages published but awaiting an ACK from Ably should be failed with a NACK
96
+ # @param [Hash] options
97
+ # @option options [Boolean] :immediately
98
+ def fail_messages_awaiting_ack(error, options = {})
99
+ immediately = options[:immediately] || false
100
+
101
+ fail_proc = Proc.new do
102
+ error = Ably::Exceptions::MessageDeliveryFailed.new("Continuity of connection was lost so published messages awaiting ACK have failed") unless error
63
103
  fail_messages_in_queue connection.__pending_message_ack_queue__, error
64
- fail_messages_in_queue connection.__outgoing_message_queue__, error
65
104
  end
105
+
106
+ # Allow a short time for other queued operations to complete before failing all messages
107
+ if immediately
108
+ fail_proc.call
109
+ else
110
+ EventMachine.add_timer(0.1) { fail_proc.call }
111
+ end
112
+ end
113
+
114
+ # When a channel becomes detached, suspended or failed,
115
+ # all queued messages should be failed immediately as we don't queue in
116
+ # any of those states
117
+ def fail_queued_messages(error)
118
+ error = Ably::Exceptions::MessageDeliveryFailed.new("Queued messages on channel '#{channel.name}' in state '#{channel.state}' will never be delivered") unless error
119
+ fail_messages_in_queue connection.__outgoing_message_queue__, error
120
+ channel.__queue__.each do |message|
121
+ nack_message message, error
122
+ end
123
+ channel.__queue__.clear
66
124
  end
67
125
 
68
126
  def fail_messages_in_queue(queue, error)
69
127
  queue.delete_if do |protocol_message|
70
- if protocol_message.channel == channel.name
71
- nack_messages protocol_message, error
72
- true
128
+ if protocol_message.action.match_any?(:presence, :message)
129
+ if protocol_message.channel == channel.name
130
+ nack_messages protocol_message, error
131
+ true
132
+ end
73
133
  end
74
134
  end
75
135
  end
76
136
 
77
137
  def nack_messages(protocol_message, error)
78
138
  (protocol_message.messages + protocol_message.presence).each do |message|
79
- logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}"
80
- message.fail error
139
+ nack_message message, error, protocol_message
81
140
  end
82
- logger.debug "Calling NACK failure callbacks for #{protocol_message.class.name} - #{protocol_message.to_json}"
141
+ logger.debug { "Calling NACK failure callbacks for #{protocol_message.class.name} - #{protocol_message.to_json}" }
83
142
  protocol_message.fail error
84
143
  end
85
144
 
145
+ def nack_message(message, error, protocol_message = nil)
146
+ logger.debug { "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json} #{"protocol message: #{protocol_message}" if protocol_message}" }
147
+ message.fail error
148
+ end
149
+
86
150
  def drop_pending_queue_from_ack(ack_protocol_message)
87
151
  message_serial_up_to = ack_protocol_message.message_serial + ack_protocol_message.count - 1
88
152
  connection.__pending_message_ack_queue__.drop_while do |protocol_message|
@@ -93,7 +157,23 @@ module Ably::Realtime
93
157
  end
94
158
  end
95
159
 
160
+ # If the connection is still connected and the channel still suspended after
161
+ # channel_retry_timeout has passed, then attempt to reattach automatically, see #RTL13b
162
+ def start_attach_from_suspended_timer
163
+ cancel_attach_from_suspended_timer
164
+ if connection.connected?
165
+ channel.unsafe_once { |event| cancel_attach_from_suspended_timer unless event == :update }
166
+ connection.unsafe_once { |event| cancel_attach_from_suspended_timer unless event == :update }
167
+
168
+ @attach_from_suspended_timer = EventMachine::Timer.new(channel_retry_timeout) do
169
+ channel.transition_state_machine! :attaching
170
+ end
171
+ end
172
+ end
173
+
96
174
  private
175
+ attr_reader :pending_state_change_timer
176
+
97
177
  def channel
98
178
  @channel
99
179
  end
@@ -104,63 +184,72 @@ module Ably::Realtime
104
184
 
105
185
  def_delegators :channel, :can_transition_to?
106
186
 
187
+ def cancel_attach_from_suspended_timer
188
+ @attach_from_suspended_timer.cancel if @attach_from_suspended_timer
189
+ @attach_from_suspended_timer = nil
190
+ end
191
+
107
192
  # If the connection has not previously connected, connect now
108
193
  def connect_if_connection_initialized
109
194
  connection.connect if connection.initialized?
110
195
  end
111
196
 
112
- def send_attach_protocol_message
113
- send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Attach
197
+ def realtime_request_timeout
198
+ connection.defaults.fetch(:realtime_request_timeout)
114
199
  end
115
200
 
116
- def send_detach_protocol_message
117
- send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Detach
201
+ def channel_retry_timeout
202
+ connection.defaults.fetch(:channel_retry_timeout)
118
203
  end
119
204
 
120
- def send_state_change_protocol_message(state)
121
- connection.send_protocol_message(
122
- action: state.to_i,
123
- channel: channel.name
124
- )
205
+ def send_attach_protocol_message
206
+ send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Attach, :suspended # move to suspended
125
207
  end
126
208
 
127
- # Any message sent before an ACK/NACK was received on the previous transport
128
- # needs to be resent to the Ably service so that a subsequent ACK/NACK is received.
129
- # It is up to Ably to ensure that duplicate messages are not retransmitted on the channel
130
- # base on the serial numbers
131
- #
132
- # TODO: Move this into the Connection class, it does not belong in a Channel class
133
- #
134
- # @api private
135
- def resend_pending_message_ack_queue
136
- connection.__pending_message_ack_queue__.delete_if do |protocol_message|
137
- if protocol_message.channel == channel.name
138
- connection.__outgoing_message_queue__ << protocol_message
139
- connection.__outgoing_protocol_msgbus__.publish :protocol_message
140
- true
141
- end
142
- end
209
+ def send_detach_protocol_message(previous_state)
210
+ send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Detach, previous_state # return to previous state if failed
143
211
  end
144
212
 
145
- def setup_connection_event_handlers
146
- connection.unsafe_on(:closed) do
147
- channel.transition_state_machine :detaching if can_transition_to?(:detaching)
213
+ def send_state_change_protocol_message(new_state, state_if_failed)
214
+ state_at_time_of_request = channel.state
215
+ @pending_state_change_timer = EventMachine::Timer.new(realtime_request_timeout) do
216
+ if channel.state == state_at_time_of_request
217
+ error = Ably::Models::ErrorInfo.new(code: 90007, message: "Channel #{new_state} operation failed (timed out)")
218
+ channel.transition_state_machine state_if_failed, reason: error
219
+ end
148
220
  end
149
221
 
150
- connection.unsafe_on(:suspended) do |error|
151
- if can_transition_to?(:detaching)
152
- channel.transition_state_machine :detaching, reason: Ably::Exceptions::ConnectionSuspended.new('Connection suspended', nil, 80002, error)
153
- end
222
+ channel.once_state_changed do
223
+ @pending_state_change_timer.cancel if @pending_state_change_timer
224
+ @pending_state_change_timer = nil
154
225
  end
155
226
 
156
- connection.unsafe_on(:failed) do |error|
157
- if can_transition_to?(:failed) && !channel.detached?
158
- channel.transition_state_machine :failed, reason: Ably::Exceptions::ConnectionFailed.new('Connection failed', nil, 80002, error)
227
+ resend_if_disconnected_and_connected = Proc.new do
228
+ connection.unsafe_once(:disconnected) do
229
+ next unless pending_state_change_timer
230
+ connection.unsafe_once(:connected) do
231
+ next unless pending_state_change_timer
232
+ connection.send_protocol_message(
233
+ action: new_state.to_i,
234
+ channel: channel.name
235
+ )
236
+ resend_if_disconnected_and_connected.call
237
+ end
159
238
  end
160
239
  end
240
+ resend_if_disconnected_and_connected.call
161
241
 
162
- connection.unsafe_on(:connected) do |error|
163
- resend_pending_message_ack_queue
242
+ connection.send_protocol_message(
243
+ action: new_state.to_i,
244
+ channel: channel.name
245
+ )
246
+ end
247
+
248
+ def update_presence_sync_state_following_attached(attached_protocol_message)
249
+ if attached_protocol_message.has_presence_flag?
250
+ channel.presence.manager.sync_expected
251
+ else
252
+ channel.presence.manager.sync_not_expected
164
253
  end
165
254
  end
166
255