Rust Quickstart (Preview)
Build a Rivet Actor in Rust
Rust support is in preview. The supported public Rust API is rivetkit and rivetkit-client; lower-level crates are internal implementation details and do not carry a stability guarantee.
Steps
Add Rivet
Add the rivetkit crate:
cargo add rivetkit@2.3.0-rc.12 anyhow async-trait
cargo add serde --features derive
cargo add tokio --features full
Create An Actor And Serve
An actor is a type that implements Actor, plus one Handles<M> implementation for each action. Persisted state lives in type State; ephemeral runtime state is just fields on your actor struct.
use std::{future::Future, pin::Pin, sync::Arc};
use async_trait::async_trait;
use rivetkit::prelude::*;
use serde::{Deserialize, Serialize};
type BoxFuture<T> = Pin<Box<dyn Future<Output = Result<T>> + Send>>;
struct Counter;
#[derive(Default, Serialize, Deserialize)]
struct CounterState {
count: i64,
}
#[derive(Serialize, Deserialize)]
struct Increment {
amount: i64,
}
impl Action for Increment {
type Output = i64;
const NAME: &'static str = "increment";
}
#[derive(Serialize, Deserialize)]
struct NewCount {
count: i64,
}
impl Event for NewCount {
const NAME: &'static str = "newCount";
}
#[async_trait]
impl Actor for Counter {
type State = CounterState;
type Input = ();
type Actions = (Increment,);
type Events = (NewCount,);
type Queue = ();
type ConnParams = ();
type ConnState = ();
type Action = action::Raw;
async fn create_state(_ctx: &Ctx<Self>, _input: Self::Input) -> Result<Self::State> {
Ok(CounterState::default())
}
async fn create(_ctx: &Ctx<Self>) -> Result<Self> {
Ok(Self)
}
}
impl Handles<Increment> for Counter {
type Future = BoxFuture<i64>;
fn handle(self: Arc<Self>, ctx: Ctx<Self>, action: Increment) -> Self::Future {
Box::pin(async move {
let count = {
let mut state = ctx.state_mut();
state.count += action.amount;
state.count
};
ctx.emit(NewCount { count })?;
Ok(count)
})
}
}
#[tokio::main]
async fn main() -> Result<()> {
let mut registry = Registry::new();
registry.register_actor::<Counter>("counter");
registry.start().await
}
Run The Server
The Rust runtime connects to the Rivet Engine. Build the engine binary once, then start your server. RIVET_ENGINE_BINARY_PATH tells the runtime where to find the engine; it spawns or reuses a local engine at http://localhost:6420.
cargo build -p rivet-engine
RIVET_ENGINE_BINARY_PATH=./target/debug/rivet-engine cargo run
Your server now connects to the Rivet Engine on http://localhost:6420. Clients connect directly to the engine on this port.
Connect To The Rivet Actor
This code can run either in your frontend or within your backend:
use anyhow::Result;
use rivetkit::{
client::{Client, ClientConfig, GetOrCreateOptions},
prelude::*,
TypedClientExt,
};
use serde::{Deserialize, Serialize};
struct Counter;
#[derive(Serialize, Deserialize)]
struct Increment {
amount: i64,
}
impl Action for Increment {
type Output = i64;
const NAME: &'static str = "increment";
}
#[derive(Serialize, Deserialize)]
struct NewCount {
count: i64,
}
impl Event for NewCount {
const NAME: &'static str = "newCount";
}
impl Actor for Counter {
type State = ();
type Input = ();
type Actions = (Increment,);
type Events = (NewCount,);
type Queue = ();
type ConnParams = ();
type ConnState = ();
type Action = action::Raw;
}
impl Handles<Increment> for Counter {
type Future = std::future::Ready<Result<i64>>;
fn handle(self: std::sync::Arc<Self>, _ctx: Ctx<Self>, _action: Increment) -> Self::Future {
unreachable!("client-only type marker")
}
}
#[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: 3 }).await?;
println!("New count: {count}");
let connection = counter.connect();
connection
.on::<NewCount>(|event| println!("Count changed: {}", event.count))
.await;
connection.send(Increment { amount: 1 }).await?;
Ok(())
}
See the hello-world-rust example for a complete runnable counter.
A TypeScript client can call your Rust actor by name through the same engine. Actor and action names are resolved at runtime, so the client is untyped here:
import { createClient } from "rivetkit/client";
const client = createClient("http://localhost:6420");
const counter = client.counter.getOrCreate(["my-counter"]);
const count = await counter.increment(3);
console.log("New count:", count);
const connection = counter.connect();
connection.on("newCount", (newCount: { count: number }) => {
console.log("Count changed:", newCount.count);
});
await connection.increment(1);
See the JavaScript client documentation for more information.
import { createRivetKit } from "@rivetkit/react";
import { useState } from "react";
const { useActor } = createRivetKit("http://localhost:6420");
function Counter() {
const [count, setCount] = useState(0);
const counter = useActor({
name: "counter",
key: ["my-counter"],
});
counter.useEvent("newCount", (event: { count: number }) => setCount(event.count));
const increment = async () => {
await counter.connection?.increment(1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
See the React documentation for more information.
Deploy
By default, Rivet stores actor state on the local file system.
To scale Rivet in production, follow a guide to deploy to your hosting provider of choice: