httpx 0.17.0 → 0.18.3

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/doc/release_notes/0_18_0.md +69 -0
  4. data/doc/release_notes/0_18_1.md +12 -0
  5. data/doc/release_notes/0_18_2.md +10 -0
  6. data/doc/release_notes/0_18_3.md +7 -0
  7. data/lib/httpx/adapters/datadog.rb +1 -1
  8. data/lib/httpx/adapters/faraday.rb +5 -3
  9. data/lib/httpx/adapters/webmock.rb +7 -1
  10. data/lib/httpx/altsvc.rb +2 -2
  11. data/lib/httpx/chainable.rb +3 -3
  12. data/lib/httpx/connection/http1.rb +8 -5
  13. data/lib/httpx/connection/http2.rb +22 -7
  14. data/lib/httpx/connection.rb +70 -71
  15. data/lib/httpx/domain_name.rb +1 -1
  16. data/lib/httpx/extensions.rb +50 -4
  17. data/lib/httpx/io/ssl.rb +5 -1
  18. data/lib/httpx/io/tls.rb +7 -7
  19. data/lib/httpx/loggable.rb +5 -5
  20. data/lib/httpx/options.rb +7 -7
  21. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  22. data/lib/httpx/plugins/aws_sigv4.rb +9 -11
  23. data/lib/httpx/plugins/compression.rb +5 -3
  24. data/lib/httpx/plugins/cookies/jar.rb +1 -1
  25. data/lib/httpx/plugins/expect.rb +7 -3
  26. data/lib/httpx/plugins/grpc/message.rb +2 -2
  27. data/lib/httpx/plugins/grpc.rb +3 -3
  28. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  29. data/lib/httpx/plugins/multipart.rb +2 -2
  30. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  31. data/lib/httpx/plugins/response_cache.rb +88 -0
  32. data/lib/httpx/plugins/retries.rb +36 -14
  33. data/lib/httpx/plugins/stream.rb +1 -1
  34. data/lib/httpx/pool.rb +39 -13
  35. data/lib/httpx/request.rb +7 -7
  36. data/lib/httpx/resolver/https.rb +5 -7
  37. data/lib/httpx/resolver/native.rb +4 -2
  38. data/lib/httpx/resolver/system.rb +2 -0
  39. data/lib/httpx/resolver.rb +2 -2
  40. data/lib/httpx/response.rb +23 -14
  41. data/lib/httpx/selector.rb +12 -17
  42. data/lib/httpx/session.rb +7 -2
  43. data/lib/httpx/session2.rb +1 -1
  44. data/lib/httpx/timers.rb +84 -0
  45. data/lib/httpx/transcoder/body.rb +2 -1
  46. data/lib/httpx/transcoder/form.rb +1 -1
  47. data/lib/httpx/transcoder/json.rb +1 -1
  48. data/lib/httpx/utils.rb +8 -0
  49. data/lib/httpx/version.rb +1 -1
  50. data/lib/httpx.rb +1 -0
  51. data/sig/chainable.rbs +1 -0
  52. data/sig/connection/http1.rbs +5 -0
  53. data/sig/connection/http2.rbs +3 -0
  54. data/sig/connection.rbs +12 -6
  55. data/sig/plugins/aws_sdk_authentication.rbs +22 -4
  56. data/sig/plugins/response_cache.rbs +35 -0
  57. data/sig/plugins/retries.rbs +3 -0
  58. data/sig/pool.rbs +6 -0
  59. data/sig/resolver/native.rbs +3 -4
  60. data/sig/resolver/system.rbs +2 -0
  61. data/sig/response.rbs +3 -2
  62. data/sig/timers.rbs +32 -0
  63. data/sig/utils.rbs +4 -0
  64. metadata +17 -17
@@ -12,19 +12,29 @@ module HTTPX
12
12
  # TODO: pass max_retries in a configure/load block
13
13
 
