io-stream 0.6.1 → 0.7.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: 4b66a5f21e372373267bc26a249194d093a07c9af7a53066dcd151662c0abda1
4
- data.tar.gz: '0944f1aafb0c79a93462d304e7e4ed722543609a2424f90ee2d045b70bf31751'
3
+ metadata.gz: c48d464c539d56862e04437f88dee2997e4393951efca83cf622a23bac9e4b75
4
+ data.tar.gz: 610277102a9233e5d470daabd4f12917d429dc6a141cf52897ff95af3a06de95
5
5
  SHA512:
6
- metadata.gz: 75ab72272639271db26f079d7d8098c5a191db8f0321ffc1b82aaffd424511f697da8d3751c4f3bd86c1f2315b52ba02ba5a7ab21f7c6b19c3c39fd649dfe3ae
7
- data.tar.gz: e0a17ddca49522facbed0e91b46da1d185aa91c68364258c093c207780f1e7f320debac1f7583762757dfa45d0b79f7206f248911723c93fb8e23a962337ce16
6
+ metadata.gz: 5761c359f16364169757dca90c4f41bfb87269ab3219b6a565b0081aafb3146f2ff6b11e77a95bebd478b31cbc702752e486c484666536f286e4ccdb4a2c6520
7
+ data.tar.gz: 6e44c1ce99d3741c6c9e8f65976f5664d287586e5a704749466a70992dcfcf5bac55ab84e1429cc5a22417af889ee7938af55ec63081309b3522629a9e05721b
checksums.yaml.gz.sig CHANGED
Binary file
@@ -6,7 +6,13 @@
6
6
  require_relative "generic"
7
7
 
8
8
  module IO::Stream
9
+ # A buffered stream implementation that wraps an underlying IO object to provide efficient buffered reading and writing.
9
10
  class Buffered < Generic
11
+ # Open a file and wrap it in a buffered stream.
12
+ # @parameter path [String] The file path to open.
13
+ # @parameter mode [String] The file mode (e.g., "r+", "w", "a").
14
+ # @parameter options [Hash] Additional options passed to the stream constructor.
15
+ # @returns [IO::Stream::Buffered] A buffered stream wrapping the opened file.
10
16
  def self.open(path, mode = "r+", **options)
11
17
  stream = self.new(::File.open(path, mode), **options)
12
18
 
@@ -19,6 +25,10 @@ module IO::Stream
19
25
  end
20
26
  end
21
27
 
28
+ # Wrap an existing IO object in a buffered stream.
29
+ # @parameter io [IO] The IO object to wrap.
30
+ # @parameter options [Hash] Additional options passed to the stream constructor.
31
+ # @returns [IO::Stream::Buffered] A buffered stream wrapping the IO object.
22
32
  def self.wrap(io, **options)
23
33
  if io.respond_to?(:buffered=)
24
34
  io.buffered = false
@@ -37,6 +47,8 @@ module IO::Stream
37
47
  end
38
48
  end
39
49
 
50
+ # Initialize a new buffered stream.
51
+ # @parameter io [IO] The underlying IO object to wrap.
40
52
  def initialize(io, ...)
41
53
  super(...)
42
54
 
@@ -47,27 +59,36 @@ module IO::Stream
47
59
  @timeout = nil
48
60
  end
49
61
  end
50
-
62
+
63
+ # @attribute [IO] The wrapped IO object.
51
64
  attr :io
52
-
65
+
66
+ # Get the underlying IO object.
67
+ # @returns [IO] The underlying IO object.
53
68
  def to_io
54
69
  @io.to_io
55
70
  end
56
-
71
+
72
+ # Check if the stream is closed.
73
+ # @returns [Boolean] True if the stream is closed.
57
74
  def closed?
58
75
  @io.closed?
59
76
  end
60
-
77
+
78
+ # Close the read end of the stream.
61
79
  def close_read
62
80
  @io.close_read
63
81
  end
64
-
82
+
83
+ # Close the write end of the stream.
65
84
  def close_write
66
85
  super
67
86
  ensure
68
87
  @io.close_write
69
88
  end
70
-
89
+
90
+ # Check if the stream is readable.
91
+ # @returns [Boolean] True if the stream is readable.
71
92
  def readable?
72
93
  super && @io.readable?
73
94
  end
@@ -4,247 +4,32 @@
4
4
  # Copyright, 2023-2024, by Samuel Williams.
5
5
 
6
6
  require_relative "string_buffer"
7
+ require_relative "readable"
8
+ require_relative "writable"
7
9
 
8
10
  require_relative "shim/buffered"
9
11
  require_relative "shim/readable"
10
- require_relative "shim/timeout"
11
12
 
12
13
  require_relative "openssl"
13
14
 
14
15
  module IO::Stream
15
- # The default block size for IO buffers. Defaults to 64KB (typical pipe buffer size).
16
- BLOCK_SIZE = ENV.fetch("IO_STREAM_BLOCK_SIZE", 1024*64).to_i
17
-
18
- # The maximum read size when appending to IO buffers. Defaults to 8MB.
19
- MAXIMUM_READ_SIZE = ENV.fetch("IO_STREAM_MAXIMUM_READ_SIZE", BLOCK_SIZE * 128).to_i
20
-
21
- class LimitError < StandardError
22
- end
23
-
16
+ # Base class for stream implementations providing common functionality.
24
17
  class Generic
