356 lines
11 KiB
C#
356 lines
11 KiB
C#
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<string> Ids { get; private set; }
|
|
|
|
public IdMessage()
|
|
{
|
|
Ids = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates IdMessage with multiple parameters serialized directly as JSON.
|
|
/// Each parameter is serialized independently without array wrapping.
|
|
/// Use object[] explicitly to pass multiple parameters.
|
|
/// </summary>
|
|
public IdMessage(object[] ids)
|
|
{
|
|
Ids = new List<string>(ids.Length);
|
|
for (var i = 0; i < ids.Length; i++)
|
|
{
|
|
Ids.Add(SerializeValue(ids[i]));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates IdMessage with a single parameter serialized as JSON.
|
|
/// Collections (List, Array, etc.) are serialized as a single JSON array.
|
|
/// </summary>
|
|
public IdMessage(object id)
|
|
{
|
|
Ids = new List<string>(1) { SerializeValue(id) };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates IdMessage with multiple Guid parameters.
|
|
/// Each Guid is serialized as a separate Id entry.
|
|
/// </summary>
|
|
public IdMessage(IEnumerable<Guid> ids)
|
|
{
|
|
var idsArray = ids as Guid[] ?? ids.ToArray();
|
|
Ids = new List<string>(idsArray.Length);
|
|
for (var i = 0; i < idsArray.Length; i++)
|
|
{
|
|
Ids.Add(SerializeGuid(idsArray[i]));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<TPostDataType> : SignalPostJsonMessage, ISignalPostMessage<TPostDataType>
|
|
{
|
|
[IgnoreMember]
|
|
[JsonIgnore]
|
|
[STJIgnore]
|
|
private TPostDataType? _postData;
|
|
|
|
[IgnoreMember]
|
|
[JsonIgnore]
|
|
[STJIgnore]
|
|
public TPostDataType PostData
|
|
{
|
|
get => _postData ??= PostDataJson.JsonTo<TPostDataType>()!;
|
|
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>(TPostData postData) : ISignalPostMessage<TPostData>
|
|
{
|
|
[Key(0)]
|
|
public TPostData? PostData { get; set; } = postData;
|
|
}
|
|
|
|
public interface ISignalPostMessage<TPostData> : ISignalRMessage
|
|
{
|
|
TPostData? PostData { get; }
|
|
}
|
|
|
|
[MessagePackObject]
|
|
public class SignalRequestByIdMessage(Guid id) : ISignalRequestMessage<Guid>, IId<Guid>
|
|
{
|
|
[Key(0)]
|
|
public Guid Id { get; set; } = id;
|
|
}
|
|
|
|
public interface ISignalRequestMessage<TRequestId> : 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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Signal response message with lazy deserialization support.
|
|
/// Used for callback-based response handling.
|
|
/// </summary>
|
|
[MessagePackObject(AllowPrivate = false)]
|
|
public sealed class SignalResponseMessage<TResponseData> : 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<TResponseData>() : 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<byte>(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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public T? GetResponseData<T>()
|
|
{
|
|
if (_cachedResponseData != null) return (T)_cachedResponseData;
|
|
if (ResponseDataBin == null) return default;
|
|
|
|
if (DataSerializerType == AcSerializerType.Binary)
|
|
return (T)(_cachedResponseData = ResponseDataBin.BinaryTo<T>()!);
|
|
|
|
// Decompress Brotli to pooled buffer and deserialize directly from ReadOnlySpan (no string allocation)
|
|
EnsureDecompressed();
|
|
var result = AcJsonDeserializer.Deserialize<T>(new ReadOnlySpan<byte>(_rentedDecompressedBuffer, 0, _decompressedLength));
|
|
_cachedResponseData = result;
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the decompressed JSON bytes as a ReadOnlySpan for direct processing.
|
|
/// </summary>
|
|
public ReadOnlySpan<byte> GetDecompressedJsonSpan()
|
|
{
|
|
if (ResponseDataBin == null) return ReadOnlySpan<byte>.Empty;
|
|
if (DataSerializerType == AcSerializerType.Binary) return ReadOnlySpan<byte>.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<byte>.Shared.Return(_rentedDecompressedBuffer);
|
|
_rentedDecompressedBuffer = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public interface IAcSignalRHubClient : IAcSignalRHubBase
|
|
{
|
|
Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId);
|
|
} |