Refactor: centralize SignalR protocol config/options
- Added AcBinaryHubProtocolOptions for unified protocol configuration (serializer, mode, buffer size, flush strategy, timeout, name, logger) with validation and DI support. - Refactored AcBinaryHubProtocol and AyCodeBinaryHubProtocol to use options object; legacy constructors now delegate to options-based API. - Added per-chunk flush timeout to AsyncPipeWriterOutput and AcBinarySerializer; throws TimeoutException on slow consumers. - Improved XML docs and comments for pipeline/backpressure/timeout clarity. - Updated SIGNALR_BINARY_PROTOCOL.md to document new options and AsyncSegment platform rules.
This commit is contained in:
parent
939ce9c39b
commit
c6e1fa8efc
|
|
@ -423,15 +423,28 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize to PipeWriter with chunked protocol framing via AsyncPipeWriterOutput.
|
||||
/// Serialize to PipeWriter with chunked protocol framing via <see cref="AsyncPipeWriterOutput"/>.
|
||||
/// Each chunk (including the last) is framed as <c>[201][UINT16 size][data]</c> and committed
|
||||
/// to the PipeWriter via Advance (zero-copy). The protocol layer writes a single <c>[202]</c>
|
||||
/// byte after to signal end-of-stream.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
|
||||
/// <param name="pipeWriter">Target pipe (typically Kestrel's transport output).</param>
|
||||
/// <param name="options">Serializer options (type wrappers, reference handling, interning, etc.).</param>
|
||||
/// <param name="waitForFlush">
|
||||
/// Per-chunk flush synchronization. <c>true</c> (default): maximum pipeline parallelism,
|
||||
/// guaranteed zero-copy, but slow consumers block the server thread (bounded by <paramref name="flushTimeout"/>).
|
||||
/// <c>false</c>: adaptive backpressure via memory threshold — safer for mixed consumer speeds.
|
||||
/// </param>
|
||||
/// <param name="flushTimeout">
|
||||
/// Per-flush timeout. <c>null</c> → wait forever (legacy). Positive value: throws
|
||||
/// <see cref="TimeoutException"/> on stuck consumers.
|
||||
/// </param>
|
||||
/// <returns>Total serialized data bytes (excluding framing overhead).</returns>
|
||||
public static int Serialize<T>(
|
||||
T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options,
|
||||
bool waitForFlush = true)
|
||||
bool waitForFlush = true,
|
||||
TimeSpan? flushTimeout = null)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
|
|
@ -444,7 +457,7 @@ public static partial class AcBinarySerializer
|
|||
|
||||
var runtimeType = value.GetType();
|
||||
var context = BinarySerializationContextPool<AsyncPipeWriterOutput>.Get(options);
|
||||
context.Output = new AsyncPipeWriterOutput(pipeWriter, options.BufferWriterChunkSize, waitForFlush);
|
||||
context.Output = new AsyncPipeWriterOutput(pipeWriter, options.BufferWriterChunkSize, waitForFlush, flushTimeout);
|
||||
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -9,24 +9,32 @@ using System.Threading.Tasks;
|
|||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Binary output that writes to a PipeWriter with per-chunk network flush and self-describing framing.
|
||||
/// Binary output that writes to a PipeWriter with per-chunk self-describing framing.
|
||||
///
|
||||
/// Each chunk (including the last) is framed as <c>[201][UINT16 size][data]</c> with a 3-byte header
|
||||
/// reserved at the start of each buffer. The serializer context writes into the space after the
|
||||
/// reserved bytes; on Grow(), the header is patched and the full chunk is committed via Advance
|
||||
/// and flushed to the network. Flush() does the same for the last (partial) chunk — zero-copy
|
||||
/// for both intermediate and final chunks.
|
||||
/// reserved bytes; on <see cref="Grow"/>, the header is patched and the full chunk is committed via
|
||||
/// Advance (zero-copy). <see cref="Flush"/> does the same for the last (partial) chunk.
|
||||
///
|
||||
/// The protocol layer writes a single <c>[202]</c> byte after all chunks to signal end-of-stream.
|
||||
///
|
||||
/// Backpressure modes (controlled by <c>waitForFlush</c> constructor parameter):
|
||||
/// <para><b>Backpressure modes</b> (controlled by <c>waitForFlush</c>):</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>waitForFlush=true</c> (default): Grow() blocks if the previous FlushAsync hasn't completed.
|
||||
/// Bounds memory to ~2 chunks in flight.</item>
|
||||
/// <item><c>waitForFlush=false</c>: Grow() never blocks. Data accumulates in the PipeWriter's internal
|
||||
/// buffer and is sent with the next completed flush. Maximum serialization throughput.</item>
|
||||
/// <item><c>waitForFlush=true</c> (default): Grow() waits for the previous FlushAsync before
|
||||
/// starting a new chunk. <b>Pro:</b> maximum pipeline parallelism, guaranteed end-to-end zero-copy.
|
||||
/// <b>Con:</b> slow consumer propagates back as server-thread blocking (bounded by <c>flushTimeout</c>).</item>
|
||||
/// <item><c>waitForFlush=false</c>: Grow() is fire-and-forget per chunk; only blocks when committed
|
||||
/// bytes exceed ~60 KB (memory threshold — itself an adaptive backpressure).
|
||||
/// <b>Pro:</b> no per-chunk waits, safer with mixed consumer speeds.
|
||||
/// <b>Con:</b> under heavy backpressure may fall back to an owned buffer, losing zero-copy
|
||||
/// for that chunk.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>Timeout safety</b>: every synchronous flush-await is bounded by <c>flushTimeout</c>
|
||||
/// (default <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> when the type is used directly;
|
||||
/// <see cref="AcBinaryHubProtocol"/> passes 10 s from its options). A <see cref="TimeoutException"/>
|
||||
/// propagates to the caller, allowing the connection to abort instead of blocking forever.</para>
|
||||
///
|
||||
/// Maximum chunk data size: 65535 bytes (UINT16 max).
|
||||
/// </summary>
|
||||
public struct AsyncPipeWriterOutput : IBinaryOutputBase
|
||||
|
|
@ -43,12 +51,19 @@ public struct AsyncPipeWriterOutput : IBinaryOutputBase
|
|||
private readonly PipeWriter _pipeWriter;
|
||||
private readonly int _chunkSize;
|
||||
private readonly bool _waitForFlush;
|
||||
private readonly TimeSpan _flushTimeout;
|
||||
private int _committedBytes;
|
||||
private int _currentChunkStart;
|
||||
private bool _ownedBuffer;
|
||||
private ValueTask<FlushResult> _lastFlush;
|
||||
|
||||
public AsyncPipeWriterOutput(PipeWriter pipeWriter, int chunkSize = 4096, bool waitForFlush = true)
|
||||
/// <summary>Creates an output bound to the given PipeWriter with self-describing chunked framing.</summary>
|
||||
/// <param name="pipeWriter">Target pipe (typically Kestrel's transport output for SignalR).</param>
|
||||
/// <param name="chunkSize">Per-chunk data size (max <see cref="MaxChunkSize"/>). Default 4 KB matches Kestrel's slab size.</param>
|
||||
/// <param name="waitForFlush">See class summary — pipeline parallelism (true) vs adaptive (false).</param>
|
||||
/// <param name="flushTimeout">Per-flush timeout. <c>null</c> → <see cref="System.Threading.Timeout.InfiniteTimeSpan"/>
|
||||
/// (wait forever — legacy behavior). Pass a positive value to fail fast on stuck consumers.</param>
|
||||
public AsyncPipeWriterOutput(PipeWriter pipeWriter, int chunkSize = 4096, bool waitForFlush = true, TimeSpan? flushTimeout = null)
|
||||
{
|
||||
if (chunkSize > MaxChunkSize)
|
||||
throw new ArgumentOutOfRangeException(nameof(chunkSize), chunkSize,
|
||||
|
|
@ -57,6 +72,9 @@ public struct AsyncPipeWriterOutput : IBinaryOutputBase
|
|||
_pipeWriter = pipeWriter;
|
||||
_chunkSize = chunkSize;
|
||||
_waitForFlush = waitForFlush;
|
||||
// null → Timeout.InfiniteTimeSpan ("wait forever" — natively supported by Task.Wait as -1ms).
|
||||
// A positive value enables bounded waiting; on timeout a TimeoutException propagates to the caller.
|
||||
_flushTimeout = flushTimeout ?? System.Threading.Timeout.InfiniteTimeSpan;
|
||||
_committedBytes = 0;
|
||||
_ownedBuffer = false;
|
||||
_lastFlush = default;
|
||||
|
|
@ -65,13 +83,24 @@ public struct AsyncPipeWriterOutput : IBinaryOutputBase
|
|||
/// <summary>
|
||||
/// Synchronously awaits a FlushAsync ValueTask.
|
||||
/// Fast-path: if already completed, returns without Task allocation.
|
||||
/// Slow-path: converts to Task for proper blocking (backpressure).
|
||||
/// Slow-path: blocks with <see cref="_flushTimeout"/> — throws <see cref="TimeoutException"/>
|
||||
/// if the flush does not complete within it (guards against slow/stuck/disconnected consumers).
|
||||
/// <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> means "wait forever" (natively supported by <c>Task.Wait</c>).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void SyncAwaitFlush(ValueTask<FlushResult> vt)
|
||||
private void SyncAwaitFlush(ValueTask<FlushResult> vt)
|
||||
{
|
||||
if (!vt.IsCompletedSuccessfully)
|
||||
vt.AsTask().GetAwaiter().GetResult();
|
||||
if (vt.IsCompletedSuccessfully) return;
|
||||
|
||||
var task = vt.AsTask();
|
||||
|
||||
if (!task.Wait(_flushTimeout))
|
||||
throw new TimeoutException(
|
||||
$"PipeWriter.FlushAsync exceeded {_flushTimeout.TotalSeconds:F1}s — " +
|
||||
"consumer may be too slow, stuck, or disconnected.");
|
||||
|
||||
// Completed within timeout — propagate any faulted exception
|
||||
task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -76,6 +76,17 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
protected readonly BinaryProtocolMode _protocolMode;
|
||||
protected readonly ILogger? _logger;
|
||||
|
||||
/// <summary>
|
||||
/// AsyncSegment per-chunk flush synchronization — see <see cref="AcBinaryHubProtocolOptions.WaitForFlush"/>.
|
||||
/// </summary>
|
||||
protected readonly bool _waitForFlush;
|
||||
|
||||
/// <summary>
|
||||
/// Per-flush wait limit — see <see cref="AcBinaryHubProtocolOptions.FlushTimeout"/>.
|
||||
/// Guaranteed positive or <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> by <see cref="AcBinaryHubProtocolOptions.Validate"/>.
|
||||
/// </summary>
|
||||
protected readonly TimeSpan _flushTimeout;
|
||||
|
||||
/// <summary>
|
||||
/// Per-connection chunk accumulation state. Key is IInvocationBinder (per-connection, GC-friendly).
|
||||
/// Always initialized regardless of ProtocolMode — any client can receive chunked data from an AsyncSegment server.
|
||||
|
|
@ -108,10 +119,38 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
public int ChunkFrameBytesConsumed;
|
||||
}
|
||||
|
||||
public AcBinaryHubProtocol() : this(AcBinarySerializerOptions.Default) { }
|
||||
/// <summary>
|
||||
/// Parameterless constructor — creates the protocol with all-default options
|
||||
/// (<see cref="BinaryProtocolMode.Bytes"/>, 4 KB buffer, 10 s flush timeout, "acbinary" name).
|
||||
/// Mainly for tests and simple scenarios. For production, pass an explicit
|
||||
/// <see cref="AcBinaryHubProtocolOptions"/> or configure via DI.
|
||||
/// </summary>
|
||||
public AcBinaryHubProtocol() : this(new AcBinaryHubProtocolOptions()) { }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy constructor — wraps the arguments into <see cref="AcBinaryHubProtocolOptions"/>
|
||||
/// and delegates to the options-based constructor. Kept for backward compatibility;
|
||||
/// will be removed in a future version in favor of the options-based API.
|
||||
/// </summary>
|
||||
public AcBinaryHubProtocol(AcBinarySerializerOptions options, BinaryProtocolMode protocolMode = BinaryProtocolMode.Bytes, ILogger? logger = null)
|
||||
: this(new AcBinaryHubProtocolOptions
|
||||
{
|
||||
SerializerOptions = options,
|
||||
ProtocolMode = protocolMode,
|
||||
Logger = logger,
|
||||
BufferSize = 4096
|
||||
// FlushTimeout, WaitForFlush, Name — use options defaults (30s, true, "acbinary")
|
||||
})
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Primary constructor. All configuration flows through <see cref="AcBinaryHubProtocolOptions"/>.
|
||||
/// </summary>
|
||||
public AcBinaryHubProtocol(AcBinaryHubProtocolOptions options)
|
||||
{
|
||||
if (options is null) throw new ArgumentNullException(nameof(options));
|
||||
options.Validate();
|
||||
|
||||
// Send-side guard: AsyncSegment uses AsyncPipeWriterOutput whose sync-over-async flush
|
||||
// would block the browser's single UI thread. The receive side converts chunked wire
|
||||
// to a synchronous deserialize on WASM automatically (see TryParseChunkData).
|
||||
|
|
@ -119,22 +158,26 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
// TEMP: commented out to test AsyncSegment on both Windows app and WASM without rebuild.
|
||||
// Small WASM payloads work; larger ones may deadlock on sync-over-async FlushAsync.
|
||||
// Restore once BinaryProtocolMode is runtime-configurable in Program.cs.
|
||||
//if (IsBrowser && protocolMode == BinaryProtocolMode.AsyncSegment)
|
||||
//if (IsBrowser && options.ProtocolMode == BinaryProtocolMode.AsyncSegment)
|
||||
// throw new PlatformNotSupportedException(
|
||||
// "BinaryProtocolMode.AsyncSegment is not supported on WebAssembly. " +
|
||||
// "Use BinaryProtocolMode.Bytes or BinaryProtocolMode.Segment instead.");
|
||||
|
||||
_options = options;
|
||||
_options.BufferWriterChunkSize = 4096;
|
||||
_protocolMode = protocolMode;
|
||||
_logger = logger;
|
||||
_options = options.SerializerOptions;
|
||||
_options.BufferWriterChunkSize = options.BufferSize;
|
||||
_protocolMode = options.ProtocolMode;
|
||||
_logger = options.Logger;
|
||||
_waitForFlush = options.WaitForFlush;
|
||||
_flushTimeout = options.FlushTimeout;
|
||||
Name = options.Name;
|
||||
_chunkStates = new ConditionalWeakTable<IInvocationBinder, AsyncChunkState>();
|
||||
|
||||
if (_logger != null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"AcBinaryHubProtocol initialized mode={ProtocolMode} isBrowser={IsBrowser} chunkSize={ChunkSize} initCap={InitCap} useGen={UseGen} wireMode={WireMode} interning={Interning} compression={Compression}",
|
||||
_protocolMode, IsBrowser, _options.BufferWriterChunkSize, _options.InitialBufferCapacity,
|
||||
"AcBinaryHubProtocol initialized name={Name} mode={ProtocolMode} isBrowser={IsBrowser} chunkSize={ChunkSize} initCap={InitCap} waitForFlush={WaitForFlush} flushTimeoutMs={FlushTimeoutMs} useGen={UseGen} wireMode={WireMode} interning={Interning} compression={Compression}",
|
||||
Name, _protocolMode, IsBrowser, _options.BufferWriterChunkSize, _options.InitialBufferCapacity,
|
||||
_waitForFlush, _flushTimeout.TotalMilliseconds,
|
||||
_options.UseGeneratedCode, _options.WireMode, _options.UseStringInterning, _options.UseCompression);
|
||||
}
|
||||
}
|
||||
|
|
@ -149,18 +192,38 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
set => _options = value;
|
||||
}
|
||||
|
||||
public string Name => "acbinary";
|
||||
/// <summary>Protocol name sent in SignalR handshake. Set via <see cref="AcBinaryHubProtocolOptions.Name"/>. Default: <c>"acbinary"</c>.</summary>
|
||||
public string Name { get; } = "acbinary";
|
||||
|
||||
public int Version => 1;
|
||||
public TransferFormat TransferFormat => TransferFormat.Binary;
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously gets the result of a PipeWriter.FlushAsync ValueTask.
|
||||
/// Fast-path: if already completed (no backpressure), returns directly without Task allocation.
|
||||
/// Slow-path: converts to Task for proper blocking when pipe backpressure is active.
|
||||
/// Slow-path: blocks with <see cref="_flushTimeout"/> — throws <see cref="TimeoutException"/>
|
||||
/// if the flush does not complete within the timeout (protects against slow/stuck/disconnected
|
||||
/// consumers holding the server thread indefinitely).
|
||||
/// <para>
|
||||
/// <see cref="AcBinaryHubProtocolOptions.Validate"/> guarantees <c>_flushTimeout</c> is either
|
||||
/// positive or <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> (which <c>Task.Wait</c>
|
||||
/// natively treats as "wait forever"), so no explicit zero-check is needed here.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static FlushResult SyncFlush(ValueTask<FlushResult> vt)
|
||||
=> vt.IsCompletedSuccessfully ? vt.Result : vt.AsTask().GetAwaiter().GetResult();
|
||||
private FlushResult SyncFlush(ValueTask<FlushResult> vt)
|
||||
{
|
||||
if (vt.IsCompletedSuccessfully) return vt.Result;
|
||||
|
||||
var task = vt.AsTask();
|
||||
|
||||
if (!task.Wait(_flushTimeout))
|
||||
throw new TimeoutException(
|
||||
$"PipeWriter.FlushAsync exceeded {_flushTimeout.TotalSeconds:F1}s — " +
|
||||
"consumer may be too slow, stuck, or disconnected.");
|
||||
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsVersionSupported(int version) => version <= Version;
|
||||
|
|
@ -473,7 +536,7 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
// --- CHUNK_DATA ([201][UINT16 size][data] per chunk, all committed by output) ---
|
||||
if (streamedArg != null)
|
||||
{
|
||||
dataBytes = AcBinarySerializer.Serialize(streamedArg, pipeWriter, _options);
|
||||
dataBytes = AcBinarySerializer.Serialize(streamedArg, pipeWriter, _options, _waitForFlush, _flushTimeout);
|
||||
_logger?.LogDebug("WriteMessageChunked CHUNK_DATA serialized dataBytes={DataBytes}", dataBytes);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
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><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 < 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()
|
||||
{
|
||||
// NOTE: WASM + AsyncSegment send-path guard is currently commented out in the protocol
|
||||
// constructor for testing. Once BinaryProtocolMode becomes runtime-configurable in
|
||||
// Program.cs, this validation will be re-enabled here as the primary guard.
|
||||
//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,
|
||||
WaitForFlush = WaitForFlush,
|
||||
FlushTimeout = FlushTimeout,
|
||||
Name = Name,
|
||||
Logger = Logger
|
||||
};
|
||||
}
|
||||
|
|
@ -24,8 +24,22 @@ namespace AyCode.Services.SignalRs;
|
|||
/// </summary>
|
||||
public class AyCodeBinaryHubProtocol : AcBinaryHubProtocol
|
||||
{
|
||||
public AyCodeBinaryHubProtocol() : this(AcBinarySerializerOptions.Default) { }
|
||||
public AyCodeBinaryHubProtocol(AcBinarySerializerOptions options, BinaryProtocolMode protocolMode = BinaryProtocolMode.Bytes, ILogger? logger = null) : base(options, protocolMode, logger) { }
|
||||
/// <summary>
|
||||
/// Parameterless constructor — creates the protocol with all-default options. See base class.
|
||||
/// </summary>
|
||||
public AyCodeBinaryHubProtocol() : base() { }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy constructor — delegates to the base legacy constructor, which wraps into
|
||||
/// <see cref="AcBinaryHubProtocolOptions"/>. Kept for backward compatibility.
|
||||
/// </summary>
|
||||
public AyCodeBinaryHubProtocol(AcBinarySerializerOptions options, BinaryProtocolMode protocolMode = BinaryProtocolMode.Bytes, ILogger? logger = null)
|
||||
: base(options, protocolMode, logger) { }
|
||||
|
||||
/// <summary>
|
||||
/// Primary constructor — accepts a fully-configured <see cref="AcBinaryHubProtocolOptions"/>.
|
||||
/// </summary>
|
||||
public AyCodeBinaryHubProtocol(AcBinaryHubProtocolOptions options) : base(options) { }
|
||||
|
||||
#region Wire header (per-message)
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue