ably-pubsub-core 0.0.1.pre → 2.0.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ably/auth.rb +5 -5
  3. data/lib/ably/exceptions.rb +5 -0
  4. data/lib/ably/internal.rb +115 -0
  5. data/lib/ably/logger.rb +3 -3
  6. data/lib/ably/models/channel_details.rb +1 -1
  7. data/lib/ably/models/channel_metrics.rb +1 -1
  8. data/lib/ably/models/channel_occupancy.rb +1 -1
  9. data/lib/ably/models/channel_state_change.rb +7 -7
  10. data/lib/ably/models/channel_status.rb +1 -1
  11. data/lib/ably/models/cipher_params.rb +1 -1
  12. data/lib/ably/models/connection_details.rb +2 -2
  13. data/lib/ably/models/connection_state_change.rb +6 -6
  14. data/lib/ably/models/http_paginated_response.rb +3 -3
  15. data/lib/ably/models/message.rb +69 -2
  16. data/lib/ably/models/message_operation.rb +53 -0
  17. data/lib/ably/models/paginated_result.rb +2 -2
  18. data/lib/ably/models/presence_message.rb +1 -1
  19. data/lib/ably/models/protocol_message.rb +14 -1
  20. data/lib/ably/models/publish_result.rb +32 -0
  21. data/lib/ably/models/stats.rb +41 -84
  22. data/lib/ably/models/update_delete_result.rb +32 -0
  23. data/lib/ably/modules/ably.rb +13 -12
  24. data/lib/ably/modules/channels_collection.rb +1 -1
  25. data/lib/ably/modules/http_helpers.rb +11 -11
  26. data/lib/ably/{rest → pubsub/http}/channel/push_channel.rb +3 -3
  27. data/lib/ably/pubsub/http/channel.rb +284 -0
  28. data/lib/ably/pubsub/http/channels.rb +49 -0
  29. data/lib/ably/pubsub/http/client.rb +738 -0
  30. data/lib/ably/pubsub/http/middleware/encoder.rb +51 -0
  31. data/lib/ably/pubsub/http/middleware/exceptions.rb +55 -0
  32. data/lib/ably/pubsub/http/middleware/external_exceptions.rb +26 -0
  33. data/lib/ably/pubsub/http/middleware/fail_if_unsupported_mime_type.rb +24 -0
  34. data/lib/ably/pubsub/http/middleware/logger.rb +60 -0
  35. data/lib/ably/pubsub/http/middleware/parse_json.rb +29 -0
  36. data/lib/ably/pubsub/http/middleware/parse_message_pack.rb +45 -0
  37. data/lib/ably/pubsub/http/presence.rb +117 -0
  38. data/lib/ably/{rest → pubsub/http}/push/admin.rb +5 -5
  39. data/lib/ably/{rest → pubsub/http}/push/channel_subscriptions.rb +1 -1
  40. data/lib/ably/{rest → pubsub/http}/push/device_registrations.rb +1 -1
  41. data/lib/ably/pubsub/http/push.rb +27 -0
  42. data/lib/ably/pubsub/http.rb +38 -0
  43. data/lib/ably/pubsub/realtime/auth.rb +285 -0
  44. data/lib/ably/{realtime → pubsub/realtime}/channel/channel_manager.rb +2 -2
  45. data/lib/ably/{realtime → pubsub/realtime}/channel/channel_properties.rb +5 -5
  46. data/lib/ably/{realtime → pubsub/realtime}/channel/channel_state_machine.rb +2 -2
  47. data/lib/ably/{realtime → pubsub/realtime}/channel/publisher.rb +9 -4
  48. data/lib/ably/{realtime → pubsub/realtime}/channel/push_channel.rb +2 -2
  49. data/lib/ably/pubsub/realtime/channel.rb +545 -0
  50. data/lib/ably/pubsub/realtime/channels.rb +92 -0
  51. data/lib/ably/{realtime → pubsub/realtime}/client/incoming_message_dispatcher.rb +43 -8
  52. data/lib/ably/{realtime → pubsub/realtime}/client/outgoing_message_dispatcher.rb +2 -2
  53. data/lib/ably/pubsub/realtime/client.rb +372 -0
  54. data/lib/ably/{realtime → pubsub/realtime}/connection/connection_manager.rb +6 -6
  55. data/lib/ably/{realtime → pubsub/realtime}/connection/connection_state_machine.rb +2 -2
  56. data/lib/ably/{realtime → pubsub/realtime}/connection/websocket_transport.rb +1 -1
  57. data/lib/ably/pubsub/realtime/connection.rb +724 -0
  58. data/lib/ably/{realtime → pubsub/realtime}/models/nil_channel.rb +3 -3
  59. data/lib/ably/{realtime → pubsub/realtime}/presence/members_map.rb +2 -2
  60. data/lib/ably/{realtime → pubsub/realtime}/presence/presence_manager.rb +3 -3
  61. data/lib/ably/{realtime → pubsub/realtime}/presence/presence_state_machine.rb +2 -2
  62. data/lib/ably/{realtime → pubsub/realtime}/presence.rb +23 -23
  63. data/lib/ably/{realtime → pubsub/realtime}/push/admin.rb +8 -8
  64. data/lib/ably/{realtime → pubsub/realtime}/push/channel_subscriptions.rb +6 -6
  65. data/lib/ably/{realtime → pubsub/realtime}/push/device_registrations.rb +6 -6
  66. data/lib/ably/pubsub/realtime/push.rb +27 -0
  67. data/lib/ably/pubsub/realtime/recovery_key_context.rb +38 -0
  68. data/lib/ably/pubsub/realtime.rb +58 -0
  69. data/lib/ably/pubsub.rb +11 -0
  70. data/lib/ably/version.rb +2 -2
  71. data/lib/ably.rb +6 -2
  72. metadata +50 -46
  73. data/lib/ably/models/stats_types.rb +0 -107
  74. data/lib/ably/realtime/auth.rb +0 -283
  75. data/lib/ably/realtime/channel.rb +0 -440
  76. data/lib/ably/realtime/channels.rb +0 -71
  77. data/lib/ably/realtime/client.rb +0 -363
  78. data/lib/ably/realtime/connection.rb +0 -738
  79. data/lib/ably/realtime/push.rb +0 -25
  80. data/lib/ably/realtime/recovery_key_context.rb +0 -36
  81. data/lib/ably/realtime.rb +0 -60
  82. data/lib/ably/rest/channel.rb +0 -194
  83. data/lib/ably/rest/channels.rb +0 -47
  84. data/lib/ably/rest/client.rb +0 -727
  85. data/lib/ably/rest/middleware/encoder.rb +0 -49
  86. data/lib/ably/rest/middleware/exceptions.rb +0 -53
  87. data/lib/ably/rest/middleware/external_exceptions.rb +0 -24
  88. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +0 -22
  89. data/lib/ably/rest/middleware/logger.rb +0 -58
  90. data/lib/ably/rest/middleware/parse_json.rb +0 -27
  91. data/lib/ably/rest/middleware/parse_message_pack.rb +0 -43
  92. data/lib/ably/rest/presence.rb +0 -113
  93. data/lib/ably/rest/push.rb +0 -25
  94. data/lib/ably/rest.rb +0 -40
