mcpField
R
rust-server

rust-server

github.com/clockworklabs开发25.4k81 次安装今天更新
代码仓库

关于 rust-server

用于实现 SpacetimeDB 服务端模块的 Rust SDK 参考资料,涵盖表结构、Reducer、视图、Procedure、HTTP 和事务,面向 Rust 开发者。

主要功能

  1. 01表结构与列属性
  2. 02Reducer 与生命周期钩子
  3. 03索引查询与数据修改
  4. 04过程式视图与查询构建视图
  5. 05Procedure 与 HTTP 处理器
  6. 06事务与定时表

适用场景

  1. 01定义 Rust 模块的表与索引
  2. 02实现 Reducer 和生命周期钩子
  3. 03创建面向客户端的数据视图
  4. 04添加 HTTP 处理器或 Procedure

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/clockworklabs/spacetimedb --skill rust-server

安装后在 Claude Code 中输入/rust-server确认已加载。

详细介绍

SpacetimeDB Rust SDK 参考

导入

use spacetimedb::{
    procedure, reducer, table, Filter, Identity, ProcedureContext, Query,
    ReducerContext, SpacetimeType, Table, ConnectionId, ScheduleAt,
    TimeDuration, Timestamp, Uuid,
};

必须导入 Table。 否则,ctx.db.*.insert()、.iter()、.find() 等将无法编译(no method named 'insert' found)。

表

在 pub struct 上使用 #[spacetimedb::table(...)]。accessor 必须采用 snake_case:

#[spacetimedb::table(accessor = entity, public)]
pub struct Entity {
    #[primary_key]
    #[auto_inc]
    pub id: u64,
    pub owner: Identity,
    pub name: String,
    #[index(btree)]
    pub active: bool,
}

选项:accessor = snake_case(必需)、public、scheduled(reducer_fn)、index(...)

ctx.db 访问器使用 accessor 名称(snake_case)。

列类型

Rust 类型说明
u8 / u16 / u32 / u64 / u128无符号整数
i8 / i16 / i32 / i64 / i128有符号整数
spacetimedb::sats::u256 / spacetimedb::sats::i256256 位整数
f32 / f64浮点数
bool布尔值
String文本
Vec<T>列表/数组
Identity用户身份
ConnectionId连接句柄
Timestamp服务端时间戳(自纪元起的微秒数)
TimeDuration以微秒为单位的时长
UuidUUID
Option<T>可空列

列属性

#[primary_key]          // primary key
#[auto_inc]             // auto-increment (use 0 as placeholder on insert)
#[unique]               // unique constraint
#[index(btree)]         // btree index (enables .filter() on this column)
#[default(true)]        // migration-safe default for a newly appended column

默认值支持以兼容方式新增并追加字段。不要在主键、唯一列或自增列上使用 #[default(...)]。

索引

单列索引优先内联使用 #[index(btree)]。多列索引使用表级声明:

// Inline (preferred for single-column):
#[index(btree)]
pub author_id: u64,
// Access: ctx.db.post().author_id().filter(author_id)

// Multi-column (table-level):
#[spacetimedb::table(accessor = membership, public,
    index(accessor = by_group_user, btree(columns = [group_id, user_id]))
)]
pub struct Membership { pub group_id: u64, pub user_id: Identity, ... }
// Access: ctx.db.membership().by_group_user().filter((group_id, &user_id))

如果经常按多个列查找行,优先使用多列索引,而不是按单列筛选后循环处理结果。

Reducer

#[spacetimedb::reducer]
pub fn create_entity(ctx: &ReducerContext, name: String) {
    ctx.db.entity().insert(Entity { id: 0, owner: ctx.sender(), name, active: true });
}

// Reducers can return Result<(), String> or Result<(), E> where E: Display
#[spacetimedb::reducer]
pub fn validate_entity(ctx: &ReducerContext, name: String) -> Result<(), String> {
    if name.is_empty() {
        return Err("Name cannot be empty".to_string());
    }
    ctx.db.entity().try_insert(Entity { id: 0, owner: ctx.sender(), name, active: true })?;
    Ok(())
}

注意:违反约束时,insert() 会发生 panic。返回 Result 时,使用带 ? 的 try_insert()。

数据库操作

ctx.db.entity().insert(Entity { id: 0, name: "Sample".into() });  // Insert (0 for autoInc)
ctx.db.entity().id().find(entity_id);                              // Find by PK → Option<Entity>
ctx.db.entity().identity().find(ctx.sender());                     // Find by unique column → Option<Entity>
ctx.db.item().author_id().filter(author_id);                       // Filter by index → iterator
ctx.db.entity().iter();                                            // All rows → iterator
ctx.db.entity().count();                                           // Count rows
ctx.db.entity().id().update(Entity { name: new_name, ..existing }); // Update (override + spread)
ctx.db.entity().id().delete(entity_id);                            // Delete by PK
ctx.db.entity().name().delete("Alice".to_string());                // Delete by indexed String column

