httpx 0.7.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +48 -0
  3. data/README.md +9 -5
  4. data/doc/release_notes/0_0_1.md +7 -0
  5. data/doc/release_notes/0_0_2.md +9 -0
  6. data/doc/release_notes/0_0_3.md +9 -0
  7. data/doc/release_notes/0_0_4.md +7 -0
  8. data/doc/release_notes/0_0_5.md +5 -0
  9. data/doc/release_notes/0_10_0.md +66 -0
  10. data/doc/release_notes/0_1_0.md +9 -0
  11. data/doc/release_notes/0_2_0.md +5 -0
  12. data/doc/release_notes/0_2_1.md +16 -0
  13. data/doc/release_notes/0_3_0.md +12 -0
  14. data/doc/release_notes/0_3_1.md +6 -0
  15. data/doc/release_notes/0_4_0.md +51 -0
  16. data/doc/release_notes/0_4_1.md +3 -0
  17. data/doc/release_notes/0_5_0.md +15 -0
  18. data/doc/release_notes/0_5_1.md +14 -0
  19. data/doc/release_notes/0_6_0.md +5 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_6_2.md +6 -0
  22. data/doc/release_notes/0_6_3.md +13 -0
  23. data/doc/release_notes/0_6_4.md +21 -0
  24. data/doc/release_notes/0_6_5.md +22 -0
  25. data/doc/release_notes/0_6_6.md +19 -0
  26. data/doc/release_notes/0_6_7.md +5 -0
  27. data/doc/release_notes/0_7_0.md +46 -0
  28. data/doc/release_notes/0_8_0.md +27 -0
  29. data/doc/release_notes/0_8_1.md +8 -0
  30. data/doc/release_notes/0_8_2.md +7 -0
  31. data/doc/release_notes/0_9_0.md +38 -0
  32. data/lib/httpx.rb +2 -0
  33. data/lib/httpx/adapters/faraday.rb +1 -1
  34. data/lib/httpx/altsvc.rb +18 -2
  35. data/lib/httpx/chainable.rb +9 -8
  36. data/lib/httpx/connection.rb +177 -72
  37. data/lib/httpx/connection/http1.rb +44 -13
  38. data/lib/httpx/connection/http2.rb +77 -34
  39. data/lib/httpx/domain_name.rb +440 -0
  40. data/lib/httpx/errors.rb +1 -0
  41. data/lib/httpx/extensions.rb +23 -3
  42. data/lib/httpx/headers.rb +2 -2
  43. data/lib/httpx/io/ssl.rb +11 -4
  44. data/lib/httpx/io/tcp.rb +16 -5
  45. data/lib/httpx/io/udp.rb +4 -1
  46. data/lib/httpx/loggable.rb +6 -6
  47. data/lib/httpx/options.rb +22 -15
  48. data/lib/httpx/parser/http1.rb +14 -17
  49. data/lib/httpx/plugins/compression.rb +49 -64
  50. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  51. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  52. data/lib/httpx/plugins/compression/gzip.rb +45 -17
  53. data/lib/httpx/plugins/cookies.rb +21 -60
  54. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  55. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  56. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  57. data/lib/httpx/plugins/digest_authentication.rb +2 -0
  58. data/lib/httpx/plugins/expect.rb +12 -1
  59. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  60. data/lib/httpx/plugins/h2c.rb +1 -1
  61. data/lib/httpx/plugins/multipart.rb +0 -8
  62. data/lib/httpx/plugins/persistent.rb +6 -1
  63. data/lib/httpx/plugins/proxy.rb +16 -12
  64. data/lib/httpx/plugins/proxy/http.rb +7 -2
  65. data/lib/httpx/plugins/proxy/socks4.rb +4 -2
  66. data/lib/httpx/plugins/proxy/socks5.rb +5 -1
  67. data/lib/httpx/plugins/push_promise.rb +2 -2
  68. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  69. data/lib/httpx/plugins/retries.rb +13 -6
  70. data/lib/httpx/plugins/stream.rb +109 -13
  71. data/lib/httpx/pool.rb +13 -15
  72. data/lib/httpx/registry.rb +2 -1
  73. data/lib/httpx/request.rb +14 -19
  74. data/lib/httpx/resolver.rb +7 -8
  75. data/lib/httpx/resolver/https.rb +22 -5
  76. data/lib/httpx/resolver/native.rb +27 -33
  77. data/lib/httpx/resolver/options.rb +2 -2
  78. data/lib/httpx/resolver/resolver_mixin.rb +1 -1
  79. data/lib/httpx/response.rb +22 -17
  80. data/lib/httpx/selector.rb +96 -97
  81. data/lib/httpx/session.rb +32 -24
  82. data/lib/httpx/timeout.rb +7 -1
  83. data/lib/httpx/transcoder/chunker.rb +0 -2
  84. data/lib/httpx/transcoder/form.rb +0 -6
  85. data/lib/httpx/transcoder/json.rb +0 -4
  86. data/lib/httpx/utils.rb +45 -0
  87. data/lib/httpx/version.rb +1 -1
  88. data/sig/buffer.rbs +24 -0
  89. data/sig/callbacks.rbs +14 -0
  90. data/sig/chainable.rbs +37 -0
  91. data/sig/connection.rbs +2 -0
  92. data/sig/connection/http2.rbs +4 -0
  93. data/sig/domain_name.rbs +17 -0
  94. data/sig/errors.rbs +3 -0
  95. data/sig/headers.rbs +42 -0
  96. data/sig/httpx.rbs +14 -0
  97. data/sig/loggable.rbs +11 -0
  98. data/sig/missing.rbs +12 -0
  99. data/sig/options.rbs +118 -0
  100. data/sig/parser/http1.rbs +50 -0
  101. data/sig/plugins/authentication.rbs +11 -0
  102. data/sig/plugins/basic_authentication.rbs +13 -0
  103. data/sig/plugins/compression.rbs +55 -0
  104. data/sig/plugins/compression/brotli.rbs +21 -0
  105. data/sig/plugins/compression/deflate.rbs +17 -0
  106. data/sig/plugins/compression/gzip.rbs +29 -0
  107. data/sig/plugins/cookies.rbs +26 -0
  108. data/sig/plugins/cookies/cookie.rbs +50 -0
  109. data/sig/plugins/cookies/jar.rbs +27 -0
  110. data/sig/plugins/digest_authentication.rbs +33 -0
  111. data/sig/plugins/expect.rbs +19 -0
  112. data/sig/plugins/follow_redirects.rbs +37 -0
  113. data/sig/plugins/h2c.rbs +26 -0
  114. data/sig/plugins/multipart.rbs +19 -0
  115. data/sig/plugins/persistent.rbs +17 -0
  116. data/sig/plugins/proxy.rbs +47 -0
  117. data/sig/plugins/proxy/http.rbs +14 -0
  118. data/sig/plugins/proxy/socks4.rbs +33 -0
  119. data/sig/plugins/proxy/socks5.rbs +36 -0
  120. data/sig/plugins/proxy/ssh.rbs +18 -0
  121. data/sig/plugins/push_promise.rbs +22 -0
  122. data/sig/plugins/rate_limiter.rbs +11 -0
  123. data/sig/plugins/retries.rbs +48 -0
  124. data/sig/plugins/stream.rbs +39 -0
  125. data/sig/pool.rbs +2 -0
  126. data/sig/registry.rbs +9 -0
  127. data/sig/request.rbs +61 -0
  128. data/sig/response.rbs +87 -0
  129. data/sig/session.rbs +49 -0
  130. data/sig/test.rbs +9 -0
  131. data/sig/timeout.rbs +29 -0
  132. data/sig/transcoder.rbs +16 -0
  133. data/sig/transcoder/body.rbs +18 -0
  134. data/sig/transcoder/chunker.rbs +32 -0
  135. data/sig/transcoder/form.rbs +16 -0
  136. data/sig/transcoder/json.rbs +14 -0
  137. metadata +120 -21
