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

118 lines
6.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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><see cref="FlushPolicy.PerChunk"/>: commit → flush → await each chunk before
/// acquiring the next. Strictly bounded peak memory (~chunk × 1). No producer/flush
/// parallelism. Best for memory-sensitive servers and unpredictable payload sizes.</item>
/// <item><see cref="FlushPolicy.DoubleBuffered"/>: fire-and-forget previous flush; next chunk
/// waits only if previous flush hasn't completed. Peak memory ~chunk × 2 with maximum
/// producer/flush parallelism — the recommended balanced choice for typical streaming.</item>
/// <item><see cref="FlushPolicy.Coalesced"/> (default for SignalR): no per-chunk wait; the
/// underlying Pipe coalesces flushes adaptively up to its PauseWriterThreshold (~64 KB).
/// Highest throughput on bounded payloads — fast consumer never triggers a wait, slow
/// consumer naturally throttles through buffer pressure.</item>
/// </list>
/// Ignored for Bytes and Segment modes.
/// </summary>
public FlushPolicy FlushPolicy { get; set; } = FlushPolicy.DoubleBuffered;
/// <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,
FlushPolicy = FlushPolicy,
FlushTimeout = FlushTimeout,
Name = Name,
Logger = Logger
};
}