14
14
  IDEMPOTENT_METHODS = %i[get options head put delete].freeze
15
- RETRYABLE_ERRORS = [IOError,
16
- EOFError,
17
- Errno::ECONNRESET,
18
- Errno::ECONNABORTED,
19
- Errno::EPIPE,
20
- TLSError,
21
- TimeoutError,
22
- Parser::Error,
23
- Errno::EINVAL,
24
- Errno::ETIMEDOUT].freeze
25
-
26
- def self.extra_options(options)
27
- options.merge(max_retries: MAX_RETRIES)
15
+ RETRYABLE_ERRORS = [
16
+ IOError,
17
+ EOFError,
18
+ Errno::ECONNRESET,
19
+ Errno::ECONNABORTED,
20
+ Errno::EPIPE,
21
+ Errno::EINVAL,
22
+ Errno::ETIMEDOUT,
23
+ Parser::Error,
24
+ TLSError,
25
+ TimeoutError,
26
+ Connection::HTTP2::GoawayError,
27
+ ].freeze
28
+ DEFAULT_JITTER = ->(interval) { interval * (0.5 * (1 + rand)) }
29
+
30
+ if ENV.key?("HTTPX_NO_JITTER")
31
+ def self.extra_options(options)
32
+ options.merge(max_retries: MAX_RETRIES)
33
+ end
34
+ else
35
+ def self.extra_options(options)
36
+ options.merge(max_retries: MAX_RETRIES, retry_jitter: DEFAULT_JITTER)
37
+ end
28
38
  end
29
39
 
30
40
  module OptionsMethods
@@ -38,6 +48,13 @@ module HTTPX
38
48
  value
39
49
  end
40
50
 
51
+ def option_retry_jitter(value)
52
+ # return early if callable
53
+ raise TypeError, ":retry_jitter must be callable" unless value.respond_to?(:call)
54
+
55
+ value
56
+ end
57
+
41
58
  def option_max_retries(value)
42
59
  num = Integer(value)
43
60
  raise TypeError, ":max_retries must be positive" unless num.positive?
@@ -87,10 +104,15 @@ module HTTPX
87
104
  retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
88
105
 
89
106
  if retry_after
107
+ # apply jitter
108
+ if (jitter = request.options.retry_jitter)
109
+ retry_after = jitter.call(retry_after)
110
+ end
90
111
 
112
+ retry_start = Utils.now
91
113
  log { "retrying after #{retry_after} secs..." }
92
114
  pool.after(retry_after) do
93
- log { "retrying!!" }
115
+ log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
94
116
  connection = find_connection(request, connections, options)
95
117
  connection.send(request)
96
118
  end
@@ -9,7 +9,7 @@ module HTTPX
9
9
  end
10
10
 
11
11
  def each(&block)
12
- return enum_for(__method__) unless block_given?
12
+ return enum_for(__method__) unless block
13
13
 
14
14
  raise Error, "response already streamed" if @response
15
15
 
data/lib/httpx/pool.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
- require "timers"
5
4
  require "httpx/selector"
6
5
  require "httpx/connection"
7
6
  require "httpx/resolver"
8
7
 
9
8
  module HTTPX
10
9
  class Pool
10
+ using ArrayExtensions
11
11
  extend Forwardable
12
12
 
13
13
  def_delegator :@timers, :after
@@ -15,7 +15,7 @@ module HTTPX
15
15
  def initialize
16
16
  @resolvers = {}
17
17
  @_resolver_ios = {}
18
- @timers = Timers::Group.new
18
+ @timers = Timers.new
19
19
  @selector = Selector.new
20
20
  @connections = []
21
21
  @connected_connections = 0
@@ -27,15 +27,18 @@ module HTTPX
27
27
 
28
28
  def next_tick
29
29
  catch(:jump_tick) do
30
- timeout = [next_timeout, @timers.wait_interval].compact.min
30
+ timeout = next_timeout
31
31
  if timeout && timeout.negative?