@@ -25,7 +25,7 @@ module HTTPX
25
25
 
26
26
  def_delegator :@connections, :empty?
27
27
 
28
- def_delegators :@resolver_connection, :to_io, :call, :interests, :close
28
+ def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close
29
29
 
30
30
  def initialize(options)
31
31
  @options = Options.new(options)
@@ -62,8 +62,20 @@ module HTTPX
62
62
  resolver_connection.closed?
63
63
  end
64
64
 
65
+ def interests
66
+ return if @queries.empty?
67
+
68
+ resolver_connection.__send__(__method__)
69
+ end
70
+
65
71
  private
66
72
 
73
+ def connect
74
+ return if @queries.empty?
75
+
76
+ resolver_connection.__send__(__method__)
77
+ end
78
+
67
79
  def pool
68
80
  Thread.current[:httpx_connection_pool] ||= Pool.new
69
81
  end
@@ -82,9 +94,14 @@ module HTTPX
82
94
  def resolve(connection = @connections.first, hostname = nil)
83
95
  return if @building_connection
84
96
 
85
- hostname = hostname || @queries.key(connection) || connection.origin.host
97
+ hostname ||= @queries.key(connection)
98
+
99
+ if hostname.nil?
100
+ hostname = connection.origin.host
101
+ log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
102
+ end
86
103
  type = @_record_types[hostname].first
