Skip to content

Client Comparison ​

SignalARRR has five client implementations. This page shows what each client supports.

Platform Support ​

.NET.NET FrameworkTSSwiftKotlin
Targetnet8.0 / net9.0 / net10.0net462+ (netstandard2.0)Node 22 / browsersiOS 14+ / macOS 11+Android 5+ (API 21) / JVM 8+
PackageCocoar.SignalARRR.ClientCocoar.SignalARRR.Client.FullFramework@cocoar/signalarrrCocoarSignalARRRdev.cocoar:signalarrr

RPC ​

.NET.NET FrameworkTSSwiftKotlin
Invoke (await result)✓✓✓✓✓
Send (fire & forget)✓✓✓✓✓
Generic arguments✓✓✓✓✓

Item Streaming ​

.NET.NET FrameworkTSSwiftKotlin
Server→Client✓✓✓✓✓
Client→Server✓✓✓✓✓
Stream method handlers✓✓✓✓✓

.NET Framework streaming

The FullFramework client supports streaming via polyfill packages (Microsoft.Bcl.AsyncInterfaces and System.Threading.Channels). IAsyncEnumerable<T>, ChannelReader<T>, and await foreach all work on .NET Framework 4.6.2+.

Server-to-Client RPC ​

.NET.NET FrameworkTSSwiftKotlin
Method handlers✓✓✓✓✓
Interface registration✓✓—✓✓
CancellationToken✓✓✓✓✓

File Transfer (HTTP Stream References) ​

.NET.NET FrameworkTSSwiftKotlin
Download (Stream return)✓✓✓✓✓
Upload (Stream parameter)✓✓✓✓✓

Authorization ​

.NET.NET FrameworkTSSwiftKotlin
credential / connectionCredential / messageCredential✓ (With…)✓ (With…)✓✓✓
Connection token on the transportheaderheaderheader — URL in a browser (WebSocket, SSE) and under Node (SSE)headerheader
Auto challenge/refresh✓✓✓✓✓
Error codes (HARRRErrorCodes)✓✓✓✓✓
HTTP status of a rejected negotiateStatusCodeStatusCodestatusCodestatusCodestatusCode
Extra headersSignalR optionsSignalR optionshttpConnectionOptionsheadersheaders

The credential options are named and behave the same in every client — see the two credentials.

Proxy Generation ​

.NET.NET FrameworkTSSwiftKotlin
Compile-time proxiesSource Generator——@HubProxy Macro@HubProxy + KSP
Runtime proxiesDispatchProxyDispatchProxy———

.NET Framework uses DispatchProxy only

The Roslyn source generator requires projects to reference Cocoar.SignalARRR.Contracts which targets net8.0+. On .NET Framework, typed proxies are created at runtime via DispatchProxy. The interfaces don't need [SignalARRRContract] — any C# interface works.

Connection ​

.NET.NET FrameworkTSSwiftKotlin
Auto-reconnect✓✓✓✓✓
Connection events✓✓✓✓✓
Raw SignalR access✓✓✓✓✓
Raw on/off overloads16——81 (typed accessors)

Concurrency Model ​

.NET.NET FrameworkTSSwiftKotlin
Async patternasync/awaitasync/awaitPromiseasync/awaitcoroutines / Flow
CancellationCancellationTokenCancellationTokenAbortSignalActorCoroutine Job
SerializationSystem.Text.JsonSystem.Text.JsonJSONCodablekotlinx.serialization
MessagePack✓ (optional)✓ (optional)✓ (optional)✓ (built-in)✓ (built-in)

MessagePack is optional

MessagePack support is not included by default. Install the protocol package separately and register it on the connection builder:

.NET / .NET Framework:

bash
dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack
csharp
builder.AddMessagePackProtocol();

TypeScript:

bash
npm install @microsoft/signalr-protocol-msgpack
ts
builder.withHubProtocol(new MessagePackHubProtocol());

Server:

csharp
builder.Services.AddSignalR().AddMessagePackProtocol();

Swift has MessagePack built-in — use hubProtocol: .messagepack when creating the connection.

Kotlin has MessagePack built-in — set hubProtocol = HubProtocolKind.MESSAGE_PACK in the options.

SignalARRR auto-detects the active protocol and uses the correct serializer. No additional configuration needed.

Transport ​

.NET.NET FrameworkTSSwiftKotlin
WebSockets✓✓✓✓✓
Server-Sent Events✓✓✓✓✓
Long Polling✓✓✓✓✓
Transport fallback✓✓✓✓✓

API Comparison ​

Creating a connection ​

