ably 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version.old +1 -0
  4. data/.travis.yml +0 -2
  5. data/Rakefile +22 -4
  6. data/SPEC.md +1676 -0
  7. data/ably.gemspec +1 -1
  8. data/lib/ably.rb +0 -8
  9. data/lib/ably/auth.rb +54 -46
  10. data/lib/ably/exceptions.rb +19 -5
  11. data/lib/ably/logger.rb +1 -1
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
  14. data/lib/ably/models/message.rb +15 -12
  15. data/lib/ably/models/message_encoders/base.rb +6 -5
  16. data/lib/ably/models/message_encoders/base64.rb +1 -0
  17. data/lib/ably/models/message_encoders/cipher.rb +6 -3
  18. data/lib/ably/models/message_encoders/json.rb +1 -0
  19. data/lib/ably/models/message_encoders/utf8.rb +2 -9
  20. data/lib/ably/models/nil_logger.rb +20 -0
  21. data/lib/ably/models/paginated_resource.rb +5 -2
  22. data/lib/ably/models/presence_message.rb +21 -12
  23. data/lib/ably/models/protocol_message.rb +22 -6
  24. data/lib/ably/modules/ably.rb +11 -0
  25. data/lib/ably/modules/async_wrapper.rb +2 -0
  26. data/lib/ably/modules/conversions.rb +23 -3
  27. data/lib/ably/modules/encodeable.rb +2 -1
  28. data/lib/ably/modules/enum.rb +2 -0
  29. data/lib/ably/modules/event_emitter.rb +7 -1
  30. data/lib/ably/modules/event_machine_helpers.rb +2 -0
  31. data/lib/ably/modules/http_helpers.rb +2 -0
  32. data/lib/ably/modules/model_common.rb +12 -2
  33. data/lib/ably/modules/state_emitter.rb +76 -0
  34. data/lib/ably/modules/state_machine.rb +53 -0
  35. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  36. data/lib/ably/modules/uses_state_machine.rb +74 -0
  37. data/lib/ably/realtime.rb +4 -2
  38. data/lib/ably/realtime/channel.rb +51 -58
  39. data/lib/ably/realtime/channel/channel_manager.rb +91 -0
  40. data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
  41. data/lib/ably/realtime/client.rb +70 -26
  42. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
  43. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  44. data/lib/ably/realtime/connection.rb +135 -92
  45. data/lib/ably/realtime/connection/connection_manager.rb +216 -33
  46. data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
  47. data/lib/ably/realtime/models/nil_channel.rb +10 -1
  48. data/lib/ably/realtime/presence.rb +336 -92
  49. data/lib/ably/rest.rb +2 -2
  50. data/lib/ably/rest/channel.rb +13 -4
  51. data/lib/ably/rest/client.rb +138 -38
  52. data/lib/ably/rest/middleware/logger.rb +24 -3
  53. data/lib/ably/rest/presence.rb +12 -7
  54. data/lib/ably/version.rb +1 -1
  55. data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
  56. data/spec/acceptance/realtime/channel_spec.rb +461 -120
  57. data/spec/acceptance/realtime/client_spec.rb +119 -0
  58. data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
  59. data/spec/acceptance/realtime/connection_spec.rb +571 -97
  60. data/spec/acceptance/realtime/message_spec.rb +347 -333
  61. data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
  62. data/spec/acceptance/realtime/presence_spec.rb +769 -239
  63. data/spec/acceptance/realtime/stats_spec.rb +14 -22
  64. data/spec/acceptance/realtime/time_spec.rb +16 -20
  65. data/spec/acceptance/rest/auth_spec.rb +425 -364
  66. data/spec/acceptance/rest/base_spec.rb +108 -176
  67. data/spec/acceptance/rest/channel_spec.rb +89 -89
  68. data/spec/acceptance/rest/channels_spec.rb +30 -32
  69. data/spec/acceptance/rest/client_spec.rb +273 -0
  70. data/spec/acceptance/rest/encoders_spec.rb +185 -0
  71. data/spec/acceptance/rest/message_spec.rb +186 -163
  72. data/spec/acceptance/rest/presence_spec.rb +150 -111
  73. data/spec/acceptance/rest/stats_spec.rb +45 -40
  74. data/spec/acceptance/rest/time_spec.rb +8 -10
  75. data/spec/rspec_config.rb +10 -1
  76. data/spec/shared/client_initializer_behaviour.rb +212 -0
  77. data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
  78. data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
  79. data/spec/spec_helper.rb +9 -0
  80. data/spec/support/api_helper.rb +11 -0
  81. data/spec/support/event_machine_helper.rb +101 -3
  82. data/spec/support/markdown_spec_formatter.rb +90 -0
  83. data/spec/support/private_api_formatter.rb +36 -0
  84. data/spec/support/protocol_helper.rb +32 -0
  85. data/spec/support/random_helper.rb +15 -0
  86. data/spec/support/test_app.rb +4 -0
  87. data/spec/unit/auth_spec.rb +68 -0
  88. data/spec/unit/logger_spec.rb +77 -66
  89. data/spec/unit/models/error_info_spec.rb +1 -1
  90. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
  91. data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
  92. data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
  93. data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
  94. data/spec/unit/models/message_spec.rb +160 -15
  95. data/spec/unit/models/paginated_resource_spec.rb +29 -27
  96. data/spec/unit/models/presence_message_spec.rb +163 -20
  97. data/spec/unit/models/protocol_message_spec.rb +43 -8
  98. data/spec/unit/modules/async_wrapper_spec.rb +2 -3
  99. data/spec/unit/modules/conversions_spec.rb +1 -1
  100. data/spec/unit/modules/enum_spec.rb +2 -3
  101. data/spec/unit/modules/event_emitter_spec.rb +62 -5
  102. data/spec/unit/modules/state_emitter_spec.rb +283 -0
  103. data/spec/unit/realtime/channel_spec.rb +107 -2
  104. data/spec/unit/realtime/channels_spec.rb +1 -0
  105. data/spec/unit/realtime/client_spec.rb +8 -48
  106. data/spec/unit/realtime/connection_spec.rb +3 -3
  107. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
  108. data/spec/unit/realtime/presence_spec.rb +13 -4
  109. data/spec/unit/realtime/realtime_spec.rb +0 -11
  110. data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
  111. data/spec/unit/rest/channel_spec.rb +109 -0
  112. data/spec/unit/rest/channels_spec.rb +4 -3
  113. data/spec/unit/rest/client_spec.rb +30 -125
  114. data/spec/unit/rest/rest_spec.rb +10 -0
  115. data/spec/unit/util/crypto_spec.rb +10 -5
  116. data/spec/unit/util/pub_sub_spec.rb +5 -5
  117. metadata +44 -12
  118. data/spec/integration/modules/state_emitter_spec.rb +0 -80
  119. data/spec/integration/rest/auth.rb +0 -9