32
32
  @timers.fire
33
33
  throw(:jump_tick)
34
34
  end
35
35
 
36
- @selector.select(timeout, &:call)
37
-
38
- @timers.fire
36
+ begin
37
+ @selector.select(timeout, &:call)
38
+ @timers.fire
39
+ rescue TimeoutError => e
40
+ @timers.fire(e)
41
+ end
39
42
  end
40
43
  rescue StandardError => e
41
44
  @connections.each do |connection|
@@ -64,6 +67,16 @@ module HTTPX
64
67
  connection.on(:open) do
65
68
  @connected_connections += 1
66
69
  end
70
+ connection.on(:activate) do
71
+ select_connection(connection)
72
+ end
73
+ end
74
+
75
+ def deactivate(connections)
76
+ connections.each do |connection|
77
+ connection.deactivate
78
+ deselect_connection(connection) if connection.state == :inactive
79
+ end
67
80
  end
68
81
 
69
82
  # opens a connection to the IP reachable through +uri+.
@@ -81,7 +94,7 @@ module HTTPX
81
94
  def resolve_connection(connection)
82
95
  @connections << connection unless @connections.include?(connection)
83
96
 
84
- if connection.addresses || connection.state == :open
97
+ if connection.addresses || connection.open?
85
98
  #
86
99
  # there are two cases in which we want to activate initialization of
87
100
  # connection immediately:
@@ -98,7 +111,7 @@ module HTTPX
98
111
  resolver << connection
99
112
  return if resolver.empty?
100
113
 
101
- @_resolver_ios[resolver] ||= @selector.register(resolver)
114
+ @_resolver_ios[resolver] ||= select_connection(resolver)
102
115
  end
103
116
 
104
117
  def on_resolver_connection(connection)
@@ -107,7 +120,7 @@ module HTTPX
107
120
  end
108
121
  return register_connection(connection) unless found_connection
109
122
 
110
- if found_connection.state == :open
123
+ if found_connection.open?
111
124
  coalesce_connections(found_connection, connection)
112
125
  throw(:coalesced, found_connection)
113
126
  else
@@ -129,7 +142,7 @@ module HTTPX
129
142
 
130
143
  @resolvers.delete(resolver_type)
131
144
 
132
- @selector.deregister(resolver)
145
+ deselect_connection(resolver)
133
146
  @_resolver_ios.delete(resolver)
134
147
  resolver.close unless resolver.closed?
135
148
  end
@@ -140,7 +153,7 @@ module HTTPX
140
153
  # consider it connected already.
141
154
  @connected_connections += 1
142
155
  end
143
- @selector.register(connection)
156
+ select_connection(connection)
144
157
  connection.on(:close) do
145
158
  unregister_connection(connection)
146
159
  end
@@ -148,10 +161,18 @@ module HTTPX
148
161
 
149
162
  def unregister_connection(connection)
150
163
  @connections.delete(connection)
151
- @selector.deregister(connection)
164
+ deselect_connection(connection)
152
165
  @connected_connections -= 1
153
166
  end
154
167
 
168
+ def select_connection(connection)
169
+ @selector.register(connection)
170
+ end
171
+
172
+ def deselect_connection(connection)
173
+ @selector.deregister(connection)
174
+ end
175
+
155
176
  def coalesce_connections(conn1, conn2)
156
177
  if conn1.coalescable?(conn2)
157
178
  conn1.merge(conn2)
@@ -162,7 +183,11 @@ module HTTPX
162
183
  end
163
184
 
164
185
  def next_timeout
165
- @resolvers.values.reject(&:closed?).map(&:timeout).compact.min || @connections.map(&:timeout).compact.min
186
+ [
187
+ @timers.wait_interval,
188
+ *@resolvers.values.reject(&:closed?).filter_map(&:timeout),
189
+ *@connections.filter_map(&:timeout),
190
+ ].compact.min
166
191
  end
