fetch-api 0.3.5 → 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: c2ece1d340c0de1c25cf5ff54b2faa9282e457a398b1e02e8b1254c869643e63
4
- data.tar.gz: af89501158b402c8b5a2d5ac6961a93546b5a6caa78c8f27492ff1b0fc4b5be4
3
+ metadata.gz: dbef9662b23db6e0efca2b67264d6450553991b551ec94f1b7099c0b4c2eda55
4
+ data.tar.gz: 0456d3ff62bcceeaa780ff9cb42c3ff231d667b977ef968e22bb5b2a5578e358
5
5
  SHA512:
6
- metadata.gz: f42136117dc9ff72634fdd12774dd6bc270d3c7f58363a0b78c74e216f12b241d35f60329f3957219d79a3d6b81ffb1b73917a7788647252b42a5271056df5fb
7
- data.tar.gz: 9902db054b692dc72bd645c7d7030ec7392369d41e70d3d27b64b19f871294749e065eae638604b23ab418133a16c0b13d444bb132aab96274327482c516ecff
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'
@@ -14,6 +15,10 @@ module Fetch
14
15
  class Client
15
16
  include Singleton
16
17
 
18
+ def initialize
19
+ @pool = ConnectionPool.new
20
+ end
21
+
17
22
  def fetch(resource, method: :get, headers: [], body: nil, redirect: :follow, _redirected: false)
18
23
  uri = URI.parse(resource)
19
24
  req = Net::HTTP.const_get(method.capitalize).new(uri)
@@ -40,10 +45,7 @@ module Fetch
40
45
  req.body = body
41
46
  end
42
47
 
43
- http = Net::HTTP.new(uri.hostname, uri.port) # steep:ignore ArgumentTypeMismatch
44
- http.use_ssl = uri.scheme == 'https'
45
-
46
- res = http.start { _1.request(req) }
48
+ res = @pool.with_connection(uri) { _1.request(req) }
47
49
 
48
50
  case res
49
51
  when Net::HTTPRedirection
@@ -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.is_a?(File) ? value : value.to_s]
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(&)
55
- @entries.each(&)
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
 
@@ -41,9 +47,5 @@ module Fetch
41
47
  def values
42
48
  @data.values.map { _1.join(', ') }
43
49
  end
44
-
45
- def each(&)
46
- entries.each(&)
47
- end
48
50
  end
49
51
  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
 
@@ -61,9 +66,5 @@ module Fetch
61
66
  def values
62
67
  @entries.map(&:last)
63
68
  end
64
-
65
- def each(&)
66
- @entries.each(&)
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.5'
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 CHANGED
@@ -1,9 +1,9 @@
1
1
  module Fetch
2
2
  class API
3
3
  def self?.fetch: (
4
- _StringLike,
4
+ string,
5
5
  ?method: String | Symbol,
6
- ?headers: Enumerable[[Object, Object]],
6
+ ?headers: _Each[[_ToS, _ToS]],
7
7
  ?body: (String | FormData | URLSearchParams)?,
8
8
  ?redirect: 'follow' | 'error' | 'manual' | :follow | :error | :manual,
9
9
  ) -> Response
data/sig/fetch/client.rbs CHANGED
@@ -3,9 +3,9 @@ module Fetch
3
3
  include Singleton
4
4
 
5
5
  def fetch: (
6
- _StringLike,
6
+ string,
7
7
  ?method: String | Symbol,
8
- ?headers: Enumerable[[Object, Object]],
8
+ ?headers: _Each[[_ToS, _ToS]],
9
9
  ?body: (String | FormData | URLSearchParams)?,
10
10
  ?redirect: 'follow' | 'error' | 'manual' | :follow | :error | :manual,
11
11
  ?_redirected: bool
@@ -13,6 +13,7 @@ module Fetch
13
13
 
14
14
  private
15
15
 
16
- def to_response: (_StringLike, Net::HTTPResponse, bool) -> Response
16
+ def initialize: () -> void
17
+ def to_response: (string, Net::HTTPResponse, bool) -> Response
17
18
  end
18
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
@@ -1,22 +1,24 @@
1
1
  module Fetch
2
2
  class FormData
3
- type value = String | File
3
+ include Enumerable[[String, String | File]]
4
4
 
5
- include Enumerable[[String, value]]
5
+ extend Forwardable
6
6
 
7
- attr_reader entries: Array[[String, value]]
7
+ attr_reader entries: Array[[String, String | File]]
8
8
 
9
- def self.build: (Enumerable[[Object, Object]]) -> FormData
9
+ def self.build: (_Each[[_ToS, _ToS | File]]) -> FormData
10
10
 
11
11
  def initialize: () -> void
12
- def append: (Object, Object) -> void
13
- def delete: (Object) -> void
14
- def get: (Object) -> value?
15
- def get_all: (Object) -> Array[value]
16
- def has: (Object) -> bool
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
17
  def keys: -> Array[String]
18
- def set: (Object, Object) -> void
19
- def values: () -> Array[value]
20
- def each: () { ([String, value]) -> void } -> Array[[String, value]]
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]]]
21
23
  end
