From 8b8abb7cbc8f24e3bb02e5bd5781def8e369ca5d Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 22 Apr 2026 22:44:37 +0200 Subject: [PATCH] [LOADED_DOCS: .github\copilot-instructions.md] Refactor SignalR protocol registration; add DI options - Added AcSignalRServerProtocolExtensions and AcSignalRProtocolExtensions for idiomatic AddAcBinaryProtocol registration (server/client), using a shared BuildProtocol factory for DI/IOptions/inline config. - Introduced AcHubConnectionOptions and AcSignalRConnectionExtensions for configuration-driven client setup. - Refactored AcSignalRClientBase to require a preconfigured IHubConnectionBuilder, moving all connection/protocol config out of the base class. - Removed legacy protocol constructors; all protocol instantiation is now options-based. - Enforced WASM + AsyncSegment guard in AcBinaryHubProtocolOptions.Validate. - Updated SIGNALR_BINARY_PROTOCOL.md and GLOSSARY.md for new DI/config patterns. - Minor: updated settings.local.json with new DLL/plugin inspection commands. --- .claude/settings.local.json | 8 ++- .../SignalRs/TestMultiSegmentProtocol.cs | 6 +- .../AcSignalRServerProtocolExtensions.cs | 35 ++++++++++ .../SignalRs/AcBinaryHubProtocol.cs | 30 +------- .../SignalRs/AcBinaryHubProtocolOptions.cs | 15 ++-- .../SignalRs/AcHubConnectionOptions.cs | 55 +++++++++++++++ .../SignalRs/AcSignalRClientBase.cs | 63 +++-------------- .../SignalRs/AcSignalRConnectionExtensions.cs | 49 +++++++++++++ .../SignalRs/AcSignalRProtocolExtensions.cs | 50 ++++++++++++++ .../SignalRs/AyCodeBinaryHubProtocol.cs | 7 -- .../docs/SIGNALR_BINARY_PROTOCOL.md | 69 ++++++++++++++++++- docs/GLOSSARY.md | 6 +- 12 files changed, 293 insertions(+), 100 deletions(-) create mode 100644 AyCode.Services.Server/SignalRs/AcSignalRServerProtocolExtensions.cs create mode 100644 AyCode.Services/SignalRs/AcHubConnectionOptions.cs create mode 100644 AyCode.Services/SignalRs/AcSignalRConnectionExtensions.cs create mode 100644 AyCode.Services/SignalRs/AcSignalRProtocolExtensions.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 45c735b..5cd849e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -56,7 +56,13 @@ "PowerShell(dotnet build \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\AyCode.Core.csproj\" --no-restore 2>&1)", "PowerShell(dotnet build \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Services\\\\AyCode.Services.csproj\" --no-restore 2>&1)", "PowerShell(Remove-Item \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Binaries\\\\PipeReaderBinaryInput.cs\" -Confirm:$false)", - "PowerShell(dotnet build \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Services.Server.Tests\\\\AyCode.Services.Server.Tests.csproj\" --no-restore 2>&1)" + "PowerShell(dotnet build \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Services.Server.Tests\\\\AyCode.Services.Server.Tests.csproj\" --no-restore 2>&1)", + "Bash(Get-ChildItem -Path \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\bin\\\\Release\\\\net9.0\" -Recurse -Filter \"AyCode.Services.dll\" -ErrorAction SilentlyContinue)", + "Bash(Select-Object FullName, LastWriteTime, Length)", + "Bash(Format-Table -AutoSize -Wrap)", + "PowerShell($paths = @\\(\"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\bin\\\\Release\\\\net9.0\", \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\bin\\\\Debug\\\\net9.0\"\\); foreach \\($p in $paths\\) { if \\(Test-Path $p\\) { Write-Output \"=== $p ===\"; Get-ChildItem -Path $p -Recurse -Include \"AyCode.Services.dll\",\"AyCode.Core.dll\",\"Mango.Nop.Core.dll\",\"Nop.Plugin.Misc.FruitBankPlugin.dll\",\"Nop.Plugin.Misc.AIPlugin.dll\",\"Mango.Nop.Services.dll\" -ErrorAction SilentlyContinue | Select-Object LastWriteTime, Length, FullName | Sort-Object FullName | Format-Table -AutoSize -Wrap } })", + "PowerShell($pluginRoots = @\\(\"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\Plugins\", \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\bin\\\\Release\\\\net9.0\\\\Plugins\", \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\bin\\\\Debug\\\\net9.0\\\\Plugins\"\\); foreach \\($p in $pluginRoots\\) { if \\(Test-Path $p\\) { Write-Output \"=== $p ===\"; Get-ChildItem -Path $p -Recurse -Include \"AyCode.Services.dll\",\"AyCode.Core.dll\",\"Mango.Nop.Core.dll\",\"Nop.Plugin.Misc.FruitBankPlugin.dll\",\"Mango.Nop.Services.dll\" -ErrorAction SilentlyContinue | Select-Object LastWriteTime, Length, FullName | Sort-Object FullName | Format-Table -AutoSize -Wrap } else { Write-Output \"NOT FOUND: $p\" } })", + "PowerShell($appDataPaths = @\\(\"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\App_Data\\\\plugins.json\", \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBank\\\\Presentation\\\\Nop.Web\\\\App_Data\\\\plugins.installed.json\"\\); foreach \\($f in $appDataPaths\\) { if \\(Test-Path $f\\) { Write-Output \"=== $f ===\"; Get-Content $f -Raw } else { Write-Output \"NOT FOUND: $f\" } })" ] } } diff --git a/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs index cf2a211..546e0fe 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs @@ -29,7 +29,11 @@ internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol private readonly BinaryProtocolMode _mode; public TestMultiSegmentProtocol(BinaryProtocolMode mode = BinaryProtocolMode.Bytes) - : base(AcBinarySerializerOptions.Default, mode) + : base(new AcBinaryHubProtocolOptions + { + SerializerOptions = AcBinarySerializerOptions.Default, + ProtocolMode = mode + }) { _mode = mode; Options.BufferWriterChunkSize = SegmentSize; diff --git a/AyCode.Services.Server/SignalRs/AcSignalRServerProtocolExtensions.cs b/AyCode.Services.Server/SignalRs/AcSignalRServerProtocolExtensions.cs new file mode 100644 index 0000000..9662808 --- /dev/null +++ b/AyCode.Services.Server/SignalRs/AcSignalRServerProtocolExtensions.cs @@ -0,0 +1,35 @@ +using AyCode.Services.SignalRs; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.Extensions.DependencyInjection; + +namespace AyCode.Services.Server.SignalRs; + +/// +/// Server-side registration extension for the ("acbinary"). +/// Mirrors the ASP.NET Core idiomatic AddJsonProtocol(...) / AddMessagePackProtocol(...). +/// +/// Kept separate from the client-side extension (in AyCode.Services) so that pure client +/// projects (MAUI, WASM) do not pull in the server SignalR assembly +/// (Microsoft.AspNetCore.SignalR.Core) through a transitive reference. +/// +/// +public static class AcSignalRServerProtocolExtensions +{ + /// + /// Registers (name: "acbinary") as a SignalR hub + /// protocol on the server. Options can be configured via either: + /// + /// services.Configure<AcBinaryHubProtocolOptions>(...) — DI-level defaults + /// The optional callback — overrides DI values inline + /// + /// + public static ISignalRServerBuilder AddAcBinaryProtocol( + this ISignalRServerBuilder builder, + Action? configure = null) + { + builder.Services.AddSingleton(sp => + AcSignalRProtocolExtensions.BuildProtocol(sp, configure)); + return builder; + } +} diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs index 5202c34..f204adc 100644 --- a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs @@ -127,42 +127,16 @@ public class AcBinaryHubProtocol : IHubProtocol /// public AcBinaryHubProtocol() : this(new AcBinaryHubProtocolOptions()) { } - /// - /// Legacy constructor — wraps the arguments into - /// 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. - /// - 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") - }) - { } - /// /// Primary constructor. All configuration flows through . + /// Invalid configuration (incl. WebAssembly + AsyncSegment send-path) throws from + /// . /// 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). - // - // 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 && options.ProtocolMode == BinaryProtocolMode.AsyncSegment) - // throw new PlatformNotSupportedException( - // "BinaryProtocolMode.AsyncSegment is not supported on WebAssembly. " + - // "Use BinaryProtocolMode.Bytes or BinaryProtocolMode.Segment instead."); - _options = options.SerializerOptions; _options.BufferWriterChunkSize = options.BufferSize; _protocolMode = options.ProtocolMode; diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocolOptions.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocolOptions.cs index 6a404cb..0925fad 100644 --- a/AyCode.Services/SignalRs/AcBinaryHubProtocolOptions.cs +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocolOptions.cs @@ -76,13 +76,14 @@ public sealed class AcBinaryHubProtocolOptions /// 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."); + // WASM + AsyncSegment send-path guard — the AsyncPipeWriterOutput sync-over-async flush + // would block the browser's single UI thread. The receive side converts chunked wire to + // synchronous deser automatically, so WASM clients can still *receive* AsyncSegment data + // from a non-WASM server — they just cannot *send* via AsyncSegment themselves. + 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, diff --git a/AyCode.Services/SignalRs/AcHubConnectionOptions.cs b/AyCode.Services/SignalRs/AcHubConnectionOptions.cs new file mode 100644 index 0000000..296b939 --- /dev/null +++ b/AyCode.Services/SignalRs/AcHubConnectionOptions.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Http.Connections; + +namespace AyCode.Services.SignalRs; + +/// +/// Options for a client-side SignalR HubConnection, designed to be bindable from +/// configuration (appsettings.json) via services.Configure<AcHubConnectionOptions>(...). +/// Applied to an IHubConnectionBuilder via +/// . +/// +/// Most properties are nullable — when null the underlying SignalR / Microsoft default +/// is kept (the framework is non-opinionated). Resilience flags default to sensible modern values +/// (auto-reconnect on). Override in code or configuration. +/// +/// Precedence (low → high): property initializer → services.Configure<T>(section) +/// → services.Configure<T>(action). +/// +public sealed class AcHubConnectionOptions +{ + /// Target hub URL — absolute, including the hub path (e.g. "https://host/fbHub"). + public string Url { get; set; } = ""; + + // --- HttpConnectionOptions (applied via WithUrl) --- + + /// Transport(s) to negotiate. null → Microsoft default (WebSockets | LongPolling | ServerSentEvents). + public HttpTransportType? Transports { get; set; } + + /// Max outbound transport buffer (bytes). null → Microsoft default (~64 KB). + public int? TransportMaxBufferSize { get; set; } + + /// Max application-level send buffer (bytes). null → Microsoft default (~64 KB). + public int? ApplicationMaxBufferSize { get; set; } + + /// WebSocket close handshake timeout. null → Microsoft default (5 s). + public TimeSpan? CloseTimeout { get; set; } + + /// Skip negotiation (WebSockets-only setups). null → Microsoft default (false). + public bool? SkipNegotiation { get; set; } + + // --- Connection-level --- + + /// Client-side keep-alive ping interval. null → SignalR default (15 s). + public TimeSpan? KeepAliveInterval { get; set; } + + /// Server timeout (no-data threshold to declare the connection dead). null → SignalR default (30 s). + public TimeSpan? ServerTimeout { get; set; } + + // --- Resilience (framework defaults — opinionated toward modern apps) --- + + /// Enable WithAutomaticReconnect(). Default: true. + public bool UseAutomaticReconnect { get; set; } = true; + + /// Enable WithStatefulReconnect() (SignalR 8+). Default: false (opt-in — requires server support). + public bool UseStatefulReconnect { get; set; } = false; +} diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index 727112c..06c9022 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -17,7 +17,6 @@ namespace AyCode.Services.SignalRs public abstract class AcSignalRClientBase : IAcSignalRHubClient { private readonly ConcurrentDictionary _responseByRequestId = new(); - private readonly bool _useAcBinaryProtocol; protected readonly HubConnection? HubConnection; protected readonly AcLoggerBase Logger; @@ -36,64 +35,24 @@ namespace AyCode.Services.SignalRs public int TransportSendTimeout = 60000; private const string TagsName = "SignalRTags"; - protected AcSignalRClientBase(string fullHubName, AcLoggerBase logger, bool useAcBinaryProtocol = true) + /// + /// Primary constructor. The is expected to be fully configured + /// (URL, transport, reconnect, keep-alive, protocol) — typically via a transient DI registration + /// in the consuming project's Program.cs. This class only calls Build() and wires + /// the dispatch callback; no connection parameters are hard-coded here. + /// + protected AcSignalRClientBase(IHubConnectionBuilder hubBuilder, AcLoggerBase logger) { - _useAcBinaryProtocol = useAcBinaryProtocol; Logger = logger; - Logger.Detail(fullHubName); - - var hubBuilder = new HubConnectionBuilder() - .WithUrl(fullHubName, HttpTransportType.WebSockets, - options => - { - options.TransportMaxBufferSize = 30_000_000; - options.ApplicationMaxBufferSize = 30_000_000; - options.CloseTimeout = TimeSpan.FromSeconds(10); - options.SkipNegotiation = true; - }) - .ConfigureLogging(logging => - { - // alap minimális MS log level - logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information); - - // regisztráljuk az AcLoggerProvider-t úgy, hogy visszaadja a meglévő Logger példányt - logging.AddAcLogger(_ => Logger); - - // ha inkább csak AcLogger legyen: - // logging.ClearProviders(); - // logging.AddProvider(new AcLoggerProvider(category => Logger)); - }) - .WithAutomaticReconnect() - .WithStatefulReconnect() - .WithKeepAliveInterval(TimeSpan.FromSeconds(60)) - .WithServerTimeout(TimeSpan.FromSeconds(180)); - - if (useAcBinaryProtocol) - { - hubBuilder.Services.AddSingleton(sp => - { - var binaryOptions = AcBinarySerializerOptions.Default; - binaryOptions.BufferWriterChunkSize = 4096; - - // AcSignalRClientBase — a 84. sor környékén: - var signalLogger = sp.GetRequiredService().CreateLogger(); - return new AyCodeBinaryHubProtocol(binaryOptions, BinaryProtocolMode.AsyncSegment, signalLogger); - // és törölhető: AcBinaryHubProtocol.DiagnosticLogger = msg => Logger.Debug(msg); - }); - - //Vagy ha az options-t is DI-ből: - //hubBuilder.Services.AddSingleton(sp => new AyCodeBinaryHubProtocol(sp.GetRequiredService())); - - AcBinaryHubProtocol.DiagnosticLogger = msg => Logger.Debug(msg); - AcBinaryDeserializer.DiagnosticLogger = msg => Logger.Debug(msg); - } - HubConnection = hubBuilder.Build(); - HubConnection.Closed += HubConnection_Closed; _ = HubConnection.On(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage); } + /// + /// Connection-less constructor — used by derived classes that manage their own connection lifecycle + /// or run in test / offline scenarios where stays null. + /// protected AcSignalRClientBase(AcLoggerBase logger) { Logger = logger; diff --git a/AyCode.Services/SignalRs/AcSignalRConnectionExtensions.cs b/AyCode.Services/SignalRs/AcSignalRConnectionExtensions.cs new file mode 100644 index 0000000..8bbff9b --- /dev/null +++ b/AyCode.Services/SignalRs/AcSignalRConnectionExtensions.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.SignalR.Client; + +namespace AyCode.Services.SignalRs; + +/// +/// Extension methods for applying to +/// a client . +/// +public static class AcSignalRConnectionExtensions +{ + /// + /// Applies to the builder: + /// WithUrl (with HttpConnectionOptions), keep-alive, server timeout, + /// WithAutomaticReconnect, WithStatefulReconnect. + /// + /// Nullable properties are skipped when null — the underlying SignalR default is kept. + /// Combine freely with any other builder extensions before or after this call; the ordering + /// follows the fluent chain (last write wins for a given setting). + /// + /// + public static IHubConnectionBuilder AddAcConnection( + this IHubConnectionBuilder builder, + AcHubConnectionOptions options) + { + if (options is null) throw new ArgumentNullException(nameof(options)); + if (string.IsNullOrWhiteSpace(options.Url)) + throw new ArgumentException("AcHubConnectionOptions.Url must be set.", nameof(options)); + + builder.WithUrl(options.Url, http => + { + if (options.Transports.HasValue) http.Transports = options.Transports.Value; + if (options.TransportMaxBufferSize.HasValue) http.TransportMaxBufferSize = options.TransportMaxBufferSize.Value; + if (options.ApplicationMaxBufferSize.HasValue) http.ApplicationMaxBufferSize = options.ApplicationMaxBufferSize.Value; + if (options.CloseTimeout.HasValue) http.CloseTimeout = options.CloseTimeout.Value; + if (options.SkipNegotiation.HasValue) http.SkipNegotiation = options.SkipNegotiation.Value; + }); + + if (options.KeepAliveInterval.HasValue) + builder.WithKeepAliveInterval(options.KeepAliveInterval.Value); + if (options.ServerTimeout.HasValue) + builder.WithServerTimeout(options.ServerTimeout.Value); + if (options.UseAutomaticReconnect) + builder.WithAutomaticReconnect(); + if (options.UseStatefulReconnect) + builder.WithStatefulReconnect(); + + return builder; + } +} diff --git a/AyCode.Services/SignalRs/AcSignalRProtocolExtensions.cs b/AyCode.Services/SignalRs/AcSignalRProtocolExtensions.cs new file mode 100644 index 0000000..e43d8ce --- /dev/null +++ b/AyCode.Services/SignalRs/AcSignalRProtocolExtensions.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AyCode.Services.SignalRs; + +/// +/// Client-side registration extension for the ("acbinary"). +/// Mirrors the ASP.NET Core idiomatic pattern of AddJsonProtocol(...) / +/// AddMessagePackProtocol(...). +/// +/// For the server-side equivalent see AcSignalRServerProtocolExtensions in +/// AyCode.Services.Server — kept separate to avoid dragging the server SignalR assembly +/// (Microsoft.AspNetCore.SignalR.Core) into pure client projects (MAUI, WASM). +/// +/// +public static class AcSignalRProtocolExtensions +{ + /// + /// Registers as the protocol for a client . + /// Call on the during client setup. + /// + public static IHubConnectionBuilder AddAcBinaryProtocol(this IHubConnectionBuilder builder, Action? configure = null) + { + builder.Services.AddSingleton(sp => BuildProtocol(sp, configure)); + return builder; + } + + /// + /// Shared factory used by both client (this file) and server + /// (AcSignalRServerProtocolExtensions in AyCode.Services.Server). + /// Resolves options from DI (IOptions<T>), clones them, applies the inline + /// override, validates, and constructs the protocol. + /// + public static IHubProtocol BuildProtocol(IServiceProvider sp, Action? configure) + { + var diOptions = sp.GetService>()?.Value; + var options = diOptions?.Clone() ?? new AcBinaryHubProtocolOptions(); + + options.Logger ??= sp.GetService>(); + + configure?.Invoke(options); + + options.Validate(); + + return new AyCodeBinaryHubProtocol(options); + } +} diff --git a/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs index aaeb2ac..5dca757 100644 --- a/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs @@ -29,13 +29,6 @@ public class AyCodeBinaryHubProtocol : AcBinaryHubProtocol /// public AyCodeBinaryHubProtocol() : base() { } - /// - /// Legacy constructor — delegates to the base legacy constructor, which wraps into - /// . Kept for backward compatibility. - /// - public AyCodeBinaryHubProtocol(AcBinarySerializerOptions options, BinaryProtocolMode protocolMode = BinaryProtocolMode.Bytes, ILogger? logger = null) - : base(options, protocolMode, logger) { } - /// /// Primary constructor — accepts a fully-configured . /// diff --git a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md index b740daa..2c31636 100644 --- a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md +++ b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md @@ -199,11 +199,76 @@ In `Bytes` and `Segment` mode, the standard `WriteMessage` path is used. 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 `AcBinaryHubProtocolOptions.Validate()` method 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`. *(Note: this guard is currently commented out in `Validate()` to enable hybrid Windows-app + WASM testing against a single protocol instance. Will be re-enabled once the options are fully wired through `Program.cs`.)* +- **Send path**: `AsyncSegment` is **not supported on WebAssembly**. `AcBinaryHubProtocolOptions.Validate()` 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) +## Registration in `Program.cs` + +### Server + +```csharp +builder.Services.AddSignalR(hubOptions => + { + hubOptions.EnableDetailedErrors = true; + hubOptions.MaximumReceiveMessageSize = 30_000_000; + hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(60); + hubOptions.ClientTimeoutInterval = TimeSpan.FromSeconds(180); + hubOptions.StatefulReconnectBufferSize = 30_000_000; + }) + .AddAcBinaryProtocol(opts => + { + opts.ProtocolMode = BinaryProtocolMode.AsyncSegment; + // opts.FlushTimeout = TimeSpan.FromSeconds(10); // default + }); +``` + +### Client — `HubConnectionBuilder` as a DI transient + +The consumer (e.g. a class derived from `AcSignalRClientBase`) receives the builder via DI: + +```csharp +services.AddTransient(sp => +{ + var logger = sp.GetRequiredService(); + var hubUrl = $"{Config.BaseUrl}/{Config.HubName}"; + + var builder = new HubConnectionBuilder() + .WithUrl(hubUrl, HttpTransportType.WebSockets, options => + { + options.TransportMaxBufferSize = 30_000_000; + options.ApplicationMaxBufferSize = 30_000_000; + options.CloseTimeout = TimeSpan.FromSeconds(10); + options.SkipNegotiation = true; + }) + .ConfigureLogging(logging => + { + logging.SetMinimumLevel(LogLevel.Information); + logging.AddAcLogger(_ => logger); + }) + .WithAutomaticReconnect() + .WithStatefulReconnect() + .WithKeepAliveInterval(TimeSpan.FromSeconds(60)) + .WithServerTimeout(TimeSpan.FromSeconds(180)); + + builder.AddAcBinaryProtocol(opts => + { + // Desktop / server / native: AsyncSegment for pipeline parallelism. + // WebAssembly: must be Bytes or Segment (Validate throws on AsyncSegment). + opts.ProtocolMode = OperatingSystem.IsBrowser() + ? BinaryProtocolMode.Segment + : BinaryProtocolMode.AsyncSegment; + }); + + return builder; +}); + +services.AddSingleton(); // derived from AcSignalRClientBase +``` + +**Note**: `AcSignalRClientBase` is `HubConnectionBuilder`-injected and calls only `Build()` + dispatch wiring internally. All transport/protocol configuration lives in `Program.cs` — visible, overridable per environment, and identical on both ends of the wire. + +**Source:** `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs` (base), `AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs` (consumer logic), `AyCode.Services/SignalRs/BinaryProtocolMode.cs` (enum), `AyCode.Services/SignalRs/AcBinaryHubProtocolOptions.cs` (options), `AyCode.Services/SignalRs/AcSignalRProtocolExtensions.cs` (DI extensions) diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 9fa783b..945773d 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -77,8 +77,10 @@ For full architecture see `AyCode.Services/docs/SIGNALR.md`. | **Message Tag** | Integer identifier mapping to a method via `[SignalR(tag)]` or `[SignalRSendToClient(tag)]` attributes. | | **DynamicMethodRegistry** | Resolves message tags to `MethodInfo` at runtime. Static `ConcurrentDictionary` cache with lazy scan on miss. | | **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`. | -| **AcBinaryHubProtocol** | Unsealed base `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. Write: `BufferWriterBinaryOutput` standalone + `AcBinarySerializer.Serialize(value, output)` zero-copy to pipe. Read: `SequenceReader` from pipe's `ReadOnlySequence`, three-path argument deser (byte[] fast-path, IsRawBytesData, typed via SignalDataType). `_currentSignalParams` captures arg[2] for type-aware arg[3] deserialization. | -| **AyCodeBinaryHubProtocol** | Derived protocol (currently empty). Exists for registration and future project-specific hooks. Register this in both client and server. | +| **AcBinaryHubProtocol** | Unsealed base `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"` (configurable). Options-based ctor: `new AcBinaryHubProtocol(AcBinaryHubProtocolOptions)`. Write: `BufferWriterBinaryOutput` / `AsyncPipeWriterOutput` zero-copy to pipe. Read: `ArrayBinaryInput` via `GetArgBytes` (zero-copy single-seg / pool-rent multi-seg) for non-chunked; chunked receive via `SegmentBufferReader` + `SegmentBufferReaderInput` with platform-aware fallback. | +| **AyCodeBinaryHubProtocol** | Consumer-specific derived protocol (per-message header with `DataFlags`, `IsRawBytesData`, type resolution). Registered via `services.AddSignalR().AddAcBinaryProtocol(...)` on the server and `hubBuilder.AddAcBinaryProtocol(...)` on the client. | +| **AcBinaryHubProtocolOptions** | Mutable config class for protocol registration. Properties: `SerializerOptions`, `ProtocolMode`, `BufferSize`, `WaitForFlush`, `FlushTimeout`, `Name`, `Logger`. `Validate()` enforces invariants (incl. WASM + AsyncSegment block). `Clone()` for DI `IOptions` safety. | +| **AcSignalRProtocolExtensions** | DI extension class: `AddAcBinaryProtocol(ISignalRServerBuilder, Action?)` for server, `AddAcBinaryProtocol(IHubConnectionBuilder, Action<...>?)` for client. DI `IOptions` + inline-configure override chain. | | **SignalResponseDataMessage** | Internal DTO for client callback routing and stream wire format (not serialized as envelope on wire). `RawResponseData` is `object?` (typed object or byte[]). `GetResponseData()` performs direct cast. | | **SignalPostJsonDataMessage** | OBSOLETE — still exists but marked `[Obsolete]`. Legacy: serialized params to JSON inside Binary envelope. | | **AcSignalRDataSource** | Generic real-time `IList` with change tracking, CRUD via SignalRCrudTags, binary merge, rollback, sync state. |