csharp
var connection = HARRRConnection.Create(builder =>
{
    builder.WithUrl("https://server/hub");
});
await connection.StartAsync();
csharp
var connection = HARRRConnection.Create(builder =>
{
    builder.WithUrl("https://server/hub");
});
await connection.StartAsync();
ts
const connection = HARRRConnection.create(builder => {
    builder.withUrl('https://server/hub');
});
await connection.start();
swift
let connection = await HARRRConnection.create(
    url: "https://server/hub"
)
try await connection.start()
kotlin
val connection = HARRRConnection.create("https://server/hub")
connection.start()

Typed proxies ​

csharp
// Shared project: mark with [SignalARRRContract]
[SignalARRRContract]
public interface IChatHub {
    Task SendMessage(string user, string message);
    Task<List<string>> GetHistory();
}

// Client: get typed proxy
var chat = connection.GetTypedMethods<IChatHub>();
await chat.SendMessage("Alice", "Hello!");
csharp
// Define interface (no attribute needed)
public interface IChatHub {
    Task SendMessage(string user, string message);
    Task<List<string>> GetHistory();
}

// Client: get typed proxy (runtime-generated via DispatchProxy)
var chat = connection.GetTypedMethods<IChatHub>();
await chat.SendMessage("Alice", "Hello!");
swift
// 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!")
kotlin
// Client: mark with @HubProxy, the KSP processor generates IChatHubProxy
@HubProxy(name = "MyApp.Contracts.IChatHub")
interface IChatHub {
    suspend fun sendMessage(user: String, message: String)
    suspend fun getHistory(): List<String>
}

// Client: get typed proxy
val chat = connection.getTypedMethods(IChatHubProxy)
chat.sendMessage("Alice", "Hello!")

TypeScript has no typed proxy generation — method names are passed as strings.

Invoke / Send / Stream ​

csharp
// Invoke (await result)
var result = await connection.InvokeCoreAsync<string>(message, ct);

// Send (fire-and-forget)
await connection.SendCoreAsync(message, ct);

// Stream
await foreach (var item in connection.StreamAsyncCore<int>(message, ct))
    Console.WriteLine(item);
csharp
// Invoke (await result)
var result = await connection.InvokeCoreAsync<string>(message, ct);

// Send (fire-and-forget)
await connection.SendCoreAsync(message, ct);

// Stream (via polyfill packages)
await foreach (var item in connection.StreamAsyncCore<int>(message, ct))
    Console.WriteLine(item);
ts
// Invoke
const result = await connection.invoke<string>('Method.Name');

// Send
await connection.send('Method.Name', arg1, arg2);

// Stream
connection.stream<number>('Method.Name').subscribe({
    next: item => console.log(item),
});
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<Int, Error> {
    print(item)
}
kotlin
// Invoke
val result: String = connection.invoke("Method.Name")

// Send
connection.send("Method.Name", arg1, arg2)

// Stream
connection.stream<Int>("Method.Name").collect { item -> println(item) }

Server-to-client handlers ​

csharp
connection.RegisterInterface<IChatClient, ChatClientImpl>(new ChatClientImpl());
csharp
connection.RegisterInterface<IChatClient, ChatClientImpl>(new ChatClientImpl());
ts
// The contract's wire name: the .NET clients resolve the interface for you, TypeScript and Swift
// address it directly. See /guide/server/contracts-wire-names
connection.onServerMethod('MyApp.Contracts.IChatClient|ReceiveMessage', (user, message) => {
    console.log(`${user}: ${message}`);
});
swift
await connection.onServerMethod("MyApp.Contracts.IChatClient|ReceiveMessage") { args in
    print("\(args[0]): \(args[1])")
    return AnyCodable.nil
}
kotlin
connection.onServerMethod("MyApp.Contracts.IChatClient|ReceiveMessage") { args ->
    println("${args.value<String>(0)}: ${args.value<String>(1)}")
}

CancellationToken handling ​

ClientMechanismType
.NETStandard CancellationTokenNative
.NET FrameworkStandard CancellationTokenNative
TypeScriptAbortSignal via CancellationManager (Map-based)Web API
SwiftActor-based CancellationManager with continuationsSwift Concurrency
KotlinThe handler's coroutine Job is cancelled via CancellationManagerStructured concurrency

Packages ​

ClientPackageInstall
.NETCocoar.SignalARRR.Clientdotnet add package
.NET FrameworkCocoar.SignalARRR.Client.FullFrameworkdotnet add package
TypeScript@cocoar/signalarrrnpm install
SwiftCocoarSignalARRR + CocoarSignalARRRMacrosSwift Package Manager
Kotlindev.cocoar:signalarrr + dev.cocoar:signalarrr-kspGradle (Maven Central)

Released under the Apache-2.0 License.