io-endpoint 0.4.0 → 0.5.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: 10203d535af830bf9ff71034262b5851ec323024a9de4db527f061cb1cd76102
4
- data.tar.gz: b150061a0414c379afb7fb5f2c142767afc5ed1822e5b106b78ece023d1cf459
3
+ metadata.gz: 204ed5172990d0df3a5d02e63b8e75d8caaf161808fea4e99b67f0d422241cc6
4
+ data.tar.gz: 7ba7e7052949565c4cb327076c493db15d6f3057493f93d4210ec5ac5bc7808b
5
5
  SHA512:
6
- metadata.gz: 82930d72359dee91627b19c771f79c9f2c6cc43dc93b9514a372a4568d1fb92c653ff2da1403c8519eddc4470dc14ef2356abe41cd202fba80bc35ba10e2d053
7
- data.tar.gz: 556e87a95eb514e643a3e6bb40468ac851cceea5da6fb6f72db37767dca18882bb9b63e61e05d2e69629a1e4eef1859c3a2b3e0797b36e0e4eb1202684f8d44c
6
+ metadata.gz: 12aa8f963bc0787de988e9efcf80fbff38a857e8130e663ed8c329402048ab9e1940dcc2637f17de2f77f43d9222914808b5ec89e91420ed0d07475dbabc9d0e
7
+ data.tar.gz: ce830974878205da1f7e91378af0bdb05f1a4017cc60715c2dca5607573992f38d97712e6b879d99ab31db95a5b12c40ab3df959da12cc56f4b2189dd37b314e
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+ require_relative 'composite_endpoint'
8
+ require_relative 'address_endpoint'
9
+
10
+ module IO::Endpoint
11
+ class BoundEndpoint < Generic
12
+ def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false)
13
+ sockets = endpoint.bind
14
+
15
+ sockets.each do |server|
16
+ # This is somewhat optional. We want to have a generic interface as much as possible so that users of this interface can just call it without knowing a lot of internal details. Therefore, we ignore errors here if it's because the underlying socket does not support the operation.
17
+ begin
18
+ server.listen(backlog)
19
+ rescue Errno::EOPNOTSUPP
20
+ # Ignore.
21
+ end
22
+
23
+ server.close_on_exec = close_on_exec
24
+ end
25
+
26
+ return self.new(endpoint, sockets, **endpoint.options)
27
+ end
28
+
29
+ def initialize(endpoint, sockets, **options)
30
+ super(**options)
31
+
32
+ @endpoint = endpoint
33
+ @sockets = sockets
34
+ end
35
+
36
+ attr :endpoint
37
+ attr :sockets
38
+
39
+ # A endpoint for the local end of the bound socket.
40
+ # @returns [CompositeEndpoint] A composite endpoint for the local end of the bound socket.
41
+ def local_address_endpoint(**options)
42
+ endpoints = @sockets.map do |socket|
43
+ AddressEndpoint.new(socket.to_io.local_address, **options)
44
+ end
45
+
46
+ return CompositeEndpoint.new(endpoints)
47
+ end
48
+
49
+ # A endpoint for the remote end of the bound socket.
50
+ # @returns [CompositeEndpoint] A composite endpoint for the remote end of the bound socket.
51
+ def remote_address_endpoint(**options)
52
+ endpoints = @sockets.map do |wrapper|
53
+ AddressEndpoint.new(socket.to_io.remote_address, **options)
54
+ end
55
+
56
+ return CompositeEndpoint.new(endpoints)
57
+ end
58
+
59
+ def close
60
+ @sockets.each(&:close)
61
+ @sockets.clear
62
+ end
63
+
64
+ def to_s
65
+ "\#<#{self.class} #{@sockets.size} bound sockets for #{@endpoint}>"
66
+ end
67
+
68
+ def bind(wrapper = Wrapper.default, &block)
69
+ @sockets.map do |server|
70
+ if block_given?
71
+ wrapper.async do
72
+ yield server
73
+ end
74
+ else
75
+ server.dup
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ class Generic
82
+ def bound(**options)
83
+ BoundEndpoint.bound(self, **options)
84
+ end
85
+ end
86
+ end
@@ -6,6 +6,7 @@
6
6
  require_relative 'generic'
