fetch-api 0.3.5 → 0.4.1

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: c2ece1d340c0de1c25cf5ff54b2faa9282e457a398b1e02e8b1254c869643e63
4
- data.tar.gz: af89501158b402c8b5a2d5ac6961a93546b5a6caa78c8f27492ff1b0fc4b5be4
3
+ metadata.gz: ff43ec8ec36d621acb463b7327146e89df97446714df7747626276bb51ff4c18
4
+ data.tar.gz: de99e42e871912c41df6a5afabb1200f662d5aeba7b6659a4483434c8ddca3c7
5
5
  SHA512:
6
- metadata.gz: f42136117dc9ff72634fdd12774dd6bc270d3c7f58363a0b78c74e216f12b241d35f60329f3957219d79a3d6b81ffb1b73917a7788647252b42a5271056df5fb
7
- data.tar.gz: 9902db054b692dc72bd645c7d7030ec7392369d41e70d3d27b64b19f871294749e065eae638604b23ab418133a16c0b13d444bb132aab96274327482c516ecff
6
+ metadata.gz: 7b56ad7b6ac2788343928c4686f48de71e1dc57491e9e13efa5cc479f4f7f57af9d4efad3e7a79cf75fd287d683bb21ec053b69de0c66cdd39a0aef571f86c7b
7
+ data.tar.gz: 8895b9835e6637a9d4cd845708e2a133b2f6239baab1729478f16ae9f25ea3df927d678c41ebfbcc26cb3482a43c5fb616779df89a21a8a1aa634f0e18ebe392
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'
@@ -40,10 +41,8 @@ module Fetch
40
41
  req.body = body
41
42
  end
42
43
 
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) }
44
+ # @type var uri: URI::HTTP
45
+ res = pool.with_connection(uri) { _1.request(req) }
47
46
 
48
47
  case res
49
48
  when Net::HTTPRedirection
@@ -64,6 +63,14 @@ module Fetch
64
63
 
65
64
  private
66
65
 
66
+ def pool
67
+ if pool = Thread.current.thread_variable_get(:fetch_connection_pool)
68
+ pool
69
+ else
70
+ Thread.current.thread_variable_set :fetch_connection_pool, ConnectionPool.new
71
+ end
72
+ end
73
+
67
74
  def to_response(url, res, redirected)
68
75
  Response.new(
69
76
  url: url.to_str,
@@ -0,0 +1,3 @@
1
+ module Fetch
2
+ Config = Struct.new(:connection_max_idle_time, :keep_alive_timeout)
3
+ end
@@ -0,0 +1,82 @@
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[uri.origin]
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[uri.origin] = 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[uri.origin]
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 |origin, 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 origin
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ 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.1'
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,8 @@ module Fetch
13
13
 
14
14
  private
15
15
 
16
- def to_response: (_StringLike, Net::HTTPResponse, bool) -> Response
16
+ def initialize: () -> void
17
+ def pool: () -> ConnectionPool
18
+ def to_response: (string, Net::HTTPResponse, bool) -> Response
17
19
  end
18
20
  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,20 @@
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
+ end
20
+ 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.1
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