# SignalR Client Client-side SignalR transport with custom binary protocol and tag-based dispatch. Source: `SignalRs/` in this project. > For server-side hub, session, broadcast see [`AyCode.Services.Server/docs/SIGNALR_SERVER.md`](../../AyCode.Services.Server/docs/SIGNALR_SERVER.md). > For the DataSource collection see [`AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`](../../AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md). ## Design Overview All communication flows through a **single hub method** with **tag-based dispatch**: ``` Client ──OnReceiveMessage(tag, bytes, requestId)──► Server Client ◄──OnReceiveMessage(tag, bytes, requestId)── Server ``` `OnReceiveMessage` is the only transport channel — all calls go through it. The `tag` (int) determines **which server method to invoke**. Projects call any endpoint by specifying the tag; the server dispatches accordingly. ``` Client side: Server side: ───────────────────── ───────────────────── AcSignalRClientBase AcWebSignalRHubBase ├─ HubConnection (WebSocket) ├─ Hub ├─ AcBinaryHubProtocol ├─ DynamicMethodRegistry ├─ Pending request tracking ├─ Parameter deserialization └─ Response callbacks └─ Broadcast to other clients ``` ## Tag System ### Tag Definition AyCode.Core defines only the base class and built-in infrastructure tags: ```csharp public class AcSignalRTags { public const int None = 0; public const int PingTag = 90001; public const int EchoTag = 90002; } ``` **Consuming projects** define their own tags by inheriting `AcSignalRTags`: ```csharp public abstract class MyProjectTags : AcSignalRTags { public const int GetOrders = 100; public const int GetOrderById = 101; public const int SaveOrder = 102; } ``` Tags are plain `int` constants. The project decides the numbering. Tags must be unique across all registered services within a hub. ### Tag Attributes Three attribute levels: ```csharp // Base: maps an integer tag to a method [Tag(42)] // Server method: tag + optional broadcast behavior [SignalR(messageTag: 100, sendToOtherClientTag: 100, sendToOtherClientType: SendToClientType.Others)] // Client receive: marks method for server→client dispatch [SignalRSendToClient(100)] ``` **SendToClientType:** `None` (no broadcast), `Others` (all except caller), `Caller` (response only), `All` (everyone). ### How Projects Use Tags Projects call the SignalR transport directly — not only through DataSource: ```csharp var orders = await signalRClient.PostAsync>(MyTags.GetOrders, companyId); var order = await signalRClient.GetByIdAsync(MyTags.GetOrderById, orderId); await signalRClient.PostDataAsync(MyTags.SaveOrder, order); // Async callback (non-blocking) await signalRClient.PostDataAsync(MyTags.SaveOrder, order, async response => { ... }); // Fire-and-forget await signalRClient.PostDataAsync(MyTags.SaveOrder, order, response => { ... }); ``` The CRUD helpers (`PostAsync`, `GetByIdAsync`, `GetAllAsync`, `PostDataAsync`) are all generic transport methods that work with any tag — they are not tied to DataSource. ## Wire Protocol ### AcBinaryHubProtocol Custom `IHubProtocol` (name: `"acbinary"`), replaces default JSON. Frame format: ``` [4 bytes: payload length, little-endian] [1 byte: message type] [payload...] ``` Message types: Invocation(1), StreamItem(2), Completion(3), Ping(6), Close(7), Ack(8), Sequence(9). Arguments serialized individually with VarUInt length prefix. Direct write to `IBufferWriter` via `BufferWriterBinaryOutput`. ### Response Message `SignalResponseDataMessage` carries: | Field | Type | Purpose | |-------|------|---------| | `MessageTag` | int | Tag identifying the operation | | `Status` | SignalResponseStatus | Success/Error | | `ResponseData` | byte[] | Binary-serialized payload (or GZip+JSON fallback) | | `DataSerializerType` | AcSerializerType | Binary or Json | **Binary mode** (default): `AcBinarySerializer.ToBinary(data)` → raw bytes. **JSON fallback**: `ToJson(data)` → `GzipHelper.Compress(json)` → bytes. ## Request/Response Flow ### Client → Server ``` 1. PostAsync(tag, postData) or PostDataAsync(tag, data, callback) 2. CreatePostMessage(postData): ├─ Primitives/strings/enums/value types → IdMessage (JSON array of serialized values) └─ Complex objects → SignalPostJsonDataMessage ⚠️ tech debt: JSON-in-Binary 3. SerializeToBinary(message) wraps in Binary envelope 4. HubConnection.SendAsync("OnReceiveMessage", tag, bytes, requestId) 5. AcBinaryHubProtocol frames it on the wire ``` ### Server → Client ``` 13. Client.OnReceiveMessage(tag, bytes, requestId) 14. Has matching requestId in pending ConcurrentDictionary? ├─ YES (response to own request): │ 15. DeserializeFromBinary(bytes) │ 16. Route based on pending request type: │ ├─ null (sync wait) → set ResponseByRequestId, WaitToAsync completes │ ├─ Action → invoke directly │ └─ Func → invoke and await │ 17. GetResponseData(): │ ├─ Binary: ResponseData.BinaryTo() │ └─ JSON: GzipDecompress → AcJsonDeserializer.Deserialize() │ └─ NO (broadcast from another client's action): 18. abstract MessageReceived(tag, bytes).Forget() └─ Consuming project overrides to handle server push ``` **Broadcast receive:** When the server broadcasts to `Others`/`All`, receiving clients have no matching `requestId`. The message falls through to `MessageReceived(int messageTag, byte[] messageBytes)` — an abstract method the consuming project overrides. **Request model pooling:** `SignalRRequestModel` instances are managed via `SignalRRequestModelPool` (ObjectPool + IResettable) to avoid allocations per request. ## Response Patterns | Pattern | Method | Blocking | |---------|--------|----------| | Sync wait | `PostAsync(tag, data)` | Yes, polls up to `TransportSendTimeout` (60s) | | Async callback | `PostDataAsync(tag, data, async msg => {...})` | No, `Func` | | Fire-and-forget | `PostDataAsync(tag, data, msg => {...})` | No, `Action` | ## Connection Lifecycle **Client configuration:** - Transport: WebSockets only (`SkipNegotiation = true`) - Buffer: 30MB transport + 30MB application - Reconnection: `WithAutomaticReconnect()` + `WithStatefulReconnect()` - Keepalive: 60s interval, 180s server timeout ## Diagnostics Enable with `AcSignalRClientBase.EnableBinaryDiagnostics = true`. Logs: hex dump (500 byte sample), header parsing (version, marker), property count + names via VarUInt reading. ## Known Technical Debt **JSON-in-Binary request parameters:** Client→server requests currently wrap parameters in JSON inside the binary envelope (`SignalPostJsonDataMessage`). This adds an unnecessary serialization round-trip. Responses are already pure binary. Do NOT attempt to fix as a side effect — requires coordinated changes across all consuming projects. ## Key Source Files | Component | Path | |-----------|------| | Client base | `SignalRs/AcSignalRClientBase.cs` | | Binary protocol | `SignalRs/AcBinaryHubProtocol.cs` | | Tag attributes | `SignalRs/SignalMessageTagAttribute.cs` | | Base tags | `SignalRs/AcSignalRTags.cs` | | CRUD tags | `SignalRs/SignalRCrudTags.cs` | | SendToClientType | `SignalRs/SendToClientType.cs` | | Message types | `SignalRs/IAcSignalRHubClient.cs` | | Serialization | `SignalRs/SignalRSerializationHelper.cs` |