7
7
 
8
8
  module IO::Endpoint
9
+ # A composite endpoint is a collection of endpoints that are used in order.
9
10
  class CompositeEndpoint < Generic
10
11
  def initialize(endpoints, **options)
11
12
  super(**options)
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+ require_relative 'composite_endpoint'
8
+ require_relative 'socket_endpoint'
9
+
10
+ require 'openssl'
11
+
12
+ module IO::Endpoint
13
+ class ConnectedEndpoint < Generic
14
+ def self.connected(endpoint, close_on_exec: false)
15
+ socket = endpoint.connect
16
+
17
+ socket.close_on_exec = close_on_exec
18
+
19
+ return self.new(endpoint, socket, **endpoint.options)
20
+ end
21
+
22
+ def initialize(endpoint, socket, **options)
23
+ super(**options)
24
+
25
+ @endpoint = endpoint
26
+ @socket = socket
27
+ end
28
+
29
+ attr :endpoint
30
+ attr :socket
31
+
32
+ # A endpoint for the local end of the bound socket.
33
+ # @returns [AddressEndpoint] A endpoint for the local end of the connected socket.
34
+ def local_address_endpoint(**options)
35
+ AddressEndpoint.new(socket.to_io.local_address, **options)
36
+ end
37
+
38
+ # A endpoint for the remote end of the bound socket.
39
+ # @returns [AddressEndpoint] A endpoint for the remote end of the connected socket.
40
+ def remote_address_endpoint(**options)
41
+ AddressEndpoint.new(socket.to_io.remote_address, **options)
42
+ end
43
+
44
+ def connect(wrapper = Wrapper.default, &block)
45
+ if block_given?
46
+ yield @socket
47
+ else
48
+ return @socket.dup
49
+ end
50
+ end
51
+
52
+ def close
53
+ if @socket
54
+ @socket.close
55
+ @socket = nil
56
+ end
57
+ end
58
+
59
+ def to_s
60
+ "\#<#{self.class} #{@socket} connected for #{@endpoint}>"
61
+ end
62
+ end
63
+
64
+ class Generic
65
+ def connected(**options)
66
+ ConnectedEndpoint.connected(self, **options)
67
+ end
68
+ end
69
+ end
@@ -59,22 +59,40 @@ module IO::Endpoint
59
59
  @options[:local_address]
60
60
  end
61
61
 
62
- # Endpoints sometimes have multiple paths.
63
- # @yield [Endpoint] Enumerate all discrete paths as endpoints.
62
+ # Bind a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
63
+ # @parameter wrapper [Wrapper] The wrapper to use for binding.
64
+ # @yields {|socket| ...} An optional block which will be passed the socket.
65
+ # @parameter socket [Socket] The socket which has been bound.
66
+ # @returns [Array(Socket)] the bound socket
67
+ def bind(wrapper = Wrapper.default, &block)
68
+ raise NotImplementedError
69
+ end
70
+
71
+ # Connects a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
72
+ # @parameter wrapper [Wrapper] The wrapper to use for connecting.
73
+ # @return [Socket] the connected socket
74
+ def connect(wrapper = Wrapper.default, &block)
75
+ raise NotImplementedError
76
+ end
77
+
78
+ # Bind and accept connections on the given address.
79
+ # @parameter wrapper [Wrapper] The wrapper to use for accepting connections.
80
+ # @yields [Socket] The accepted socket.
81
+ def accept(wrapper = Wrapper.default, &block)
82
+ bind(wrapper) do |server|
83
+ wrapper.accept(server, **@options, &block)
84
+ end
85
+ end
86
+
87
+ # Enumerate all discrete paths as endpoints.
88
+ # @yields {|endpoint| ...} A block which will be passed each endpoint.
89
+ # @parameter endpoint [Endpoint] The endpoint.
64
90
  def each
