ably-rest 1.0.6 → 1.1.4.rc

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +23 -15
  4. data/ably-rest.gemspec +6 -6
  5. data/lib/submodules/ably-ruby/.editorconfig +14 -0
  6. data/lib/submodules/ably-ruby/.travis.yml +10 -8
  7. data/lib/submodules/ably-ruby/CHANGELOG.md +75 -3
  8. data/lib/submodules/ably-ruby/LICENSE +1 -3
  9. data/lib/submodules/ably-ruby/README.md +12 -7
  10. data/lib/submodules/ably-ruby/Rakefile +32 -0
  11. data/lib/submodules/ably-ruby/SPEC.md +1277 -835
  12. data/lib/submodules/ably-ruby/ably.gemspec +15 -10
  13. data/lib/submodules/ably-ruby/lib/ably/auth.rb +30 -4
  14. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +10 -4
  15. data/lib/submodules/ably-ruby/lib/ably/logger.rb +7 -1
  16. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +1 -1
  17. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +1 -1
  18. data/lib/submodules/ably-ruby/lib/ably/models/device_details.rb +87 -0
  19. data/lib/submodules/ably-ruby/lib/ably/models/device_push_details.rb +86 -0
  20. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +23 -2
  21. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  22. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +32 -2
  23. data/lib/submodules/ably-ruby/lib/ably/models/push_channel_subscription.rb +89 -0
  24. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +1 -1
  25. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +1 -1
  26. data/lib/submodules/ably-ruby/lib/ably/modules/exception_codes.rb +128 -0
  27. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +15 -2
  28. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -2
  29. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +1 -0
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +1 -1
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +24 -102
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +2 -6
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/publisher.rb +74 -0
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/push_channel.rb +62 -0
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +91 -3
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +34 -20
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +25 -9
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  42. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +4 -4
  43. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +3 -3
  44. data/lib/submodules/ably-ruby/lib/ably/realtime/push.rb +40 -0
  45. data/lib/submodules/ably-ruby/lib/ably/realtime/push/admin.rb +61 -0
  46. data/lib/submodules/ably-ruby/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  47. data/lib/submodules/ably-ruby/lib/ably/realtime/push/device_registrations.rb +105 -0
  48. data/lib/submodules/ably-ruby/lib/ably/rest.rb +1 -0
  49. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +53 -17
  50. data/lib/submodules/ably-ruby/lib/ably/rest/channel/push_channel.rb +62 -0
  51. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +162 -35
  52. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
  53. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  54. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -0
  55. data/lib/submodules/ably-ruby/lib/ably/rest/push.rb +42 -0
  56. data/lib/submodules/ably-ruby/lib/ably/rest/push/admin.rb +54 -0
  57. data/lib/submodules/ably-ruby/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  58. data/lib/submodules/ably-ruby/lib/ably/rest/push/device_registrations.rb +103 -0
  59. data/lib/submodules/ably-ruby/lib/ably/version.rb +7 -2
  60. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +245 -17
  61. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +26 -20
  62. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +177 -59
  63. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +153 -0
  64. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +72 -6
  65. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +129 -18
  66. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +36 -34
  67. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +201 -167
  68. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_admin_spec.rb +736 -0
  69. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_spec.rb +27 -0
  70. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +41 -3
  71. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +2 -2
  72. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +79 -4
  73. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +6 -0
  74. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +129 -10
  75. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +158 -6
  76. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_admin_spec.rb +952 -0
  77. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_spec.rb +25 -0
  78. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +1 -1
  79. data/lib/submodules/ably-ruby/spec/run_parallel_tests +33 -0
  80. data/lib/submodules/ably-ruby/spec/spec_helper.rb +1 -1
  81. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +9 -5
  82. data/lib/submodules/ably-ruby/spec/support/test_app.rb +2 -2
  83. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +10 -3
  84. data/lib/submodules/ably-ruby/spec/unit/models/device_details_spec.rb +102 -0
  85. data/lib/submodules/ably-ruby/spec/unit/models/device_push_details_spec.rb +101 -0
  86. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +51 -3
  87. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +17 -2
  88. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  90. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +1 -1
  91. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +13 -1
  92. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +1 -1
  93. data/lib/submodules/ably-ruby/spec/unit/realtime/push_channel_spec.rb +36 -0
  94. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +8 -1
  95. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +30 -0
  96. data/lib/submodules/ably-ruby/spec/unit/rest/push_channel_spec.rb +36 -0
  97. metadata +46 -21
