ably 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.travis.yml +2 -2
  2. data/Rakefile +2 -0
  3. data/SPEC.md +230 -194
  4. data/ably.gemspec +2 -0
  5. data/lib/ably/auth.rb +7 -5
  6. data/lib/ably/models/idiomatic_ruby_wrapper.rb +5 -7
  7. data/lib/ably/models/paginated_resource.rb +14 -21
  8. data/lib/ably/models/protocol_message.rb +1 -1
  9. data/lib/ably/modules/ably.rb +4 -0
  10. data/lib/ably/modules/async_wrapper.rb +2 -2
  11. data/lib/ably/modules/channels_collection.rb +31 -8
  12. data/lib/ably/modules/conversions.rb +10 -0
  13. data/lib/ably/modules/enum.rb +2 -3
  14. data/lib/ably/modules/state_emitter.rb +8 -8
  15. data/lib/ably/modules/state_machine.rb +7 -3
  16. data/lib/ably/realtime/channel.rb +6 -5
  17. data/lib/ably/realtime/channel/channel_manager.rb +11 -10
  18. data/lib/ably/realtime/channel/channel_state_machine.rb +10 -9
  19. data/lib/ably/realtime/channels.rb +3 -0
  20. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +11 -1
  21. data/lib/ably/realtime/connection.rb +55 -16
  22. data/lib/ably/realtime/connection/connection_manager.rb +25 -8
  23. data/lib/ably/realtime/connection/connection_state_machine.rb +9 -9
  24. data/lib/ably/realtime/connection/websocket_transport.rb +2 -2
  25. data/lib/ably/realtime/presence.rb +16 -17
  26. data/lib/ably/util/crypto.rb +1 -1
  27. data/lib/ably/version.rb +1 -1
  28. data/spec/acceptance/realtime/channel_history_spec.rb +6 -5
  29. data/spec/acceptance/realtime/connection_failures_spec.rb +103 -27
  30. data/spec/acceptance/realtime/connection_spec.rb +81 -17
  31. data/spec/acceptance/realtime/presence_spec.rb +82 -30
  32. data/spec/acceptance/rest/auth_spec.rb +22 -19
  33. data/spec/acceptance/rest/client_spec.rb +4 -4
  34. data/spec/acceptance/rest/presence_spec.rb +12 -6
  35. data/spec/rspec_config.rb +9 -0
  36. data/spec/shared/model_behaviour.rb +1 -1
  37. data/spec/spec_helper.rb +4 -1
  38. data/spec/support/event_machine_helper.rb +26 -37
  39. data/spec/support/markdown_spec_formatter.rb +96 -68
  40. data/spec/support/rest_testapp_before_retry.rb +15 -0
  41. data/spec/support/test_app.rb +4 -0
  42. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +20 -2
  43. data/spec/unit/models/message_spec.rb +1 -1
  44. data/spec/unit/models/paginated_resource_spec.rb +15 -1
  45. data/spec/unit/modules/enum_spec.rb +10 -0
  46. data/spec/unit/realtime/channels_spec.rb +30 -0
  47. data/spec/unit/rest/channels_spec.rb +30 -0
  48. metadata +101 -35
  49. checksums.yaml +0 -7
data/ably.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ['lib']
20
20
 
21
21
  spec.add_runtime_dependency 'eventmachine', '~> 1.0'
22
+ spec.add_runtime_dependency 'em-http-request', '~> 1.1'
22
23
  spec.add_runtime_dependency 'statesman', '~> 1.0.0'
23
24
  spec.add_runtime_dependency 'faraday', '~> 0.9'
24
25
  spec.add_runtime_dependency 'json'
@@ -29,6 +30,7 @@ Gem::Specification.new do |spec|
29
30
  spec.add_development_dependency 'rake'
30
31
  spec.add_development_dependency 'redcarpet'
31
32
  spec.add_development_dependency 'rspec', '~> 3.1.0' # version lock, see event_machine_helper.rb#patch_example_block_with_surrounding_eventmachine_reactor
33
+ spec.add_development_dependency 'rspec-retry'
32
34
  spec.add_development_dependency 'yard'
33
35
  spec.add_development_dependency 'webmock'
34
36
  end
data/lib/ably/auth.rb CHANGED
@@ -37,6 +37,7 @@ module Ably
37
37
  # @param options (see Ably::Rest::Client#initialize)
38
38
  # @option (see Ably::Rest::Client#initialize)
