Client Rust RivetKit
Utilise cette skill lors de la création de clients Rust qui se connectent à des Rivet Actors avec rivetkit::client.
Version
Version RivetKit : 2.3.2
Premiers pas
- Ajoute la dépendance
cargo add rivetkit anyhow async-trait cargo add serde --features derive cargo add tokio --features full - Crée un client avec
Client::new(ClientConfig::new(endpoint))et appelle les actions typées avecget_or_create_typed_default::<A>(...).
Politique de gestion des erreurs
- Préfère le comportement fail-fast par défaut. Propage
anyhow::Resultavec?. - Évite d'étouffer les erreurs avec des branches
matchlarges sauf si c'est absolument nécessaire. - Si une erreur est gérée inline, gère-la explicitement, au minimum en la journalisant.
Le support Rust est en bêta. L'API publique Rust supportée est rivetkit et rivetkit::client ; les crates de bas niveau sont des détails d'implémentation interne et ne portent aucune garantie de stabilité. Consulte la référence API complète sur docs.rs/rivetkit, ou l'exemple exécutable hello-world-rust.
Démarrage
Consulte le guide de démarrage rapide Rust pour commencer.
Installation
Ajoute la crate rivetkit et ses compagnons :
cargo add rivetkit anyhow async-trait
cargo add serde --features derive
cargo add tokio --features full
Le client Rust est fortement typé. Il partage les mêmes types d'actions et d'événements que ton actor, donc définis ton actor dans src/lib.rs et importe ces types depuis ton serveur et ton client. Il n'est pas nécessaire de redéfinir l'actor côté client. Consulte Define Your Actor dans le guide de démarrage rapide pour la définition d'actor sur laquelle cette page s'appuie.
Client minimal
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 ici est le nom de ta crate (le name du package dans Cargo.toml, avec des traits d'union en tirets bas). Counter et Increment sont les types que tu as définis aux côtés de ton actor.
Sans état vs avec état
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"])?;
// Sans état : chaque appel est indépendant
counter.send(Increment { amount: 1 }).await?;
// Avec état : garde une connexion ouverte pour les événements en temps réel
let connection = counter.connect();
connection
.on::<NewCount>(|event| println!("count: {}", event.count))
.await;
connection.send(Increment { amount: 1 }).await?;
connection.disconnect().await;
Ok(())
}
Un appel sans état sur le handle ouvre une requête courte durée par action. Une connexion garde un WebSocket ouvert pour que tu puisses recevoir des événements et le réutiliser entre les appels.
Obtenir les Actors
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"));
// Obtiens ou crée un actor
let room = client.get_or_create_typed_default::<Counter>("counter", ["room-42"])?;
// Obtiens un handle d'actor existant (échoue s'il n'existe pas)
let existing = client.get_typed_default::<Counter>("counter", ["room-42"])?;
// Crée un nouvel actor avec l'entrée
let created = client.get_or_create_typed::<Counter>(
"counter",
["game-1"],
GetOrCreateOptions {
create_with_input: Some(json!({ "mode": "ranked" })),
..Default::default()
},
)?;
// Obtiens un handle d'actor par ID
let by_id = client.get_for_id("counter", "actor-id", Default::default())?;
// Résous l'ID de l'actor
let resolved_id = room.inner().resolve().await?;
println!("Resolved ID: {resolved_id}");
Ok(())
}
get_typed_default / get_or_create_typed_default utilisent les options par défaut. Les variantes non-défaut (get_typed / get_or_create_typed) prennent GetOptions / GetOrCreateOptions pour les paramètres de connexion, l'entrée et la région.
Paramètres de connexion
Passe les paramètres de connexion via les options du handle. Ils sont livrés au callback create_conn_state de l'actor :
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(())
}
S'abonner aux événements
on enregistre un callback typé pour un événement et revient une fois que l'abonnement est enregistré :
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(())
}
Les callbacks d'événements sont synchrones et s'exécutent pour chaque événement correspondant. Le type d'événement émis par l'actor (ici NewCount) est décodé en valeur typée pour toi.
Cycle de vie de la connexion
La connexion de bas niveau expose les callbacks de cycle de vie et le statut actuel. Accède-y avec connection.inner() :
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 est l'un de Idle, Connecting, Connected, ou Disconnected. Les connexions se reconnectent automatiquement avec backoff jusqu'à ce que tu appelles disconnect.
HTTP et WebSocket de bas niveau
Pour les actors qui implémentent on_request ou on_websocket, appelle-les directement sur le handle non typé (handle.inner()). fetch retourne une reqwest::Response, et web_socket retourne un stream tokio_tungstenite. Cet exemple a aussi besoin de quelques crates supplémentaires :
cargo add futures-util tokio-tungstenite
cargo add reqwest --features json
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"])?;
// Requête HTTP brute
let response = handle
.inner()
.fetch("history", Method::GET, HeaderMap::new(), None)
.await?;
let history: Vec<String> = response.json().await?;
println!("history: {history:?}");
// Connexion WebSocket brute
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(())
}
Appeler depuis le Backend
Le client est un type Tokio normal, tu peux donc le tenir dans ton backend (Axum, Actix, etc.) et appeler des actors depuis des gestionnaires de requêtes. Le client est Clone et bon marché à partager :
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)
}
Gestion des erreurs
Les appels d'actions et de connexions retournent anyhow::Result. Les erreurs côté actor font surface comme une anyhow::Error portant le groupe d'erreur, le code, le message et les métadonnées :
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
Clés
Les clés identifient de manière unique les instances d'actor. Utilise des clés composées (tableaux) pour l'adressage hiérarchique :
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"));
// Clé composée : [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(())
}
Les clés acceptent des tableaux de &str ou String (["org-acme", "general"]). Ne construis pas de clés avec l'interpolation de chaîne comme format!("org:{user_id}") quand user_id contient des données utilisateur. Utilise plutôt des tableaux pour éviter les attaques par injection de clé.
Configuration
ClientConfig::new(endpoint) est un builder. L'endpoint est toujours requis ; il n'y a pas de défaut. Options courantes :
use rivetkit::client::ClientConfig;
let config = ClientConfig::new("http://localhost:6420")
.namespace("default")
.token("pk_...")
.pool_name("my-pool")
.header("x-custom", "value");
namespace- namespace cible (par défaut celui configuré par le moteur).token- token d'authentification pour le moteur.pool_name- pool de runner à cibler.header/headers- en-têtes HTTP supplémentaires envoyés avec chaque requête.max_input_size- limite sur la taille de l'entrée d'action encodée.
Pour les déploiements serverless, définis l'endpoint à l'URL /api/rivet de ton application. Consulte Endpoints pour les détails.
Référence API
Consulte la documentation API client complète sur docs.rs/rivetkit-client.
Besoin de plus que le Client ?
Si tu as besoin de plus sur les Rivet Actors, les registries, ou le RivetKit côté serveur, ajoute la skill principale :
npx skills add rivet-dev/skills
Puis utilise la skill rivetkit pour des conseils sur le backend.