@@ -1,6 +1,7 @@
1
1
  require 'ably/rest/channel'
2
2
  require 'ably/rest/channels'
3
3
  require 'ably/rest/client'
4
+ require 'ably/rest/push'
4
5
  require 'ably/rest/presence'
5
6
 
6
7
  require 'ably/models/message_encoders/base'
@@ -3,8 +3,6 @@ module Ably
3
3
  # The Ably Realtime service organises the traffic within any application into named channels.
4
4
  # Channels are the "unit" of message distribution; clients attach to channels to subscribe to messages, and every message broadcast by the service is associated with a unique channel.
5
5
  #
6
- # @!attribute [r] client
7
- # @return {Ably::Realtime::Client} Ably client associated with this channel
8
6
  # @!attribute [r] name
9
7
  # @return {String} channel name
10
8
  # @!attribute [r] options
@@ -12,7 +10,19 @@ module Ably
12
10
  class Channel
13
11
  include Ably::Modules::Conversions
14
12
 
15
- attr_reader :client, :name, :options
13
+ # Ably client associated with this channel
14
+ # @return [Ably::Realtime::Client]
15
+ # @api private
16
+ attr_reader :client
17
+
18
+ attr_reader :name, :options
19
+
20
+ # Push channel used for push notification (client-side)
21
+ # @return [Ably::Rest::Channel::PushChannel]
22
+ # @api private
23
+ attr_reader :push
24
+
25
+ IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH = 9 # See spec RSL1k1
16
26
 
17
27
  # Initialize a new Channel object
18
28
  #
@@ -27,17 +37,17 @@ module Ably
27
37
  update_options channel_options
28
38
  @client = client
29
39
  @name = name
40
+ @push = PushChannel.new(self)
30
41
  end
31
42
 
32
- # Publish one or more messages to the channel.
33
- #
34
- # @param name [String, Array<Ably::Models::Message|Hash>, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs
35
- # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
36
- # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
43
+ # Publish one or more messages to the channel. Three overloaded forms
44
+ # @param name [String, Array<Ably::Models::Message|Hash>, Ably::Models::Message, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs, or a single Ably::Model::Message object
45
+ # @param data [String, ByteArray, Hash, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument, in which case an optional hash of query parameters
46
+ # @param attributes [Hash, nil] Optional additional message attributes such as :extras, :id, :client_id or :connection_id, applied when name attribute is nil or a string (Deprecated, will be removed in 2.0 in favour of constructing a Message object)
37
47
  # @return [Boolean] true if the message was published, otherwise false
38
48
  #
39
49
  # @example
40
- # # Publish a single message
50
+ # # Publish a single message with (name, data) form
41
51
  # channel.publish 'click', { x: 1, y: 2 }
42
52
  #
43
53
  # # Publish an array of message Hashes
@@ -54,16 +64,28 @@ module Ably
54
64
  # ]
55
65
  # channel.publish messages
56
66
  #
57
- def publish(name, data = nil, attributes = {})
58
- messages = if name.kind_of?(Enumerable)
59
- name
67
+ # # Publish a single Ably::Models::Message object, with a query params
68
+ # # specifying quickAck: true
69
+ # message = Ably::Models::Message(name: 'click', { x: 1, y: 2 })
70
+ # channel.publish message, quickAck: 'true'
71
+ #
72
+ def publish(first, second = nil, third = {})
73
+ messages, qs_params = if first.kind_of?(Enumerable)
74
+ # ([Message], qs_params) form
75
+ [first, second]
76
+ elsif first.kind_of?(Ably::Models::Message)
77
+ # (Message, qs_params) form
78
+ [[first], second]
60
79
  else
61
- name = ensure_utf_8(:name, name, allow_nil: true)
62
- ensure_supported_payload data
63
- [{ name: name, data: data }.merge(attributes)]
80
+ # (name, data, attributes) form
81
+ first = ensure_utf_8(:name, first, allow_nil: true)
82
+ ensure_supported_payload second
83
+ # RSL1h - attributes as an extra method parameter is extra-spec but need to
84
+ # keep it for backcompat until version 2
85
+ [[{ name: first, data: second }.merge(third)], nil]
64
86
  end