注意:iter() 和 filter() 返回迭代器。如果需要使用 .sort()、.filter()、.map(),请收集到 Vec 中。

btree 索引上的范围查询:filter(18..=65)、filter(18..)、filter(..18)。

字符串列访问器使用列自身拥有的 String 类型,而不是 &str。向 find 和 delete 传入 String 或 &String;索引筛选会借用键,例如 ctx.db.product().category().filter(&"hardware".to_string())。

生命周期钩子

#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) { ... }

#[spacetimedb::reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) { ... }

#[spacetimedb::reducer(client_disconnected)]
pub fn on_disconnect(ctx: &ReducerContext) { ... }

当前连接 ID 可通过 ctx.connection_id()(而非公开字段)获取,并且在连接作用域调用之外可能不存在。

视图

// Anonymous view (same result for all clients):
use spacetimedb::{view, AnonymousViewContext};

#[view(accessor = active_users, public)]
fn active_users(ctx: &AnonymousViewContext) -> Vec<Entity> {
    ctx.db.entity().active().filter(true).collect()
}

// Per-user view (result varies by sender):
use spacetimedb::{view, ViewContext};

#[view(accessor = my_profile, public)]
fn my_profile(ctx: &ViewContext) -> Option<Entity> {
    ctx.db.entity().identity().find(ctx.sender())
}

过程式视图的表句柄支持通过索引进行 find 和 filter 访问,但不支持全表 iter()。请从合适的表索引开始构建过程式视图。

在视图属性中声明过程式视图的主键:

#[view(accessor = catalog_entry, public, primary_key = sku)]
fn catalog_entry(ctx: &AnonymousViewContext) -> Vec<CatalogEntry> { ... }

过程式视图主键是显式的架构元数据。仅当视图本身需要公开主键时才添加;视图不会继承源表的主键。

查询构建器视图使用 ViewContext、ctx.from,并返回 impl Query<Row>。使用 filter 设置谓词;当结果应包含与某个左侧行匹配的右侧行时,使用 right_semijoin:

ctx.from.article().filter(|article| article.published.eq(true))
ctx.from.subscription().right_semijoin(
    ctx.from.account(),
    |subscription, account| subscription.account_id.eq(account.id),
)

查询构建器与 String 列进行比较时需要传入拥有所有权的 String,例如 row.label.eq("active".to_string())。

客户端可见性筛选器

use spacetimedb::Filter;

#[spacetimedb::client_visibility_filter]
const PRIVATE_NOTE_FILTER: Filter =
    Filter::Sql("SELECT * FROM owned_row WHERE owner = :sender");

Reducer 上下文 API

ReducerContext(ctx)是发送方身份、时间和随机数的唯一来源;模块中无法使用标准库时钟和随机数生成器。

// Auth: ctx.sender() is the caller's Identity
if row.owner != ctx.sender() {
    panic!("unauthorized");
    // or: return Err(anyhow::anyhow!("unauthorized"));
}

// Server timestamp (deterministic per reducer call)
ctx.db.item().insert(Item { id: 0, owner: ctx.sender(), created_at: ctx.timestamp, .. });

// Timestamp arithmetic
let expiry = ctx.timestamp + TimeDuration::from_micros(delay_micros);

// Deterministic RNG: ctx.random() for a single value, ctx.rng() for the rand::Rng trait
use spacetimedb::rand::Rng;
let n: u32 = ctx.random();                    // random u32
let roll: u32 = ctx.rng().gen_range(1..=6);   // any rand::Rng method

// Client: Timestamp → milliseconds since epoch
timestamp.to_micros_since_unix_epoch() / 1000

定时表

#[spacetimedb::table(accessor = tick_timer, scheduled(tick), public)]
pub struct TickTimer {
    #[primary_key]
    #[auto_inc]
    scheduled_id: u64,
    scheduled_at: spacetimedb::ScheduleAt,
}

#[spacetimedb::reducer]
pub fn tick(ctx: &ReducerContext, timer: TickTimer) {
    // timer row is auto-deleted after this reducer runs
}

// One-time: fires once at a specific time
let at = ScheduleAt::Time(ctx.timestamp + std::time::Duration::from_secs(10));
// Repeating: fires on an interval
let at = ScheduleAt::Interval(std::time::Duration::from_secs(5).into());

