This is an automated email from the ASF dual-hosted git repository.

Cole-Greer pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/master by this push:
     new 0d485a6e95 Add custom read buffer for GraphBinary responses (#3517)
0d485a6e95 is described below

commit 0d485a6e9570a362a51f9781eff0d3062f229894
Author: kirill-stepanishin <[email protected]>
AuthorDate: Sat Jul 18 18:11:05 2026 -0700

    Add custom read buffer for GraphBinary responses (#3517)
    
    The GraphBinary response reader in Gremlin.Net used a `BufferedStream` so 
it could refill from the network in 8 KiB chunks. But even when the next few 
bytes were already sitting in that buffer, every primitive read still went 
through an async state machine and allocated a small scratch array. On large 
responses, that per-read overhead adds up to a large share of deserialization 
time.
    
    `BufferedStream` is replaced with a small custom `GraphBinaryReadBuffer`. 
Refill behavior is the same, but when the bytes are already buffered the typed 
readers complete synchronously with no allocation. When they are not, a slow 
path handles the refill and keeps the previous EOF and cancellation behavior.
    
    Variable-length payloads (Binary, String, Char) also switch to 
`ReadExactlyAsync`, since `Stream.ReadAsync` may return fewer bytes than 
requested and the old code assumed it wouldn't.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 .../IO/GraphBinary4/GraphBinaryReadBuffer.cs       |  305 ++++++
 .../IO/GraphBinary4/ResponseSerializer.cs          |   10 +-
 .../Structure/IO/GraphBinary4/StreamExtensions.cs  |  130 ++-
 .../IO/GraphBinary4/Types/BinarySerializer.cs      |    2 +-
 .../IO/GraphBinary4/Types/CharSerializer.cs        |    2 +-
 .../IO/GraphBinary4/Types/StringSerializer.cs      |    2 +-
 .../IO/GraphBinary4/GraphBinaryReadBufferTests.cs  | 1114 ++++++++++++++++++++
 7 files changed, 1518 insertions(+), 47 deletions(-)

diff --git 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReadBuffer.cs
 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReadBuffer.cs
new file mode 100644
index 0000000000..4058a763a1
--- /dev/null
+++ 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReadBuffer.cs
@@ -0,0 +1,305 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4
+{
+    /// <summary>
+    ///     A read-only buffering <see cref="Stream"/> for a single 
GraphBinary 4.0 response. Exposes
+    ///     typed readers for each fixed-width value. When the bytes are 
already in the internal buffer,
+    ///     the reader parses them inline and completes synchronously. Used by 
a single reader at a
+    ///     time; the underlying stream is owned by 
<c>StreamingResponseContext</c> and is never
+    ///     disposed here.
+    /// </summary>
+    internal sealed class GraphBinaryReadBuffer : Stream
+    {
+        private const int DefaultBufferSize = 8192;
+
+        private readonly Stream _stream;
+        private readonly byte[] _buffer;
+
+        // Unread bytes sit in _buffer[_start.._end). Reads advance _start; 
when _start == _end the buffer
+        // is empty and needs a refill. Invariant: 0 <= _start <= _end.
+        private int _start;
+        private int _end;
+
+        /// <summary>
+        ///     Initializes a new instance of the <see 
cref="GraphBinaryReadBuffer"/> class wrapping the given
+        ///     underlying stream, using the default 8192-byte buffer.
+        /// </summary>
+        /// <param name="stream">The underlying stream to read from. Not owned 
by this instance.</param>
+        internal GraphBinaryReadBuffer(Stream stream) : this(stream, 
DefaultBufferSize)
+        {
+        }
+
+        /// <summary>
+        ///     Initializes a new instance of the <see 
cref="GraphBinaryReadBuffer"/> class wrapping the given
+        ///     underlying stream, using a buffer of the given size. Mainly 
exists so tests can force small
+        ///     buffers and frequent refills.
+        /// </summary>
+        /// <param name="stream">The underlying stream to read from. Not owned 
by this instance.</param>
+        /// <param name="bufferSize">The size of the internal read buffer in 
bytes.</param>
+        internal GraphBinaryReadBuffer(Stream stream, int bufferSize)
+        {
+            _stream = stream ?? throw new 
ArgumentNullException(nameof(stream));
+            if (bufferSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(bufferSize));
+            }
+            _buffer = new byte[bufferSize];
+        }
+
+        /// <inheritdoc />
+        public override bool CanRead => true;
+
+        /// <inheritdoc />
+        public override bool CanWrite => false;
+
+        /// <inheritdoc />
+        public override bool CanSeek => false;
+
+        /// <inheritdoc />
+        public override long Length => throw new NotSupportedException();
+
+        /// <inheritdoc />
+        public override long Position
+        {
+            get => throw new NotSupportedException();
+            set => throw new NotSupportedException();
+        }
+
+        /// <inheritdoc />
+        public override void Flush()
+        {
+        }
+
+        /// <inheritdoc />
+        public override long Seek(long offset, SeekOrigin origin) => throw new 
NotSupportedException();
+
+        /// <inheritdoc />
+        public override void SetLength(long value) => throw new 
NotSupportedException();
+
+        /// <inheritdoc />
+        public override void Write(byte[] buffer, int offset, int count) => 
throw new NotSupportedException();
+
+        // --- General read side (serve from the internal buffer, refill from 
the underlying stream) ---
+        //
+        // Synchronous Read is unsupported. The buffer is always driven 
asynchronously by
+        // ResponseSerializer.ReadStreamingAsync. Throwing here matches 
Seek/Write and prevents a future
+        // caller from accidentally blocking a thread on a refill.
+
+        /// <inheritdoc />
+        public override int Read(byte[] buffer, int offset, int count) => 
throw new NotSupportedException();
+
+        /// <inheritdoc />
+        public override int Read(Span<byte> buffer) => throw new 
NotSupportedException();
+
+        /// <inheritdoc />
+        public override Task<int> ReadAsync(byte[] buffer, int offset, int 
count,
+            CancellationToken cancellationToken)
+        {
+            return ReadAsync(buffer.AsMemory(offset, count), 
cancellationToken).AsTask();
+        }
+
+        /// <inheritdoc />
+        public override async ValueTask<int> ReadAsync(Memory<byte> buffer,
+            CancellationToken cancellationToken = default)
+        {
+            if (buffer.Length == 0)
+            {
+                return 0;
+            }
+
+            if (_start >= _end)
+            {
+                await RefillAsync(cancellationToken).ConfigureAwait(false);
+                if (_start >= _end)
+                {
+                    return 0;
+                }
+            }
+
+            var available = _end - _start;
+            var toCopy = Math.Min(available, buffer.Length);
+            _buffer.AsSpan(_start, toCopy).CopyTo(buffer.Span);
+            _start += toCopy;
+            return toCopy;
+        }
+
+        // Only called when the buffer is empty, so overwriting _buffer never 
discards unread data.
+        // Indices are updated only after the underlying read returns, so if 
it throws the buffer stays
+        // in its previous state instead of replaying bytes that were already 
consumed.
+        private async ValueTask RefillAsync(CancellationToken 
cancellationToken)
+        {
+            var bytesRead = await _stream.ReadAsync(_buffer.AsMemory(0, 
_buffer.Length), cancellationToken)
+                .ConfigureAwait(false);
+            _start = 0;
+            _end = bytesRead;
+        }
+
+        // --- Typed value readers ---
+        //
+        // Fast path: TryReadSpan(n) parses inline from the internal buffer. 
Slow path: FillSlowAsync
+        // for multi-byte types (throws EndOfStreamException on EOF); 
ReadByteValueSlowAsync for a
+        // single byte (throws IOException on EOF). The EOF split is confined 
to those two methods.
+
+        /// <summary>
+        ///     Tries to serve <paramref name="n"/> bytes directly from the 
internal buffer with no I/O.
+        ///     If <paramref name="n"/> bytes are available, sets <paramref 
name="span"/> over them, advances
+        ///     past them, and returns true. Otherwise returns false and 
leaves the position unchanged. The
+        ///     span points into the internal buffer, so callers must parse it 
before any <c>await</c>.
+        /// </summary>
+        // Inlined so a buffered Read*ValueAsync compiles to a single 
check-and-advance.
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private bool TryReadSpan(int n, out ReadOnlySpan<byte> span)
+        {
+            if (_end - _start >= n)
+            {
+                span = _buffer.AsSpan(_start, n);
+                _start += n;
+                return true;
+            }
+
+            span = default;
+            return false;
+        }
+
+        /// <summary>
+        ///     Reads a single <see cref="byte"/>, returning it inline when 
already buffered. Throws
+        ///     <see cref="IOException"/> on end of stream to match the 
existing read contract.
+        /// </summary>
+        internal ValueTask<byte> ReadByteValueAsync(CancellationToken 
cancellationToken = default)
+        {
+            if (_end - _start >= 1)
+            {
+                var value = _buffer[_start];
+                _start += 1;
+                return new ValueTask<byte>(value);
+            }
+
+            return ReadByteValueSlowAsync(cancellationToken);
+        }
+
+        // Kept separate from the wider slow path because a byte EOF throws 
IOException, not
+        // EndOfStreamException (see the note above).
+        private async ValueTask<byte> ReadByteValueSlowAsync(CancellationToken 
cancellationToken)
+        {
+            await RefillAsync(cancellationToken).ConfigureAwait(false);
+            if (_start >= _end)
+            {
+                throw new IOException("Unexpected end of stream");
+            }
+            var value = _buffer[_start];
+            _start += 1;
+            return value;
+        }
+
+        /// <summary>
+        ///     Reads a big-endian <see cref="int"/>, returning it inline when 
already buffered.
+        /// </summary>
+        internal ValueTask<int> ReadIntValueAsync(CancellationToken 
cancellationToken = default)
+            => TryReadSpan(4, out var span)
+                ? new ValueTask<int>(BinaryPrimitives.ReadInt32BigEndian(span))
+                : ReadIntValueSlowAsync(cancellationToken);
+
+        private async ValueTask<int> ReadIntValueSlowAsync(CancellationToken 
cancellationToken)
+            => BinaryPrimitives.ReadInt32BigEndian(await FillSlowAsync(4, 
cancellationToken).ConfigureAwait(false));
+
+        /// <summary>
+        ///     Reads a big-endian <see cref="long"/>, returning it inline 
when already buffered.
+        /// </summary>
+        internal ValueTask<long> ReadLongValueAsync(CancellationToken 
cancellationToken = default)
+            => TryReadSpan(8, out var span)
+                ? new 
ValueTask<long>(BinaryPrimitives.ReadInt64BigEndian(span))
+                : ReadLongValueSlowAsync(cancellationToken);
+
+        private async ValueTask<long> ReadLongValueSlowAsync(CancellationToken 
cancellationToken)
+            => BinaryPrimitives.ReadInt64BigEndian(await FillSlowAsync(8, 
cancellationToken).ConfigureAwait(false));
+
+        /// <summary>
+        ///     Reads a big-endian <see cref="short"/>, returning it inline 
when already buffered.
+        /// </summary>
+        internal ValueTask<short> ReadShortValueAsync(CancellationToken 
cancellationToken = default)
+            => TryReadSpan(2, out var span)
+                ? new 
ValueTask<short>(BinaryPrimitives.ReadInt16BigEndian(span))
+                : ReadShortValueSlowAsync(cancellationToken);
+
+        private async ValueTask<short> 
ReadShortValueSlowAsync(CancellationToken cancellationToken)
+            => BinaryPrimitives.ReadInt16BigEndian(await FillSlowAsync(2, 
cancellationToken).ConfigureAwait(false));
+
+        /// <summary>
+        ///     Reads a big-endian <see cref="float"/>, returning it inline 
when already buffered.
+        /// </summary>
+        internal ValueTask<float> ReadFloatValueAsync(CancellationToken 
cancellationToken = default)
+            => TryReadSpan(4, out var span)
+                ? new 
ValueTask<float>(BinaryPrimitives.ReadSingleBigEndian(span))
+                : ReadFloatValueSlowAsync(cancellationToken);
+
+        private async ValueTask<float> 
ReadFloatValueSlowAsync(CancellationToken cancellationToken)
+            => BinaryPrimitives.ReadSingleBigEndian(await FillSlowAsync(4, 
cancellationToken).ConfigureAwait(false));
+
+        /// <summary>
+        ///     Reads a big-endian <see cref="double"/>, returning it inline 
when already buffered.
+        /// </summary>
+        internal ValueTask<double> ReadDoubleValueAsync(CancellationToken 
cancellationToken = default)
+            => TryReadSpan(8, out var span)
+                ? new 
ValueTask<double>(BinaryPrimitives.ReadDoubleBigEndian(span))
+                : ReadDoubleValueSlowAsync(cancellationToken);
+
+        private async ValueTask<double> 
ReadDoubleValueSlowAsync(CancellationToken cancellationToken)
+            => BinaryPrimitives.ReadDoubleBigEndian(await FillSlowAsync(8, 
cancellationToken).ConfigureAwait(false));
+
+        /// <summary>
+        ///     Shared slow path for the multi-byte readers. Reads exactly 
<paramref name="n"/> bytes into a
+        ///     small local buffer via <c>ReadExactlyAsync</c> (which goes 
through this buffer's async read
+        ///     path, so the cancellation token flows) and returns it for the 
caller to parse. Throws
+        ///     <see cref="EndOfStreamException"/> on a short stream.
+        /// </summary>
+        private async ValueTask<byte[]> FillSlowAsync(int n, CancellationToken 
cancellationToken)
+        {
+            var bytes = new byte[n];
+            await this.ReadExactlyAsync(bytes, 0, n, 
cancellationToken).ConfigureAwait(false);
+            return bytes;
+        }
+
+        /// <inheritdoc />
+        protected override void Dispose(bool disposing)
+        {
+            // Deliberately does not dispose the underlying stream. That 
stream is owned by
+            // StreamingResponseContext, and there is nothing else to release.
+        }
+
+        /// <inheritdoc />
+        public override ValueTask DisposeAsync()
+        {
+            // Deliberately does not dispose the underlying stream. See 
Dispose(bool).
+            return ValueTask.CompletedTask;
+        }
+    }
+}
diff --git 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ResponseSerializer.cs
 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ResponseSerializer.cs