65
91
  return to_enum unless block_given?
66
92
 
67
93
  yield self
68
94
  end
69
95
 
70
- # Accept connections from the specified endpoint.
71
- # @param backlog [Integer] the number of connections to listen for.
72
- def accept(wrapper = Wrapper.default, *arguments, **options, &block)
73
- bind(wrapper, *arguments, **options) do |server|
74
- wrapper.accept(server, &block)
75
- end
76
- end
77
-
78
96
  # Create an Endpoint instance by URI scheme. The host and port of the URI will be passed to the Endpoint factory method, along with any options.
79
97
  #
80
98
  # You should not use untrusted input as it may execute arbitrary code.
@@ -1,136 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023, by Samuel Williams.
4
+ # Copyright, 2024, by Samuel Williams.
5
5
 
6
- require_relative 'generic'
7
- require_relative 'composite_endpoint'
8
- require_relative 'socket_endpoint'
9
-
10
- require 'openssl'
11
-
12
- module IO::Endpoint
13
- # Pre-connect and pre-bind sockets so that it can be used between processes.
14
- class SharedEndpoint < Generic
15
- # Create a new `SharedEndpoint` by binding to the given endpoint.
16
- def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false, **options)
17
- sockets = endpoint.bind(**options)
18
-
19
- sockets.each do |server|
20
- # This is somewhat optional. We want to have a generic interface as much as possible so that users of this interface can just call it without knowing a lot of internal details. Therefore, we ignore errors here if it's because the underlying socket does not support the operation.
21
- begin
22
- server.listen(backlog)
23
- rescue Errno::EOPNOTSUPP
24
- # Ignore.
25
- end
26
-
27
- server.close_on_exec = close_on_exec
28
- end
29
-
30
- return self.new(endpoint, sockets)
31
- end
32
-
33
- # Create a new `SharedEndpoint` by connecting to the given endpoint.
34
- def self.connected(endpoint, close_on_exec: false)
35
- socket = endpoint.connect
36
-
37
- socket.close_on_exec = close_on_exec
38
-
39
- return self.new(endpoint, [socket])
40
- end
41
-
42
- def initialize(endpoint, sockets, **options)
43
- super(**options)
44
-
45
- raise TypeError, "sockets must be an Array" unless sockets.is_a?(Array)
46
-
47
- @endpoint = endpoint
48
- @sockets = sockets
49
- end
50
-
51
- attr :endpoint
52
- attr :sockets
53
-
54
- def local_address_endpoint(**options)
55
- endpoints = @sockets.map do |wrapper|
56
- AddressEndpoint.new(wrapper.to_io.local_address, **options)
57
- end
58
-
59
- return CompositeEndpoint.new(endpoints)
60
- end
61
-
62
- def remote_address_endpoint(**options)
63
- endpoints = @sockets.map do |wrapper|
64
- AddressEndpoint.new(wrapper.to_io.remote_address, **options)
65
- end
66
-
67
- return CompositeEndpoint.new(endpoints)
68
- end
69
-
70
- # Close all the internal sockets.
71
- def close
72
- @sockets.each(&:close)
73
- @sockets.clear
74
- end
75
-
76
- def each(&block)
77
- return to_enum unless block_given?
78
-
79
- @sockets.each do |socket|
80
- yield SocketEndpoint.new(socket.dup)
81
- end
82
- end
83
-
84
- def bind(wrapper = Wrapper.default, &block)
85
- @sockets.each.map do |server|
86
- server = server.dup
87
-
88
- if block_given?
89
- wrapper.async do
90
- begin
91
- yield server
92
- ensure
93
- server.close
94
- end
95
- end
96
- else
97
- server
98
- end
99
- end
100
- end
101
-
102
- def connect(wrapper = Wrapper.default, &block)
103
- @sockets.each do |socket|
104
- socket = socket.dup
105
-
106
- return socket unless block_given?
107
-
108
- begin
109
- return yield(socket)
110
- ensure
111
- socket.close
112
- end
113
- end
114
- end
115
-
116
- def accept(wrapper = Wrapper.default, &block)
117
- bind(wrapper) do |server|
118
- wrapper.accept(server, &block)
119
- end
120
- end
121
-
122
- def to_s
123
- "\#<#{self.class} #{@sockets.size} descriptors for #{@endpoint}>"
124
- end
125
- end
126
-
127
- class Generic
128
- def bound(**options)
129
- SharedEndpoint.bound(self, **options)
130
- end
131
-
132
- def connected(**options)
133
- SharedEndpoint.connected(self, **options)
134
- end
135
- end
136
- end
6
+ require_relative 'bound_endpoint'
7
+ require_relative 'connected_endpoint'
@@ -8,34 +8,52 @@ require_relative 'generic'
8
8
 
