Skip to content

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 ​

bash
dotnet add package Cocoar.SignalARRR.Server
bash
dotnet add package Cocoar.SignalARRR.Client
bash
dotnet add package Cocoar.SignalARRR.Client.FullFramework
bash
dotnet add package Cocoar.SignalARRR.Contracts
bash
npm install @cocoar/signalarrr
swift
.package(url: "https://github.com/cocoar-dev/Cocoar.SignalARRR.git", from: "5.0.0")
// Products: CocoarSignalARRR, CocoarSignalARRRMacros
kotlin
implementation("dev.cocoar:signalarrr:5.2.0")
ksp("dev.cocoar:signalarrr-ksp:5.2.0")   // typed proxies via @HubProxy

1. Define shared interfaces ​

Create a shared project and reference Cocoar.SignalARRR.Contracts. Mark each interface with [SignalARRRContract] — the source generator that comes with the Server and Client packages will produce typed proxies at build time.

csharp
using Cocoar.SignalARRR.Common.Attributes;

[SignalARRRContract]
public interface IChatHub
{
    Task SendMessage(string user, string message);
    Task<List<string>> GetHistory();
    IAsyncEnumerable<string> StreamMessages(CancellationToken ct);
}

[SignalARRRContract]
public interface IChatClient
{
    void ReceiveMessage(string user, string message);
    Task<string> 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.MapSignalARRRHub<ChatHub>("/chathub");

Create an empty hub — the actual method implementations go into ServerMethods<T> classes:

csharp
public class ChatHub : HARRR
{
    public ChatHub(IServiceProvider sp) : base(sp) { }
}

public class ChatMethods : ServerMethods<ChatHub>, IChatHub
{
    public Task SendMessage(string user, string message)
    {
        // Broadcast to all clients
        var client = ClientContext.GetTypedMethods<IChatClient>();
        client.ReceiveMessage(user, message);
        return Task.CompletedTask;
    }

    public Task<List<string>> GetHistory() =>
        Task.FromResult(new List<string> { "Hello", "World" });

    public async IAsyncEnumerable<string> 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<IChatHub>();
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<string[]>('ChatMethods.GetHistory');

// Fire-and-forget
await connection.send('ChatMethods.SendMessage', 'Alice', 'Hello!');

// Stream
connection.stream<string>('ChatMethods.StreamMessages').subscribe({
    next: msg => console.log(msg),
    complete: () => console.log('done'),
});

// Handle server-to-client calls. The name is the contract's wire name, 'interface|method' —
// a bare method name never matches a contract call. See /guide/server/contracts-wire-names
connection.onServerMethod('MyApp.Contracts.IChatClient|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<String, Error>
}

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. The name is the contract's wire name, "interface|method".
await connection.onServerMethod("MyApp.Contracts.IChatClient|GetClientName") { _ in
    AnyCodable(stringLiteral: UIDevice.current.name)
}

6. Kotlin client (Android / JVM) ​

kotlin
import dev.cocoar.signalarrr.HARRRConnection
import dev.cocoar.signalarrr.HubProxy
import kotlinx.coroutines.flow.Flow

@HubProxy(name = "MyApp.Contracts.IChatHub")
interface IChatHub {
    suspend fun sendMessage(user: String, message: String)
    suspend fun getHistory(): List<String>
    fun streamMessages(): Flow<String>
}

val connection = HARRRConnection.create("https://localhost:5001/chathub")
connection.start()

// Typed calls through the KSP-generated proxy
val chat = connection.getTypedMethods(IChatHubProxy)
chat.sendMessage("Alice", "Hello!")
val history = chat.getHistory()

// Streaming
chat.streamMessages().collect { msg -> println(msg) }

// Handle server-to-client calls. The name is the contract's wire name, "interface|method".
connection.onServerMethod("MyApp.Contracts.IChatClient|GetClientName") { Build.MODEL }

Next steps ​

Released under the Apache-2.0 License.