167
192
 
168
193
  def find_resolver_for(connection)
@@ -172,6 +197,7 @@ module HTTPX
172
197
 
173
198
  @resolvers[resolver_type] ||= begin
174
199
  resolver = resolver_type.new(connection_options)
200
+ resolver.pool = self if resolver.respond_to?(:pool=)
175
201
  resolver.on(:resolve, &method(:on_resolver_connection))
176
202
  resolver.on(:error, &method(:on_resolver_error))
177
203
  resolver.on(:close) { on_resolver_close(resolver) }
data/lib/httpx/request.rb CHANGED
@@ -148,10 +148,10 @@ module HTTPX
148
148
  # :nocov:
149
149
  def inspect
150
150
  "#<HTTPX::Request:#{object_id} " \
151
- "#{@verb.to_s.upcase} " \
152
- "#{uri} " \
153
- "@headers=#{@headers} " \
154
- "@body=#{@body}>"
151
+ "#{@verb.to_s.upcase} " \
152
+ "#{uri} " \
153
+ "@headers=#{@headers} " \
154
+ "@body=#{@body}>"
155
155
  end
156
156
  # :nocov:
157
157
 
@@ -181,7 +181,7 @@ module HTTPX
181
181
  end
182
182
 
183
183
  def each(&block)
184
- return enum_for(__method__) unless block_given?
184
+ return enum_for(__method__) unless block
185
185
  return if @body.nil?
186
186
 
187
187
  body = stream(@body)
@@ -236,7 +236,7 @@ module HTTPX
236
236
  # :nocov:
237
237
  def inspect
238
238
  "#<HTTPX::Request::Body:#{object_id} " \
239
- "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
239
+ "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
240
240
  end
241
241
  # :nocov:
242
242
  end
@@ -285,7 +285,7 @@ module HTTPX
285
285
  end
286
286
 
287
287
  def write(data)
288
- @block.call(data)
288
+ @block.call(data.dup)
289
289
  data.bytesize
290
290
  end
291
291
  end
@@ -24,7 +24,9 @@ module HTTPX
24
24
  record_types: RECORD_TYPES.keys,
25
25
  }.freeze
26
26
 
27
- def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close
27
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
28
+
29
+ attr_writer :pool
28
30
 
29
31
  def initialize(options)
30
32
  @options = Options.new(options)
@@ -63,15 +65,11 @@ module HTTPX
63
65
 
64
66
  private
65
67
 
66
- def pool
67
- Thread.current[:httpx_connection_pool] ||= Pool.new
68
- end
69
-
70
68
  def resolver_connection
71
- @resolver_connection ||= pool.find_connection(@uri, @options) || begin
69
+ @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
72
70
  @building_connection = true
73
71
  connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
74
- pool.init_connection(connection, @options)
72
+ @pool.init_connection(connection, @options)
75
73
  emit_addresses(connection, @uri_addresses)
76
74
  @building_connection = false
77
75
  connection
@@ -47,6 +47,8 @@ module HTTPX
47
47
 
48
48
  def_delegator :@connections, :empty?
49
49
 
50
+ attr_reader :state
51
+
50
52
  def initialize(options)
51
53
  @options = Options.new(options)
52
54
  @ns_index = 0
@@ -120,7 +122,7 @@ module HTTPX
120
122
  def timeout
121
123
  return if @connections.empty?
122
124
 
123
- @start_timeout = Process.clock_gettime(Process::CLOCK_MONOTONIC)
125
+ @start_timeout = Utils.now
124
126
  hosts = @queries.keys
125
127
  @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
126
128
  end
@@ -140,7 +142,7 @@ module HTTPX
140
142
  def do_retry
141
143
  return if @queries.empty?
142
144
 
143
- loop_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_timeout
145
+ loop_time = Utils.elapsed_time(@start_timeout)
144
146
  connections = []
145
147
  queries = {}