9
9
  require 'openssl'
10
10
 
11
- module OpenSSL::SSL::SocketForwarder
12
- unless method_defined?(:close_on_exec=)
13
- def close_on_exec=(value)
14
- to_io.close_on_exec = value
15
- end
16
- end
17
-
18
- unless method_defined?(:close_on_exec)
19
- def local_address
20
- to_io.local_address
21
- end
22
- end
23
-
24
- unless method_defined?(:wait)
25
- def wait(*arguments)
26
- to_io.wait(*arguments)
27
- end
28
- end
29
-
30
- unless method_defined?(:wait_readable)
31
- def wait_readable(*arguments)
32
- to_io.wait_readable(*arguments)
33
- end
34
- end
35
-
36
- unless method_defined?(:wait_writable)
37
- def wait_writable(*arguments)
38
- to_io.wait_writable(*arguments)
11
+ module OpenSSL
12
+ module SSL
13
+ module SocketForwarder
14
+ unless method_defined?(:close_on_exec=)
15
+ def close_on_exec=(value)
16
+ to_io.close_on_exec = value
17
+ end
18
+ end
19
+
20
+ unless method_defined?(:close_on_exec)
21
+ def local_address
22
+ to_io.local_address
23
+ end
24
+ end
25
+
26
+ unless method_defined?(:wait)
27
+ def wait(*arguments)
28
+ to_io.wait(*arguments)
29
+ end
30
+ end
31
+
32
+ unless method_defined?(:wait_readable)
33
+ def wait_readable(*arguments)
34
+ to_io.wait_readable(*arguments)
35
+ end
36
+ end
37
+
38
+ unless method_defined?(:wait_writable)
39
+ def wait_writable(*arguments)
40
+ to_io.wait_writable(*arguments)
41
+ end
42
+ end
43
+
44
+ if IO.method_defined?(:timeout)
45
+ unless method_defined?(:timeout)
46
+ def timeout
47
+ to_io.timeout
48
+ end
49
+ end
50
+
51
+ unless method_defined?(:timeout=)
52
+ def timeout=(value)
53
+ to_io.timeout = value
54
+ end
55
+ end
56
+ end
39
57
  end
40
58
  end
41
59
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  class IO
7
7
  module Endpoint
8
- VERSION = "0.4.0"
8
+ VERSION = "0.5.0"
9
9
  end
10
10
  end
@@ -38,50 +38,30 @@ module IO::Endpoint
38
38
  raise NotImplementedError
39
39
  end
40
40
 
