fetch-api 0.3.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="