Skip to content

Commit 75b905c

Browse files
committed
Add support for duplex streams.
1 parent 2f6d670 commit 75b905c

12 files changed

Lines changed: 260 additions & 10 deletions

File tree

lib/io/stream.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2023-2025, by Samuel Williams.
4+
# Copyright, 2023-2026, by Samuel Williams.
55

66
require_relative "stream/version"
77
require_relative "stream/buffered"
8+
require_relative "stream/duplex"
89

910
# @namespace
1011
class IO
1112
# @namespace
1213
module Stream
14+
def self.Duplex(input, output = input, **options)
15+
Buffered.wrap(Duplex.new(input, output), **options)
16+
end
1317
end
1418

1519
# Convert any IO-like object into a buffered stream.

lib/io/stream/buffered.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2024-2025, by Samuel Williams.
4+
# Copyright, 2024-2026, by Samuel Williams.
55

66
require_relative "generic"
77
require_relative "connection_reset_error"

lib/io/stream/duplex.rb

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2026, by Samuel Williams.
5+
6+
module IO::Stream
7+
# A low-level duplex IO adapter that composes distinct readable and writable endpoints.
8+
class Duplex
9+
def initialize(input, output = input)
10+
@input = input
11+
@output = output
12+
end
13+
14+
attr :input
15+
attr :output
16+
17+
def to_io
18+
@input || @output
19+
end
20+
21+
def timeout
22+
[@input.timeout, @output.timeout].compact.max
23+
end
24+
25+
def timeout=(duration)
26+
@input.timeout = duration
27+
@output.timeout = duration
28+
end
29+
30+
def closed?
31+
@input.closed? && @output.closed?
32+
end
33+
34+
def close_read
35+
return if @input.closed?
36+
37+
if @input.respond_to?(:close_read)
38+
@input.close_read
39+
else
40+
@input.close
41+
end
42+
end
43+
44+
def close_write
45+
return if @output.closed?
46+
47+
if @output.respond_to?(:close_write)
48+
@output.close_write
49+
else
50+
@output.close
51+
end
52+
end
53+
54+
def readable?
55+
@input.readable?
56+
end
57+
58+
def close
59+
@output.close unless @output.closed?
60+
@input.close unless @input.closed?
61+
end
62+
63+
def write(buffer)
64+
@output.write(buffer)
65+
end
66+
67+
def read_nonblock(size, buffer, exception: false)
68+
@input.read_nonblock(size, buffer, exception: exception)
69+
end
70+
71+
def wait_readable(duration = @timeout)
72+
@input.wait_readable(duration)
73+
end
74+
75+
def wait_writable(duration = @timeout)
76+
@output.wait_writable(duration)
77+
end
78+
end
79+
end

lib/io/stream/generic.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2023-2025, by Samuel Williams.
4+
# Copyright, 2023-2026, by Samuel Williams.
55

66
require_relative "string_buffer"
77
require_relative "readable"
88
require_relative "writable"
99

1010
require_relative "shim/buffered"
1111
require_relative "shim/readable"
12+
require_relative "shim/timeout"
1213

1314
require_relative "openssl"
1415

lib/io/stream/openssl.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2024-2025, by Samuel Williams.
4+
# Copyright, 2024-2026, by Samuel Williams.
55

66
require "openssl"
77

lib/io/stream/readable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2025, by Samuel Williams.
4+
# Copyright, 2025-2026, by Samuel Williams.
55

66
require_relative "string_buffer"
77

lib/io/stream/shim/timeout.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2024-2026, by Samuel Williams.
5+
6+
require "stringio"
7+
8+
class StringIO
9+
unless method_defined?(:timeout)
10+
def timeout
11+
@timeout
12+
end
13+
end
14+
15+
unless method_defined?(:timeout=)
16+
def timeout=(duration)
17+
@timeout = duration
18+
end
19+
end
20+
end

license.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# MIT License
22

3-
Copyright, 2023-2025, by Samuel Williams.
3+
Copyright, 2023-2026, by Samuel Williams.
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

releases.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Introduce `IO::Stream::Duplex` as a low-level duplex transport for composing separate input and output endpoints.
6+
- Add `IO::Stream::Duplex(input, output)` as a convenient constructor that returns a buffered stream wrapping a duplex transport.
7+
- Add a timeout compatibility shim for `StringIO` so duplex streams composed from in-memory endpoints can participate in the timeout interface consistently.
58
- Remove old OpenSSL method shims.
69

710
## v0.11.0

test/io/stream.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2024, by Samuel Williams.
4+
# Copyright, 2024-2026, by Samuel Williams.
55

66
require "io/stream"
77

@@ -22,4 +22,23 @@
2222

2323
expect(stream2).to be_equal(stream)
2424
end
25+
26+
it "can wrap an existing duplex stream" do
27+
input = StringIO.new
28+
output = StringIO.new
29+
30+
duplex = IO::Stream::Duplex.new(input, output)
31+
stream = IO::Stream(duplex)
32+
33+
expect(stream).to be_a(IO::Stream::Buffered)
34+
expect(stream.io).to be_equal(duplex)
35+
end
36+
37+
it "provides timeout shims for StringIO-backed duplex streams" do
38+
duplex = IO::Stream::Duplex.new(StringIO.new, StringIO.new)
39+
40+
expect(duplex.timeout).to be_nil
41+
expect(duplex.timeout = 0.5).to be == 0.5
42+
expect(duplex.timeout).to be == 0.5
43+
end
2544
end

0 commit comments

Comments
 (0)