AyCode.Core/AyCode.Services/SignalRs/AcBinaryHubProtocolOptions.cs

115 lines
5.8 KiB
C#

using AyCode.Core.Serializers.Binaries;
using Microsoft.Extensions.Logging;
namespace AyCode.Services.SignalRs;
/// <summary>
/// Configuration for <see cref="AcBinaryHubProtocol"/> and derived protocols.
/// Use via <c>services.Configure&lt;AcBinaryHubProtocolOptions&gt;(...)</c> in Program.cs,
/// or pass an instance directly to the protocol constructor.
/// </summary>
public sealed class AcBinaryHubProtocolOptions
{
// --- Serializer (standalone sub-group, also usable without SignalR via ToBinary/BinaryTo) ---
/// <summary>Binary serializer options. Default: <see cref="AcBinarySerializerOptions.Default"/>.</summary>
public AcBinarySerializerOptions SerializerOptions { get; set; } = AcBinarySerializerOptions.Default;
// --- Transport ---
/// <summary>Wire format and pipeline strategy. Default: <see cref="BinaryProtocolMode.Bytes"/>.</summary>
public BinaryProtocolMode ProtocolMode { get; set; } = BinaryProtocolMode.Bytes;
/// <summary>Chunk size for BufferWriterBinaryOutput / AsyncPipeWriterOutput.
/// Default: 4096 (aligns with Kestrel's slab size for optimal latency-to-first-byte).</summary>
public int BufferSize { get; set; } = 4096;
/// <summary>
/// Per-chunk flush synchronization in <see cref="BinaryProtocolMode.AsyncSegment"/> mode.
/// <list type="bullet">
/// <item><c>true</c> (default): every Grow waits for the previous FlushAsync to complete
/// before starting the next chunk. Guarantees end-to-end zero-copy (no owned-buffer fallback)
/// and maximum pipeline parallelism. Best for high-throughput servers with fast consumers.</item>
/// <item><c>false</c>: fire-and-forget flush per chunk; blocks only when committed bytes exceed
/// the memory threshold (~60 KB). Itself an adaptive backpressure mode — a fast consumer
/// never triggers a wait, a slow consumer naturally throttles through buffer pressure.
/// Safer for mixed consumer speeds and memory-sensitive environments.</item>
/// </list>
/// Ignored for Bytes and Segment modes.
/// </summary>
public bool WaitForFlush { get; set; } = true;
/// <summary>
/// Maximum wait for a single synchronous <c>FlushAsync</c> before throwing
/// <see cref="TimeoutException"/>. Protects against slow/stuck/disconnected consumers
/// blocking the server thread indefinitely.
/// <para>
/// Default: 10 seconds. Rationale: AsyncSegment chunks are max 65 KB (UINT16 size field);
/// even GPRS-class connections (~60 Kbit/s) transfer 65 KB in ~9 s. If a flush exceeds 10 s,
/// the consumer is effectively stuck — faster failure detection is preferable to indefinite blocking.
/// </para>
/// <para>
/// Note: complementary to SignalR's connection-level timeouts (<c>ClientTimeoutInterval</c>,
/// <c>KeepAliveInterval</c>). This is a per-operation guard; SignalR timeouts manage the overall
/// connection lifetime. Set <c>FlushTimeout &lt; ClientTimeoutInterval</c> so this guard fires first.
/// </para>
/// Set to <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> to disable.
/// </summary>
public TimeSpan FlushTimeout { get; set; } = TimeSpan.FromSeconds(10);
// --- Identity ---
/// <summary>Protocol name in the SignalR handshake. Client and server must match. Default: "acbinary".</summary>
public string Name { get; set; } = "acbinary";
// --- Diagnostics ---
/// <summary>Optional logger. When null, no protocol-internal logging occurs.</summary>
public ILogger? Logger { get; set; }
// --- Validation ---
/// <summary>
/// Validates option values and platform compatibility.
/// Throws <see cref="ArgumentOutOfRangeException"/> / <see cref="ArgumentException"/> on invalid values,
/// or <see cref="PlatformNotSupportedException"/> for unsupported platform/mode combinations.
/// </summary>
public void Validate()
{
// WASM + AsyncSegment send-path guard — the AsyncPipeWriterOutput sync-over-async flush
// would block the browser's single UI thread. The receive side converts chunked wire to
// synchronous deser automatically, so WASM clients can still *receive* AsyncSegment data
// from a non-WASM server — they just cannot *send* via AsyncSegment themselves.
if (OperatingSystem.IsBrowser() && ProtocolMode == BinaryProtocolMode.AsyncSegment)
throw new PlatformNotSupportedException(
"BinaryProtocolMode.AsyncSegment is not supported on WebAssembly. " +
"Use BinaryProtocolMode.Bytes or BinaryProtocolMode.Segment instead.");
if (BufferSize < 256 || BufferSize > AsyncPipeWriterOutput.MaxChunkSize)
throw new ArgumentOutOfRangeException(nameof(BufferSize), BufferSize,
$"BufferSize must be between 256 and {AsyncPipeWriterOutput.MaxChunkSize} (UINT16 max).");
if (FlushTimeout <= TimeSpan.Zero && FlushTimeout != System.Threading.Timeout.InfiniteTimeSpan)
throw new ArgumentOutOfRangeException(nameof(FlushTimeout), FlushTimeout,
"FlushTimeout must be positive, or Timeout.InfiniteTimeSpan to disable.");
if (string.IsNullOrWhiteSpace(Name))
throw new ArgumentException("Name cannot be null or whitespace.", nameof(Name));
}
/// <summary>
/// Creates a shallow copy. Required for DI <c>IOptions&lt;T&gt;</c> integration —
/// the singleton from DI must not be mutated by per-connection <c>configure</c> callbacks.
/// </summary>
public AcBinaryHubProtocolOptions Clone() => new()
{
SerializerOptions = SerializerOptions,
ProtocolMode = ProtocolMode,
BufferSize = BufferSize,
WaitForFlush = WaitForFlush,
FlushTimeout = FlushTimeout,
Name = Name,
Logger = Logger
};
}