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.
- checksums.yaml +4 -4
- data/ably-rest.gemspec +2 -1
- data/lib/submodules/ably-ruby/.travis.yml +6 -4
- data/lib/submodules/ably-ruby/CHANGELOG.md +52 -61
- data/lib/submodules/ably-ruby/README.md +10 -0
- data/lib/submodules/ably-ruby/SPEC.md +1473 -852
- data/lib/submodules/ably-ruby/ably.gemspec +2 -1
- data/lib/submodules/ably-ruby/lib/ably/auth.rb +57 -25
- data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +34 -8
- data/lib/submodules/ably-ruby/lib/ably/logger.rb +10 -1
- data/lib/submodules/ably-ruby/lib/ably/models/auth_details.rb +42 -0
- data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +18 -4
- data/lib/submodules/ably-ruby/lib/ably/models/connection_details.rb +6 -3
- data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +4 -3
- data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +1 -1
- data/lib/submodules/ably-ruby/lib/ably/models/message.rb +12 -1
- data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +101 -97
- data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +13 -1
- data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +20 -3
- data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +7 -3
- data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +17 -7
- data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +29 -14
- data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +7 -4
- data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -4
- data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +7 -3
- data/lib/submodules/ably-ruby/lib/ably/realtime.rb +2 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +79 -31
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +62 -26
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +154 -65
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
- data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +16 -3
- data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
- data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +108 -49
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +165 -59
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -10
- data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +67 -45
- data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +198 -36
- data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +30 -6
- data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
- data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +3 -3
- data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +21 -8
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +1 -3
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +2 -2
- data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
- data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +1 -1
- data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +26 -0
- data/lib/submodules/ably-ruby/lib/ably/version.rb +2 -2
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +416 -99
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +5 -3
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +1011 -160
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +2 -2
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +436 -97
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +52 -23
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +5 -3
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1160 -105
- data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +151 -22
- data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +88 -27
- data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +42 -15
- data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +4 -4
- data/lib/submodules/ably-ruby/spec/rspec_config.rb +2 -1
- data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +2 -2
- data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +6 -2
- data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +20 -4
- data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +32 -1
- data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +4 -11
- data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +28 -2
- data/lib/submodules/ably-ruby/spec/unit/models/auth_details_spec.rb +49 -0
- data/lib/submodules/ably-ruby/spec/unit/models/channel_state_change_spec.rb +23 -3
- data/lib/submodules/ably-ruby/spec/unit/models/connection_details_spec.rb +12 -1
- data/lib/submodules/ably-ruby/spec/unit/models/connection_state_change_spec.rb +15 -4
- data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +34 -2
- data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +73 -2
- data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +64 -6
- data/lib/submodules/ably-ruby/spec/unit/models/token_details_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/unit/models/token_request_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -1
- data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +69 -0
- data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +149 -22
- data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +9 -3
- data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +8 -5
- data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +4 -3
- data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +3 -3
- 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
|
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
|
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
|
-
|
157
|
-
|
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
|
-
|
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(
|
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
|
-
#
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
213
|
-
#
|
214
|
-
def
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
142
|
-
|
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
|
-
|
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
|
-
|
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.
|
211
|
-
|
212
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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'
|
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}'"
|
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
|
-
|
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
|
-
!
|
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
|
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
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
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
|
-
#
|
53
|
-
|
54
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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.
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
113
|
-
|
197
|
+
def realtime_request_timeout
|
198
|
+
connection.defaults.fetch(:realtime_request_timeout)
|
114
199
|
end
|
115
200
|
|
116
|
-
def
|
117
|
-
|
201
|
+
def channel_retry_timeout
|
202
|
+
connection.defaults.fetch(:channel_retry_timeout)
|
118
203
|
end
|
119
204
|
|
120
|
-
def
|
121
|
-
|
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
|
-
|
128
|
-
|
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
|
146
|
-
|
147
|
-
|
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
|
-
|
151
|
-
if
|
152
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
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.
|
163
|
-
|
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
|
|