39
39
  # @yield (see Ably::Rest::Client#initialize)
40
+ #
40
41
  def initialize(client, options, &token_request_block)
41
42
  auth_options = options.dup
42
43
 
@@ -140,12 +141,12 @@ module Ably
140
141
  # token_request
141
142
  # end
142
143
  #
143
- def request_token(options = {}, &token_request_block)
144
+ def request_token(options = {})
144
145
  token_options = self.auth_options.merge(options)
145
146
 
146
147
  auth_url = token_options.delete(:auth_url)
147
148
  token_request = if block_given?
148
- token_request_block.call(token_options)
149
+ yield token_options
149
150
  elsif default_token_block
150
151
  default_token_block.call(token_options)
151
152
  elsif auth_url
@@ -203,7 +204,7 @@ module Ably
203
204
 
204
205
  token_request = {
205
206
  id: request_key_id,
206
- client_id: client_id,
207
+ clientId: client_id,
207
208
  ttl: Ably::Models::Token::DEFAULTS[:ttl],
208
209
  timestamp: timestamp,
209
210
  capability: Ably::Models::Token::DEFAULTS[:capability],
@@ -217,7 +218,8 @@ module Ably
217
218
  ensure_utf_8 :nonce, token_request[:nonce], allow_nil: true
218
219
 
219
220
  token_request[:mac] = sign_params(token_request, request_key_secret)
220
- token_request
221
+
222
+ convert_to_mixed_case_hash(token_request)
221
223
  end
222
224
 
223
225
  def api_key
@@ -350,7 +352,7 @@ module Ably
350
352
  ).map { |t| "#{t}\n" }.join("")
351
353
 
352
354
  encode64(
353
- Digest::HMAC.digest(text, secret, Digest::SHA256)
355
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, text)
354
356
  )
355
357
  end
356
358
 
@@ -76,7 +76,7 @@ module Ably::Models
76
76
  hash[source_key_for(key)] = value
77
77
  end
78
78
 
79
- def fetch(key, default = nil, &missing_block)
79
+ def fetch(key, default = nil)
80
80
  if has_key?(key)
81
81
  self[key]
82
82
  else
@@ -107,15 +107,13 @@ module Ably::Models
107
107
  end
108
108
 
109
109
  # Method ensuring this {IdiomaticRubyWrapper} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
110
- def each(&block)
110
+ def each
111
+ return to_enum(:each) unless block_given?
112
+
111
113
  hash.each do |key, value|
112
114
  key = convert_to_snake_case_symbol(key)
113
115
  value = self[key]
114
- if block_given?
115
- block.call key, value
116
- else
117
- yield key, value
118
- end
116
+ yield key, value
119
117
  end
120
118
  end
121
119
 
@@ -5,7 +5,7 @@ module Ably::Models
5
5
  # Paging information is provided by Ably in the LINK HTTP headers
6
6
  class PaginatedResource
7
7
  include Enumerable
8
- include Ably::Modules::AsyncWrapper if defined?(EventMachine)
8
+ include Ably::Modules::AsyncWrapper if defined?(Ably::Realtime)
9
9
 
10
10
  # @param [Faraday::Response] http_response Initial HTTP response from an Ably request to a paged resource
11
11
  # @param [String] base_url Base URL for request that generated the http_response so that subsequent paged requests can be made
@@ -25,19 +25,9 @@ module Ably::Models
25
25
  @each_block = each_block
26
26
  @make_async = options.fetch(:async_blocking_operations, false)
27
27
 
28
- @body = if @coerce_into
29
- http_response.body.map do |item|
30
- @coerce_into.split('::').inject(Kernel) do |base, klass_name|
31
- base.public_send(:const_get, klass_name)
32
- end.new(item)
33
- end
34
- else
35
- http_response.body
36
- end
37
-
38
- @body = @body.map do |resource|
39
- each_block.call(resource)
40
- end if block_given?
28
+ @body = http_response.body
29
+ @body = coerce_items_into(body, @coerce_into) if @coerce_into
30
+ @body = body.map { |item| yield item } if block_given?
41
31
  end
42
32
 
43
33
  # Retrieve the first page of results.
@@ -100,13 +90,8 @@ module Ably::Models
100
90
 
101
91
  # Method to allow {PaginatedResource} to be {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
102
92
  def each(&block)