146
148
  while (query = @queries.shift)
@@ -12,6 +12,8 @@ module HTTPX
12
12
  Resolv::DNS::EncodeError,
13
13
  Resolv::DNS::DecodeError].freeze
14
14
 
15
+ attr_reader :state
16
+
15
17
  def initialize(options)
16
18
  @options = Options.new(options)
17
19
  @resolver_options = @options.resolver_options
@@ -26,14 +26,14 @@ module HTTPX
26
26
  module_function
27
27
 
28
28
  def cached_lookup(hostname)
29
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ now = Utils.now
30
30
  @lookup_mutex.synchronize do
31
31
  lookup(hostname, now)
32
32
  end
33
33
  end
34
34
 
35
35
  def cached_lookup_set(hostname, entries)
36
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
+ now = Utils.now
37
37
  entries.each do |entry|
38
38
  entry["TTL"] += now
39
39
  end
@@ -57,17 +57,23 @@ module HTTPX
57
57
  # :nocov:
58
58
  def inspect
59
59
  "#<Response:#{object_id} "\
60
- "HTTP/#{version} " \
61
- "@status=#{@status} " \
62
- "@headers=#{@headers} " \
63
- "@body=#{@body.bytesize}>"
60
+ "HTTP/#{version} " \
61
+ "@status=#{@status} " \
62
+ "@headers=#{@headers} " \
63
+ "@body=#{@body.bytesize}>"
64
64
  end
65
65
  # :nocov:
66
66
 
67
- def raise_for_status
67
+ def error
68
68
  return if @status < 400
69
69
 
70
- raise HTTPError, self
70
+ HTTPError.new(self)
71
+ end
72
+
73
+ def raise_for_status
74
+ return self unless (err = error)
75
+
76
+ raise err
71
77
  end
72
78
 
73
79
  def json(options = nil)
@@ -211,18 +217,20 @@ module HTTPX
211
217
  end
212
218
 
213
219
  def ==(other)
214
- if other.respond_to?(:read)
215
- _with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
216
- else
217
- to_s == other.to_s
220
+ object_id == other.object_id || begin
221
+ if other.respond_to?(:read)
222
+ _with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
223
+ else
224
+ to_s == other.to_s
225
+ end
218
226
  end
219
227
  end
220
228
 
221
229
  # :nocov:
222
230
  def inspect
223
231
  "#<HTTPX::Response::Body:#{object_id} " \
224
- "@state=#{@state} " \
225
- "@length=#{@length}>"
232
+ "@state=#{@state} " \
233
+ "@length=#{@length}>"
226
234
  end
227
235
  # :nocov:
228
236
 
@@ -311,17 +319,18 @@ module HTTPX
311
319
  end
312
320
 
313
321
  def status
322
+ warn ":#{__method__} is deprecated, use :error.message instead"
314
323
  @error.message
315
324
  end
316
325
 
317
326
  if Exception.method_defined?(:full_message)
318
327
  def to_s
319
- @error.full_message
328
+ @error.full_message(highlight: false)
320
329
  end
321
330
  else
322
331
  def to_s
323
332
  "#{@error.message} (#{@error.class})\n" \
324
- "#{@error.backtrace.join("\n") if @error.backtrace}"
333
+ "#{@error.backtrace.join("\n") if @error.backtrace}"
325
334
  end
326
335
  end
327
336
 
@@ -2,20 +2,6 @@
2
2
 
3
3
  require "io/wait"
4
4
 
5
- module IOExtensions
6
- refine IO do
7
- # provides a fallback for rubies where IO#wait isn't implemented,
8
- # but IO#wait_readable and IO#wait_writable are.
9
- def wait(timeout = nil, _mode = :read_write)
10
- r, w = IO.select([self], [self], nil, timeout)
11
-
12
- return unless r || w
13
-
14
- self
15
- end
16
- end
17
- end
18
-
19
5
  class HTTPX::Selector