65
87
 
66
- payload = messages.map do |message|
88
+ payload = messages.each_with_index.map do |message, index|
67
89
  Ably::Models::Message(message.dup).tap do |msg|
68
90
  msg.encode client.encoders, options
69
91
 
@@ -75,9 +97,21 @@ module Ably
75
97
  raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{msg.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'")
76
98
  end
77
99
  end.as_json
100
+ end.tap do |payload|
101
+ if client.idempotent_rest_publishing
102
+ # We cannot mutate for idempotent publishing if one or more messages already has an ID
103
+ if payload.all? { |msg| !msg['id'] }
104
+ # Mutate the JSON to support idempotent publishing where a Message.id does not exist
105
+ idempotent_publish_id = SecureRandom.base64(IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH)
106
+ payload.each_with_index do |msg, idx|
107
+ msg['id'] = "#{idempotent_publish_id}:#{idx}"
108
+ end
109
+ end
110
+ end
78
111
  end
79
112
 
80
- response = client.post("#{base_path}/publish", payload.length == 1 ? payload.first : payload)
113
+ options = qs_params ? { qs_params: qs_params } : {}
114
+ response = client.post("#{base_path}/publish", payload.length == 1 ? payload.first : payload, options)
81
115
 
82
116
  [201, 204].include?(response.status)
83
117
  end
@@ -141,3 +175,5 @@ module Ably
141
175
  end
142
176
  end
143
177
  end
178
+
179
+ require 'ably/rest/channel/push_channel'
@@ -0,0 +1,62 @@
1
+ module Ably::Rest
2
+ class Channel
3
+ # A push channel used for push notifications
4
+ # Each PushChannel maps to exactly one Rest Channel
5
+ #
6
+ # @!attribute [r] channel
7
+ # @return [Ably::Rest::Channel] Underlying channel object
8
+ #
9
+ class PushChannel
10
+ attr_reader :channel
11
+
12
+ def initialize(channel)
13
+ raise ArgumentError, "Unsupported channel type '#{channel.class}'" unless channel.kind_of?(Ably::Rest::Channel)
14
+ @channel = channel
15
+ end
16
+
17
+ def to_s
18
+ "<PushChannel: name=#{channel.name}>"
19
+ end
20
+
21
+ # Subscribe local device for push notifications on this channel
22
+ #
23
+ # @note This is unsupported in the Ruby library
24
+ def subscribe_device(*args)
25
+ raise_unsupported
26
+ end
27
+
28
+ # Subscribe all devices registered to this client's authenticated client_id for push notifications on this channel
29
+ #
30
+ # @note This is unsupported in the Ruby library
31
+ def subscribe_client_id(*args)
32
+ raise_unsupported
33
+ end
34
+
35
+ # Unsubscribe local device for push notifications on this channel
36
+ #
37
+ # @note This is unsupported in the Ruby library
38
+ def unsubscribe_device(*args)
39
+ raise_unsupported
40
+ end
41
+
42
+ # Unsubscribe all devices registered to this client's authenticated client_id for push notifications on this channel
43
+ #
44
+ # @note This is unsupported in the Ruby library
45
+ def unsubscribe_client_id(*args)
46
+ raise_unsupported
47
+ end
48
+
49
+ # Get list of subscriptions on this channel for this device or authenticate client_id
50
+ #
51
+ # @note This is unsupported in the Ruby library
52
+ def get_subscriptions(*args)
53
+ raise_unsupported
54
+ end
55
+
56
+ private
57
+ def raise_unsupported
58
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. All PushChannel methods are unavailable'
59
+ end
60
+ end
61
+ end
62
+ end
@@ -3,6 +3,9 @@ require 'json'
3
3
  require 'logger'
4
4
  require 'uri'
5
5
 
6
+ require 'typhoeus'
7
+ require 'typhoeus/adapters/faraday'
8
+
6
9
  require 'ably/rest/middleware/exceptions'
7
10
 
8
11
  module Ably
@@ -30,6 +33,15 @@ module Ably
30
33
  max_retry_count: 3
31
34
  }.freeze