@@ -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, &auth_block)
40
- Ably::Rest::Client.new(options, &auth_block)
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
@@ -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
- merge_options = { live: true } # TODO: Remove live param as all history should be live
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.merge(merge_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.decode self
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
@@ -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
- attr_reader :environment, :protocol, :auth, :channels, :log_level
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
- # @api private
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 disbles TLS. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
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, &auth_block)
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
- @log_level = ::Logger.const_get(log_level.to_s.upcase) if log_level.kind_of?(Symbol) || log_level.kind_of?(String)
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, &auth_block)
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
- URI::Generic.build(
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
- Object.const_get(encoder)
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
- reauthorise_on_authorisation_failure do
213
- connection.send(method, path, params) do |request|
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
- attempts = 0
223
- begin
224
- yield
225
- rescue Ably::Exceptions::InvalidRequest => e
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
- retry
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
- # Return a Faraday::Connection to use to make HTTP requests
237
- #
238
- # @return [Faraday::Connection]
239
- def connection
240
- @connection ||= Faraday.new(endpoint.to_s, connection_options)
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: 5,
256
- timeout: 10
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.body}"
22
- @app.call env
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.body}"
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
@@ -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 |message|
45
- message.decode self.channel
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
- merge_options = { live: true } # TODO: Remove live param as all history should be live
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.merge(merge_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 |message|
76
- message.decode self.channel
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
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = '0.6.2'
2
+ VERSION = '0.7.0'
3
3
  end
@@ -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
- include RSpec::EventMachine
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
- [:json].each do |protocol| # :msgpack,
8
- context "over #{protocol}" do
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
- let(:client) do
12
- Ably::Realtime::Client.new(default_options)
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
- let(:channel_name) { "persisted:#{SecureRandom.hex(2)}" }
22
- let(:payload) { SecureRandom.hex(4) }
23
- let(:messages) { [] }
14
+ let(:channel_name) { "persisted:#{random_str(2)}" }
15
+ let(:payload) { random_str }
16
+ let(:messages) { [] }
24
17
 
25
- let(:options) { { :protocol => :json } }
18
+ let(:options) { { :protocol => :json } }
26
19
 
27
- it 'returns a Deferrable' do
28
- run_reactor do
29
- channel.publish('event', payload) do |message|
30
- expect(channel.history).to be_a(EventMachine::Deferrable)
31
- stop_reactor
32
- end
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
- run_reactor do
38
- channel.publish('event', payload) do |message|
39
- channel.history do |history|
40
- expect(history.length).to eql(1)
41
- expect(history[0].data).to eql(payload)
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
- it 'retrieves real-time history across two channels' do
49
- run_reactor do
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
- context 'with multiple messages' do
63
- let(:messages_sent) { 20 }
64
- let(:limit) { 10 }
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
- def check_limited_history(direction)
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
- history.next_page do |history|
74
- expect(history.length).to eql(limit)
75
- limit.times do |index|
76
- expect(history[index].data).to eql("history#{index + limit}")
77
- end
78
- expect(history.last_page?).to eql(true)
80
+ stop_reactor
81
+ end
82
+ end
83
+ end
79
84
 
80
- stop_reactor
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
- context 'as one ProtocolMessage' do
86
- it 'retrieves limited history forwards with pagination' do
87
- run_reactor(5) do
88
- messages_sent.times do |index|
89
- channel.publish('event', "history#{index}") do
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
- it 'retrieves limited history backwards with pagination' do
97
- run_reactor(5) do
98
- messages_sent.times.to_a.reverse.each do |index|
99
- channel.publish('event', "history#{index}") do
100
- check_limited_history :backwards if index == messages_sent - 1
101
- end
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
- context 'in multiple ProtocolMessages' do
108
- it 'retrieves limited history forwards with pagination' do
109
- run_reactor(5) do
110
- messages_sent.times do |index|
111
- EventMachine.add_timer(index.to_f / 10) do
112
- channel.publish('event', "history#{index}") do
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
- it 'retrieves limited history backwards with pagination' do
121
- run_reactor(5) do
122
- messages_sent.times.to_a.reverse.each do |index|
123
- EventMachine.add_timer((messages_sent - index).to_f / 10) do
124
- channel.publish('event', "history#{index}") do
125
- check_limited_history :backwards if index == 0
126
- end
127
- end
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