20
6
  READABLE = %i[rw r].freeze
21
7
  WRITABLE = %i[rw w].freeze
@@ -23,7 +9,7 @@ class HTTPX::Selector
23
9
  private_constant :READABLE
24
10
  private_constant :WRITABLE
25
11
 
26
- using IOExtensions unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
12
+ using HTTPX::IOExtensions
27
13
 
28
14
  def initialize
29
15
  @selectables = []
@@ -58,11 +44,13 @@ class HTTPX::Selector
58
44
  selectables = @selectables
59
45
  @selectables = []
60
46
 
61
- selectables.each do |io|
47
+ selectables.delete_if do |io|
62
48
  interests = io.interests
63
49
 
64
50
  (r ||= []) << io if READABLE.include?(interests)
65
51
  (w ||= []) << io if WRITABLE.include?(interests)
52
+
53
+ io.state == :closed
66
54
  end
67
55
 
68
56
  if @selectables.empty?
@@ -70,7 +58,7 @@ class HTTPX::Selector
70
58
 
71
59
  # do not run event loop if there's nothing to wait on.
72
60
  # this might happen if connect failed and connection was unregistered.
73
- return if (!r || r.empty?) && (!w || w.empty?)
61
+ return if (!r || r.empty?) && (!w || w.empty?) && !selectables.empty?
74
62
 
75
63
  break
76
64
  else
@@ -129,6 +117,13 @@ class HTTPX::Selector
129
117
  end
130
118
 
131
119
  def select(interval, &block)
120
+ # do not cause an infinite loop here.
121
+ #
122
+ # this may happen if timeout calculation actually triggered an error which causes
123
+ # the connections to be reaped (such as the total timeout error) before #select
124
+ # gets called.
125
+ return if interval.nil? && @selectables.empty?
126
+
132
127
  return select_one(interval, &block) if @selectables.size == 1
133
128
 
134
129
  select_many(interval, &block)
data/lib/httpx/session.rb CHANGED
@@ -11,7 +11,7 @@ module HTTPX
11
11
  @options = self.class.default_options.merge(options)
12
12
  @responses = {}
13
13
  @persistent = @options.persistent
14
- wrap(&blk) if block_given?
14
+ wrap(&blk) if blk
15
15
  end
16
16
 
17
17
  def wrap
@@ -21,6 +21,7 @@ module HTTPX
21
21
  yield self
22
22
  ensure
23
23
  @persistent = prev_persistent
24
+ close unless @persistent
24
25
  end
25
26
  end
26
27
 
@@ -226,7 +227,11 @@ module HTTPX
226
227
  end
227
228
  responses
228
229
  ensure
229
- close(connections) unless @persistent
230
+ if @persistent
231
+ pool.deactivate(connections)
232
+ else
233
+ close(connections)
234
+ end
230
235
  end
231
236
  end
232
237
 
@@ -7,7 +7,7 @@ module HTTPX
7
7
  @options = self.class.default_options.merge(options)
8
8
  @responses = {}
9
9
  @persistent = @options.persistent
10
- wrap(&blk) if block_given?
10
+ wrap(&blk) if blk
11
11
  end
12
12
 
