using System;
using System.Buffers;
using AyCode.Core.Serializers.Binaries;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.Logging;
namespace AyCode.Services.SignalRs;
///
/// Project-specific binary protocol.
///
/// Overrides the base / hooks to carry the
/// runtime type of the streamed / last data argument in each message. This is needed because
/// our OnReceiveMessage(int, int?, SignalParams, object) convention has the last argument
/// typed as object, so the binder can't tell the deserializer what concrete type to produce.
///
/// With the header in place, the concrete type travels on the wire and is available before the
/// (non-streamed) data argument is read, and before the streamed argument's Task.Run starts.
/// There is no dependency on reading SignalParams first, so it works regardless of whether
/// SignalParams is inline or streamed.
///
public class AyCodeBinaryHubProtocol : AcBinaryHubProtocol
{
///
/// Parsed SignalParams from current message (arg[2]).
/// Still used for , which opts out of deserialization.
///
private SignalParams? _currentSignalParams;
public AyCodeBinaryHubProtocol() : this(AcBinarySerializerOptions.Default) { }
public AyCodeBinaryHubProtocol(AcBinarySerializerOptions options, BinaryProtocolMode protocolMode = BinaryProtocolMode.Bytes, ILogger? logger = null) : base(options, protocolMode, logger) { }
#region Header: per-message concrete type of the data argument
///
/// Writes the AssemblyQualifiedName of the concrete type of the data argument.
///
/// When chunked mode is active, is the argument being streamed.
/// When non-chunked, we pick the last non-null argument from the message (project convention:
/// OnReceiveMessage(int, int?, SignalParams, object data) — the data arg is last).
///
///
protected override void WriteHeader(ref BufferWriterBinaryOutput bw, HubMessage message, object? streamedArg)
{
var typeSource = streamedArg ?? GetDataArg(message);
var typeName = typeSource?.GetType().AssemblyQualifiedName;
WriteNullableString(ref bw, typeName);
}
///
/// Reads the type AQN and resolves it via .
/// Returns the resolved (or null if absent / unresolvable).
///
protected override object? ReadHeader(ref SequenceReader r)
{
var typeName = ReadNullableString(ref r);
return typeName != null ? Type.GetType(typeName) : null;
}
private static object? GetDataArg(HubMessage message) => message switch
{
InvocationMessage m when m.Arguments.Length > 0 => m.Arguments[m.Arguments.Length - 1],
StreamInvocationMessage m when m.Arguments.Length > 0 => m.Arguments[m.Arguments.Length - 1],
StreamItemMessage m => m.Item,
CompletionMessage m => m.HasResult ? m.Result : null,
_ => null
};
#endregion
protected override void OnArgumentRead(object? value, int index)
{
if (value is SignalParams sp)
_currentSignalParams = sp;
}
protected override object? ReadSingleArgument(ref SequenceReader r, Type targetType)
{
r.TryReadLittleEndian(out int argLength);
if (argLength == 0)
return null;
// AsyncSegment: streamed arg marker (INT32 -1) → placeholder for chunked deserialization
if (argLength == -1)
return StreamedArgPlaceholder;
if (argLength == 1)
{
r.TryPeek(out byte marker);
if (marker == 0) { r.Advance(1); return null; }
}
var argSlice = r.UnreadSequence.Slice(0, argLength);
r.Advance(argLength);
// byte[] fast-path: tag only, no VarUInt (argLength implies size)
var argReader = new SequenceReader(argSlice);
if (argReader.TryPeek(out byte tag) && tag == BinaryTypeCode.ByteArray)
{
return SequenceToByteArray(argSlice.Slice(1));
}
// IsRawBytesData: return raw bytes, consumer deserializes later
if (_currentSignalParams is { IsRawBytesData: true })
return SequenceToByteArray(argSlice);
// Type resolution: prefer concrete type from the per-message header
if (targetType == typeof(object) && _currentHeaderContext is Type headerType)
targetType = headerType;
// Bytes mode: linearize to byte[] → ArrayBinaryInput (fastest deser, no segment overhead)
if (_protocolMode == BinaryProtocolMode.Bytes)
{
var bytes = SequenceToByteArray(argSlice);
return AcBinaryDeserializer.Deserialize(bytes, targetType, Options);
}
return DeserializeFromSequence(argSlice, targetType, Options);
}
}