diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index bff6a29..bdc223e 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -50,7 +50,8 @@
"Read(//h/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/**)",
"Bash(2)",
"Bash(dotnet --version)",
- "WebSearch"
+ "WebSearch",
+ "Bash(dotnet script:*)"
]
}
}
diff --git a/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs
index 8affdbd..229578b 100644
--- a/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs
+++ b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs
@@ -71,7 +71,12 @@ public struct SequenceBinaryInput : IBinaryInputBase
buffer = _savedBuffer!;
position = _savedPosition;
bufferLength = _savedBufferLength;
- return position < bufferLength;
+
+ // Restored segment has enough bytes for the requested read — done
+ if (bufferLength - position >= needed)
+ return true;
+
+ // Not enough bytes in restored segment — fall through to cross-boundary or next segment
}
var remaining = bufferLength - position;
diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs
index 41581f2..b97f595 100644
--- a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs
@@ -1,4 +1,5 @@
-using AyCode.Core.Extensions;
+using System.Buffers;
+using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
@@ -7,6 +8,7 @@ using AyCode.Services.SignalRs;
using MessagePack.Resolvers;
using AyCode.Core.Tests.Serialization;
using AyCode.Core.Serializers;
+using Microsoft.AspNetCore.SignalR.Protocol;
namespace AyCode.Services.Server.Tests.SignalRs;
@@ -1049,6 +1051,35 @@ public abstract class SignalRClientToHubTestBase
///
/// Runs all SignalR tests with JSON serialization.
///
+///
+/// Diagnostic test: isolates protocol round-trip from the full client-hub flow.
+///
+[TestClass]
+public class ProtocolRoundTripDiagnosticTest
+{
+ [TestMethod]
+ public void Protocol_RoundTrip_BasicInvocation()
+ {
+ var protocol = new AyCodeBinaryHubProtocol();
+ var binder = new TestInvocationBinder();
+
+ var signalParams = new SignalParams { Status = SignalResponseStatus.Success };
+ var invocation = new InvocationMessage(
+ nameof(IAcSignalRHubClient.OnReceiveMessage),
+ new object?[] { 42, (int?)1, signalParams, Array.Empty() });
+
+ var bytes = protocol.GetMessageBytes(invocation);
+
+ // Diagnostic: dump first bytes
+ var arr = bytes.ToArray();
+ var lengthPrefix = BitConverter.ToInt32(arr, 0);
+ var firstByte = arr.Length > 4 ? arr[4] : (byte)0;
+
+ Assert.Fail($"Bytes={arr.Length}, LengthPrefix={lengthPrefix}, Remaining={arr.Length - 4}, " +
+ $"FirstPayloadByte=0x{firstByte:X2}, First16={BitConverter.ToString(arr, 0, Math.Min(16, arr.Length))}");
+ }
+}
+
[TestClass]
public class SignalRClientToHubTest_Json : SignalRClientToHubTestBase
{
diff --git a/AyCode.Services.Server.Tests/SignalRs/TestInvocationBinder.cs b/AyCode.Services.Server.Tests/SignalRs/TestInvocationBinder.cs
new file mode 100644
index 0000000..d4e37da
--- /dev/null
+++ b/AyCode.Services.Server.Tests/SignalRs/TestInvocationBinder.cs
@@ -0,0 +1,21 @@
+using AyCode.Services.SignalRs;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.AspNetCore.SignalR.Protocol;
+
+namespace AyCode.Services.Server.Tests.SignalRs;
+
+///
+/// Test IInvocationBinder that returns OnReceiveMessage parameter types
+/// for protocol deserialization: (int messageTag, int? requestId, SignalParams signalParams, object data).
+///
+internal class TestInvocationBinder : IInvocationBinder
+{
+ private static readonly Type[] OnReceiveMessageTypes =
+ [typeof(int), typeof(int?), typeof(SignalParams), typeof(object)];
+
+ public IReadOnlyList GetParameterTypes(string methodName) => OnReceiveMessageTypes;
+
+ public Type GetReturnType(string invocationId) => typeof(object);
+
+ public Type GetStreamItemType(string streamId) => typeof(object);
+}
diff --git a/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs
new file mode 100644
index 0000000..4840708
--- /dev/null
+++ b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs
@@ -0,0 +1,52 @@
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using AyCode.Services.SignalRs;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.AspNetCore.SignalR.Protocol;
+
+namespace AyCode.Services.Server.Tests.SignalRs;
+
+///
+/// Test protocol that forces multi-segment ReadOnlySequence parsing.
+/// Splits serialized bytes into chunks before calling base.TryParseMessage,
+/// exercising SequenceBinaryInput cross-boundary reads and SequenceToByteArray multi-segment paths.
+///
+internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
+{
+ private const int SegmentSize = 4096;
+
+ public override bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder,
+ [NotNullWhen(true)] out HubMessage? message)
+ {
+ // Temporarily bypass multi-segment to isolate the issue
+ return base.TryParseMessage(ref input, binder, out message);
+ }
+
+ private static ReadOnlySequence CreateMultiSegmentSequence(ReadOnlySequence source, int chunkSize)
+ {
+ var bytes = source.ToArray();
+
+ var first = new MemorySegment(bytes.AsMemory(0, Math.Min(chunkSize, bytes.Length)));
+ var current = first;
+
+ for (var offset = chunkSize; offset < bytes.Length; offset += chunkSize)
+ {
+ var length = Math.Min(chunkSize, bytes.Length - offset);
+ current = current.Append(bytes.AsMemory(offset, length));
+ }
+
+ return new ReadOnlySequence(first, 0, current, current.Memory.Length);
+ }
+
+ private sealed class MemorySegment : ReadOnlySequenceSegment
+ {
+ public MemorySegment(ReadOnlyMemory memory) => Memory = memory;
+
+ public MemorySegment Append(ReadOnlyMemory memory)
+ {
+ var next = new MemorySegment(memory) { RunningIndex = RunningIndex + Memory.Length };
+ Next = next;
+ return next;
+ }
+ }
+}
diff --git a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs
index 2fdcbf1..c6fb215 100644
--- a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs
@@ -1,3 +1,4 @@
+using System.Buffers;
using AyCode.Core;
using AyCode.Core.Extensions;
using AyCode.Core.Tests.TestModels;
@@ -5,16 +6,21 @@ using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;
using AyCode.Services.Tests.SignalRs;
using Microsoft.AspNetCore.SignalR.Client;
+using Microsoft.AspNetCore.SignalR.Protocol;
namespace AyCode.Services.Server.Tests.SignalRs;
///
/// Testable SignalR client that allows testing without real HubConnection.
+/// Routes all messages through AyCodeBinaryHubProtocol with multi-segment splitting
+/// to exercise the full serialization/deserialization pipeline.
///
public class TestableSignalRClient2 : AcSignalRClientBase, IAcSignalRHubItemServer
{
private HubConnectionState _connectionState = HubConnectionState.Connected;
private readonly TestableSignalRHub2 _signalRHub;
+ private readonly TestMultiSegmentProtocol _protocol = new();
+ private readonly TestInvocationBinder _binder = new();
///
/// Testable SignalR client that allows testing without real HubConnection.
@@ -35,7 +41,7 @@ public class TestableSignalRClient2 : AcSignalRClientBase, IAcSignalRHubItemServ
}
protected override HubConnectionState GetConnectionState() => _connectionState;
-
+
protected override bool IsConnected() => _connectionState == HubConnectionState.Connected;
protected override Task StartConnectionInternal()
@@ -54,7 +60,21 @@ public class TestableSignalRClient2 : AcSignalRClientBase, IAcSignalRHubItemServ
protected override Task SendToHubAsync(int messageTag, int? requestId, SignalParams signalParams, object? data)
{
- return _signalRHub.OnReceiveMessage(messageTag, requestId, signalParams, data ?? Array.Empty());
+ // Protocol round-trip: serialize → multi-segment split → deserialize
+ var invocation = new InvocationMessage(
+ nameof(IAcSignalRHubClient.OnReceiveMessage),
+ [messageTag, requestId, signalParams, data ?? Array.Empty()]);
+
+ var bytes = _protocol.GetMessageBytes(invocation);
+ var sequence = new ReadOnlySequence(bytes);
+ if (!_protocol.TryParseMessage(ref sequence, _binder, out var parsed) || parsed is not InvocationMessage invMsg)
+ throw new InvalidOperationException(
+ $"Protocol round-trip failed. ByteCount={bytes.Length}, Parsed={parsed?.GetType().Name ?? "null"}, " +
+ $"Tag={messageTag}, RequestId={requestId}, DataType={data?.GetType().Name ?? "null"}");
+
+ var args = invMsg.Arguments;
+ return _signalRHub.OnReceiveMessage(
+ (int)args[0]!, (int?)args[1], (SignalParams)args[2]!, args[3]!);
}
#endregion
diff --git a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs
index 9310ad3..db0809e 100644
--- a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs
@@ -1,9 +1,14 @@
+using System.Buffers;
using System.Security.Claims;
+using AyCode.Core;
+using AyCode.Core.Extensions;
using AyCode.Core.Serializers;
+using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
using AyCode.Models.Server.DynamicMethods;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;
+using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.Configuration;
namespace AyCode.Services.Server.Tests.SignalRs;
@@ -11,12 +16,14 @@ namespace AyCode.Services.Server.Tests.SignalRs;
///
/// Testable SignalR hub that overrides infrastructure dependencies.
/// Enables unit testing without SignalR server or mocks.
-/// Uses base SendMessageToClient which sends raw objects directly.
-/// GetResponseData<T>() handles deserialization with 3-tier fallback.
+/// Routes responses through AyCodeBinaryHubProtocol with multi-segment splitting
+/// to exercise the full serialization/deserialization pipeline.
///
public class TestableSignalRHub2 : AcWebSignalRHubBase
{
private IAcSignalRHubItemServer _callerClient;
+ private readonly TestMultiSegmentProtocol _protocol = new();
+ private readonly TestInvocationBinder _binder = new();
#region Test Configuration
@@ -90,5 +97,39 @@ public class TestableSignalRHub2 : AcWebSignalRHubBase SendMessageToClient(_callerClient, messageTag, status, responseData, requestId, clientSignalParams);
+ protected override Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag,
+ SignalResponseStatus status, object? responseData, int? requestId = null, SignalParams? clientSignalParams = null)
+ {
+ var isRawBytes = clientSignalParams?.IsRawBytesData == true;
+
+ if (isRawBytes && responseData != null && responseData is not byte[])
+ {
+ responseData = SerializerOptions.SerializerType == AcSerializerType.Binary
+ ? AcBinarySerializer.Serialize(responseData)
+ : AyCode.Core.Compression.GzipHelper.Compress(responseData.ToJson());
+ }
+
+ var signalParams = new SignalParams
+ {
+ Status = status,
+ DataSerializerType = SerializerOptions.SerializerType,
+ SignalDataType = isRawBytes ? null : responseData?.GetType().AssemblyQualifiedName,
+ IsRawBytesData = isRawBytes
+ };
+
+ // Protocol round-trip: serialize → multi-segment split → deserialize
+ var invocation = new InvocationMessage(
+ nameof(IAcSignalRHubClient.OnReceiveMessage),
+ [messageTag, requestId, signalParams, responseData ?? Array.Empty()]);
+
+ var bytes = _protocol.GetMessageBytes(invocation);
+ var sequence = new ReadOnlySequence(bytes);
+ _protocol.TryParseMessage(ref sequence, _binder, out var parsed);
+
+ var args = ((InvocationMessage)parsed!).Arguments;
+ return sendTo.OnReceiveMessage(
+ (int)args[0]!, (int?)args[1], (SignalParams)args[2]!, args[3]!);
+ }
+
#endregion
}
diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs
index c03b955..2626655 100644
--- a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs
+++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs
@@ -209,7 +209,7 @@ public class AcBinaryHubProtocol : IHubProtocol
#region TryParseMessage
- public bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder, [NotNullWhen(true)] out HubMessage? message)
+ public virtual bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder, [NotNullWhen(true)] out HubMessage? message)
{
message = null;