13
13
  def wrap
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ class Timers
5
+ def initialize
6
+ @intervals = []
7
+ end
8
+
9
+ def after(interval_in_secs, &blk)
10
+ return unless interval_in_secs
11
+
12
+ # I'm assuming here that most requests will have the same
13
+ # request timeout, as in most cases they share common set of
14
+ # options. A user setting different request timeouts for 100s of
15
+ # requests will already have a hard time dealing with that.
16
+ unless (interval = @intervals.find { |t| t == interval_in_secs })
17
+ interval = Interval.new(interval_in_secs)
18
+ @intervals << interval
19
+ @intervals.sort!
20
+ end
21
+
22
+ interval << blk
23
+ end
24
+
25
+ def wait_interval
26
+ return if @intervals.empty?
27
+
28
+ @next_interval_at = Utils.now
29
+
30
+ @intervals.first.interval
31
+ end
32
+
33
+ def fire(error = nil)
34
+ raise error if error && error.timeout != @intervals.first
35
+ return if @intervals.empty? || !@next_interval_at
36
+
37
+ elapsed_time = Utils.elapsed_time(@next_interval_at)
38
+
39
+ @intervals.delete_if { |interval| interval.elapse(elapsed_time) <= 0 }
40
+ end
41
+
42
+ def cancel
43
+ @intervals.clear
44
+ end
45
+
46
+ class Interval
47
+ include Comparable
48
+
49
+ attr_reader :interval
50
+
51
+ def initialize(interval)
52
+ @interval = interval
53
+ @callbacks = []
54
+ end
55
+
56
+ def <=>(other)
57
+ @interval <=> other.interval
58
+ end
59
+
60
+ def ==(other)
61
+ return @interval == other if other.is_a?(Numeric)
62
+
63
+ @interval == other.to_f # rubocop:disable Lint/FloatComparison
64
+ end
65
+
66
+ def to_f
67
+ @interval
68
+ end
69
+
70
+ def <<(callback)
71
+ @callbacks << callback
72
+ end
73
+
74
+ def elapse(elapsed)
75
+ @interval -= elapsed
76
+
77
+ @callbacks.each(&:call) if @interval <= 0
78
+
79
+ @interval
80
+ end
81
+ end
82
+ private_constant :Interval
83
+ end
84
+ end
@@ -9,6 +9,7 @@ module HTTPX::Transcoder
9
9
  module_function
10
10
 
11
11
  class Encoder
12
+ using HTTPX::ArrayExtensions
12
13
  extend Forwardable
13
14
 
14
15
  def_delegator :@raw, :to_s
@@ -21,7 +22,7 @@ module HTTPX::Transcoder
21
22
  if @raw.respond_to?(:bytesize)
22
23
  @raw.bytesize
23
24
  elsif @raw.respond_to?(:to_ary)
24
- @raw.map(&:bytesize).reduce(0, :+)
25
+ @raw.sum(&:bytesize)
25
26
  elsif @raw.respond_to?(:size)
26
27
  @raw.size || Float::INFINITY
27
28
  elsif @raw.respond_to?(:length)
@@ -50,7 +50,7 @@ module HTTPX::Transcoder
50
50
  def decode(response)
51
51
  content_type = response.content_type.mime_type
52
52
 
53
- raise Error, "invalid form mime type (#{content_type})" unless content_type == "application/x-www-form-urlencoded"
53
+ raise HTTPX::Error, "invalid form mime type (#{content_type})" unless content_type == "application/x-www-form-urlencoded"
54
54
 
55
55
  Decoder
56
56
  end
@@ -35,7 +35,7 @@ module HTTPX::Transcoder
35
35
  def decode(response)
36
36
  content_type = response.content_type.mime_type
37
37
 
38
- raise Error, "invalid json mime type (#{content_type})" unless JSON_REGEX.match?(content_type)
38
+ raise HTTPX::Error, "invalid json mime type (#{content_type})" unless JSON_REGEX.match?(content_type)
39
39
 
40
40
  ::JSON.method(:parse)
41
41
  end
data/lib/httpx/utils.rb CHANGED
@@ -6,6 +6,14 @@ module HTTPX
6
6
 
7
7
  module_function
8
8
 
9
+ def now
10
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
+ end
12
+
13
+ def elapsed_time(monotonic_timestamp)
14
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - monotonic_timestamp
15
+ end
16
+
9
17
  # The value of this field can be either an HTTP-date or a number of
10
18
  # seconds to delay after the response is received.
11
19
  def parse_retry_after(retry_after)
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "0.17.0"
4
+ VERSION = "0.18.3"
5
5
  end