SignalRConnector
Microsoft SignalR hub adapter. @microsoft/signalr is loaded as a dynamic peer dependency — install it explicitly:
pnpm add @microsoft/signalr
import { SignalRConnector } from "@chativa/connector-signalr";
ConnectorRegistry.register(
new SignalRConnector({
url: "https://my-server.example.com/chathub",
receiveMethod: "ReceiveMessage",
sendMethod: "SendMessage",
accessTokenFactory: async () => localStorage.getItem("token") ?? "",
})
);
chatStore.getState().setConnector("signalr");
Tool calls, GenUI and human-in-the-loop chips work over this connector via the shared JSON frame vocabulary — see Rich frames.
Options
Schema: schemas/connectors/signalr.schema.json.
| Field | Default | Description |
|---|---|---|
url (required) | — | Hub URL. |
hubName | "chat" | Logical name — informational, not sent on the wire. |
receiveMethod | "ReceiveMessage" | Server → client method name. The connector subscribes via connection.on(receiveMethod, ...). |
sendMethod | "SendMessage" | Client → server method invoked by sendMessage(). |
surveyMethod | "SendSurvey" | Client → server method invoked by sendSurvey(). |
genUIEventMethod | "SendGenUIEvent" | Client → server method invoked when a GenUI component fires an event. |
accessTokenFactory | () => "" | Function returning a bearer token (sync or Promise<string>). |
Server hub example
public class ChatHub : Hub
{
public async Task SendMessage(OutgoingMessage msg)
{
var reply = new IncomingMessage {
Id = Guid.NewGuid().ToString(),
Type = "text",
Data = new { text = $"Echo: {msg.Data.text}" },
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
};
await Clients.Caller.SendAsync("ReceiveMessage", reply);
}
public Task SendSurvey(SurveyPayload payload) => /* persist */;
}
Tool calls, GenUI and HITL from the hub
All of it travels through the same receiveMethod you already use for messages — no extra hub method, no client changes beyond registering any GenUI component. See Rich frames for the full vocabulary.
public class ChatHub : Hub
{
public async Task SendMessage(OutgoingMessage msg)
{
var send = (object frame) => Clients.Caller.SendAsync("ReceiveMessage", frame);
await send(new { type = "typing", isTyping = true });
// A tool call is a lifecycle: re-send the SAME id to advance the row.
await send(new { type = "tool_call", data = new {
id = "c1", name = "get_sales_stats", status = "running", @params = new { region = "EMEA" } } });
await send(new { type = "tool_call", data = new {
id = "c1", name = "get_sales_stats", status = "completed", result = "42 orders" } });
// Stream a GenUI component into the bubble.
await send(new { type = "genui", streamId = "s1", done = true, chunk = new {
type = "ui", component = "sales-chart", props = new { orders = 42 }, id = 1 } });
await send(new { type = "typing", isTyping = false });
// Ask for approval — chips are the human-in-the-loop interrupt.
await send(new {
type = "text",
id = Guid.NewGuid().ToString(),
from = "bot",
data = new { text = "Generate the PDF report?" },
actions = new[] {
new { label = "Approve", value = "approve_pdf" },
new { label = "Cancel" }
}
});
}
/// A mounted GenUI component fired an event (form submit, card action…).
/// Name it whatever you like and set `genUIEventMethod` to match.
public Task SendGenUIEvent(GenUIEventFrame frame)
{
// frame.StreamId, frame.EventType, frame.Payload
return Task.CompletedTask;
}
}
public record GenUIEventFrame(string StreamId, string EventType, JsonElement Payload);
The tapped chip arrives as an ordinary SendMessage call with "approve_pdf" — there's no separate resume channel to build.
On the client, register the component before connecting:
import { GenUIRegistry } from "@chativa/genui";
GenUIRegistry.register("sales-chart", "sales-chart-element"); // your LitElement tag
Reconnect
The connector enables withAutomaticReconnect() on the hub builder. SignalR handles backoff internally; you don't need to configure anything else.