25
- def initialize(block_size: BLOCK_SIZE, maximum_read_size: MAXIMUM_READ_SIZE)
26
- @eof = false
27
-
28
- @writing = ::Thread::Mutex.new
29
-
30
- @block_size = block_size
31
- @maximum_read_size = maximum_read_size
32
-
33
- @read_buffer = StringBuffer.new
34
- @write_buffer = StringBuffer.new
35
-
36
- # Used as destination buffer for underlying reads.
37
- @input_buffer = StringBuffer.new
38
- end
39
-
40
- attr_accessor :block_size
41
-
42
- # Reads `size` bytes from the stream. If size is not specified, read until end of file.
43
- def read(size = nil)
44
- return String.new(encoding: Encoding::BINARY) if size == 0
45
-
46
- if size
47
- until @eof or @read_buffer.bytesize >= size
48
- # Compute the amount of data we need to read from the underlying stream:
49
- read_size = size - @read_buffer.bytesize
50
-
51
- # Don't read less than @block_size to avoid lots of small reads:
52
- fill_read_buffer(read_size > @block_size ? read_size : @block_size)
53
- end
54
- else
55
- until @eof
56
- fill_read_buffer
57
- end
58
- end
59
-
60
- return consume_read_buffer(size)
61
- end
62
-
63
- # Read at most `size` bytes from the stream. Will avoid reading from the underlying stream if possible.
64
- def read_partial(size = nil)
65
- return String.new(encoding: Encoding::BINARY) if size == 0
66
-
67
- if !@eof and @read_buffer.empty?
68
- fill_read_buffer
69
- end
70
-
71
- return consume_read_buffer(size)
72
- end
73
-
74
- def read_exactly(size, exception: EOFError)
75
- if buffer = read(size)
76
- if buffer.bytesize != size
77
- raise exception, "could not read enough data"
78
- end
79
-
80
- return buffer
81
- end
82
-
83
- raise exception, "encountered eof while reading data"
84
- end
85
-
86
- # This is a compatibility shim for existing code that uses `readpartial`.
87
- def readpartial(size = nil)
88
- read_partial(size) or raise EOFError, "Encountered eof while reading data!"
89
- end
90
-
91
- private def index_of(pattern, offset, limit)
92
- # We don't want to split on the pattern, so we subtract the size of the pattern.
93
- split_offset = pattern.bytesize - 1
94
-
95
- until index = @read_buffer.index(pattern, offset)
96
- offset = @read_buffer.bytesize - split_offset
97
-
98
- offset = 0 if offset < 0
99
-
100
- return nil if limit and offset >= limit
101
- return nil unless fill_read_buffer
102
- end
103
-
104
- return index
105
- end
106
-
107
- # Efficiently read data from the stream until encountering pattern.
108
- # @parameter pattern [String] The pattern to match.
109
- # @parameter offset [Integer] The offset to start searching from.
110
- # @parameter limit [Integer] The maximum number of bytes to read, including the pattern (even if chomped).
111
- # @returns [String | Nil] The contents of the stream up until the pattern, which is consumed but not returned.
112
- def read_until(pattern, offset = 0, limit: nil, chomp: true)
113
- if index = index_of(pattern, offset, limit)
114
- return nil if limit and index >= limit
115
-
116
- @read_buffer.freeze
117
- matched = @read_buffer.byteslice(0, index+(chomp ? 0 : pattern.bytesize))
118
- @read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize)
119
-
120
- return matched
121
- end
122
- end
123
-
124
- def peek(size = nil)
125
- if size
126
- until @eof or @read_buffer.bytesize >= size
127
- # Compute the amount of data we need to read from the underlying stream:
128
- read_size = size - @read_buffer.bytesize
129
-
130
- # Don't read less than @block_size to avoid lots of small reads:
131
- fill_read_buffer(read_size > @block_size ? read_size : @block_size)
132
- end
133
- return @read_buffer[..([size, @read_buffer.size].min - 1)]
134
- end
135
- until (block_given? && yield(@read_buffer)) or @eof
136
- fill_read_buffer
137
- end
138
- return @read_buffer
139
- end
140
-
141
- def gets(separator = $/, limit = nil, chomp: false)
142
- # Compatibility with IO#gets:
143
- if separator.is_a?(Integer)
144
- limit = separator
145
- separator = $/
146
- end
147
-
148
- # We don't want to split in the middle of the separator, so we subtract the size of the separator from the start of the search:
149
- split_offset = separator.bytesize - 1
150
-
151
- offset = 0
152
-
153
- until index = @read_buffer.index(separator, offset)
154
- offset = @read_buffer.bytesize - split_offset
155
- offset = 0 if offset < 0
156
-
157
- # If a limit was given, and the offset is beyond the limit, we should return up to the limit:
158
- if limit and offset >= limit
159
- # As we didn't find the separator, there is nothing to chomp either.
160
- return consume_read_buffer(limit)
161
- end
162
-
163
- # If we can't read any more data, we should return what we have:
164
- return consume_read_buffer unless fill_read_buffer
165
- end
166
-
167
- # If the index of the separator was beyond the limit:
168
- if limit and index >= limit
169
- # Return up to the limit:
170
- return consume_read_buffer(limit)
171
- end
172
-
173
- # Freeze the read buffer, as this enables us to use byteslice without generating a hidden copy:
174
- @read_buffer.freeze
175
-
176
- line = @read_buffer.byteslice(0, index+(chomp ? 0 : separator.bytesize))
177
- @read_buffer = @read_buffer.byteslice(index+separator.bytesize, @read_buffer.bytesize)
178
-
179
- return line
180
- end
18
+ include Readable
19
+ include Writable
181
20
 
182
- private def drain(buffer)
183
- begin
184
- syswrite(buffer)
185
- ensure
186
- # If the write operation fails, we still need to clear this buffer, and the data is essentially lost.
187
- buffer.clear
188
- end
189
- end
190
-
191
- # Flushes buffered data to the stream.
192
- def flush
193
- return if @write_buffer.empty?
194
-
195
- @writing.synchronize do
196
- self.drain(@write_buffer)
197
- end
198
- end
199
-
200
- # Writes `string` to the buffer. When the buffer is full or #sync is true the
201
- # buffer is flushed to the underlying `io`.
202
- # @parameter string [String] the string to write to the buffer.
203
- # @returns [Integer] the number of bytes appended to the buffer.
204
- def write(string, flush: false)
205
- @writing.synchronize do
206
- @write_buffer << string
207
-
208
- flush |= (@write_buffer.bytesize >= @block_size)
209
-
210
- if flush
211
- self.drain(@write_buffer)
212
- end
213
- end
214
-
215
- return string.bytesize
216
- end
217
-
218
- # Writes `string` to the stream and returns self.
219
- def <<(string)
220
- write(string)
221
-
222
- return self
223
- end
224
-
225
- def puts(*arguments, separator: $/)
226
- return if arguments.empty?
227
-
228
- @writing.synchronize do
229
- arguments.each do |argument|
230
- @write_buffer << argument << separator
231
- end
232
-
233
- self.drain(@write_buffer)
234
- end
21
+ # Initialize a new generic stream.
22
+ # @parameter options [Hash] Options passed to included modules.
23
+ def initialize(**options)
24
+ super(**options)
235
25
  end