index 295705c8e3..84963a4ddc 100644
--- 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ResponseSerializer.cs
+++ 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ResponseSerializer.cs
@@ -50,12 +50,12 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
             GraphBinaryReader reader,
             [EnumeratorCancellation] CancellationToken cancellationToken = 
default)
         {
-            // Wrap in BufferedStream for efficient reads (8192 bytes, like 
Go's bufio).
-            // Intentionally not disposed — disposing BufferedStream would 
also close the
-            // underlying stream, which is owned by StreamingResponseContext.
-            var buffered = new BufferedStream(stream, 8192);
+            // Wrap the response stream in a GraphBinaryReadBuffer so 
already-buffered reads
+            // complete synchronously. Not disposed here; the underlying 
stream is owned by
+            // StreamingResponseContext.
+            var buffered = new GraphBinaryReadBuffer(stream);
 
-            // 1. Version byte — validate MSB is set
+            // 1. Version byte, validate MSB is set
             var version = await 
buffered.ReadByteAsync(cancellationToken).ConfigureAwait(false) & 0xFF;
             if (version >> 7 != 1)
             {
diff --git 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs
index 8094146409..740554b9e9 100644
--- 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs
+++ 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs
@@ -53,8 +53,19 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
         /// <param name="stream">The <see cref="Stream"/> to read from.</param>
         /// <param name="cancellationToken">The token to cancel the operation. 
The default value is None.</param>
         /// <returns>The read <see cref="byte"/>.</returns>
-        public static async ValueTask<byte> ReadByteAsync(this Stream stream,
+        // Dispatch note that applies to every Read*Async below. The 
GraphBinary response reader wraps
+        // its stream in a GraphBinaryReadBuffer, whose typed readers return 
an already-buffered value
+        // synchronously. Any other stream (a MemoryStream in tests, or any 
external caller) takes the
+        // per-call async fallback, which allocates a small scratch buffer and 
reads exactly the
+        // requested bytes.
+        public static ValueTask<byte> ReadByteAsync(this Stream stream,
             CancellationToken cancellationToken = default)
+            => stream is GraphBinaryReadBuffer b
+                ? b.ReadByteValueAsync(cancellationToken)
+                : ReadByteFallbackAsync(stream, cancellationToken);
+
+        private static async ValueTask<byte> ReadByteFallbackAsync(Stream 
stream,
+            CancellationToken cancellationToken)
         {
             var readBuffer = new byte[1];
             var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(0, 1), 
cancellationToken)
@@ -84,12 +95,18 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
         /// <param name="stream">The <see cref="Stream"/> to read from.</param>
         /// <param name="cancellationToken">The token to cancel the operation. 
The default value is None.</param>
         /// <returns>The read <see cref="sbyte"/>.</returns>
-        public static async ValueTask<sbyte> ReadSByteAsync(this Stream stream,
+        public static ValueTask<sbyte> ReadSByteAsync(this Stream stream,
             CancellationToken cancellationToken = default)
         {
-            return (sbyte)await 
stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+            var byteTask = stream.ReadByteAsync(cancellationToken);
+            return byteTask.IsCompletedSuccessfully
+                ? new ValueTask<sbyte>((sbyte)byteTask.Result)
+                : ReadSByteFallbackAsync(byteTask);
         }
 
+        private static async ValueTask<sbyte> 
ReadSByteFallbackAsync(ValueTask<byte> byteTask)
+            => (sbyte)await byteTask.ConfigureAwait(false);
+
         /// <summary>
         ///     Asynchronously writes an <see cref="int"/> to a <see 
cref="Stream"/>.
         /// </summary>
@@ -110,12 +127,28 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
         /// <param name="stream">The <see cref="Stream"/> to read from.</param>
         /// <param name="cancellationToken">The token to cancel the operation. 
The default value is None.</param>
         /// <returns>The read <see cref="int"/>.</returns>
-        public static async ValueTask<int> ReadIntAsync(this Stream stream,
+        public static ValueTask<int> ReadIntAsync(this Stream stream,
             CancellationToken cancellationToken = default)
+            => stream is GraphBinaryReadBuffer b
+                ? b.ReadIntValueAsync(cancellationToken)
+                : ReadIntFallbackAsync(stream, cancellationToken);
+
+        private static async ValueTask<int> ReadIntFallbackAsync(Stream stream,
+            CancellationToken cancellationToken)
+            => BinaryPrimitives.ReadInt32BigEndian(
+                await FillFallbackAsync(stream, 4, 
cancellationToken).ConfigureAwait(false));
+
+        // Shared fallback filler for the multi-byte readers 
(int/long/short/float/double). Only reached
+        // when the stream is not a GraphBinaryReadBuffer; the dispatch branch 
above never touches this.
+        // Reads exactly n bytes and returns them for the caller to parse. EOF 
becomes an
+        // EndOfStreamException via ReadExactlyAsync, matching the buffered 
readers and deliberately
+        // different from ReadByteFallbackAsync, which throws IOException on 
EOF.
+        private static async ValueTask<byte[]> FillFallbackAsync(Stream 
stream, int n,
+            CancellationToken cancellationToken)
         {
-            var bytes = new byte[4];
-            await stream.ReadExactlyAsync(bytes, 0, 4, 
cancellationToken).ConfigureAwait(false);
-            return BinaryPrimitives.ReadInt32BigEndian(bytes);
+            var bytes = new byte[n];
+            await stream.ReadExactlyAsync(bytes, 0, n, 
cancellationToken).ConfigureAwait(false);
+            return bytes;
         }
 
         /// <summary>
@@ -138,13 +171,16 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
         /// <param name="stream">The <see cref="Stream"/> to read from.</param>
         /// <param name="cancellationToken">The token to cancel the operation. 
The default value is None.</param>
         /// <returns>The read <see cref="long"/>.</returns>
-        public static async ValueTask<long> ReadLongAsync(this Stream stream,
+        public static ValueTask<long> ReadLongAsync(this Stream stream,
             CancellationToken cancellationToken = default)
-        {
-            var bytes = new byte[8];
-            await stream.ReadExactlyAsync(bytes, 0, 8, 
cancellationToken).ConfigureAwait(false);
-            return BinaryPrimitives.ReadInt64BigEndian(bytes);
-        }
+            => stream is GraphBinaryReadBuffer b
+                ? b.ReadLongValueAsync(cancellationToken)
+                : ReadLongFallbackAsync(stream, cancellationToken);
+
+        private static async ValueTask<long> ReadLongFallbackAsync(Stream 
stream,
+            CancellationToken cancellationToken)
+            => BinaryPrimitives.ReadInt64BigEndian(
+                await FillFallbackAsync(stream, 8, 
cancellationToken).ConfigureAwait(false));
 
         /// <summary>
         ///     Asynchronously writes a <see cref="float"/> to a <see 
cref="Stream"/>.
@@ -166,13 +202,16 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
         /// <param name="stream">The <see cref="Stream"/> to read from.</param>
         /// <param name="cancellationToken">The token to cancel the operation. 
The default value is None.</param>
         /// <returns>The read <see cref="float"/>.</returns>
-        public static async ValueTask<float> ReadFloatAsync(this Stream stream,
+        public static ValueTask<float> ReadFloatAsync(this Stream stream,
             CancellationToken cancellationToken = default)
-        {
-            var bytes = new byte[4];
-            await stream.ReadExactlyAsync(bytes, 0, 4, 
cancellationToken).ConfigureAwait(false);
-            return BinaryPrimitives.ReadSingleBigEndian(bytes);
-        }
+            => stream is GraphBinaryReadBuffer b
+                ? b.ReadFloatValueAsync(cancellationToken)
+                : ReadFloatFallbackAsync(stream, cancellationToken);
+
+        private static async ValueTask<float> ReadFloatFallbackAsync(Stream 
stream,
+            CancellationToken cancellationToken)
+            => BinaryPrimitives.ReadSingleBigEndian(
+                await FillFallbackAsync(stream, 4, 
cancellationToken).ConfigureAwait(false));
 
         /// <summary>
         ///     Asynchronously writes a <see cref="double"/> to a <see 
cref="Stream"/>.
@@ -194,13 +233,16 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
         /// <param name="stream">The <see cref="Stream"/> to read from.</param>
         /// <param name="cancellationToken">The token to cancel the operation. 
The default value is None.</param>
         /// <returns>The read <see cref="double"/>.</returns>
-        public static async ValueTask<double> ReadDoubleAsync(this Stream 
stream,
+        public static ValueTask<double> ReadDoubleAsync(this Stream stream,
             CancellationToken cancellationToken = default)
-        {
-            var bytes = new byte[8];
-            await stream.ReadExactlyAsync(bytes, 0, 8, 
cancellationToken).ConfigureAwait(false);
-            return BinaryPrimitives.ReadDoubleBigEndian(bytes);
-        }
+            => stream is GraphBinaryReadBuffer b
+                ? b.ReadDoubleValueAsync(cancellationToken)
+                : ReadDoubleFallbackAsync(stream, cancellationToken);
+
+        private static async ValueTask<double> ReadDoubleFallbackAsync(Stream 
stream,
+            CancellationToken cancellationToken)
+            => BinaryPrimitives.ReadDoubleBigEndian(
+                await FillFallbackAsync(stream, 8, 
cancellationToken).ConfigureAwait(false));
 
         /// <summary>
         ///     Asynchronously writes a <see cref="short"/> to a <see 
cref="Stream"/>.
@@ -222,13 +264,16 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
         /// <param name="stream">The <see cref="Stream"/> to read from.</param>
         /// <param name="cancellationToken">The token to cancel the operation. 
The default value is None.</param>
         /// <returns>The read <see cref="short"/>.</returns>
-        public static async ValueTask<short> ReadShortAsync(this Stream stream,
+        public static ValueTask<short> ReadShortAsync(this Stream stream,
             CancellationToken cancellationToken = default)
-        {
-            var bytes = new byte[2];
-            await stream.ReadExactlyAsync(bytes, 0, 2, 
cancellationToken).ConfigureAwait(false);
-            return BinaryPrimitives.ReadInt16BigEndian(bytes);
-        }
+            => stream is GraphBinaryReadBuffer b
+                ? b.ReadShortValueAsync(cancellationToken)
+                : ReadShortFallbackAsync(stream, cancellationToken);
+
+        private static async ValueTask<short> ReadShortFallbackAsync(Stream 
stream,
+            CancellationToken cancellationToken)
+            => BinaryPrimitives.ReadInt16BigEndian(
+                await FillFallbackAsync(stream, 2, 
cancellationToken).ConfigureAwait(false));
 
         /// <summary>
         ///     Asynchronously writes a <see cref="bool"/> to a <see 
cref="Stream"/>.
@@ -248,18 +293,25 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
         /// <param name="stream">The <see cref="Stream"/> to read from.</param>
         /// <param name="cancellationToken">The token to cancel the operation. 
The default value is None.</param>
         /// <returns>The read <see cref="bool"/>.</returns>
-        public static async ValueTask<bool> ReadBoolAsync(this Stream stream,
+        public static ValueTask<bool> ReadBoolAsync(this Stream stream,
             CancellationToken cancellationToken = default)
         {
-            var b = await 
stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
-            return b switch
-            {
-                1 => true,
-                0 => false,
-                _ => throw new IOException($"Cannot read byte {b} as a 
boolean.")
-            };
+            var byteTask = stream.ReadByteAsync(cancellationToken);
+            return byteTask.IsCompletedSuccessfully
+                ? new ValueTask<bool>(BoolFromByte(byteTask.Result))
+                : ReadBoolFallbackAsync(byteTask);
         }
 
+        private static async ValueTask<bool> 
ReadBoolFallbackAsync(ValueTask<byte> byteTask)
+            => BoolFromByte(await byteTask.ConfigureAwait(false));
+
+        private static bool BoolFromByte(byte b) => b switch
+        {
+            1 => true,
+            0 => false,
+            _ => throw new IOException($"Cannot read byte {b} as a boolean.")
+        };
+
         /// <summary>
         ///     Asynchronously writes a <see cref="T:byte[]"/> to a <see 
cref="Stream"/>.
         /// </summary>
diff --git 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs
 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs
index 58723e77a2..7fbd157229 100644
--- 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs
+++ 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs
@@ -54,7 +54,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
         {
             var length = (int)await 
reader.ReadNonNullableValueAsync<int>(stream, 
cancellationToken).ConfigureAwait(false);
             var buffer = new byte[length];
-            await stream.ReadAsync(buffer, 0, length, 
cancellationToken).ConfigureAwait(false);
+            await stream.ReadExactlyAsync(buffer, 0, length, 
cancellationToken).ConfigureAwait(false);
             return buffer;
         }
     }
diff --git 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs
 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs
index 7ea3cb5cc5..57ae76ba1e 100644
--- 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs
+++ 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs
@@ -78,7 +78,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
             {
                 bytes = new byte[byteLength];
                 bytes[0] = firstByte;
-                await stream.ReadAsync(bytes, 1, byteLength - 1, 
cancellationToken).ConfigureAwait(false);
+                await stream.ReadExactlyAsync(bytes, 1, byteLength - 1, 
cancellationToken).ConfigureAwait(false);
             }
 
             return Encoding.UTF8.GetChars(bytes)[0];
diff --git 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs
 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs
index 73cce54aa5..ba51f2c290 100644
--- 
a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs
+++ 
b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs
@@ -55,7 +55,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
         {
             var length = (int)await 
reader.ReadNonNullableValueAsync<int>(stream, 
cancellationToken).ConfigureAwait(false);
             var bytes = new byte[length];
-            await stream.ReadAsync(bytes, 0, length, 
cancellationToken).ConfigureAwait(false);
+            await stream.ReadExactlyAsync(bytes, 0, length, 
cancellationToken).ConfigureAwait(false);
             return Encoding.UTF8.GetString(bytes);
         }
     }
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinaryReadBufferTests.cs
 
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinaryReadBufferTests.cs
new file mode 100644
index 0000000000..6dc242e35a
--- /dev/null
+++ 
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinaryReadBufferTests.cs
@@ -0,0 +1,1114 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Gremlin.Net.Structure.IO.GraphBinary4;
+using Xunit;
+
+namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary4
+{
+    public class GraphBinaryReadBufferTests
+    {
+        // --- Test doubles ---
+
+        /// <summary>
+        ///     A stream that returns at most <c>chunkSize</c> bytes per 
underlying ReadAsync call and always
+        ///     completes asynchronously, forcing the buffer to refill in 
small increments so that reads
+        ///     straddle refill boundaries.
+        /// </summary>
+        private sealed class DripStream : Stream
+        {
+            private readonly byte[] _data;
+            private readonly int _chunkSize;
+            private int _pos;
+
+            public DripStream(byte[] data, int chunkSize)
+            {
+                _data = data;
+                _chunkSize = chunkSize;
+            }
+
+            public override bool CanRead => true;
+            public override bool CanWrite => false;
+            public override bool CanSeek => false;
+            public override long Length => throw new NotSupportedException();
+            public override long Position
+            {
+                get => throw new NotSupportedException();
+                set => throw new NotSupportedException();
+            }
+
+            public override void Flush()
+            {
+            }
+
+            public override long Seek(long offset, SeekOrigin origin) => throw 
new NotSupportedException();
+            public override void SetLength(long value) => throw new 
NotSupportedException();
+            public override void Write(byte[] buffer, int offset, int count) 
=> throw new NotSupportedException();
+
+            public override int Read(byte[] buffer, int offset, int count)
+            {
+                var remaining = _data.Length - _pos;
+                if (remaining <= 0)
+                {
+                    return 0;
+                }
+                var toCopy = Math.Min(Math.Min(_chunkSize, count), remaining);
+                Array.Copy(_data, _pos, buffer, offset, toCopy);
+                _pos += toCopy;
+                return toCopy;
+            }
+
+            public override async ValueTask<int> ReadAsync(Memory<byte> buffer,
+                CancellationToken cancellationToken = default)
+            {
+                await Task.Yield();
+                cancellationToken.ThrowIfCancellationRequested();
+                var remaining = _data.Length - _pos;
+                if (remaining <= 0)
+                {
+                    return 0;
+                }
+                var toCopy = Math.Min(Math.Min(_chunkSize, buffer.Length), 
remaining);
+                _data.AsSpan(_pos, toCopy).CopyTo(buffer.Span);
+                _pos += toCopy;
+                return toCopy;
+            }
+        }
+
+        /// <summary>
+        ///     A stream whose ReadAsync never completes until the 
cancellation token trips.
+        /// </summary>
+        private sealed class StallingStream : Stream
+        {
+            public override bool CanRead => true;
+            public override bool CanWrite => false;
+            public override bool CanSeek => false;
+            public override long Length => throw new NotSupportedException();
+            public override long Position
+            {
+                get => throw new NotSupportedException();
+                set => throw new NotSupportedException();
+            }
+
+            public override void Flush()
+            {
+            }
+
+            public override long Seek(long offset, SeekOrigin origin) => throw 
new NotSupportedException();
+            public override void SetLength(long value) => throw new 
NotSupportedException();
+            public override void Write(byte[] buffer, int offset, int count) 
=> throw new NotSupportedException();
+
+            public override int Read(byte[] buffer, int offset, int count) => 
throw new NotSupportedException();
+
+            public override async ValueTask<int> ReadAsync(Memory<byte> buffer,
+                CancellationToken cancellationToken = default)
+            {
+                await Task.Delay(Timeout.Infinite, 
cancellationToken).ConfigureAwait(false);
+                return 0;
+            }
+        }
+
+        /// <summary>
+        ///     A stream that records whether Dispose was called; used to 
prove GraphBinaryReadBuffer does not
+        ///     dispose the underlying stream.
+        /// </summary>
+        private sealed class ObservableStream : Stream
+        {
+            private readonly MemoryStream _inner;
+            public bool Disposed { get; private set; }
+
+            public ObservableStream(byte[] data)
+            {
+                _inner = new MemoryStream(data);
+            }
+
+            public override bool CanRead => true;
+            public override bool CanWrite => false;
+            public override bool CanSeek => false;
+            public override long Length => throw new NotSupportedException();
+            public override long Position
+            {
+                get => throw new NotSupportedException();
+                set => throw new NotSupportedException();
+            }
+
+            public override void Flush()
+            {
+            }
+
+            public override long Seek(long offset, SeekOrigin origin) => throw 
new NotSupportedException();
+            public override void SetLength(long value) => throw new 
NotSupportedException();
+            public override void Write(byte[] buffer, int offset, int count) 
=> throw new NotSupportedException();
+
+            public override int Read(byte[] buffer, int offset, int count) => 
_inner.Read(buffer, offset, count);
+
+            public override ValueTask<int> ReadAsync(Memory<byte> buffer,
+                CancellationToken cancellationToken = default) => 
_inner.ReadAsync(buffer, cancellationToken);
+
+            protected override void Dispose(bool disposing)
+            {
+                Disposed = true;
+                _inner.Dispose();
+                base.Dispose(disposing);
+            }
+        }
+
+        /// <summary>
+        ///     A stream whose ReadAsync always throws. Used to prove that a 
failed underlying refill
+        ///     surfaces the exception cleanly and does not leave the buffer 
in a half-consumed state.
+        /// </summary>
+        private sealed class ThrowingStream : Stream
+        {
+            public override bool CanRead => true;
+            public override bool CanWrite => false;
+            public override bool CanSeek => false;
+            public override long Length => throw new NotSupportedException();
+            public override long Position
+            {
+                get => throw new NotSupportedException();
+                set => throw new NotSupportedException();
+            }
+
+            public override void Flush()
+            {
+            }
+
+            public override long Seek(long offset, SeekOrigin origin) => throw 
new NotSupportedException();
+            public override void SetLength(long value) => throw new 
NotSupportedException();
+            public override void Write(byte[] buffer, int offset, int count) 
=> throw new NotSupportedException();
+
+            public override int Read(byte[] buffer, int offset, int count) => 
throw new NotSupportedException();
+
+            public override ValueTask<int> ReadAsync(Memory<byte> buffer,
+                CancellationToken cancellationToken = default)
+                => throw new IOException("simulated underlying stream 
failure");
+        }
+
+        private static GraphBinaryReadBuffer NewBuffer(byte[] data)
+            => new GraphBinaryReadBuffer(new MemoryStream(data));
+
+        // --- 1. Already-buffered reads (no I/O) ---
+
+        [Fact]
+        public async Task ReadByteValueShouldReturnCorrectValueOnHitPath()
+        {
+            var buffer = NewBuffer(new byte[] { 0xAB });
+            Assert.Equal(0xAB, await buffer.ReadByteValueAsync());
+        }
+
+        [Fact]
+        public async Task 
ReadIntValueShouldReturnCorrectBigEndianValueOnHitPath()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02, 0x03, 0x04 });
+            Assert.Equal(0x01020304, await buffer.ReadIntValueAsync());
+        }
+
+        [Fact]
+        public async Task ReadIntValueShouldHandleAllOnesOnHitPath()
+        {
+            var buffer = NewBuffer(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF });
+            Assert.Equal(-1, await buffer.ReadIntValueAsync());
+        }
+
+        [Fact]
+        public async Task 
ReadLongValueShouldReturnCorrectBigEndianValueOnHitPath()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 
0x06, 0x07, 0x08 });
+            Assert.Equal(0x0102030405060708L, await 
buffer.ReadLongValueAsync());
+        }
+
+        [Fact]
+        public async Task ReadLongValueShouldHandleAllOnesOnHitPath()
+        {
+            var buffer = NewBuffer(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
0xFF, 0xFF, 0xFF });
+            Assert.Equal(-1L, await buffer.ReadLongValueAsync());
+        }
+
+        [Fact]
+        public async Task 
ReadShortValueShouldReturnCorrectBigEndianValueOnHitPath()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02 });
+            Assert.Equal((short)0x0102, await buffer.ReadShortValueAsync());
+        }
+
+        [Fact]
+        public async Task ReadShortValueShouldHandleAllOnesOnHitPath()
+        {
+            var buffer = NewBuffer(new byte[] { 0xFF, 0xFF });
+            Assert.Equal((short)-1, await buffer.ReadShortValueAsync());
+        }
+
+        [Fact]
+        public async Task 
ReadFloatValueShouldReturnCorrectBigEndianValueOnHitPath()
+        {
+            var buffer = NewBuffer(new byte[] { 0x3F, 0x80, 0x00, 0x00 }); // 
1.0f
+            Assert.Equal(1.0f, await buffer.ReadFloatValueAsync());
+        }
+
+        [Fact]
+        public async Task 
ReadDoubleValueShouldReturnCorrectBigEndianValueOnHitPath()
+        {
+            var buffer = NewBuffer(new byte[] { 0x3F, 0xF0, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00 }); // 1.0d
+            Assert.Equal(1.0d, await buffer.ReadDoubleValueAsync());
+        }
+
+        // --- 2. Boundary path: a value spans a refill boundary ---
+
+        private static byte[] IntBytes(int value)
+        {
+            var b = new byte[4];
+            BinaryPrimitives.WriteInt32BigEndian(b, value);
+            return b;
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(5)]
+        [InlineData(7)]
+        public async Task 
ReadIntValueShouldBeCorrectAcrossRefillViaDripStream(int chunkSize)
+        {
+            var buffer = new GraphBinaryReadBuffer(new 
DripStream(IntBytes(0x0A0B0C0D), chunkSize));
+            Assert.Equal(0x0A0B0C0D, await buffer.ReadIntValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(5)]
+        [InlineData(7)]
+        public async Task 
ReadIntValueShouldBeCorrectAcrossRefillViaTinyBuffer(int bufferSize)
+        {
+            var buffer = new GraphBinaryReadBuffer(new 
MemoryStream(IntBytes(0x0A0B0C0D)), bufferSize);
+            Assert.Equal(0x0A0B0C0D, await buffer.ReadIntValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(5)]
+        [InlineData(7)]
+        public async Task ReadLongValueShouldBeCorrectAcrossRefill(int 
chunkSize)
+        {
+            var data = new byte[8];
+            BinaryPrimitives.WriteInt64BigEndian(data, 0x1122334455667788L);
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 
chunkSize));
+            Assert.Equal(0x1122334455667788L, await 
buffer.ReadLongValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(5)]
+        [InlineData(7)]
+        public async Task ReadDoubleValueShouldBeCorrectAcrossRefill(int 
chunkSize)
+        {
+            var data = new byte[8];
+            BinaryPrimitives.WriteDoubleBigEndian(data, 3.14159265358979);
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 
chunkSize));
+            Assert.Equal(3.14159265358979, await 
buffer.ReadDoubleValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        public async Task ReadShortValueShouldBeCorrectAcrossRefill(int 
chunkSize)
+        {
+            var data = new byte[2];
+            BinaryPrimitives.WriteInt16BigEndian(data, 
unchecked((short)0xBEEF));
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 
chunkSize));
+            Assert.Equal(unchecked((short)0xBEEF), await 
buffer.ReadShortValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        public async Task ReadFloatValueShouldBeCorrectAcrossRefill(int 
chunkSize)
+        {
+            var data = new byte[4];
+            BinaryPrimitives.WriteSingleBigEndian(data, 3.14f);
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 
chunkSize));
+            Assert.Equal(3.14f, await buffer.ReadFloatValueAsync());
+        }
+
+        [Fact]
+        public async Task 
ReadByteValueShouldBeCorrectAcrossRefillViaTinyBuffer()
+        {
+            // A one-byte value read after buffer is exhausted forces the slow 
path.
+            var buffer = new GraphBinaryReadBuffer(new DripStream(new byte[] { 
0x11, 0x22 }, 1), 1);
+            Assert.Equal(0x11, await buffer.ReadByteValueAsync());
+            Assert.Equal(0x22, await buffer.ReadByteValueAsync());
+        }
+
+        // --- 3. Mixed-width record crossing refill boundaries ---
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(5)]
+        [InlineData(7)]
+        public async Task MixedWidthRecordShouldReassembleAcrossRefills(int 
chunkSize)
+        {
+            // Lay out int, long, byte, short, float, double back-to-back.
+            using var ms = new MemoryStream();
+            var scratch = new byte[8];
+
+            BinaryPrimitives.WriteInt32BigEndian(scratch.AsSpan(0, 4), 
-123456);
+            ms.Write(scratch, 0, 4);
+
+            BinaryPrimitives.WriteInt64BigEndian(scratch, 0x0102030405060708L);
+            ms.Write(scratch, 0, 8);
+
+            ms.WriteByte(0xC3);
+
+            BinaryPrimitives.WriteInt16BigEndian(scratch.AsSpan(0, 2), 
unchecked((short)0xABCD));
+            ms.Write(scratch, 0, 2);
+
+            BinaryPrimitives.WriteSingleBigEndian(scratch.AsSpan(0, 4), 
2.71828f);
+            ms.Write(scratch, 0, 4);
+
+            BinaryPrimitives.WriteDoubleBigEndian(scratch, 1.41421356237);
+            ms.Write(scratch, 0, 8);
+
+            var data = ms.ToArray();
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 
chunkSize), 4);
+
+            Assert.Equal(-123456, await buffer.ReadIntValueAsync());
+            Assert.Equal(0x0102030405060708L, await 
buffer.ReadLongValueAsync());
+            Assert.Equal(0xC3, await buffer.ReadByteValueAsync());
+            Assert.Equal(unchecked((short)0xABCD), await 
buffer.ReadShortValueAsync());
+            Assert.Equal(2.71828f, await buffer.ReadFloatValueAsync());
+            Assert.Equal(1.41421356237, await buffer.ReadDoubleValueAsync());
+        }
+
+        // --- 4. EOF contract ---
+
+        [Fact]
+        public async Task ReadByteValueShouldThrowIOExceptionOnEmptyStream()
+        {
+            var buffer = NewBuffer(Array.Empty<byte>());
+            await Assert.ThrowsAsync<IOException>(async () => await 
buffer.ReadByteValueAsync());
+        }
+
+        [Fact]
+        public async Task ReadIntValueShouldThrowEndOfStreamOnShortStream()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02, 0x03 });
+            await Assert.ThrowsAsync<EndOfStreamException>(async () => await 
buffer.ReadIntValueAsync());
+        }
+
+        [Fact]
+        public async Task ReadLongValueShouldThrowEndOfStreamOnShortStream()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 
0x06, 0x07 });
+            await Assert.ThrowsAsync<EndOfStreamException>(async () => await 
buffer.ReadLongValueAsync());
+        }
+
+        [Fact]
+        public async Task ReadShortValueShouldThrowEndOfStreamOnShortStream()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01 });
+            await Assert.ThrowsAsync<EndOfStreamException>(async () => await 
buffer.ReadShortValueAsync());
+        }
+
+        [Fact]
+        public async Task ReadFloatValueShouldThrowEndOfStreamOnShortStream()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02 });
+            await Assert.ThrowsAsync<EndOfStreamException>(async () => await 
buffer.ReadFloatValueAsync());
+        }
+
+        [Fact]
+        public async Task ReadDoubleValueShouldThrowEndOfStreamOnShortStream()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 
});
+            await Assert.ThrowsAsync<EndOfStreamException>(async () => await 
buffer.ReadDoubleValueAsync());
+        }
+
+        // --- 5. General ReadAsync path across refills ---
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(5)]
+        [InlineData(7)]
+        public async Task 
ReadAsyncCountShouldReturnCorrectBytesAcrossRefills(int chunkSize)
+        {
+            var expected = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 
0x03, 0x04, 0x05, 0x06 };
+            var buffer = new GraphBinaryReadBuffer(new DripStream(expected, 
chunkSize), 4);
+
+            var result = await buffer.ReadAsync(expected.Length);
+
+            Assert.Equal(expected, result);
+        }
+
+        [Fact]
+        public async Task ReadAsyncCountShouldConsumeAfterPrimitiveReads()
+        {
+            using var ms = new MemoryStream();
+            var scratch = new byte[4];
+            BinaryPrimitives.WriteInt32BigEndian(scratch, 42);
+            ms.Write(scratch, 0, 4);
+            ms.Write(new byte[] { 0x0A, 0x0B, 0x0C }, 0, 3);
+
+            var buffer = new GraphBinaryReadBuffer(new 
DripStream(ms.ToArray(), 2), 3);
+
+            Assert.Equal(42, await buffer.ReadIntValueAsync());
+            Assert.Equal(new byte[] { 0x0A, 0x0B, 0x0C }, await 
buffer.ReadAsync(3));
+        }
+
+        [Fact]
+        public async Task 
ReadAsyncByteArrayShouldFillFromNonZeroOffsetAcrossRefills()
+        {
+            // Exercises the ReadAsync(byte[], offset, count) overload with a 
non-zero offset: the leading
+            // guard byte must be left intact and the payload written starting 
at index 1, reassembled across
+            // refills.
+            var payload = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
+            var buffer = new GraphBinaryReadBuffer(new DripStream(payload, 2), 
2);
+
+            var dest = new byte[payload.Length + 1];
+            dest[0] = 0xEE;
+            var offset = 1;
+            while (offset < dest.Length)
+            {
+                var n = await buffer.ReadAsync(dest, offset, dest.Length - 
offset, CancellationToken.None);
+                Assert.True(n > 0);
+                offset += n;
+            }
+
+            Assert.Equal(new byte[] { 0xEE, 0x11, 0x22, 0x33, 0x44, 0x55 }, 
dest);
+        }
+
+        // --- 6. Cancellation ---
+
+        [Fact]
+        public async Task PrimitiveBoundaryReadShouldHonorCancellation()
+        {
+            using var cts = new CancellationTokenSource();
+            var buffer = new GraphBinaryReadBuffer(new StallingStream());
+            var task = buffer.ReadIntValueAsync(cts.Token);
+            cts.Cancel();
+            await Assert.ThrowsAnyAsync<OperationCanceledException>(async () 
=> await task);
+        }
+
+        [Fact]
+        public async Task ReadAsyncCountShouldHonorCancellation()
+        {
+            using var cts = new CancellationTokenSource();
+            var buffer = new GraphBinaryReadBuffer(new StallingStream());
+            var task = buffer.ReadAsync(4, cts.Token);
+            cts.Cancel();
+            await Assert.ThrowsAnyAsync<OperationCanceledException>(async () 
=> await task);
+        }
+
+        // --- 7. Dispose does not dispose the underlying stream ---
+
+        [Fact]
+        public void DisposeShouldNotDisposeUnderlyingStream()
+        {
+            var underlying = new ObservableStream(new byte[] { 0x01, 0x02, 
0x03, 0x04 });
+            var buffer = new GraphBinaryReadBuffer(underlying);
+
+            buffer.Dispose();
+
+            Assert.False(underlying.Disposed);
+        }
+
+        [Fact]
+        public async Task DisposeAsyncShouldNotDisposeUnderlyingStream()
+        {
+            var underlying = new ObservableStream(new byte[] { 0x01, 0x02, 
0x03, 0x04 });
+            var buffer = new GraphBinaryReadBuffer(underlying);
+
+            await buffer.DisposeAsync();
+
+            Assert.False(underlying.Disposed);
+        }
+
+        // --- 8. Public StreamExtensions dispatch onto GraphBinaryReadBuffer 
---
+        //
+        // These call the public extension methods, which internally check 
`stream is GraphBinaryReadBuffer`
+        // and forward to the corresponding typed reader. A cross-wired 
dispatch (say, ReadShortAsync
+        // routing to ReadIntValueAsync) would surface here. Each width uses a 
distinctive value so a
+        // cross-wire produces a wrong result instead of a coincidental match.
+
+        [Fact]
+        public async Task PublicReadByteAsyncShouldDispatchToByteReader()
+        {
+            var buffer = NewBuffer(new byte[] { 0xAB });
+            Assert.Equal(0xAB, await buffer.ReadByteAsync());
+        }
+
+        [Fact]
+        public async Task PublicReadSByteAsyncShouldDispatchToByteReader()
+        {
+            var buffer = NewBuffer(new byte[] { 0xFF });
+            Assert.Equal((sbyte)-1, await buffer.ReadSByteAsync());
+        }
+
+        [Fact]
+        public async Task PublicReadBoolAsyncShouldDispatchToByteReader()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01 });
+            Assert.True(await buffer.ReadBoolAsync());
+        }
+
+        [Fact]
+        public async Task PublicReadShortAsyncShouldDispatchToShortReader()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02 });
+            Assert.Equal((short)0x0102, await buffer.ReadShortAsync());
+        }
+
+        [Fact]
+        public async Task PublicReadIntAsyncShouldDispatchToIntReader()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02, 0x03, 0x04 });
+            Assert.Equal(0x01020304, await buffer.ReadIntAsync());
+        }
+
+        [Fact]
+        public async Task PublicReadLongAsyncShouldDispatchToLongReader()
+        {
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 
0x06, 0x07, 0x08 });
+            Assert.Equal(0x0102030405060708L, await buffer.ReadLongAsync());
+        }
+
+        [Fact]
+        public async Task PublicReadFloatAsyncShouldDispatchToFloatReader()
+        {
+            var buffer = NewBuffer(new byte[] { 0x3F, 0x80, 0x00, 0x00 }); // 
1.0f
+            Assert.Equal(1.0f, await buffer.ReadFloatAsync());
+        }
+
+        [Fact]
+        public async Task PublicReadDoubleAsyncShouldDispatchToDoubleReader()
+        {
+            var buffer = NewBuffer(new byte[] { 0x3F, 0xF0, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00 }); // 1.0d
+            Assert.Equal(1.0d, await buffer.ReadDoubleAsync());
+        }
+
+        // --- 9. Explicit partial-prefix boundary: k of N bytes buffered, N-k 
need a refill ---
+        //
+        // For each split point k (1..N-1) we prime the buffer so that a 
leading single-byte read leaves
+        // exactly k bytes of the target value already buffered, and the 
remaining N-k bytes must be
+        // fetched from the underlying (drip) stream. This exercises the slow 
path at every possible
+        // split, plus a matching short-stream case that must throw 
EndOfStreamException.
+
+        // Buffer size is chosen so that (1 leading byte + k value bytes) fit 
before a refill. The
+        // leading 0xEE marker is consumed first, leaving k value bytes 
buffered and N-k to refill.
+        private static byte[] PrefixedPrimitive(byte[] primitive)
+        {
+            var result = new byte[primitive.Length + 1];
+            result[0] = 0xEE;
+            Array.Copy(primitive, 0, result, 1, primitive.Length);
+            return result;
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(2)]
+        [InlineData(3)]
+        public async Task ReadShortValueShouldSplitAtEveryBoundary(int 
bufferSize)
+        {
+            var primitive = new byte[2];
+            BinaryPrimitives.WriteInt16BigEndian(primitive, 
unchecked((short)0xBEEF));
+            var data = PrefixedPrimitive(primitive);
+            // Drip one byte at a time to guarantee refills land mid-value.
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 1), 
bufferSize);
+
+            Assert.Equal(0xEE, await buffer.ReadByteValueAsync());
+            Assert.Equal(unchecked((short)0xBEEF), await 
buffer.ReadShortValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(2)]
+        [InlineData(3)]
+        [InlineData(4)]
+        [InlineData(5)]
+        public async Task ReadIntValueShouldSplitAtEveryBoundary(int 
bufferSize)
+        {
+            var data = PrefixedPrimitive(IntBytes(0x0A0B0C0D));
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 1), 
bufferSize);
+
+            Assert.Equal(0xEE, await buffer.ReadByteValueAsync());
+            Assert.Equal(0x0A0B0C0D, await buffer.ReadIntValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(5)]
+        [InlineData(7)]
+        [InlineData(9)]
+        public async Task ReadLongValueShouldSplitAtEveryBoundary(int 
bufferSize)
+        {
+            var primitive = new byte[8];
+            BinaryPrimitives.WriteInt64BigEndian(primitive, 
0x1122334455667788L);
+            var data = PrefixedPrimitive(primitive);
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 1), 
bufferSize);
+
+            Assert.Equal(0xEE, await buffer.ReadByteValueAsync());
+            Assert.Equal(0x1122334455667788L, await 
buffer.ReadLongValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(5)]
+        public async Task ReadFloatValueShouldSplitAtEveryBoundary(int 
bufferSize)
+        {
+            var primitive = new byte[4];
+            BinaryPrimitives.WriteSingleBigEndian(primitive, 2.71828f);
+            var data = PrefixedPrimitive(primitive);
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 1), 
bufferSize);
+
+            Assert.Equal(0xEE, await buffer.ReadByteValueAsync());
+            Assert.Equal(2.71828f, await buffer.ReadFloatValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(5)]
+        [InlineData(7)]
+        [InlineData(9)]
+        public async Task ReadDoubleValueShouldSplitAtEveryBoundary(int 
bufferSize)
+        {
+            var primitive = new byte[8];
+            BinaryPrimitives.WriteDoubleBigEndian(primitive, 1.41421356237);
+            var data = PrefixedPrimitive(primitive);
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 1), 
bufferSize);
+
+            Assert.Equal(0xEE, await buffer.ReadByteValueAsync());
+            Assert.Equal(1.41421356237, await buffer.ReadDoubleValueAsync());
+        }
+
+        // Partial value then EOF: a leading byte is consumed, then only part 
of the value is available
+        // before the stream ends, so the slow path throws 
EndOfStreamException.
+
+        [Theory]
+        [InlineData(1)] // 1 of 4 int bytes present after the leading byte
+        [InlineData(2)]
+        [InlineData(3)]
+        public async Task 
ReadIntValueShouldThrowEndOfStreamOnPartialAfterPrefix(int 
primitiveBytesPresent)
+        {
+            var data = new byte[1 + primitiveBytesPresent];
+            data[0] = 0xEE;
+            for (var i = 0; i < primitiveBytesPresent; i++)
+            {
+                data[i + 1] = (byte)(i + 1);
+            }
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 1), 2);
+
+            Assert.Equal(0xEE, await buffer.ReadByteValueAsync());
+            await Assert.ThrowsAsync<EndOfStreamException>(async () => await 
buffer.ReadIntValueAsync());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(4)]
+        [InlineData(7)] // 7 of 8 long bytes present after the leading byte
+        public async Task 
ReadLongValueShouldThrowEndOfStreamOnPartialAfterPrefix(int 
primitiveBytesPresent)
+        {
+            var data = new byte[1 + primitiveBytesPresent];
+            data[0] = 0xEE;
+            for (var i = 0; i < primitiveBytesPresent; i++)
+            {
+                data[i + 1] = (byte)(i + 1);
+            }
+            var buffer = new GraphBinaryReadBuffer(new DripStream(data, 1), 2);
+
+            Assert.Equal(0xEE, await buffer.ReadByteValueAsync());
+            await Assert.ThrowsAsync<EndOfStreamException>(async () => await 
buffer.ReadLongValueAsync());
+        }
+
+        // --- 10. General ReadAsync(count) delivering fewer than count bytes 
then EOF ---
+        //
+        // Proves the refill loop inside ReadExactlyAsync terminates (no hang 
/ infinite loop) and
+        // surfaces EndOfStreamException when the underlying stream ends early.
+
+        [Fact]
+        public async Task 
ReadAsyncCountShouldThrowEndOfStreamWhenStreamEndsEarly()
+        {
+            // Only 3 bytes available but 8 requested; DripStream returns 0 
after exhausting its data.
+            var buffer = new GraphBinaryReadBuffer(new DripStream(new byte[] { 
0x01, 0x02, 0x03 }, 1), 2);
+            await Assert.ThrowsAsync<EndOfStreamException>(async () => await 
buffer.ReadAsync(8));
+        }
+
+        // --- 11. Pre-cancelled token on a refill-forcing read ---
+        //
+        // With an already-cancelled token, a read whose buffer is empty must 
observe the cancellation
+        // via the underlying ReadAsync and throw OperationCanceledException.
+        //
+        // Note: when the bytes are already buffered, the typed reader 
intentionally does not observe
+        // the cancellation token. It does no I/O and returns synchronously. 
That behavior is pinned by
+        // FullyBufferedFastPathShouldNotObservePreCancelledToken below.
+
+        [Fact]
+        public async Task PreCancelledTokenShouldThrowWhenRefillRequired()
+        {
+            using var cts = new CancellationTokenSource();
+            cts.Cancel();
+            // Empty internal buffer forces a refill, which must observe the 
cancelled token.
+            var buffer = new GraphBinaryReadBuffer(new DripStream(new byte[] { 
0x01, 0x02, 0x03, 0x04 }, 1));
+            await Assert.ThrowsAnyAsync<OperationCanceledException>(
+                async () => await buffer.ReadIntValueAsync(cts.Token));
+        }
+
+        [Fact]
+        public async Task 
FullyBufferedFastPathShouldNotObservePreCancelledToken()
+        {
+            using var cts = new CancellationTokenSource();
+            // Prime the internal buffer with a first (uncancelled) read: this 
refill pulls all 8 bytes
+            // from the MemoryStream into the default 8192-byte buffer.
+            var buffer = NewBuffer(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 
0x06, 0x07, 0x08 });
+            Assert.Equal(0x01020304, await buffer.ReadIntValueAsync());
+
+            // The remaining 4 bytes are already buffered, so the second read 
returns synchronously
+            // without any I/O and does not observe the cancelled token. This 
is intentional; see the
+            // note above.
+            cts.Cancel();
+            Assert.Equal(0x05060708, await 
buffer.ReadIntValueAsync(cts.Token));
+        }
+
+        // --- 12. End-to-end drip through ResponseSerializer over a 
non-seekable fragmented stream ---
+        //
+        // Mirrors ResponseSerializerTests payload construction, then replays 
the same bytes through a
+        // DripStream so the response arrives in tiny chunks. This proves the 
buffering wrapper works
+        // end-to-end on a fragmented, non-seekable stream (something no other 
test exercises).
+
+        private static byte[] BuildNonBulkedIntResponse(params int[] values)
+        {
+            using var ms = new MemoryStream();
+            ms.WriteByte(0x84); // version
+            ms.WriteByte(0x00); // bulked = false
+            var scratch = new byte[4];
+            foreach (var value in values)
+            {
+                ms.WriteByte(0x01); // DataType.Int
+                ms.WriteByte(0x00); // value_flag = not null
+                BinaryPrimitives.WriteInt32BigEndian(scratch, value);
+                ms.Write(scratch, 0, 4);
+            }
+            // Marker
+            ms.WriteByte(0xFD);
+            ms.WriteByte(0x00);
+            ms.WriteByte(0x00);
+            // Status footer: 200, null message, null exception
+            BinaryPrimitives.WriteInt32BigEndian(scratch, 200);
+            ms.Write(scratch, 0, 4);
+            ms.WriteByte(0x01);
+            ms.WriteByte(0x01);
+            return ms.ToArray();
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(7)]
+        public async Task 
ResponseSerializerShouldDeserializeAcrossFragmentedDripStream(int chunkSize)
+        {
+            var payload = BuildNonBulkedIntResponse(1, 2, 3, 42, -7, 
int.MaxValue, int.MinValue);
+            var reader = new GraphBinaryReader();
+            var serializer = new ResponseSerializer();
+
+            // Baseline: full (non-dripped) MemoryStream.
+            var baseline = new List<object>();
+            await foreach (var item in serializer.ReadStreamingAsync(new 
MemoryStream(payload), reader))
+            {
+                baseline.Add(item);
+            }
+
+            // Fragmented: same bytes delivered chunkSize at a time through a 
non-seekable stream.
+            var dripped = new List<object>();
+            await foreach (var item in serializer.ReadStreamingAsync(new 
DripStream(payload, chunkSize), reader))
+            {
+                dripped.Add(item);
+            }
+
+            Assert.Equal(new object[] { 1, 2, 3, 42, -7, int.MaxValue, 
int.MinValue }, baseline);
+            Assert.Equal(baseline, dripped);
+        }
+
+        // --- 13. Multi-byte value straddling a refill boundary (short-read 
regression guard) ---
+        //
+        // A general Read/ReadAsync legitimately returns only what is 
currently buffered, so a value read
+        // (e.g. a string) that straddles the internal refill boundary comes 
back in more than one chunk.
+        // Consumers that issue a single non-looping stream.ReadAsync(bytes, 
offset, count) and ignore the
+        // returned count would silently truncate the value and desynchronize 
the parser. These tests build
+        // a real GraphBinary String response whose payload crosses the buffer 
boundary and assert it still
+        // round-trips end-to-end through ResponseSerializer.
+
+        private static byte[] BuildNonBulkedStringResponse(params string[] 
values)
+        {
+            using var ms = new MemoryStream();
+            ms.WriteByte(0x84); // version
+            ms.WriteByte(0x00); // bulked = false
+            var scratch = new byte[4];
+            foreach (var value in values)
+            {
+                ms.WriteByte(0x03); // DataType.String
+                ms.WriteByte(0x00); // value_flag = not null
+                var utf8 = Encoding.UTF8.GetBytes(value);
+                BinaryPrimitives.WriteInt32BigEndian(scratch, utf8.Length);
+                ms.Write(scratch, 0, 4);
+                ms.Write(utf8, 0, utf8.Length);
+            }
+            // Marker
+            ms.WriteByte(0xFD);
+            ms.WriteByte(0x00);
+            ms.WriteByte(0x00);
+            // Status footer: 200, null message, null exception
+            BinaryPrimitives.WriteInt32BigEndian(scratch, 200);
+            ms.Write(scratch, 0, 4);
+            ms.WriteByte(0x01);
+            ms.WriteByte(0x01);
+            return ms.ToArray();
+        }
+
+        [Theory]
+        [InlineData(4)]   // chunk far smaller than the string payload
+        [InlineData(8)]
+        [InlineData(16)]
+        public async Task 
ResponseSerializerShouldDeserializeStringLargerThanBuffer(int chunkSize)
+        {
+            // A string whose UTF-8 length far exceeds the drip chunk size 
forces the payload to span
+            // multiple refills. Feed it through a DripStream (not a 
pre-wrapped GraphBinaryReadBuffer)
+            // so the chunk size directly controls the refill granularity of 
the buffer that
+            // ResponseSerializer creates internally.
+            var value = new string('x', 100);
+            var payload = BuildNonBulkedStringResponse(value);
+            var reader = new GraphBinaryReader();
+            var serializer = new ResponseSerializer();
+
+            var results = new List<object>();
+            await foreach (var item in serializer.ReadStreamingAsync(
+                new DripStream(payload, chunkSize), reader))
+            {
+                results.Add(item);
+            }
+
+            Assert.Equal(new object[] { value }, results);
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(7)]
+        public async Task 
ResponseSerializerShouldDeserializeStringsAcrossFragmentedDripStream(int 
chunkSize)
+        {
+            // Multiple strings of varying lengths, delivered a few bytes at a 
time through a non-seekable
+            // stream, so almost every value straddles a refill boundary.
+            var values = new[] { "", "a", "hello world", new string('z', 50), 
"τ" };
+            var payload = BuildNonBulkedStringResponse(values);
+            var reader = new GraphBinaryReader();
+            var serializer = new ResponseSerializer();
+
+            var results = new List<object>();
+            await foreach (var item in serializer.ReadStreamingAsync(new 
DripStream(payload, chunkSize), reader))
+            {
+                results.Add(item);
+            }
+
+            Assert.Equal(values, results);
+        }
+
+        // CharSerializer reads a multi-byte UTF-8 char as a leading byte plus 
ReadExactlyAsync for the
+        // rest. This is a distinct code path (type code 0x80) from the 
length-prefixed StringSerializer.
+        // With a 1-byte drip the continuation bytes land after a refill, 
pinning that the read loops
+        // correctly.
+        private static byte[] BuildNonBulkedCharResponse(params char[] values)
+        {
+            using var ms = new MemoryStream();
+            ms.WriteByte(0x84); // version
+            ms.WriteByte(0x00); // bulked = false
+            var scratch = new byte[4];
+            foreach (var value in values)
+            {
+                ms.WriteByte(0x80); // DataType.Char
+                ms.WriteByte(0x00); // value_flag = not null
+                var utf8 = Encoding.UTF8.GetBytes(value.ToString());
+                ms.Write(utf8, 0, utf8.Length); // Char has no length prefix; 
leading byte encodes width
+            }
+            ms.WriteByte(0xFD);
+            ms.WriteByte(0x00);
+            ms.WriteByte(0x00);
+            BinaryPrimitives.WriteInt32BigEndian(scratch, 200);
+            ms.Write(scratch, 0, 4);
+            ms.WriteByte(0x01);
+            ms.WriteByte(0x01);
+            return ms.ToArray();
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(2)]
+        [InlineData(3)]
+        public async Task 
ResponseSerializerShouldDeserializeMultiByteCharsAcrossRefills(int chunkSize)
+        {
+            // 'a' (1 byte), 'τ' (2 bytes, U+03C4), '€' (3 bytes, U+20AC). 
Exercises the 2- and 3-byte
+            // CharSerializer branches whose continuation bytes straddle a 
refill under a tiny drip chunk.
+            var values = new[] { 'a', 'τ', '€' };
+            var payload = BuildNonBulkedCharResponse(values);
+            var reader = new GraphBinaryReader();
+            var serializer = new ResponseSerializer();
+
+            var results = new List<object>();
+            await foreach (var item in serializer.ReadStreamingAsync(new 
DripStream(payload, chunkSize), reader))
+            {
+                results.Add(item);
+            }
+
+            Assert.Equal(new object[] { 'a', 'τ', '€' }, results);
+        }
+
+        // BinarySerializer (byte[]) reads a length-prefixed payload via 
ReadExactlyAsync, the same shape
+        // as StringSerializer; a large byte[] straddling refills exercises 
that read path directly.
+        private static byte[] BuildNonBulkedBinaryResponse(params byte[][] 
values)
+        {
+            using var ms = new MemoryStream();
+            ms.WriteByte(0x84); // version
+            ms.WriteByte(0x00); // bulked = false
+            var scratch = new byte[4];
+            foreach (var value in values)
+            {
+                ms.WriteByte(0x25); // DataType.Binary
+                ms.WriteByte(0x00); // value_flag = not null
+                BinaryPrimitives.WriteInt32BigEndian(scratch, value.Length);
+                ms.Write(scratch, 0, 4);
+                ms.Write(value, 0, value.Length);
+            }
+            ms.WriteByte(0xFD);
+            ms.WriteByte(0x00);
+            ms.WriteByte(0x00);
+            BinaryPrimitives.WriteInt32BigEndian(scratch, 200);
+            ms.Write(scratch, 0, 4);
+            ms.WriteByte(0x01);
+            ms.WriteByte(0x01);
+            return ms.ToArray();
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(7)]
+        public async Task 
ResponseSerializerShouldDeserializeByteArrayLargerThanBuffer(int chunkSize)
+        {
+            var value = new byte[100];
+            for (var i = 0; i < value.Length; i++)
+            {
+                value[i] = (byte)(i + 1); // non-zero so silent truncation 
(zero-fill) would be caught
+            }
+            var payload = BuildNonBulkedBinaryResponse(value);
+            var reader = new GraphBinaryReader();
+            var serializer = new ResponseSerializer();
+
+            var results = new List<object>();
+            await foreach (var item in serializer.ReadStreamingAsync(new 
DripStream(payload, chunkSize), reader))
+            {
+                results.Add(item);
+            }
+
+            var single = Assert.Single(results);
+            Assert.Equal(value, Assert.IsType<byte[]>(single));
+        }
+
+        // --- 14. Constructor argument validation ---
+
+        [Fact]
+        public void ConstructorShouldThrowOnNullStream()
+        {
+            Assert.Throws<ArgumentNullException>(() => new 
GraphBinaryReadBuffer(null!));
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(-1)]
+        public void ConstructorShouldThrowOnNonPositiveBufferSize(int 
bufferSize)
+        {
+            Assert.Throws<ArgumentOutOfRangeException>(
+                () => new GraphBinaryReadBuffer(new MemoryStream(), 
bufferSize));
+        }
+
+        // --- 15. Zero-length read early-return ---
+
+        [Fact]
+        public async Task 
ReadAsyncEmptyBufferShouldReturnZeroWithoutConsuming()
+        {
+            var buffer = NewBuffer(new byte[] { 0xAB });
+
+            Assert.Equal(0, await buffer.ReadAsync(Array.Empty<byte>(), 0, 0));
+
+            // The single available byte must be untouched by the zero-length 
read.
+            Assert.Equal(0xAB, await buffer.ReadByteValueAsync());
+        }
+
+        // --- 16. General ReadAsync(count) then a fixed-width read that 
straddles a refill ---
+        //
+        // The reverse of ReadAsyncCountShouldConsumeAfterPrimitiveReads: a 
general read leaves the buffer
+        // partially consumed at a non-zero _start, and the following 
fixed-width read must span from
+        // that mid-buffer offset across a refill. Exercises the _start/_end 
accounting most prone to
+        // off-by-one.
+
+        [Fact]
+        public async Task 
PrimitiveReadShouldBeCorrectAfterGeneralReadLeavesPartialBuffer()
+        {
+            using var ms = new MemoryStream();
+            ms.Write(new byte[] { 0x0A, 0x0B, 0x0C }, 0, 3);
+            var scratch = new byte[8];
+            BinaryPrimitives.WriteInt64BigEndian(scratch, 0x1122334455667788L);
+            ms.Write(scratch, 0, 8);
+
+            // Small buffer + drip so the long straddles refills starting from 
a non-zero _start.
+            var buffer = new GraphBinaryReadBuffer(new 
DripStream(ms.ToArray(), 2), 4);
+
+            Assert.Equal(new byte[] { 0x0A, 0x0B, 0x0C }, await 
buffer.ReadAsync(3));
+            Assert.Equal(0x1122334455667788L, await 
buffer.ReadLongValueAsync());
+        }
+
+        // --- 17. Refill exception propagation ---
+        //
+        // RefillAsync commits _start/_end only after the awaited underlying 
read returns, so a thrown
+        // exception must surface to the caller without leaving the buffer in 
a half-consumed state.
+
+        [Fact]
+        public async Task RefillFailureShouldPropagateAndLeaveBufferEmpty()
+        {
+            var buffer = new GraphBinaryReadBuffer(new ThrowingStream());
+
+            await Assert.ThrowsAsync<IOException>(async () => await 
buffer.ReadIntValueAsync());
+
+            // A retry hits the same underlying failure, proving no bytes were 
spuriously committed.
+            await Assert.ThrowsAsync<IOException>(async () => await 
buffer.ReadByteValueAsync());
+        }
+    }
+}

Reply via email to