103
- body.each do |item|
104
- if block_given?
105
- block.call item
106
- else
107
- yield item
108
- end
109
- end
93
+ return to_enum(:each) unless block_given?
94
+ body.each(&block)
110
95
  end
111
96
 
112
97
  # First item in this page
@@ -134,6 +119,14 @@ module Ably::Models
134
119
  private
135
120
  attr_reader :body, :http_response, :base_url, :client, :coerce_into, :raw_body, :each_block, :make_async
136
121
 
122
+ def coerce_items_into(items, type_string)
123
+ items.map do |item|
124
+ @coerce_into.split('::').inject(Kernel) do |base, klass_name|
125
+ base.public_send(:const_get, klass_name)
126
+ end.new(item)
127
+ end
128
+ end
129
+
137
130
  def pagination_headers
138
131
  link_regex = %r{<(?<url>[^>]+)>; rel="(?<rel>[^"]+)"}
139
132
  @pagination_headers ||= begin
@@ -37,7 +37,7 @@ module Ably::Models
37
37
  #
38
38
  class ProtocolMessage
39
39
  include Ably::Modules::ModelCommon
40
- include EventMachine::Deferrable
40
+ include EventMachine::Deferrable if defined?(Ably::Realtime)
41
41
  extend Ably::Modules::Enum
42
42
 
43
43
  # Actions which are sent by the Ably Realtime API
@@ -8,4 +8,8 @@ module Ably
8
8
  # network failures either at the client, between the client and Ably, within an Ably data center, or at the IO domain registrar
9
9
  #
10
10
  FALLBACK_HOSTS = %w(A.ably-realtime.com B.ably-realtime.com C.ably-realtime.com D.ably-realtime.com E.ably-realtime.com)
11
+ INTERNET_CHECK = {
12
+ url: 'http://internet-up.ably.io/is-the-internet-up.txt', #ably-realtime.com
13
+ ok_text: 'yes'
14
+ }
11
15
  end
@@ -37,8 +37,8 @@ module Ably::Modules
37
37
  # @yield [Object] operation block that is run in a thread
38
38
  # @return [EventMachine::Deferrable]
39
39
  #
40
- def async_wrap(success_callback = nil, &operation)
41
- raise ArgumentError, "Operation block is missing" unless block_given?
40
+ def async_wrap(success_callback = nil)
41
+ raise ArgumentError, 'Block required' unless block_given?
42
42
 
43
43
  EventMachine::DefaultDeferrable.new.tap do |deferrable|
44
44
  deferrable.callback &success_callback if success_callback
@@ -2,6 +2,8 @@ module Ably::Modules
2
2
  # ChannelsCollection module provides common functionality to the Rest and Realtime Channels objects
3
3
  # such as #get, #[], #fetch, and #release
4
4
  module ChannelsCollection
5
+ include Enumerable
6
+
5
7
  def initialize(client, channel_klass)
6
8
  @client = client
7
9
  @channel_klass = channel_klass
@@ -11,11 +13,12 @@ module Ably::Modules
11
13
  # Return a Channel for the given name
12
14
  #
13
15
  # @param name [String] The name of the channel
14
- # @param channel_options [Hash] Channel options, currently reserved for Encryption options
16
+ # @param channel_options [Hash] Channel options including the encryption options
17
+ #
18
+ # @return [Channel]
15
19
  #
16
- # @return Channel
17
20
  def get(name, channel_options = {})
18
- @channels[name] ||= channel_klass.new(client, name, channel_options)
21
+ channels[name] ||= channel_klass.new(client, name, channel_options)
19
22
  end
20
23
  alias_method :[], :get
21
24
 
@@ -27,20 +30,40 @@ module Ably::Modules
27
30
  # @yield [options] (optional) if a missing_block is passed to this method and no channel exists matching the name, this block is called
28
31
  # @yieldparam [String] name of the missing channel
29
32
  #
30
- # @return Channel
33
+ # @return [Channel]
34
+ #
31
35
  def fetch(name, &missing_block)
32
- @channels.fetch(name, &missing_block)
36
+ channels.fetch(name, &missing_block)
33
37
  end
34
38
 
35
39
  # Destroy the Channel and releases the associated resources.
36
40
  #
37
41
  # Releasing a Channel is not typically necessary as a channel consumes no resources other than the memory footprint of the
38
42
  # Channel object. Explicitly release channels to free up resources if required