ctx.db.tick_timer().insert(TickTimer { scheduled_id: 0, scheduled_at: at });

使用 ConnectionId::from_u128(value),从数字表示构造连接 ID。

流程与 HTTP

流程使用 &mut ProcedureContext,并可以返回类型化的值:

use spacetimedb::{procedure, ProcedureContext, SpacetimeType};

#[derive(SpacetimeType)]
pub struct ResultValue { pub value: String }

#[procedure]
pub fn inspect(_ctx: &mut ProcedureContext, input: String) -> ResultValue {
    ResultValue { value: input }
}

可通过 ctx.http 发起出站 HTTP 请求。get 等便捷方法会返回响应,其他方法则使用 Request::builder() 和 ctx.http.send(request)。response.status() 会返回 StatusCode;使用 is_success() 检查状态,或在需要数值状态码时使用 as_u16()。响应不可克隆,因此请在使用 response.into_body().into_string_lossy() 消耗响应体之前检查状态和标头值。标头值使用可能失败的 to_str() 转换;它们不提供 as_str()。

使用 ctx.with_tx(|tx| ...) 开启短时数据库事务。在回调中通过 tx.db 访问表,而不是直接通过 tx 访问。该方法会直接返回回调的值,因此除非回调本身返回 Result,否则不要对结果调用 unwrap 或 expect。回调实现的是 Fn,因此存储捕获的自有值时请先克隆,而不要将它们移出闭包。请在开启事务前执行网络 I/O。

对于没有便捷方法的出站请求:

let request = Request::builder()
    .method("POST")
    .uri(url)
    .body(Body::from_bytes(data))
    .unwrap();
let response = ctx.http.send(request).expect("request failed");

计划流程使用常规的计划表结构,但计划函数标记为 #[procedure],并接收 &mut ProcedureContext 和计划行。

入站 HTTP 使用处理函数和一个路由器:

use spacetimedb::http::{handler, router, Body, HandlerContext, Request, Response, Router};

#[handler]
fn health(_ctx: &mut HandlerContext, _request: Request) -> Response {
    Response::builder().status(200).body(Body::from_bytes("ok")).unwrap()
}

#[router]
fn routes() -> Router { Router::new().get("/health", health) }

HandlerContext 不会公开 db;请使用 ctx.with_tx(|tx| ...) 开启数据库访问。HTTP 响应体必须拥有其数据,或借用 'static 数据,因此在将动态借用文本传递给 Body::from_bytes 之前,请将其转换为自有的 String。

日志记录

log::info!("Player connected: {:?}", ctx.sender());
log::warn!("Low health: {}", hp);
log::error!("Failed to find entity");

自定义类型

#[derive(SpacetimeType)]
pub enum Status { Online, Away, Offline }

#[derive(SpacetimeType)]
pub struct Point { x: f32, y: f32 }

完整示例

// src/lib.rs
use spacetimedb::{Identity, ReducerContext, SpacetimeType, Table, Timestamp};

#[spacetimedb::table(accessor = entity, public)]
pub struct Entity {
    #[primary_key]
    pub identity: Identity,
    pub name: String,
    pub active: bool,
}

#[spacetimedb::table(accessor = record, public)]
pub struct Record {
    #[primary_key]
    #[auto_inc]
    pub id: u64,
    pub owner: Identity,
    pub value: u32,
    pub created_at: Timestamp,
}

#[spacetimedb::reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) {
    if let Some(existing) = ctx.db.entity().identity().find(ctx.sender()) {
        ctx.db.entity().identity().update(Entity { active: true, ..existing });
    }
}

#[spacetimedb::reducer(client_disconnected)]
pub fn on_disconnect(ctx: &ReducerContext) {
    if let Some(existing) = ctx.db.entity().identity().find(ctx.sender()) {
        ctx.db.entity().identity().update(Entity { active: false, ..existing });
    }
}

#[spacetimedb::reducer]
pub fn create_entity(ctx: &ReducerContext, name: String) {
    if ctx.db.entity().identity().find(ctx.sender()).is_some() {
        panic!("already exists");
    }
    ctx.db.entity().insert(Entity { identity: ctx.sender(), name, active: true });
}

#[spacetimedb::reducer]
pub fn add_record(ctx: &ReducerContext, value: u32) {
    if ctx.db.entity().identity().find(ctx.sender()).is_none() {
        panic!("not found");
    }
    ctx.db.record().insert(Record {
        id: 0,
        owner: ctx.sender(),
        value,
        created_at: ctx.timestamp,
    });
}

评论

支持 Markdown