using AyCode.Core.Extensions; using MessagePack; using AyCode.Core.Interfaces; using AyCode.Core.Compression; using System.Buffers; using System.Runtime.CompilerServices; using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute; using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute; namespace AyCode.Services.SignalRs; public class IdMessage { public List Ids { get; private set; } public IdMessage() { Ids = []; } /// /// Creates IdMessage with multiple parameters serialized directly as JSON. /// Each parameter is serialized independently without array wrapping. /// Use object[] explicitly to pass multiple parameters. /// public IdMessage(object[] ids) { Ids = new List(ids.Length); for (var i = 0; i < ids.Length; i++) { Ids.Add(SerializeValue(ids[i])); } } /// /// Creates IdMessage with a single parameter serialized as JSON. /// Collections (List, Array, etc.) are serialized as a single JSON array. /// public IdMessage(object id) { Ids = new List(1) { SerializeValue(id) }; } /// /// Creates IdMessage with multiple Guid parameters. /// Each Guid is serialized as a separate Id entry. /// 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(SerializeGuid(idsArray[i])); } } /// /// Optimized serialization for common primitive types to avoid full JSON serialization overhead. /// Falls back to full JSON serialization for complex types or strings with special characters. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string SerializeValue(object value) { return value switch { int i => i.ToString(), long l => l.ToString(), Guid g => SerializeGuid(g), bool b => b ? "true" : "false", // Strings need proper JSON escaping for special characters string => value.ToJson(), _ => value.ToJson() }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string SerializeGuid(Guid g) { // Pre-allocate exact size: 38 chars = 2 quotes + 36 guid chars return string.Create(38, g, static (span, guid) => { span[0] = '"'; guid.TryFormat(span[1..], out _); span[37] = '"'; }); } public override string ToString() => string.Join("; ", Ids); } [MessagePackObject] public class SignalPostJsonMessage { [Key(0)] public string PostDataJson { get; set; } = ""; public SignalPostJsonMessage() { } protected SignalPostJsonMessage(string postDataJson) => PostDataJson = postDataJson; } [MessagePackObject(AllowPrivate = false)] public class SignalPostJsonDataMessage : SignalPostJsonMessage, ISignalPostMessage { [IgnoreMember] [JsonIgnore] [STJIgnore] private TPostDataType? _postData; [IgnoreMember] [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) { } } [MessagePackObject] public class SignalPostMessage(TPostData postData) : ISignalPostMessage { [Key(0)] public TPostData? PostData { get; set; } = postData; } public interface ISignalPostMessage : ISignalRMessage { TPostData? PostData { get; } } [MessagePackObject] public class SignalRequestByIdMessage(Guid id) : ISignalRequestMessage, IId { [Key(0)] 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 } /// /// Signal response message with lazy deserialization support. /// Used for callback-based response handling. /// [MessagePackObject(AllowPrivate = false)] public sealed class SignalResponseMessage : ISignalResponseMessage { [IgnoreMember] [JsonIgnore] [STJIgnore] private TResponseData? _responseData; [IgnoreMember] [JsonIgnore] [STJIgnore] private bool _isDeserialized; [Key(0)] public int MessageTag { get; set; } [Key(1)] public SignalResponseStatus Status { get; set; } [Key(2)] public string? ResponseDataJson { get; set; } [IgnoreMember] [JsonIgnore] [STJIgnore] public TResponseData? ResponseData { get { if (!_isDeserialized) { _isDeserialized = true; _responseData = ResponseDataJson != null ? ResponseDataJson.JsonTo() : default; } return _responseData; } set { _isDeserialized = true; _responseData = value; ResponseDataJson = value?.ToJson(); } } public SignalResponseMessage() { } public SignalResponseMessage(int messageTag, SignalResponseStatus status) { MessageTag = messageTag; Status = status; } public SignalResponseMessage(int messageTag, SignalResponseStatus status, TResponseData? responseData) : this(messageTag, status) => ResponseData = responseData; public SignalResponseMessage(int messageTag, SignalResponseStatus status, string? responseDataJson) : this(messageTag, status) => ResponseDataJson = responseDataJson; } /// /// 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[]? ResponseDataBin { get; set; } [JsonIgnore] [STJIgnore] private object? _cachedResponseData; [JsonIgnore] [STJIgnore] private byte[]? _rentedDecompressedBuffer; [JsonIgnore] [STJIgnore] private int _decompressedLength; 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; if (responseData == null) { ResponseDataBin = null; return; } if (serializerOptions.SerializerType == AcSerializerType.Binary) { if (responseData is byte[] byteData) { ResponseDataBin = byteData; return; } var binaryOptions = serializerOptions as AcBinarySerializerOptions ?? AcBinarySerializerOptions.Default; // Use ArrayBufferWriter for zero-copy serialization var writer = new ArrayBufferWriter(256); responseData.ToBinary(writer, binaryOptions); ResponseDataBin = writer.WrittenSpan.ToArray(); } else { string json; if (responseData is string strData) { var trimmed = strData.AsSpan().Trim(); if (trimmed.Length > 1 && (trimmed[0] == '{' || trimmed[0] == '[') && (trimmed[^1] == '}' || trimmed[^1] == ']')) json = strData; else { var jsonOptions = serializerOptions as AcJsonSerializerOptions ?? AcJsonSerializerOptions.Default; json = responseData.ToJson(jsonOptions); } } else { var jsonOptions = serializerOptions as AcJsonSerializerOptions ?? AcJsonSerializerOptions.Default; json = responseData.ToJson(jsonOptions); } ResponseDataBin = BrotliHelper.Compress(json); } } /// /// Deserializes the ResponseData to the specified type. /// For JSON mode, decompresses Brotli to pooled buffer and deserializes directly (no string allocation). /// Uses cached result for repeated calls. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public T? GetResponseData() { if (_cachedResponseData != null) return (T)_cachedResponseData; if (ResponseDataBin == null) return default; if (DataSerializerType == AcSerializerType.Binary) return (T)(_cachedResponseData = ResponseDataBin.BinaryTo()!); // Decompress Brotli to pooled buffer and deserialize directly from ReadOnlySpan (no string allocation) EnsureDecompressed(); var result = AcJsonDeserializer.Deserialize(new ReadOnlySpan(_rentedDecompressedBuffer, 0, _decompressedLength)); _cachedResponseData = result; return result; } /// /// Gets the decompressed JSON bytes as a ReadOnlySpan for direct processing. /// public ReadOnlySpan GetDecompressedJsonSpan() { if (ResponseDataBin == 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; var (buffer, length) = BrotliHelper.DecompressToRentedBuffer(ResponseDataBin.AsSpan()); _rentedDecompressedBuffer = buffer; _decompressedLength = length; } public void Dispose() { if (_rentedDecompressedBuffer != null) { ArrayPool.Shared.Return(_rentedDecompressedBuffer); _rentedDecompressedBuffer = null; } } } public interface IAcSignalRHubClient : IAcSignalRHubBase { Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId); }