39
- def release(channel)
40
- @channels.delete(channel)
43
+ #
44
+ # @param name [String] The name of the channel
45
+ #
46
+ # @return [void]
47
+ #
48
+ def release(name)
49
+ channels.delete(name)
50
+ end
51
+
52
+ # @!attribute [r] length
53
+ # @return [Integer] number of channels created
54
+ def length
55
+ channels.length
56
+ end
57
+ alias_method :count, :length
58
+ alias_method :size, :length
59
+
60
+ # Method to allow {ChannelsCollection} to be {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
61
+ def each(&block)
62
+ return to_enum(:each) unless block_given?
63
+ channels.values.each(&block)
41
64
  end
42
65
 
43
66
  private
44
- attr_reader :client, :channel_klass
67
+ attr_reader :client, :channel_klass, :channels
45
68
  end
46
69
  end
@@ -58,6 +58,16 @@ module Ably::Modules
58
58
  join
59
59
  end
60
60
 
61
+ # Convert a Hash into a mixed case Hash objet
62
+ # i.e. { client_id: 1 } becomes { 'clientId' => 1 }
63
+ def convert_to_mixed_case_hash(hash, options = {})
64
+ raise ArgumentError, 'Hash expected' unless hash.kind_of?(Hash)
65
+ hash.each_with_object({}) do |pair, new_hash|
66
+ key, val = pair
67
+ new_hash[convert_to_mixed_case(key, options)] = val
68
+ end
69
+ end
70
+
61
71
  # Convert key to :snake_case from snakeCase
62
72
  def convert_to_snake_case_symbol(key)
63
73
  key.to_s.gsub(/::/, '/').
@@ -80,9 +80,8 @@ module Ably::Modules
80
80
 
81
81
  # Method ensuring this {Enum} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
82
82
  def each(&block)
83
- by_symbol.each do |key, value|
84
- yield value
85
- end
83
+ return to_enum(:each) unless block_given?
84
+ by_symbol.values.each(&block)
86
85
  end
87
86
 
88
87
  # The name provided in the constructor for this Enum
@@ -69,17 +69,17 @@ module Ably::Modules
69
69
  # @yield block is called if the state is matched immediately or once when the state is reached
70
70
  #
71
71
  # @return [void]
72
- def once_or_if(target_states, options = {}, &success_block)
73
- raise ArgumentError, 'Block is expected' unless block_given?
72
+ def once_or_if(target_states, options = {})
73
+ raise ArgumentError, 'Block required' unless block_given?
74
74
 
75
75
  if Array(target_states).any? { |target_state| state == target_state }
76
- success_block.call
76
+ yield
77
77
  else
78
78
  failure_block = options.fetch(:else, nil)
79
79
  failure_wrapper = nil
80
80
 
81
81
  success_wrapper = Proc.new do
82
- success_block.call
82
+ yield
83
83
  off &success_wrapper
84
84
  off &failure_wrapper if failure_wrapper
85
85
  end
@@ -107,7 +107,7 @@ module Ably::Modules
107
107
  #
108
108
  # @api private
109
109
  def once_state_changed(&block)
110
- raise ArgumentError, 'Block is expected' unless block_given?
110
+ raise ArgumentError, 'Block required' unless block_given?
111
111
 
112
112
  once_block = proc do |*args|
113
113
  off *self.class::STATE.map, &once_block
@@ -120,13 +120,13 @@ module Ably::Modules
120
120
  private
121
121
 
122
122
  # Returns an {EventMachine::Deferrable} and once the target state is reached, the
123
- # success_block if provided and {EventMachine::Deferrable#callback} is called.
123
+ # success block if provided and {EventMachine::Deferrable#callback} is called.
124
124
  # If the state changes to any other state, the {EventMachine::Deferrable#errback} is called.
125
125
  #
126
- def deferrable_for_state_change_to(target_state, &success_block)
126
+ def deferrable_for_state_change_to(target_state)
127
127
  EventMachine::DefaultDeferrable.new.tap do |deferrable|
128
128
  once_or_if(target_state, else: proc { |*args| deferrable.fail self, *args }) do
129
- success_block.call self if block_given?
129
+ yield self if block_given?
130
130
  deferrable.succeed self
131
131
  end
132
132
  end
@@ -13,6 +13,7 @@ module Ably::Modules
13
13
  include Statesman::Machine
14
14
  end
15
15
  klass.extend Ably::Modules::StatesmanMonkeyPatch
16
+ klass.extend ClassMethods
16
17
  end
17
18
 
18
19
  # Alternative to Statesman's #transition_to that:
