fetch-api 0.3.4 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 872edd7141621bba9ac97f850fd7affb5daf229d3c2db581bf2f5422424fdecf
4
- data.tar.gz: 1bcce61d1a762827070efc219024d3520b5c2959ede385bcfc2b3ba5d6897dc3
3
+ metadata.gz: dbef9662b23db6e0efca2b67264d6450553991b551ec94f1b7099c0b4c2eda55
4
+ data.tar.gz: 0456d3ff62bcceeaa780ff9cb42c3ff231d667b977ef968e22bb5b2a5578e358
5
5
  SHA512:
6
- metadata.gz: 4c88efee5d27ab2b0d1765e68d59625fc2b8249f70ced9bcee8467999b7c0f3a59bc43f4289b0e6510b6d31782c16a2e95577dce57294c9281ac6b0c8556cff9
7
- data.tar.gz: deb9958e04948dc8e9c809d44517cda3af48497984297478046c0707f28d3daf9dca5fc665b86240bd92c4e77ca9f34b530111c193d9e0e878dfd28397888bc2
6
+ metadata.gz: 516ebe663dc6f346a8acc56f7a1c8e1a3bc11f3e133f6ca0c10a8544516a6dc8a8e34efd660585de627c595ba66ed17b6df3f3415ec90a439be84e21e50d7343
7
+ data.tar.gz: 3b0630efe83154f9a71df06aec28b551720a6041e24ce1797626a105eedacc54a96495362e061b391018847cbfd2021e4fd750854b75a546841738f2a58d441c
data/README.md CHANGED
@@ -102,6 +102,19 @@ res = fetch('http://example.com', **{
102
102
  })