87
- log(label: "resolver: ") { "query #{type} for #{hostname}" }
104
+ log { "resolver: query #{type} for #{hostname}" }
88
105
  begin
89
106
  request = build_request(hostname, type)
90
107
  @requests[request] = connection
@@ -111,7 +128,7 @@ module HTTPX
111
128
  end
112
129
 
113
130
  def on_promise(_, stream)
114
- log(level: 2, label: "#{stream.id}: ") { "refusing stream!" }
131
+ log(level: 2) { "#{stream.id}: refusing stream!" }
115
132
  stream.refuse
116
133
  end
117
134
 
@@ -194,7 +211,7 @@ module HTTPX
194
211
  case response.headers["content-type"]
195
212
  when "application/dns-json",
196
213
  "application/json",
197
- %r{^application\/x\-javascript} # because google...
214
+ %r{^application/x-javascript} # because google...
198
215
  payload = JSON.parse(response.to_s)
199
216
  payload["Answer"]
200
217
  when "application/dns-udpwireformat",
@@ -7,6 +7,7 @@ module HTTPX
7
7
  class Resolver::Native
8
8
  extend Forwardable
9
9
  include Resolver::ResolverMixin
10
+ using URIExtensions
10
11
 
11
12
  RESOLVE_TIMEOUT = 5
