368 lines
11 KiB
Markdown
368 lines
11 KiB
Markdown
# Rust (Beta)
|
|
|
|
> Source: `src/content/docs/clients/rust.mdx`
|
|
> Canonical URL: https://rivet.dev/docs/clients/rust
|
|
> Description: Connect Rust apps to Rivet Actors.
|
|
|
|
---
|
|
Rust support is in beta. The supported public Rust API is `rivetkit` and `rivetkit::client`; lower-level crates are internal implementation details and do not carry a stability guarantee. See the full API reference on [docs.rs/rivetkit](https://docs.rs/rivetkit), or the runnable [`hello-world-rust`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-rust) example.
|
|
|
|
## Getting Started
|
|
|
|
See the [Rust quickstart guide](/docs/actors/quickstart/rust) for getting started.
|
|
|
|
## Install
|
|
|
|
Add the `rivetkit` crate and its companions:
|
|
|
|
```sh
|
|
cargo add rivetkit anyhow async-trait
|
|
cargo add serde --features derive
|
|
cargo add tokio --features full
|
|
```
|
|
|
|
The Rust client is strongly typed. It shares the same action and event types as your actor, so define your actor in `src/lib.rs` and import those types from both your server and your client. There is no need to redefine the actor on the client. See [Define Your Actor](/docs/actors/quickstart/rust#define-your-actor) in the quickstart for the actor definition this page builds on.
|
|
|
|
## Minimal Client
|
|
|
|
```rust @nocheck
|
|
use counter::{Counter, Increment};
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
|
|
|
let counter = client.get_or_create_typed_default::<Counter>("counter", ["my-counter"])?;
|
|
let count = counter.send(Increment { amount: 1 }).await?;
|
|
println!("New count: {count}");
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
`counter` here is your crate name (the package `name` in `Cargo.toml`, with dashes as underscores). `Counter` and `Increment` are the types you defined alongside your actor.
|
|
|
|
## Stateless vs Stateful
|
|
|
|
```rust @nocheck
|
|
use counter::{Counter, Increment, NewCount};
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
|
let counter = client.get_or_create_typed_default::<Counter>("counter", ["my-counter"])?;
|
|
|
|
// Stateless: each call is independent
|
|
counter.send(Increment { amount: 1 }).await?;
|
|
|
|
// Stateful: keep a connection open for realtime events
|
|
let connection = counter.connect();
|
|
connection
|
|
.on::<NewCount>(|event| println!("count: {}", event.count))
|
|
.await;
|
|
connection.send(Increment { amount: 1 }).await?;
|
|
|
|
connection.disconnect().await;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
A stateless call on the handle opens a short-lived request per action. A connection keeps a WebSocket open so you can receive events and reuse it across calls.
|
|
|
|
## Getting Actors
|
|
|
|
```rust @nocheck
|
|
use counter::Counter;
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig, GetOrCreateOptions},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
use serde_json::json;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
|
|
|
// Get or create an actor
|
|
let room = client.get_or_create_typed_default::<Counter>("counter", ["room-42"])?;
|
|
|
|
// Get an existing actor handle (fails when used if the actor does not exist)
|
|
let existing = client.get_typed_default::<Counter>("counter", ["room-42"])?;
|
|
|
|
// Create a new actor with input
|
|
let created = client.get_or_create_typed::<Counter>(
|
|
"counter",
|
|
["game-1"],
|
|
GetOrCreateOptions {
|
|
create_with_input: Some(json!({ "mode": "ranked" })),
|
|
..Default::default()
|
|
},
|
|
)?;
|
|
|
|
// Get an actor handle by ID
|
|
let by_id = client.get_for_id("counter", "actor-id", Default::default())?;
|
|
|
|
// Resolve the actor ID
|
|
let resolved_id = room.inner().resolve().await?;
|
|
println!("Resolved ID: {resolved_id}");
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
`get_typed_default` / `get_or_create_typed_default` use default options. The non-default variants (`get_typed` / `get_or_create_typed`) take `GetOptions` / `GetOrCreateOptions` for connection parameters, input, and region.
|
|
|
|
## Connection Parameters
|
|
|
|
Pass connection parameters through the handle options. They are delivered to the actor's `create_conn_state` callback:
|
|
|
|
```rust @nocheck
|
|
use counter::Counter;
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig, GetOrCreateOptions},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
use serde_json::json;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
|
|
|
let chat = client.get_or_create_typed::<Counter>(
|
|
"counter",
|
|
["general"],
|
|
GetOrCreateOptions {
|
|
params: Some(json!({ "authToken": "jwt-token-here" })),
|
|
..Default::default()
|
|
},
|
|
)?;
|
|
|
|
let connection = chat.connect();
|
|
connection.disconnect().await;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## Subscribing to Events
|
|
|
|
`on` registers a typed callback for an event and returns once the subscription is registered:
|
|
|
|
```rust @nocheck
|
|
use counter::{Counter, NewCount};
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
|
let connection = client
|
|
.get_or_create_typed_default::<Counter>("counter", ["general"])?
|
|
.connect();
|
|
|
|
connection
|
|
.on::<NewCount>(|event| println!("count changed: {}", event.count))
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
Event callbacks are synchronous and run for every matching event. The actor's emitted event type (here `NewCount`) is decoded into the typed value for you.
|
|
|
|
## Connection Lifecycle
|
|
|
|
The lower-level connection exposes lifecycle callbacks and the current status. Reach it with `connection.inner()`:
|
|
|
|
```rust @nocheck
|
|
use counter::Counter;
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
|
let connection = client
|
|
.get_or_create_typed_default::<Counter>("counter", ["general"])?
|
|
.connect();
|
|
let inner = connection.inner().clone();
|
|
|
|
inner.on_open(|| println!("connected")).await;
|
|
inner.on_close(|| println!("disconnected")).await;
|
|
inner.on_error(|message| eprintln!("error: {message}")).await;
|
|
inner
|
|
.on_status_change(|status| println!("status: {status:?}"))
|
|
.await;
|
|
|
|
println!("current status: {:?}", inner.conn_status());
|
|
|
|
connection.disconnect().await;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
`ConnectionStatus` is one of `Idle`, `Connecting`, `Connected`, or `Disconnected`. Connections reconnect automatically with backoff until you call `disconnect`.
|
|
|
|
## Low-Level HTTP & WebSocket
|
|
|
|
For actors that implement `on_request` or `on_websocket`, call them directly on the untyped handle (`handle.inner()`). `fetch` returns a `reqwest::Response`, and `web_socket` returns a `tokio_tungstenite` stream. This example also needs a few extra crates:
|
|
|
|
```sh
|
|
cargo add futures-util tokio-tungstenite
|
|
cargo add reqwest --features json
|
|
```
|
|
|
|
```rust @nocheck
|
|
use counter::Counter;
|
|
use futures_util::{SinkExt, StreamExt};
|
|
use reqwest::{header::HeaderMap, Method};
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
use tokio_tungstenite::tungstenite::Message;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
|
let handle = client.get_or_create_typed_default::<Counter>("counter", ["general"])?;
|
|
|
|
// Raw HTTP request
|
|
let response = handle
|
|
.inner()
|
|
.fetch("history", Method::GET, HeaderMap::new(), None)
|
|
.await?;
|
|
let history: Vec<String> = response.json().await?;
|
|
println!("history: {history:?}");
|
|
|
|
// Raw WebSocket connection
|
|
let mut ws = handle.inner().web_socket("stream", None).await?;
|
|
ws.send(Message::text("hello")).await?;
|
|
if let Some(message) = ws.next().await {
|
|
println!("received: {:?}", message?);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## Calling from Backend
|
|
|
|
The client is a normal Tokio type, so you can hold it in your backend (Axum, Actix, etc.) and call actors from request handlers. The client is `Clone` and cheap to share:
|
|
|
|
```rust @nocheck
|
|
use counter::{Counter, Increment};
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
|
|
async fn increment(client: Client) -> Result<i64> {
|
|
let counter = client.get_or_create_typed_default::<Counter>("counter", ["server-counter"])?;
|
|
let count = counter.send(Increment { amount: 1 }).await?;
|
|
Ok(count)
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
Action and connection calls return `anyhow::Result`. Actor-side errors surface as an `anyhow::Error` carrying the error group, code, message, and metadata:
|
|
|
|
```rust @nocheck
|
|
use counter::{Counter, Increment};
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
|
let counter = client.get_or_create_typed_default::<Counter>("counter", ["my-counter"])?;
|
|
|
|
match counter.send(Increment { amount: 1 }).await {
|
|
Ok(count) => println!("count: {count}"),
|
|
Err(error) => eprintln!("action failed: {error:#}"),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## Concepts
|
|
|
|
### Keys
|
|
|
|
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
|
|
|
```rust @nocheck
|
|
use counter::Counter;
|
|
use rivetkit::{
|
|
client::{Client, ClientConfig},
|
|
prelude::*,
|
|
TypedClientExt,
|
|
};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
|
|
|
|
// Compound key: [org, room]
|
|
let room = client.get_or_create_typed_default::<Counter>("counter", ["org-acme", "general"])?;
|
|
let actor_id = room.inner().resolve().await?;
|
|
println!("Actor ID: {actor_id}");
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
Keys accept arrays of `&str` or `String` (`["org-acme", "general"]`). Don't build keys with string interpolation like `format!("org:{user_id}")` when `user_id` contains user data. Use arrays instead to prevent key injection attacks.
|
|
|
|
### Configuration
|
|
|
|
`ClientConfig::new(endpoint)` is a builder. The endpoint is always required; there is no default. Common options:
|
|
|
|
```rust @nocheck
|
|
use rivetkit::client::ClientConfig;
|
|
|
|
let config = ClientConfig::new("http://localhost:6420")
|
|
.namespace("default")
|
|
.token("pk_...")
|
|
.pool_name("my-pool")
|
|
.header("x-custom", "value");
|
|
```
|
|
|
|
- `namespace` - target namespace (defaults to the engine's configured namespace).
|
|
- `token` - authentication token for the engine.
|
|
- `pool_name` - runner pool to target.
|
|
- `header` / `headers` - extra HTTP headers sent with each request.
|
|
- `max_input_size` - cap on encoded action input size.
|
|
|
|
For serverless deployments, set the endpoint to your app's `/api/rivet` URL. See [Endpoints](/docs/general/endpoints) for details.
|
|
|
|
## API Reference
|
|
|
|
See the full client API documentation on [docs.rs/rivetkit-client](https://docs.rs/rivetkit-client/latest/rivetkit_client/).
|
|
|
|
_Source doc path: /docs/clients/rust_
|