236
26
 
27
+ # Check if the stream is closed.
28
+ # @returns [Boolean] False by default, should be overridden by subclasses.
237
29
  def closed?
238
30
  false
239
31
  end
240
32
 
241
- def close_read
242
- end
243
-
244
- def close_write
245
- flush
246
- end
247
-
248
33
  # Best effort to flush any unwritten data, and then close the underling IO.
249
34
  def close
250
35
  return if closed?
@@ -258,115 +43,26 @@ module IO::Stream
258
43
  end
259
44
  end
260
45
 
261
- # Determins if the stream has consumed all available data. May block if the stream is not readable.
262
- # See {readable?} for a non-blocking alternative.
263
- #
264
- # @returns [Boolean] If the stream is at file which means there is no more data to be read.
265
- def eof?
266
- if !@read_buffer.empty?
267
- return false
268
- elsif @eof
269
- return true
270
- else
271
- return !self.fill_read_buffer
272
- end
273
- end
274
-
275
- def eof!
276
- @read_buffer.clear
277
- @eof = true
278
-
279
- raise EOFError
280
- end
281
-
282
- # Whether there is a chance that a read operation will succeed or not.
283
- # @returns [Boolean] If the stream is readable, i.e. a `read` operation has a chance of success.
284
- def readable?
285
- # If we are at the end of the file, we can't read any more data:
286
- if @eof
287
- return false
288
- end
289
-
290
- # If the read buffer is not empty, we can read more data:
291
- if !@read_buffer.empty?
292
- return true
293
- end
294
-
295
- # If the underlying stream is readable, we can read more data:
296
- return !closed?
297
- end
298
-
299
46
  protected
300
47
 
48
+ # Closes the underlying IO stream.
49
+ # This method should be implemented by subclasses to handle the specific closing logic.
301
50
  def sysclose
302
51
  raise NotImplementedError
303
52
  end
304
53
 
54
+ # Writes data to the underlying stream.
55
+ # This method should be implemented by subclasses to handle the specific writing logic.
56
+ # @parameter buffer [String] The data to write.
57
+ # @returns [Integer] The number of bytes written.
305
58
  def syswrite(buffer)
306
59
  raise NotImplementedError
307
60
  end
308
61
 
309
62
  # Reads data from the underlying stream as efficiently as possible.
63
+ # This method should be implemented by subclasses to handle the specific reading logic.
310
64
  def sysread(size, buffer)
311
65
  raise NotImplementedError
312
66
  end
313
-
314
- private
315
-
316
- # Fills the buffer from the underlying stream.
317
- def fill_read_buffer(size = @block_size)
318
- # We impose a limit because the underlying `read` system call can fail if we request too much data in one go.
319
- if size > @maximum_read_size
320
- size = @maximum_read_size
321
- end
322
-
323
- # This effectively ties the input and output stream together.
324
- flush
325
-
326
- if @read_buffer.empty?
327
- if sysread(size, @read_buffer)
328
- # Console.info(self, name: "read") {@read_buffer.inspect}
329
- return true
330
- end
331
- else
332
- if chunk = sysread(size, @input_buffer)
333
- @read_buffer << chunk
334
- # Console.info(self, name: "read") {@read_buffer.inspect}
335
-
336
- return true
337
- end
338
- end
339
-
340
- # else for both cases above:
341
- @eof = true
342
- return false
343
- end
344
-
345
- # Consumes at most `size` bytes from the buffer.
346
- # @parameter size [Integer|nil] The amount of data to consume. If nil, consume entire buffer.
347
- def consume_read_buffer(size = nil)
348
- # If we are at eof, and the read buffer is empty, we can't consume anything.
349
- return nil if @eof && @read_buffer.empty?
350
-
351
- result = nil
352
-
353
- if size.nil? or size >= @read_buffer.bytesize
354
- # Consume the entire read buffer:
355
- result = @read_buffer
356
- @read_buffer = StringBuffer.new
357
- else
358
- # This approach uses more memory.
359
- # result = @read_buffer.slice!(0, size)
360
-
361
- # We know that we are not going to reuse the original buffer.
362
- # But byteslice will generate a hidden copy. So let's freeze it first:
363
- @read_buffer.freeze
364
-
365
- result = @read_buffer.byteslice(0, size)
366
- @read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize)
367
- end
368
-
369
- return result
370
- end
371
67
  end
372
68
  end
@@ -5,52 +5,67 @@
5
5
 
6
6
  require "openssl"
7
7
 
8
+ # @namespace
8
9
  module OpenSSL
10
+ # @namespace
9
11
  module SSL
12
+ # SSL socket extensions for stream compatibility.
10
13
  class SSLSocket
11
14
  unless method_defined?(:close_read)
15
+ # Close the read end of the SSL socket.
12
16
  def close_read
13
17
  # Ignored.
14
18
  end
15
19
  end
16
20
 
17
21
  unless method_defined?(:close_write)
22
+ # Close the write end of the SSL socket.
18
23
  def close_write
19
24
  self.stop
20
25
  end
21
26
  end
22
27
 
23
28
  unless method_defined?(:wait_readable)
29
+ # Wait for the SSL socket to become readable.
24
30
  def wait_readable(...)
25
31
  to_io.wait_readable(...)
26
32
  end
27
33
  end
28
34
 
29
35
  unless method_defined?(:wait_writable)
36
+ # Wait for the SSL socket to become writable.
30
37
  def wait_writable(...)
31
38
  to_io.wait_writable(...)
32
39
  end
33
40
  end
34
41
 
35
42
  unless method_defined?(:timeout)
43
+ # Get the timeout for SSL socket operations.
44
+ # @returns [Numeric | Nil] The timeout value.
36
45
  def timeout
37
46
  to_io.timeout
38
47
  end
39
48
  end
40
49
 
41
50
  unless method_defined?(:timeout=)
