rust-server
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSpacetimeDB Rust SDK Reference
SpacetimeDB Rust SDK 参考
Imports
导入
rust
use spacetimedb::{
procedure, reducer, table, Filter, Identity, ProcedureContext, Query,
ReducerContext, SpacetimeType, Table, ConnectionId, ScheduleAt,
TimeDuration, Timestamp, Uuid,
};Tablectx.db.*.insert().iter().find()no method named 'insert' foundrust
use spacetimedb::{
procedure, reducer, table, Filter, Identity, ProcedureContext, Query,
ReducerContext, SpacetimeType, Table, ConnectionId, ScheduleAt,
TimeDuration, Timestamp, Uuid,
};Tablectx.db.*.insert().iter().find()no method named 'insert' foundTables
表(Tables)
#[spacetimedb::table(...)]pub structaccessorrust
#[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,
}Options: (required), , ,
accessor = snake_casepublicscheduled(reducer_fn)index(...)ctx.dbaccessor在 上使用 。 必须采用 snake_case:
pub struct#[spacetimedb::table(...)]accessorrust
#[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_casepublicscheduled(reducer_fn)index(...)ctx.dbaccessorColumn Types
列类型(Column Types)
| Rust type | Notes |
|---|---|
| unsigned integers |
| signed integers |
| 256-bit integers |
| floats |
| boolean |
| text |
| list/array |
| user identity |
| connection handle |
| server timestamp (microseconds since epoch) |
| duration in microseconds |
| UUID |
| nullable column |
| Rust 类型 | 说明 |
|---|---|
| 无符号整数 |
| 有符号整数 |
| 256 位整数 |
| 浮点数 |
| 布尔值 |
| 文本 |
| 列表/数组 |
| 用户身份 |
| 连接句柄 |
| 服务端时间戳(自 epoch 起的微秒数) |
| 以微秒为单位的时长 |
| UUID |
| 可空列 |
Column Attributes
列属性(Column Attributes)
rust
#[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 columnDefaults support compatible addition of a newly appended field. Do not place on primary-key, unique, or auto-increment columns.
#[default(...)]rust
#[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默认值支持以兼容方式新增追加字段。不要在 primary-key、unique 或 auto-increment 列上使用 。
#[default(...)]Indexes
索引(Indexes)
Prefer inline for single-column. Multi-column uses table-level:
#[index(btree)]rust
// 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))When you frequently look up rows by multiple columns, prefer a multi-column index over filtering by one column and looping over the results.
单列优先内联使用 。多列使用表级索引:
#[index(btree)]rust
// 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))如果经常按多个列查找行,应优先使用多列索引,而不是按单个列过滤后遍历结果。
Reducers
Reducers(归约器)
rust
#[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(())
}Note: panics on constraint violations. Use with when returning .
insert()try_insert()?Resultrust
#[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(())
}注意: 在违反约束时会 panic。返回 时,请使用 配合 。
insert()Resulttry_insert()?DB Operations
数据库操作(DB Operations)
rust
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 columnNote: and return iterators. Collect to Vec if you need , , .
iter()filter().sort().filter().map()Range queries on btree indexes: , , .
filter(18..=65)filter(18..)filter(..18)String column accessors operate on the column's owned type, not . Pass a or to and ; index filters borrow the key, as in .
String&strString&Stringfinddeletectx.db.product().category().filter(&"hardware".to_string())rust
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()Vecbtree 索引支持范围查询:、、。
filter(18..=65)filter(18..)filter(..18)String 列访问器操作的是该列拥有的 类型,而不是 。向 和 传入 或 ;索引过滤器会借用键,例如 。
String&strfinddeleteString&Stringctx.db.product().category().filter(&"hardware".to_string())Lifecycle Hooks
生命周期钩子(Lifecycle Hooks)
rust
#[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) { ... }The current connection ID is available through (not a public field) and may be absent outside connection-scoped calls.
ctx.connection_id()rust
#[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()Views
视图(Views)
rust
// 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())
}Procedural-view table handles support indexed and access, but not full-table . Start a procedural view from an appropriate table index.
findfilteriter()Declare a procedural view primary key in the view attribute:
rust
#[view(accessor = catalog_entry, public, primary_key = sku)]
fn catalog_entry(ctx: &AnonymousViewContext) -> Vec<CatalogEntry> { ... }Procedural-view primary keys are explicit schema metadata. Add one only when the view itself is required to expose a primary key; a source table's primary key is not inherited by the view.
Query-builder views use , , and return . Use for predicates and when the result should contain right-side rows that have a matching left-side row:
ViewContextctx.fromimpl Query<Row>filterright_semijoinrust
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),
)Query-builder comparisons against a column take an owned , for example .
StringStringrow.label.eq("active".to_string())Views can also accept arguments (2.13+), after the context:
rust
#[spacetimedb::view(accessor = entity_by_id, public)]
pub fn entity_by_id(ctx: &spacetimedb::AnonymousViewContext, id: u64) -> Option<Entity> {
ctx.db.entity().id().find(id)
}Floating-point arguments and primary keys are rejected on argument-bearing views. See client skills for SQL subscription syntax and restrictions.
rust
// 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())
}过程式视图的表句柄支持通过索引进行 和 访问,但不支持对整表执行 。过程式视图应从合适的表索引开始。
findfilteriter()在视图属性中声明过程式视图的 primary key:
rust
#[view(accessor = catalog_entry, public, primary_key = sku)]
fn catalog_entry(ctx: &AnonymousViewContext) -> Vec<CatalogEntry> { ... }过程式视图的 primary key 是显式 schema 元数据。仅当视图本身需要公开 primary key 时才添加;源表的 primary key 不会被视图继承。
Query-builder 视图使用 、,并返回 。使用 表示谓词;当结果应包含存在匹配左侧行的右侧行时,使用 :
ViewContextctx.fromimpl Query<Row>filterright_semijoinrust
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),
)Query-builder 对 列进行比较时需传入拥有的 ,例如 。
StringStringrow.label.eq("active".to_string())视图也可以接受参数(2.13+),参数位于 context 之后:
rust
#[spacetimedb::view(accessor = entity_by_id, public)]
pub fn entity_by_id(ctx: &spacetimeb::AnonymousViewContext, id: u64) -> Option<Entity> {
ctx.db.entity().id().find(id)
}带参数的视图会拒绝浮点参数和 primary key。有关 SQL 订阅语法和限制,请参阅 client skills。
Client Visibility Filters
客户端可见性过滤器(Client Visibility Filters)
rust
use spacetimedb::Filter;
#[spacetimedb::client_visibility_filter]
const PRIVATE_NOTE_FILTER: Filter =
Filter::Sql("SELECT * FROM owned_row WHERE owner = :sender");rust
use spacetimedb::Filter;
#[spacetimedb::client_visibility_filter]
const PRIVATE_NOTE_FILTER: Filter =
Filter::Sql("SELECT * FROM owned_row WHERE owner = :sender");Reducer Context API
Reducer Context API(Reducer 上下文 API)
ReducerContextctxrust
// 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() / 1000ReducerContextctxrust
// 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() / 1000Environment Variables
环境变量(Environment Variables)
rust
#[spacetimedb::env]
pub struct Env {
pub API_KEY: String,
pub LOG_LEVEL: Option<String>,
}
// Inside a root entry point:
// ctx.env.API_KEY() -> String
// ctx.env.LOG_LEVEL() -> Option<String>
// ctx.env.get("LOG_LEVEL") -> Option<String>For constrained values, derive on a payload-free enum and use it as a field type (or ). on a variant supplies its exact accepted string; without it, the Rust variant name is used. also constrains / fields.
spacetimedb::EnvironmentValueOption<Enum>#[env(value = "production")]#[env(values("info", "debug"))]StringOption<String>See the concepts skill for shared environment and secret-handling rules.
rust
#[spacetimedb::env]
pub struct Env {
pub API_KEY: String,
pub LOG_LEVEL: Option<String>,
}
// Inside a root entry point:
// ctx.env.API_KEY() -> String
// ctx.env.LOG_LEVEL() -> Option<String>
// ctx.env.get("LOG_LEVEL") -> Option<String>对于受约束的值,在无 payload 的 enum 上派生 ,并将其用作字段类型(或 )。enum 变体上的 指定其唯一可接受的字符串;未指定时使用 Rust 变体名。 还会约束 / 字段。
spacetimedb::EnvironmentValueOption<Enum>#[env(value = "production")]#[env(values("info", "debug"))]StringOption<String>有关共享环境和 secret 处理规则,请参阅 concepts skill。
Scheduled Tables
定时表(Scheduled Tables)
rust
#[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 });Construct a connection ID from a numeric representation with .
ConnectionId::from_u128(value)rust
#[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 });使用 可从数值表示构造连接 ID。
ConnectionId::from_u128(value)Inter-Database Calls (Async IDC)
数据库间调用(Async IDC)
A reducer calls a reducer in another database by inserting a row into an outbox table. The call is sent only if the transaction commits, runs exactly once, and runs in order with other calls through the same outbox to the same database. The sending reducer doesn't wait for it. Both databases must be on the same server.
Generate receiver bindings by listing the receiving module under in the sending database's , then running . Rust bindings are written to .
dependenciesspacetime.jsonspacetime generate <sending-database>src/remote_bindings/json
{
"database": "lobby",
"module-path": "./lobby",
"dependencies": [{ "name": "game_world", "database": "game-world" }],
"children": [{ "database": "game-world", "module-path": "./game_world" }]
}rust
mod remote_bindings;
use remote_bindings::game_world;
// Each row is one call to game_world's `admit_player(name: String)` reducer.
#[spacetimedb::table(accessor = admit_player_outbox, outbox(game_world::admit_player))]
pub struct AdmitPlayerOutbox {
#[primary_key]
#[auto_inc]
msg_id: u64, // exactly one u64 auto_inc primary key
#[target]
world: game_world::Identity, // exactly one target, typed by the bindings
name: String, // one column per reducer parameter, same name and type
// #[param(name = name)] player_name: String, // alias a column to a parameter
// Extra columns are allowed. They aren't sent, but on_result can read them.
}
#[spacetimedb::reducer]
pub fn send_to_world(ctx: &ReducerContext, name: String, world: game_world::Identity) {
// Sent if and only if this transaction commits.
ctx.db.admit_player_outbox().insert(AdmitPlayerOutbox { msg_id: 0, world, name });
}
// Optional, at most one per outbox. Private. Runs once per row, then the row is deleted.
#[spacetimedb::reducer(on_result(admit_player_outbox))]
pub fn on_admit_player_result(ctx: &ReducerContext, row: AdmitPlayerOutbox, result: Result<(), String>) {
if let Err(error) = result {
log::warn!("{} was not admitted: {error}", row.name);
}
}- The receiving reducer is an ordinary reducer and needs no changes. In it, is the sending database's identity, so authorize calls by checking it against an allow-list table.
ctx.sender() - The receiving database runs and
client_connectedaround every delivered call, with no JWT. Ifclient_disconnectedrejects the sending database, the call is retried forever.client_connected - Outbox rows are insert-only, so a pending call can't be cancelled. A call is never retried once it has a result. To try again, insert a new row, for example from .
on_result - Wrap a plain to get a target:
Identity.game_world::Identity(identity) - Index lookups (,
find) on afiltercolumn don't compile. Iterate and compare instead.game_world::Identity - An outbox table can't also be a scheduled table or an event table.
- After the receiving reducer's parameters change, regenerate the bindings and republish the sending database. Calls built from old bindings fail with a signature mismatch error.
See https://spacetimedb.com/docs/databases/inter-database-communication for details.
reducer 通过向 outbox table 插入一行来调用另一数据库中的 reducer。仅当事务提交时才会发送该调用;调用恰好执行一次,并与通过同一 outbox 发往同一数据库的其他调用按顺序执行。发送方 reducer 不会等待它。两个数据库必须位于同一服务器上。
要生成接收端 bindings,请在发送数据库的 中将接收模块列在 下,然后运行 。Rust bindings 会写入 。
spacetime.jsondependenciesspacetime generate <sending-database>src/remote_bindings/json
{
"database": "lobby",
"module-path": "./lobby",
"dependencies": [{ "name": "game_world", "database": "game-world" }],
"children": [{ "database": "game-world", "module-path": "./game_world" }]
}rust
mod remote_bindings;
use remote_bindings::game_world;
// Each row is one call to game_world's `admit_player(name: String)` reducer.
#[spacetimedb::table(accessor = admit_player_outbox, outbox(game_world::admit_player))]
pub struct AdmitPlayerOutbox {
#[primary_key]
#[auto_inc]
msg_id: u64, // exactly one u64 auto_inc primary key
#[target]
world: game_world::Identity, // exactly one target, typed by the bindings
name: String, // one column per reducer parameter, same name and type
// #[param(name = name)] player_name: String, // alias a column to a parameter
// Extra columns are allowed. They aren't sent, but on_result can read them.
}
#[spacetimedb::reducer]
pub fn send_to_world(ctx: &ReducerContext, name: String, world: game_world::Identity) {
// Sent if and only if this transaction commits.
ctx.db.admit_player_outbox().insert(AdmitPlayerOutbox { msg_id: 0, world, name });
}
// Optional, at most one per outbox. Private. Runs once per row, then the row is deleted.
#[spacetimedb::reducer(on_result(admit_player_outbox))]
pub fn on_admit_player_result(ctx: &ReducerContext, row: AdmitPlayerOutbox, result: Result<(), String>) {
if let Err(error) = result {
log::warn!("{} was not admitted: {error}", row.name);
}
}- 接收端 reducer 是普通 reducer,无需修改。在其中,是发送数据库的身份,因此应通过与 allow-list 表进行核对来授权调用。
ctx.sender() - 接收数据库会围绕每次送达的调用执行 和
client_connected,且不携带 JWT。如果client_disconnected拒绝发送数据库,该调用将被无限重试。client_connected - Outbox 行仅支持插入,因此无法取消待处理调用。调用一旦产生结果就绝不会重试。如需重试,请插入新行,例如从 中插入。
on_result - 用普通 包装即可获得 target:
Identity。game_world::Identity(identity) - 列上的索引查找(
game_world::Identity、find)无法编译。请改为遍历并比较。filter - Outbox table 不能同时是 scheduled table 或 event table。
- 接收端 reducer 的参数发生变化后,请重新生成 bindings 并重新发布发送数据库。使用旧 bindings 构造的调用会因 signature mismatch 而失败。
Procedures and HTTP
Procedures 与 HTTP
Procedures use and may return typed values:
&mut ProcedureContextrust
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 }
}Outbound HTTP is available through . Convenience methods such as return a response, and other methods use with . returns a ; use to test it or when a numeric status is needed. Responses are not cloneable, so inspect status and headers before consuming the body with . Header values use the fallible conversion; they do not provide .
ctx.httpgetRequest::builder()ctx.http.send(request)response.status()StatusCodeis_success()as_u16()response.into_body().into_string_lossy()to_str()as_str()Open short database transactions with . Access tables inside the callback through , not directly on . It returns the callback's value directly, so do not call or on the result unless the callback itself returns a . The callback implements , so clone captured owned values when storing them rather than moving them out of the closure. Perform network I/O before opening the transaction.
ctx.with_tx(|tx| ...)tx.dbtxunwrapexpectResultFnFor an outbound request without a convenience method:
rust
let request = Request::builder()
.method("POST")
.uri(url)
.body(Body::from_bytes(data))
.unwrap();
let response = ctx.http.send(request).expect("request failed");Scheduled procedures use the ordinary scheduled-table shape, but the scheduled function is marked and receives plus the scheduled row.
#[procedure]&mut ProcedureContextInbound HTTP uses handler functions and one router:
rust
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) }HandlerContextdbctx.with_tx(|tx| ...)'staticStringBody::from_bytesProcedures 使用 ,并且可以返回类型化值:
&mut ProcedureContextrust
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 }
}可通过 发起出站 HTTP 请求。 等便捷方法会返回 response,其他方法使用 配合 。 返回 ;可使用 测试,或在需要数值状态时使用 。Response 不可 clone,因此应在消费 body 之前检查 status 和 headers,可使用 。Header 值使用可失败的 转换;它们不提供 。
ctx.httpgetRequest::builder()ctx.http.send(request)response.status()StatusCodeis_success()as_u16()response.into_body().into_string_lossy()to_str()as_str()使用 开启短数据库事务。在 callback 内通过 访问 table,而不是直接通过 访问。它会直接返回 callback 的值,因此除非 callback 自身返回 ,否则不要对结果调用 或 。Callback 实现 ,因此存储捕获的 owned 值时应 clone 该值,而不是将其移出 closure。应在开启事务前执行网络 I/O。
ctx.with_tx(|tx| ...)tx.dbtxResultunwrapexpectFn对于没有便捷方法的出站请求:
rust
let request = Request::builder()
.method("POST")
.uri(url)
.body(Body::from_bytes(data))
.unwrap();
let response = ctx.http.send(request).expect("request failed");定时 procedures 使用普通 scheduled-table 结构,但定时函数标记为 ,并接收 及 scheduled row。
#[procedure]&mut ProcedureContext入站 HTTP 使用 handler functions 和单个 router:
rust
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) }HandlerContextdbctx.with_tx(|tx| ...)'staticStringBody::from_bytesLogging
日志(Logging)
rust
log::info!("Player connected: {:?}", ctx.sender());
log::warn!("Low health: {}", hp);
log::error!("Failed to find entity");rust
log::info!("Player connected: {:?}", ctx.sender());
log::warn!("Low health: {}", hp);
log::error!("Failed to find entity");Custom Types
自定义类型(Custom Types)
rust
#[derive(SpacetimeType)]
pub enum Status { Online, Away, Offline }
#[derive(SpacetimeType)]
pub struct Point { x: f32, y: f32 }rust
#[derive(SpacetimeType)]
pub enum Status { Online, Away, Offline }
#[derive(SpacetimeType)]
pub struct Point { x: f32, y: f32 }Complete Example
完整示例(Complete Example)
rust
// 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,
});
}rust
// 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,
});
}Module Unit Tests
模块单元测试(Module Unit Tests)
The native in-memory harness is available in 2.12+. Keep the same version in normal and dev dependencies, enabling only for tests:
spacetimedbtest-utilstoml
[dependencies]
spacetimedb = { version = "2.13.0", features = ["unstable"] }
[dev-dependencies]
spacetimedb = { version = "2.13.0", features = ["test-utils"] }
icu_calendar = ">=2.1, <2.2" # temporal_rs requires icu_calendar 2.1.xRun natively, not with a WebAssembly target. For the reducer above:
cargo testcreate_entityrust
#[cfg(test)]
mod tests {
use super::*;
use spacetimedb::test_utils::{TestAuth, TestContext};
#[test]
fn inserts_entity() {
let test = TestContext::new().expect("test database");
test.with_reducer_tx(TestAuth::internal(), |ctx| {
create_entity(ctx, "Alice".to_owned());
Ok::<(), String>(())
}).expect("transaction");
assert_eq!(test.db.entity().count(), 1);
}
}with_reducer_txOkErr?ResultTestContextTestAuth::from_jwt_payload(payload, connection_id)?TestAuth::internal()clockrngSee https://spacetimedb.com/docs/how-to/unit-testing for test setup, HTTP mocking, and current limitations.
原生内存测试 harness 从 2.12+ 开始可用。在常规依赖和开发依赖中保持相同的 版本,仅为测试启用 :
spacetimedbtest-utilstoml
[dependencies]
spacetimedb = { version = "2.13.0", features = ["unstable"] }
[dev-dependencies]
spacetimedb = { version = "2.13.0", features = ["test-utils"] }
icu_calendar = ">=2.1, <2.2" # temporal_rs requires icu_calendar 2.1.x原生运行 ,不要使用 WebAssembly target。对于上面的 reducer:
cargo testcreate_entityrust
#[cfg(test)]
mod tests {
use super::*;
use spacetimedb::test_utils::{TestAuth, TestContext};
#[test]
fn inserts_entity() {
let test = TestContext::new().expect("test database");
test.with_reducer_tx(TestAuth::internal(), |ctx| {
create_entity(ctx, "Alice".to_owned());
Ok::<(), String>(())
}).expect("transaction");
assert_eq!(test.db.entity().count(), 1);
}
}with_reducer_txOkErrResult?TestContextTestAuth::from_jwt_payload(payload, connection_id)?TestAuth::internal()clockrng有关测试设置、HTTP mocking 和当前限制,请参阅 https://spacetimedb.com/docs/how-to/unit-testing。