ably-pubsub-core 0.0.1.pre

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