ably 0.7.0 → 0.7.1

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