using AyCode.Core.Extensions; using AyCode.Core.Interfaces; using System.Buffers; using System.Runtime.CompilerServices; using AyCode.Core.Serializers.Jsons; using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute; using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute; namespace AyCode.Services.SignalRs; /// /// Message container for serialized parameter IDs. /// Optimized for common primitive types to avoid full JSON overhead. /// public class IdMessage { public List Ids { get; private set; } public IdMessage() { Ids = []; } /// /// Creates IdMessage with multiple parameters serialized directly as JSON. /// public IdMessage(object[] ids) { Ids = new List(ids.Length); for (var i = 0; i < ids.Length; i++) { Ids.Add(SignalRSerializationHelper.SerializePrimitiveToJson(ids[i])); } } /// /// Creates IdMessage with a single parameter serialized as JSON. /// public IdMessage(object id) { Ids = [SignalRSerializationHelper.SerializePrimitiveToJson(id)]; } /// /// Creates IdMessage with multiple Guid parameters. /// public IdMessage(IEnumerable ids) { var idsArray = ids as Guid[] ?? ids.ToArray(); Ids = new List(idsArray.Length); for (var i = 0; i < idsArray.Length; i++) { Ids.Add(SignalRSerializationHelper.SerializeGuidToJson(idsArray[i])); } } public override string ToString() => string.Join("; ", Ids); } /// /// Message containing JSON-serialized post data. /// public class SignalPostJsonMessage { public string PostDataJson { get; set; } = ""; public SignalPostJsonMessage() { } protected SignalPostJsonMessage(string postDataJson) => PostDataJson = postDataJson; } /// /// Generic message containing JSON-serialized post data with typed access. /// public class SignalPostJsonDataMessage : SignalPostJsonMessage, ISignalPostMessage { [JsonIgnore] [STJIgnore] private TPostDataType? _postData; [JsonIgnore] [STJIgnore] public TPostDataType PostData { get => _postData ??= PostDataJson.JsonTo()!; private init { _postData = value; PostDataJson = _postData.ToJson(); } } public SignalPostJsonDataMessage() : base() { } public SignalPostJsonDataMessage(TPostDataType postData) => PostData = postData; public SignalPostJsonDataMessage(string postDataJson) : base(postDataJson) { } } /// /// Simple message containing post data. /// public class SignalPostMessage(TPostData postData) : ISignalPostMessage { public TPostData? PostData { get; set; } = postData; } public interface ISignalPostMessage : ISignalRMessage { TPostData? PostData { get; } } /// /// Message for requesting by Guid ID. /// public class SignalRequestByIdMessage(Guid id) : ISignalRequestMessage, IId { public Guid Id { get; set; } = id; } public interface ISignalRequestMessage : ISignalRMessage { TRequestId Id { get; set; } } public interface ISignalRMessage { } public interface ISignalResponseMessage : ISignalRMessage { int MessageTag { get; set; } SignalResponseStatus Status { get; set; } } public enum SignalResponseStatus : byte { Error = 0, Success = 5 } /// /// Unified signal response message that supports both JSON and Binary serialization. /// JSON mode uses Brotli compression for reduced payload size. /// Optimized: uses pooled buffers for decompression, zero-copy deserialization path. /// public sealed class SignalResponseDataMessage : ISignalResponseMessage, IDisposable { public int MessageTag { get; set; } public SignalResponseStatus Status { get; set; } public AcSerializerType DataSerializerType { get; set; } public byte[]? ResponseData { get; set; } [JsonIgnore] [STJIgnore] private object? _cachedResponseData; [JsonIgnore] [STJIgnore] private byte[]? _rentedDecompressedBuffer; [JsonIgnore] [STJIgnore] private int _decompressedLength; /// /// Enable diagnostic logging for ResponseData deserialization. /// When set, logs hex dump and header info before deserialization. /// [JsonIgnore] [STJIgnore] public static Action? DiagnosticLogger { get; set; } public SignalResponseDataMessage() { } public SignalResponseDataMessage(int messageTag, SignalResponseStatus status) { MessageTag = messageTag; Status = status; } public SignalResponseDataMessage(int messageTag, SignalResponseStatus status, object? responseData, AcSerializerOptions serializerOptions) : this(messageTag, status) { DataSerializerType = serializerOptions.SerializerType; ResponseData = SignalRSerializationHelper.CreateResponseData(responseData, serializerOptions); } /// /// Deserializes the ResponseData to the specified type. /// Uses cached result for repeated calls. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public T? GetResponseData() { if (_cachedResponseData != null) return (T)_cachedResponseData; if (ResponseData == null) return default; if (DataSerializerType == AcSerializerType.Binary) { try { // Log diagnostics if enabled LogResponseDataDiagnostics(); return (T)(_cachedResponseData = ResponseData.BinaryTo()!); } catch (Exception ex) { // Log detailed error diagnostics LogResponseDataError(ex); throw; } } // Decompress Brotli to pooled buffer and deserialize directly EnsureDecompressed(); var result = AcJsonDeserializer.Deserialize(new ReadOnlySpan(_rentedDecompressedBuffer, 0, _decompressedLength)); _cachedResponseData = result; return result; } private void LogResponseDataDiagnostics() { if (DiagnosticLogger == null || ResponseData == null) return; try { var targetType = typeof(T); DiagnosticLogger($"=== RESPONSE DATA DIAGNOSTICS (DESERIALIZE) ==="); DiagnosticLogger($"Target Type: {targetType.Name}"); DiagnosticLogger($"Target FullName: {targetType.FullName}"); DiagnosticLogger($"Target Namespace: {targetType.Namespace}"); DiagnosticLogger($"Target Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}"); DiagnosticLogger($"Target AssemblyQualifiedName: {targetType.AssemblyQualifiedName}"); DiagnosticLogger($"Target Assembly Location: {targetType.Assembly.Location}"); // Log element type for collections if (targetType.IsGenericType) { var genericArgs = targetType.GetGenericArguments(); DiagnosticLogger($"Generic Arguments: [{string.Join(", ", genericArgs.Select(t => t.FullName))}]"); if (genericArgs.Length == 1) { var elementType = genericArgs[0]; DiagnosticLogger($"--- ELEMENT TYPE INFO ---"); DiagnosticLogger($"Element Type: {elementType.Name}"); DiagnosticLogger($"Element FullName: {elementType.FullName}"); DiagnosticLogger($"Element Namespace: {elementType.Namespace}"); DiagnosticLogger($"Element Assembly: {elementType.Assembly.GetName().Name} v{elementType.Assembly.GetName().Version}"); DiagnosticLogger($"Element AssemblyQualifiedName: {elementType.AssemblyQualifiedName}"); DiagnosticLogger($"Element Assembly Location: {elementType.Assembly.Location}"); DiagnosticLogger($"Element BaseType: {elementType.BaseType?.FullName ?? "null"}"); // Log inheritance chain var baseType = elementType.BaseType; var inheritanceChain = new List(); while (baseType != null && baseType != typeof(object)) { inheritanceChain.Add($"{baseType.Name} ({baseType.Assembly.GetName().Name})"); baseType = baseType.BaseType; } if (inheritanceChain.Count > 0) { DiagnosticLogger($"Element Inheritance: {string.Join(" -> ", inheritanceChain)}"); } LogTypeProperties(elementType, "Element"); } } else { DiagnosticLogger($"BaseType: {targetType.BaseType?.FullName ?? "null"}"); LogTypeProperties(targetType, "Target"); } DiagnosticLogger($"ResponseData.Length: {ResponseData.Length}"); DiagnosticLogger($"HEX (first 500 bytes): {Convert.ToHexString(ResponseData.AsSpan(0, Math.Min(500, ResponseData.Length)))}"); // Parse header with VarInt support LogBinaryHeader(ResponseData); } catch (Exception ex) { DiagnosticLogger($"Failed to log diagnostics: {ex.Message}"); } } private void LogTypeProperties(Type type, string prefix) { if (DiagnosticLogger == null) return; var props = type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) .ToArray(); // Log in declaration order (not alphabetically) to match serialization order DiagnosticLogger($"{prefix} Property Count: {props.Length}"); for (int i = 0; i < props.Length; i++) { var p = props[i]; var declaringType = p.DeclaringType?.Name ?? "?"; var declaringAssembly = p.DeclaringType?.Assembly.GetName().Name ?? "?"; DiagnosticLogger($" {prefix}[{i}]: {p.Name} : {p.PropertyType.Name} (declared in {declaringType} @ {declaringAssembly})"); } } private void LogBinaryHeader(byte[] data) { if (DiagnosticLogger == null || data.Length < 3) return; var version = data[0]; var marker = data[1]; DiagnosticLogger($"Binary Version: {version}; Marker: 0x{marker:X2}"); // Check if metadata flag is set if ((marker & 0x10) == 0) { DiagnosticLogger("Header: No metadata (property names inline)"); return; } // Read property count as VarUInt var pos = 2; var (propCount, bytesRead) = ReadVarUIntFromSpan(data.AsSpan(pos)); pos += bytesRead; DiagnosticLogger($"Header Property Count: {propCount}"); for (int i = 0; i < (int)propCount && pos < data.Length; i++) { // Read string length as VarUInt var (strLen, strLenBytes) = ReadVarUIntFromSpan(data.AsSpan(pos)); pos += strLenBytes; if (pos + (int)strLen <= data.Length) { var propName = System.Text.Encoding.UTF8.GetString(data, pos, (int)strLen); pos += (int)strLen; DiagnosticLogger($" Header[{i}]: '{propName}'"); } else { DiagnosticLogger($" Header[{i}]: "); break; } } } private static (uint value, int bytesRead) ReadVarUIntFromSpan(ReadOnlySpan span) { uint value = 0; int shift = 0; int bytesRead = 0; while (bytesRead < span.Length) { var b = span[bytesRead++]; value |= (uint)(b & 0x7F) << shift; if ((b & 0x80) == 0) break; shift += 7; if (shift > 35) break; } return (value, bytesRead); } private void LogResponseDataError(Exception error) { if (DiagnosticLogger == null || ResponseData == null) return; try { var targetType = typeof(T); DiagnosticLogger($"=== RESPONSE DATA DESERIALIZATION ERROR ==="); DiagnosticLogger($"Error: {error.Message}"); DiagnosticLogger($"Target Type: {targetType.Name}"); DiagnosticLogger($"Target FullName: {targetType.FullName}"); DiagnosticLogger($"Target Namespace: {targetType.Namespace}"); DiagnosticLogger($"Target Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}"); DiagnosticLogger($"Target AssemblyQualifiedName: {targetType.AssemblyQualifiedName}"); // Log element type for collections if (targetType.IsGenericType) { var genericArgs = targetType.GetGenericArguments(); DiagnosticLogger($"Generic Arguments: [{string.Join(", ", genericArgs.Select(t => t.FullName))}]"); if (genericArgs.Length == 1) { var elementType = genericArgs[0]; DiagnosticLogger($"Element Type: {elementType.FullName}"); DiagnosticLogger($"Element Assembly: {elementType.Assembly.GetName().Name}"); LogTypeProperties(elementType, "Element"); } } else { LogTypeProperties(targetType, "Target"); } DiagnosticLogger($"ResponseData.Length: {ResponseData.Length}"); DiagnosticLogger($"HEX (first 1000 bytes): {Convert.ToHexString(ResponseData.AsSpan(0, Math.Min(1000, ResponseData.Length)))}"); // Parse header LogBinaryHeader(ResponseData); // Log inner exception if present if (error.InnerException != null) { DiagnosticLogger($"Inner Exception: {error.InnerException.Message}"); } // Log stack trace DiagnosticLogger($"Stack Trace: {error.StackTrace}"); } catch (Exception ex) { DiagnosticLogger?.Invoke($"Failed to log error diagnostics: {ex.Message}"); } } /// /// Gets the decompressed JSON bytes as a ReadOnlySpan for direct processing. /// public ReadOnlySpan GetDecompressedJsonSpan() { if (ResponseData == null) return ReadOnlySpan.Empty; if (DataSerializerType == AcSerializerType.Binary) return ReadOnlySpan.Empty; EnsureDecompressed(); return _rentedDecompressedBuffer.AsSpan(0, _decompressedLength); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureDecompressed() { if (_rentedDecompressedBuffer != null) return; (_rentedDecompressedBuffer, _decompressedLength) = SignalRSerializationHelper.DecompressToRentedBuffer(ResponseData!); } public void Dispose() { } } public interface IAcSignalRHubClient : IAcSignalRHubBase { Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId); }