51
+ # Set the timeout for SSL socket operations.
52
+ # @parameter value [Numeric | Nil] The timeout value.
42
53
  def timeout=(value)
43
54
  to_io.timeout = value
44
55
  end
45
56
  end
46
57
 
47
58
  unless method_defined?(:buffered?)
59
+ # Check if the SSL socket is buffered.
60
+ # @returns [Boolean] True if the SSL socket is buffered.
48
61
  def buffered?
49
62
  return to_io.buffered?
50
63
  end
51
64
  end
52
65
 
53
66
  unless method_defined?(:buffered=)
67
+ # Set the buffered state of the SSL socket.
68
+ # @parameter value [Boolean] True to enable buffering, false to disable.
54
69
  def buffered=(value)
55
70
  to_io.buffered = value
56
71
  end
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023-2024, by Samuel Williams.
5
+
6
+ require_relative "string_buffer"
7
+
8
+ module IO::Stream
9
+ # The default block size for IO buffers. Defaults to 256KB (optimized for modern SSDs and networks).
10
+ BLOCK_SIZE = ENV.fetch("IO_STREAM_BLOCK_SIZE", 1024*256).to_i
11
+
12
+ # The minimum read size for efficient I/O operations. Defaults to the same as BLOCK_SIZE.
13
+ MINIMUM_READ_SIZE = ENV.fetch("IO_STREAM_MINIMUM_READ_SIZE", BLOCK_SIZE).to_i
14
+
15
+ # The maximum read size for a single read operation. This limit exists because:
16
+ # 1. System calls like read() cannot handle requests larger than SSIZE_MAX
17
+ # 2. Very large reads can cause memory pressure and poor interactive performance
18
+ # 3. Most socket buffers and pipe capacities are much smaller anyway
19
+ # On 64-bit systems SSIZE_MAX is ~8.8 million MB, on 32-bit it's ~2GB.
20
+ # Our default of 16MB provides a good balance of throughput and responsiveness, and is page aligned.
21
+ # It is also a multiple of the minimum read size, so that we can read in chunks without exceeding the maximum.
22
+ MAXIMUM_READ_SIZE = ENV.fetch("IO_STREAM_MAXIMUM_READ_SIZE", MINIMUM_READ_SIZE * 64).to_i
23
+
24
+ # A module providing readable stream functionality.
25
+ #
26
+ # You must implement the `sysread` method to read data from the underlying IO.
27
+ module Readable
28
+ # Initialize readable stream functionality.
29
+ # @parameter minimum_read_size [Integer] The minimum size for read operations.
30
+ # @parameter maximum_read_size [Integer] The maximum size for read operations.
31
+ # @parameter block_size [Integer] Legacy parameter, use minimum_read_size instead.
32
+ def initialize(minimum_read_size: MINIMUM_READ_SIZE, maximum_read_size: MAXIMUM_READ_SIZE, block_size: nil, **, &block)
33
+ @done = false
34
+ @read_buffer = StringBuffer.new
35
+ # Used as destination buffer for underlying reads.
36
+ @input_buffer = StringBuffer.new
37
+
38
+ # Support legacy block_size parameter for backwards compatibility
39
+ @minimum_read_size = block_size || minimum_read_size
40
+ @maximum_read_size = maximum_read_size
41
+
42
+ super(**, &block) if defined?(super)
43
+ end
44
+
45
+ attr_accessor :minimum_read_size
46
+
47
+ # Legacy accessor for backwards compatibility
48
+ # @returns [Integer] The minimum read size.
49
+ def block_size
50
+ @minimum_read_size
51
+ end
52
+
53
+ # Legacy setter for backwards compatibility
54
+ # @parameter value [Integer] The minimum read size.
55
+ def block_size=(value)
56
+ @minimum_read_size = value
57
+ end
58
+
59
+ # Read data from the stream.
60
+ # @parameter size [Integer | Nil] The number of bytes to read. If nil, read until end of stream.
61
+ # @returns [String] The data read from the stream.
62
+ def read(size = nil)
63
+ return String.new(encoding: Encoding::BINARY) if size == 0
64
+
65
+ if size
66
+ until @done or @read_buffer.bytesize >= size
67
+ # Compute the amount of data we need to read from the underlying stream:
68
+ read_size = size - @read_buffer.bytesize
69
+
70
+ # Don't read less than @minimum_read_size to avoid lots of small reads:
71
+ fill_read_buffer(read_size > @minimum_read_size ? read_size : @minimum_read_size)
72
+ end
73
+ else
74
+ until @done
75
+ fill_read_buffer
76
+ end
77
+ end
78
+
79
+ return consume_read_buffer(size)
80
+ end
81
+
82
+ # Read at most `size` bytes from the stream. Will avoid reading from the underlying stream if possible.
83
+ def read_partial(size = nil)
84
+ return String.new(encoding: Encoding::BINARY) if size == 0
85
+
86
+ if !@done and @read_buffer.empty?
87
+ fill_read_buffer
88
+ end
89
+
90
+ return consume_read_buffer(size)
91
+ end
92
+
93
+ # Read exactly the specified number of bytes.
94
+ # @parameter size [Integer] The number of bytes to read.
95
+ # @parameter exception [Class] The exception to raise if not enough data is available.
96
+ # @returns [String] The data read from the stream.
97
+ def read_exactly(size, exception: EOFError)
98
+ if buffer = read(size)
99
+ if buffer.bytesize != size
100
+ raise exception, "Could not read enough data!"
101
+ end
102
+
103
+ return buffer
104
+ end
105
+
106
+ raise exception, "Encountered done while reading data!"
107
+ end
108
+
109
+ # This is a compatibility shim for existing code that uses `readpartial`.
110
+ def readpartial(size = nil)
111
+ read_partial(size) or raise EOFError, "Encountered done while reading data!"
112
+ end
113
+
114
+ # Find the index of a pattern in the read buffer, reading more data if needed.
115
+ # @parameter pattern [String] The pattern to search for.
116
+ # @parameter offset [Integer] The offset to start searching from.
117
+ # @parameter limit [Integer | Nil] The maximum number of bytes to read while searching.
118
+ # @returns [Integer | Nil] The index of the pattern, or nil if not found.
119
+ private def index_of(pattern, offset, limit)
120
+ # We don't want to split on the pattern, so we subtract the size of the pattern.
121
+ split_offset = pattern.bytesize - 1
122
+
123
+ until index = @read_buffer.index(pattern, offset)
124
+ offset = @read_buffer.bytesize - split_offset
125
+
126
+ offset = 0 if offset < 0
127
+
128
+ return nil if limit and offset >= limit
129
+ return nil unless fill_read_buffer
130
+ end
131
+
132
+ return index
133
+ end
134
+
135
+ # Efficiently read data from the stream until encountering pattern.
136
+ # @parameter pattern [String] The pattern to match.
137
+ # @parameter offset [Integer] The offset to start searching from.
138
+ # @parameter limit [Integer] The maximum number of bytes to read, including the pattern (even if chomped).
139
+ # @returns [String | Nil] The contents of the stream up until the pattern, which is consumed but not returned.
140
+ def read_until(pattern, offset = 0, limit: nil, chomp: true)
141
+ if index = index_of(pattern, offset, limit)
142
+ return nil if limit and index >= limit
143
+
144
+ @read_buffer.freeze
145
+ matched = @read_buffer.byteslice(0, index+(chomp ? 0 : pattern.bytesize))
146
+ @read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize)
147
+
148
+ return matched
149
+ end
150
+ end
151
+
152
+ # Peek at data in the buffer without consuming it.
153
+ # @parameter size [Integer | Nil] The number of bytes to peek at. If nil, peek at all available data.
154
+ # @returns [String] The data in the buffer without consuming it.
155
+ def peek(size = nil)
156
+ if size
157
+ until @done or @read_buffer.bytesize >= size
158
+ # Compute the amount of data we need to read from the underlying stream:
159
+ read_size = size - @read_buffer.bytesize
160
+
161
+ # Don't read less than @minimum_read_size to avoid lots of small reads:
162
+ fill_read_buffer(read_size > @minimum_read_size ? read_size : @minimum_read_size)
163
+ end
164
+ return @read_buffer[..([size, @read_buffer.size].min - 1)]
165
+ end
166
+ until (block_given? && yield(@read_buffer)) or @done
167
+ fill_read_buffer
168
+ end
169
+ return @read_buffer
170
+ end
171
+
172
+ # Read a line from the stream, similar to IO#gets.
173
+ # @parameter separator [String] The line separator to search for.
174
+ # @parameter limit [Integer | Nil] The maximum number of bytes to read.
175
+ # @parameter chomp [Boolean] Whether to remove the separator from the returned line.
176
+ # @returns [String | Nil] The line read from the stream, or nil if at end of stream.
177
+ def gets(separator = $/, limit = nil, chomp: false)
178
+ # Compatibility with IO#gets:
179
+ if separator.is_a?(Integer)
180
+ limit = separator
181
+ separator = $/
182
+ end
183
+
184
+ # We don't want to split in the middle of the separator, so we subtract the size of the separator from the start of the search:
185
+ split_offset = separator.bytesize - 1
186
+
187
+ offset = 0
188
+
189
+ until index = @read_buffer.index(separator, offset)
190
+ offset = @read_buffer.bytesize - split_offset
191
+ offset = 0 if offset < 0
192
+
193
+ # If a limit was given, and the offset is beyond the limit, we should return up to the limit:
194
+ if limit and offset >= limit
195
+ # As we didn't find the separator, there is nothing to chomp either.
196
+ return consume_read_buffer(limit)
197
+ end
198
+
199
+ # If we can't read any more data, we should return what we have:
200
+ return consume_read_buffer unless fill_read_buffer
201
+ end
202
+
203
+ # If the index of the separator was beyond the limit:
204
+ if limit and index >= limit
205
+ # Return up to the limit:
206
+ return consume_read_buffer(limit)
207
+ end
208
+
209
+ # Freeze the read buffer, as this enables us to use byteslice without generating a hidden copy:
210
+ @read_buffer.freeze
211
+
212
+ line = @read_buffer.byteslice(0, index+(chomp ? 0 : separator.bytesize))
213
+ @read_buffer = @read_buffer.byteslice(index+separator.bytesize, @read_buffer.bytesize)
214
+
215
+ return line
216
+ end
217
+
218
+ # Determins if the stream has consumed all available data. May block if the stream is not readable.
219
+ # See {readable?} for a non-blocking alternative.
220
+ #
221
+ # @returns [Boolean] If the stream is at file which means there is no more data to be read.
222
+ def done?
223
+ if !@read_buffer.empty?
224
+ return false
225
+ elsif @done
226
+ return true
227
+ else
228
+ return !self.fill_read_buffer
229
+ end
230
+ end
231
+
232
+ alias eof? done?
233
+
234
+ # Mark the stream as done and raise `EOFError`.
235
+ def done!
236
+ @read_buffer.clear
237
+ @done = true
238
+
239
+ raise EOFError
240
+ end
241
+
242
+ alias eof! done!
243
+
244
+ # Whether there is a chance that a read operation will succeed or not.
245
+ # @returns [Boolean] If the stream is readable, i.e. a `read` operation has a chance of success.
246
+ def readable?
247
+ # If we are at the end of the file, we can't read any more data:
248
+ if @done
249
+ return false
250
+ end
251
+
252
+ # If the read buffer is not empty, we can read more data:
253
+ if !@read_buffer.empty?
254
+ return true
255
+ end
256
+
257
+ # If the underlying stream is readable, we can read more data:
258
+ return !closed?
259
+ end
260
+
261
+ # Close the read end of the stream.
262
+ def close_read
263
+ end
264
+
265
+ private
266
+
267
+ # Fills the buffer from the underlying stream.
268
+ def fill_read_buffer(size = @minimum_read_size)
269
+ # Limit the read size to avoid exceeding SSIZE_MAX and to manage memory usage.
270
+ # Very large reads can also hurt interactive performance by blocking for too long.
271
+ if size > @maximum_read_size
272
+ size = @maximum_read_size
273
+ end
274
+
275
+ # This effectively ties the input and output stream together.
276
+ flush
277
+
278
+ if @read_buffer.empty?
279
+ if sysread(size, @read_buffer)
280
+ # Console.info(self, name: "read") {@read_buffer.inspect}
281
+ return true
282
+ end
283
+ else
284
+ if chunk = sysread(size, @input_buffer)
285
+ @read_buffer << chunk
286
+ # Console.info(self, name: "read") {@read_buffer.inspect}
287
+
288
+ return true
289
+ end
290
+ end
291
+
292
+ # else for both cases above:
293
+ @done = true
294
+ return false
295
+ end
296
+
297
+ # Consumes at most `size` bytes from the buffer.
298
+ # @parameter size [Integer | Nil] The amount of data to consume. If nil, consume entire buffer.
299
+ def consume_read_buffer(size = nil)
300
+ # If we are at done, and the read buffer is empty, we can't consume anything.
301
+ return nil if @done && @read_buffer.empty?
302
+
303
+ result = nil
304
+
305
+ if size.nil? or size >= @read_buffer.bytesize
306
+ # Consume the entire read buffer:
307
+ result = @read_buffer
308
+ @read_buffer = StringBuffer.new
309
+ else
310
+ # This approach uses more memory.
311
+ # result = @read_buffer.slice!(0, size)
312
+
313
+ # We know that we are not going to reuse the original buffer.
314
+ # But byteslice will generate a hidden copy. So let's freeze it first:
315
+ @read_buffer.freeze
316
+
317
+ result = @read_buffer.byteslice(0, size)
318
+ @read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize)
319
+ end
320
+
321
+ return result
322
+ end
323
+ end
324
+ end
@@ -5,10 +5,14 @@
5
5
 