22
24
  end
@@ -2,15 +2,19 @@ module Fetch
2
2
  class Headers
3
3
  include Enumerable[[String, String]]
4
4
 
5
- def initialize: (Enumerable[[Object, Object]]) -> void
6
- def append: (Object, Object) -> void
7
- def delete: (Object) -> void
5
+ extend Forwardable
6
+
7
+ def initialize: (_Each[[_ToS, _ToS]]) -> void
8
+ def append: (_ToS, _ToS) -> void
9
+ def delete: (_ToS) -> void
8
10
  def entries: () -> Array[[String, String]]
9
- def get: (Object) -> String?
10
- def has: (Object) -> bool
11
+ def get: (_ToS) -> String?
12
+ def has: (_ToS) -> bool
11
13
  def keys: -> Array[String]
12
- def set: (Object, Object) -> void
14
+ def set: (_ToS, _ToS) -> void
13
15
  def values: () -> Array[String]
16
+
14
17
  def each: () { ([String, String]) -> void } -> Array[[String, String]]
18
+ | () -> Enumerator[[String, String], Array[[String, String]]]
15
19
  end
16
20
  end
@@ -1,7 +1,5 @@
1
1
  module Fetch
2
2
  class Response
3
- include Data::_DataClass
4
-
5
3
  attr_reader url: String
6
4
  attr_reader status: Integer
7
5
  attr_reader headers: Headers
@@ -11,6 +9,6 @@ module Fetch
11
9
  def initialize: (url: String, status: Integer, headers: Headers, body: String?, redirected: bool) -> void
12
10
  def ok: () -> bool
13
11
  def status_text: () -> String
14
- def json: (json_options) -> Object?
12
+ def json: (json_options) -> untyped
15
13
  end
16
14
  end
@@ -2,19 +2,23 @@ module Fetch
2
2
  class URLSearchParams
3
3
  include Enumerable[[String, String]]
4
4
 
5
+ extend Forwardable
6
+
5
7
  attr_reader entries: Array[[String, String]]
6
8
 
7
- def self.build: (Enumerable[[Object, Object]]) -> FormData
9
+ def self.build: (_Each[[_ToS, _ToS]]) -> FormData
8
10
 
9
- def initialize: (String | Enumerable[[Object, Object]]) -> void
10
- def append: (Object, Object) -> void
11
- def delete: (Object) -> void
12
- def get: (Object) -> String?
13
- def get_all: (Object) -> Array[String]
14
- def has: (Object) -> bool
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
15
17
  def keys: -> Array[String]
16
- def set: (Object, Object) -> void
18
+ def set: (_ToS, _ToS) -> void
17
19
  def values: () -> Array[String]
20
+
18
21
  def each: () { ([String, String]) -> void } -> Array[[String, String]]
22
+ | () -> Enumerator[[String, String], Array[[String, String]]]
19
23
  end
20
24
  end
data/sig/fetch.rbs CHANGED
@@ -1,7 +1,7 @@
1
1
  module Fetch
2
- interface _StringLike
3
- def to_str: () -> String
4
- end
5
-
6
2
  VERSION: String
3
+
4
+ attr_accessor self.config: Config
5
+
6
+ def self.configure: () { (Config) -> void } -> void
7
7
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fetch-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
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'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: json
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -104,8 +118,11 @@ files:
104
118
  - LICENSE.txt
105
119
  - README.md
106
120
  - lib/fetch-api.rb
121
+ - lib/fetch.rb
107
122
  - lib/fetch/api.rb
108
123
  - lib/fetch/client.rb
124
+ - lib/fetch/config.rb
125
+ - lib/fetch/connection_pool.rb
109
126
  - lib/fetch/errors.rb
110
127
  - lib/fetch/form_data.rb
111
128
  - lib/fetch/headers.rb
@@ -115,6 +132,8 @@ files:
115
132
  - sig/fetch.rbs
116
133
  - sig/fetch/api.rbs
117
134
  - sig/fetch/client.rbs
135
+ - sig/fetch/config.rbs
136
+ - sig/fetch/connection_pool.rbs
118
137
  - sig/fetch/errors.rbs
119
138
  - sig/fetch/form_data.rbs
120
139
  - sig/fetch/headers.rbs