httpx 0.7.0 → 0.10.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 (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