32
35
 
36
+ FALLBACK_RETRY_TIMEOUT = 10 * 60
37
+
38
+ # Faraday 1.0 introduced new error types, however we want to support Faraday <1 too which only used Faraday::ClientError
39
+ FARADAY_CLIENT_OR_SERVER_ERRORS = if defined?(Faraday::ParsingError)
40
+ [Faraday::ClientError, Faraday::ServerError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ParsingError]
41
+ else
42
+ Faraday::ClientError
43
+ end
44
+
33
45
  def_delegators :auth, :client_id, :auth_options
34
46
 
35
47
  # Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
@@ -83,10 +95,23 @@ module Ably
83
95
  # if empty or nil then fallback host functionality is disabled
84
96
  attr_reader :fallback_hosts
85
97
 
86
- # Whethere the {Client} has to add a random identifier to the path of a request
98
+ # Whether the {Client} has to add a random identifier to the path of a request
87
99
  # @return [Boolean]
88
100
  attr_reader :add_request_ids
89
101
 
102
+ # Retries are logged by default to warn and error. When true, retries are logged at info level
103
+ # @return [Boolean]
104
+ # @api private
105
+ attr_reader :log_retries_as_info
106
+
107
+ # True when idempotent publishing is enabled for all messages published via REST.
108
+ # When this feature is enabled, the client library will add a unique ID to every published message (without an ID)
109
+ # ensuring any failed published attempts (due to failures such as HTTP requests failing mid-flight) that are
110
+ # automatically retried will not result in duplicate messages being published to the Ably platform.
111
+ # Note: This is a beta unsupported feature!
112
+ # @return [Boolean]
113
+ attr_reader :idempotent_rest_publishing
114
+
90
115
  # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
91
116
  #
92
117
  # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key or Token ID
@@ -117,6 +142,10 @@ module Ably
117
142
  #
118
143
  # @option options [Boolean] :fallback_hosts_use_default (false) When true, forces the user of fallback hosts even if a non-default production endpoint is being used
119
144
  # @option options [Array<String>] :fallback_hosts When an array of fallback hosts are provided, these fallback hosts are always used if a request fails to the primary endpoint. If an empty array is provided, the fallback host functionality is disabled
145
+ # @option options [Integer] :fallback_retry_timeout (600 seconds) amount of time in seconds a REST client will continue to use a working fallback host when the primary fallback host has previously failed
146
+ #
147
+ # @option options [Boolean] :add_request_ids (false) When true, adds a unique request_id to each request sent to Ably servers. This is handy when reporting issues, because you can refer to a specific request.
148
+ # @option options [Boolean] :idempotent_rest_publishing (false if ver < 1.2) When true, idempotent publishing is enabled for all messages published via REST
120
149
  #
121
150
  # @return [Ably::Rest::Client]
122
151
  #
@@ -139,18 +168,21 @@ module Ably
139
168
  end
140
169
  end
141
170
 
142
- @realtime_client = options.delete(:realtime_client)
143
- @tls = options.delete(:tls) == false ? false : true
144
- @environment = options.delete(:environment) # nil is production
145
- @environment = nil if [:production, 'production'].include?(@environment)
146
- @protocol = options.delete(:protocol) || :msgpack
147
- @debug_http = options.delete(:debug_http)
148
- @log_level = options.delete(:log_level) || ::Logger::WARN
149
- @custom_logger = options.delete(:logger)
150
- @custom_host = options.delete(:rest_host)
151
- @custom_port = options.delete(:port)
152
- @custom_tls_port = options.delete(:tls_port)
153
- @add_request_ids = options.delete(:add_request_ids)
171
+ @realtime_client = options.delete(:realtime_client)
172
+ @tls = options.delete(:tls) == false ? false : true
173
+ @environment = options.delete(:environment) # nil is production
174
+ @environment = nil if [:production, 'production'].include?(@environment)
175
+ @protocol = options.delete(:protocol) || :msgpack
176
+ @debug_http = options.delete(:debug_http)
177
+ @log_level = options.delete(:log_level) || ::Logger::WARN
178
+ @custom_logger = options.delete(:logger)
179
+ @custom_host = options.delete(:rest_host)
180
+ @custom_port = options.delete(:port)
181
+ @custom_tls_port = options.delete(:tls_port)
182
+ @add_request_ids = options.delete(:add_request_ids)
183
+ @log_retries_as_info = options.delete(:log_retries_as_info)
184
+ @idempotent_rest_publishing = options.delete(:idempotent_rest_publishing) || Ably.major_minor_version_numeric > 1.1
185
+
154
186
 