12
13
  RECORD_TYPES = {
@@ -73,14 +74,6 @@ module HTTPX
73
74
  end
74
75
 
75
76
  def to_io
76
- case @state
77
- when :idle
78
- transition(:open)
79
- when :closed
80
- transition(:idle)
81
- transition(:open)
82
- end
83
- resolve if @queries.empty?
84
77
  @io.to_io
85
78
  end
86
79
 
@@ -93,11 +86,7 @@ module HTTPX
93
86
  rescue Errno::EHOSTUNREACH => e
94
87
  @ns_index += 1
95
88
  if @ns_index < @nameserver.size
96
- log(label: "resolver: ") do
97
- # :nocov:
98
- "failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})"
99
- # :nocov:
100
- end
89
+ log { "resolver: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
101
90
  transition(:idle)
102
91
  else
103
92
  handle_error(e)
@@ -107,6 +96,14 @@ module HTTPX
107
96
  end
108
97
 
109
98
  def interests
99
+ case @state
100
+ when :idle
101
+ transition(:open)
102
+ when :closed
103
+ transition(:idle)
104
+ transition(:open)
105
+ end
106
+
110
107
  !@write_buffer.empty? || @queries.empty? ? :w : :r
111
108
  end
112
109
 
@@ -160,11 +157,7 @@ module HTTPX
160
157
  raise NativeResolveError.new(connection, host)
161
158
  else
162
159
  connections << connection
163
- log(label: "resolver: ") do
164
- # :nocov:
165
- "timeout after #{prev_timeout}s, retry(#{timeouts.first}) #{host}..."
166
- # :nocov:
167
- end
160
+ log { "resolver: timeout after #{prev_timeout}s, retry(#{timeouts.first}) #{host}..." }
168
161
  end
169
162
  end
170
163
  @queries = queries
@@ -174,14 +167,10 @@ module HTTPX
174
167
  def dread(wsize = @resolver_options.packet_size)
175
168
  loop do
176
169
  siz = @io.read(wsize, @read_buffer)
177
- unless siz
178
- emit(:close)
179
- return
180
- end
181
- return if siz.zero?
170
+ return unless siz && siz.positive?
182
171
 
183
- log(label: "resolver: ") { "READ: #{siz} bytes..." }
184
172
  parse(@read_buffer)
173
+ return if @state == :closed
185
174
  end
186
175
  end
187
176
 
@@ -190,12 +179,9 @@ module HTTPX
190
179
  return if @write_buffer.empty?
191
180
 
192
181
  siz = @io.write(@write_buffer)
193
- unless siz
194
- emit(:close)
195
- return
196
- end
197
- log(label: "resolver: ") { "WRITE: #{siz} bytes..." }
198
- return if siz.zero?
182
+ return unless siz && siz.positive?
183
+
184
+ return if @state == :closed
199
185
  end
200
186
  end
201
187
 
@@ -250,10 +236,15 @@ module HTTPX
250
236
  raise Error, "no URI to resolve" unless connection
251
237
  return unless @write_buffer.empty?
252
238
 
253
- hostname = hostname || @queries.key(connection) || connection.origin.host
239
+ hostname ||= @queries.key(connection)
240
+
241
+ if hostname.nil?
242
+ hostname = connection.origin.host
243
+ log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
244
+ end
254
245
  @queries[hostname] = connection
255
246
  type = @_record_types[hostname].first
256
- log(label: "resolver: ") { "query #{type} for #{hostname}" }
247
+ log { "resolver: query #{type} for #{hostname}" }
257
248
  begin
258
249
  @write_buffer << Resolver.encode_dns_query(hostname, type: RECORD_TYPES[type])
259
250
  rescue Resolv::DNS::EncodeError => e
@@ -269,7 +260,7 @@ module HTTPX
269
260
  uri = URI::Generic.build(scheme: "udp", port: port)
270
261
  uri.hostname = ip
271
262
  type = IO.registry(uri.scheme)
272
- log(label: "resolver: ") { "server: #{uri}..." }
263
+ log { "resolver: server: #{uri}..." }
273
264
  @io = type.new(uri, [IPAddr.new(ip)], @options)
274
265
  end
275
266
 
@@ -285,8 +276,11 @@ module HTTPX
285
276
  return unless @state == :idle
286
277
 
287
278
  build_socket
279
+
288
280
  @io.connect
289
281
  return unless @io.connected?
282
+
283
+ resolve if @queries.empty?
290
284
  when :closed
291
285
  return unless @state == :open
292
286
 
@@ -6,7 +6,7 @@ module HTTPX
6
6
  @options = options
7
7
  end
8
8
 
9
- def method_missing(m, *args, &block)
9
+ def method_missing(m, *, &block)
10
10
  if @options.key?(m)
11
11
  @options[m]
12
12
  else
@@ -14,7 +14,7 @@ module HTTPX
14
14
  end
15
15
  end
16
16
 
17
- def respond_to_missing?(m)
17
+ def respond_to_missing?(m, *)
18
18
  @options.key?(m) || super
19
19
  end
20
20
 
@@ -30,7 +30,7 @@ module HTTPX
30
30
  addresses.map! do |address|
31
31
  address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
32
32
  end
33
- log(label: "resolver: ") { "answer #{connection.origin.host}: #{addresses.inspect}" }
33
+ log { "resolver: answer #{connection.origin.host}: #{addresses.inspect}" }
34
34
  connection.addresses = addresses
35
35
  catch(:coalesced) { emit(:resolve, connection) }
36
36
  end
@@ -58,7 +58,7 @@ module HTTPX
58
58
  "HTTP/#{version} " \
59
59
  "@status=#{@status} " \
60
60
  "@headers=#{@headers} " \
61
- "@body=#{@body}>"
61
+ "@body=#{@body.bytesize}>"
62
62
  end
63
63
  # :nocov:
64
64
 
@@ -75,11 +75,11 @@ module HTTPX
75
75
  @status == 204 ||
76
76
  @status == 205 ||
77
77
  @status == 304 || begin
78
- content_length = @headers["content-length"]
79
- return false if content_length.nil?
78
+ content_length = @headers["content-length"]
79
+ return false if content_length.nil?
80
80
 
81
- content_length == "0"
82
- end
81
+ content_length == "0"
82
+ end
83
83
  end
84
84
 
85
85
  class Body
@@ -95,6 +95,8 @@ module HTTPX
95
95
  end
96
96
 
97
97
  def write(chunk)
98
+ return if @state == :closed
99
+
98
100
  @length += chunk.bytesize
99
101
  transition
100
102
  @buffer.write(chunk)
@@ -116,7 +118,7 @@ module HTTPX
116
118
  return enum_for(__method__) unless block_given?
117
119
 
118
120
  begin
119
- unless @state == :idle
121
+ if @buffer
120
122
  rewind
121
123
  while (chunk = @buffer.read(@window_size))
122
124
  yield(chunk.force_encoding(@encoding))
@@ -150,23 +152,24 @@ module HTTPX
150
152
  def copy_to(dest)
151
153
  return unless @buffer
152
154
 
155
+ rewind
156
+
153
157
  if dest.respond_to?(:path) && @buffer.respond_to?(:path)
154
158
  FileUtils.mv(@buffer.path, dest.path)
155
159
  else
156
- @buffer.rewind
157
160
  ::IO.copy_stream(@buffer, dest)
158
161
  end
159
162
  end
160
163
 
161
164
  # closes/cleans the buffer, resets everything
162
165
  def close
163
- return if @state == :idle
164
-
165
- @buffer.close
166
- @buffer.unlink if @buffer.respond_to?(:unlink)
167
- @buffer = nil
166
+ if @buffer
167
+ @buffer.close
168
+ @buffer.unlink if @buffer.respond_to?(:unlink)
169
+ @buffer = nil
170
+ end
168
171
  @length = 0
169
- @state = :idle
172
+ @state = :closed
170
173
  end
171
174
 
172
175
  def ==(other)
@@ -184,7 +187,7 @@ module HTTPX
184
187
  private
185
188
 
186
189
  def rewind
187
- return if @state == :idle
190
+ return unless @buffer
188
191
 
189
192
  @buffer.rewind
190
193
  end
@@ -237,13 +240,11 @@ module HTTPX
237
240
 
238
241
  private
239
242
 
240
- # :nodoc:
241
243
  def mime_type(str)
242
244
  m = str.to_s[MIME_TYPE_RE, 1]
243
245
  m && m.strip.downcase
244
246
  end
245
247
 
246
- # :nodoc:
247
248
  def charset(str)
248
249
  m = str.to_s[CHARSET_RE, 1]
249
250
  m && m.strip.delete('"')
@@ -267,13 +268,17 @@ module HTTPX
267
268
  @error.message
268
269
  end
269
270
 
271
+ def to_s
272
+ @error.backtrace.join("\n")
273
+ end
274
+
270
275
  def raise_for_status
271
276
  raise @error
272
277
  end
273
278
 
274
279
  # rubocop:disable Style/MissingRespondToMissing
275
280
  def method_missing(meth, *, &block)
276
- raise NoMethodError, "undefined response method `#{meth}' for error response" if Response.public_method_defined?(meth)
281
+ raise NoMethodError, "undefined response method `#{meth}' for error response" if @options.response_class.public_method_defined?(meth)
277
282
 
278
283
  super
279
284
  end
@@ -1,5 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "io/wait"
4
+
5
+ module IOExtensions
6
+ refine IO do
7
+ def wait(timeout = nil, mode = :read)
8
+ case mode
9
+ when :read
10
+ wait_readable(timeout)
11
+ when :write
12
+ wait_writable(timeout)
13
+ when :read_write
14
+ r, w = IO.select([self], [self], nil, timeout)
15
+
16
+ return unless r || w
17
+
18
+ self
19
+ end
20
+ end
21
+ end
22
+ end
23
+
3
24
  class HTTPX::Selector
4
25
  READABLE = %i[rw r].freeze
5
26
  WRITABLE = %i[rw w].freeze
@@ -7,129 +28,107 @@ class HTTPX::Selector
7
28
  private_constant :READABLE
8
29
  private_constant :WRITABLE
9
30
 
10
- #
11
- # I/O monitor
12
- #
13
- class Monitor
14
- attr_accessor :io, :interests, :readiness
15
-
16
- def initialize(io, interests, reactor)
17
- @io = io
18
- @interests = interests
19
- @reactor = reactor
20
- @closed = false
21
- end
22
-
23
- def readable?
24
- READABLE.include?(@interests)
25
- end
26
-
27
- def writable?
28
- WRITABLE.include?(@interests)
29
- end
30
-
31
- # closes +@io+, deregisters from reactor (unless +deregister+ is false)
32
- def close(deregister = true)
33
- return if @closed
34
-
35
- @closed = true
36
- @reactor.deregister(@io) if deregister
37
- end
38
-
39
- def closed?
40
- @closed
41
- end
42
-
43
- # :nocov:
44
- def to_s
45
- "#<#{self.class}: #{@io}(closed:#{@closed}) #{@interests} #{object_id.to_s(16)}>"
46
- end
47
- # :nocov:
48
- end
31
+ using IOExtensions unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
49
32
 
50
33
  def initialize
51
- @selectables = {}
52
- @__r__, @__w__ = IO.pipe
53
- @closed = false
34
+ @selectables = []
54
35
  end
55
36
 
56
37
  # deregisters +io+ from selectables.
57
38
  def deregister(io)
58
- monitor = @selectables.delete(io)
59
- monitor.close(false) if monitor
39
+ @selectables.delete(io)
60
40
  end
61
41
 
62
- # register +io+ for +interests+ events.
63
- def register(io, interests)
64
- monitor = @selectables[io]
65
- if monitor
66
- monitor.interests = interests
67
- else
68
- monitor = Monitor.new(io, interests, self)
69
- @selectables[io] = monitor
70
- end
71
- monitor
72
- end
42
+ # register +io+.
43
+ def register(io)
44
+ return if @selectables.include?(io)
73
45
 
74
- # waits for read/write events for +interval+. Yields for monitors of
75
- # selected IO objects.
76
- #
77
- def select(interval)
78
- begin
79
- r = [@__r__]
80
- w = []
46
+ @selectables << io
47
+ end
81
48
 
82
- @selectables.each do |io, monitor|
83
- r << io if monitor.interests == :r || monitor.interests == :rw
84
- w << io if monitor.interests == :w || monitor.interests == :rw
85
- monitor.readiness = nil
49
+ private
50
+
51
+ READ_INTERESTS = %i[r rw].freeze
52
+ WRITE_INTERESTS = %i[w rw].freeze
53
+
54
+ def select_many(interval, &block)
55
+ selectables, r, w = nil
56
+
57
+ # first, we group IOs based on interest type. On call to #interests however,
58
+ # things might already happen, and new IOs might be registered, so we might
59
+ # have to start all over again. We do this until we group all selectables
60
+ loop do
61
+ begin
62
+ r = nil
63
+ w = nil
64
+
65
+ selectables = @selectables
66
+ @selectables = []
67
+
68
+ selectables.each do |io|
69
+ interests = io.interests
70
+
71
+ (r ||= []) << io if READ_INTERESTS.include?(interests)
72
+ (w ||= []) << io if WRITE_INTERESTS.include?(interests)
73
+ end
74
+
75
+ if @selectables.empty?
76
+ @selectables = selectables
77
+ break
78
+ else
79
+ @selectables = [*selectables, @selectables]
80
+ end
81
+ rescue StandardError
82
+ @selectables = selectables if selectables
83
+ raise
86
84
  end
85
+ end
86
+
87
+ # TODO: what to do if there are no selectables?
87
88
 
89
+ begin
88
90
  readers, writers = IO.select(r, w, nil, interval)
89
91
 
90
92
  raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") if readers.nil? && writers.nil?
91
93
  rescue IOError, SystemCallError
92
- @selectables.reject! { |io, _| io.closed? }
94
+ @selectables.reject!(&:closed?)
93
95
  retry
94
96
  end
95
97
 
96
98
  readers.each do |io|
97
- if io == @__r__
98
- # clean up wakeups
99
- @__r__.read(@__r__.stat.size)
100
- else
101
- monitor = io.closed? ? @selectables.delete(io) : @selectables[io]
102
- next unless monitor
103
-
104
- monitor.readiness = writers.delete(io) ? :rw : :r
105
- yield monitor
106
- end
107
- end if readers
99
+ yield io
108
100
 
109
- writers.each do |io|
110
- monitor = io.closed? ? @selectables.delete(io) : @selectables[io]
111
- next unless monitor
101
+ # so that we don't yield 2 times
102
+ writers.delete(io)
103
+ end if readers
112
104
 
113
- # don't double run this, the last iteration might have run this task already
114
- monitor.readiness = :w
115
- yield monitor
116
- end if writers
105
+ writers.each(&block) if writers
117
106
  end
118
107
 
119
- # Closes the selector.
120
- #
121
- def close
122
- return if @closed
108
+ def select_one(interval)
109
+ io = @selectables.first
110
+
111
+ interests = io.interests
112
+
113
+ result = case interests
114
+ when :r then io.to_io.wait_readable(interval)
115
+ when :w then io.to_io.wait_writable(interval)
116
+ when :rw then io.to_io.wait(interval, :read_write)
117
+ when nil then return
118
+ end
119
+
120
+ raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") unless result
123
121
 
124
- @__r__.close
125
- @__w__.close
126
- rescue IOError
127
- ensure
128
- @closed = true
122
+ yield io
123
+ rescue IOError, SystemCallError
124
+ @selectables.reject!(&:closed?)
129
125
  end
130
126
 
131
- # interrupts the select call.
132
- def wakeup
133
- @__w__.write_nonblock("\0", exception: false)
127
+ def select(interval, &block)
128
+ return select_one(interval, &block) if @selectables.size == 1
129
+
130
+ select_many(interval, &block)
134
131
  end
132
+
133
+ public :select
135
134
  end