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. |