6
6
  unless IO.method_defined?(:buffered?, false)
7
7
  class IO
8
+ # Check if the IO is buffered.
9
+ # @returns [Boolean] True if the IO is buffered (not synchronized).
8
10
  def buffered?
9
11
  return !self.sync
10
12
  end
11
13
 
14
+ # Set the buffered state of the IO.
15
+ # @parameter value [Boolean] True to enable buffering, false to disable.
12
16
  def buffered=(value)
13
17
  self.sync = !value
14
18
  end
@@ -18,13 +22,18 @@ end
18
22
  require "socket"
19
23
 
20
24
  unless BasicSocket.method_defined?(:buffered?, false)
25
+ # Socket extensions for buffering support.
21
26
  class BasicSocket
27
+ # Check if this socket uses TCP protocol.
28
+ # @returns [Boolean] True if the socket is TCP over IPv4 or IPv6.
22
29
  def ip_protocol_tcp?
23
30
  local_address = self.local_address
24
31
 
25
32
  return (local_address.afamily == ::Socket::AF_INET || local_address.afamily == ::Socket::AF_INET6) && local_address.socktype == ::Socket::SOCK_STREAM
26
33
  end
27
34
 
35
+ # Check if the socket is buffered.
36
+ # @returns [Boolean] True if the socket is buffered.
28
37
  def buffered?