@@ -45,9 +46,12 @@ module Ably::Modules
45
46
  Ably::Exceptions::StateChangeError.new(error_message, nil, 80020)
46
47
  end
47
48
 
48
- private
49
- def self.is_error_type?(error)
50
- error.kind_of?(Ably::Models::ErrorInfo) || error.kind_of?(StandardError)
49
+ module ClassMethods
50
+ private
51
+
52
+ def is_error_type?(error)
53
+ error.kind_of?(Ably::Models::ErrorInfo) || error.kind_of?(StandardError)
54
+ end
51
55
  end
52
56
  end
53
57
  end
@@ -214,6 +214,12 @@ module Ably
214
214
  @error_reason = error
215
215
  end
216
216
 
217
+ # Used by {Ably::Modules::StateEmitter} to debug state changes
218
+ # @api private
219
+ def logger
220
+ client.logger
221
+ end
222
+
217
223
  private
218
224
  attr_reader :queue, :subscriptions
219
225
 
@@ -276,11 +282,6 @@ module Ably
276
282
  client.rest_client.channel(name)
277
283
  end
278
284
 
279
- # Used by {Ably::Modules::StateEmitter} to debug state changes
280
- def logger
281
- client.logger
282
- end
283
-
284
285
  def connection
285
286
  client.connection
286
287
  end
@@ -20,10 +20,6 @@ module Ably::Realtime
20
20
  connection.on(:failed) do |error|
21
21
  channel.transition_state_machine :failed, error if can_transition_to?(:failed)
22
22
  end
23
-
24
- channel.on(:attached, :detached) do
25
- channel.set_failed_channel_error_reason nil
26
- end
27
23
  end
28
24
 
29
25
  # Commence attachment
@@ -35,9 +31,9 @@ module Ably::Realtime
35
31
  end
36
32
 
37
33
  # Commence attachment
38
- def detach
39
- if connection.closed?
40
- channel.transition_state_machine :detached
34
+ def detach(error = nil)
35
+ if connection.closed? || connection.connecting?
36
+ channel.transition_state_machine :detached, error
41
37
  elsif can_transition_to?(:detached)
42
38
  send_detach_protocol_message
43
39
  end
@@ -52,12 +48,17 @@ module Ably::Realtime
52
48
  end
53
49
  end
54
50
 
55
- # Channel has failed
56
- def failed(error)
57
- logger.error "Channel #{channel.name} error: #{error}"
51
+ # An error has occurred on the channel
52
+ def emit_error(error)
53
+ logger.error "ChannelManager: Channel '#{channel.name}' error: #{error}"
58
54
  channel.trigger :error, error
59
55
  end
60
56
 
57
+ # Detach a channel as a result of an error
58
+ def suspend(error)
59
+ channel.transition_state_machine! :detaching, error
60
+ end
61
+
61
62
  private
62
63
 
63
64
  attr_reader :channel, :connection
@@ -38,21 +38,22 @@ module Ably::Realtime
38
38
  channel.manager.sync current_transition.metadata
39
39
  end
40
40
 
41
- after_transition(to: [:detaching]) do |channel|
42
- channel.manager.detach
41
+ after_transition(to: [:detaching]) do |channel, current_transition|
42
+ channel.manager.detach current_transition.metadata
43
43
  end
44
44
 
45
- after_transition(to: [:failed]) do |channel, current_transition|
46
- channel.manager.failed current_transition.metadata
45
+ after_transition(to: [:detached]) do |channel, current_transition|
46
+ channel.manager.emit_error current_transition.metadata if current_transition.metadata
47
47
  end
48
48
 
49
- # Transitions responsible for updating channel#error_reason
50
- before_transition(to: [:failed]) do |channel, current_transition|
51
- channel.set_failed_channel_error_reason current_transition.metadata
49
+ after_transition(to: [:failed]) do |channel, current_transition|
50
+ channel.manager.emit_error current_transition.metadata
52
51
  end
53
52
 
54
- before_transition(to: [:attached, :detached]) do |channel, current_transition|
55
- channel.set_failed_channel_error_reason nil
53
+ # Transitions responsible for updating channel#error_reason
54
+ before_transition(to: [:attached, :detached, :failed]) do |channel, current_transition|
55
+ reason = current_transition.metadata if is_error_type?(current_transition.metadata)
56
+ channel.set_failed_channel_error_reason reason
56
57
  end
57
58
 
58
59
  private