@@ -0,0 +1,738 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'logger'
4
+ require 'uri'
5
+
6
+ require 'typhoeus'
7
+ require 'faraday/typhoeus'
8
+
9
+ require 'ably/pubsub/http/middleware/exceptions'
10
+
11
+ module Ably
12
+ module PubSub
13
+ module Http
14
+ # A client that offers a simple stateless API to interact directly with Ably's REST API.
15
+ #
16
+ class Client
17
+ include Ably::Modules::Conversions
18
+ include Ably::Modules::HttpHelpers
19
+ extend Forwardable
20
+ using Ably::Util::AblyExtensions
21
+
22
+ MAX_MESSAGE_SIZE = 65536 # See spec TO3l8
23
+ MAX_FRAME_SIZE = 524288 # See spec TO3l8
24
+
25
+ # Configuration for HTTP timeouts and HTTP request reattempts to fallback hosts
26
+ HTTP_DEFAULTS = {
27
+ open_timeout: 4,
28
+ request_timeout: 10,
29
+ max_retry_duration: 15,
30
+ max_retry_count: 3
31
+ }.freeze
32
+
33
+ FALLBACK_RETRY_TIMEOUT = 10 * 60
34
+
35
+ # Faraday 1.0 introduced new error types, however we want to support Faraday <1 too which only used Faraday::ClientError
36
+ FARADAY_CLIENT_OR_SERVER_ERRORS = if defined?(Faraday::ParsingError)
37
+ [Faraday::ClientError, Faraday::ServerError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ParsingError]
38
+ else
39
+ Faraday::ClientError
40
+ end
41
+
42
+ def_delegators :auth, :client_id, :auth_options
43
+
44
+ # The +:endpoint+ client option: a routing policy name such as +main+, a +nonprod:[id]+ routing policy, or a hostname
45
+ # @spec REC1
46
+ # @return [String]
47
+ attr_reader :endpoint
48
+
49
+ # The domain that REST requests and realtime connections are sent to, derived from {#endpoint}
50
+ # @spec REC1b
51
+ # @return [String]
52
+ attr_reader :primary_domain
53
+
54
+ # The protocol configured for this client, either binary `:msgpack` or text based `:json`
55
+ # @return [Symbol]
56
+ attr_reader :protocol
57
+
58
+ # Client agent i.e. `example-gem/1.2.0 ably-pubsub-ruby/1.1.5 ruby/3.1.1`
59
+ # @return [String]
60
+ attr_reader :agent
61
+
62
+ # An {Ably::Auth} object.
63
+ # @spec RSC5
64
+ # @return [Ably::Auth]
65
+ attr_reader :auth
66
+
67
+ # A {Ably::PubSub::Http::Channels} object.
68
+ # @spec RSN1
69
+ # @return [Ably::PubSub::Http::Channels]
70
+ attr_reader :channels
71
+
72
+ # Log level configured for this {Client}
73
+ # @return [Logger::Severity]
74
+ attr_reader :log_level
75
+
76
+ # The custom port for non-TLS requests if it was provided with the option +:port+ when the {Client} was created
77
+ # @return [Integer,Nil]
78
+ attr_reader :custom_port
79
+
80
+ # The custom TLS port for TLS requests if it was provided with the option +:tls_port+ when the {Client} was created
81
+ # @return [Integer,Nil]
82
+ attr_reader :custom_tls_port
83
+
84
+ # The immutable configured HTTP defaults for this client.
85
+ # See {#initialize} for the configurable HTTP defaults prefixed with +http_+
86
+ # @return [Hash]
87
+ attr_reader :http_defaults
88
+
89
+ # The registered encoders that are used to encode and decode message payloads
90
+ # @return [Array<Ably::Models::MessageEncoder::Base>]
91
+ # @api private
92
+ attr_reader :encoders
93
+
94
+ # The additional options passed to this Client's #initialize method not available as attributes of this class
95
+ # @return [Hash]
96
+ # @api private
97
+ attr_reader :options
98
+
99
+ # The list of fallback hosts to be used by this client
100
+ # if empty or nil then fallback host functionality is disabled
101
+ attr_reader :fallback_hosts
102
+
103
+ # Whether the {Client} has to add a random identifier to the path of a request
104
+ # @return [Boolean]
105
+ attr_reader :add_request_ids
106
+
107
+ # Retries are logged by default to warn and error. When true, retries are logged at info level
108
+ # @return [Boolean]
109
+ # @api private
110
+ attr_reader :log_retries_as_info
111
+
112
+ # True when idempotent publishing is enabled for all messages published via REST.
113
+ # When this feature is enabled, the client library will add a unique ID to every published message (without an ID)
114
+ # ensuring any failed published attempts (due to failures such as HTTP requests failing mid-flight) that are
115
+ # automatically retried will not result in duplicate messages being published to the Ably platform.
116
+ # Note: This is a beta unsupported feature!
117
+ # @return [Boolean]
118
+ attr_reader :idempotent_rest_publishing
119
+
120
+ # Max message size (TO2, TO3l8) by default (65536 bytes) 64KiB
121
+ # @return [Integer]
122
+ attr_reader :max_message_size
123
+
124
+ # Max frame size (TO2, TO3l8) by default (524288 bytes) 512KiB
125
+ # @return [Integer]
126
+ attr_reader :max_frame_size
127
+
128
+ # Refuses direct construction: the package a client is created from is what declares
129
+ # the client's side to the platform, and a directly constructed client declares none.
130
+ # Use {Ably::PubSub::Server.create_http_client} from the +ably-pubsub-server+ gem.
131
+ #
132
+ # @raise [Ably::Exceptions::DirectConstructionNotSupported] always
133
+ def self.new(*args, **kwargs, &block)
134
+ raise Ably::Internal.direct_construction_error(
135
+ 'Ably::PubSub::Http::Client.new', 'Ably::PubSub::Server.create_http_client(options)'
136
+ )
137
+ end
138
+
139
+ # Constructs a {Ably::PubSub::Http::Client} object using an Ably API key or token string.
140
+ #
141
+ # @spec RSC1
142
+ #
143
+ # @param [Hash,String] options an options Hash or String used to configure the client and the authentication, or String with an API key or Token ID
144
+ # @option options [Boolean] :tls (true) When false, TLS is disabled. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
145
+ # @option options [String] :key API key comprising the key name and key secret in a single string
146
+ # @option options [String] :token Token string or {Models::TokenDetails} used to authenticate requests
147
+ # @option options [String] :token_details {Models::TokenDetails} used to authenticate requests
148
+ # @option options [Boolean] :use_token_auth Will force Basic Auth if set to false, and Token auth if set to true
149
+ # @option options [String] :endpoint ('main') A routing policy name such as 'main', a non-production routing policy such as 'nonprod:sandbox', or a hostname such as 'localhost'
150
+ # @option options [Symbol] :protocol (:msgpack) Protocol used to communicate with Ably, :json and :msgpack currently supported
151
+ # @option options [Boolean] :use_binary_protocol (true) When true will use the MessagePack binary protocol, when false it will use JSON encoding. This option will overide :protocol option
152
+ # @option options [Logger::Severity,Symbol] :log_level (Logger::WARN) Log level for the standard Logger that outputs to STDOUT. Can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG) or :none
153
+ # @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-3.1.1/libdoc/logger/rdoc/Logger.html
154
+ # @option options [String] :client_id client ID identifying this connection to other clients
155
+ # @option options [String] :auth_url a URL to be used to GET or POST a set of token request params, to obtain a signed token request
156
+ # @option options [Hash] :auth_headers a set of application-specific headers to be added to any request made to the +auth_url+
157
+ # @option options [Hash] :auth_params a set of application-specific query params to be added to any request made to the +auth_url+
158
+ # @option options [Symbol] :auth_method (:get) HTTP method to use with +auth_url+, must be either +:get+ or +:post+
159
+ # @option options [Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required.
160
+ # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent
161
+ # @option options [Boolean] :query_time when true will query the {https://www.ably.com Ably} system for the current time instead of using the local time
162
+ # @option options [Hash] :default_token_params convenience to pass in +token_params+ that will be used as a default for all token requests. See {Auth#create_token_request}
163
+ #
164
+ # @option options [Integer] :http_open_timeout (4 seconds) timeout in seconds for opening an HTTP connection for all HTTP requests
165
+ # @option options [Integer] :http_request_timeout (10 seconds) timeout in seconds for any single complete HTTP request and response
166
+ # @option options [Integer] :http_max_retry_count (3) maximum number of fallback host retries for HTTP requests that fail due to network issues or server problems
167
+ # @option options [Integer] :http_max_retry_duration (15 seconds) maximum elapsed time in which fallback host retries for HTTP requests will be attempted i.e. if the first default host attempt takes 5s, and then the subsequent fallback retry attempt takes 7s, no further fallback host attempts will be made as the total elapsed time of 12s exceeds the default 10s limit
168
+ #
169
+ # @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 domain. If an empty array is provided, the fallback host functionality is disabled.
170
+ # When not provided, fallback hosts are derived from +:endpoint+, and none are used if +:endpoint+ is a hostname
171
+ # @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
172
+ #
173
+ # @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.
174
+ # @option options [Boolean] :idempotent_rest_publishing (false if ver < 1.2) When true, idempotent publishing is enabled for all messages published via REST
175
+ # @option options [Integer] :max_message_size (65536 bytes) Maximum size of all messages when publishing via REST publish()
176
+ # @option options [Integer] :max_frame_size (524288 bytes) Maximum size of frame
177
+ #
178
+ # @return [Ably::PubSub::Http::Client]
179
+ #
180
+ # @example
181
+ # # Create a new client authenticating with basic auth using a String object
182
+ # client = Ably::PubSub::Server.create_http_client('key.id:secret')
183
+ #
184
+ # # Construct a client using a Hash object.
185
+ # client = Ably::PubSub::Server.create_http_client(key: 'key.id:secret', client_id: 'john')
186
+ #
187
+ # Applications do not call this constructor: {.new} refuses direct construction, and
188
+ # a per-side package reaches it through {Ably::Internal.create_http_client}.
189
+ #
190
+ def initialize(options)
191
+ raise ArgumentError, 'Options Hash is expected' if options.nil?
192
+
193
+ options = options.clone
194
+ if options.kind_of?(String)
195
+ options = if options.match(Auth::API_KEY_REGEX)
196
+ { key: options }
197
+ else
198
+ { token: options }
199
+ end
200
+ end
201
+
202
+ @agent = options.delete(:agent) || Ably::AGENT
203
+ # Additive agent entries (`identifier => version`), appended to the base agent
204
+ # string. This is how a package layered on this one (such as ably-pubsub-server)
205
+ # declares itself — including the side-declaring entry that MAU classification
206
+ # reads — without replacing the base identifiers the way :agent does.
207
+ options.delete(:agents).to_h.each do |identifier, version|
208
+ @agent = "#{@agent} #{version ? "#{identifier}/#{version}" : identifier}"
209
+ end
210
+ @realtime_client = options.delete(:realtime_client)
211
+
212
+ @endpoint = (options.delete(:endpoint) || Ably::ENDPOINT).to_s
213
+ @primary_domain = Ably::Internal.primary_domain(endpoint)
214
+ @tls = options.delete_with_default(:tls, true)
215
+ @protocol = options.delete(:protocol) || :msgpack
216
+ @debug_http = options.delete(:debug_http)
217
+ @log_level = options.delete(:log_level) || ::Logger::WARN
218
+ @custom_logger = options.delete(:logger)
219
+ @custom_port = options.delete(:port)
220
+ @custom_tls_port = options.delete(:tls_port)
221
+ @add_request_ids = options.delete(:add_request_ids)
222
+ @log_retries_as_info = options.delete(:log_retries_as_info)
223
+ @max_message_size = options.delete(:max_message_size) || MAX_MESSAGE_SIZE
224
+ @max_frame_size = options.delete(:max_frame_size) || MAX_FRAME_SIZE
225
+ @idempotent_rest_publishing = options.delete_with_default(:idempotent_rest_publishing, true)
226
+
227
+ # REC2a2, REC2c
228
+ @fallback_hosts = options.delete(:fallback_hosts) || Ably::Internal.fallback_domains(endpoint)
229
+
230
+ options[:fallback_retry_timeout] ||= FALLBACK_RETRY_TIMEOUT
231
+
232
+ # Take option keys prefixed with `http_`, remove the http_ and
233
+ # check if the option exists in HTTP_DEFAULTS. If so, update http_defaults
234
+ @http_defaults = HTTP_DEFAULTS.dup
235
+ options.each do |key, val|
236
+ if http_key = key[/^http_(.+)/, 1]
237
+ # Typhoeus converts decimal durations to milliseconds, so 0.0001 timeout is treated as 0 (no timeout)
238
+ val = 0.001 if val.kind_of?(Numeric) && (val > 0) && (val < 0.001)
239
+ @http_defaults[http_key.to_sym] = val if val && @http_defaults.has_key?(http_key.to_sym)
240
+ end
241
+ end
242
+ @http_defaults.freeze
243
+
244
+ if @log_level == :none
245
+ @custom_logger = Ably::Models::NilLogger.new
246
+ else
247
+ @log_level = ::Logger.const_get(log_level.to_s.upcase) if log_level.kind_of?(Symbol) || log_level.kind_of?(String)
248
+ end
249
+
250
+ options.delete(:use_binary_protocol).tap do |use_binary_protocol|
251
+ if use_binary_protocol == true
252
+ @protocol = :msgpack
253
+ elsif use_binary_protocol == false
254
+ @protocol = :json
255
+ end
256
+ end
257
+ raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
258
+
259
+ token_params = options.delete(:default_token_params) || {}
260
+ @options = options
261
+ init_auth_options = options.select do |key, _|
262
+ Auth::AUTH_OPTIONS_KEYS.include?(key.to_s)
263
+ end
264
+
265
+ @auth = Auth.new(self, token_params, init_auth_options)
266
+ @channels = Ably::PubSub::Http::Channels.new(self)
267
+ @encoders = []
268
+
269
+ options.freeze
270
+
271
+ initialize_default_encoders
272
+ end
273
+
274
+ # Return a REST {Ably::PubSub::Http::Channel} for the given name
275
+ #
276
+ # @param (see Ably::PubSub::Http::Channels#get)
277
+ #
278
+ # @return (see Ably::PubSub::Http::Channels#get)
279
+ def channel(name, channel_options = {})
280
+ channels.get(name, channel_options)
281
+ end
282
+
283
+ # Queries the REST /stats API and retrieves your application's usage statistics. Returns a {Ably::Models::PaginatedResult} object, containing an array of {Ably::Models::Stats} objects. See the Stats docs.
284
+ #
285
+ # @spec RSC6a, RSC6b1, RSC6b2, RSC6b3, RSC6b4
286
+ #
287
+ # @param [Hash] options the options for the stats request
288
+ # @option options [Integer,Time] :start The time from which stats are retrieved, specified as milliseconds since the Unix epoch. RSC6b1
289
+ # @option options [Integer,Time] :end The time until stats are retrieved, specified as milliseconds since the Unix epoch. RSC6b1
290
+ # @option options [Symbol] :direction The order for which stats are returned in. Valid values are backwards which orders stats from most recent to oldest, or forwards which orders stats from oldest to most recent. The default is backwards. RSC6b2
291
+ # @option options [Integer] :limit An upper limit on the number of stats returned. The default is 100, and the maximum is 1000. RSC6b3
292
+ # @option options [Symbol] :unit minute, hour, day or month. Based on the unit selected, the given start or end times are rounded down to the start of the relevant interval depending on the unit granularity of the query. RSC6b4
293
+ #
294
+ # @return [Ably::Models::PaginatedResult<Ably::Models::Stats>] A {Ably::Models::PaginatedResult} object containing an array of {Ably::Models::Stats} objects.
295
+ #
296
+ def stats(options = {})
297
+ options = {
298
+ :direction => :backwards,
299
+ :unit => :minute,
300
+ :limit => 100
301
+ }.merge(options)
302
+
303
+ [:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) }
304
+ raise ArgumentError, ":end must be equal to or after :start" if options[:start] && options[:end] && (options[:start] > options[:end])
305
+
306
+ paginated_options = {
307
+ coerce_into: 'Ably::Models::Stats'
308
+ }
309
+
310
+ url = '/stats'
311
+ response = get(url, options)
312
+
313
+ Ably::Models::PaginatedResult.new(response, url, self, paginated_options)
314
+ end
315
+
316
+ # Retrieves the time from the Ably service as milliseconds since the Unix epoch. Clients that do not have access
317
+ # to a sufficiently well maintained time source and wish to issue Ably {Ably::Models::TokenRequest} with
318
+ # a more accurate timestamp should use the {Ably::PubSub::Http::Client#queryTime} property instead of this method.
319
+ #
320
+ # @spec RSC16
321
+ #
322
+ # @return [Time] The time as milliseconds since the Unix epoch.
323
+ def time
324
+ response = get('/time', {}, send_auth_header: false)
325
+
326
+ as_time_from_epoch(response.body.first)
327
+ end
328
+
329
+ # @!attribute [r] use_tls?
330
+ # @return [Boolean] True if client is configured to use TLS for all Ably communication
331
+ def use_tls?
332
+ @tls == true
333
+ end
334
+
335
+ # Perform an HTTP GET request to the API using configured authentication
336
+ #
337
+ # @return [Faraday::Response]
338
+ #
339
+ # @api private
340
+ def get(path, params = {}, options = {})
341
+ raw_request(:get, path, params, options)
342
+ end
343
+
344
+ # Perform an HTTP POST request to the API using configured authentication
345
+ #
346
+ # @return [Faraday::Response]
347
+ #
348
+ # @api private
349
+ def post(path, params, options = {})
350
+ raw_request(:post, path, params, options)
351
+ end
352
+
353
+ # Perform an HTTP PUT request to the API using configured authentication
354
+ #
355
+ # @return [Faraday::Response]
356
+ #
357
+ # @api private
358
+ def put(path, params, options = {})
359
+ raw_request(:put, path, params, options)
360
+ end
361
+
362
+ # Perform an HTTP PATCH request to the API using configured authentication
363
+ #
364
+ # @return [Faraday::Response]
365
+ #
366
+ # @api private
367
+ def patch(path, params, options = {})
368
+ raw_request(:patch, path, params, options)
369
+ end
370
+
371
+ # Perform an HTTP DELETE request to the API using configured authentication
372
+ #
373
+ # @return [Faraday::Response]
374
+ #
375
+ # @api private
376
+ def delete(path, params, options = {})
377
+ raw_request(:delete, path, params, options)
378
+ end
379
+
380
+ # Makes a REST request to a provided path. This is provided as a convenience for developers who wish to use REST API
381
+ # functionality that is either not documented or is not yet included in the public API, without having to directly
382
+ # handle features such as authentication, paging, fallback hosts, MsgPack and JSON support.
383
+ #
384
+ # @spec RSC19
385
+ #
386
+ # @param method [Symbol] The request method to use, such as :get, :post.
387
+ # @param path [String] The request path.
388
+ # @param params [Hash, nil] The parameters to include in the URL query of the request. The parameters depend on the endpoint being queried. See the REST API reference for the available parameters of each endpoint.
389
+ # @param body [Hash, nil] The JSON body of the request.
390
+ # @param headers [Hash, nil] Additional HTTP headers to include in the request.
391
+ #
392
+ # @return [Ably::Models::HttpPaginatedResponse<>] An {Ably::Models::HttpPaginatedResponse} object returned by the HTTP request, containing an empty or JSON-encodable object.
393
+ def request(method, path, params = {}, body = nil, headers = {}, options = {})
394
+ raise "Method #{method.to_s.upcase} not supported" unless %i(get put patch post delete).include?(method.to_sym)
395
+
396
+ response = case method.to_sym
397
+ when :get, :delete
398
+ reauthorize_on_authorization_failure do
399
+ send_request(method, path, params, headers: headers)
400
+ end
401
+ when :post, :patch, :put
402
+ if body.to_json.bytesize > max_frame_size
403
+ raise Ably::Exceptions::MaxFrameSizeExceeded.new("Maximum frame size exceeded #{max_frame_size} bytes.")
404
+ end
405
+ path_with_params = Addressable::URI.new
406
+ path_with_params.query_values = params || {}
407
+ query = path_with_params.query
408
+ reauthorize_on_authorization_failure do
409
+ send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers)
410
+ end
411
+ end
412
+
413
+ paginated_options = {
414
+ async_blocking_operations: options.delete(:async_blocking_operations),
415
+ }
416
+
417
+ Ably::Models::HttpPaginatedResponse.new(response, path, self, paginated_options)
418
+
419
+ rescue Exceptions::ResourceMissing, Exceptions::ForbiddenRequest, Exceptions::ResourceMissing => e
420
+ response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message)
421
+ Models::HttpPaginatedResponse.new(response, path, self)
422
+ rescue Exceptions::TokenExpired, Exceptions::UnauthorizedRequest => e
423
+ response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message)
424
+ Models::HttpPaginatedResponse.new(response, path, self)
425
+ rescue Exceptions::InvalidRequest, Exceptions::ServerError => e
426
+ response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message)
427
+ Models::HttpPaginatedResponse.new(response, path, self)
428
+ end
429
+
430
+ # Retrieves an object that represents the current state of the device as a target for push notifications.
431
+ # @spec RSH8
432
+ # @return [Ably::Models::LocalDevice]
433
+ #
434
+ # @note This is unsupported in the Ruby library
435
+ def device
436
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable'
437
+ end
438
+
439
+ # A {Ably::PubSub::Http::Push} object.
440
+ # @spec RSH7
441
+ # @return [Ably::PubSub::Http::Push]
442
+ def push
443
+ @push ||= Push.new(self)
444
+ end
445
+
446
+ # @!attribute [r] uri
447
+ # @return [URI::Generic] Ably REST URI for the {#primary_domain}, used for all requests unless a fallback host is in use
448
+ def uri
449
+ uri_for_host(primary_domain)
450
+ end
451
+
452
+ # @!attribute [r] logger
453
+ # @return [Logger] The {Ably::Logger} for this client.
454
+ # Configure the log_level with the `:log_level` option, refer to {Client#initialize}
455
+ def logger
456
+ @logger ||= Ably::Logger.new(self, log_level, @custom_logger)
457
+ end
458
+
459
+ # @!attribute [r] mime_type
460
+ # @return [String] Mime type used for HTTP requests
461
+ def mime_type
462
+ case protocol
463
+ when :json
464
+ 'application/json'
465
+ else
466
+ 'application/x-msgpack'
467
+ end
468
+ end
469
+
470
+ # Register a message encoder and decoder that implements Ably::Models::MessageEncoders::Base interface.
471
+ # Message encoders are used to encode and decode message payloads automatically.
472
+ # @note Encoders and decoders are processed in the order they are added so the first encoder will be given priority when encoding and decoding
473
+ #
474
+ # @param [Ably::Models::MessageEncoders::Base] encoder
475
+ # @return [void]
476
+ #
477
+ # @api private
478
+ def register_encoder(encoder, options = {})
479
+ encoders << Ably::Models::MessageEncoders.encoder_from(encoder, options)
480
+ end
481
+
482
+ # @!attribute [r] protocol_binary?
483
+ # @return [Boolean] True of the transport #protocol communicates with Ably with a binary protocol
484
+ def protocol_binary?
485
+ protocol == :msgpack
486
+ end
487
+
488
+ # Connection used to make HTTP requests
489
+ #
490
+ # @param [Hash] options
491
+ # @option options [Boolean] :use_fallback when true, one of the fallback connections is used randomly, see the default {Ably::FALLBACK_HOSTS}
492
+ #
493
+ # @return [Faraday::Connection]
494
+ #
495
+ # @api private
496
+ def connection(options = {})
497
+ if options[:use_fallback]
498
+ fallback_connection
499
+ else
500
+ @connection ||= Faraday.new(uri.to_s, connection_options)
501
+ end
502
+ end
503
+
504
+ # Fallback connection used to make HTTP requests.
505
+ # Note, each request uses a random and then subsequent random {Ably::FALLBACK_HOSTS fallback hosts}
506
+ # are used (unless custom fallback hosts are provided with fallback_hosts)
507
+ #
508
+ # @return [Faraday::Connection]
509
+ #
510
+ # @api private
511
+ def fallback_connection
512
+ unless defined?(@fallback_connections) && @fallback_connections
513
+ @fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(uri_for_host(host).to_s, connection_options) }
514
+ end
515
+ @fallback_index ||= 0
516
+
517
+ @fallback_connections[@fallback_index % @fallback_connections.count].tap do
518
+ @fallback_index += 1
519
+ end
520
+ end
521
+
522
+ # Allowable duration for an external auth request
523
+ # For REST client this defaults to request_timeout
524
+ # For Realtime clients this defaults to 250ms less than the realtime_request_timeout
525
+ # ensuring an auth failure will be triggered before the realtime request timeout fires
526
+ # which would lead to a misleading error message (connection timeout as opposed to auth request timeout)
527
+ # @api private
528
+ def auth_request_timeout
529
+ if @realtime_client
530
+ @realtime_client.connection.defaults.fetch(:realtime_request_timeout) - 0.25
531
+ else
532
+ http_defaults.fetch(:request_timeout)
533
+ end
534
+ end
535
+
536
+ # If the primary host endpoint fails, and a subsequent fallback host succeeds, the fallback
537
+ # host that succeeded is used for +ClientOption+ +fallback_retry_timeout+ seconds to avoid
538
+ # retries to known failing hosts for a short period of time.
539
+ # See https://github.com/ably/docs/pull/554, spec id #RSC15f
540
+ #
541
+ # @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
542
+ def using_preferred_fallback_host?
543
+ if preferred_fallback_connection && (preferred_fallback_connection.fetch(:expires_at) > Time.now)
544
+ preferred_fallback_connection.fetch(:connection_object).host
545
+ end
546
+ end
547
+
548
+ private
549
+
550
+ attr_reader :preferred_fallback_connection
551
+
552
+ # See #using_preferred_fallback_host? for context
553
+ def set_preferred_fallback_connection(connection)
554
+ @preferred_fallback_connection = if connection == @connection
555
+ # If the succeeded connection is in fact the primary connection (tried after a failed fallback)
556
+ # then clear the preferred fallback connection
557
+ nil
558
+ else
559
+ {
560
+ expires_at: Time.now + options.fetch(:fallback_retry_timeout),
561
+ connection_object: connection,
562
+ }
563
+ end
564
+ end
565
+
566
+ def get_preferred_fallback_connection_object
567
+ preferred_fallback_connection.fetch(:connection_object) if using_preferred_fallback_host?
568
+ end
569
+
570
+ def raw_request(method, path, params = {}, options = {})
571
+ options = options.clone
572
+ if options.delete(:disable_automatic_reauthorize) == true
573
+ send_request(method, path, params, options)
574
+ else
575
+ reauthorize_on_authorization_failure do
576
+ send_request(method, path, params, options)
577
+ end
578
+ end
579
+ end
580
+
581
+ # Sends HTTP request to connection end point
582
+ # Connection failures will automatically be reattempted until thresholds are met
583
+ def send_request(method, path, params, options)
584
+ max_retry_count = http_defaults.fetch(:max_retry_count)
585
+ max_retry_duration = http_defaults.fetch(:max_retry_duration)
586
+ requested_at = Time.now
587
+ retry_count = 0
588
+ retry_sequence_id = nil
589
+ request_id = SecureRandom.urlsafe_base64(10) if add_request_ids
590
+
591
+ preferred_fallback_connection_for_first_request = get_preferred_fallback_connection_object
592
+
593
+ begin
594
+ use_fallback = can_fallback_to_alternate_ably_host? && (retry_count > 0)
595
+
596
+ conn = if preferred_fallback_connection_for_first_request
597
+ case retry_count
598
+ when 0
599
+ preferred_fallback_connection_for_first_request
600
+ when 1
601
+ # Ensure the root host is used first if the preferred fallback fails, see #RSC15f
602
+ connection(use_fallback: false)
603
+ end
604
+ end || connection(use_fallback: use_fallback) # default to normal connection selection process if not preferred connection set
605
+
606
+ conn.send(method, path, params) do |request|
607
+ if add_request_ids
608
+ request.params[:request_id] = request_id
609
+ request.options.context = {} if request.options.context.nil?
610
+ request.options.context[:request_id] = request_id
611
+ end
612
+ if options[:qs_params]
613
+ request.params.merge!(options[:qs_params])
614
+ end
615
+ unless options[:send_auth_header] == false
616
+ request.headers[:authorization] = auth.auth_header
617
+ # RSA7e2
618
+ options[:headers].to_h.merge(auth.extra_auth_headers).map do |key, val|
619
+ request.headers[key] = val
620
+ end
621
+ end
622
+ end.tap do
623
+ if retry_count > 0
624
+ retry_log_severity = log_retries_as_info ? :info : :warn
625
+ logger.public_send(retry_log_severity) do
626
+ "Ably::PubSub::Http::Client - Request SUCCEEDED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
627
+ " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
628
+ end
629
+ set_preferred_fallback_connection conn
630
+ end
631
+ end
632
+
633
+ rescue *([Faraday::TimeoutError, Ably::Exceptions::ServerError] + FARADAY_CLIENT_OR_SERVER_ERRORS) => error
634
+ retry_sequence_id ||= SecureRandom.urlsafe_base64(4)
635
+ time_passed = Time.now - requested_at
636
+
637
+ if can_fallback_to_alternate_ably_host? && (retry_count < max_retry_count) && (time_passed <= max_retry_duration)
638
+ retry_count += 1
639
+ retry_log_severity = log_retries_as_info ? :info : :warn
640
+ logger.public_send(retry_log_severity) { "Ably::PubSub::Http::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" }
641
+ retry
642
+ end
643
+
644
+ retry_log_severity = log_retries_as_info ? :info : :error
645
+ logger.public_send(retry_log_severity) do
646
+ "Ably::PubSub::Http::Client - Request FAILED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
647
+ " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
648
+ end
649
+
650
+ case error
651
+ when Faraday::TimeoutError
652
+ raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT, error, { request_id: request_id })
653
+ when *FARADAY_CLIENT_OR_SERVER_ERRORS
654
+ # request_id is also available in the request context
655
+ raise Ably::Exceptions::ConnectionError.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error, { request_id: request_id })
656
+ else
657
+ raise error
658
+ end
659
+ end
660
+ end
661
+
662
+ def reauthorize_on_authorization_failure
663
+ yield
664
+ rescue Ably::Exceptions::TokenExpired => e
665
+ if auth.token_renewable?
666
+ auth.authorize
667
+ yield
668
+ else
669
+ raise e
670
+ end
671
+ end
672
+
673
+ def uri_for_host(host)
674
+ port = if use_tls?
675
+ custom_tls_port
676
+ else
677
+ custom_port
678
+ end
679
+
680
+ raise ArgumentError, "Custom port must be an Integer or nil" if port && !port.kind_of?(Integer)
681
+
682
+ options = {
683
+ scheme: use_tls? ? 'https' : 'http',
684
+ host: host
685
+ }
686
+ options.merge!(port: port) if port
687
+
688
+ URI::Generic.build(options)
689
+ end
690
+
691
+ # Return a Hash of connection options to initiate the Faraday::Connection with
692
+ #
693
+ # @return [Hash]
694
+ def connection_options
695
+ @connection_options ||= {
696
+ builder: middleware,
697
+ headers: {
698
+ content_type: mime_type,
699
+ accept: mime_type,
700
+ user_agent: user_agent,
701
+ 'X-Ably-Version' => Ably::PROTOCOL_VERSION,
702
+ 'Ably-Agent' => agent
703
+ },
704
+ request: {
705
+ open_timeout: http_defaults.fetch(:open_timeout),
706
+ timeout: http_defaults.fetch(:request_timeout)
707
+ }
708
+ }
709
+ end
710
+
711
+ # Return a Faraday middleware stack to initiate the Faraday::RackBuilder with
712
+ #
713
+ # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
714
+ def middleware
715
+ @middleware ||= Faraday::RackBuilder.new do |builder|
716
+ setup_outgoing_middleware builder
717
+
718
+ # Raise exceptions if response code is invalid
719
+ builder.use Ably::PubSub::Http::Middleware::Exceptions
720
+
721
+ setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true
722
+
723
+ # Set Faraday's HTTP adapter with support for HTTP/2
724
+ builder.adapter :typhoeus, http_version: :httpv2_0
725
+ end
726
+ end
727
+
728
+ def can_fallback_to_alternate_ably_host?
729
+ fallback_hosts && !fallback_hosts.empty?
730
+ end
731
+
732
+ def initialize_default_encoders
733
+ Ably::Models::MessageEncoders.register_default_encoders self, binary_protocol: protocol == :msgpack
734
+ end
735
+ end
736
+ end
737
+ end
738
+ end