29
38
  return false unless super
30
39
 
@@ -35,6 +44,8 @@ unless BasicSocket.method_defined?(:buffered?, false)
35
44
  end
36
45
  end
37
46
 
47
+ # Set the buffered state of the socket.
48
+ # @parameter value [Boolean] True to enable buffering, false to disable.
38
49
  def buffered=(value)
39
50
  super
40
51
 
@@ -53,11 +64,16 @@ end
53
64
  require "stringio"
54
65
 
55
66
  unless StringIO.method_defined?(:buffered?, false)
67
+ # StringIO extensions for buffering support.
56
68
  class StringIO
69
+ # Check if the StringIO is buffered.
70
+ # @returns [Boolean] True if the StringIO is buffered (not synchronized).
57
71
  def buffered?
58
72
  return !self.sync
59
73
  end
60
74
 
75
+ # Set the buffered state of the StringIO.
76
+ # @parameter value [Boolean] True to enable buffering, false to disable.
61
77
  def buffered=(value)
62
78
  self.sync = !value
63
79
  end
@@ -5,6 +5,8 @@
5
5
 
6
6
  class IO
7
7
  unless method_defined?(:readable?, false)
8
+ # Check if the IO is readable.
9
+ # @returns [Boolean] True if the IO is readable (not closed).
8
10
  def readable?
9
11
  # Do not call `eof?` here as it is not concurrency-safe and it can block.
10
12
  !closed?
@@ -16,6 +18,8 @@ require "socket"
16
18
 
17
19
  class BasicSocket
18
20
  unless method_defined?(:readable?, false)
21
+ # Check if the socket is readable.
22
+ # @returns [Boolean] True if the socket is readable.
19
23
  def readable?
20
24
  # If we can wait for the socket to become readable, we know that the socket may still be open.
21
25
  result = self.recv_nonblock(1, ::Socket::MSG_PEEK, exception: false)
@@ -36,6 +40,8 @@ require "stringio"
36
40
 
37
41
  class StringIO
38
42
  unless method_defined?(:readable?, false)
43
+ # Check if the StringIO is readable.
44
+ # @returns [Boolean] True if the StringIO is readable (not at EOF).
39
45
  def readable?
40
46
  !eof?
41
47
  end
@@ -46,6 +52,8 @@ require "openssl"
46
52
 
47
53
  class OpenSSL::SSL::SSLSocket
48
54
  unless method_defined?(:readable?, false)
55
+ # Check if the SSL socket is readable.
56
+ # @returns [Boolean] True if the SSL socket is readable.
49
57
  def readable?
50
58
  to_io.readable?
51
59
  end
@@ -4,15 +4,20 @@
4
4
  # Copyright, 2023-2024, by Samuel Williams.
5
5
 
6
6
  module IO::Stream
7
+ # A specialized string buffer for binary data with automatic encoding handling.
7
8
  class StringBuffer < String
8
9
  BINARY = Encoding::BINARY
9
10
 
11
+ # Initialize a new string buffer with binary encoding.
10
12
  def initialize
11
13
  super
12
14
 
13
15
  force_encoding(BINARY)
14
16
  end
15
17
 
18
+ # Append a string to the buffer, converting to binary encoding if necessary.
19
+ # @parameter string [String] The string to append.
20
+ # @returns [StringBuffer] Self for method chaining.
16
21
  def << string
