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)