--- url: /reference/api.md --- # API Overview This page lists the public API surface of SignalARRR across all packages. ## Server API (`Cocoar.SignalARRR.Server`) ### Registration ```csharp // IServiceCollection extension services.AddSignalARRR(options => options .AddServerMethodsFrom(assembly)); // IEndpointRouteBuilder extension app.MapHARRRController(path); app.MapHARRRController(path, configureOptions); ``` ### HARRR (Hub base class) | Member | Type | Description | |--------|------|-------------| | `ServiceProvider` | `IServiceProvider` | DI container | | `Logger` | `ILogger?` | Logger instance | | `ClientContext` | `ClientContext` | Current client context | | `OnConnectedAsync()` | `Task` | Client connected (registers in ClientManager) | | `OnDisconnectedAsync(Exception?)` | `Task` | Client disconnected (unregisters) | **Hub methods** (wire protocol — called by clients internally): | Method | Description | |--------|-------------| | `InvokeMessage(ClientRequestMessage)` | Fire-and-forget | | `InvokeMessageResult(ClientRequestMessage)` | Returns result | | `SendMessage(ClientRequestMessage)` | Fire-and-forget (async void) | | `StreamMessage(ClientRequestMessage, CancellationToken)` | Server-to-client stream | | `StreamItemToServer(Guid, object)` | Client-to-server stream item | | `StreamCompleteToServer(Guid, string?)` | Client-to-server stream completion | ### ServerMethods / ServerMethods\ | Property | Type | Description | |----------|------|-------------| | `ClientContext` | `ClientContext` | Current client context | | `Context` | `HubCallerContext` | SignalR caller context | | `Clients` | `IHubCallerClients` | Client connections | | `Groups` | `IGroupManager` | Group management | | `Logger` | `ILogger` | Logger | ### ClientContext | Property | Type | Description | |----------|------|-------------| | `Id` | `string` | Connection ID | | `HARRRType` | `Type` | Hub type | | `RemoteIp` | `IPAddress?` | Client IP | | `User` | `ClaimsPrincipal` | Authenticated user | | `UserValidUntil` | `DateTime` | Token expiration | | `ConnectedAt` | `DateTime` | Connection time | | `ReconnectedAt` | `List` | Reconnection history | | `Attributes` | `ClientAttributes` | Custom key-value storage | | `ConnectedTo` | `Uri` | Hub URL | | Method | Description | |--------|-------------| | `GetTypedMethods()` | Get typed proxy to call this client | | `TryAuthenticate(MethodInfo)` | Validate token, challenge if expired | ### ClientManager | Method | Description | |--------|-------------| | `GetClientById(string)` | Get client by connection ID | | `GetAllClients()` | All connected clients | | `GetAllClients(predicate)` | Filter clients | | `GetHARRRClients()` | Clients for a specific hub type | | `GetHARRRClients(predicate)` | Filter clients for a hub type | ## Client API (`Cocoar.SignalARRR.Client`) ### HARRRConnection | Static method | Description | |--------------|-------------| | `Create(Action, options?)` | Create from builder | | `Create(HubConnection, options?)` | Wrap existing connection | | Method | Description | |--------|-------------| | `GetTypedMethods()` | Get typed proxy for a contract interface | | `InvokeCoreAsync(message, ct)` | Call server, await typed result | | `SendCoreAsync(message, ct)` | Fire-and-forget | | `StreamAsyncCore(message, ct)` | Server-to-client stream | | `OnServerRequest(name, handler)` | Register server-to-client handler | | `StartAsync(ct)` | Connect | | `StopAsync(ct)` | Disconnect | | `DisposeAsync()` | Dispose | | `AsSignalRHubConnection()` | Access raw HubConnection | | Property | Type | Description | |----------|------|-------------| | `ConnectionId` | `string?` | Connection ID | | `State` | `HubConnectionState` | Connection state | | `ServerTimeout` | `TimeSpan` | Server timeout | | `KeepAliveInterval` | `TimeSpan` | Keepalive interval | | `HandshakeTimeout` | `TimeSpan` | Handshake timeout | | Event | Description | |-------|-------------| | `Closed` | Fires when connection closes | | `Reconnecting` | Fires when reconnecting | | `Reconnected` | Fires when reconnected | ## TypeScript API (`@cocoar/signalarrr`) ### HARRRConnection ```ts class HARRRConnection { static create(builderOrConnection, options?): HARRRConnection invoke(methodName: string, ...args: unknown[]): Promise send(methodName: string, ...args: unknown[]): Promise stream(methodName: string, ...args: unknown[]): IStreamResult onServerMethod(methodName: string, handler: (...args) => unknown): this start(): Promise stop(): Promise asSignalRHubConnection(): signalR.HubConnection onClose(callback: (error?) => void): void onReconnecting(callback: (error?) => void): void onReconnected(callback: (connectionId?) => void): void connectionId: string | null state: HubConnectionState baseUrl: string serverTimeoutInMilliseconds: number keepAliveIntervalInMilliseconds: number } ``` ### Exported types ```ts export { HARRRConnection } from './harrr-connection.js' export { HARRRConnectionOptions } from './harrr-connection-options.js' export type { ClientRequestMessage } from './models/client-request-message.js' export type { ServerRequestMessage } from './models/server-request-message.js' export type { CancellationTokenReference } from './models/cancellation-token-reference.js' ``` ## Common types (`Cocoar.SignalARRR.Common`) ### ClientRequestMessage | Property | Type | Description | |----------|------|-------------| | `Method` | `string` | Method name (`ClassName.MethodName`) | | `Arguments` | `object[]` | Method arguments | | `Authorization` | `string?` | Bearer token | | `GenericArguments` | `string[]` | Generic type arguments | ### ServerRequestMessage | Property | Type | Description | |----------|------|-------------| | `Id` | `Guid` | Correlation ID for reply | | `Method` | `string` | Method name | | `Arguments` | `object[]` | Method arguments | | `GenericArguments` | `string[]` | Generic type arguments | | `CancellationGuid` | `Guid?` | Cancellation correlation ID | | `StreamId` | `Guid?` | Stream correlation ID | ### Attributes | Attribute | Description | |-----------|-------------| | `[SignalARRRContract]` | Marks an interface for proxy generation | | `[MessageName(string)]` | Override the default method name | ## Next steps * [Wire Protocol](/reference/wire-protocol) — message flow details * [Packages](/reference/packages) — package selection guide * [Getting Started](/guide/getting-started) — quick setup --- --- url: /guide/server/authorization.md --- # Authorization SignalARRR integrates with ASP.NET Core's authorization system. Apply `[Authorize]` at the method, class, or hub level. Tokens are validated continuously, and expired tokens trigger an automatic challenge/refresh flow. ## Method-level authorization Apply `[Authorize]` to individual methods: ```csharp public class AdminMethods : ServerMethods, IAdminHub { [Authorize(Policy = "AdminOnly")] public Task DeleteUser(string userId) { ... } [Authorize(Roles = "Admin,Moderator")] public Task BanUser(string userId) { ... } [AllowAnonymous] public Task GetServerInfo() { ... } } ``` ## Class-level authorization Apply `[Authorize]` to the entire class — all methods require authentication: ```csharp [Authorize] public class SecureMethods : ServerMethods, ISecureHub { public Task GetSecret() { ... } // requires authentication [AllowAnonymous] public Task GetPublicData() { ... } // opt-out for this method } ``` ## Hub-level inheritance If the hub class itself has `[Authorize]`, all `ServerMethods` classes inherit it automatically: ```csharp [Authorize] public class SecureHub : HARRR { public SecureHub(IServiceProvider sp) : base(sp) { } } // All methods in this class require authentication, inherited from the hub public class SecureMethods : ServerMethods, ISecureHub { ... } ``` ## Authentication setup Configure ASP.NET Core authentication as usual. SignalARRR reads the `Authorization` header from client requests: ```csharp builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidIssuer = "your-issuer", ValidAudience = "your-audience", IssuerSigningKey = new SymmetricSecurityKey(key) }; }); builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")); }); ``` ## Client-side token provider ### .NET Client Provide a token factory when creating the connection: ```csharp var connection = HARRRConnection.Create(builder => { builder.WithUrl("https://localhost:5001/apphub", options => { options.AccessTokenProvider = () => Task.FromResult(GetCurrentToken()); }); }); ``` ### TypeScript Client ```ts const connection = HARRRConnection.create(builder => { builder.withUrl('https://localhost:5001/apphub', { accessTokenFactory: () => getAuthToken(), }); }); ``` ## Automatic token challenge When a client's token expires during an active connection, SignalARRR doesn't disconnect it. Instead, the next authorized method call triggers a **challenge flow**: 1. Server detects the cached authentication has expired 2. Server sends `ChallengeAuthentication` to the client (via SignalR's native client results) 3. Client's `AccessTokenProvider` is called to get a fresh token 4. Client returns the new token directly from the handler 5. Server validates the new token against the configured authentication scheme and continues the request This happens transparently — no client-side code needed beyond providing a token factory. ::: tip Authentication results are cached per client (default: 3 minutes). When a client connects to a hub with `[Authorize]`, the cache is initialized from the SignalR negotiate authentication, so the first method call uses the cached principal without triggering a challenge. ::: The cache duration is configurable: ```csharp builder.Services.AddSignalARRR(options => options .AddServerMethodsFrom(typeof(Program).Assembly) .WithAuthCacheDuration(TimeSpan.FromMinutes(5))); ``` When `[Authorize]` is used without specifying a scheme (the common case), SignalARRR automatically uses the default authentication scheme configured via `AddAuthentication()`. ## ClientContext user data Inside `ServerMethods`, access the authenticated user through `ClientContext`: ```csharp public Task GetUserInfo() { var user = ClientContext.User; var name = user.FindFirst(ClaimTypes.Name)?.Value; var roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value); return Task.FromResult($"{name} ({string.Join(", ", roles)})"); } ``` ## Next steps * [Client Manager](/guide/server/client-manager) — query authenticated clients * [Connection Setup (.NET)](/guide/dotnet-client/connection-setup) — configure token providers * [TypeScript Setup](/guide/typescript-client/setup) — authentication in the TypeScript client --- --- url: /guide/advanced/cancellation.md --- # Cancellation Propagation SignalARRR supports server-initiated cancellation of client operations. The server can pass a `CancellationToken` to a client method and cancel it remotely. On the TypeScript client, `CancellationToken` is converted to an `AbortSignal`. ## Server side When a server method calls a client method with a `CancellationToken` parameter, SignalARRR serializes a `CancellationTokenReference` marker and tracks it: ```csharp public async Task StartLongRunningTask() { using var cts = new CancellationTokenSource(); var client = ClientContext.GetTypedMethods(); // The CancellationToken is propagated to the client var task = client.ProcessData("data", cts.Token); // Cancel after 5 seconds cts.CancelAfter(TimeSpan.FromSeconds(5)); await task; } ``` When `cts.Cancel()` is called, the server sends a `CancelTokenFromServer` message to the client with the matching cancellation GUID. ## .NET client On the .NET client, the `CancellationToken` is received as a standard `CancellationToken`: ```csharp connection.OnServerRequest("ProcessData", (string data, CancellationToken ct) => { while (!ct.IsCancellationRequested) { // Process chunks... } return "completed"; }); ``` ## TypeScript client On the TypeScript client, `CancellationToken` parameters are converted to `AbortSignal`: ```ts connection.onServerMethod('ProcessData', async (data: string, signal: AbortSignal) => { for (let i = 0; i < 100; i++) { if (signal.aborted) { throw new Error('Cancelled'); } await processChunk(data, i); } return 'done'; }); ``` ## How it works ```mermaid sequenceDiagram participant Server participant Client Server->>Client: InvokeServerRequest (args include CancellationTokenReference) Note over Client: Replaces CancellationTokenReference with AbortSignal/CancellationToken Client->>Client: Starts handler execution Server->>Client: CancelTokenFromServer (CancellationGuid) Note over Client: AbortController.abort() / CancellationToken cancels Note over Client: Handler throws (native SignalR client result returns error) Server-->>Server: InvokeCoreAsync completes with error ``` ### Internal flow 1. Server serializes `CancellationToken` parameters as `CancellationTokenReference { Id: guid }` 2. Server stores the `CancellationGuid` in the `ServerRequestMessage` 3. Client detects `CancellationTokenReference` in the arguments 4. Client creates an `AbortController` (TypeScript) or `CancellationTokenSource` (.NET) mapped to the GUID 5. Client passes the `AbortSignal`/`CancellationToken` to the handler 6. When the server cancels, it sends `CancelTokenFromServer` with the GUID 7. Client triggers the abort/cancellation ## CancellationManager (TypeScript internals) The `CancellationManager` class maps GUIDs to `AbortController` instances: | Method | Description | |--------|-------------| | `create(id)` | Creates an `AbortController`, returns its `AbortSignal` | | `cancel(id)` | Calls `abort()` on the controller, removes from map | | `remove(id)` | Removes the controller without aborting | ## Next steps * [Server Method Handlers (TypeScript)](/guide/typescript-client/server-methods) — registering handlers * [Server-to-Client Handlers (.NET)](/guide/dotnet-client/server-to-client) — .NET handler registration * [Wire Protocol](/reference/wire-protocol) — protocol message details --- --- url: /reference/client-comparison.md --- # Client Comparison SignalARRR has three client implementations. This page shows what each client supports. ## RPC | | .NET | TS | Swift | |-|:----:|:--:|:-----:| | Invoke (await result) | ✓ | ✓ | ✓ | | Send (fire & forget) | ✓ | ✓ | ✓ | | Generic arguments | ✓ | ✓ | ✓ | ## Item Streaming | | .NET | TS | Swift | |-|:----:|:--:|:-----:| | Server→Client | ✓ | ✓ | ✓ | | Client→Server | ✓ | ✓ | ✓ | | Stream method handlers | ✓ | ✓ | ✓ | ## Server-to-Client RPC | | .NET | TS | Swift | |-|:----:|:--:|:-----:| | Method handlers | ✓ | ✓ | ✓ | | CancellationToken | ✓ | ✓ | ✓ | ## File Transfer (HTTP Stream References) | | .NET | TS | Swift | |-|:----:|:--:|:-----:| | `Stream` parameters | ✓ | ✓ | ✓ | ## Authorization | | .NET | TS | Swift | |-|:----:|:--:|:-----:| | Token provider | ✓ | ✓ | ✓ | | Auto challenge/refresh | ✓ | ✓ | ✓ | ## Proxy Generation | | .NET | TS | Swift | |-|:----:|:--:|:-----:| | Compile-time proxies | Source Generator | — | `@HubProxy` Macro | | Runtime fallback | DispatchProxy | — | — | ## Connection | | .NET | TS | Swift | |-|:----:|:--:|:-----:| | Auto-reconnect | ✓ | ✓ | ✓ | | Connection events | ✓ | ✓ | ✓ | | Raw SignalR access | ✓ | ✓ | ✓ | | Raw `on/off` overloads | 16 | — | 8 | | Interface registration | ✓ | — | ✓ | ## Concurrency Model | | .NET | TS | Swift | |-|:----:|:--:|:-----:| | Async pattern | async/await | Promise | async/await | | Cancellation | CancellationToken | AbortSignal | Actor | | Serialization | System.Text.Json | JSON | Codable | | MessagePack | ✓ | ✓ | ✓ | ## Transport | | .NET | TS | Swift | |-|:----:|:--:|:-----:| | WebSockets | ✓ | ✓ | ✓ | | Server-Sent Events | ✓ | ✓ | ✓ | | Long Polling | ✓ | ✓ | ✓ | | Transport fallback | ✓ | ✓ | ✓ | ## API Comparison ### Creating a connection ::: code-group ```csharp [.NET] var connection = HARRRConnection.Create(builder => { builder.WithUrl("https://server/hub"); }); await connection.StartAsync(); ``` ```ts [TypeScript] const connection = HARRRConnection.create(builder => { builder.withUrl('https://server/hub'); }); await connection.start(); ``` ```swift [Swift] let connection = await HARRRConnection.create( url: "https://server/hub" ) try await connection.start() ``` ::: ### With MessagePack ::: code-group ```csharp [.NET] var connection = HARRRConnection.Create(builder => { builder.WithUrl("https://server/hub"); builder.AddMessagePackProtocol(); }); ``` ```ts [TypeScript] import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack'; const connection = HARRRConnection.create(builder => { builder.withUrl('https://server/hub'); builder.withHubProtocol(new MessagePackHubProtocol()); }); ``` ```swift [Swift] let connection = await HARRRConnection.create( url: "https://server/hub", hubProtocol: .messagepack ) ``` ::: ### Typed proxies ::: code-group ```csharp [.NET — Source Generator] // Shared project: mark with [SignalARRRContract] [SignalARRRContract] public interface IChatHub { Task SendMessage(string user, string message); Task> GetHistory(); } // Client: get typed proxy var chat = connection.GetTypedMethods(); await chat.SendMessage("Alice", "Hello!"); ``` ```swift [Swift — @HubProxy Macro] // Client: mark with @HubProxy @HubProxy protocol IChatHub { func sendMessage(user: String, message: String) async throws func getHistory() async throws -> [String] } // Client: get typed proxy let chat = connection.getTypedMethods(IChatHubProxy.self) try await chat.sendMessage(user: "Alice", message: "Hello!") ``` ::: TypeScript has no typed proxy generation — method names are passed as strings. ### Invoke / Send / Stream ::: code-group ```csharp [.NET] // Invoke (await result) var result = await connection.InvokeCoreAsync(message, ct); // Send (fire-and-forget) await connection.SendCoreAsync(message, ct); // Stream await foreach (var item in connection.StreamAsyncCore(message, ct)) Console.WriteLine(item); ``` ```ts [TypeScript] // Invoke const result = await connection.invoke('Method.Name'); // Send await connection.send('Method.Name', arg1, arg2); // Stream connection.stream('Method.Name').subscribe({ next: item => console.log(item), }); ``` ```swift [Swift] // Invoke let result: String = try await connection.invoke("Method.Name") // Send try await connection.send("Method.Name", arguments: arg1, arg2) // Stream for try await item in try await connection.stream("Method.Name") as AsyncThrowingStream { print(item) } ``` ::: ### Server-to-client handlers ::: code-group ```csharp [.NET] connection.OnServerRequest("GetClientName", name => { return Environment.MachineName; }); ``` ```ts [TypeScript] connection.onServerMethod('GetClientName', () => { return navigator.userAgent; }); ``` ```swift [Swift] await connection.onServerMethod("GetClientName") { _ in AnyCodable(stringLiteral: UIDevice.current.name) } ``` ::: ### CancellationToken handling | Client | Mechanism | Type | |--------|-----------|------| | .NET | Standard `CancellationToken` | Native | | TypeScript | `AbortSignal` via `CancellationManager` (Map-based) | Web API | | Swift | Actor-based `CancellationManager` with continuations | Swift Concurrency | ### Packages | Client | Package | Install | |--------|---------|---------| | .NET | `Cocoar.SignalARRR.Client` | `dotnet add package` | | TypeScript | `@cocoar/signalarrr` | `npm install` | | Swift | `CocoarSignalARRR` + `CocoarSignalARRRMacros` | Swift Package Manager | --- --- url: /guide/server/client-manager.md --- # Client Manager `ClientManager` tracks all connected clients and enables server-to-client RPC from anywhere — controllers, background services, or other hubs. Inject it from DI as a singleton. ## Inject ClientManager ```csharp public class NotificationService { private readonly ClientManager _clients; public NotificationService(ClientManager clients) => _clients = clients; public void NotifyUser(string connectionId) { var methods = _clients.GetTypedMethods(connectionId); methods.ReceiveMessage("System", "You have a new notification"); } } ``` ## Query clients | Method | Description | |--------|-------------| | `GetClientById(string id)` | Get a single client by connection ID | | `GetAllClients()` | All connected clients | | `GetAllClients(predicate)` | Filter clients by a predicate | | `GetHARRRClients()` | Clients connected to a specific hub type | | `GetHARRRClients(predicate)` | Filter clients of a specific hub type | ### Examples ```csharp // Get all clients connected to the ChatHub var chatClients = _clients.GetHARRRClients(); // Find a specific user var adminClients = _clients.GetAllClients(c => c.User.IsInRole("Admin")); // Filter by custom attributes var mobileClients = _clients.GetAllClients() .WithAttribute("Platform", "iOS"); ``` ## Typed extension methods Extension methods on `ClientManager` combine client lookup and typed proxy creation in one step: | Method | Description | |--------|-------------| | `GetTypedMethods(connectionId)` | Typed proxy for a specific client | | `GetAllTypedMethods()` | `(ClientContext, T)` tuples for all clients | | `GetTypedMethodsForHub()` | `(ClientContext, T)` tuples scoped to a hub type | ```csharp // Call a single client by ID var methods = _clients.GetTypedMethods(connectionId); methods.ReceiveMessage("System", "Hello!"); // Broadcast to all clients (typed) foreach (var (ctx, methods) in _clients.GetAllTypedMethods()) { methods.ReceiveMessage("System", $"Hello {ctx.Id}!"); } // Broadcast scoped to a specific hub type foreach (var (ctx, methods) in _clients.GetTypedMethodsForHub()) { methods.ReceiveMessage("System", "Hub-scoped broadcast"); } ``` ## Collection extensions on ClientContext `IEnumerable` has extension methods for batch invocations: | Method | Description | |--------|-------------| | `InvokeAllAsync(method, args, ct)` | Invoke method on all clients, await all results | | `InvokeOneAsync(method, args, ct)` | Invoke on clients until one succeeds | | `WithAttribute(key)` | Filter by attribute existence | | `WithAttribute(key, value)` | Filter by attribute key-value match | ```csharp // Ask all clients with a specific attribute and get the first successful response var result = await _clients.GetAllClients() .WithAttribute("Tag", "primary") .InvokeOneAsync("GetStatus", Array.Empty(), ct); // result.ClientId — which client responded // result.Value — the return value ``` ## Use in controllers ```csharp [ApiController] [Route("api/[controller]")] public class NotificationController : ControllerBase { private readonly ClientManager _clients; public NotificationController(ClientManager clients) => _clients = clients; [HttpPost("broadcast")] public IActionResult Broadcast([FromBody] string message) { foreach (var (_, methods) in _clients.GetTypedMethodsForHub()) { methods.ReceiveMessage("API", message); } return Ok(); } } ``` ## Use in background services ```csharp public class HeartbeatService : BackgroundService { private readonly ClientManager _clients; public HeartbeatService(ClientManager clients) => _clients = clients; protected override async Task ExecuteAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { foreach (var (_, methods) in _clients.GetTypedMethodsForHub()) { methods.ReceiveMessage("System", "heartbeat"); } await Task.Delay(30_000, ct); } } } ``` ## ClientContext properties Each `ClientContext` provides detailed information about the connected client: | Property | Type | Description | |----------|------|-------------| | `Id` | `string` | Connection ID | | `HARRRType` | `Type` | Hub type the client is connected to | | `RemoteIp` | `IPAddress?` | Client's IP address | | `User` | `ClaimsPrincipal` | Authenticated user claims | | `ConnectedAt` | `DateTime` | Connection timestamp | | `ReconnectedAt` | `List` | Reconnection history | | `Attributes` | `ClientAttributes` | Custom key-value storage | | `ConnectedTo` | `Uri` | Hub URL | ## Custom client attributes Clients can pass custom attributes via HTTP headers (prefixed with `#`) or query parameters (prefixed with `@`) during the initial handshake: ```csharp // Server: read custom attributes var version = client.Attributes["AppVersion"]; var platform = client.Attributes["Platform"]; // Check attribute existence bool hasPlatform = client.Attributes.Has("Platform"); bool isIOS = client.Attributes.Has("Platform", "iOS"); ``` ## Next steps * [Server Methods](/guide/server/server-methods) — server-to-client calls inside the hub * [Authorization](/guide/server/authorization) — filter clients by authentication state * [Connection Setup (.NET)](/guide/dotnet-client/connection-setup) — configure the .NET client --- --- url: /guide/streaming/client-to-server.md --- # Client-to-Server Streaming Clients can stream data to the server using `StreamItemToServer` and `StreamCompleteToServer` hub methods. This enables scenarios like uploading large files, sending telemetry, or live data feeds. ## How it works The client sends stream items one at a time using the `StreamItemToServer` hub method, identified by a stream GUID. When the stream is complete, the client sends `StreamCompleteToServer`. ```mermaid sequenceDiagram participant Client participant Server Client->>Server: StreamItemToServer(streamId, item1) Client->>Server: StreamItemToServer(streamId, item2) Client->>Server: StreamItemToServer(streamId, item3) Client->>Server: StreamCompleteToServer(streamId, null) Note over Server: Stream completed ``` ## Server-side handling The `ServerStreamManager` correlates stream items by their GUID and delivers them to the requesting `ServerMethods` class: ```csharp public class UploadMethods : ServerMethods { private readonly ServerStreamManager _streams; public UploadMethods(ServerStreamManager streams) => _streams = streams; public async Task ProcessStream(Guid streamId, CancellationToken ct) { await foreach (var item in _streams.ReadStream(streamId, ct)) { // Process each streamed item await ProcessItem(item); } } } ``` ## Error handling The client can signal an error by passing an error message to `StreamCompleteToServer`: ```ts // TypeScript connection.asSignalRHubConnection().send('StreamCompleteToServer', streamId, 'Upload cancelled'); ``` ```csharp // .NET connection.AsSignalRHubConnection().SendAsync("StreamCompleteToServer", streamId, "Upload cancelled"); ``` ## Next steps * [Server-to-Client Streaming](/guide/streaming/server-to-client) — stream from server to client * [Wire Protocol](/reference/wire-protocol) — stream protocol details --- --- url: /guide/dotnet-client/connection-setup.md --- # Connection Setup `HARRRConnection` wraps ASP.NET Core's `HubConnection` with typed RPC support. Create one using the static factory method. ## Create a connection Use the builder pattern to configure the underlying SignalR connection: ```csharp var connection = HARRRConnection.Create(builder => { builder.WithUrl("https://localhost:5001/apphub"); }); ``` Or wrap an existing `HubConnection`: ```csharp var hubConnection = new HubConnectionBuilder() .WithUrl("https://localhost:5001/apphub") .Build(); var connection = HARRRConnection.Create(hubConnection); ``` ## Connection with authentication Pass a token factory through SignalR's `WithUrl` options: ```csharp var connection = HARRRConnection.Create(builder => { builder.WithUrl("https://localhost:5001/apphub", options => { options.AccessTokenProvider = () => Task.FromResult(GetCurrentToken()); }); }); ``` The token is sent with every SignalARRR request and automatically refreshed when the server challenges an expired token. ## Auto-reconnect ```csharp var connection = HARRRConnection.Create(builder => { builder.WithUrl("https://localhost:5001/apphub"); builder.WithAutomaticReconnect(); }); ``` ## Start and stop ```csharp await connection.StartAsync(); // ... use the connection ... await connection.StopAsync(); await connection.DisposeAsync(); ``` ## Error handling When a server method throws an exception, the client receives a structured error with the exception type and message: ```csharp try { var result = await chat.GetHistory(); } catch (HubException ex) { var error = HARRRError.Parse(ex); Console.WriteLine($"{error.Type}: {error.Message}"); // "System.ArgumentException: Invalid value provided" } ``` `HARRRException` extends `HubException`, so error details always reach the client — no `EnableDetailedErrors` configuration needed. ## Connection events ```csharp connection.Closed += error => { Console.WriteLine($"Connection closed: {error?.Message}"); return Task.CompletedTask; }; connection.Reconnecting += error => { Console.WriteLine($"Reconnecting: {error?.Message}"); return Task.CompletedTask; }; connection.Reconnected += connectionId => { Console.WriteLine($"Reconnected as {connectionId}"); return Task.CompletedTask; }; ``` ## Connection properties | Property | Type | Description | |----------|------|-------------| | `ConnectionId` | `string?` | Current connection ID (null when disconnected) | | `State` | `HubConnectionState` | `Disconnected`, `Connecting`, `Connected`, `Reconnecting` | | `ServerTimeout` | `TimeSpan` | Server keepalive timeout | | `KeepAliveInterval` | `TimeSpan` | Client keepalive ping interval | | `HandshakeTimeout` | `TimeSpan` | Handshake timeout | ## Access the raw HubConnection If you need SignalR features not exposed by `HARRRConnection`: ```csharp var hubConnection = connection.AsSignalRHubConnection(); ``` ## Next steps * [Typed Methods](/guide/dotnet-client/typed-methods) — call server methods through interfaces * [Server-to-Client Handlers](/guide/dotnet-client/server-to-client) — handle server calls * [Streaming](/guide/streaming/server-to-client) — stream data from the server --- --- url: /roadmap/status.md --- # Feature Status Current implementation status across all platforms. See [Client Comparison](/reference/client-comparison) for a detailed side-by-side comparison with code examples. | Feature | .NET Server | .NET Client | TypeScript | Swift | |---------|:-----------:|:-----------:|:----------:|:-----:| | Invoke (call & wait) | Done | Done | Done | Done | | Send (fire & forget) | Done | Done | Done | Done | | Server→Client item streaming | Done | Done | Done | Done | | Client→Server item streaming | Done | Done | Done | Done | | CancellationToken propagation | Done | Done | Done | Done | | Authorization + token refresh | Done | Done | Done | Done | | Compile-time proxies | Done | Done (Source Generator) | N/A | Done (@HubProxy Macro) | | DynamicProxy fallback | Done | Done | N/A | N/A | | File transfer (HTTP stream refs) | Done | Done | Done | Done | | Structured error types | Done | Done | Done | Done | | MessagePack protocol | Done | Done | Done | Done | | Observable/ChannelReader returns | Done | Done | — | N/A | | Auto-reconnect | Done | Done | Done | Done | | Multiple transports (WS/SSE/LP) | Done | Done | Done | Done | | Logging | Done | Done | Done | Done (os\_log) | --- --- url: /guide/getting-started.md --- # Getting Started SignalARRR enables typed bidirectional RPC over ASP.NET Core SignalR. Define shared interfaces, implement them on the server, and call them from .NET or TypeScript clients with full type safety. ## Installation ::: code-group ```bash [Server] dotnet add package Cocoar.SignalARRR.Server ``` ```bash [.NET Client] dotnet add package Cocoar.SignalARRR.Client ``` ```bash [Shared Contracts] dotnet add package Cocoar.SignalARRR.Contracts ``` ```bash [TypeScript / JavaScript] npm install @cocoar/signalarrr ``` ```swift [Swift (Package.swift)] .package(url: "https://github.com/cocoar-dev/Cocoar.SignalARRR.git", from: "4.0.0") // Products: CocoarSignalARRR, CocoarSignalARRRMacros ``` ::: ## 1. Define shared interfaces Create a shared project and reference `Cocoar.SignalARRR.Contracts`. Mark each interface with `[SignalARRRContract]` — the source generator will produce typed proxies at build time. ```csharp using Cocoar.SignalARRR.Common.Attributes; [SignalARRRContract] public interface IChatHub { Task SendMessage(string user, string message); Task> GetHistory(); IAsyncEnumerable StreamMessages(CancellationToken ct); } [SignalARRRContract] public interface IChatClient { void ReceiveMessage(string user, string message); Task GetClientName(); } ``` ## 2. Server setup Register SignalARRR and map the hub in `Program.cs`: ```csharp builder.Services.AddSignalR(); builder.Services.AddSignalARRR(options => options .AddServerMethodsFrom(typeof(Program).Assembly)); app.UseRouting(); app.MapHARRRController("/chathub"); ``` Create an empty hub — the actual method implementations go into `ServerMethods` classes: ```csharp public class ChatHub : HARRR { public ChatHub(IServiceProvider sp) : base(sp) { } } public class ChatMethods : ServerMethods, IChatHub { public Task SendMessage(string user, string message) { // Broadcast to all clients var client = ClientContext.GetTypedMethods(); client.ReceiveMessage(user, message); return Task.CompletedTask; } public Task> GetHistory() => Task.FromResult(new List { "Hello", "World" }); public async IAsyncEnumerable StreamMessages( [EnumeratorCancellation] CancellationToken ct) { while (!ct.IsCancellationRequested) { yield return $"msg-{DateTime.Now:ss}"; await Task.Delay(1000, ct); } } } ``` ## 3. .NET client ```csharp var connection = HARRRConnection.Create(builder => { builder.WithUrl("https://localhost:5001/chathub"); }); await connection.StartAsync(); // Typed calls through the shared interface var chat = connection.GetTypedMethods(); await chat.SendMessage("Alice", "Hello!"); var history = await chat.GetHistory(); // Streaming await foreach (var msg in chat.StreamMessages(cancellationToken)) { Console.WriteLine(msg); } ``` ## 4. TypeScript client ```ts import { HARRRConnection } from '@cocoar/signalarrr'; const connection = HARRRConnection.create(builder => { builder.withUrl('https://localhost:5001/chathub'); builder.withAutomaticReconnect(); }); await connection.start(); // Invoke with return value const history = await connection.invoke('ChatMethods.GetHistory'); // Fire-and-forget await connection.send('ChatMethods.SendMessage', 'Alice', 'Hello!'); // Stream connection.stream('ChatMethods.StreamMessages').subscribe({ next: msg => console.log(msg), complete: () => console.log('done'), }); // Handle server-to-client calls connection.onServerMethod('GetClientName', () => navigator.userAgent); ``` ## 5. Swift client (iOS / macOS) ```swift import CocoarSignalARRR import CocoarSignalARRRMacros @HubProxy protocol IChatHub { func sendMessage(user: String, message: String) async throws func getHistory() async throws -> [String] func streamMessages() async throws -> AsyncThrowingStream } let connection = await HARRRConnection.create { builder in builder.withUrl(url: "https://localhost:5001/chathub") builder.withAutoReconnect() } try await connection.start() // Typed calls through @HubProxy macro let chat = connection.getTypedMethods(IChatHubProxy.self) try await chat.sendMessage(user: "Alice", message: "Hello!") let history = try await chat.getHistory() // Streaming for try await msg in try await chat.streamMessages() { print(msg) } // Handle server-to-client calls await connection.onServerMethod("GetClientName") { _ in AnyCodable(stringLiteral: UIDevice.current.name) } ``` ## Next steps * [Why SignalARRR?](/guide/why-signalarrr) — what problems SignalARRR solves compared to raw SignalR * [Hub Setup](/guide/server/hub-setup) — HARRR base class and configuration * [Server Methods](/guide/server/server-methods) — organizing hub logic across classes * [TypeScript Client](/guide/typescript-client/setup) — complete TypeScript/JavaScript guide * [Swift Client](/guide/swift-client/setup) — complete Swift/iOS/macOS guide --- --- url: /guide/advanced/http-streams.md --- # HTTP Stream References SignalARRR can transparently transfer large files through what looks like a normal RPC call. When a server-to-client method has a `Stream` parameter, SignalARRR automatically routes the data through HTTP instead of the WebSocket — no code changes needed. ## How it works When the server calls a client method with a `Stream` argument: 1. Server stores the stream and generates a download URI 2. Only a `StreamReference` (a small JSON object with the URI) is sent over WebSocket 3. Client detects the `Stream` parameter, fetches the actual data via HTTP GET 4. Client receives the stream and passes it to the handler The developer sees a regular method call with a `Stream` parameter on both sides. The HTTP transfer is invisible. ```mermaid sequenceDiagram participant Server participant WebSocket participant HTTP participant Client Server->>Server: Store stream in ServerPushStreamManager Server->>WebSocket: InvokeServerRequest (StreamReference with URI) WebSocket->>Client: Receive StreamReference Client->>HTTP: GET /hub/download/{guid} HTTP->>Client: Stream data (128KB chunks) Client->>Client: Pass Stream to handler Note over Client: Handler returns result (native SignalR client result) Server-->>Server: InvokeCoreAsync completes with result ``` ## Server side Define an interface with a `Stream` parameter: ```csharp [SignalARRRContract] public interface IFileClient { long ProcessFile(string filename, Stream fileStream); } ``` Call it from a `ServerMethods` class or controller — the stream is automatically routed through HTTP: ```csharp public class FileMethods : ServerMethods { public async Task SendFileToClient(string filename) { var fileStream = File.OpenRead($"/data/{filename}"); var client = ClientContext.GetTypedMethods(); return await client.ProcessFile(filename, fileStream); } } ``` You can also pass an HTTP request body directly as a stream: ```csharp [ApiController] [Route("api/files")] public class FileController : ControllerBase { private readonly ClientManager _clients; public FileController(ClientManager clients) => _clients = clients; [HttpPost("push/{connectionId}")] public async Task PushFile(string connectionId) { var client = _clients.GetTypedMethods(connectionId); var size = await client.ProcessFile("upload.bin", HttpContext.Request.Body); return Ok(new { size }); } } ``` ## .NET client side The client handler receives a regular `Stream` — no awareness of the HTTP transfer needed: ```csharp connection.OnServerRequest("ProcessFile", (string filename, Stream fileStream) => { using (fileStream) { // Process the stream — even multi-GB files work var length = fileStream.Length; return length; } }); ``` ::: tip Streaming vs Buffered By default, the resolved stream is buffered in memory. For large files, use the streaming API directly: ```csharp // .NET — already streaming by default (ReadAsStreamAsync with ResponseHeadersRead) var resolver = new StreamReferenceResolver(streamRef, context); var stream = await resolver.ProcessStreamArgument(); // Stream (not buffered) var bytes = await resolver.ProcessStreamArgumentBuffered(); // byte[] (buffered) ``` ```ts // TypeScript import { resolveStreamReference, resolveStreamReferenceAsStream } from '@cocoar/signalarrr'; const buffer = await resolveStreamReference(ref); // ArrayBuffer (buffered) const stream = await resolveStreamReferenceAsStream(ref); // ReadableStream (not buffered) ``` ```swift // Swift let data = try await StreamReferenceResolver.resolve(ref) // Data (buffered) let bytes = try await StreamReferenceResolver.resolveAsStream(ref) // AsyncBytes (not buffered) ``` ::: ## Client → Server (Stream as argument) Clients can also send `Stream` data TO the server. When a `Stream` (.NET), `Blob`/`ArrayBuffer` (TypeScript), or `Data` (Swift) is passed as an argument to a server method, the client automatically uploads it via HTTP before the call: ::: code-group ```csharp [.NET] var fileStream = File.OpenRead("/data/report.pdf"); await connection.InvokeCoreAsync( new ClientRequestMessage("FileMethods.ProcessUpload", new object[] { fileStream }), ct); ``` ```ts [TypeScript] const data = new Blob([fileContent], { type: 'application/octet-stream' }); await connection.invoke('FileMethods.ProcessUpload', data); ``` ```swift [Swift] let data = try Data(contentsOf: URL(fileURLWithPath: "/data/report.pdf")) try await connection.invoke("FileMethods.ProcessUpload", arguments: data) ``` ::: The server method receives a regular `Stream`: ```csharp public class FileMethods : ServerMethods { public string ProcessUpload(Stream data) { using var reader = new StreamReader(data); return reader.ReadToEnd(); } } ``` ::: warning One Stream per Client A `Stream` can only be read once. Do NOT pass the same `Stream` instance to multiple clients — only the first client would receive data, the others would get empty responses. SignalARRR throws an `InvalidOperationException` if you try. To send the same file to multiple clients, open a separate `Stream` for each: ```csharp foreach (var client in clients.GetHARRRClients()) { // Each client gets its own FileStream — do NOT reuse the same stream using var fileStream = File.OpenRead("/data/movie.mp4"); var methods = client.GetTypedMethods(); methods.ProcessFile("movie.mp4", fileStream); } ``` ::: ## Why not just use WebSocket? WebSocket connections have practical limits for large binary transfers: | Concern | WebSocket | HTTP Stream Reference | |---------|-----------|----------------------| | Multi-GB files | Message buffer limits, memory pressure | Streamed in 128KB chunks | | Backpressure | Blocks the hub connection for all clients | Independent HTTP connection | | Timeout | Hub timeout applies | Separate HTTP timeout | | Concurrent transfers | Shares the single WebSocket | Parallel HTTP connections | ## Under the hood ### StreamReference The only thing sent over WebSocket is a small reference object: ```json { "Uri": "http://server/apphub/download/550e8400-e29b-41d4-a716-446655440000" } ``` ### ServerPushStreamManager Manages both download streams (server → client) and upload slots (client → server). Registered as a singleton. Streams are automatically disposed after transfer or after a 10-minute cleanup timeout. ## Endpoints `MapHARRRController()` registers the hub and two HTTP endpoints: ``` /apphub ← SignalR hub /apphub/download/{id} ← HTTP stream downloads (server → client) /apphub/upload/{id} ← HTTP stream uploads (client → server) ``` ## Limitations * **`Stream` / `Blob` / `Data` types only** — other large types (e.g., `byte[]`) are not automatically intercepted * **In-memory storage** — download streams are held in memory on the server until the client fetches them (10 minute timeout) * **One Stream per client** — the same `Stream` instance cannot be sent to multiple clients (see warning above) * **Download URL uses GUID as access token** — secure over HTTPS, but no user-level authentication on the endpoint ## Next steps * [Item Streaming: Server-to-Client](/guide/streaming/server-to-client) — IAsyncEnumerable patterns (different from file transfer) * [Client Manager](/guide/server/client-manager) — push files from controllers * [Wire Protocol](/reference/wire-protocol) — protocol details --- --- url: /roadmap/http-streams.md --- # HTTP Stream References File transfer via `Stream` parameters works in both directions for all three clients, with both buffered and streaming download options. ## Streaming Download (Not Buffered) **Status:** Implemented All three clients now offer both buffered and streaming download options: | Client | Buffered (default) | Streaming | |--------|-------------------|-----------| | .NET | `ProcessStreamArgument()` → `Stream` | Same — already streaming via `ResponseHeadersRead` | | .NET | `ProcessStreamArgumentBuffered()` → `byte[]` | — | | TypeScript | `resolveStreamReference()` → `ArrayBuffer` | `resolveStreamReferenceAsStream()` → `ReadableStream` | | Swift | `StreamReferenceResolver.resolve()` → `Data` | `StreamReferenceResolver.resolveAsStream()` → `URLSession.AsyncBytes` | The default `_prepareArgs` in all clients uses the buffered variant for backward compatibility. Developers can use the streaming variants directly when handling large files. **.NET** additionally uses `HttpCompletionOption.ResponseHeadersRead` so even the default `Stream` return doesn't wait for the full response body before returning. ## Extensible Reference Type Registry **Status:** Future consideration Currently, the reference type system is hard-coded for `Stream` → `StreamReference`. If more types need the "send reference, resolve on the other side" pattern, the system should be made extensible with an `IReferenceTypeHandler` interface and a registry. Build this when a second use case emerges — not before. --- --- url: /guide/server/hub-setup.md --- # Hub Setup The `HARRR` class is the SignalARRR hub base class. It extends ASP.NET Core's `Hub` with typed method dispatch, authorization, client context tracking, and streaming support. ## Create a hub Every SignalARRR application needs at least one hub. The hub itself can be empty — actual method implementations go into [ServerMethods](/guide/server/server-methods) classes. ```csharp public class AppHub : HARRR { public AppHub(IServiceProvider sp) : base(sp) { } } ``` ## Register and map In `Program.cs`, register SignalARRR services and map the hub endpoint: ```csharp builder.Services.AddSignalR(); builder.Services.AddSignalARRR(options => options .AddServerMethodsFrom(typeof(Program).Assembly)); var app = builder.Build(); app.UseRouting(); app.MapHARRRController("/apphub"); ``` ### MessagePack support For better performance with many clients, add MessagePack alongside JSON: ```csharp builder.Services.AddSignalR() .AddMessagePackProtocol(); // clients can choose JSON or MessagePack ``` Both protocols run simultaneously — JSON clients and MessagePack clients connect to the same hub. `AddServerMethodsFrom()` scans the assembly for all `ServerMethods` classes and registers them in DI. `MapHARRRController()` maps the hub at the specified path and also registers a download endpoint at `{path}/download/{id}` for file stream references. ## Multiple hubs You can have multiple hubs in the same application: ```csharp public class ChatHub : HARRR { public ChatHub(IServiceProvider sp) : base(sp) { } } public class AdminHub : HARRR { public AdminHub(IServiceProvider sp) : base(sp) { } } ``` ```csharp app.MapHARRRController("/chathub"); app.MapHARRRController("/adminhub"); ``` ServerMethods classes are scoped to a specific hub type via the generic parameter: ```csharp public class ChatMethods : ServerMethods, IChatHub { ... } // only on ChatHub public class AdminMethods : ServerMethods, IAdminHub { ... } // only on AdminHub ``` ## Connection options `MapHARRRController` accepts SignalR's `HttpConnectionDispatcherOptions` for configuring transports, buffer sizes, and authorization: ```csharp app.MapHARRRController("/apphub", options => { options.Transports = HttpTransportType.WebSockets; options.ApplicationMaxBufferSize = 64 * 1024; }); ``` ## Hub lifecycle `HARRR` provides the standard SignalR lifecycle hooks. Override them to run logic when clients connect or disconnect: ```csharp public class AppHub : HARRR { public AppHub(IServiceProvider sp) : base(sp) { } public override async Task OnConnectedAsync() { await base.OnConnectedAsync(); // registers client in ClientManager Logger?.LogInformation("Client {Id} connected", Context.ConnectionId); } public override async Task OnDisconnectedAsync(Exception? exception) { Logger?.LogInformation("Client {Id} disconnected", Context.ConnectionId); await base.OnDisconnectedAsync(exception); // unregisters client } } ``` ::: warning Always call `base.OnConnectedAsync()` and `base.OnDisconnectedAsync()` — they manage the client registration in [ClientManager](/guide/server/client-manager). ::: ## Hub properties | Property | Type | Description | |----------|------|-------------| | `ServiceProvider` | `IServiceProvider` | DI container for the current request | | `Logger` | `ILogger?` | Logger instance (resolved from DI) | | `ClientContext` | `ClientContext` | Enhanced context for the calling client | | `Context` | `HubCallerContext` | Standard SignalR caller context | | `Clients` | `IHubCallerClients` | Access to client connections | | `Groups` | `IGroupManager` | Group management | ## Next steps * [Server Methods](/guide/server/server-methods) — split hub logic across classes * [Authorization](/guide/server/authorization) — protect methods with `[Authorize]` * [Client Manager](/guide/server/client-manager) — call clients from outside the hub --- --- url: /roadmap/messagepack.md --- # MessagePack Protocol **Status:** Fully implemented across all clients (.NET, TypeScript, Swift). ## Overview SignalARRR supports MessagePack alongside JSON. Both protocols run simultaneously — different clients can use different protocols on the same hub. **Architecture:** * `IProtocolSerializer` abstraction in `Cocoar.SignalARRR.Common.Serialization` * `JsonProtocolSerializer` handles both `JsonElement` (JSON) and plain .NET objects (MessagePack) via its fallback * All `JsonElement`/`JsonSerializer` direct usage removed from `MessageHandler` and `ServerStreamManager` ## Server Setup ```csharp builder.Services.AddSignalR() .AddMessagePackProtocol(); // Add this line — that's it builder.Services.AddSignalARRR(options => options .AddServerMethodsFrom(typeof(Program).Assembly)); ``` ## .NET Client ```bash dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack ``` ```csharp var connection = HARRRConnection.Create(builder => { builder.WithUrl("https://server/hub"); builder.AddMessagePackProtocol(); }); ``` ## TypeScript Client ```bash npm install @microsoft/signalr-protocol-msgpack ``` ```ts import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack'; const connection = HARRRConnection.create(builder => { builder.withUrl('https://server/hub'); builder.withHubProtocol(new MessagePackHubProtocol()); }); ``` ## Swift Client No external dependencies — MessagePack is implemented natively in the Swift client. ```swift let connection = await HARRRConnection.create( url: "https://server/hub", hubProtocol: .messagepack ) ``` Or using `SignalRWebSocketClient` directly: ```swift let client = SignalRWebSocketClient( url: "https://server/hub", hubProtocol: .messagepack ) ``` ## Tests * 5 .NET MessagePack integration tests (invoke, send, echo, guid, multi-param) * 5 TypeScript MessagePack integration tests (same scenarios) * 5 Swift MessagePack integration tests (invoke, guid, send, echo, streaming with multiple int params) * All running alongside JSON tests on the same server instance --- --- url: /guide/migration/from-v2.md --- # Migration from v2.x SignalARRR v4 is a major release with significant architectural changes. This guide covers all breaking changes and the steps to upgrade. ## Target framework **v2.x:** `netstandard2.0` **v4:** `net10.0` (except SourceGenerator which targets `netstandard2.0` per Roslyn requirements) Update your project target frameworks: ```xml net10.0 ``` ## Proxy generation **v2.x:** `ImpromptuInterface` for runtime proxy creation **v4:** Roslyn source generator + optional `DispatchProxy` fallback ### Steps 1. Remove `ImpromptuInterface` package references 2. Add `Cocoar.SignalARRR.Contracts` to shared interface projects 3. Mark interfaces with `[SignalARRRContract]`: ```csharp // Before (v2.x) — no attribute needed public interface IChatHub { ... } // After (v4) — attribute required [SignalARRRContract] public interface IChatHub { ... } ``` 4. If you need runtime proxy generation (plugin scenarios), add `Cocoar.SignalARRR.DynamicProxy` ## Authentication **v2.x:** Custom `IAuthenticator` interface with `TryAuthenticate()` and `SetAuthData()` **v4:** Standard ASP.NET Core `[Authorize]` attributes and authentication handlers ### Steps 1. Remove `IAuthenticator` implementations 2. Configure ASP.NET Core authentication: ```csharp builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { ... }); ``` 3. Use `[Authorize]` on methods and classes instead of custom auth logic ## Hub-level authorization inheritance **v2.x:** Hub-level `[Authorize]` was disabled — ServerMethods didn't inherit it **v4:** Hub-level `[Authorize]` is inherited by all ServerMethods classes If you had a hub with `[Authorize]` but relied on individual ServerMethods classes being unauthenticated, add `[AllowAnonymous]` to those classes: ```csharp [Authorize] public class SecureHub : HARRR { ... } [AllowAnonymous] // opt out of hub-level auth public class PublicMethods : ServerMethods { ... } ``` ## HTTP proxy pass-through **v2.x:** Available for large response streaming **v4:** Removed The `UseHttpResponse()` option and all related code has been removed. For large data transfer, use [HTTP Stream References](/guide/advanced/http-streams) instead — `Stream` parameters are automatically routed through HTTP. ## Removed APIs | Removed | Replacement | |---------|-------------| | `ImpromptuInterface` | `[SignalARRRContract]` + source generator | | `IAuthenticator` | ASP.NET Core `[Authorize]` | | `RegisterMethods()` | `RegisterInterface()` or typed proxies | | `TryAuthenticate()` / `SetAuthData()` | `ClientContext.User` / `[Authorize]` | | `netstandard2.0` polyfill packages | Native `net10.0` APIs | | Non-generic `Invoke(Type, ...)` overloads | Generic `Invoke(...)` | ## New packages | Package | Purpose | |---------|---------| | `Cocoar.SignalARRR.Contracts` | `[SignalARRRContract]` + source generator — add to shared projects | | `Cocoar.SignalARRR.DynamicProxy` | Optional runtime proxy fallback | ## New features * **Source generator** — compile-time proxies, AOT-friendly * **CancellationToken propagation** — server can cancel client operations * **Server stream requests** — server can request `IAsyncEnumerable` from clients * **`StreamItemToServer` / `StreamCompleteToServer`** — client-to-server streaming * **`ClientManager` typed extensions** — `GetTypedMethods(connectionId)` for server-to-client RPC outside hub context * **Authorization tests** — comprehensive test coverage for auth scenarios ## Next steps * [Getting Started](/guide/getting-started) — fresh start with v4 * [Proxy Generation](/guide/advanced/proxy-generation) — source generator details * [Authorization](/guide/server/authorization) — new authorization system --- --- url: /reference/packages.md --- # Packages SignalARRR is distributed as multiple NuGet packages and one npm package. Choose the packages that match your project's role. ## .NET Packages | Package | Target | Purpose | |---------|--------|---------| | `Cocoar.SignalARRR.Contracts` | net10.0 | `[SignalARRRContract]` attribute + Roslyn source generator. Reference from shared interface projects. | | `Cocoar.SignalARRR.Server` | net10.0 | Server-side: `HARRR` hub, `ServerMethods`, authorization, `ClientManager`, streaming. | | `Cocoar.SignalARRR.Client` | net10.0 | Client-side: `HARRRConnection`, typed proxies, server-to-client handlers. | | `Cocoar.SignalARRR.DynamicProxy` | net10.0 | Optional runtime proxy fallback via `DispatchProxy`. For plugin/dynamic scenarios. Not AOT-compatible. | ### Internal packages These packages are referenced transitively — you normally don't need to reference them directly: | Package | Purpose | |---------|---------| | `Cocoar.SignalARRR.Common` | Shared types, wire protocol constants, message models | | `Cocoar.SignalARRR.ProxyGenerator` | Base classes for proxy creation (`ProxyCreator`, `ProxyCreatorHelper`) | | `Cocoar.SignalARRR.SourceGenerator` | Roslyn incremental source generator (bundled in Contracts) | ## npm Package | Package | Version | Purpose | |---------|---------|---------| | `@cocoar/signalarrr` | 4.0.0 | TypeScript/JavaScript client: `HARRRConnection`, `invoke`, `send`, `stream`, `onServerMethod` | ### Peer dependency The npm package requires `@microsoft/signalr` ^10.0.0: ```bash npm install @cocoar/signalarrr @microsoft/signalr ``` ## Swift Package | Package | Purpose | |---------|---------| | `CocoarSignalARRR` | Core Swift client: `HARRRConnection`, `invoke`, `send`, `stream`, `onServerMethod`, stream references | | `CocoarSignalARRRMacros` | `@HubProxy` macro for compile-time proxy generation from Swift protocols | ### Dependencies * `signalr-client-swift` (1.0.0-preview.1+) — Microsoft's SignalR Swift client * `swift-syntax` (510.0.0+) — for macro code generation ### Swift Package Manager ```swift dependencies: [ .package(url: "https://github.com/cocoar-dev/Cocoar.SignalARRR.git", from: "4.0.0"), ], targets: [ .target( name: "MyApp", dependencies: [ .product(name: "CocoarSignalARRR", package: "Cocoar.SignalARRR"), .product(name: "CocoarSignalARRRMacros", package: "Cocoar.SignalARRR"), ] ), ] ``` **Platforms:** iOS 14+, macOS 11+, tvOS 14+, watchOS 7+ ## Typical project setup ### Shared interfaces (class library) ```xml net10.0 ``` ### Server (ASP.NET Core) ```xml ``` ### .NET Client (Console / WPF / etc.) ```xml ``` ### TypeScript Client ```json { "dependencies": { "@cocoar/signalarrr": "^4.0.0", "@microsoft/signalr": "^10.0.0" } } ``` ## Dependency graph ```mermaid graph TD Contracts["Cocoar.SignalARRR.Contracts"] Server["Cocoar.SignalARRR.Server"] Client["Cocoar.SignalARRR.Client"] Common["Cocoar.SignalARRR.Common"] ProxyGen["Cocoar.SignalARRR.ProxyGenerator"] SourceGen["Cocoar.SignalARRR.SourceGenerator"] DynProxy["Cocoar.SignalARRR.DynamicProxy"] npm["@cocoar/signalarrr"] signalr["@microsoft/signalr"] Contracts --> ProxyGen Contracts --> SourceGen Server --> Common Client --> Common Client --> ProxyGen Server --> ProxyGen DynProxy --> ProxyGen npm --> signalr ``` ## Next steps * [Getting Started](/guide/getting-started) — install and set up * [API Overview](/reference/api) — public API surface * [Proxy Generation](/guide/advanced/proxy-generation) — how the source generator works --- --- url: /guide/advanced/proxy-generation.md --- # Proxy Generation SignalARRR uses a Roslyn source generator to produce typed proxy classes at compile time. This enables zero-reflection RPC calls and is AOT-compatible. ## How it works 1. Mark an interface with `[SignalARRRContract]` 2. The source generator finds it during build 3. A proxy class is generated that implements the interface 4. A module initializer registers the proxy in `ProxyCreator` 5. `GetTypedMethods()` returns the generated proxy ## Setup Reference `Cocoar.SignalARRR.Contracts` in your shared interface project: ```xml ``` This package bundles: * The `[SignalARRRContract]` attribute * The Roslyn source generator (runs at build time) * The `ProxyCreator` and `ProxyCreatorHelper` base classes ## Mark interfaces ```csharp [SignalARRRContract] public interface IChatHub { Task SendMessage(string user, string message); Task> GetHistory(); IAsyncEnumerable StreamMessages(CancellationToken ct); } ``` ## Generated code For `IChatHub`, the generator produces: **Proxy class** (`IChatHub.SignalARRRProxy.g.cs`): ```csharp internal sealed class ChatHubProxy : IChatHub { private readonly ProxyCreatorHelper _helper; private const string Prefix = "MyNamespace.IChatHub"; public ChatHubProxy(ProxyCreatorHelper helper) => _helper = helper; public Task SendMessage(string user, string message) => _helper.SendAsync(Prefix + "|SendMessage", new object[] { user, message }, Array.Empty()); public Task> GetHistory() => _helper.InvokeAsync>(Prefix + "|GetHistory", Array.Empty(), Array.Empty()); public IAsyncEnumerable StreamMessages(CancellationToken ct) => _helper.StreamAsync(Prefix + "|StreamMessages", Array.Empty(), Array.Empty()); } ``` **Registration** (`SignalARRRProxyRegistration.g.cs`): ```csharp internal static class SignalARRRProxyRegistration { [ModuleInitializer] internal static void Initialize() { ProxyCreator.RegisterFactory( helper => new ChatHubProxy(helper)); } } ``` The module initializer runs when the assembly loads, making the proxy available immediately. ## Proxy naming The generator strips the leading `I` from interface names: | Interface | Proxy class | |-----------|-------------| | `IChatHub` | `ChatHubProxy` | | `IAdminService` | `AdminServiceProxy` | | `IMyContract` | `MyContractProxy` | ## Return type classification The generator classifies return types to determine the correct proxy method: | Return type | Proxy call | Protocol | |-------------|------------|----------| | `void` | `Send()` | `SendMessage` | | `Task` | `SendAsync()` | `SendMessage` | | `T` (sync) | `Invoke()` | `InvokeMessageResult` | | `Task` | `InvokeAsync()` | `InvokeMessageResult` | | `IAsyncEnumerable` | `StreamAsync()` | `StreamMessage` | | `IObservable` | `StreamAsync()` → `ToObservable()` | `StreamMessage` | | `ChannelReader` | `StreamAsync()` → `ToChannelReader()` | `StreamMessage` | ## Multi-assembly support Each assembly with `[SignalARRRContract]` interfaces generates its own module initializer. Proxies from all referenced assemblies are available through `ProxyCreator`: ``` SharedContracts.dll → registers IChatHub, IAdminHub PluginA.dll → registers IPluginAContract PluginB.dll → registers IPluginBContract ``` ## DynamicProxy fallback For scenarios where compile-time generation isn't possible (e.g., plugin systems loading interfaces at runtime), add the `Cocoar.SignalARRR.DynamicProxy` package: ```xml ``` This registers a fallback factory in `ProxyCreator` that uses `DispatchProxy` for runtime proxy creation. ::: warning `DynamicProxy` requires `System.Reflection.Emit` and is **not AOT-compatible**. Use the source generator for AOT scenarios. ::: ## ProxyCreator API | Method | Description | |--------|-------------| | `RegisterFactory(factory)` | Register a compiled proxy factory | | `RegisterFallbackFactory(factory)` | Register a runtime fallback (e.g., DispatchProxy) | | `HasFactory()` | Check if a proxy factory exists for `T` | | `CreateInstanceFromInterface(helper)` | Create a proxy instance | ## Next steps * [Typed Methods](/guide/dotnet-client/typed-methods) — use generated proxies on the client * [Getting Started](/guide/getting-started) — full setup walkthrough * [Packages](/reference/packages) — which packages to reference --- --- url: /guide/typescript-client/server-methods.md --- # Server Method Handlers The server can call methods on the TypeScript client. Use `onServerMethod()` to register handlers that respond to these calls. ## Register a handler ```ts connection.onServerMethod('ReceiveMessage', (user: string, message: string) => { console.log(`${user}: ${message}`); }); ``` The handler is called when the server invokes `InvokeServerRequest` or `InvokeServerMessage` with the matching method name. ## Return values If the server expects a return value (`InvokeServerRequest`), return it from the handler: ```ts connection.onServerMethod('GetClientName', () => { return navigator.userAgent; }); connection.onServerMethod('GetClientTime', () => { return new Date().toISOString(); }); ``` The return value is sent back to the server automatically via SignalR's native client results feature. ## Async handlers Handlers can be async: ```ts connection.onServerMethod('FetchData', async (url: string) => { const response = await fetch(url); return await response.json(); }); ``` ## Chaining `onServerMethod()` returns `this`, so you can chain multiple registrations: ```ts const connection = HARRRConnection.create(builder => { builder.withUrl('https://localhost:5001/apphub'); }); connection .onServerMethod('ReceiveMessage', (user, msg) => console.log(`${user}: ${msg}`)) .onServerMethod('GetClientName', () => navigator.userAgent) .onServerMethod('Ping', () => 'pong'); await connection.start(); ``` ## Cancellation support When the server passes a `CancellationToken` to a client method, SignalARRR converts it to an `AbortSignal` in the TypeScript handler: ```ts connection.onServerMethod('LongRunningTask', async (data: string, signal: AbortSignal) => { for (let i = 0; i < 100; i++) { if (signal.aborted) { throw new Error('Operation cancelled'); } await processChunk(data, i); } return 'done'; }); ``` The server can cancel the operation by calling `CancelTokenFromServer`. See [Cancellation Propagation](/guide/advanced/cancellation) for details. ## How it works The client registers handlers for four internal SignalR methods: | Internal Method | Behavior | |----------------|----------| | `InvokeServerRequest` | Calls handler, returns result via native SignalR client results | | `InvokeServerMessage` | Calls handler (fire-and-forget, no reply) | | `ChallengeAuthentication` | Automatic — calls token factory, sends token back | | `CancelTokenFromServer` | Triggers `AbortController.abort()` for the matching cancellation ID | Responses are transported automatically by SignalR's native client results feature -- no separate reply message is needed. ## Next steps * [Cancellation Propagation](/guide/advanced/cancellation) — server-initiated cancellation with AbortSignal * [Setup & Usage](/guide/typescript-client/setup) — TypeScript client basics * [Server Methods](/guide/server/server-methods) — how the server calls client methods --- --- url: /guide/server/server-methods.md --- # Server Methods `ServerMethods` classes let you organize hub logic into separate, focused classes instead of putting everything into a single hub. They are auto-discovered, support full dependency injection, and implement shared contract interfaces. ## Basic usage Create a class that extends `ServerMethods` where `T` is your hub type. Implement a shared contract interface: ```csharp [SignalARRRContract] public interface IChatHub { Task SendMessage(string user, string message); Task> GetHistory(); } public class ChatMethods : ServerMethods, IChatHub { public Task SendMessage(string user, string message) { var client = ClientContext.GetTypedMethods(); client.ReceiveMessage(user, message); return Task.CompletedTask; } public Task> GetHistory() => Task.FromResult(new List { "Hello", "World" }); } ``` No registration needed — `AddSignalARRR()` discovers all `ServerMethods` classes in the scanned assemblies. ## Multiple classes per hub Split your hub into domain-specific classes: ```csharp public class ChatMethods : ServerMethods, IChatHub { ... } public class UserMethods : ServerMethods, IUserHub { ... } public class AdminMethods : ServerMethods, IAdminHub { ... } ``` All three classes serve methods on the same `AppHub` endpoint. ## Method naming Clients call methods using the pattern `ClassName.MethodName`: ```csharp // .NET client var chat = connection.GetTypedMethods(); // uses interface name mapping await chat.SendMessage("Alice", "Hello!"); ``` ```ts // TypeScript client await connection.invoke('ChatMethods.SendMessage', 'Alice', 'Hello!'); ``` When using typed proxies on the .NET client, the naming is handled automatically through the interface mapping. ## Available properties Every `ServerMethods` class has these properties auto-injected at invocation time: | Property | Type | Description | |----------|------|-------------| | `ClientContext` | `ClientContext` | Current client's context (ID, user, attributes) | | `Context` | `HubCallerContext` | Standard SignalR caller context | | `Clients` | `IHubCallerClients` | Send messages to other clients | | `Groups` | `IGroupManager` | Add/remove clients from groups | | `Logger` | `ILogger` | Logger instance | ## Dependency injection ServerMethods classes are resolved from DI as transient services. Inject dependencies through the constructor: ```csharp public class ChatMethods : ServerMethods, IChatHub { private readonly IChatRepository _repo; private readonly ILogger _logger; public ChatMethods(IChatRepository repo, ILogger logger) { _repo = repo; _logger = logger; } public async Task> GetHistory() { _logger.LogInformation("Client {Id} requested history", ClientContext.Id); return await _repo.GetRecentMessages(); } } ``` ## Parameter injection with \[FromServices] Individual method parameters can be injected from DI using `[FromServices]`: ```csharp public async Task ProcessData( string input, [FromServices] IDataProcessor processor) { await processor.Process(input); } ``` ## Server-to-client calls Inside a `ServerMethods` class, use `ClientContext.GetTypedMethods()` to call back the current client: ```csharp public async Task Greet() { var client = ClientContext.GetTypedMethods(); string name = await client.GetClientName(); // awaits client response return $"Hello, {name}!"; } ``` To call other clients, inject `ClientManager` and use the typed extension methods: ```csharp public class ChatMethods : ServerMethods, IChatHub { private readonly ClientManager _clients; public ChatMethods(ClientManager clients) => _clients = clients; public void BroadcastMessage(string message) { // GetTypedMethodsForHub returns (ClientContext, T) tuples foreach (var (ctx, methods) in _clients.GetTypedMethodsForHub()) { methods.ReceiveMessage("System", message); } } public async Task AskClient(string connectionId) { // GetTypedMethods extension combines GetClientById + GetTypedMethods var methods = _clients.GetTypedMethods(connectionId); return await methods.GetClientName(); } } ``` See [Client Manager](/guide/server/client-manager) for more details. ## Custom method names Use `[MessageName]` to override the default method name: ```csharp [MessageName("CustomName")] public Task MyMethod() { ... } ``` Clients call this as `ClassName.CustomName` instead of `ClassName.MyMethod`. ## Next steps * [Authorization](/guide/server/authorization) — protect methods with `[Authorize]` * [Client Manager](/guide/server/client-manager) — call clients from controllers and services * [Streaming](/guide/streaming/server-to-client) — return streams from server methods --- --- url: /guide/dotnet-client/server-to-client.md --- # Server-to-Client Handlers The server can call methods on the client and optionally await a response. Register handlers on the client to respond to these calls. ## Typed handlers via interfaces The cleanest approach is to implement a shared contract interface and register it: ```csharp [SignalARRRContract] public interface IChatClient { void ReceiveMessage(string user, string message); Task GetClientName(); } ``` The server calls these methods through `ClientContext.GetTypedMethods()`, and the client handles them through registered handlers. ## Register ad-hoc handlers Use `OnServerRequest()` to register handlers by method name: ```csharp // Handle a void method (fire-and-forget from server) connection.OnServerRequest("ReceiveMessage", (string user, string message) => { Console.WriteLine($"{user}: {message}"); return null; }); // Handle a method with a return value (server awaits response) connection.OnServerRequest("GetClientName", name => { return Environment.MachineName; }); ``` ## Typed overloads `OnServerRequest` supports up to 4 typed parameters: ```csharp // 1 parameter connection.OnServerRequest("Echo", input => input.ToUpper()); // 2 parameters connection.OnServerRequest("Repeat", (text, count) => string.Concat(Enumerable.Repeat(text, count))); // 3 parameters connection.OnServerRequest("Compare", (a, b, ignoreCase) => string.Equals(a, b, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); ``` ## Extension method overloads The `HARRRConnectionExtensions` class provides `On()` overloads for up to 16 parameters: ```csharp connection.On("ReceiveMessage", (user, message) => { Console.WriteLine($"{user}: {message}"); }); ``` ## How it works When the server calls a client method: 1. Server sends `InvokeServerRequest` (expects reply) or `InvokeServerMessage` (fire-and-forget) 2. Client receives the message and dispatches to the registered handler 3. If `InvokeServerRequest`, the handler's return value is sent back automatically via SignalR's native client results (no separate reply message needed) The `ChallengeAuthentication` message is handled automatically — the client's token factory is called and the token is sent back without developer intervention. ## Next steps * [Server Methods](/guide/server/server-methods) — how the server calls client methods * [Cancellation Propagation](/guide/advanced/cancellation) — server-initiated cancellation * [TypeScript Client](/guide/typescript-client/setup) — same pattern in TypeScript --- --- url: /guide/streaming/server-to-client.md --- # Server-to-Client Streaming SignalARRR supports streaming results from server methods to clients using `IAsyncEnumerable`, `IObservable`, or `ChannelReader`. ## IAsyncEnumerable (recommended) The simplest streaming pattern — use `yield return` in an async iterator: ```csharp [SignalARRRContract] public interface IDataHub { IAsyncEnumerable StreamMessages(CancellationToken ct); IAsyncEnumerable StreamPrices(string symbol, CancellationToken ct); } ``` ```csharp public class DataMethods : ServerMethods, IDataHub { public async IAsyncEnumerable StreamMessages( [EnumeratorCancellation] CancellationToken ct) { var i = 0; while (!ct.IsCancellationRequested) { yield return $"Message {i++}"; await Task.Delay(1000, ct); } } public async IAsyncEnumerable StreamPrices( string symbol, [EnumeratorCancellation] CancellationToken ct) { while (!ct.IsCancellationRequested) { yield return await _stockService.GetPrice(symbol); await Task.Delay(500, ct); } } } ``` ## Consume on the .NET client ```csharp var data = connection.GetTypedMethods(); await foreach (var msg in data.StreamMessages(cancellationToken)) { Console.WriteLine(msg); } ``` ## Consume in TypeScript ```ts connection.stream('DataMethods.StreamMessages').subscribe({ next: msg => console.log(msg), error: err => console.error('Stream error:', err), complete: () => console.log('Stream ended'), }); ``` ## IObservable Use Rx.NET observables for reactive streaming: ```csharp public IObservable ObservePrices(string symbol) { return Observable.Create(async (observer, ct) => { while (!ct.IsCancellationRequested) { var price = await _stockService.GetPrice(symbol); observer.OnNext(price); await Task.Delay(500, ct); } }); } ``` ## ChannelReader Use channels for producer/consumer patterns: ```csharp public ChannelReader StreamLogs() { var channel = Channel.CreateUnbounded(); _ = Task.Run(async () => { await foreach (var entry in _logService.WatchLogs()) { await channel.Writer.WriteAsync(entry); } channel.Writer.Complete(); }); return channel.Reader; } ``` ## Cancellation All streaming patterns support cancellation. The client disconnecting or explicitly cancelling the stream triggers the `CancellationToken`: ```csharp // .NET client — cancel after 10 seconds using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await foreach (var msg in data.StreamMessages(cts.Token)) { Console.WriteLine(msg); } ``` ```ts // TypeScript — cancel by disposing the subscription const sub = connection.stream('DataMethods.StreamMessages').subscribe({ next: msg => console.log(msg), }); // Later... sub.dispose(); ``` ## Next steps * [Client-to-Server Streaming](/guide/streaming/client-to-server) — stream data to the server * [Cancellation Propagation](/guide/advanced/cancellation) — remote cancellation * [Typed Methods](/guide/dotnet-client/typed-methods) — how return types map to protocol --- --- url: /guide/swift-client/setup.md --- # Swift Client Setup The `CocoarSignalARRR` Swift package provides a native client for iOS, macOS, tvOS, and watchOS. It is a full SignalR client built from scratch — no dependency on Microsoft's `signalr-client-swift`. ::: info Requirements Swift 5.10+, iOS 14+ / macOS 11+ / tvOS 14+ / watchOS 7+. No external dependencies beyond Swift standard library and Foundation. ::: ## Installation Add the package to your `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/cocoar-dev/Cocoar.SignalARRR.git", from: "4.0.0"), ], targets: [ .target( name: "MyApp", dependencies: [ .product(name: "CocoarSignalARRR", package: "Cocoar.SignalARRR"), .product(name: "CocoarSignalARRRMacros", package: "Cocoar.SignalARRR"), ] ), ] ``` ## Create a connection ```swift import CocoarSignalARRR let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub" ) ``` ## Authentication ```swift let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub", accessTokenFactory: { await getAuthToken() } ) ``` Token challenges are handled automatically — when the server detects an expired token, the client calls `accessTokenFactory` to get a fresh token. ## All options ```swift let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub", hubProtocol: .json, // or .messagepack accessTokenFactory: { await getAuthToken() }, serverTimeout: 30, keepAliveInterval: 15, handshakeTimeout: 15, reconnectPolicy: .default, // immediate, 2s, 10s, 30s — then give up allowedTransports: [.webSockets, .serverSentEvents, .longPolling], logLevel: .info ) ``` ## Start and stop ```swift try await connection.start() // ... use the connection ... await connection.stop() ``` ## Invoke (call with return value) ```swift let history: [String] = try await connection.invoke("ChatMethods.GetHistory") let user: User = try await connection.invoke("UserMethods.GetUser", arguments: userId) ``` ## Send (fire-and-forget) ```swift try await connection.send("ChatMethods.SendMessage", arguments: "Alice", "Hello!") ``` ## Stream ```swift let stream: AsyncThrowingStream = try await connection.stream( "ChatMethods.StreamMessages" ) for try await msg in stream { print(msg) } ``` ## Connection events ```swift await connection.onClosed { error in print("Connection closed: \(error?.localizedDescription ?? "clean")") } await connection.onReconnecting { error in print("Reconnecting: \(error?.localizedDescription ?? "")") } await connection.onReconnected { print("Reconnected") } ``` ## Connection properties | Property | Type | Description | |----------|------|-------------| | `connectionId` | `String?` | Current connection ID | | `state` | `HubConnectionState` | Connection state (async) | | `serverTimeoutInterval` | `TimeInterval` | Server timeout | | `keepAliveIntervalValue` | `TimeInterval` | Keepalive interval | | `handshakeTimeoutValue` | `TimeInterval` | Handshake timeout | ## MessagePack protocol Use MessagePack instead of JSON for better performance and smaller payloads: ```swift let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub", hubProtocol: .messagepack ) ``` No additional dependencies required — MessagePack is implemented natively in the client. ::: info Server setup The server needs `.AddMessagePackProtocol()`. See [MessagePack](/roadmap/messagepack) for details. ::: ## Reconnection policy ```swift // Default: immediate retry, then 2s, 10s, 30s — then give up let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub", reconnectPolicy: .default ) // Custom delays let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub", reconnectPolicy: ReconnectPolicy(retryDelays: [0, 1, 5, 10, 30]) ) // Disable reconnection let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub", reconnectPolicy: .disabled ) ``` ## Transport selection The client tries transports in preference order and connects via the first one the server supports: ```swift // Default: WebSockets → SSE → Long Polling let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub", allowedTransports: [.webSockets, .serverSentEvents, .longPolling] ) // Force WebSockets only let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub", allowedTransports: [.webSockets] ) ``` ## Logging ```swift let connection = await HARRRConnection.create( url: "https://localhost:5001/apphub", logLevel: .debug // .debug, .info, .warning, .error, .none ) ``` Logs are emitted via `os_log` and visible in Xcode's console and macOS Console.app under the `com.cocoar.signalarrr` subsystem. ## Using SignalRWebSocketClient directly `HARRRConnection` is the SignalARRR-specific wrapper. If you want to connect to any standard SignalR hub (without the SignalARRR server library), use `SignalRWebSocketClient` directly: ```swift let client = SignalRWebSocketClient( url: "https://any-signalr-server.com/hub", hubProtocol: .messagepack ) try await client.start() let result: String = try await client.invoke(method: "MyMethod", arguments: ["param"]) client.on("OnMessage") { args in print(args) return nil } ``` ## Next steps * [Typed Proxies & Server Methods](/guide/swift-client/typed-proxies) — `@HubProxy` macro and server-to-client handlers * [Getting Started](/guide/getting-started) — full setup walkthrough * [Packages](/reference/packages) — all available packages --- --- url: /roadmap/test-coverage.md --- # Test Coverage All three client ecosystems (.NET, TypeScript, Swift) test against one shared `IntegrationTestServer` running real Kestrel. No mocking. ## Summary | Platform | Framework | Tests | |----------|-----------|------:| | .NET Unit (SourceGenerator) | xUnit | 3 | | .NET Unit (DynamicProxy) | xUnit | 12 | | .NET Integration (JSON) | xUnit | 50 | | .NET Integration (MessagePack) | xUnit | 5 | | TypeScript Integration (JSON) | vitest | 25 | | TypeScript Integration (MessagePack) | vitest | 5 | | Swift Unit | XCTest | 31 | | Swift Macro | XCTest | 6 | | Swift Integration | XCTest | 6 | ## What's Tested * **Client → Server:** invoke (sync/async, multiple return types), send (fire-and-forget), echo, streaming (ChannelReader, IAsyncEnumerable) * **Server → Client:** fire-and-forget, typed proxy calls, return values (string, list, guid), streaming (IAsyncEnumerable), cancellation * **Complex Types:** DateTime, Guid, List, Dictionary, multiple parameter types * **Multi-ServerMethods:** second class on same hub, `[MessageName]` attribute, hub method coexistence * **Authorization:** authenticated calls (sync/async), unauthenticated rejection, token challenge/refresh, `[AllowAnonymous]` override, second ServerMethods class with hub-level auth * **Error Handling:** structured error types (ArgumentException, InvalidOperationException), non-existent method * **File Transfer:** RequestUploadSlot, HTTP upload, automatic Stream argument preparation, Stream return values * **Advanced:** `[FromServices]` injection, ClientContext.Attributes from headers * **MessagePack:** invoke, send, echo, guid, multi-param — same scenarios as JSON * **Proxy Generation:** ModuleInitializer registration, all 7 return type categories, method name format, CancellationToken extraction, fallback factory ## Test Infrastructure * **IntegrationTestServer** — standalone .NET server, dynamic port, shared by all clients * **`scripts/test-server.sh`** — server lifecycle coordinator (acquire/release with ref counting) * **`scripts/run-integration-tests.sh`** — runs all available client tests in sequence * **.NET fixture** auto-starts server if no `SIGNALARRR_TEST_SERVER_URL` environment variable --- --- url: /guide/dotnet-client/typed-methods.md --- # Typed Methods Call server methods through shared interfaces with full compile-time safety. The source generator produces proxy classes that handle serialization and method dispatch. ## Get a typed proxy Use `GetTypedMethods()` to get a proxy for a shared contract interface: ```csharp var connection = HARRRConnection.Create(builder => { builder.WithUrl("https://localhost:5001/apphub"); }); await connection.StartAsync(); var chat = connection.GetTypedMethods(); ``` ## Call methods The proxy maps interface method calls to the appropriate SignalARRR protocol message: ```csharp [SignalARRRContract] public interface IChatHub { Task SendMessage(string user, string message); // fire-and-forget Task> GetHistory(); // invoke with result IAsyncEnumerable StreamMessages(CancellationToken ct); // stream } ``` ```csharp // Fire-and-forget (sends InvokeMessage) await chat.SendMessage("Alice", "Hello!"); // Invoke with result (sends InvokeMessageResult, awaits response) List history = await chat.GetHistory(); // Stream (sends StreamMessage, returns IAsyncEnumerable) await foreach (var msg in chat.StreamMessages(cancellationToken)) { Console.WriteLine(msg); } ``` ## Return type mapping The proxy determines the protocol message based on the method's return type: | Return type | Protocol | Behavior | |-------------|----------|----------| | `void` | `SendMessage` | Fire-and-forget (blocking) | | `Task` | `SendMessage` | Fire-and-forget, awaitable | | `T` (sync) | `InvokeMessageResult` | Invoke, blocks for result | | `Task` | `InvokeMessageResult` | Invoke, await result | | `IAsyncEnumerable` | `StreamMessage` | Server-to-client stream | | `IObservable` | `StreamMessage` | Server-to-client stream (Rx) | | `ChannelReader` | `StreamMessage` | Server-to-client stream (channel) | ## Generic method support Proxies support generic method arguments. The type arguments are serialized as strings and resolved on the server: ```csharp [SignalARRRContract] public interface IDataHub { Task GetItem(string key); } ``` ```csharp var data = connection.GetTypedMethods(); var user = await data.GetItem("user-123"); ``` ## Proxy caching `GetTypedMethods()` caches proxy instances. Calling it multiple times with the same type returns the same proxy object. ## Source generator requirements For proxy generation to work, the shared interface project must reference `Cocoar.SignalARRR.Contracts`: ```xml ``` The `[SignalARRRContract]` attribute triggers the Roslyn source generator, which produces a proxy class at build time. See [Proxy Generation](/guide/advanced/proxy-generation) for details on how this works. ## Next steps * [Server-to-Client Handlers](/guide/dotnet-client/server-to-client) — handle callbacks from the server * [Streaming](/guide/streaming/server-to-client) — stream data from server methods * [Proxy Generation](/guide/advanced/proxy-generation) — how the source generator works --- --- url: /guide/swift-client/typed-proxies.md --- # Typed Proxies & Server Methods The Swift client supports compile-time proxy generation via the `@HubProxy` macro and server-to-client method handling. ## @HubProxy Macro Mark a protocol with `@HubProxy` to generate a typed proxy class at build time: ```swift import CocoarSignalARRRMacros @HubProxy protocol IChatHub { func sendMessage(user: String, message: String) async throws func getHistory() async throws -> [String] func streamMessages() async throws -> AsyncThrowingStream } ``` The macro generates `IChatHubProxy` that routes each method to the correct connection call: | Return type | Generated call | Protocol | |-------------|----------------|----------| | `async throws` (void) | `connection.send("IChatHub\|method", ...)` | `SendMessage` | | `async throws -> T` | `connection.invoke("IChatHub\|method", ...)` | `InvokeMessageResult` | | `async throws -> AsyncThrowingStream` | `connection.stream("IChatHub\|method", ...)` | `StreamMessage` | ## Use typed proxies ```swift let chat = connection.getTypedMethods(IChatHubProxy.self) try await chat.sendMessage(user: "Alice", message: "Hello!") let history = try await chat.getHistory() for try await msg in try await chat.streamMessages() { print(msg) } ``` ## Server-to-client handlers Register handlers for methods the server can call on the client: ```swift await connection.onServerMethod("ReceiveMessage") { args in let user = args[0] as! String let message = args[1] as! String print("\(user): \(message)") return AnyCodable(nilLiteral: ()) } await connection.onServerMethod("GetClientName") { _ in return AnyCodable(stringLiteral: UIDevice.current.name) } ``` Handlers return `AnyCodable` — a type-erased wrapper that supports all JSON-serializable types. ## Streaming handlers For server-to-client streaming methods, use `onServerStreamMethod`: ```swift await connection.onServerStreamMethod("StreamData") { args in AsyncThrowingStream { continuation in for i in 0..<10 { continuation.yield(AnyCodable(integerLiteral: i)) } continuation.finish() } } ``` ## Interface registration For structured handler registration with a prefix: ```swift await connection.registerHandlers(prefix: "ChatClient", handlers: [ "ReceiveMessage": { args in // handle message return AnyCodable(nilLiteral: ()) }, "GetClientName": { _ in return AnyCodable(stringLiteral: "SwiftClient") }, ]) ``` Or implement the `ServerInterfaceHandler` protocol: ```swift struct MyChatClient: ServerInterfaceHandler { static var interfaceName: String { "ChatClient" } func handlers() -> [String: @Sendable ([Any]) async throws -> AnyCodable] { [ "ReceiveMessage": { args in /* ... */ }, "GetClientName": { _ in AnyCodable(stringLiteral: "SwiftClient") }, ] } } await connection.registerInterface(MyChatClient()) ``` ## Cancellation support When the server passes a `CancellationToken` to a client method, the Swift client uses an actor-based `CancellationManager`. The handler's continuation is cancelled when the server sends `CancelTokenFromServer`. ## HTTP stream references The Swift client supports `StreamReference` resolution. When a server-to-client call includes a `Stream` parameter, the client detects the `StreamReference` marker and downloads the data via HTTP: ```swift let data = try await StreamReferenceResolver.resolve(streamRef) ``` ::: info Stream reference detection in server method dispatch is available. The client downloads via `URLSession`. ::: ## Next steps * [Setup & Usage](/guide/swift-client/setup) — connection basics * [Cancellation Propagation](/guide/advanced/cancellation) — how cancellation works across clients * [HTTP Stream References](/guide/advanced/http-streams) — large file transfer --- --- url: /guide/typescript-client/setup.md --- # TypeScript Client Setup The `@cocoar/signalarrr` npm package provides a TypeScript/JavaScript client for SignalARRR with support for `invoke`, `send`, `stream`, bidirectional streaming, and server-to-client method handling. ::: info Feature Parity The TypeScript client has full feature parity with the .NET client: core RPC, cancellation, bidirectional streaming, and HTTP stream references. ::: ## Installation ```bash npm install @cocoar/signalarrr @microsoft/signalr ``` The package ships as ESM and CJS with full TypeScript declarations. ## Create a connection Use the static `create()` factory with a builder callback: ```ts import { HARRRConnection } from '@cocoar/signalarrr'; import * as signalR from '@microsoft/signalr'; const connection = HARRRConnection.create(builder => { builder.withUrl('https://localhost:5001/apphub'); builder.withAutomaticReconnect(); }); ``` Or wrap an existing `HubConnection`: ```ts const hubConnection = new signalR.HubConnectionBuilder() .withUrl('https://localhost:5001/apphub') .build(); const connection = HARRRConnection.create(hubConnection); ``` ## Start and stop ```ts await connection.start(); // ... use the connection ... await connection.stop(); ``` ## Invoke (call with return value) `invoke()` calls a server method and awaits the result: ```ts const history = await connection.invoke('ChatMethods.GetHistory'); const user = await connection.invoke('UserMethods.GetUser', userId); ``` ## Send (fire-and-forget) `send()` calls a server method without waiting for a return value: ```ts await connection.send('ChatMethods.SendMessage', 'Alice', 'Hello!'); ``` ## Stream `stream()` opens a server-to-client stream: ```ts connection.stream('ChatMethods.StreamMessages').subscribe({ next: msg => console.log(msg), error: err => console.error(err), complete: () => console.log('Stream ended'), }); ``` ## Error handling When a server method throws an exception, `invoke()` rejects with a structured error containing the exception type and message: ```ts try { await connection.invoke('SomeMethod'); } catch (err: any) { console.log(err.type); // "System.ArgumentException" console.log(err.message); // "Invalid value provided" } ``` For more control, use `parseHARRRError()` from the package: ```ts import { parseHARRRError } from '@cocoar/signalarrr'; try { await connection.invoke('SomeMethod'); } catch (err) { const error = parseHARRRError(err); console.log(error.Type, error.Message); } ``` ## MessagePack protocol For better performance with many clients, use MessagePack instead of JSON: ```bash npm install @microsoft/signalr-protocol-msgpack ``` ```ts import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack'; const connection = HARRRConnection.create(builder => { builder.withUrl('https://localhost:5001/apphub'); builder.withHubProtocol(new MessagePackHubProtocol()); }); ``` The server must also have MessagePack enabled (`.AddMessagePackProtocol()`). Both JSON and MessagePack clients can connect to the same hub simultaneously. ## Authentication Provide a token factory through SignalR's connection options: ```ts const connection = HARRRConnection.create(builder => { builder.withUrl('https://localhost:5001/apphub', { accessTokenFactory: () => getAuthToken(), }); }); ``` Token challenges are handled automatically — when the server detects an expired token, it sends a `ChallengeAuthentication` message, and the client calls `accessTokenFactory()` to get a fresh token. ## Connection events ```ts connection.onClose(error => { console.log('Connection closed', error); }); connection.onReconnecting(error => { console.log('Reconnecting...', error); }); connection.onReconnected(connectionId => { console.log('Reconnected as', connectionId); }); ``` ## Connection properties | Property | Type | Description | |----------|------|-------------| | `connectionId` | `string \| null` | Current connection ID | | `state` | `HubConnectionState` | `Disconnected`, `Connecting`, `Connected`, `Reconnecting` | | `baseUrl` | `string` | Hub URL (get/set) | | `serverTimeoutInMilliseconds` | `number` | Server timeout | | `keepAliveIntervalInMilliseconds` | `number` | Keepalive interval | ## Access the raw HubConnection ```ts const hubConnection = connection.asSignalRHubConnection(); ``` ## Method naming The TypeScript client uses string method names. The pattern is `ClassName.MethodName`: ```ts // Calls ChatMethods.SendMessage on the server await connection.send('ChatMethods.SendMessage', 'Alice', 'Hello!'); // Calls UserMethods.GetUser on the server const user = await connection.invoke('UserMethods.GetUser', userId); ``` ## Next steps * [Server Method Handlers](/guide/typescript-client/server-methods) — handle server-to-client calls * [Streaming](/guide/streaming/server-to-client) — stream data from the server * [Getting Started](/guide/getting-started) — full setup walkthrough --- --- url: /guide/why-signalarrr.md --- # Why SignalARRR? ASP.NET Core SignalR is excellent for real-time communication. SignalARRR builds on top of it to solve the problems that emerge in production applications with complex server-client interactions. ## The problems ### Magic strings everywhere With raw SignalR, every method call uses string identifiers. Rename a method and nothing warns you until runtime: ```csharp // Raw SignalR — no compile-time safety await Clients.All.SendAsync("ReceiveMessage", user, message); // string! await connection.InvokeAsync("GetHistory"); // string! ``` With SignalARRR, calls go through typed interfaces. Rename a method and the compiler catches it: ```csharp // SignalARRR — typed var client = ClientContext.GetTypedMethods(); client.ReceiveMessage(user, message); // compile-time checked var chat = connection.GetTypedMethods(); await chat.GetHistory(); // compile-time checked ``` ### Bidirectional RPC without type safety Since .NET 7, SignalR supports server-to-client calls with return values via `ISingleClientProxy.InvokeAsync()`. But these calls still rely on string method names: ```csharp // Raw SignalR — works, but no compile-time safety var result = await Clients.Single(connectionId) .InvokeAsync("GetClientName", cancellationToken); // string! ``` SignalARRR makes bidirectional RPC fully typed through shared interfaces: ```csharp // SignalARRR — typed bidirectional RPC var client = ClientContext.GetTypedMethods(); string name = await client.GetClientName(); // compile-time checked ``` ### Hub classes grow large As your application grows, a single hub class becomes unwieldy. SignalARRR lets you split methods across multiple `ServerMethods` classes that are auto-discovered and fully DI-enabled: ```csharp public class ChatMethods : ServerMethods, IChatHub { ... } public class AdminMethods : ServerMethods, IAdminHub { ... } public class NotificationMethods : ServerMethods, INotificationHub { ... } ``` ### Authorization without token lifecycle management Raw SignalR supports `[Authorize]` on hub methods, but there's no built-in mechanism for continuous token validation or automatic token refresh during a long-lived connection. SignalARRR adds a challenge/refresh flow and authorization inheritance across `ServerMethods` classes: ```csharp [Authorize] // inherited by all ServerMethods classes for this hub public class SecureHub : HARRR { ... } public class AdminMethods : ServerMethods, IAdminHub { [Authorize(Policy = "AdminOnly")] public Task DeleteUser(string userId) { ... } [AllowAnonymous] public Task GetPublicInfo() { ... } } ``` When a token expires mid-connection, SignalARRR automatically challenges the client for a fresh token instead of disconnecting. ### Large file transfer via HTTP (`Stream` parameters) WebSocket connections aren't built for multi-GB transfers — buffer limits, memory pressure, and blocking the shared connection for all clients. SignalARRR solves this transparently with **HTTP Stream References**: when a method has a `System.IO.Stream` parameter, the data is automatically routed through HTTP while the RPC call looks completely normal: ```csharp // Server sends a file to the client — looks like a normal call var client = ClientContext.GetTypedMethods(); long size = await client.ProcessFile("video.mp4", fileStream); // Client receives a regular Stream — no idea it came via HTTP long FileLength(string name, Stream fileStream) => fileStream.Length; ``` No chunking logic, no upload endpoints, no manual HTTP calls. Just a `Stream` parameter. ### Item streaming in both directions Separately from file transfer, SignalARRR supports **item streaming** — sending sequences of items over SignalR using `IAsyncEnumerable`, `IObservable`, and `ChannelReader` in both directions. ## What you get | Feature | Raw SignalR | SignalARRR | |---------|-------------|------------| | Typed method calls | No | Yes — shared interfaces | | Compile-time safety | No | Yes — source generator | | Server → Client RPC with return | Yes (string-based) | Yes — typed via interfaces | | Organized hub methods | Single class | Multiple ServerMethods classes | | Authorization inheritance across classes | No | Hub → ServerMethods inheritance | | Token auto-refresh on expiry | No | Built-in challenge flow | | Large file transfer via RPC | No | Automatic HTTP stream references | | CancellationToken propagation | Limited | Full — server can cancel client | | Client Manager (outside hub) | Manual | Built-in | | TypeScript client | Basic | Full protocol support | | Swift client (iOS/macOS) | No | Native client with `@HubProxy` macro | ## How it works SignalARRR wraps SignalR's standard hub protocol. All communication flows through a small set of well-defined hub methods (`InvokeMessage`, `InvokeMessageResult`, `StreamMessage`, etc.) that carry typed payloads. The source generator produces proxy classes that serialize interface method calls into these messages — no runtime reflection needed. This means SignalARRR is **fully backward-compatible** with standard SignalR clients. You can mix SignalARRR and raw SignalR clients on the same hub. ## Next steps * [Getting Started](/guide/getting-started) — install and set up your first hub * [Hub Setup](/guide/server/hub-setup) — understand the HARRR base class * [Packages](/reference/packages) — choose the right NuGet/npm packages --- --- url: /reference/wire-protocol.md --- # Wire Protocol SignalARRR communicates over standard SignalR using a fixed set of hub methods. This page documents the protocol for advanced use cases and custom client implementations. ## Hub method names ### Client → Server | Hub method | Purpose | Payload | Returns | |------------|---------|---------|---------| | `InvokeMessage` | Fire-and-forget (void methods) | `ClientRequestMessage` | — | | `InvokeMessageResult` | Call with return value | `ClientRequestMessage` | `object` | | `SendMessage` | Fire-and-forget (Task methods) | `ClientRequestMessage` | — | | `StreamMessage` | Open a server-to-client stream | `ClientRequestMessage` | `IAsyncEnumerable` | | `StreamItemToServer` | Send a stream item | `Guid streamId, object item` | — | | `StreamCompleteToServer` | Complete a stream | `Guid streamId, string? error` | — | ### Server → Client | Message | Purpose | Payload | |---------|---------|---------| | `InvokeServerRequest` | Call client method, expect reply (native client result) | `ServerRequestMessage` | | `InvokeServerMessage` | Call client method, fire-and-forget | `ServerRequestMessage` | | `ChallengeAuthentication` | Request fresh auth token | `ServerRequestMessage` | | `CancelTokenFromServer` | Cancel a client operation | `ServerRequestMessage` (with `CancellationGuid`) | ## Message types ### ClientRequestMessage Sent from client to server with every RPC call. ```json { "Method": "ChatMethods.SendMessage", "Arguments": ["Alice", "Hello!"], "Authorization": "Bearer eyJ...", "GenericArguments": [] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `Method` | string | Yes | `ClassName.MethodName` or `InterfaceName\|MethodName` | | `Arguments` | array | Yes | Serialized method arguments | | `Authorization` | string | No | Bearer token for authenticated methods | | `GenericArguments` | string\[] | No | Type names for generic methods | ### ServerRequestMessage Sent from server to client for bidirectional RPC. ```json { "Id": "550e8400-e29b-41d4-a716-446655440000", "Method": "GetClientName", "Arguments": [], "CancellationGuid": null, "StreamId": null } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `Id` | GUID | Yes | Correlation ID — used internally by SignalR's native client results | | `Method` | string | Yes | Method name to invoke on client | | `Arguments` | array | No | Method arguments | | `GenericArguments` | string\[] | No | Generic type arguments | | `CancellationGuid` | GUID | No | Links to `CancelTokenFromServer` for remote cancellation | | `StreamId` | GUID | No | Stream correlation for client-to-server streams | ### CancellationTokenReference Marker object placed in `Arguments` where a `CancellationToken` parameter exists. The client detects this and substitutes an `AbortSignal` (TypeScript) or `CancellationToken` (.NET). ```json { "Id": "550e8400-e29b-41d4-a716-446655440001" } ``` ## Protocol flows ### Client calls server (with result) ```mermaid sequenceDiagram participant Client participant Server Client->>Server: InvokeMessageResult(ClientRequestMessage) Server->>Server: Resolve method, authorize, execute Server-->>Client: Return result ``` ### Server calls client (with result) ```mermaid sequenceDiagram participant Server participant Client Server->>Client: InvokeServerRequest(ServerRequestMessage) Client->>Client: Dispatch to registered handler Note over Client: Handler returns result (native SignalR client result) Server-->>Server: InvokeCoreAsync completes with result ``` ### Token challenge flow ```mermaid sequenceDiagram participant Client participant Server Client->>Server: InvokeMessageResult (expired token) Server->>Client: ChallengeAuthentication(ServerRequestMessage) Client->>Client: Call AccessTokenProvider Note over Client: Handler returns new token (native SignalR client result) Server-->>Server: InvokeCoreAsync completes with token Server->>Server: Validate new token, continue request Server-->>Client: Return result ``` ### Client-to-server streaming ```mermaid sequenceDiagram participant Client participant Server Client->>Server: StreamItemToServer(streamId, item1) Client->>Server: StreamItemToServer(streamId, item2) Client->>Server: StreamCompleteToServer(streamId, null) ``` ## Method name resolution The server resolves method names in two formats: | Format | Example | Resolution | |--------|---------|------------| | `ClassName.MethodName` | `ChatMethods.SendMessage` | Direct lookup in method collection | | `InterfaceName\|MethodName` | `IChatHub\|SendMessage` | Interface-based lookup (used by typed proxies) | ## Next steps * [API Overview](/reference/api) — public API surface * [Packages](/reference/packages) — package guide * [Cancellation Propagation](/guide/advanced/cancellation) — cancellation protocol