17
22
  if string.encoding == BINARY
18
23
  super(string)
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2023-2024, by Samuel Williams.
5
5
 
6
6
  module IO::Stream
7
- VERSION = "0.6.1"
7
+ VERSION = "0.7.0"
8
8
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023-2024, by Samuel Williams.
5
+
6
+ require_relative "readable"
7
+
8
+ module IO::Stream
9
+ # The minimum write size before flushing. Defaults to 64KB.
10
+ MINIMUM_WRITE_SIZE = ENV.fetch("IO_STREAM_MINIMUM_WRITE_SIZE", BLOCK_SIZE).to_i
11
+
12
+ # A module providing writable stream functionality.
13
+ #
14
+ # You must implement the `syswrite` method to write data to the underlying IO.
15
+ module Writable
16
+ # Initialize writable stream functionality.
17
+ # @parameter minimum_write_size [Integer] The minimum buffer size before flushing.
18
+ def initialize(minimum_write_size: MINIMUM_WRITE_SIZE, **, &block)
19
+ @writing = ::Thread::Mutex.new
20
+ @write_buffer = StringBuffer.new
21
+ @minimum_write_size = minimum_write_size
22
+
23
+ super(**, &block) if defined?(super)
24
+ end
25
+
26
+ attr_accessor :minimum_write_size
27
+
28
+ # Flushes buffered data to the stream.
29
+ def flush
30
+ return if @write_buffer.empty?
31
+
32
+ @writing.synchronize do
33
+ self.drain(@write_buffer)
34
+ end
35
+ end
36
+
37
+ # Writes `string` to the buffer. When the buffer is full or #sync is true the
38
+ # buffer is flushed to the underlying `io`.
39
+ # @parameter string [String] the string to write to the buffer.
40
+ # @returns [Integer] the number of bytes appended to the buffer.
41
+ def write(string, flush: false)
42
+ @writing.synchronize do
43
+ @write_buffer << string
44
+
45
+ flush |= (@write_buffer.bytesize >= @minimum_write_size)
46
+
47
+ if flush
48
+ self.drain(@write_buffer)
49
+ end
50
+ end
51
+
52
+ return string.bytesize
53
+ end
54
+
55
+ # Appends `string` to the buffer and returns self for method chaining.
56
+ # @parameter string [String] the string to write to the stream.
57
+ def <<(string)
58
+ write(string)
59
+
60
+ return self
61
+ end
62
+
63
+ # Write arguments to the stream followed by a separator and flush immediately.
64
+ # @parameter arguments [Array] The arguments to write to the stream.
65
+ # @parameter separator [String] The separator to append after each argument.
66
+ def puts(*arguments, separator: $/)
67
+ return if arguments.empty?
68
+
69
+ @writing.synchronize do
70
+ arguments.each do |argument|
71
+ @write_buffer << argument << separator
72
+ end
73
+
74
+ self.drain(@write_buffer)
75
+ end
76
+ end
77
+
78
+ # Close the write end of the stream by flushing any remaining data.
79
+ def close_write
80
+ flush
81
+ end
82
+
83
+ private def drain(buffer)
84
+ begin
85
+ syswrite(buffer)
86
+ ensure
87
+ # If the write operation fails, we still need to clear this buffer, and the data is essentially lost.
88
+ buffer.clear
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/io/stream.rb CHANGED
@@ -6,10 +6,15 @@
6
6
  require_relative "stream/version"
7
7
  require_relative "stream/buffered"
8
8
 
9
+ # @namespace
9
10
  class IO
11
+ # @namespace
10
12
  module Stream
11
13
  end
12
14
 
15
+ # Convert any IO-like object into a buffered stream.
16
+ # @parameter io [IO] The IO object to wrap.
17
+ # @returns [IO::Stream::Buffered] A buffered stream wrapper.
13
18
  def self.Stream(io)
14
19
  if io.is_a?(Stream::Buffered)
15
20
  io
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2023-2024, by Samuel Williams.
3
+ Copyright, 2023-2025, by Samuel Williams.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -8,6 +8,64 @@ Provide a buffered stream implementation for Ruby, independent of the underlying
8
8
 
