[LOADED_DOCS: .github\copilot-instructions.md]

Add WASM detection and fallback to AcBinaryHubProtocol

Added a static IsBrowser flag to AcBinaryHubProtocol, initialized at type-load using OperatingSystem.IsBrowser(). The constructor now throws PlatformNotSupportedException if AsyncSegment mode is used on WebAssembly. Receive path adapts: skips background Task on WASM and deserializes synchronously on CHUNK_END. Updated logging and documentation to reflect browser-specific behavior.
This commit is contained in:
Loretta 2026-04-20 09:54:08 +02:00
parent 71ccff3ad4
commit dc16f493d5
2 changed files with 58 additions and 9 deletions

View File

@ -56,6 +56,22 @@ public class AcBinaryHubProtocol : IHubProtocol
/// <summary>Sentinel object placed in the args array for the streamed argument (replaced after chunk deserialization).</summary>
protected static readonly object StreamedArgPlaceholder = new();
/// <summary>
/// True when running on a browser (WebAssembly) runtime. Cached at type-load because
/// the value is invariant per process and the check is used on hot paths.
/// <para>
/// Browser implications:
/// <list type="bullet">
/// <item>Send path: <c>AsyncSegment</c> is unsupported (sync-over-async flush blocks the single UI thread).</item>
/// <item>Receive path: when chunked wire arrives, background <c>Task.Run</c> is skipped;
/// the deserializer runs synchronously on <c>CHUNK_END</c> over the already-buffered data
/// (<see cref="SegmentBufferReader"/>'s <c>ManualResetEventSlim.Wait()</c> would throw
/// <see cref="PlatformNotSupportedException"/>).</item>
/// </list>
/// </para>
/// </summary>
private static readonly bool IsBrowser = OperatingSystem.IsBrowser();
protected volatile AcBinarySerializerOptions _options;
protected readonly BinaryProtocolMode _protocolMode;
protected readonly ILogger? _logger;
@ -96,6 +112,14 @@ public class AcBinaryHubProtocol : IHubProtocol
public AcBinaryHubProtocol(AcBinarySerializerOptions options, BinaryProtocolMode protocolMode = BinaryProtocolMode.Bytes, ILogger? logger = null)
{
// 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).
if (IsBrowser && 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;
@ -105,8 +129,8 @@ public class AcBinaryHubProtocol : IHubProtocol
if (_logger != null)
{
_logger.LogInformation(
"AcBinaryHubProtocol initialized mode={ProtocolMode} chunkSize={ChunkSize} initCap={InitCap} useGen={UseGen} wireMode={WireMode} interning={Interning} compression={Compression}",
_protocolMode, _options.BufferWriterChunkSize, _options.InitialBufferCapacity,
"AcBinaryHubProtocol initialized mode={ProtocolMode} isBrowser={IsBrowser} chunkSize={ChunkSize} initCap={InitCap} useGen={UseGen} wireMode={WireMode} interning={Interning} compression={Compression}",
_protocolMode, IsBrowser, _options.BufferWriterChunkSize, _options.InitialBufferCapacity,
_options.UseGeneratedCode, _options.WireMode, _options.UseStringInterning, _options.UseCompression);
}
}
@ -822,7 +846,11 @@ public class AcBinaryHubProtocol : IHubProtocol
// Lazy start: begin background deserialization after first chunk is written.
// SegmentBufferReaderInput.Initialize reads the already-written data immediately.
if (state.DeserTask == null)
// Browser fallback: skip Task.Run — SegmentBufferReader.WaitForData relies on
// ManualResetEventSlim.Wait which throws PlatformNotSupportedException on WASM.
// Instead, buffer all chunks and run the deserializer synchronously on CHUNK_END,
// where state.Buffer.Complete() has already been called and no wait is needed.
if (state.DeserTask == null && !IsBrowser)
{
_logger?.LogDebug("TryParseChunkData starting background deserialization targetType={TargetType}", state.StreamedArgType.Name);
@ -850,7 +878,18 @@ public class AcBinaryHubProtocol : IHubProtocol
{
if (state.DeserTask != null)
{
// Desktop / server: background task has been deserializing concurrently
// with chunk arrival (pipeline parallelism). Wait for its result here.
deserializedArg = state.DeserTask.GetAwaiter().GetResult();
}
else
{
// Browser (WASM) fallback: all chunks are buffered, state.Buffer.Complete()
// has been called above, so the synchronous deserializer reads through the
// completed buffer without any Monitor.Wait.
deserializedArg = AcBinaryDeserializer.Deserialize(
state.Buffer, state.StreamedArgType, _options);
}
if (_logger != null)
{
@ -860,7 +899,6 @@ public class AcBinaryHubProtocol : IHubProtocol
_logger.LogDebug("TryParseChunkData deserialization complete resultType={ResultType}", deserializedArg?.GetType().Name ?? "null");
}
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "TryParseChunkData deserialization FAILED targetType={TargetType}", state.StreamedArgType.Name);

View File

@ -172,4 +172,15 @@ In `AsyncSegment` mode, `WriteMessage` dispatches to `WriteMessageChunked` which
In `Bytes` and `Segment` mode, the standard `WriteMessage` path is used.
### WebAssembly compatibility
The send and receive paths handle WASM (`OperatingSystem.IsBrowser()`) asymmetrically — **send** is strictly bound to `_protocolMode`, **receive** adapts to the wire format and falls back to a synchronous path only when the platform cannot support the optimal strategy.
- **Send path**: `AsyncSegment` is **not supported on WebAssembly**. The constructor throws `PlatformNotSupportedException` if `IsBrowser && protocolMode == AsyncSegment` (the `AsyncPipeWriterOutput.SyncAwaitFlush` sync-over-async pattern would block the single UI thread). WASM clients must use `Bytes` or `Segment`.
- **Receive path**: works on WASM with **any** server-side mode (including `AsyncSegment` → chunked wire). `TryParseChunkData` detects the platform at runtime:
- **Non-browser**: first `CHUNK_DATA` spawns a background `Task.Run` over a `SegmentBufferReader` (pipeline parallelism — serialize / network / deserialize overlap). `CHUNK_END` awaits the task's result.
- **Browser**: the background task is skipped. Chunks accumulate in `SegmentBufferReader`; on `CHUNK_END` the buffer is `Complete()`d and the deserializer runs synchronously on the current thread. `SegmentBufferReaderInput.TryAdvanceSegment` sees `_completed=true` and never calls `ManualResetEventSlim.Wait()` (which throws `PlatformNotSupportedException` on WASM).
Consequence: a mixed topology (desktop server in `AsyncSegment`, WASM client in `Bytes`) works without any negotiation or protocol-name variation — the client converts the incoming chunked wire to its own synchronous processing model.
**Source:** `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs` (base), `AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs` (consumer logic), `AyCode.Services/SignalRs/BinaryProtocolMode.cs` (enum)