155
187
  if options[:fallback_hosts_use_default] && options[:fallback_jhosts]
156
188
  raise ArgumentError, "fallback_hosts_use_default cannot be set to trye when fallback_jhosts is also provided"
@@ -166,9 +198,15 @@ module Ably
166
198
  Ably::FALLBACK_HOSTS
167
199
  end
168
200
 
201
+ options[:fallback_retry_timeout] ||= FALLBACK_RETRY_TIMEOUT
202
+
203
+ # Take option keys prefixed with `http_`, remove the http_ and
204
+ # check if the option exists in HTTP_DEFAULTS. If so, update http_defaults
169
205
  @http_defaults = HTTP_DEFAULTS.dup
170
206
  options.each do |key, val|
171
207
  if http_key = key[/^http_(.+)/, 1]
208
+ # Typhoeus converts decimal durations to milliseconds, so 0.0001 timeout is treated as 0 (no timeout)
209
+ val = 0.001 if val.kind_of?(Numeric) && (val > 0) && (val < 0.001)
172
210
  @http_defaults[http_key.to_sym] = val if val && @http_defaults.has_key?(http_key.to_sym)
173
211
  end
174
212
  end
@@ -190,8 +228,12 @@ module Ably
190
228
  raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
191
229
 
192
230
  token_params = options.delete(:default_token_params) || {}
193
- @options = options
194
- @auth = Auth.new(self, token_params, options)
231
+ @options = options
232
+ init_auth_options = options.select do |key, _|
233
+ Auth::AUTH_OPTIONS_KEYS.include?(key.to_s)
234
+ end
235
+
236
+ @auth = Auth.new(self, token_params, init_auth_options)
195
237
  @channels = Ably::Rest::Channels.new(self)
196
238
  @encoders = []
197
239
 
@@ -273,6 +315,24 @@ module Ably
273
315
  raw_request(:post, path, params, options)
274
316
  end
275
317
 
318
+ # Perform an HTTP PUT request to the API using configured authentication
319
+ #
320
+ # @return [Faraday::Response]
321
+ #
322
+ # @api private
323
+ def put(path, params, options = {})
324
+ raw_request(:put, path, params, options)
325
+ end
326
+
327
+ # Perform an HTTP DELETE request to the API using configured authentication
328
+ #
329
+ # @return [Faraday::Response]
330
+ #
331
+ # @api private
332
+ def delete(path, params, options = {})
333
+ raw_request(:delete, path, params, options)
334
+ end
335
+
276
336
  # Perform an HTTP request to the Ably API
277
337
  # This is a convenience for customers who wish to use bleeding edge REST API functionality
278
338
  # that is either not documented or is not included in the API for our client libraries.
@@ -292,14 +352,14 @@ module Ably
292
352
 
293
353
  response = case method.to_sym
294
354
  when :get
295
- reauthorize_on_authorisation_failure do
355
+ reauthorize_on_authorization_failure do
296
356
  send_request(method, path, params, headers: headers)
297
357
  end
298
358
  when :post
299
359
  path_with_params = Addressable::URI.new
300
360
  path_with_params.query_values = params || {}
301
361
  query = path_with_params.query
302
- reauthorize_on_authorisation_failure do
362
+ reauthorize_on_authorization_failure do
303
363
  send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers)
304
364
  end
305
365
  end
@@ -321,6 +381,20 @@ module Ably
321
381
  Models::HttpPaginatedResponse.new(response, path, self)
322
382
  end
323
383
 
