[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:
parent
71ccff3ad4
commit
dc16f493d5
|
|
@ -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,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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue