# SignalR Client

Client-side SignalR transport: custom binary protocol, tag-based dispatch. Source: `SignalRs/`

> Server-side hub, session, broadcast: `AyCode.Services.Server/docs/SIGNALR/README.md`
> DataSource collection: `../SIGNALR_DATASOURCE/README.md`

## Design

Single hub method, tag-based dispatch:

```
Client ──OnReceiveMessage(tag, requestId, signalParams, data)──► Server
Client ◄──OnReceiveMessage(tag, requestId, signalParams, data)── Server
```

Tag (int) selects server method; all calls go through `OnReceiveMessage`. Metadata (`SignalParams`) and payload (`object data`) travel as **separate hub args** — `SignalParams` AcBinary-serialized; `data` zero-copy to pipe via `AcBinarySerializer`, or raw `byte[]` via protocol fast-path.

```
Client:                               Server:
AcSignalRClientBase                   AcWebSignalRHubBase<TTags, TLogger>
  ├─ HubConnection (WebSocket)          ├─ Hub<IAcSignalRHubItemServer>
  ├─ AyCodeBinaryHubProtocol            ├─ DynamicMethodRegistry
  ├─ Pending request tracking           ├─ Parameter deserialization
  └─ Response callbacks                 └─ Broadcast to other clients
```

## Tag System

Base tags in AyCode.Core:

```csharp
public class AcSignalRTags
{
    public const int None = 0;
    public const int PingTag = 90001;
    public const int EchoTag = 90002;
}
```

Consuming projects inherit and define own tags (plain int constants, must be unique per hub):

```csharp
public abstract class MyProjectTags : AcSignalRTags
{
    public const int GetOrders = 100;
    public const int GetOrderById = 101;
}
```

**Attributes:**
- `[Tag(42)]` — base: maps int tag → method
- `[SignalR(messageTag, sendToOtherClientTag, sendToOtherClientType)]` — server routing + broadcast
- `[SignalRSendToClient(100)]` — client receive dispatch

**SendToClientType:** `None` | `Others` | `Caller` | `All`

**Usage:**
```csharp
var orders = await client.PostAsync<List<Order>>(MyTags.GetOrders, companyId);        // single param
var result = await client.PostAsync<T>(MyTags.Query, [companyId, filter]);            // object[] params
await client.PostDataAsync(MyTags.SaveOrder, order);
await client.PostDataAsync(MyTags.SaveOrder, order, async response => { ... });       // async callback
```

CRUD helpers (`PostAsync`, `GetByIdAsync`, `GetAllAsync`, `PostDataAsync`) are generic transport, not tied to DataSource.

## Wire Protocol

### AcBinaryHubProtocol / AyCodeBinaryHubProtocol

Custom `IHubProtocol` (`"acbinary"`) replacing JSON. Zero-copy write: `BufferWriterBinaryOutput` + `AcBinarySerializer.Serialize(value, output)` directly to pipe. Zero-copy read: `SequenceReader<byte>` from pipe's `ReadOnlySequence`. `BinaryProtocolMode` ctor param selects transport: `Bytes` (default, ArrayBinaryOutput → byte[]), `Segment` (BWO zero-copy to PipeWriter, single flush), `AsyncSegment` (AsyncPipeWriterOutput, per-chunk FlushAsync + pipeline parallelism).

`AcBinaryHubProtocol` is the unsealed base — general binary framing. `AyCodeBinaryHubProtocol` derives with consumer-specific logic: `SignalParams` capture (`OnArgumentRead` hook), `IsRawBytesData` path, `SignalDataType` resolution. Register `AyCodeBinaryHubProtocol` on both client and server.

> Wire format, argument framing, dual BWO pattern, length prefix patching: `../SIGNALR_BINARY_PROTOCOL/README.md`

### SignalParams + Payload Separation

`SignalParams` (separate hub argument, `[AcBinarySerializable]`):

| Field | Type | Purpose |
|-------|------|---------|
| `Status` | SignalResponseStatus | Success/Error |
| `DataSerializerType` | AcSerializerType | Binary or JsonGZip — tells client how to deserialize response data |
| `Parameters` | `byte[]?` | Serialized `byte[][]` as single `byte[]` (protocol fast-path). Null when no parameters. |
| `SignalDataType` | `string?` | `AssemblyQualifiedName` of response object type. Server sets before sending. Protocol uses this for eager type-aware deserialization. Null for raw byte[] responses. |
| `IsRawBytesData` | `bool` | Client sets true when `T == byte[]` (e.g. DataSource populate/merge). Protocol returns raw byte[] without deserialization. |

Typed access via methods (PostDataJson pattern):
- **Client**: `SetParameterValues(object[])` — packs each param via `ToBinary()` → `byte[][]` → `byte[]`
- **Server**: `GetParameterValues(ParameterInfo[])` — unpacks `byte[]` → `byte[][]` → per-element `BinaryTo(targetType)`
- Protocol never sees `byte[][]` — only `byte[]`.