384
+ # The local device detilas
385
+ # @return [Ably::Models::LocalDevice]
386
+ #
387
+ # @note This is unsupported in the Ruby library
388
+ def device
389
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable'
390
+ end
391
+
392
+ # Push notification object for publishing and managing push notifications
393
+ # @return [Ably::Rest::Push]
394
+ def push
395
+ @push ||= Push.new(self)
396
+ end
397
+
324
398
  # @!attribute [r] endpoint
325
399
  # @return [URI::Generic] Default Ably REST endpoint used for all requests
326
400
  def endpoint
@@ -389,7 +463,6 @@ module Ably
389
463
  def fallback_connection
390
464
  unless defined?(@fallback_connections) && @fallback_connections
391
465
  @fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
392
- @fallback_connections << Faraday.new(endpoint.to_s, connection_options) # Try the original host last if all fallbacks have been used
393
466
  end
394
467
  @fallback_index ||= 0
395
468
 
@@ -410,23 +483,58 @@ module Ably
410
483
 
411
484
  # Allowable duration for an external auth request
412
485
  # For REST client this defaults to request_timeout
413
- # For Realtime clients this defaults to realtime_request_timeout
486
+ # For Realtime clients this defaults to 250ms less than the realtime_request_timeout
487
+ # ensuring an auth failure will be triggered before the realtime request timeout fires
488
+ # which would lead to a misleading error message (connection timeout as opposed to auth request timeout)
414
489
  # @api private
415
490
  def auth_request_timeout
416
491
  if @realtime_client
417
- @realtime_client.connection.defaults.fetch(:realtime_request_timeout)
492
+ @realtime_client.connection.defaults.fetch(:realtime_request_timeout) - 0.25
418
493
  else
419
494
  http_defaults.fetch(:request_timeout)
420
495
  end
421
496
  end
422
497
 
498
+ # If the primary host endpoint fails, and a subsequent fallback host succeeds, the fallback
499
+ # host that succeeded is used for +ClientOption+ +fallback_retry_timeout+ seconds to avoid
500
+ # retries to known failing hosts for a short period of time.
501
+ # See https://github.com/ably/docs/pull/554, spec id #RSC15f
502
+ #
503
+ # @return [nil, String] Returns nil (falsey) if the primary host is being used, or the currently used host if a fallback host is currently preferred
504
+ def using_preferred_fallback_host?
505
+ if preferred_fallback_connection && (preferred_fallback_connection.fetch(:expires_at) > Time.now)
506
+ preferred_fallback_connection.fetch(:connection_object).host
507
+ end
508
+ end
509
+
423
510
  private
511
+
512
+ attr_reader :preferred_fallback_connection
513
+
514
+ # See #using_preferred_fallback_host? for context
515
+ def set_preferred_fallback_connection(connection)
516
+ @preferred_fallback_connection = if connection == @connection
517
+ # If the succeeded connection is in fact the primary connection (tried after a failed fallback)
518
+ # then clear the preferred fallback connection
519
+ nil
520
+ else
521
+ {
522
+ expires_at: Time.now + options.fetch(:fallback_retry_timeout),
523
+ connection_object: connection,
524
+ }
525
+ end
526
+ end
527
+
528
+ def get_preferred_fallback_connection_object
529
+ preferred_fallback_connection.fetch(:connection_object) if using_preferred_fallback_host?
530
+ end
531
+
424
532
  def raw_request(method, path, params = {}, options = {})
425
533
  options = options.clone
426
534
  if options.delete(:disable_automatic_reauthorize) == true
427
535
  send_request(method, path, params, options)
428
536
  else
429
- reauthorize_on_authorisation_failure do
537
+ reauthorize_on_authorization_failure do
430
538
  send_request(method, path, params, options)
431
539
  end
432
540
  end
@@ -442,15 +550,30 @@ module Ably
442
550
  retry_sequence_id = nil
443
551
  request_id = SecureRandom.urlsafe_base64(10) if add_request_ids
444
552
 
553
+ preferred_fallback_connection_for_first_request = get_preferred_fallback_connection_object
554
+
445
555
  begin
