SignalR for Rust
A real SignalR client, native and in the browser.
dXn.SignalR connects Rust programs to ASP.NET Core SignalR hubs from Windows, Linux, macOS, Android, iOS and the browser. One API covers invoke, send, server and client streams, server calls with client results, and reconnection, modelled on the official .NET client so its lifecycle and options carry over.
cargo add dxn-signalr --git https://github.com/dXnSix/dXn.SignalR --tag v1.1.0use dxn_signalr::SignalRClient;
use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<(), dxn_signalr::Error> {
let mut client = SignalRClient::connect_with("localhost", "chat", |c| {
c.unsecure();
c.with_port(5000);
})
.await?;
let _on = client.register_typed("ReceiveMessage", |_ctx, (user, text): (String, String)| {
println!("{user}: {text}");
});
client
.send_with_args("Send".to_string(), |a| { a.argument("rust").argument("hi"); })
.await?;
let sum: i32 = client
.invoke_with_args("Add".to_string(), |a| { a.argument(2).argument(3); })
.await?;
let mut prices = client.enumerate::<f64>("Prices".to_string()).await;
while let Some(price) = prices.next().await {
println!("{sum} {price}");
}
Ok(())
}- Rust
- 1.97+ (edition 2021)
- Targets
- Tokio native + browser WASM
- Protocols
- JSON, MessagePack
- Transports
- WebSockets, SSE, Long Polling
- Server
- ASP.NET Core SignalR
- Release
- git tag v1.1.0
What you get
What you get
Every call style
invoke for typed results, send for fire-and-forget, enumerate for server streams you cancel by dropping them, and client streams as arguments. register handles hub pushes, including client results for InvokeAsync.
Read more →Native and browser, one API
Tokio on Windows, Linux, macOS, Android and iOS; wasm32-unknown-unknown on the main thread, in Web Workers and in multithreaded builds. All three transports work on every target.
Read more →Reconnection that keeps calls alive
Retry policies including the official 0/2/10/30 s schedule, manual control through a handler, and ASP.NET Core stateful reconnect, which replays unacknowledged messages across short drops.
Read more →JSON and MessagePack with Serde
Your Serde types travel unchanged on both hub protocols. JSON is read case-insensitively and the MessagePack encoder keeps your field names, so one DTO pair serves both.
Read more →The .NET client's lifecycle and options
Build, register, start, stop. Closed, Reconnecting and Reconnected hooks, keep-alive and server timeouts, pause and resume for mobile, access-token factories, request hooks and cookie jars.
Read more →Measured against the .NET client
In the repository's same-run comparison, invoke latency is on par and server streams and fan-in run 1.4 to 2.6 times the .NET client's throughput. Native TLS is rustls, with no OpenSSL.
Read more →Quick tour
#Call, stream and listen
Method names are strings and arguments are positional, so a call reads like the hub method it targets. Server streams are futures::Streams; dropping one sends CancelInvocation, so the hub stops producing.
use dxn_signalr::SignalRClient;
use futures::StreamExt;
async fn tour(mut client: SignalRClient) -> Result<(), dxn_signalr::Error> {
let total: i32 = client.invoke("Total".to_string()).await?; // typed result
client.send("Ping".to_string()).await?; // fire and forget
let mut ticks = client.enumerate::<f64>("Prices".to_string()).await;
let first_ten: Vec<f64> = (&mut ticks).take(10).collect().await;
drop(ticks); // cancels the server-side enumeration
println!("{total} {first_ten:?}");
Ok(())
}
Calling the hub covers arguments, client streams, cancellation and timeouts.
#Reconnect without losing in-flight calls
Reconnection is opt-in, as in .NET. With ASP.NET Core 8 stateful reconnect, a short network drop resumes the same logical connection: pending calls stay pending and unacknowledged messages are replayed.
use std::sync::Arc;
use dxn_signalr::{DefaultRetryPolicy, ReconnectionConfig, SignalRClient, StatefulReconnectConfig};
async fn resilient() -> Result<SignalRClient, dxn_signalr::Error> {
let stateful = StatefulReconnectConfig::new(256 * 1024)?; // replay buffer in bytes
SignalRClient::connect_with("chat.example.com", "chat", move |c| {
c.with_messagepack_protocol();
c.with_stateful_reconnect(stateful);
c.with_reconnection_policy(ReconnectionConfig {
policy: Arc::new(DefaultRetryPolicy), // 0, 2, 10, 30 s
});
})
.await
}
Reconnection explains policies, manual control and what a resume guarantees.
#The same code in the browser
The browser build uses fetch, EventSource and WebSocket and runs on the JavaScript event loop. Spawn the client with spawn_local; everything else is the API you used natively.
use dxn_signalr::SignalRClient;
pub fn start() {
wasm_bindgen_futures::spawn_local(async {
let result = SignalRClient::connect_with("chat.example.com", "chat", |c| {
c.authenticate_bearer("<access token>".to_string());
})
.await;
match result {
Ok(mut client) => {
let _ = client.send("Ping".to_string()).await;
}
Err(error) => log::error!("cannot connect: {error}"),
}
});
}
Browser and WebAssembly covers the build, CORS and WebSocket authentication.
#Errors you can branch on
Every failure is one Error type with an ErrorKind and structured fields. Never match on message text.
use dxn_signalr::{ErrorKind, SignalRClient};
async fn total(client: &mut SignalRClient) -> Option<i32> {
match client.invoke::<i32>("Total".to_string()).await {
Ok(total) => Some(total),
Err(error) if error.kind() == ErrorKind::Hub => {
eprintln!("the hub failed: {}", error.server_message().unwrap_or(""));
None
}
Err(error) if error.is_transient() => {
eprintln!("try again later: {error}");
None
}
Err(error) => {
eprintln!("{error}");
None
}
}
}
Errors and logging lists every kind, and Troubleshooting works backwards from the symptom.
Where next
Get started
Add the dependency and connect your first client.
Continue →A complete example
An ASP.NET Core hub and a Rust client using every call style.
Continue →From the .NET client
Map HubConnection and the JavaScript client to Rust.
Continue →Troubleshooting
Find the cause from the symptom or the error kind.
Continue →