From dc16f493d5884b24c9add58dc3c1e22ed11da0ed Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 20 Apr 2026 09:54:08 +0200 Subject: [PATCH] [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. --- .../SignalRs/AcBinaryHubProtocol.cs | 56 ++++++++++++++++--- .../docs/SIGNALR_BINARY_PROTOCOL.md | 11 ++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs index ec21b9a..782054d 100644 --- a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs @@ -56,6 +56,22 @@ public class AcBinaryHubProtocol : IHubProtocol /// Sentinel object placed in the args array for the streamed argument (replaced after chunk deserialization). protected static readonly object StreamedArgPlaceholder = new(); + /// + /// 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. + /// + /// Browser implications: + /// + /// Send path: AsyncSegment is unsupported (sync-over-async flush blocks the single UI thread). + /// Receive path: when chunked wire arrives, background Task.Run is skipped; + /// the deserializer runs synchronously on CHUNK_END over the already-buffered data + /// ('s ManualResetEventSlim.Wait() would throw + /// ). + /// + /// + /// + 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,15 +878,25 @@ 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) - { - _logger.LogInformation("Deserialize end (chunked)"); + if (_logger != null) + { + _logger.LogInformation("Deserialize end (chunked)"); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("TryParseChunkData deserialization complete resultType={ResultType}", deserializedArg?.GetType().Name ?? "null"); - } + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("TryParseChunkData deserialization complete resultType={ResultType}", deserializedArg?.GetType().Name ?? "null"); } } catch (Exception ex) diff --git a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md index 84304c3..ebec83d 100644 --- a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md +++ b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md @@ -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)