446
- use_fallback = can_fallback_to_alternate_ably_host? && retry_count > 0
556
+ use_fallback = can_fallback_to_alternate_ably_host? && (retry_count > 0)
557
+
558
+ conn = if preferred_fallback_connection_for_first_request
559
+ case retry_count
560
+ when 0
561
+ preferred_fallback_connection_for_first_request
562
+ when 1
563
+ # Ensure the root host is used first if the preferred fallback fails, see #RSC15f
564
+ connection(use_fallback: false)
565
+ end
566
+ end || connection(use_fallback: use_fallback) # default to normal connection selection process if not preferred connection set
447
567
 
448
- connection(use_fallback: use_fallback).send(method, path, params) do |request|
568
+ conn.send(method, path, params) do |request|
449
569
  if add_request_ids
450
570
  request.params[:request_id] = request_id
451
571
  request.options.context = {} if request.options.context.nil?
452
572
  request.options.context[:request_id] = request_id
453
573
  end
574
+ if options[:qs_params]
575
+ request.params.merge!(options[:qs_params])
576
+ end
454
577
  unless options[:send_auth_header] == false
455
578
  request.headers[:authorization] = auth.auth_header
456
579
  if options[:headers]
@@ -461,41 +584,45 @@ module Ably
461
584
  end
462
585
  end.tap do
463
586
  if retry_count > 0
464
- logger.warn do
587
+ retry_log_severity = log_retries_as_info ? :info : :warn
588
+ logger.public_send(retry_log_severity) do
465
589
  "Ably::Rest::Client - Request SUCCEEDED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
466
590
  " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
467
591
  end
592
+ set_preferred_fallback_connection conn
468
593
  end
469
594
  end
470
595
 
471
- rescue Faraday::TimeoutError, Faraday::ClientError, Ably::Exceptions::ServerError => error
596
+ rescue *([Faraday::TimeoutError, Ably::Exceptions::ServerError] + FARADAY_CLIENT_OR_SERVER_ERRORS) => error
472
597
  retry_sequence_id ||= SecureRandom.urlsafe_base64(4)
473
598
  time_passed = Time.now - requested_at
474
599
 
475
- if can_fallback_to_alternate_ably_host? && retry_count < max_retry_count && time_passed <= max_retry_duration
600
+ if can_fallback_to_alternate_ably_host? && (retry_count < max_retry_count) && (time_passed <= max_retry_duration)
476
601
  retry_count += 1
477
- logger.warn { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" }
602
+ retry_log_severity = log_retries_as_info ? :info : :warn
603
+ logger.public_send(retry_log_severity) { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" }
478
604
  retry
479
605
  end
480
606
 
481
- logger.error do
607
+ retry_log_severity = log_retries_as_info ? :info : :error
608
+ logger.public_send(retry_log_severity) do
482
609
  "Ably::Rest::Client - Request FAILED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
483
610
  " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
484
611
  end
485
612
 
486
613
  case error
487
614
  when Faraday::TimeoutError
488
- raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, 80014, error, { request_id: request_id })
489
- when Faraday::ClientError
615
+ raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT, error, { request_id: request_id })
616
+ when *FARADAY_CLIENT_OR_SERVER_ERRORS
490
617
  # request_id is also available in the request context
491
- raise Ably::Exceptions::ConnectionError.new(error.message, nil, 80000, error, { request_id: request_id })
618
+ raise Ably::Exceptions::ConnectionError.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error, { request_id: request_id })
492
619
  else
493
620
  raise error
494
621
  end
495
622
  end
496
623
  end
497
624
 
498
- def reauthorize_on_authorisation_failure
625
+ def reauthorize_on_authorization_failure
499
626
  yield
500
627
  rescue Ably::Exceptions::TokenExpired => e
501
628
  if auth.token_renewable?
@@ -544,7 +671,7 @@ module Ably
544
671
  }
545
672
  end
546
673
 
547
- # Return a Faraday middleware stack to initiate the Faraday::Connection with
674
+ # Return a Faraday middleware stack to initiate the Faraday::RackBuilder with
548
675
  #
549
676
  # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
550
677
  def middleware
@@ -556,8 +683,8 @@ module Ably
556
683
 
557
684
  setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true
558
685
 
559
- # Set Faraday's HTTP adapter
560
- builder.adapter :excon
686
+ # Set Faraday's HTTP adapter with support for HTTP/2
687
+ builder.adapter :typhoeus, http_version: :httpv2_0
561
688
  end
562
689
  end
563
690