41
- # Build and wrap the underlying io.
42
- # @option reuse_port [Boolean] Allow this port to be bound in multiple processes.
43
- # @option reuse_address [Boolean] Allow this port to be bound in multiple processes.
44
- # @option linger [Boolean] Wait for data to be sent before closing the socket.
45
- # @option buffered [Boolean] Enable or disable Nagle's algorithm for TCP sockets.
46
- def build(*arguments, timeout: nil, reuse_address: true, reuse_port: nil, linger: nil, buffered: false)
47
- socket = ::Socket.new(*arguments)
48
-
49
- # Set the timeout:
50
- if timeout
51
- set_timeout(socket, timeout)
52
- end
53
-
54
- if reuse_address
55
- socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
56
- end
57
-
58
- if reuse_port
59
- socket.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1)
60
- end
61
-
62
- if linger
63
- socket.setsockopt(SOL_SOCKET, SO_LINGER, 1)
64
- end
65
-
66
- if buffered == false
67
- set_buffered(socket, buffered)
68
- end
69
-
70
- yield socket if block_given?
71
-
72
- return socket
73
- rescue
74
- socket&.close
75
- raise
76
- end
77
-
78
41
  # Establish a connection to a given `remote_address`.
79
42
  # @example
80
43
  # socket = Async::IO::Socket.connect(Async::IO::Address.tcp("8.8.8.8", 53))
81
- # @param remote_address [Address] The remote address to connect to.
82
- # @option local_address [Address] The local address to bind to before connecting.
83
- def connect(remote_address, local_address: nil, **options)
84
- socket = build(remote_address.afamily, remote_address.socktype, remote_address.protocol, **options) do |socket|
44
+ # @parameter remote_address [Address] The remote address to connect to.
45
+ # @parameter linger [Boolean] Wait for data to be sent before closing the socket.
46
+ # @parameter local_address [Address] The local address to bind to before connecting.
47
+ def connect(remote_address, local_address: nil, linger: nil, timeout: nil, buffered: false, **options)
48
+ socket = nil
49
+
50
+ begin
51
+ socket = ::Socket.new(remote_address.afamily, remote_address.socktype, remote_address.protocol)
52
+
53
+ if linger
54
+ socket.setsockopt(SOL_SOCKET, SO_LINGER, 1)
55
+ end
56
+
57
+ if buffered == false
58
+ set_buffered(socket, buffered)
59
+ end
60
+
61
+ if timeout
62
+ set_timeout(socket, timeout)
63
+ end
64
+
85
65
  if local_address
86
66
  if defined?(IP_BIND_ADDRESS_NO_PORT)
87
67
  # Inform the kernel (Linux 4.2+) to not reserve an ephemeral port when using bind(2) with a port number of 0. The port will later be automatically chosen at connect(2) time, in a way that allows sharing a source port as long as the 4-tuple is unique.
@@ -90,6 +70,9 @@ module IO::Endpoint
90
70
 
91
71
  socket.bind(local_address.to_sockaddr)
92
72
  end
73
+ rescue
74
+ socket&.close
75
+ raise
93
76
  end
94
77
 
95
78
  begin
@@ -108,14 +91,48 @@ module IO::Endpoint
108
91
  end
109
92
  end
110
93
 
94
+ # JRuby requires ServerSocket
95
+ if defined?(::ServerSocket)
96
+ ServerSocket = ::ServerSocket
97
+ else
98
+ ServerSocket = ::Socket
99
+ end
100
+
111
101
  # Bind to a local address.
112
102
  # @example
113
103
  # socket = Async::IO::Socket.bind(Async::IO::Address.tcp("0.0.0.0", 9090))
114
- # @param local_address [Address] The local address to bind to.
115
- # @option protocol [Integer] The socket protocol to use.
116
- def bind(local_address, protocol: 0, **options, &block)
117
- socket = build(local_address.afamily, local_address.socktype, protocol, **options) do |socket|
104
+ # @parameter local_address [Address] The local address to bind to.
105
+ # @parameter reuse_port [Boolean] Allow this port to be bound in multiple processes.
106
+ # @parameter reuse_address [Boolean] Allow this port to be bound in multiple processes.
107
+ # @parameter linger [Boolean] Wait for data to be sent before closing the socket.
108
+ # @parameter protocol [Integer] The socket protocol to use.
109
+ def bind(local_address, protocol: 0, reuse_address: true, reuse_port: nil, linger: nil, bound_timeout: nil, **options, &block)
110
+ socket = nil
111
+
112
+ begin
113
+ socket = ServerSocket.new(local_address.afamily, local_address.socktype, protocol)
114
+
115
+ if reuse_address
116
+ socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
117
+ end
118
+
119
+ if reuse_port
120
+ socket.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1)
121
+ end
122
+
123
+ if linger
124
+ socket.setsockopt(SOL_SOCKET, SO_LINGER, 1)
125
+ end
126
+
127
+ # Set the timeout:
128
+ if bound_timeout
129
+ set_timeout(socket, bound_timeout)
130
+ end
131
+
118
132
  socket.bind(local_address.to_sockaddr)
