ably-rest 0.9.3 → 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 (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