using AyCode.Core.Serializers.Binaries; using Microsoft.Extensions.Logging; namespace AyCode.Services.SignalRs; /// /// Configuration for and derived protocols. /// Use via services.Configure<AcBinaryHubProtocolOptions>(...) in Program.cs, /// or pass an instance directly to the protocol constructor. /// public sealed class AcBinaryHubProtocolOptions { // --- Serializer (standalone sub-group, also usable without SignalR via ToBinary/BinaryTo) --- /// Binary serializer options. Default: . public AcBinarySerializerOptions SerializerOptions { get; set; } = AcBinarySerializerOptions.Default; // --- Transport --- /// Wire format and pipeline strategy. Default: . public BinaryProtocolMode ProtocolMode { get; set; } = BinaryProtocolMode.Bytes; /// Chunk size for BufferWriterBinaryOutput / AsyncPipeWriterOutput. /// Default: 4096 (aligns with Kestrel's slab size for optimal latency-to-first-byte). public int BufferSize { get; set; } = 4096; /// /// Per-chunk flush synchronization in mode. /// /// : 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. /// : 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. /// (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. /// /// Ignored for Bytes and Segment modes. /// public FlushPolicy FlushPolicy { get; set; } = FlushPolicy.Coalesced; /// /// Maximum wait for a single synchronous FlushAsync before throwing /// . Protects against slow/stuck/disconnected consumers /// blocking the server thread indefinitely. /// /// 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. /// /// /// Note: complementary to SignalR's connection-level timeouts (ClientTimeoutInterval, /// KeepAliveInterval). This is a per-operation guard; SignalR timeouts manage the overall /// connection lifetime. Set FlushTimeout < ClientTimeoutInterval so this guard fires first. /// /// Set to to disable. /// public TimeSpan FlushTimeout { get; set; } = TimeSpan.FromSeconds(10); // --- Identity --- /// Protocol name in the SignalR handshake. Client and server must match. Default: "acbinary". public string Name { get; set; } = "acbinary"; // --- Diagnostics --- /// Optional logger. When null, no protocol-internal logging occurs. public ILogger? Logger { get; set; } // --- Validation --- /// /// Validates option values and platform compatibility. /// Throws / on invalid values, /// or for unsupported platform/mode combinations. /// 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)); } /// /// Creates a shallow copy. Required for DI IOptions<T> integration — /// the singleton from DI must not be mutated by per-connection configure callbacks. /// public AcBinaryHubProtocolOptions Clone() => new() { SerializerOptions = SerializerOptions, ProtocolMode = ProtocolMode, BufferSize = BufferSize, FlushPolicy = FlushPolicy, FlushTimeout = FlushTimeout, Name = Name, Logger = Logger }; }