ably 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.ruby-version.old +1 -0
- data/.travis.yml +0 -2
- data/Rakefile +22 -4
- data/SPEC.md +1676 -0
- data/ably.gemspec +1 -1
- data/lib/ably.rb +0 -8
- data/lib/ably/auth.rb +54 -46
- data/lib/ably/exceptions.rb +19 -5
- data/lib/ably/logger.rb +1 -1
- data/lib/ably/models/error_info.rb +1 -1
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
- data/lib/ably/models/message.rb +15 -12
- data/lib/ably/models/message_encoders/base.rb +6 -5
- data/lib/ably/models/message_encoders/base64.rb +1 -0
- data/lib/ably/models/message_encoders/cipher.rb +6 -3
- data/lib/ably/models/message_encoders/json.rb +1 -0
- data/lib/ably/models/message_encoders/utf8.rb +2 -9
- data/lib/ably/models/nil_logger.rb +20 -0
- data/lib/ably/models/paginated_resource.rb +5 -2
- data/lib/ably/models/presence_message.rb +21 -12
- data/lib/ably/models/protocol_message.rb +22 -6
- data/lib/ably/modules/ably.rb +11 -0
- data/lib/ably/modules/async_wrapper.rb +2 -0
- data/lib/ably/modules/conversions.rb +23 -3
- data/lib/ably/modules/encodeable.rb +2 -1
- data/lib/ably/modules/enum.rb +2 -0
- data/lib/ably/modules/event_emitter.rb +7 -1
- data/lib/ably/modules/event_machine_helpers.rb +2 -0
- data/lib/ably/modules/http_helpers.rb +2 -0
- data/lib/ably/modules/model_common.rb +12 -2
- data/lib/ably/modules/state_emitter.rb +76 -0
- data/lib/ably/modules/state_machine.rb +53 -0
- data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
- data/lib/ably/modules/uses_state_machine.rb +74 -0
- data/lib/ably/realtime.rb +4 -2
- data/lib/ably/realtime/channel.rb +51 -58
- data/lib/ably/realtime/channel/channel_manager.rb +91 -0
- data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
- data/lib/ably/realtime/client.rb +70 -26
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
- data/lib/ably/realtime/connection.rb +135 -92
- data/lib/ably/realtime/connection/connection_manager.rb +216 -33
- data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
- data/lib/ably/realtime/models/nil_channel.rb +10 -1
- data/lib/ably/realtime/presence.rb +336 -92
- data/lib/ably/rest.rb +2 -2
- data/lib/ably/rest/channel.rb +13 -4
- data/lib/ably/rest/client.rb +138 -38
- data/lib/ably/rest/middleware/logger.rb +24 -3
- data/lib/ably/rest/presence.rb +12 -7
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
- data/spec/acceptance/realtime/channel_spec.rb +461 -120
- data/spec/acceptance/realtime/client_spec.rb +119 -0
- data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
- data/spec/acceptance/realtime/connection_spec.rb +571 -97
- data/spec/acceptance/realtime/message_spec.rb +347 -333
- data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
- data/spec/acceptance/realtime/presence_spec.rb +769 -239
- data/spec/acceptance/realtime/stats_spec.rb +14 -22
- data/spec/acceptance/realtime/time_spec.rb +16 -20
- data/spec/acceptance/rest/auth_spec.rb +425 -364
- data/spec/acceptance/rest/base_spec.rb +108 -176
- data/spec/acceptance/rest/channel_spec.rb +89 -89
- data/spec/acceptance/rest/channels_spec.rb +30 -32
- data/spec/acceptance/rest/client_spec.rb +273 -0
- data/spec/acceptance/rest/encoders_spec.rb +185 -0
- data/spec/acceptance/rest/message_spec.rb +186 -163
- data/spec/acceptance/rest/presence_spec.rb +150 -111
- data/spec/acceptance/rest/stats_spec.rb +45 -40
- data/spec/acceptance/rest/time_spec.rb +8 -10
- data/spec/rspec_config.rb +10 -1
- data/spec/shared/client_initializer_behaviour.rb +212 -0
- data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
- data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/support/api_helper.rb +11 -0
- data/spec/support/event_machine_helper.rb +101 -3
- data/spec/support/markdown_spec_formatter.rb +90 -0
- data/spec/support/private_api_formatter.rb +36 -0
- data/spec/support/protocol_helper.rb +32 -0
- data/spec/support/random_helper.rb +15 -0
- data/spec/support/test_app.rb +4 -0
- data/spec/unit/auth_spec.rb +68 -0
- data/spec/unit/logger_spec.rb +77 -66
- data/spec/unit/models/error_info_spec.rb +1 -1
- data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
- data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
- data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
- data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
- data/spec/unit/models/message_spec.rb +160 -15
- data/spec/unit/models/paginated_resource_spec.rb +29 -27
- data/spec/unit/models/presence_message_spec.rb +163 -20
- data/spec/unit/models/protocol_message_spec.rb +43 -8
- data/spec/unit/modules/async_wrapper_spec.rb +2 -3
- data/spec/unit/modules/conversions_spec.rb +1 -1
- data/spec/unit/modules/enum_spec.rb +2 -3
- data/spec/unit/modules/event_emitter_spec.rb +62 -5
- data/spec/unit/modules/state_emitter_spec.rb +283 -0
- data/spec/unit/realtime/channel_spec.rb +107 -2
- data/spec/unit/realtime/channels_spec.rb +1 -0
- data/spec/unit/realtime/client_spec.rb +8 -48
- data/spec/unit/realtime/connection_spec.rb +3 -3
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
- data/spec/unit/realtime/presence_spec.rb +13 -4
- data/spec/unit/realtime/realtime_spec.rb +0 -11
- data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
- data/spec/unit/rest/channel_spec.rb +109 -0
- data/spec/unit/rest/channels_spec.rb +4 -3
- data/spec/unit/rest/client_spec.rb +30 -125
- data/spec/unit/rest/rest_spec.rb +10 -0
- data/spec/unit/util/crypto_spec.rb +10 -5
- data/spec/unit/util/pub_sub_spec.rb +5 -5
- metadata +44 -12
- data/spec/integration/modules/state_emitter_spec.rb +0 -80
- data/spec/integration/rest/auth.rb +0 -9
data/lib/ably/rest.rb
CHANGED
@@ -36,8 +36,8 @@ module Ably
|
|
36
36
|
# # create a new client authenticating with basic auth and a client_id
|
37
37
|
# client = Ably::Rest.new(api_key: 'key.id:secret', client_id: 'john')
|
38
38
|
#
|
39
|
-
def self.new(options, &
|
40
|
-
Ably::Rest::Client.new(options, &
|
39
|
+
def self.new(options, &token_request_block)
|
40
|
+
Ably::Rest::Client.new(options, &token_request_block)
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
data/lib/ably/rest/channel.rb
CHANGED
@@ -23,6 +23,8 @@ module Ably
|
|
23
23
|
# @option channel_options [Hash] :cipher_params A hash of options to configure the encryption. *:key* is required, all other options are optional. See {Ably::Util::Crypto#initialize} for a list of `cipher_params` options
|
24
24
|
#
|
25
25
|
def initialize(client, name, channel_options = {})
|
26
|
+
ensure_utf_8 :name, name
|
27
|
+
|
26
28
|
@client = client
|
27
29
|
@name = name
|
28
30
|
@options = channel_options.clone.freeze
|
@@ -34,6 +36,8 @@ module Ably
|
|
34
36
|
# @param data [String] The message payload
|
35
37
|
# @return [Boolean] true if the message was published, otherwise false
|
36
38
|
def publish(name, data)
|
39
|
+
ensure_utf_8 :name, name
|
40
|
+
|
37
41
|
payload = {
|
38
42
|
name: name,
|
39
43
|
data: data
|
@@ -62,19 +66,18 @@ module Ably
|
|
62
66
|
url = "#{base_path}/messages"
|
63
67
|
options = options.dup
|
64
68
|
|
65
|
-
|
66
|
-
[:start, :end].each { |option| merge_options[option] = as_since_epoch(options[option]) if options.has_key?(option) }
|
69
|
+
[:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) }
|
67
70
|
|
68
71
|
paginated_options = {
|
69
72
|
coerce_into: 'Ably::Models::Message',
|
70
73
|
async_blocking_operations: options.delete(:async_blocking_operations),
|
71
74
|
}
|
72
75
|
|
73
|
-
response = client.get(url, options
|
76
|
+
response = client.get(url, options)
|
74
77
|
|
75
78
|
Ably::Models::PaginatedResource.new(response, url, client, paginated_options) do |message|
|
76
79
|
message.tap do |message|
|
77
|
-
message
|
80
|
+
decode_message message
|
78
81
|
end
|
79
82
|
end
|
80
83
|
end
|
@@ -90,6 +93,12 @@ module Ably
|
|
90
93
|
def base_path
|
91
94
|
"/channels/#{CGI.escape(name)}"
|
92
95
|
end
|
96
|
+
|
97
|
+
def decode_message(message)
|
98
|
+
message.decode self
|
99
|
+
rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e
|
100
|
+
client.logger.error "Decoding Error on channel '#{name}', message event name '#{message.name}'. #{e.class.name}: #{e.message}"
|
101
|
+
end
|
93
102
|
end
|
94
103
|
end
|
95
104
|
end
|
data/lib/ably/rest/client.rb
CHANGED
@@ -8,34 +8,56 @@ module Ably
|
|
8
8
|
module Rest
|
9
9
|
# Client for the Ably REST API
|
10
10
|
#
|
11
|
-
# @!attribute [r] auth
|
12
|
-
# @return {Ably::Auth} authentication object configured for this connection
|
13
11
|
# @!attribute [r] client_id
|
14
12
|
# @return [String] A client ID, used for identifying this client for presence purposes
|
15
13
|
# @!attribute [r] auth_options
|
16
14
|
# @return [Hash] {Ably::Auth} options configured for this client
|
17
|
-
# @!attribute [r] environment
|
18
|
-
# @return [String] May contain 'sandbox' when testing the client library against an alternate Ably environment
|
19
|
-
# @!attribute [r] log_level
|
20
|
-
# @return [Logger::Severity] Log level configured for this {Client}
|
21
|
-
# @!attribute [r] channels
|
22
|
-
# @return [Aby::Rest::Channels] The collection of {Ably::Rest::Channel}s that have been created
|
23
|
-
# @!attribute [r] protocol
|
24
|
-
# @return [Symbol] The protocol configured for this client, either binary `:msgpack` or text based `:json`
|
25
15
|
#
|
26
16
|
class Client
|
27
17
|
include Ably::Modules::Conversions
|
28
18
|
include Ably::Modules::HttpHelpers
|
29
19
|
extend Forwardable
|
30
20
|
|
21
|
+
# Default Ably domain for REST
|
31
22
|
DOMAIN = 'rest.ably.io'
|
32
23
|
|
33
|
-
|
24
|
+
# Configuration for connection retry attempts
|
25
|
+
CONNECTION_RETRY = {
|
26
|
+
single_request_open_timeout: 4,
|
27
|
+
single_request_timeout: 15,
|
28
|
+
cumulative_request_open_timeout: 10,
|
29
|
+
max_retry_attempts: 3
|
30
|
+
}.freeze
|
31
|
+
|
34
32
|
def_delegators :auth, :client_id, :auth_options
|
35
33
|
|
36
|
-
#
|
34
|
+
# Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
|
35
|
+
# @return [String]
|
36
|
+
attr_reader :environment
|
37
|
+
|
38
|
+
# The protocol configured for this client, either binary `:msgpack` or text based `:json`
|
39
|
+
# @return [Symbol]
|
40
|
+
attr_reader :protocol
|
41
|
+
|
42
|
+
# {Ably::Auth} authentication object configured for this connection
|
43
|
+
# @return [Ably::Auth]
|
44
|
+
attr_reader :auth
|
45
|
+
|
46
|
+
# The collection of {Ably::Rest::Channel}s that have been created
|
47
|
+
# @return [Aby::Rest::Channels]
|
48
|
+
attr_reader :channels
|
49
|
+
|
50
|
+
# Log level configured for this {Client}
|
51
|
+
# @return [Logger::Severity]
|
52
|
+
attr_reader :log_level
|
53
|
+
|
54
|
+
# The custom host that is being used if it was provided with the option `:rest_host` when the {Client} was created
|
55
|
+
# @return [String,Nil]
|
56
|
+
attr_reader :custom_host
|
57
|
+
|
37
58
|
# The registered encoders that are used to encode and decode message payloads
|
38
59
|
# @return [Array<Ably::Models::MessageEncoder::Base>]
|
60
|
+
# @api private
|
39
61
|
attr_reader :encoders
|
40
62
|
|
41
63
|
# The additional options passed to this Client's #initialize method not available as attributes of this class
|
@@ -47,12 +69,13 @@ module Ably
|
|
47
69
|
#
|
48
70
|
# @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key
|
49
71
|
# @option options (see Ably::Auth#authorise)
|
50
|
-
# @option options [Boolean] :tls TLS is used by default, providing a value of false
|
72
|
+
# @option options [Boolean] :tls TLS is used by default, providing a value of false disables TLS. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
|
51
73
|
# @option options [String] :api_key API key comprising the key ID and key secret in a single string
|
74
|
+
# @option options [Boolean] :use_token_auth Will force Basic Auth if set to false, and TOken auth if set to true
|
52
75
|
# @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment
|
53
76
|
# @option options [Symbol] :protocol Protocol used to communicate with Ably, :json and :msgpack currently supported. Defaults to :msgpack
|
54
77
|
# @option options [Boolean] :use_binary_protocol Protocol used to communicate with Ably, defaults to true and uses MessagePack protocol. This option will overide :protocol option
|
55
|
-
# @option options [Logger::Severity,Symbol] :log_level Log level for the standard Logger that outputs to STDOUT. Defaults to Logger::ERROR, can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG)
|
78
|
+
# @option options [Logger::Severity,Symbol] :log_level Log level for the standard Logger that outputs to STDOUT. Defaults to Logger::ERROR, can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG) or :none
|
56
79
|
# @option options [Logger] :logger A custom logger can be used however it must adhere to the Ruby Logger interface, see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html
|
57
80
|
#
|
58
81
|
# @yield (see Ably::Auth#authorise)
|
@@ -68,7 +91,9 @@ module Ably
|
|
68
91
|
# # create a new client and configure a client ID used for presence
|
69
92
|
# client = Ably::Rest::Client.new(api_key: 'key.id:secret', client_id: 'john')
|
70
93
|
#
|
71
|
-
def initialize(options, &
|
94
|
+
def initialize(options, &token_request_block)
|
95
|
+
raise ArgumentError, 'Options Hash is expected' if options.nil?
|
96
|
+
|
72
97
|
options = options.clone
|
73
98
|
if options.kind_of?(String)
|
74
99
|
options = { api_key: options }
|
@@ -80,8 +105,13 @@ module Ably
|
|
80
105
|
@debug_http = options.delete(:debug_http)
|
81
106
|
@log_level = options.delete(:log_level) || ::Logger::ERROR
|
82
107
|
@custom_logger = options.delete(:logger)
|
108
|
+
@custom_host = options.delete(:rest_host)
|
83
109
|
|
84
|
-
|
110
|
+
if @log_level == :none
|
111
|
+
@custom_logger = Ably::Models::NilLogger.new
|
112
|
+
else
|
113
|
+
@log_level = ::Logger.const_get(log_level.to_s.upcase) if log_level.kind_of?(Symbol) || log_level.kind_of?(String)
|
114
|
+
end
|
85
115
|
|
86
116
|
options.delete(:use_binary_protocol).tap do |use_binary_protocol|
|
87
117
|
if use_binary_protocol == true
|
@@ -93,7 +123,7 @@ module Ably
|
|
93
123
|
raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
|
94
124
|
|
95
125
|
@options = options.freeze
|
96
|
-
@auth = Auth.new(self, options, &
|
126
|
+
@auth = Auth.new(self, options, &token_request_block)
|
97
127
|
@channels = Ably::Rest::Channels.new(self)
|
98
128
|
@encoders = []
|
99
129
|
|
@@ -157,10 +187,7 @@ module Ably
|
|
157
187
|
# @!attribute [r] endpoint
|
158
188
|
# @return [URI::Generic] Default Ably REST endpoint used for all requests
|
159
189
|
def endpoint
|
160
|
-
|
161
|
-
scheme: use_tls? ? "https" : "http",
|
162
|
-
host: [@environment, DOMAIN].compact.join('-')
|
163
|
-
)
|
190
|
+
endpoint_for_host(custom_host || [@environment, DOMAIN].compact.join('-'))
|
164
191
|
end
|
165
192
|
|
166
193
|
# @!attribute [r] logger
|
@@ -191,7 +218,9 @@ module Ably
|
|
191
218
|
# @api private
|
192
219
|
def register_encoder(encoder)
|
193
220
|
encoder_klass = if encoder.kind_of?(String)
|
194
|
-
|
221
|
+
encoder.split('::').inject(Kernel) do |base, klass_name|
|
222
|
+
base.public_send(:const_get, klass_name)
|
223
|
+
end
|
195
224
|
else
|
196
225
|
encoder
|
197
226
|
end
|
@@ -207,37 +236,104 @@ module Ably
|
|
207
236
|
protocol == :msgpack
|
208
237
|
end
|
209
238
|
|
239
|
+
# Connection used to make HTTP requests
|
240
|
+
#
|
241
|
+
# @param [Hash] options
|
242
|
+
# @option options [Boolean] :use_fallback when true, one of the fallback connections is used randomly, see {Ably::FALLBACK_HOSTS}
|
243
|
+
#
|
244
|
+
# @return [Faraday::Connection]
|
245
|
+
#
|
246
|
+
# @api private
|
247
|
+
def connection(options = {})
|
248
|
+
if options[:use_fallback]
|
249
|
+
fallback_connection
|
250
|
+
else
|
251
|
+
@connection ||= Faraday.new(endpoint.to_s, connection_options)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Fallback connection used to make HTTP requests.
|
256
|
+
# Note, each request uses a random and then subsequent random {Ably::FALLBACK_HOSTS fallback host}
|
257
|
+
#
|
258
|
+
# @return [Faraday::Connection]
|
259
|
+
#
|
260
|
+
# @api private
|
261
|
+
def fallback_connection
|
262
|
+
unless @fallback_connections
|
263
|
+
@fallback_connections = Ably::FALLBACK_HOSTS.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
|
264
|
+
end
|
265
|
+
@fallback_index ||= 0
|
266
|
+
|
267
|
+
@fallback_connections[@fallback_index % @fallback_connections.count].tap do
|
268
|
+
@fallback_index += 1
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
210
272
|
private
|
211
273
|
def request(method, path, params = {}, options = {})
|
212
|
-
|
213
|
-
|
274
|
+
options = options.clone
|
275
|
+
if options.delete(:disable_automatic_reauthorise) == true
|
276
|
+
send_request(method, path, params, options)
|
277
|
+
else
|
278
|
+
reauthorise_on_authorisation_failure do
|
279
|
+
send_request(method, path, params, options)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Sends HTTP request to connection end point
|
285
|
+
# Connection failures will automatically be reattempted until thresholds are met
|
286
|
+
def send_request(method, path, params, options)
|
287
|
+
max_retry_attempts = CONNECTION_RETRY.fetch(:max_retry_attempts)
|
288
|
+
cumulative_timeout = CONNECTION_RETRY.fetch(:cumulative_request_open_timeout)
|
289
|
+
requested_at = Time.now
|
290
|
+
retry_count = 0
|
291
|
+
|
292
|
+
begin
|
293
|
+
use_fallback = can_fallback_to_alternate_ably_host? && retry_count > 0
|
294
|
+
|
295
|
+
connection(use_fallback: use_fallback).send(method, path, params) do |request|
|
214
296
|
unless options[:send_auth_header] == false
|
215
297
|
request.headers[:authorization] = auth.auth_header
|
216
298
|
end
|
217
299
|
end
|
300
|
+
|
301
|
+
rescue Faraday::TimeoutError, Faraday::ClientError => error
|
302
|
+
time_passed = Time.now - requested_at
|
303
|
+
if can_fallback_to_alternate_ably_host? && retry_count < max_retry_attempts && time_passed <= cumulative_timeout
|
304
|
+
retry_count += 1
|
305
|
+
retry
|
306
|
+
end
|
307
|
+
|
308
|
+
case error
|
309
|
+
when Faraday::TimeoutError
|
310
|
+
raise Ably::Exceptions::ConnectionTimeoutError.new(error.message, nil, 80014, error)
|
311
|
+
when Faraday::ClientError
|
312
|
+
raise Ably::Exceptions::ConnectionError.new(error.message, nil, 80000, error)
|
313
|
+
end
|
218
314
|
end
|
219
315
|
end
|
220
316
|
|
221
317
|
def reauthorise_on_authorisation_failure
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
attempts += 1
|
227
|
-
if attempts == 1 && e.code == 40140 && auth.token_renewable?
|
318
|
+
yield
|
319
|
+
rescue Ably::Exceptions::InvalidRequest => e
|
320
|
+
if e.code == 40140
|
321
|
+
if auth.token_renewable?
|
228
322
|
auth.authorise force: true
|
229
|
-
|
323
|
+
yield
|
230
324
|
else
|
231
325
|
raise Ably::Exceptions::InvalidToken.new(e.message, e.status, e.code)
|
232
326
|
end
|
327
|
+
else
|
328
|
+
raise e
|
233
329
|
end
|
234
330
|
end
|
235
331
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
332
|
+
def endpoint_for_host(host)
|
333
|
+
URI::Generic.build(
|
334
|
+
scheme: use_tls? ? 'https' : 'http',
|
335
|
+
host: host
|
336
|
+
)
|
241
337
|
end
|
242
338
|
|
243
339
|
# Return a Hash of connection options to initiate the Faraday::Connection with
|
@@ -252,8 +348,8 @@ module Ably
|
|
252
348
|
user_agent: user_agent
|
253
349
|
},
|
254
350
|
request: {
|
255
|
-
open_timeout:
|
256
|
-
timeout:
|
351
|
+
open_timeout: CONNECTION_RETRY.fetch(:single_request_open_timeout),
|
352
|
+
timeout: CONNECTION_RETRY.fetch(:single_request_timeout)
|
257
353
|
}
|
258
354
|
}
|
259
355
|
end
|
@@ -275,6 +371,10 @@ module Ably
|
|
275
371
|
end
|
276
372
|
end
|
277
373
|
|
374
|
+
def can_fallback_to_alternate_ably_host?
|
375
|
+
!custom_host && !environment
|
376
|
+
end
|
377
|
+
|
278
378
|
def initialize_default_encoders
|
279
379
|
Ably::Models::MessageEncoders.register_default_encoders self
|
280
380
|
end
|
@@ -18,19 +18,40 @@ module Ably
|
|
18
18
|
|
19
19
|
def call(env)
|
20
20
|
debug "=> URL: #{env.method} #{env.url}, Headers: #{dump_headers env.request_headers}"
|
21
|
-
debug "=> Body: #{env
|
22
|
-
|
21
|
+
debug "=> Body: #{body_for(env)}"
|
22
|
+
super
|
23
23
|
end
|
24
24
|
|
25
25
|
def on_complete(env)
|
26
26
|
debug "<= Status: #{env.status}, Headers: #{dump_headers env.response_headers}"
|
27
|
-
debug "<= Body: #{env
|
27
|
+
debug "<= Body: #{body_for(env)}"
|
28
28
|
end
|
29
29
|
|
30
30
|
private
|
31
31
|
def dump_headers(headers)
|
32
32
|
headers.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
33
33
|
end
|
34
|
+
|
35
|
+
def body_for(env)
|
36
|
+
return '' if !env.body || env.body.empty?
|
37
|
+
|
38
|
+
if env.request_headers['Content-Type'] == 'application/x-msgpack'
|
39
|
+
MessagePack.unpack(env.body)
|
40
|
+
else
|
41
|
+
env.body
|
42
|
+
end
|
43
|
+
|
44
|
+
rescue StandardError
|
45
|
+
"Error displaying body: (as hex) '#{readable_body(env.body)}'"
|
46
|
+
end
|
47
|
+
|
48
|
+
def readable_body(body)
|
49
|
+
if body.respond_to?(:encoding) && body.encoding == Encoding::ASCII_8BIT
|
50
|
+
body.unpack('H*')
|
51
|
+
else
|
52
|
+
body
|
53
|
+
end
|
54
|
+
end
|
34
55
|
end
|
35
56
|
end
|
36
57
|
end
|
data/lib/ably/rest/presence.rb
CHANGED
@@ -41,8 +41,8 @@ module Ably
|
|
41
41
|
response = client.get(base_path, options)
|
42
42
|
|
43
43
|
Ably::Models::PaginatedResource.new(response, base_path, client, paginated_options) do |presence_message|
|
44
|
-
presence_message.tap do |
|
45
|
-
|
44
|
+
presence_message.tap do |presence_message|
|
45
|
+
decode_message presence_message
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
@@ -61,19 +61,18 @@ module Ably
|
|
61
61
|
url = "#{base_path}/history"
|
62
62
|
options = options.dup
|
63
63
|
|
64
|
-
|
65
|
-
[:start, :end].each { |option| merge_options[option] = as_since_epoch(options[option]) if options.has_key?(option) }
|
64
|
+
[:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) }
|
66
65
|
|
67
66
|
paginated_options = {
|
68
67
|
coerce_into: 'Ably::Models::PresenceMessage',
|
69
68
|
async_blocking_operations: options.delete(:async_blocking_operations),
|
70
69
|
}
|
71
70
|
|
72
|
-
response = client.get(url, options
|
71
|
+
response = client.get(url, options)
|
73
72
|
|
74
73
|
Ably::Models::PaginatedResource.new(response, url, client, paginated_options) do |presence_message|
|
75
|
-
presence_message.tap do |
|
76
|
-
|
74
|
+
presence_message.tap do |presence_message|
|
75
|
+
decode_message presence_message
|
77
76
|
end
|
78
77
|
end
|
79
78
|
end
|
@@ -82,6 +81,12 @@ module Ably
|
|
82
81
|
def base_path
|
83
82
|
"/channels/#{CGI.escape(channel.name)}/presence"
|
84
83
|
end
|
84
|
+
|
85
|
+
def decode_message(presence_message)
|
86
|
+
presence_message.decode self.channel
|
87
|
+
rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e
|
88
|
+
client.logger.error "Decoding Error on presence channel '#{channel.name}', presence message client_id '#{presence_message.client_id}'. #{e.class.name}: #{e.message}"
|
89
|
+
end
|
85
90
|
end
|
86
91
|
end
|
87
92
|
end
|
data/lib/ably/version.rb
CHANGED
@@ -1,136 +1,152 @@
|
|
1
|
+
# encoding: utf-8
|
1
2
|
require 'spec_helper'
|
2
|
-
require 'securerandom'
|
3
3
|
|
4
|
-
describe Ably::Realtime::Channel do
|
5
|
-
|
4
|
+
describe Ably::Realtime::Channel, '#history', :event_machine do
|
5
|
+
vary_by_protocol do
|
6
|
+
let(:default_options) { options.merge(api_key: api_key, environment: environment, protocol: protocol) }
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
let(:default_options) { options.merge(api_key: api_key, environment: environment, protocol: protocol) }
|
8
|
+
let(:client) { Ably::Realtime::Client.new(default_options) }
|
9
|
+
let(:channel) { client.channel(channel_name) }
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
let(:channel) { client.channel(channel_name) }
|
15
|
-
|
16
|
-
let(:client2) do
|
17
|
-
Ably::Realtime::Client.new(default_options)
|
18
|
-
end
|
19
|
-
let(:channel2) { client2.channel(channel_name) }
|
11
|
+
let(:client2) { Ably::Realtime::Client.new(default_options) }
|
12
|
+
let(:channel2) { client2.channel(channel_name) }
|
20
13
|
|
21
|
-
|
22
|
-
|
23
|
-
|
14
|
+
let(:channel_name) { "persisted:#{random_str(2)}" }
|
15
|
+
let(:payload) { random_str }
|
16
|
+
let(:messages) { [] }
|
24
17
|
|
25
|
-
|
18
|
+
let(:options) { { :protocol => :json } }
|
26
19
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
20
|
+
it 'returns a Deferrable' do
|
21
|
+
channel.publish('event', payload) do |message|
|
22
|
+
history = channel.history
|
23
|
+
expect(history).to be_a(EventMachine::Deferrable)
|
24
|
+
history.callback do |messages|
|
25
|
+
expect(messages.count).to eql(1)
|
26
|
+
expect(messages).to be_a(Ably::Models::PaginatedResource)
|
27
|
+
stop_reactor
|
33
28
|
end
|
34
29
|
end
|
30
|
+
end
|
35
31
|
|
32
|
+
context 'with a single client publishing and receiving' do
|
36
33
|
it 'retrieves real-time history' do
|
37
|
-
|
38
|
-
channel.
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
stop_reactor
|
43
|
-
end
|
34
|
+
channel.publish('event', payload) do |message|
|
35
|
+
channel.history do |history|
|
36
|
+
expect(history.length).to eql(1)
|
37
|
+
expect(history[0].data).to eql(payload)
|
38
|
+
stop_reactor
|
44
39
|
end
|
45
40
|
end
|
46
41
|
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'with two clients publishing messages on the same channel' do
|
45
|
+
it 'retrieves real-time history on both channels' do
|
46
|
+
channel.publish('event', payload) do |message|
|
47
|
+
channel2.publish('event', payload) do |message|
|
48
|
+
channel.history do |history|
|
49
|
+
expect(history.length).to eql(2)
|
50
|
+
expect(history.map(&:data).uniq).to eql([payload])
|
47
51
|
|
48
|
-
|
49
|
-
|
50
|
-
channel.publish('event', payload) do |message|
|
51
|
-
channel2.publish('event', payload) do |message|
|
52
|
-
channel2.history do |history|
|
53
|
-
expect(history.length).to eql(2)
|
54
|
-
expect(history.map(&:data).uniq).to eql([payload])
|
52
|
+
channel2.history do |history_2|
|
53
|
+
expect(history_2.length).to eql(2)
|
55
54
|
stop_reactor
|
56
55
|
end
|
57
56
|
end
|
58
57
|
end
|
59
58
|
end
|
60
59
|
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'with lots of messages published with a single client and channel' do
|
63
|
+
let(:messages_sent) { 40 }
|
64
|
+
let(:limit) { 20 }
|
61
65
|
|
62
|
-
|
63
|
-
|
64
|
-
|
66
|
+
def ensure_message_history_direction_and_paging_is_correct(direction)
|
67
|
+
channel.history(direction: direction, limit: limit) do |history|
|
68
|
+
expect(history.length).to eql(limit)
|
69
|
+
limit.times do |index|
|
70
|
+
expect(history[index].data).to eql("history#{index}")
|
71
|
+
end
|
65
72
|
|
66
|
-
|
67
|
-
channel.history(direction: direction, limit: limit) do |history|
|
73
|
+
history.next_page do |history|
|
68
74
|
expect(history.length).to eql(limit)
|
69
75
|
limit.times do |index|
|
70
|
-
expect(history[index].data).to eql("history#{index}")
|
76
|
+
expect(history[index].data).to eql("history#{index + limit}")
|
71
77
|
end
|
78
|
+
expect(history.last_page?).to eql(true)
|
72
79
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
78
|
-
expect(history.last_page?).to eql(true)
|
80
|
+
stop_reactor
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
79
84
|
|
80
|
-
|
85
|
+
context 'as one ProtocolMessage' do
|
86
|
+
it 'retrieves history forwards with pagination through :limit option' do
|
87
|
+
messages_sent.times do |index|
|
88
|
+
channel.publish('event', "history#{index}") do
|
89
|
+
next unless index == messages_sent - 1
|
90
|
+
ensure_message_history_direction_and_paging_is_correct :forwards
|
81
91
|
end
|
82
92
|
end
|
83
93
|
end
|
84
94
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
check_limited_history :forwards if index == messages_sent - 1
|
91
|
-
end
|
92
|
-
end
|
95
|
+
it 'retrieves history backwards with pagination through :limit option' do
|
96
|
+
messages_sent.times.to_a.reverse.each do |index|
|
97
|
+
channel.publish('event', "history#{index}") do
|
98
|
+
next unless index == 0
|
99
|
+
ensure_message_history_direction_and_paging_is_correct :backwards
|
93
100
|
end
|
94
101
|
end
|
102
|
+
end
|
103
|
+
end
|
95
104
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
105
|
+
context 'in multiple ProtocolMessages' do
|
106
|
+
it 'retrieves limited history forwards with pagination' do
|
107
|
+
messages_sent.times do |index|
|
108
|
+
EventMachine.add_timer(index.to_f / 10) do
|
109
|
+
channel.publish('event', "history#{index}") do
|
110
|
+
next unless index == messages_sent - 1
|
111
|
+
ensure_message_history_direction_and_paging_is_correct :forwards
|
102
112
|
end
|
103
113
|
end
|
104
114
|
end
|
105
115
|
end
|
106
116
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
check_limited_history :forwards if index == messages_sent - 1
|
114
|
-
end
|
115
|
-
end
|
117
|
+
it 'retrieves limited history backwards with pagination' do
|
118
|
+
messages_sent.times.to_a.reverse.each do |index|
|
119
|
+
EventMachine.add_timer((messages_sent - index).to_f / 10) do
|
120
|
+
channel.publish('event', "history#{index}") do
|
121
|
+
next unless index == 0
|
122
|
+
ensure_message_history_direction_and_paging_is_correct :backwards if index == 0
|
116
123
|
end
|
117
124
|
end
|
118
125
|
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'and REST history' do
|
130
|
+
let(:batches) { 3 }
|
131
|
+
let(:messages_per_batch) { 10 }
|
119
132
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
133
|
+
it 'return the same results with unique matching message IDs' do
|
134
|
+
batches.times do |batch|
|
135
|
+
EventMachine.add_timer(batch.to_f / batches.to_f) do
|
136
|
+
messages_per_batch.times { channel.publish('event', 'data') }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
channel.subscribe('event') do |message|
|
141
|
+
messages << message
|
142
|
+
if messages.count == batches * messages_per_batch
|
143
|
+
channel.history do |history|
|
144
|
+
expect(history.map(&:id).sort).to eql(messages.map(&:id).sort)
|
145
|
+
stop_reactor
|
128
146
|
end
|
129
147
|
end
|
130
148
|
end
|
131
149
|
end
|
132
|
-
|
133
|
-
skip 'ensure REST history message IDs match ProtocolMessage wrapped message IDs via Realtime'
|
134
150
|
end
|
135
151
|
end
|
136
152
|
end
|