103
103
  ```
104
104
 
105
+ ## Connection pooling
106
+
107
+ Fetch API automatically pools and reuses connections to the same origin. Connections are closed after a certain amount of time has passed since the last use, and a new connection is used the next time. Different connections are returned for different threads (in other words, Fetch API is thread-safe).
108
+
109
+ These values (in seconds) can be configured as follows:
110
+
111
+ ``` ruby
112
+ Fetch.configure do |config|
113
+ config.connection_max_idle_time = 10 # default
114
+ config.keep_alive_timeout = 2 # default
115
+ end
116
+ ```
117
+
105
118
  ## Development
106
119
 
107
120
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/lib/fetch/client.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative 'connection_pool'
1
2
  require_relative 'errors'
2
3
  require_relative 'form_data'
3
4
  require_relative 'headers'
@@ -6,6 +7,7 @@ require_relative 'url_search_params'
6
7
 
7
8
  require 'mini_mime'
8
9
  require 'net/http'
10
+ require 'net/https'
9
11
  require 'singleton'
10
12
  require 'uri'
11
13
 
@@ -13,12 +15,16 @@ module Fetch
13
15
  class Client
14
16
  include Singleton
15
17
 
18
+ def initialize
19
+ @pool = ConnectionPool.new
20
+ end
21
+
16
22
  def fetch(resource, method: :get, headers: [], body: nil, redirect: :follow, _redirected: false)
17
23
  uri = URI.parse(resource)
18
24
  req = Net::HTTP.const_get(method.capitalize).new(uri)
19
25
 
20
26
  headers.each do |k, v|
21
- req[k] = v
27
+ req[k.to_s] = v.to_s
22
28
  end
23
29
 
24
30
  case body
@@ -39,16 +45,13 @@ module Fetch
39
45
  req.body = body
40
46
  end
41
47
 
42
- http = Net::HTTP.new(uri.hostname, uri.port)
43
- http.use_ssl = uri.scheme == 'https'
44
-
45
- res = http.start { _1.request(req) }
48
+ res = @pool.with_connection(uri) { _1.request(req) }
46
49
 
47
50
  case res
48
51
  when Net::HTTPRedirection
49
52
  case redirect.to_s
50
53
  when 'follow'
51
- fetch(res['Location'], method:, headers:, body:, redirect:, _redirected: true)
54
+ fetch(res['Location'], method:, headers:, body:, redirect:, _redirected: true) # steep:ignore ArgumentTypeMismatch
52
55
  when 'error'
53
56
  raise RedirectError, "redirected to #{res['Location']}"
54
57
  when 'manual'
@@ -65,9 +68,9 @@ module Fetch
65
68
 
66
69
  def to_response(url, res, redirected)
67
70
  Response.new(
68
- url: ,
71
+ url: url.to_str,
69
72
  status: res.code.to_i,
70
- headers: Headers.new(res),
73
+ headers: Headers.new(res.each),
71
74
  body: res.body,
72
75
  redirected:
73
76
  )
@@ -0,0 +1,3 @@
1
+ module Fetch
2
+ Config = Struct.new(:connection_max_idle_time, :keep_alive_timeout)
3
+ end
@@ -0,0 +1,86 @@
1
+ require_relative '../fetch'
2
+
3
+ require 'thread'
4
+
5
+ module Fetch
6
+ class ConnectionPool
7
+ Entry = Struct.new(:connection, :in_use, :last_used, keyword_init: true)
8
+
9
+ def initialize
10
+ @connections = {}
11
+ @mutex = Mutex.new
12
+
13
+ @sweeper = Thread.new {
14
+ loop do
15
+ sleep 1
16
+
17
+ sweep
18
+
19
+ Thread.stop if @connections.empty?
20
+ end
21
+ }
22
+ end
23
+
24
+ def with_connection(uri, &block)
25
+ conn = acquire(uri)
26
+
27
+ begin
28
+ block.call(conn)
29
+ ensure
30
+ release uri
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def acquire(uri)
37
+ @mutex.synchronize {
38
+ entry = @connections[key(uri)]
39
+
40
+ if entry
41
+ entry.in_use = true
42
+
43
+ entry.connection
44
+ else
45
+ Net::HTTP.new(uri.host, uri.port).tap {|http| # steep:ignore ArgumentTypeMismatch
46
+ http.use_ssl = uri.scheme == 'https'
47
+ http.keep_alive_timeout = Fetch.config.keep_alive_timeout
48
+
49
+ @connections[key(uri)] = Entry.new(connection: http, in_use: true)
50
+
51
+ http.start
52
+ }
53
+ end
54
+ }.tap {
55
+ @sweeper.wakeup
56
+ }
57
+ end
58
+
59
+ def release(uri)
60
+ @mutex.synchronize do
61
+ if entry = @connections[key(uri)]
62
+ entry.in_use = false
63
+ entry.last_used = Time.now
64
+ end
65
+ end
66
+ end
67
+
68
+ def sweep
69
+ @mutex.synchronize do
70
+ @connections.each do |key, entry|
71
+ next if entry.in_use
72
+
73
+ if entry.last_used + Fetch.config.connection_max_idle_time < Time.now
74
+ entry.connection.finish
75
+
76
+ @connections.delete key
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def key(uri)
83
+ "#{Thread.current.object_id}/#{uri.origin}".freeze
84
+ end
85
+ end
86
+ end
@@ -1,7 +1,13 @@
1
+ require 'forwardable'
2
+
1
3
  module Fetch
2
4
  class FormData
3
5
  include Enumerable
4
6
 
7
+ extend Forwardable
8
+
9
+ def_delegators :entries, :each
10
+
5
11
  def self.build(enumerable)
6
12
  data = FormData.new
7
13
 
@@ -19,7 +25,7 @@ module Fetch
19
25
  attr_reader :entries
20
26
 
21
27
  def append(key, value)
22
- @entries.push [key.to_s, value]
28
+ @entries.push [key.to_s, File === value ? value : value.to_s]
23
29
  end
24
30
 
25
31
  def delete(key)
@@ -50,9 +56,5 @@ module Fetch
50
56
  def values
51
57
  @entries.map(&:last)
52
58
  end
53
-
54
- def each(&block)
55
- @entries.each(&block)
56
- end
57
59
  end
58
60
  end
data/lib/fetch/headers.rb CHANGED
@@ -1,7 +1,13 @@
1
+ require 'forwardable'
2
+
1
3
  module Fetch
2
4
  class Headers
3
5
  include Enumerable
4
6
 
7
+ extend Forwardable
8
+
9
+ def_delegators :entries, :each
10
+
5
11
  def initialize(init = [])
6
12
  @data = {}
7
13
 
@@ -11,7 +17,7 @@ module Fetch
11
17
  end
12
18
 
13
19
  def append(key, value)
14
- (@data[key.to_s.downcase] ||= []) << value
20
+ (@data[key.to_s.downcase] ||= []) << value.to_s
15
21
  end
16
22
 
17
23
  def delete(key)
@@ -41,9 +47,5 @@ module Fetch
41
47
  def values
42
48
  @data.values.map { _1.join(', ') }
43
49
  end
44
-
45
- def each(&block)
46
- entries.each(&block)
47
- end
48
50
  end
49
51
  end
@@ -2,7 +2,17 @@ require 'json'
2
2
  require 'rack/utils'
3
3
 
4
4
  module Fetch
5
- Response = Data.define(:url, :status, :headers, :body, :redirected) {
5
+ class Response
6
+ def initialize(url:, status:, headers:, body:, redirected:)
7
+ @url = url
8
+ @status = status
9
+ @headers = headers
10
+ @body = body
11
+ @redirected = redirected
12
+ end
13
+
14
+ attr_reader :url, :status, :headers, :body, :redirected
15
+
6
16
  def ok
7
17
  status.between?(200, 299)
8
18
  end
@@ -12,7 +22,9 @@ module Fetch
12
22
  end
13
23
 
14
24
  def json(...)
25
+ return nil unless body
26
+
15
27
  JSON.parse(body, ...)
16
28
  end
17
- }
29
+ end
18
30
  end
@@ -1,9 +1,14 @@
1
+ require 'forwardable'
1
2
  require 'uri'
2
3
 
3
4
  module Fetch
4
5
  class URLSearchParams
5
6
  include Enumerable
6
7
 
8
+ extend Forwardable
9
+
10
+ def_delegators :entries, :each
11
+
7
12
  def initialize(options = [])
8
13
  @entries = []
9
14
 
@@ -22,7 +27,7 @@ module Fetch
22
27
  attr_reader :entries
23
28
 
24
29
  def append(key, value)
25
- @entries.push [key.to_s, value]
30
+ @entries.push [key.to_s, value.to_s]
26
31
  end
27
32
 
28
33
  def delete(key)
@@ -61,9 +66,5 @@ module Fetch
61
66
  def values
62
67
  @entries.map(&:last)
63
68
  end
64
-
65
- def each(&block)
66
- @entries.each(&block)
67
- end
68
69
  end
69
70
  end
data/lib/fetch/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Fetch
2
- VERSION = '0.3.4'
2
+ VERSION = '0.4.0'
3
3
  end
data/lib/fetch.rb ADDED
@@ -0,0 +1,16 @@
1
+ require_relative 'fetch/config'
2
+
3
+ module Fetch
4
+ singleton_class.attr_accessor :config
5
+
6
+ self.config = Config.new
7
+
8
+ def self.configure(&block)
9
+ block.call(config)
10
+ end
11
+
12
+ configure do |config|
13
+ config.connection_max_idle_time = 10
14
+ config.keep_alive_timeout = 2
15
+ end
16
+ end
data/sig/fetch/api.rbs ADDED
@@ -0,0 +1,11 @@
1
+ module Fetch
2
+ class API
3
+ def self?.fetch: (
4
+ string,
5
+ ?method: String | Symbol,
6
+ ?headers: _Each[[_ToS, _ToS]],
7
+ ?body: (String | FormData | URLSearchParams)?,
8
+ ?redirect: 'follow' | 'error' | 'manual' | :follow | :error | :manual,
9
+ ) -> Response
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ module Fetch
2
+ class Client
3
+ include Singleton
4
+
5
+ def fetch: (
6
+ string,
7
+ ?method: String | Symbol,
8
+ ?headers: _Each[[_ToS, _ToS]],
9
+ ?body: (String | FormData | URLSearchParams)?,
10
+ ?redirect: 'follow' | 'error' | 'manual' | :follow | :error | :manual,
11
+ ?_redirected: bool
12
+ ) -> Response
13
+
14
+ private
15
+
16
+ def initialize: () -> void
17
+ def to_response: (string, Net::HTTPResponse, bool) -> Response
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ module Fetch
2
+ class Config
3
+ attr_accessor connection_max_idle_time: Integer
4
+ attr_accessor keep_alive_timeout: Integer
5
+ end
6
+ end
@@ -0,0 +1,21 @@
1
+ module Fetch
2
+ class ConnectionPool
3
+ class Entry
4
+ attr_accessor connection: Net::HTTP
5
+ attr_accessor in_use: bool
6
+ attr_accessor last_used: Time
7
+
8
+ def initialize: (connection: Net::HTTP, in_use: bool, ?last_used: Time) -> void
9
+ end
10
+
11
+ def initialize: () -> void
12
+ def with_connection: [T] (URI::HTTP) { (Net::HTTP) -> T } -> T
13
+
14
+ private
15
+
16
+ def acquire: (URI::HTTP) -> Net::HTTP
17
+ def release: (URI::HTTP) -> void
18
+ def sweep: () -> void
19
+ def key: (URI::HTTP) -> String
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module Fetch
2
+ class Error < StandardError
3
+ end
4
+
5
+ class RedirectError < Error
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module Fetch
2
+ class FormData
3
+ include Enumerable[[String, String | File]]
4
+
5
+ extend Forwardable
6
+
7
+ attr_reader entries: Array[[String, String | File]]
8
+
9
+ def self.build: (_Each[[_ToS, _ToS | File]]) -> FormData
10
+
11
+ def initialize: () -> void
12
+ def append: (_ToS, _ToS | File) -> void
13
+ def delete: (_ToS) -> void
14
+ def get: (_ToS) -> (String | File)?
15
+ def get_all: (_ToS) -> Array[String | File]
16
+ def has: (_ToS) -> bool
17
+ def keys: -> Array[String]
18
+ def set: (_ToS, _ToS | File) -> void
19
+ def values: () -> Array[String | File]
20
+
21
+ def each: () { ([String, String | File]) -> void } -> Array[[String, String | File]]
22
+ | () -> Enumerator[[String, String | File], Array[[String, String | File]]]
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ module Fetch
2
+ class Headers
3
+ include Enumerable[[String, String]]
4
+
5
+ extend Forwardable
6
+
7
+ def initialize: (_Each[[_ToS, _ToS]]) -> void
8
+ def append: (_ToS, _ToS) -> void
9
+ def delete: (_ToS) -> void
10
+ def entries: () -> Array[[String, String]]
11
+ def get: (_ToS) -> String?
12
+ def has: (_ToS) -> bool
13
+ def keys: -> Array[String]
14
+ def set: (_ToS, _ToS) -> void
15
+ def values: () -> Array[String]
16
+
17
+ def each: () { ([String, String]) -> void } -> Array[[String, String]]
18
+ | () -> Enumerator[[String, String], Array[[String, String]]]
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module Fetch
2
+ class Response
3
+ attr_reader url: String
4
+ attr_reader status: Integer
5
+ attr_reader headers: Headers
6
+ attr_reader body: String?
7
+ attr_reader redirected: bool
8
+
9
+ def initialize: (url: String, status: Integer, headers: Headers, body: String?, redirected: bool) -> void
10
+ def ok: () -> bool
11
+ def status_text: () -> String
12
+ def json: (json_options) -> untyped
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ module Fetch
2
+ class URLSearchParams
3
+ include Enumerable[[String, String]]
4
+
5
+ extend Forwardable
6
+
7
+ attr_reader entries: Array[[String, String]]
8
+
9
+ def self.build: (_Each[[_ToS, _ToS]]) -> FormData
10
+
11
+ def initialize: (String | _Each[[_ToS, _ToS]]) -> void
12
+ def append: (_ToS, _ToS) -> void
13
+ def delete: (_ToS) -> void
14
+ def get: (_ToS) -> String?
15
+ def get_all: (_ToS) -> Array[String]
16
+ def has: (_ToS) -> bool
17
+ def keys: -> Array[String]
18
+ def set: (_ToS, _ToS) -> void
19
+ def values: () -> Array[String]
20
+
21
+ def each: () { ([String, String]) -> void } -> Array[[String, String]]
22
+ | () -> Enumerator[[String, String], Array[[String, String]]]
23
+ end
24
+ end
data/sig/fetch.rbs CHANGED
@@ -1,4 +1,7 @@
1
1
  module Fetch
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ attr_accessor self.config: Config
5
+
6
+ def self.configure: () { (Config) -> void } -> void
4
7
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fetch-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keita Urashima
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-07 00:00:00.000000000 Z
11
+ date: 2024-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: forwardable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: mini_mime
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -90,8 +118,11 @@ files:
90
118
  - LICENSE.txt
91
119
  - README.md
92
120
  - lib/fetch-api.rb
121
+ - lib/fetch.rb
93
122
  - lib/fetch/api.rb
94
123
  - lib/fetch/client.rb
124
+ - lib/fetch/config.rb
125
+ - lib/fetch/connection_pool.rb
95
126
  - lib/fetch/errors.rb
96
127
  - lib/fetch/form_data.rb
97
128
  - lib/fetch/headers.rb
@@ -99,6 +130,15 @@ files:
99
130
  - lib/fetch/url_search_params.rb
100
131
  - lib/fetch/version.rb
101
132
  - sig/fetch.rbs
133
+ - sig/fetch/api.rbs
134
+ - sig/fetch/client.rbs
135
+ - sig/fetch/config.rbs
136
+ - sig/fetch/connection_pool.rbs
137
+ - sig/fetch/errors.rbs
138
+ - sig/fetch/form_data.rbs
139
+ - sig/fetch/headers.rbs
140
+ - sig/fetch/response.rbs
141
+ - sig/fetch/url_search_params.rbs
102
142
  homepage: https://github.com/ursm/fetch-api
103
143
  licenses:
104
144
  - MIT
@@ -113,7 +153,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
113
153
  requirements:
114
154
  - - ">="
115
155
  - !ruby/object:Gem::Version
116
- version: 3.2.0
156
+ version: 3.1.0
117
157
  required_rubygems_version: !ruby/object:Gem::Requirement
118
158
  requirements:
119
159
  - - ">="