`object data` (4th hub argument) — protocol handles three cases on read:
1. **byte[] fast-path**: first byte is `BinaryTypeCode.ByteArray(0x44)` → skip tag, rest is raw payload bytes. No VarUInt (argLength implies size). No deserializer.
2. **IsRawBytesData** (AyCodeBinaryHubProtocol): `SignalParams.IsRawBytesData == true` → return entire argSlice as raw `byte[]`. No deserialization. Consumer handles deserialization.
3. **Typed deserialization** (AyCodeBinaryHubProtocol): resolve type from `SignalParams.SignalDataType` → `AcBinaryDeserializer.Deserialize(sequence, type)` → return typed object.

`Parameters` and `data` are **independent** — both can be null or filled in any direction (SignalR is bidirectional).

| Combination | Parameters | data | Example |
|------------|-----------|------|---------|
| Request | `byte[]` (packed params) | null/empty | client calls server method |
| Response | null | typed object or byte[] | server returns result |
| Request + data | `byte[]` | typed object | client responds to server with data |
| Signal | null | null/empty | ping, status change, broadcast trigger |

`SignalResponseDataMessage` is an **internal DTO** for client-side callback routing and stream wire format — constructed in-memory from `signalParams` + `data`, never serialized as envelope on wire. `RawResponseData` is `object?` (typed object or byte[]). `GetResponseData<T>()` performs a safe `is T` cast — type-mismatch returns null, does **not** throw (so a caller-requested subtype that differs from the wire type silently yields null, not an error).

## Request/Response Flow

### Client → Server

```
1. PostAsync<T>(tag, param) / PostAsync<T>(tag, params[]) / PostDataAsync(tag, data, callback)
2. signalParams.SetParameterValues(object[]):
   Each param ToBinary() → byte[][] → ToBinary() → byte[] (single wire blob)
3. SignalParams { Status = Success, Parameters = byte[], IsRawBytesData = (typeof(T) == typeof(byte[])) }
4. SendCoreAsync → HubConnection.SendAsync("OnReceiveMessage", tag, requestId, signalParams, null)
5. AyCodeBinaryHubProtocol frames on wire (signalParams via AcBinary, data = null for requests)
```

### Server → Client

```
OnReceiveMessage(tag, requestId, signalParams, object data)
├─ Construct SignalResponseDataMessage in-memory:
│  └─ { MessageTag, Status, DataSerializerType, RawResponseData = data }
├─ Matching requestId in pending dict:
│  ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
│  └─ GetResponseData<T>(): safe `is T` cast (mismatch → null, no throw)
│     Protocol deserialized via SignalDataType = wire/server type (may ≠ caller's T)
└─ No match (broadcast):
   └─ abstract MessageReceived(tag, signalParams, object data).Forget()
```

Request pooling: `SignalRRequestModel` via `SignalRRequestModelPool` (ObjectPool + IResettable).

## Parameter Deserialization (Server)

Server calls `signalParams.GetParameterValues(paramInfos)`:

```
GetParameterValues(ParameterInfo[]):
  1. byte[] → BinaryTo<byte[][]>() (cached in _parameterValues)
  2. For each: byte[][i].BinaryTo(paramInfos[i].ParameterType)
  3. Trailing optional params filled with defaults
  Hub validates: missing required params throw ArgumentException.
```

Type-guided — each parameter individually serialized/deserialized with its concrete type, avoiding the `object[]` → dictionary problem of untyped binary deserialization.

> Known concerns on parameter serialization (per-parameter overhead, AcBinary-only): `SIGNALR_ISSUES.md` under `ACCORE-SIG-I-L5K3` and `ACCORE-SIG-I-H8D6`.

## Response Patterns

| Pattern | Method | Blocking |
|---------|--------|----------|
| Sync wait | `PostAsync<T>(tag, data)` | Yes (60s timeout) |
| Async callback | `PostDataAsync(tag, data, async msg => {...})` | No |
| Fire-and-forget | `PostDataAsync(tag, data, msg => {...})` | No |

## Connection

- WebSockets only (`SkipNegotiation = true`)
- 30MB transport + 30MB application buffer
- `WithAutomaticReconnect()` + `WithStatefulReconnect()`
- Keepalive 60s, server timeout 180s

## Diagnostics

`AcSignalRClientBase.EnableBinaryDiagnostics = true` — hex dump, header parsing, property enumeration.

## Source Files

| Component | Path |
|-----------|------|
| Client base | `SignalRs/AcSignalRClientBase.cs` |
| Binary protocol (base) | `SignalRs/AcBinaryHubProtocol.cs` |
| Binary protocol (derived) | `SignalRs/AyCodeBinaryHubProtocol.cs` |
| Protocol mode enum | `SignalRs/BinaryProtocolMode.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` |
| Params class | `SignalRs/ISignalParams.cs` |
| Serialization | `SignalRs/SignalRSerializationHelper.cs` |
