using System.Buffers; using System.Runtime.CompilerServices; using AyCode.Core.Compression; using AyCode.Core.Extensions; using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; namespace AyCode.Services.SignalRs; /// /// Centralized helper for SignalR serialization operations. /// Provides optimized primitives for JSON/Binary serialization with pooled buffers. /// public static class SignalRSerializationHelper { // Pre-boxed boolean values to avoid repeated boxing private static readonly string JsonTrue = "true"; private static readonly string JsonFalse = "false"; #region Primitive JSON Serialization /// /// Serialize a primitive value to JSON string with minimal overhead. /// Falls back to full JSON serialization for complex types. /// [Obsolete("Use direct object[] binary serialization instead of JSON-encoded primitives.")] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string SerializePrimitiveToJson(object value) { return value switch { int i => i.ToString(), long l => l.ToString(), Guid g => SerializeGuidToJson(g), bool b => b ? JsonTrue : JsonFalse, // Strings need proper JSON escaping for special characters string => value.ToJson(), _ => value.ToJson() }; } /// /// Serialize a Guid to JSON string with pre-allocated buffer. /// [Obsolete("Use direct object[] binary serialization instead of JSON-encoded Guids.")] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string SerializeGuidToJson(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] = '"'; }); } #endregion #region Binary Serialization /// /// Serialize object to binary using pooled ArrayBufferWriter. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] SerializeToBinary(T value, AcBinarySerializerOptions? options = null) { var writer = new ArrayBufferWriter(256); value.ToBinary(writer, options ?? AcBinarySerializerOptions.Default); return writer.WrittenSpan.ToArray(); } /// /// Serialize object to binary and write to existing ArrayBufferWriter. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SerializeToBinary(T value, ArrayBufferWriter writer, AcBinarySerializerOptions? options = null) { value.ToBinary(writer, options ?? AcBinarySerializerOptions.Default); } /// /// Deserialize binary data to object. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? DeserializeFromBinary(byte[] data) { return data.BinaryTo(); } /// /// Deserialize binary data from ReadOnlySpan. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? DeserializeFromBinary(ReadOnlySpan data) { return data.BinaryTo(); } /// /// Serialize method parameters as individually length-prefixed binary segments. /// Format: [VarUInt count] [for each: INT32 length + AcBinary bytes] /// This allows the server to deserialize each parameter with its known target type. /// public static byte[] SerializeParametersToBinary(object[] parameters, AcBinarySerializerOptions? options = null) { var opts = options ?? AcBinarySerializerOptions.Default; var writer = new ArrayBufferWriter(256); // Write parameter count as VarUInt WriteVarUInt(writer, (uint)parameters.Length); // Write each parameter with INT32 length prefix for (var i = 0; i < parameters.Length; i++) { var paramWriter = new ArrayBufferWriter(128); parameters[i].ToBinary(paramWriter, opts); // Write length prefix var lenSpan = writer.GetSpan(4); System.Runtime.CompilerServices.Unsafe.WriteUnaligned(ref lenSpan[0], paramWriter.WrittenCount); writer.Advance(4); // Write parameter bytes var paramSpan = writer.GetSpan(paramWriter.WrittenCount); paramWriter.WrittenSpan.CopyTo(paramSpan); writer.Advance(paramWriter.WrittenCount); } return writer.WrittenSpan.ToArray(); } /// /// Deserialize method parameters from length-prefixed binary format, using target types. /// public static object?[] DeserializeParametersFromBinary(byte[] data, System.Reflection.ParameterInfo[] paramInfos) { var span = data.AsSpan(); var pos = 0; // Read parameter count var count = (int)ReadVarUInt(span, ref pos); var result = new object?[paramInfos.Length]; for (var i = 0; i < count && i < paramInfos.Length; i++) { // Read length prefix var len = System.Runtime.CompilerServices.Unsafe.ReadUnaligned(ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span.Slice(pos))); pos += 4; if (len > 0) { var paramBytes = span.Slice(pos, len).ToArray(); result[i] = paramBytes.BinaryTo(paramInfos[i].ParameterType); pos += len; } } // Fill remaining with defaults for (var i = count; i < paramInfos.Length; i++) { if (paramInfos[i].HasDefaultValue) result[i] = paramInfos[i].DefaultValue; } return result; } private static void WriteVarUInt(ArrayBufferWriter writer, uint value) { while (value >= 0x80) { var span = writer.GetSpan(1); span[0] = (byte)(value | 0x80); writer.Advance(1); value >>= 7; } var lastSpan = writer.GetSpan(1); lastSpan[0] = (byte)value; writer.Advance(1); } private static uint ReadVarUInt(ReadOnlySpan span, ref int pos) { uint value = 0; var shift = 0; while (true) { var b = span[pos++]; value |= (uint)(b & 0x7F) << shift; if ((b & 0x80) == 0) return value; shift += 7; } } #endregion #region JSON Serialization with Brotli /// /// Serialize object to JSON and compress with Brotli. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] SerializeToCompressedJson(T value, AcJsonSerializerOptions? options = null) { var json = value.ToJson(options ?? AcJsonSerializerOptions.Default); return GzipHelper.Compress(json); } /// /// Decompress Brotli data and deserialize JSON to object. /// Uses pooled buffer for decompression. /// public static T? DeserializeFromCompressedJson(byte[] compressedData) { var (buffer, length) = GzipHelper.DecompressToRentedBuffer(compressedData.AsSpan()); try { return AcJsonDeserializer.Deserialize(new ReadOnlySpan(buffer, 0, length)); } finally { ArrayPool.Shared.Return(buffer); } } /// /// Decompress Brotli data to rented buffer for direct processing. /// Caller must return buffer to ArrayPool. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (byte[] Buffer, int Length) DecompressToRentedBuffer(byte[] compressedData) { return GzipHelper.DecompressToRentedBuffer(compressedData.AsSpan()); } #endregion #region Response Data Helpers /// /// Check if string appears to be valid JSON (starts with { or [). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsValidJsonString(ReadOnlySpan text) { var trimmed = text.Trim(); return trimmed.Length > 1 && (trimmed[0] == '{' || trimmed[0] == '[') && (trimmed[^1] == '}' || trimmed[^1] == ']'); } /// /// Create response binary data based on serializer type. /// public static byte[]? CreateResponseData(object? responseData, AcSerializerOptions serializerOptions) { if (responseData == null) return null; if (serializerOptions.SerializerType == AcSerializerType.Binary) { if (responseData is byte[] byteData) return byteData; var binaryOptions = serializerOptions as AcBinarySerializerOptions ?? AcBinarySerializerOptions.Default; return SerializeToBinary(responseData, binaryOptions); } // JSON mode with Brotli compression string json; if (responseData is string strData && IsValidJsonString(strData.AsSpan())) { json = strData; } else { var jsonOptions = serializerOptions as AcJsonSerializerOptions ?? AcJsonSerializerOptions.Default; json = responseData.ToJson(jsonOptions); } return GzipHelper.Compress(json); } #endregion }