118 lines
6.0 KiB
C#
118 lines
6.0 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<AcBinaryHubProtocolOptions>(...)</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.Coalesced;
|
||
|
||
/// <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 < 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<T></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
|
||
};
|
||
}
|