9
9
  Please see the [project documentation](https://socketry.github.io/io-stream) for more details.
10
10
 
11
+ ## Releases
12
+
13
+ Please see the [project releases](https://socketry.github.io/io-streamreleases/index) for all releases.
14
+
15
+ ### v0.7.0
16
+
17
+ - Split stream functionality into separate `Readable` and `Writable` modules for better modularity and composition.
18
+ - Remove unused timeout shim functionality.
19
+ - 100% documentation coverage.
20
+
21
+ ### v0.6.1
22
+
23
+ - Fix compatibility with Ruby v3.3.0 - v3.3.6 where broken `@io.close` could hang.
24
+
25
+ ### v0.6.0
26
+
27
+ - Improve compatibility of `gets` implementation to better match Ruby's IO\#gets behavior.
28
+
29
+ ### v0.5.0
30
+
31
+ - Add support for `read_until(limit:)` parameter to limit the amount of data read.
32
+ - Minor documentation improvements.
33
+
34
+ ### v0.4.3
35
+
36
+ - Add comprehensive tests for `buffered?` method on `SSLSocket`.
37
+ - Ensure TLS connections have correct buffering behavior.
38
+ - Improve test suite organization and readability.
39
+
40
+ ### v0.4.2
41
+
42
+ - Add external test suite for better integration testing.
43
+ - Update dependencies and improve code style with RuboCop.
44
+
45
+ ### v0.4.1
46
+
47
+ - Add compatibility fix for `SSLSocket` raising `EBADF` errors.
48
+ - Fix `IO#close` hang issue in certain scenarios.
49
+ - Add `#to_io` method to `IO::Stream::Buffered` for better compatibility.
50
+ - Modernize gem structure and dependencies.
51
+
52
+ ### v0.4.0
53
+
54
+ - Add convenient `IO.Stream()` constructor method for creating buffered streams.
55
+
56
+ ### v0.3.0
57
+
58
+ - Add support for timeouts with compatibility shims for various IO types.
59
+
60
+ ### v0.2.0
61
+
62
+ - Prefer `write_nonblock` in `syswrite` implementation for better non-blocking behavior.
63
+ - Add test cases for crash scenarios.
64
+
65
+ ## See Also
66
+
67
+ - [async-io](https://github.com/socketry/async-io) — Where this implementation originally came from.
68
+
11
69
  ## Contributing
12
70
 
13
71
  We welcome contributions to this project.
@@ -25,7 +83,3 @@ In order to protect users of this project, we require all contributors to comply
25
83
  ### Community Guidelines
26
84
 
27
85
  This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
28
-
29
- ## See Also
30
-
31
- - [async-io](https://github.com/socketry/async-io) — Where this implementation originally came from.
data/releases.md ADDED
@@ -0,0 +1,70 @@
1
+ # Releases
2
+
3
+ ## v0.7.0
4
+
5
+ - Split stream functionality into separate `Readable` and `Writable` modules for better modularity and composition.
6
+ - Remove unused timeout shim functionality.
7
+ - 100% documentation coverage.
8
+
9
+ ## v0.6.1
10
+
11
+ - Fix compatibility with Ruby v3.3.0 - v3.3.6 where broken `@io.close` could hang.
12
+
13
+ ## v0.6.0
14
+
15
+ - Improve compatibility of `gets` implementation to better match Ruby's IO\#gets behavior.
16
+
17
+ ## v0.5.0
18
+
19
+ - Add support for `read_until(limit:)` parameter to limit the amount of data read.
20
+ - Minor documentation improvements.
21
+
22
+ ## v0.4.3
23
+
24
+ - Add comprehensive tests for `buffered?` method on `SSLSocket`.
25
+ - Ensure TLS connections have correct buffering behavior.
26
+ - Improve test suite organization and readability.
27
+
28
+ ## v0.4.2
29
+
30
+ - Add external test suite for better integration testing.
31
+ - Update dependencies and improve code style with RuboCop.
32
+
33
+ ## v0.4.1
34
+
35
+ - Add compatibility fix for `SSLSocket` raising `EBADF` errors.
36
+ - Fix `IO#close` hang issue in certain scenarios.
37
+ - Add `#to_io` method to `IO::Stream::Buffered` for better compatibility.
38
+ - Modernize gem structure and dependencies.
39
+
40
+ ## v0.4.0
41
+
42
+ - Add convenient `IO.Stream()` constructor method for creating buffered streams.
43
+
44
+ ## v0.3.0
45
+
46
+ - Add support for timeouts with compatibility shims for various IO types.
47
+
48
+ ## v0.2.0
49
+
50
+ - Prefer `write_nonblock` in `syswrite` implementation for better non-blocking behavior.
51
+ - Add test cases for crash scenarios.
52
+
53
+ ## v0.1.1
54
+
55
+ - Improve buffering compatibility by falling back to `sync=` when `buffered=` is not available.
56
+
57
+ ## v0.1.0
58
+
59
+ - Rename `IO::Stream::BufferedStream` to `IO::Stream::Buffered` for consistency.
60
+ - Add comprehensive tests and improved OpenSSL support with compatibility shims.
61
+ - Improve compatibility with Darwin/macOS systems.
62
+ - Fix monkey patches for various IO types.
63
+ - Add support for `StringIO#buffered?` method.
64
+
65
+ ## v0.0.1
66
+
67
+ - Initial release with basic buffered stream functionality.
68
+ - Provide `IO::Stream::Buffered` class for efficient buffered I/O operations.
69
+ - Add `readable?` method to check stream readability status.
70
+ - Include basic test suite.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,11 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-stream
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain:
11
10
  - |
@@ -37,10 +36,8 @@ cert_chain:
37
36
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
37
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
38
  -----END CERTIFICATE-----
40
- date: 2024-11-08 00:00:00.000000000 Z
39
+ date: 1980-01-02 00:00:00.000000000 Z
41
40
  dependencies: []
42
- description:
43
- email:
44
41
  executables: []
45
42
  extensions: []
46
43
  extra_rdoc_files: []
@@ -49,21 +46,21 @@ files:
49
46
  - lib/io/stream/buffered.rb
50
47
  - lib/io/stream/generic.rb
51
48
  - lib/io/stream/openssl.rb
49
+ - lib/io/stream/readable.rb
52
50
  - lib/io/stream/shim/buffered.rb
53
51
  - lib/io/stream/shim/readable.rb
54
- - lib/io/stream/shim/shim.md
55
- - lib/io/stream/shim/timeout.rb
56
52
  - lib/io/stream/string_buffer.rb
57
53
  - lib/io/stream/version.rb
54
+ - lib/io/stream/writable.rb
58
55
  - license.md
59
56
  - readme.md
57
+ - releases.md
60
58
  homepage: https://github.com/socketry/io-stream
61
59
  licenses:
62
60
  - MIT
63
61
  metadata:
64
62
  documentation_uri: https://socketry.github.io/io-stream
65
63
  source_code_uri: https://github.com/socketry/io-stream.git
66
- post_install_message:
67
64
  rdoc_options: []
68
65
  require_paths:
69
66
  - lib
@@ -71,15 +68,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
71
68
  requirements:
72
69
  - - ">="
73
70
  - !ruby/object:Gem::Version
74
- version: '3.1'
71
+ version: '3.2'
75
72
  required_rubygems_version: !ruby/object:Gem::Requirement
76
73
  requirements:
77
74
  - - ">="
78
75
  - !ruby/object:Gem::Version
79
76
  version: '0'
80
77
  requirements: []
81
- rubygems_version: 3.5.11
82
- signing_key:
78
+ rubygems_version: 3.6.7
83
79
  specification_version: 4
84
80
  summary: Provides a generic stream wrapper for IO instances.
85
81
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,4 +0,0 @@
1
- # Shims
2
-
3
- Several shims are included for compatibility.
4
-
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
5
-
6
- class IO
7
- unless const_defined?(:TimeoutError)
8
- # Compatibility shim.
9
- class TimeoutError < IOError
10
- end
11
- end
12
-
13
- unless method_defined?(:timeout)
14
- # Compatibility shim.
15
- attr_accessor :timeout
16
- end
17
- end