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); } }