fetch-api 0.3.5 → 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: 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