133
+ rescue
134
+ socket&.close
135
+ raise
119
136
  end
120
137
 
121
138
  return socket unless block_given?
@@ -130,11 +147,11 @@ module IO::Endpoint
130
147
  end
131
148
 
132
149
  # Bind to a local address and accept connections in a loop.
133
- def accept(server, timeout: server.timeout, &block)
150
+ def accept(server, timeout: nil, &block)
134
151
  while true
135
152
  socket, address = server.accept
136
153
 
137
- socket.timeout = timeout if timeout != false
154
+ set_timeout(socket, timeout) if timeout != false
138
155
 
139
156
  async do
140
157
  yield socket, address
@@ -145,20 +162,26 @@ module IO::Endpoint
145
162
 
146
163
  class ThreadWrapper < Wrapper
147
164
  def async(&block)
148
- Thread.new(&block)
165
+ ::Thread.new(&block)
149
166
  end
150
167
  end
151
168
 
152
169
  class FiberWrapper < Wrapper
153
170
  def async(&block)
154
- Fiber.schedule(&block)
171
+ ::Fiber.schedule(&block)
155
172
  end
156
173
  end
157
174
 
158
- def Wrapper.default
159
- if Fiber.scheduler
160
- FiberWrapper.new
161
- else
175
+ if Fiber.respond_to?(:scheduler)
176
+ def Wrapper.default
177
+ if Fiber.scheduler
178
+ FiberWrapper.new
179
+ else
180
+ ThreadWrapper.new
181
+ end
182
+ end
183
+ else
184
+ def Wrapper.default
162
185
  ThreadWrapper.new
163
186
  end
164
187
  end
data/lib/io/endpoint.rb CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  require_relative "endpoint/version"
7
7
  require_relative "endpoint/generic"
8
+ require_relative "endpoint/shared_endpoint"
8
9
 
9
10
  module IO::Endpoint
10
11
  def self.file_descriptor_limit
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-endpoint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -37,7 +37,7 @@ cert_chain:
37
37
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
38
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
39
  -----END CERTIFICATE-----
40
- date: 2024-01-01 00:00:00.000000000 Z
40
+ date: 2024-01-25 00:00:00.000000000 Z
41
41
  dependencies: []
42
42
  description:
43
43
  email:
@@ -48,7 +48,9 @@ files:
48
48
  - lib/io/connected.rb
49
49
  - lib/io/endpoint.rb
50
50
  - lib/io/endpoint/address_endpoint.rb
51
+ - lib/io/endpoint/bound_endpoint.rb
51
52
  - lib/io/endpoint/composite_endpoint.rb
53
+ - lib/io/endpoint/connected_endpoint.rb
52
54
  - lib/io/endpoint/generic.rb
53
55
  - lib/io/endpoint/host_endpoint.rb
54
56
  - lib/io/endpoint/shared_endpoint.rb
@@ -79,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
81
  - !ruby/object:Gem::Version
80
82
  version: '0'
81
83
  requirements: []
82
- rubygems_version: 3.4.10
84
+ rubygems_version: 3.5.3
83
85
  signing_key:
84
86
  specification_version: 4
85
87
  summary: Provides a separation of concerns interface for IO endpoints.
metadata.gz.sig CHANGED
Binary file