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.
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