From e3178ebd4e4779dcb8e85cbe0c4376db7d6e9778 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 5 Sep 2025 14:22:26 +0200 Subject: [PATCH] Backported HTTP transport changes --- src/All.slnx | 1 + src/Directory.Packages.props | 1 + ...HotChocolate.Transport.Abstractions.csproj | 1 + .../Transport.Abstractions/OperationResult.cs | 16 + .../GraphQLHttpEventStreamProcessor.cs | 328 ----------------- .../src/Transport.Http/GraphQLHttpResponse.cs | 24 +- .../GraphQLHttpStreamException.cs | 8 + .../HotChocolate.Transport.Http.csproj | 1 + .../src/Transport.Http/Sse/SseEventData.cs | 11 + .../src/Transport.Http/Sse/SseEventParser.cs | 173 +++++++++ .../src/Transport.Http/Sse/SseEventType.cs | 8 + .../src/Transport.Http/Sse/SseReader.cs | 173 +++++++++ .../GraphQLHttpClientTests.cs | 26 +- .../Utilities/HotChocolate.Utilities.sln | 220 ++++++----- .../src/Utilities.Buffers/ArrayMemoryOwner.cs | 63 ++++ .../src/Utilities.Buffers/BufferPools.cs | 47 +++ .../HotChocolate.Utilities.Buffers.csproj | 40 ++ .../src/Utilities.Buffers/IWritableMemory.cs | 19 + .../Utilities.Buffers/JsonDocumentOwner.cs | 63 ++++ .../Utilities.Buffers/PooledArrayWriter.cs | 347 ++++++++++++++++++ .../PooledArrayWriterEventSource.cs | 62 ++++ .../Properties/BuffersResources.Designer.cs | 54 +++ .../Properties/BuffersResources.resx | 24 ++ .../ReadOnlyMemorySegment.cs | 139 +++++++ 24 files changed, 1389 insertions(+), 460 deletions(-) delete mode 100644 src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpEventStreamProcessor.cs create mode 100644 src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpStreamException.cs create mode 100644 src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventData.cs create mode 100644 src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventParser.cs create mode 100644 src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventType.cs create mode 100644 src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseReader.cs create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/ArrayMemoryOwner.cs create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/BufferPools.cs create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/HotChocolate.Utilities.Buffers.csproj create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/IWritableMemory.cs create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/JsonDocumentOwner.cs create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriter.cs create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriterEventSource.cs create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/Properties/BuffersResources.Designer.cs create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/Properties/BuffersResources.resx create mode 100644 src/HotChocolate/Utilities/src/Utilities.Buffers/ReadOnlyMemorySegment.cs diff --git a/src/All.slnx b/src/All.slnx index 4f03887cc71..129ef40e8e8 100644 --- a/src/All.slnx +++ b/src/All.slnx @@ -273,6 +273,7 @@ + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 987e221a7f1..a42a7ca472f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -124,6 +124,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/HotChocolate.Transport.Abstractions.csproj b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/HotChocolate.Transport.Abstractions.csproj index 9fb80718801..d3012de8985 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/HotChocolate.Transport.Abstractions.csproj +++ b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/HotChocolate.Transport.Abstractions.csproj @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/OperationResult.cs b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/OperationResult.cs index fe57797e792..018a609f62c 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/OperationResult.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/OperationResult.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using HotChocolate.Buffers; using static HotChocolate.Transport.Properties.TransportAbstractionResources; using static HotChocolate.Transport.Serialization.Utf8GraphQLResultProperties; @@ -86,6 +87,21 @@ public OperationResult( public void Dispose() => _memoryOwner?.Dispose(); + public static OperationResult Parse(JsonDocumentOwner documentOwner) + { + ArgumentNullException.ThrowIfNull(documentOwner); + + var root = documentOwner.Document.RootElement; + + return new OperationResult( + documentOwner, + root.TryGetProperty(DataProp, out var data) ? data : default, + root.TryGetProperty(ErrorsProp, out var errors) ? errors : default, + root.TryGetProperty(ExtensionsProp, out var extensions) ? extensions : default, + root.TryGetProperty(RequestIndexProp, out var requestIndex) ? requestIndex.GetInt32() : null, + root.TryGetProperty(VariableIndexProp, out var variableIndex) ? variableIndex.GetInt32() : null); + } + public static OperationResult Parse(JsonDocument document) { if (document == null) diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpEventStreamProcessor.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpEventStreamProcessor.cs deleted file mode 100644 index a917326028a..00000000000 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpEventStreamProcessor.cs +++ /dev/null @@ -1,328 +0,0 @@ -using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.IO.Pipelines; -using System.Runtime.CompilerServices; -using HotChocolate.Utilities; - -namespace HotChocolate.Transport.Http; - -internal static class GraphQLHttpEventStreamProcessor -{ - private static ReadOnlySpan Event => "event:"u8; - private static ReadOnlySpan Next => "next\n"u8; - private static ReadOnlySpan Complete => "complete\n"u8; - private static ReadOnlySpan Data => "data:"u8; - - public static IAsyncEnumerable ReadStream(Stream stream, CancellationToken ct) - { - var pipe = new Pipe(); - var reader = pipe.Reader; - var writer = pipe.Writer; - - ReadFromTransportAsync(stream, writer, ct).FireAndForget(); - return ReadMessagesPipeAsync(reader, ct); - } - - private static async Task ReadFromTransportAsync(Stream stream, PipeWriter writer, CancellationToken ct) - { - const int bufferSize = 64; - var buffer = new byte[bufferSize]; - var bufferMemory = new Memory(buffer); - - await using var tokenRegistration = ct.Register( - static writer => ((PipeWriter)writer!).CancelPendingFlush(), - state: writer, - useSynchronizationContext: false); - - while (true) - { - try - { - var bytesRead = await stream.ReadAsync(bufferMemory, ct).ConfigureAwait(false); - - if (bytesRead == 0) - { - break; - } - - var memory = writer.GetMemory(bytesRead); - buffer.AsSpan()[..bytesRead].CopyTo(memory.Span); - writer.Advance(bytesRead); - } - catch - { - break; - } - - // ReSharper disable once RedundantArgumentDefaultValue - var result = await writer.FlushAsync(default).ConfigureAwait(false); - if (result.IsCompleted || result.IsCanceled) - { - break; - } - } - - await writer.CompleteAsync().ConfigureAwait(false); - } - - private static async IAsyncEnumerable ReadMessagesPipeAsync( - PipeReader reader, - [EnumeratorCancellation] CancellationToken ct) - { - using var message = new ArrayWriter(); - - await using var tokenRegistration = ct.Register( - static reader => ((PipeReader)reader!).CancelPendingRead(), - state: reader, - useSynchronizationContext: false); - - while (true) - { - // ReSharper disable once RedundantArgumentDefaultValue - var result = await reader.ReadAsync(default).ConfigureAwait(false); - if (result.IsCanceled) - { - break; - } - - var buffer = result.Buffer; - SequencePosition? position; - - do - { - position = buffer.PositionOf((byte) '\n'); - - if (position == null) - { - continue; - } - - WriteToMessage(message, buffer.Slice(0, position.Value)); - - if (IsMessageComplete(message)) - { - if (!TryReadMessage(message.GetWrittenMemory(), out var operationResult)) - { - await reader.CompleteAsync().ConfigureAwait(false); - yield break; - } - - message.Reset(); - yield return operationResult; - } - - buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); - } while (position != null); - - reader.AdvanceTo(buffer.Start, buffer.End); - - if (result.IsCompleted) - { - break; - } - } - - await reader.CompleteAsync().ConfigureAwait(false); - } - - private static void WriteToMessage(ArrayWriter message, ReadOnlySequence buffer) - { - if (buffer.IsSingleSegment) - { - var size = buffer.First.Span.Length; - - if (size > 0) - { - var span = message.GetSpan(size); - buffer.First.Span.CopyTo(span); - message.Advance(size); - } - } - else - { - foreach (var segment in buffer) - { - var size = segment.Span.Length; - var span = message.GetSpan(size); - segment.Span.CopyTo(span); - message.Advance(size); - } - } - - var lineEnd = message.GetSpan(1); - lineEnd[0] = (byte) '\n'; - message.Advance(1); - } - - private static bool IsMessageComplete(ArrayWriter message) - { - var length = message.Length; - - if (length < 2) - { - return false; - } - - if (length == 2) - { - var span = message.GetWrittenSpan().Slice(0, 2); - - if (span[0] == (byte) '\n' && span[1] == (byte) '\n') - { - message.Reset(); - } - return false; - } - - if (length == 3) - { - var span = message.GetWrittenSpan().Slice(0, 3); - - if (span[0] == (byte) ':' && span[1] == (byte) '\n' && span[2] == (byte) '\n') - { - message.Reset(); - return false; - } - - if (span[1] == (byte) '\n' && span[2] == (byte) '\n') - { - return true; - } - - return false; - } - - var last = message.GetWrittenSpan().Slice(length - 2, 2); - - if (last[0] == (byte) '\n' && last[1] == (byte) '\n') - { - return true; - } - - return false; - } - - private static bool TryReadMessage( - ReadOnlyMemory message, - [NotNullWhen(true)] out OperationResult? result) - { - var span = message.Span; - var type = ParseEventType(ref span); - - if(type is EventType.Next) - { - result = ParseData(ref span); - return true; - } - - result = null; - return false; - } - - private static EventType ParseEventType(ref ReadOnlySpan span) - { - if (ExpectEvent(ref span)) - { - if (ExpectNext(ref span)) - { - return EventType.Next; - } - - if (ExpectComplete(ref span)) - { - return EventType.Complete; - } - } - - throw new InvalidOperationException("Invalid Message Format."); - } - - private static OperationResult ParseData(ref ReadOnlySpan span) - { - if (ExpectData(ref span)) - { - var data = ReadData(ref span); - return OperationResult.Parse(data); - } - - throw new InvalidOperationException("Invalid Message Format."); - } - - private static bool ExpectEvent(ref ReadOnlySpan span) - { - if (span.Slice(0, 6).SequenceEqual(Event)) - { - span = span.Slice(6); - return true; - } - - return false; - } - - private static bool ExpectNext(ref ReadOnlySpan span) - { - SkipWhitespaces(ref span); - - if (!span.Slice(0, 5).SequenceEqual(Next)) - { - return false; - } - - span = span.Slice(5); - - return true; - } - - private static bool ExpectComplete(ref ReadOnlySpan span) - { - SkipWhitespaces(ref span); - - if (!span.Slice(0, 9).SequenceEqual(Complete)) - { - return false; - } - - span = span.Slice(9); - - return true; - } - - private static bool ExpectData(ref ReadOnlySpan span) - { - if (span.Slice(0, 5).SequenceEqual(Data)) - { - span = span.Slice(5); - return true; - } - - return false; - } - - private static ReadOnlySpan ReadData(ref ReadOnlySpan span) - { - var linebreak = span.IndexOf((byte) '\n'); - - if (linebreak == -1) - { - throw new InvalidOperationException("Invalid Message Format."); - } - - var data = span.Slice(0, linebreak); - span = span.Slice(linebreak + 1); - return data; - } - - private static void SkipWhitespaces(ref ReadOnlySpan span) - { - while (span.Length > 0 && span[0] == (byte)' ') - { - span = span.Slice(1); - } - } - - private enum EventType - { - Next, - Complete, - } -} diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs index 0b357cb3171..a7508501f3b 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs @@ -1,7 +1,6 @@ using System.Net; using System.Diagnostics; using System.Net.Http.Headers; -using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using HotChocolate.Utilities; @@ -166,7 +165,7 @@ public IAsyncEnumerable ReadAsResultStreamAsync(CancellationTok if (contentType?.MediaType.EqualsOrdinal(ContentType.EventStream) ?? false) { - return ReadAsResultStreamInternalAsync(contentType.CharSet, cancellationToken); + return new SseReader(_message); } // The server supports the newer graphql-response+json media type and users are free @@ -187,27 +186,6 @@ public IAsyncEnumerable ReadAsResultStreamAsync(CancellationTok return SingleResult(new ValueTask(_transportError)); } - private async IAsyncEnumerable ReadAsResultStreamInternalAsync( - string? charSet, - [EnumeratorCancellation] CancellationToken ct) - { - await using var contentStream = await _message.Content.ReadAsStreamAsync(ct) - .ConfigureAwait(false); - - var stream = contentStream; - - var sourceEncoding = GetEncoding(charSet); - if (sourceEncoding is not null && !Equals(sourceEncoding.EncodingName, _utf8.EncodingName)) - { - stream = GetTranscodingStream(contentStream, sourceEncoding); - } - - await foreach (var item in GraphQLHttpEventStreamProcessor.ReadStream(stream, ct).ConfigureAwait(false)) - { - yield return item; - } - } - private static async IAsyncEnumerable SingleResult(ValueTask result) { yield return await result.ConfigureAwait(false); diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpStreamException.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpStreamException.cs new file mode 100644 index 00000000000..dfa672c5709 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpStreamException.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Transport.Http; + +public class GraphQLHttpStreamException : Exception +{ + public GraphQLHttpStreamException(string message) : base(message) + { + } +} diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/HotChocolate.Transport.Http.csproj b/src/HotChocolate/AspNetCore/src/Transport.Http/HotChocolate.Transport.Http.csproj index 72f41b3e8ec..166e4e579b5 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/HotChocolate.Transport.Http.csproj +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/HotChocolate.Transport.Http.csproj @@ -8,6 +8,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventData.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventData.cs new file mode 100644 index 00000000000..f05a972104d --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventData.cs @@ -0,0 +1,11 @@ +using HotChocolate.Buffers; + +namespace HotChocolate.Transport.Http; + +internal readonly ref struct SseEventData( + SseEventType type, + PooledArrayWriter? data) +{ + public readonly SseEventType Type = type; + public readonly PooledArrayWriter? Data = data; +} diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventParser.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventParser.cs new file mode 100644 index 00000000000..1f17b0a06c3 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventParser.cs @@ -0,0 +1,173 @@ +using System.Runtime.CompilerServices; +using HotChocolate.Buffers; + +namespace HotChocolate.Transport.Http; + +internal static class SseEventParser +{ + private static ReadOnlySpan Event => "event:"u8; + private static ReadOnlySpan Data => "data:"u8; + private static ReadOnlySpan NextEvent => "next"u8; + private static ReadOnlySpan CompleteEvent => "complete"u8; + + public static SseEventData Parse(ReadOnlySpan message) + { + var type = ParseEventType(ref message); + + switch (type) + { + case SseEventType.Next: + var buffer = ParseData(ref message); + return new SseEventData(SseEventType.Next, buffer); + + case SseEventType.Complete: + return new SseEventData(SseEventType.Complete, null); + + default: + return new SseEventData(SseEventType.Unknown, null); + } + } + + /// + /// Collects data: lines until the blank-line separator and concatenates them with LF. + /// + private static PooledArrayWriter ParseData(ref ReadOnlySpan span) + { + if (span.Length < Data.Length || !span.StartsWith(Data)) + { + throw new GraphQLHttpStreamException("Invalid GraphQL over SSE Message Format."); + } + + var payload = new PooledArrayWriter(); + + try + { + while (ConsumeToken(ref span, Data)) + { + SkipWhitespaces(ref span); + + // read one logical line up to LF or end + var lineBreak = span.IndexOf((byte)'\n'); + ReadOnlySpan line; + switch (lineBreak) + { + case -1: + line = span; + span = default; + break; + case > 0 when span[lineBreak - 1] == (byte)'\r': + line = span[..(lineBreak - 1)]; + span = span[(lineBreak + 1)..]; + break; + default: + line = span[..lineBreak]; + span = span[(lineBreak + 1)..]; + break; + } + + if (line.Length > 0) + { + // Remove optional leading space + SkipWhitespaces(ref line); + } + + // append to buffer (insert LF between lines) + if (payload.Length > 0) + { + payload.GetSpan(1)[0] = (byte)'\n'; + payload.Advance(1); + } + + if (line.Length > 0) + { + line.CopyTo(payload.GetSpan(line.Length)); + payload.Advance(line.Length); + } + } + + return payload; + } + catch + { + payload.Dispose(); + throw; + } + } + + private static SseEventType ParseEventType(ref ReadOnlySpan span) + { + if (ExpectEvent(ref span)) + { + if (ExpectNext(ref span)) + { + return SseEventType.Next; + } + + if (ExpectComplete(ref span)) + { + return SseEventType.Complete; + } + } + + return SseEventType.Unknown; + } + + private static bool ExpectEvent(ref ReadOnlySpan span) + => ConsumeToken(ref span, Event); + + private static bool ExpectNext(ref ReadOnlySpan span) + => ConsumeTokenWithOptionalWhitespace(ref span, NextEvent); + + private static bool ExpectComplete(ref ReadOnlySpan span) + => ConsumeTokenWithOptionalWhitespace(ref span, CompleteEvent); + + private static bool ConsumeTokenWithOptionalWhitespace(ref ReadOnlySpan span, ReadOnlySpan token) + { + SkipWhitespaces(ref span); + + if (ConsumeToken(ref span, token)) + { + SkipNewLine(ref span); + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ConsumeToken(ref ReadOnlySpan span, ReadOnlySpan token) + { + if (span.Length < token.Length || !span.StartsWith(token)) + { + return false; + } + + span = span[token.Length..]; + return true; + } + + private static void SkipWhitespaces(ref ReadOnlySpan span) + { + while (span.Length > 0 && IsWhitespace(span[0])) + { + span = span[1..]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsWhitespace(byte b) => b is (byte)' ' or (byte)'\t'; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SkipNewLine(ref ReadOnlySpan span) + { + if (span.Length > 0 && span[0] == (byte)'\r') + { + span = span[1..]; + } + + if (span.Length > 0 && span[0] == (byte)'\n') + { + span = span[1..]; + } + } +} diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventType.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventType.cs new file mode 100644 index 00000000000..acd8ee0bce9 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseEventType.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Transport.Http; + +internal enum SseEventType : byte +{ + Unknown = 0, + Next = 1, + Complete = 2 +} diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseReader.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseReader.cs new file mode 100644 index 00000000000..706ff8c26ca --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseReader.cs @@ -0,0 +1,173 @@ +using System.Buffers; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Text.Json; +using HotChocolate.Buffers; + +namespace HotChocolate.Transport.Http; + +internal class SseReader(HttpResponseMessage message) : IAsyncEnumerable +{ + private static readonly StreamPipeReaderOptions s_options = new( + pool: MemoryPool.Shared, + bufferSize: 4096, + minimumReadSize: 1, + leaveOpen: true, + useZeroByteReads: true); + + public async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + await using var stream = await message.Content.ReadAsStreamAsync(cts.Token); + using var eventBuffer = new PooledArrayWriter(); + var reader = PipeReader.Create(stream, s_options); + + try + { + ReadResult result; + do + { + result = await reader.ReadAsync(cts.Token).ConfigureAwait(false); + if (result.IsCanceled) + { + yield break; + } + + var buffer = result.Buffer; + var consumed = buffer.Start; + var examined = buffer.End; + + do + { + var position = buffer.PositionOf((byte)'\n'); + if (position is null) + { + // Mark what we've examined but not consumed + examined = buffer.End; + break; + } + + WriteLineToMessage(eventBuffer, buffer.Slice(0, position.Value)); + + if (IsMessageComplete(eventBuffer.WrittenSpan)) + { + if (IsKeepAlive(eventBuffer.WrittenSpan)) + { + eventBuffer.Reset(); + } + else + { + var eventMessage = SseEventParser.Parse(eventBuffer.WrittenSpan); + + switch (eventMessage.Type) + { + case SseEventType.Complete: + reader.AdvanceTo(buffer.GetPosition(1, position.Value)); + yield break; + + case SseEventType.Next when eventMessage.Data is not null: + eventBuffer.Reset(); + var document = JsonDocument.Parse(eventMessage.Data.WrittenMemory); + var documentOwner = new JsonDocumentOwner(document, eventMessage.Data); + yield return OperationResult.Parse(documentOwner); + break; + + default: + throw new GraphQLHttpStreamException("Malformed message received."); + } + } + } + + // Move past the processed line + var nextPosition = buffer.GetPosition(1, position.Value); + consumed = nextPosition; + buffer = buffer.Slice(nextPosition); + } while (!buffer.IsEmpty); + + // Tell the reader how much we've consumed and examined + reader.AdvanceTo(consumed, examined); + } while (!result.IsCompleted); + } + finally + { + await cts.CancelAsync().ConfigureAwait(false); + await reader.CompleteAsync().ConfigureAwait(false); + } + } + + private static void WriteLineToMessage(PooledArrayWriter message, ReadOnlySequence buffer) + { + message.EnsureBufferCapacity((int)buffer.Length); + + if (buffer.IsSingleSegment) + { + var span = buffer.First.Span; + + // normalize line breaks. + if (span.Length > 0 && span[^1] == (byte)'\r') + { + span = span[..^1]; + } + + span.CopyTo(message.GetSpan(span.Length)); + message.Advance(span.Length); + } + else + { + var position = buffer.Start; + while (buffer.TryGet(ref position, out var memory)) + { + var span = memory.Span; + + // normalize line breaks. + if (position.Equals(buffer.End) && span.Length > 0 && span[^1] == (byte)'\r') + { + span = span[..^1]; + } + + span.CopyTo(message.GetSpan(span.Length)); + message.Advance(span.Length); + } + } + + // re-add unified line break (LF only) + message.GetSpan(1)[0] = (byte)'\n'; + message.Advance(1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMessageComplete(ReadOnlySpan message) + => message.Length >= 2 && message[^1] == (byte)'\n' && message[^2] == (byte)'\n'; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsKeepAlive(ReadOnlySpan message) + { + // The minimal keep alive message is 3 characters `:\n\n`. + // Any message not starting with `:` or shorter than 3 lines is invalid/ + if (message.Length < 3 || message[0] != (byte)':') + { + return false; + } + + // Each message must end with to new lines, if we find none it's an invalid message. + var firstNewline = message.IndexOf((byte)'\n'); + if (firstNewline == -1) + { + return false; + } + + // After the ':', it should either be: + // 1. End of line (just ":\n") + // 2. A space followed by arbitrary text (": some text\n") + // 3. Arbitrary text without space (":keep-alive\n") + + // But it should NOT contain any SSE field syntax like "event:" or "data:" + // Check if the rest of the message (after this comment line) contains valid SSE fields + var remaining = message[(firstNewline + 1)..]; + + // If there's more content after the comment, it should be another \n (for message termination) + // or it should be empty. Keep-alive messages shouldn't have event/data fields. + return remaining.Length == 1 && remaining[0] == (byte)'\n'; + } +} diff --git a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs index 2ccd454d104..afa174bcd8e 100644 --- a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs +++ b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs @@ -653,13 +653,13 @@ mutation CreateReviewForEpisode( mutationResponse.EnsureSuccessStatusCode(); // assert - await foreach (var result in subscriptionResponse.ReadAsResultStreamAsync(cts.Token)) + await foreach (var result in subscriptionResponse.ReadAsResultStreamAsync().WithCancellation(cts.Token)) { result.MatchInlineSnapshot( """ Data: {"onReview":{"stars":5}} """); - cts.Cancel(); + break; } } @@ -710,14 +710,24 @@ mutation CreateReviewForEpisode( mutationResponse.EnsureSuccessStatusCode(); // assert - await foreach (var result in subscriptionResponse.ReadAsResultStreamAsync(cts.Token)) + var canceled = false; + try { - result.MatchInlineSnapshot( - """ - Data: {"onReview":{"stars":5}} - """); - cts.Cancel(); + await foreach (var result in subscriptionResponse.ReadAsResultStreamAsync().WithCancellation(cts.Token)) + { + result.MatchInlineSnapshot( + """ + Data: {"onReview":{"stars":5}} + """); + await cts.CancelAsync(); + } } + catch (OperationCanceledException) + { + canceled = true; + } + + Assert.True(canceled, "Cancellation was received."); } [Fact] diff --git a/src/HotChocolate/Utilities/HotChocolate.Utilities.sln b/src/HotChocolate/Utilities/HotChocolate.Utilities.sln index 5b30a529e9b..4d1b2807dff 100644 --- a/src/HotChocolate/Utilities/HotChocolate.Utilities.sln +++ b/src/HotChocolate/Utilities/HotChocolate.Utilities.sln @@ -1,101 +1,119 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6A4640C6-E7EB-4DDD-9B67-B5612BD8F23D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities", "src\Utilities\HotChocolate.Utilities.csproj", "{ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities.Introspection", "src\Utilities.Introspection\HotChocolate.Utilities.Introspection.csproj", "{C23DFCA5-0F3B-438F-B358-452034F99F06}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B65DA1F2-0B0B-4C39-90D1-8F157BA205EB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities.Introspection.Tests", "test\Utilities.Introspection.Tests\HotChocolate.Utilities.Introspection.Tests.csproj", "{DF0C6883-2123-414D-954F-7CF587DF3CE0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities.Tests", "test\Utilities.Tests\HotChocolate.Utilities.Tests.csproj", "{F21E8424-BD36-49F9-8F51-9D7261B2FF91}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities.DependencyInjection", "src\Utilities.DependencyInjection\HotChocolate.Utilities.DependencyInjection.csproj", "{DDC5D574-939C-434C-876F-4E3BBC7B304D}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|x64.ActiveCfg = Debug|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|x64.Build.0 = Debug|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|x86.ActiveCfg = Debug|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|x86.Build.0 = Debug|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|Any CPU.Build.0 = Release|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|x64.ActiveCfg = Release|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|x64.Build.0 = Release|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|x86.ActiveCfg = Release|Any CPU - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|x86.Build.0 = Release|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|x64.ActiveCfg = Debug|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|x64.Build.0 = Debug|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|x86.ActiveCfg = Debug|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|x86.Build.0 = Debug|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|Any CPU.Build.0 = Release|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|x64.ActiveCfg = Release|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|x64.Build.0 = Release|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|x86.ActiveCfg = Release|Any CPU - {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|x86.Build.0 = Release|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|x64.ActiveCfg = Debug|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|x64.Build.0 = Debug|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|x86.ActiveCfg = Debug|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|x86.Build.0 = Debug|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|Any CPU.Build.0 = Release|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|x64.ActiveCfg = Release|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|x64.Build.0 = Release|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|x86.ActiveCfg = Release|Any CPU - {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|x86.Build.0 = Release|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|x64.ActiveCfg = Debug|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|x64.Build.0 = Debug|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|x86.ActiveCfg = Debug|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|x86.Build.0 = Debug|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|Any CPU.Build.0 = Release|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|x64.ActiveCfg = Release|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|x64.Build.0 = Release|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|x86.ActiveCfg = Release|Any CPU - {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|x86.Build.0 = Release|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|x64.ActiveCfg = Debug|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|x64.Build.0 = Debug|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|x86.ActiveCfg = Debug|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|x86.Build.0 = Debug|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|Any CPU.Build.0 = Release|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|x64.ActiveCfg = Release|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|x64.Build.0 = Release|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|x86.ActiveCfg = Release|Any CPU - {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB} = {6A4640C6-E7EB-4DDD-9B67-B5612BD8F23D} - {C23DFCA5-0F3B-438F-B358-452034F99F06} = {6A4640C6-E7EB-4DDD-9B67-B5612BD8F23D} - {DF0C6883-2123-414D-954F-7CF587DF3CE0} = {B65DA1F2-0B0B-4C39-90D1-8F157BA205EB} - {F21E8424-BD36-49F9-8F51-9D7261B2FF91} = {B65DA1F2-0B0B-4C39-90D1-8F157BA205EB} - {DDC5D574-939C-434C-876F-4E3BBC7B304D} = {6A4640C6-E7EB-4DDD-9B67-B5612BD8F23D} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6A4640C6-E7EB-4DDD-9B67-B5612BD8F23D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities", "src\Utilities\HotChocolate.Utilities.csproj", "{ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities.Introspection", "src\Utilities.Introspection\HotChocolate.Utilities.Introspection.csproj", "{C23DFCA5-0F3B-438F-B358-452034F99F06}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B65DA1F2-0B0B-4C39-90D1-8F157BA205EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities.Introspection.Tests", "test\Utilities.Introspection.Tests\HotChocolate.Utilities.Introspection.Tests.csproj", "{DF0C6883-2123-414D-954F-7CF587DF3CE0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities.Tests", "test\Utilities.Tests\HotChocolate.Utilities.Tests.csproj", "{F21E8424-BD36-49F9-8F51-9D7261B2FF91}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities.DependencyInjection", "src\Utilities.DependencyInjection\HotChocolate.Utilities.DependencyInjection.csproj", "{DDC5D574-939C-434C-876F-4E3BBC7B304D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities.Buffers", "Utilities.Buffers", "{21460C0F-DF92-76DC-B304-DCF2D798072A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities.Buffers", "src\Utilities.Buffers\HotChocolate.Utilities.Buffers.csproj", "{3036A31D-A393-4BCB-993A-95C5E69B1C32}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|x64.Build.0 = Debug|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Debug|x86.Build.0 = Debug|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|Any CPU.Build.0 = Release|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|x64.ActiveCfg = Release|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|x64.Build.0 = Release|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|x86.ActiveCfg = Release|Any CPU + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB}.Release|x86.Build.0 = Release|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|x64.ActiveCfg = Debug|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|x64.Build.0 = Debug|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|x86.ActiveCfg = Debug|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Debug|x86.Build.0 = Debug|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|Any CPU.Build.0 = Release|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|x64.ActiveCfg = Release|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|x64.Build.0 = Release|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|x86.ActiveCfg = Release|Any CPU + {C23DFCA5-0F3B-438F-B358-452034F99F06}.Release|x86.Build.0 = Release|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|x64.Build.0 = Debug|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Debug|x86.Build.0 = Debug|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|Any CPU.Build.0 = Release|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|x64.ActiveCfg = Release|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|x64.Build.0 = Release|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|x86.ActiveCfg = Release|Any CPU + {DF0C6883-2123-414D-954F-7CF587DF3CE0}.Release|x86.Build.0 = Release|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|x64.ActiveCfg = Debug|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|x64.Build.0 = Debug|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|x86.ActiveCfg = Debug|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Debug|x86.Build.0 = Debug|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|Any CPU.Build.0 = Release|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|x64.ActiveCfg = Release|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|x64.Build.0 = Release|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|x86.ActiveCfg = Release|Any CPU + {F21E8424-BD36-49F9-8F51-9D7261B2FF91}.Release|x86.Build.0 = Release|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|x64.ActiveCfg = Debug|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|x64.Build.0 = Debug|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|x86.ActiveCfg = Debug|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Debug|x86.Build.0 = Debug|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|Any CPU.Build.0 = Release|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|x64.ActiveCfg = Release|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|x64.Build.0 = Release|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|x86.ActiveCfg = Release|Any CPU + {DDC5D574-939C-434C-876F-4E3BBC7B304D}.Release|x86.Build.0 = Release|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Debug|x64.ActiveCfg = Debug|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Debug|x64.Build.0 = Debug|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Debug|x86.ActiveCfg = Debug|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Debug|x86.Build.0 = Debug|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Release|Any CPU.Build.0 = Release|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Release|x64.ActiveCfg = Release|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Release|x64.Build.0 = Release|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Release|x86.ActiveCfg = Release|Any CPU + {3036A31D-A393-4BCB-993A-95C5E69B1C32}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {ADEC4154-1AF3-4B83-9C76-D11F9BB2E5FB} = {6A4640C6-E7EB-4DDD-9B67-B5612BD8F23D} + {C23DFCA5-0F3B-438F-B358-452034F99F06} = {6A4640C6-E7EB-4DDD-9B67-B5612BD8F23D} + {DF0C6883-2123-414D-954F-7CF587DF3CE0} = {B65DA1F2-0B0B-4C39-90D1-8F157BA205EB} + {F21E8424-BD36-49F9-8F51-9D7261B2FF91} = {B65DA1F2-0B0B-4C39-90D1-8F157BA205EB} + {DDC5D574-939C-434C-876F-4E3BBC7B304D} = {6A4640C6-E7EB-4DDD-9B67-B5612BD8F23D} + {21460C0F-DF92-76DC-B304-DCF2D798072A} = {6A4640C6-E7EB-4DDD-9B67-B5612BD8F23D} + {3036A31D-A393-4BCB-993A-95C5E69B1C32} = {21460C0F-DF92-76DC-B304-DCF2D798072A} + EndGlobalSection +EndGlobal diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/ArrayMemoryOwner.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/ArrayMemoryOwner.cs new file mode 100644 index 00000000000..fa5dce1e44e --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/ArrayMemoryOwner.cs @@ -0,0 +1,63 @@ +using System.Buffers; + +namespace HotChocolate.Buffers; + +/// +/// A memory owner for a byte array. +/// +public sealed class ArrayMemoryOwner : IMemoryOwner +{ + private readonly byte[] _buffer; + private readonly int _start; + private readonly int _length; + + public ArrayMemoryOwner(byte[] buffer) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(buffer); +#else + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } +#endif + + _buffer = buffer; + _start = 0; + _length = buffer.Length; + } + + public ArrayMemoryOwner(byte[] buffer, int start, int length) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(buffer); + ArgumentOutOfRangeException.ThrowIfLessThan(start, 0); + ArgumentOutOfRangeException.ThrowIfLessThan(length, 0); +#else + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (start < 0) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } +#endif + + _buffer = buffer; + _start = start; + _length = length; + } + + public Memory Memory => _buffer.AsMemory().Slice(_start, _length); + + public void Dispose() + { + // do nothing + } +} diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/BufferPools.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/BufferPools.cs new file mode 100644 index 00000000000..ff3ff2a315a --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/BufferPools.cs @@ -0,0 +1,47 @@ +using System.Buffers; + +namespace HotChocolate.Buffers; + +internal static class BufferPools +{ + private static readonly ArrayPool s_veryLarge = ArrayPool.Create( + maxArrayLength: 16 * 1024 * 1024, + maxArraysPerBucket: 2); + private static readonly ArrayPool s_large = ArrayPool.Create( + maxArrayLength: 8 * 1024 * 1024, + maxArraysPerBucket: 10); + private static readonly ArrayPool s_standard = ArrayPool.Shared; + + public static byte[] Rent(int minimumSize) + { + if (minimumSize <= 1 * 1024 * 1024) + { + return s_standard.Rent(minimumSize); + } + + if(minimumSize <= 8 * 1024 * 1024) + { + return s_large.Rent(minimumSize); + } + + return s_veryLarge.Rent(minimumSize); + } + + public static void Return(byte[] array, bool clearArray = false) + { + var length = array.Length; + + if (length <= 1 * 1024 * 1024) + { + s_standard.Return(array, clearArray); + } + else if (length <= 8 * 1024 * 1024) + { + s_large.Return(array, clearArray); + } + else + { + s_veryLarge.Return(array, clearArray); + } + } +} diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/HotChocolate.Utilities.Buffers.csproj b/src/HotChocolate/Utilities/src/Utilities.Buffers/HotChocolate.Utilities.Buffers.csproj new file mode 100644 index 00000000000..a91e3a4b917 --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/HotChocolate.Utilities.Buffers.csproj @@ -0,0 +1,40 @@ + + + + $(ExtendedTargetFrameworks) + + + + HotChocolate.Buffers + HotChocolate.Buffers + HotChocolate.Utilities.Buffers + This package contains the HotChocolate.Buffers.ArrayWriter. + + + + true + true + + + + + + + + + + + ResXFileCodeGenerator + BuffersResources.Designer.cs + + + + + + True + True + BuffersResources.resx + + + + diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/IWritableMemory.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/IWritableMemory.cs new file mode 100644 index 00000000000..c4c3b67f21b --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/IWritableMemory.cs @@ -0,0 +1,19 @@ +using System.Buffers; + +namespace HotChocolate.Buffers; + +/// +/// Represents expandable memory that can be referenced, written to and be dismissed. +/// +public interface IWritableMemory : IBufferWriter, IMemoryOwner +{ + /// + /// Gets the part of the memory that has been written to. + /// + ReadOnlySpan WrittenSpan { get; } + + /// + /// Gets the part of the memory that has been written to. + /// + ReadOnlyMemory WrittenMemory { get; } +} diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonDocumentOwner.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonDocumentOwner.cs new file mode 100644 index 00000000000..3091af2c3a9 --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonDocumentOwner.cs @@ -0,0 +1,63 @@ +using System.Buffers; +using System.Text.Json; + +namespace HotChocolate.Buffers; + +/// +/// A that owns the memory it was created with. +/// This helper holds both the and the memory the document was parsed from. +/// +public sealed class JsonDocumentOwner : IDisposable +{ + private readonly IMemoryOwner _memory; + private bool _disposed; + + /// + /// Initializes a new instance of . + /// + /// + /// The to own. + /// + /// + /// The memory that was used to create the . + /// + public JsonDocumentOwner(JsonDocument document, IMemoryOwner memory) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(document); + ArgumentNullException.ThrowIfNull(memory); +#else + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } + + if (memory is null) + { + throw new ArgumentNullException(nameof(memory)); + } +#endif + + Document = document; + _memory = memory; + } + + /// + /// Gets the that is owned by this instance. + /// + public JsonDocument Document { get; } + + /// + /// Disposes the and the memory the document was parsed from. + /// + public void Dispose() + { + if (!_disposed) + { + Document.Dispose(); + _memory.Dispose(); + + _disposed = true; + } + } +} diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriter.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriter.cs new file mode 100644 index 00000000000..8b92c2e6a91 --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriter.cs @@ -0,0 +1,347 @@ +using System.Buffers; +using static HotChocolate.Buffers.PooledArrayWriterEventSource; +#if NET8_0_OR_GREATER +using System.Runtime.InteropServices; +#endif +using static HotChocolate.Buffers.Properties.BuffersResources; + +namespace HotChocolate.Buffers; + +/// +/// A that writes to a rented buffer. +/// +public sealed class PooledArrayWriter : IWritableMemory +{ + private const int InitialBufferSize = 4096; + private const int LargeAllocationThreshold = 1024 * 1024; // 1MB + + private byte[] _buffer; + private int _capacity; + private int _start; + private int _resizeCount; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + public PooledArrayWriter() + { + _buffer = BufferPools.Rent(InitialBufferSize); + _capacity = _buffer.Length; + _start = 0; + _resizeCount = 0; + + Log.WriterCreated(InitialBufferSize, _capacity); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The initial buffer size. + /// + public PooledArrayWriter(int initialBufferSize) + { + _buffer = BufferPools.Rent(initialBufferSize); + _capacity = _buffer.Length; + _start = 0; + _resizeCount = 0; + + Log.WriterCreated(initialBufferSize, _capacity); + + if (initialBufferSize > LargeAllocationThreshold) + { + Log.LargeAllocation(initialBufferSize, LargeAllocationThreshold); + } + } + + /// + /// Gets the number of bytes written to the buffer. + /// + public int Length => _start; + + /// + /// Gets the current internal capacity of the internal buffer. + /// + public int Capacity => _buffer.Length; + + /// + /// Gets the total number of buffer resizes that have occurred. + /// + public int ResizeCount => _resizeCount; + + /// + /// Gets the underlying buffer. + /// + /// + /// The underlying buffer. + /// + /// + /// Accessing the underlying buffer directly is not recommended. + /// If possible use or . + /// + internal byte[] GetInternalBuffer() => _buffer; + + /// + /// Gets access to the full underlying buffer. + /// + public Memory Memory + => _buffer; + + /// + /// Gets the part of the buffer that has been written to. + /// + /// + /// A of the written portion of the buffer. + /// + public ReadOnlyMemory WrittenMemory +#if NETSTANDARD2_0 + => _buffer.AsMemory().Slice(0, _start); +#else + => _buffer.AsMemory()[.._start]; +#endif + + /// + /// Gets the part of the buffer that has been written to. + /// + /// + /// A of the written portion of the buffer. + /// + public ReadOnlySpan WrittenSpan +#if NETSTANDARD2_0 + => _buffer.AsSpan(0, _start); +#else + => MemoryMarshal.CreateSpan(ref _buffer[0], _start); +#endif + + /// + /// Gets the buffer as an + /// + /// + /// A to reference to a certain part of the written memory. + /// + public ArraySegment WrittenArraySegment + => new(_buffer, 0, _start); + + /// + /// Gets a read-only memory segment to reference to a certain part of the written memory. + /// + /// + /// The start index of the memory segment. + /// + /// + /// The length of the memory segment. + /// + /// + /// A to reference to a certain part of the written memory. + /// + public ReadOnlyMemorySegment GetWrittenMemorySegment(int start, int length) + => new(this, start, length); + + /// + /// Advances the writer by the specified number of bytes. + /// + /// + /// The number of bytes to advance the writer by. + /// + /// + /// Thrown if is negative or + /// if is greater than the + /// available capacity on the internal buffer. + /// + public void Advance(int count) + { +#if NETSTANDARD2_0 + if (_disposed) + { + throw new ObjectDisposedException(typeof(PooledArrayWriter).FullName!); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } +#else + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentOutOfRangeException.ThrowIfNegative(count); +#endif + + if (count > _capacity) + { + Log.BufferOverflow(count, _capacity); + throw new ArgumentOutOfRangeException( + nameof(count), + count, + ArrayWriter_Advance_BufferOverflow); + } + + _start += count; + _capacity -= count; + + Log.WriterAdvanced(count, _start, _capacity); + } + + /// + /// Gets a to write to. + /// + /// + /// The minimum size of the returned . + /// + /// + /// A to write to. + /// + /// + /// Thrown if is negative. + /// + public Memory GetMemory(int sizeHint = 0) + { +#if NETSTANDARD2_0 + if (_disposed) + { + throw new ObjectDisposedException(typeof(PooledArrayWriter).FullName!); + } + + if (sizeHint < 0) + { + throw new ArgumentOutOfRangeException(nameof(sizeHint)); + } +#else + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentOutOfRangeException.ThrowIfNegative(sizeHint); +#endif + + var size = sizeHint < 1 ? InitialBufferSize : sizeHint; + var resizeRequired = _capacity < size; + + EnsureBufferCapacity(size); + + Log.MemoryRequested(sizeHint, size, resizeRequired); + + return _buffer.AsMemory().Slice(_start, size); + } + + /// + /// Gets a to write to. + /// + /// + /// The minimum size of the returned . + /// + /// + /// A to write to. + /// + /// + /// Thrown if is negative. + /// + public Span GetSpan(int sizeHint = 0) + { +#if NETSTANDARD2_0 + if (_disposed) + { + throw new ObjectDisposedException(typeof(PooledArrayWriter).FullName!); + } + + if (sizeHint < 0) + { + throw new ArgumentOutOfRangeException(nameof(sizeHint)); + } +#else + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentOutOfRangeException.ThrowIfNegative(sizeHint); +#endif + + var size = sizeHint < 1 ? InitialBufferSize : sizeHint; + var resizeRequired = _capacity < size; + + EnsureBufferCapacity(size); + + Log.MemoryRequested(sizeHint, size, resizeRequired); + +#if NETSTANDARD2_0 + return _buffer.AsSpan(_start, size); +#else + return MemoryMarshal.CreateSpan(ref _buffer[_start], size); +#endif + } + + /// + /// Ensures that the internal buffer has the necessary capacity. + /// + /// + /// The necessary capacity on the internal buffer. + /// + public void EnsureBufferCapacity(int neededCapacity) + { + // check if we have enough capacity available on the buffer. + if (_capacity < neededCapacity) + { + // if we need to expand the buffer, we first capture the original buffer. + var buffer = _buffer; + var oldSize = buffer.Length; + + // next we determine the new size of the buffer, we at least double the size to avoid + // expanding the buffer too often. + var newSize = buffer.Length * 2; + + // if that new buffer size is not enough to satisfy the necessary capacity, + // we add the necessary capacity to the doubled buffer capacity. + if (neededCapacity > newSize - _start) + { + newSize += neededCapacity; + } + + // next we will rent a new array from the array pool that supports + // the new capacity requirements. + _buffer = BufferPools.Rent(newSize); + var actualNewSize = _buffer.Length; + + // the rented array might have a larger size than the necessary capacity, + // so we will take the buffer length and calculate from that the free capacity. + _capacity += _buffer.Length - buffer.Length; + + // finally, we copy the data from the original buffer to the new buffer. + buffer.AsSpan().CopyTo(_buffer); + + // last but not least, we return the original buffer to the array pool. + BufferPools.Return(buffer); + + _resizeCount++; + + // Log the resize operation + Log.BufferResize(oldSize, actualNewSize, _start, neededCapacity, _resizeCount); + + if (actualNewSize > LargeAllocationThreshold) + { + Log.LargeAllocation(actualNewSize, LargeAllocationThreshold); + } + } + } + + public void Reset() + { + var previousLength = _start; + _capacity = _buffer.Length; + _start = 0; + + Log.WriterReset(previousLength, _capacity); + } + + /// + public void Dispose() + { + if (!_disposed) + { + Log.WriterDisposed(_start, _buffer.Length, _resizeCount); + + if (_start > 0) + { + _buffer.AsSpan(0, _start).Clear(); + } + + BufferPools.Return(_buffer); + _buffer = []; + _capacity = 0; + _start = 0; + _disposed = true; + } + } +} diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriterEventSource.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriterEventSource.cs new file mode 100644 index 00000000000..32eefd0f22d --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriterEventSource.cs @@ -0,0 +1,62 @@ +using System.Diagnostics.Tracing; + +namespace HotChocolate.Buffers; + +/// +/// EventSource for monitoring PooledArrayWriter performance and buffer operations. +/// +[EventSource(Name = "HotChocolate-Buffers-PooledArrayWriter")] +internal sealed class PooledArrayWriterEventSource : EventSource +{ + public static readonly PooledArrayWriterEventSource Log = new(); + + private PooledArrayWriterEventSource() { } + + [Event(1, Level = EventLevel.Informational)] + public void WriterCreated(int initialBufferSize, int actualCapacity) + { + WriteEvent(1, initialBufferSize, actualCapacity); + } + + [Event(2, Level = EventLevel.Warning)] + public void BufferResize(int oldSize, int newSize, int dataLength, int requestedSize, int resizeCount) + { + WriteEvent(2, oldSize, newSize, dataLength, requestedSize, resizeCount); + } + + [Event(3, Level = EventLevel.Verbose)] + public void MemoryRequested(int sizeHint, int actualSize, bool resizeRequired) + { + WriteEvent(3, sizeHint, actualSize, resizeRequired); + } + + [Event(4, Level = EventLevel.Verbose)] + public void WriterAdvanced(int count, int newPosition, int remainingCapacity) + { + WriteEvent(4, count, newPosition, remainingCapacity); + } + + [Event(5, Level = EventLevel.Informational)] + public void WriterReset(int previousLength, int capacity) + { + WriteEvent(5, previousLength, capacity); + } + + [Event(6, Level = EventLevel.Informational)] + public void WriterDisposed(int finalLength, int capacity, int totalResizes) + { + WriteEvent(6, finalLength, capacity, totalResizes); + } + + [Event(7, Level = EventLevel.Error)] + public void BufferOverflow(int requestedCount, int availableCapacity) + { + WriteEvent(7, requestedCount, availableCapacity); + } + + [Event(8, Level = EventLevel.Warning)] + public void LargeAllocation(int size, int threshold) + { + WriteEvent(8, size, threshold); + } +} diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/Properties/BuffersResources.Designer.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/Properties/BuffersResources.Designer.cs new file mode 100644 index 00000000000..6388029e0e7 --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/Properties/BuffersResources.Designer.cs @@ -0,0 +1,54 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace HotChocolate.Buffers.Properties { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class BuffersResources { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal BuffersResources() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("HotChocolate.Buffers.Properties.BuffersResources", typeof(BuffersResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string ArrayWriter_Advance_BufferOverflow { + get { + return ResourceManager.GetString("ArrayWriter_Advance_BufferOverflow", resourceCulture); + } + } + } +} diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/Properties/BuffersResources.resx b/src/HotChocolate/Utilities/src/Utilities.Buffers/Properties/BuffersResources.resx new file mode 100644 index 00000000000..5449972ccdc --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/Properties/BuffersResources.resx @@ -0,0 +1,24 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cannot advance past the end of the buffer. + + diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/ReadOnlyMemorySegment.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/ReadOnlyMemorySegment.cs new file mode 100644 index 00000000000..69553b65dd4 --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/ReadOnlyMemorySegment.cs @@ -0,0 +1,139 @@ +using System.Buffers; +#if NET8_0_OR_GREATER +#endif + +namespace HotChocolate.Buffers; + +/// +/// A segment of memory that is owned by a . +/// +public readonly struct ReadOnlyMemorySegment +{ + private readonly IMemoryOwner _owner; + private readonly int _start; + private readonly int _length; + + /// + /// Initializes a new instance of . + /// + /// + /// The owner of the memory segment. + /// + /// + /// The start index of the memory segment. + /// + /// + /// The length of the memory segment. + /// + public ReadOnlyMemorySegment(IMemoryOwner owner, int start, int length) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(owner); + ArgumentOutOfRangeException.ThrowIfLessThan(start, 0); + ArgumentOutOfRangeException.ThrowIfLessThan(length, 0); +#else + if (owner is null) + { + throw new ArgumentNullException(nameof(owner)); + } + + if (start < 0) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } +#endif + + _owner = owner; + _start = start; + _length = length; + } + + /// + /// Initializes a new instance of . + /// + /// + /// The buffer to create the memory segment from. + /// + public ReadOnlyMemorySegment(byte[] buffer) + : this(buffer, 0, buffer.Length) + { + } + + /// + /// Initializes a new instance of . + /// + /// + /// The buffer to create the memory segment from. + /// + /// + /// The start index of the memory segment. + /// + /// + /// The length of the memory segment. + /// + public ReadOnlyMemorySegment(byte[] buffer, int start, int length) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(buffer); + ArgumentOutOfRangeException.ThrowIfLessThan(start, 0); + ArgumentOutOfRangeException.ThrowIfLessThan(length, 0); +#else + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (start < 0) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } +#endif + _owner = new ArrayMemoryOwner(buffer, start, length); + _start = start; + _length = length; + } + + /// + /// Gets a value indicating whether the memory segment is empty. + /// + public bool IsEmpty => _owner is null; + + /// + /// Gets the length of the memory segment. + /// + public int Length => _length; + + /// + /// Gets the memory segment as a . + /// + public ReadOnlyMemory Memory + { + get + { + return _owner is not null + ? _owner.Memory.Slice(_start, _length) + : Array.Empty(); + } + } + + /// + /// Gets the memory segment as a . + /// + public ReadOnlySpan Span + { + get + { + return _owner is not null + ? _owner.Memory.Span.Slice(_start, _length) + : []; + } + } +}