ably 0.6.2 → 0.7.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.
- 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
|