spacetimedb-rust

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SpacetimeDB Rust Module Development

SpacetimeDB Rust模块开发

SpacetimeDB modules are WebAssembly applications that run inside the database. They define tables to store data and reducers to modify data. Clients connect directly to the database and execute application logic inside it.
Tested with: SpacetimeDB runtime 1.11.x,
spacetimedb
crate 1.1.x

SpacetimeDB模块是运行在数据库内部的WebAssembly应用。它们定义用于存储数据的表和用于修改数据的reducer。客户端直接连接到数据库并在其中执行应用逻辑。
已测试版本: SpacetimeDB runtime 1.11.x,
spacetimedb
crate 1.1.x

HALLUCINATED APIs — DO NOT USE

幻觉API — 请勿使用

These APIs DO NOT EXIST. LLMs frequently hallucinate them.
rust
// WRONG — these macros/attributes don't exist
#[spacetimedb::table]           // Use #[table] after importing
#[spacetimedb::reducer]         // Use #[reducer] after importing
#[derive(Table)]                // Tables use #[table] attribute, not derive
#[derive(Reducer)]              // Reducers use #[reducer] attribute

// WRONG — SpacetimeType on tables
#[derive(SpacetimeType)]        // DO NOT use on #[table] structs!
#[table(name = my_table)]
pub struct MyTable { ... }

// WRONG — mutable context
pub fn my_reducer(ctx: &mut ReducerContext, ...) { }  // Should be &ReducerContext

// WRONG — table access without parentheses
ctx.db.player                   // Should be ctx.db.player()
ctx.db.player.find(id)          // Should be ctx.db.player().id().find(&id)
以下API并不存在。大语言模型经常会编造这些API。
rust
// 错误示例 — 这些宏/属性不存在
#[spacetimedb::table]           // 导入后请使用#[table]
#[spacetimedb::reducer]         // 导入后请使用#[reducer]
#[derive(Table)]                // 表使用#[table]属性,而非derive
#[derive(Reducer)]              // Reducer使用#[reducer]属性

// 错误示例 — 为表添加SpacetimeType
#[derive(SpacetimeType)]        // 请勿在#[table]结构体上使用!
#[table(name = my_table)]
pub struct MyTable { ... }

// 错误示例 — 可变上下文
pub fn my_reducer(ctx: &mut ReducerContext, ...) { }  // 应该使用&ReducerContext

// 错误示例 — 不带括号的表访问
ctx.db.player                   // 应该是ctx.db.player()
ctx.db.player.find(id)          // 应该是ctx.db.player().id().find(&id)

CORRECT PATTERNS:

正确写法:

rust
// CORRECT IMPORTS
use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp};
use spacetimedb::SpacetimeType;  // Only for custom types, NOT tables

// CORRECT TABLE — no SpacetimeType derive!
#[table(name = player, public)]
pub struct Player {
    #[primary_key]
    pub id: u64,
    pub name: String,
}

// CORRECT REDUCER — immutable context reference
#[reducer]
pub fn create_player(ctx: &ReducerContext, name: String) {
    ctx.db.player().insert(Player { id: 0, name });
}

// CORRECT TABLE ACCESS — methods with parentheses
let player = ctx.db.player().id().find(&player_id);
rust
// 正确导入
use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp};
use spacetimedb::SpacetimeType;  // 仅用于自定义类型,不用于表

// 正确的表定义 — 无需派生SpacetimeType!
#[table(name = player, public)]
pub struct Player {
    #[primary_key]
    pub id: u64,
    pub name: String,
}

// 正确的Reducer — 不可变上下文引用
#[reducer]
pub fn create_player(ctx: &ReducerContext, name: String) {
    ctx.db.player().insert(Player { id: 0, name });
}

// 正确的表访问 — 使用带括号的方法
let player = ctx.db.player().id().find(&player_id);

DO NOT:

禁止操作:

  • Derive
    SpacetimeType
    on
    #[table]
    structs
    — the macro handles this
  • Use mutable context
    &ReducerContext
    , not
    &mut ReducerContext
  • Forget
    Table
    trait import
    — required for table operations
  • Use field access for tables
    ctx.db.player()
    not
    ctx.db.player

  • #[table]
    结构体上派生
    SpacetimeType
    — 宏会自动处理
  • 使用可变上下文 — 应使用
    &ReducerContext
    ,而非
    &mut ReducerContext
  • 忘记导入
    Table
    trait
    — 表操作必须导入该trait
  • 使用字段访问表 — 应使用
    ctx.db.player()
    而非
    ctx.db.player

Common Mistakes Table

常见错误对照表

Server-side errors

服务端错误

WrongRightError
#[derive(SpacetimeType)]
on
#[table]
Remove it — macro handles thisConflicting derive macros
ctx.db.player
(field access)
ctx.db.player()
(method)
"no field
player
on type"
ctx.db.player().find(id)
ctx.db.player().id().find(&id)
Must access via index
&mut ReducerContext
&ReducerContext
Wrong context type
Missing
use spacetimedb::Table;
Add import"no method named
insert
"
#[table(name = "my_table")]
#[table(name = my_table)]
String literals not allowed
Missing
public
on table
Add
public
flag
Clients can't subscribe
#[spacetimedb::reducer]
#[reducer]
after import
Wrong attribute path
Network/filesystem in reducerUse procedures insteadSandbox violation
Panic for expected errorsReturn
Result<(), String>
WASM instance destroyed
错误写法正确写法错误原因
#[table]
上使用
#[derive(SpacetimeType)]
移除该派生 — 宏会自动处理派生宏冲突
ctx.db.player
(字段访问)
ctx.db.player()
(方法调用)
"类型中不存在字段
player
"
ctx.db.player().find(id)
ctx.db.player().id().find(&id)
必须通过索引访问
&mut ReducerContext
&ReducerContext
上下文类型错误
缺少
use spacetimedb::Table;
添加该导入"不存在名为
insert
的方法"
#[table(name = "my_table")]
#[table(name = my_table)]
不允许使用字符串字面量
表上缺少
public
标识
添加
public
标识
客户端无法订阅
#[spacetimedb::reducer]
导入后使用
#[reducer]
属性路径错误
在reducer中进行网络/文件系统操作使用procedure替代沙箱违规
预期错误触发panic返回
Result<(), String>
WASM实例被销毁

Client-side errors

客户端错误

WrongRightError
Wrong crate name
spacetimedb-sdk
Dependency not found
Manual event loopUse
tokio
runtime
Async issues

错误写法正确写法错误原因
错误的crate名称
spacetimedb-sdk
依赖未找到
手动事件循环使用
tokio
运行时
异步问题

Hard Requirements

硬性要求

  1. DO NOT derive
    SpacetimeType
    on
    #[table]
    structs
    — the macro handles this
  2. Import
    Table
    trait
    — required for all table operations
  3. Use
    &ReducerContext
    — not
    &mut ReducerContext
  4. Tables are methods
    ctx.db.table()
    not
    ctx.db.table
  5. Reducers must be deterministic — no filesystem, network, timers, or external RNG
  6. Use
    ctx.random()
    or
    ctx.rng
    — not
    rand
    crate for random numbers
  7. Add
    public
    flag
    — if clients need to subscribe to a table

  1. 禁止在
    #[table]
    结构体上派生
    SpacetimeType
    — 宏会自动处理
  2. 必须导入
    Table
    trait
    — 所有表操作都需要它
  3. 使用
    &ReducerContext
    — 而非
    &mut ReducerContext
  4. 表是方法调用 — 使用
    ctx.db.table()
    而非
    ctx.db.table
  5. Reducer必须是确定性的 — 不能使用文件系统、网络、定时器或外部随机数生成器
  6. 使用
    ctx.random()
    ctx.rng
    — 不要使用
    rand
    crate生成随机数
  7. 添加
    public
    标识
    — 如果客户端需要订阅该表

Project Setup

项目设置

Cargo.toml Requirements

Cargo.toml要求

toml
[package]
name = "my-module"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
spacetimedb = "1.0"
log = "0.4"
The
crate-type = ["cdylib"]
is required for WebAssembly compilation.
toml
[package]
name = "my-module"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
spacetimedb = "1.0"
log = "0.4"
crate-type = ["cdylib"]
是WebAssembly编译的必要配置。

Essential Imports

核心导入

rust
use spacetimedb::{ReducerContext, Table};
Additional imports as needed:
rust
use spacetimedb::{Identity, Timestamp, ConnectionId, ScheduleAt};
use spacetimedb::sats::{i256, u256};  // For 256-bit integers
rust
use spacetimedb::{ReducerContext, Table};
根据需要添加额外导入:
rust
use spacetimedb::{Identity, Timestamp, ConnectionId, ScheduleAt};
use spacetimedb::sats::{i256, u256};  // 用于256位整数

Table Definitions

表定义

Tables store data in SpacetimeDB. Define tables using the
#[spacetimedb::table]
macro on a struct.
表用于在SpacetimeDB中存储数据。通过在结构体上使用
#[spacetimedb::table]
宏来定义表。

Basic Table

基础表

rust
#[spacetimedb::table(name = player, public)]
pub struct Player {
    #[primary_key]
    #[auto_inc]
    id: u64,
    name: String,
    score: u32,
}
rust
#[spacetimedb::table(name = player, public)]
pub struct Player {
    #[primary_key]
    #[auto_inc]
    id: u64,
    name: String,
    score: u32,
}

Table Attributes

表属性

AttributeDescription
name = identifier
Required. The table name used in
ctx.db.{name}()
public
Makes table visible to clients via subscriptions
scheduled(reducer_name)
Creates a schedule table that triggers the named reducer
index(name = idx, btree(columns = [a, b]))
Creates a multi-column index
属性描述
name = identifier
必填项。在
ctx.db.{name}()
中使用的表名
public
使表可通过订阅对客户端可见
scheduled(reducer_name)
创建一个调度表,触发指定名称的reducer
index(name = idx, btree(columns = [a, b]))
创建多列索引

Column Attributes

列属性

AttributeDescription
#[primary_key]
Unique identifier for the row (one per table max)
#[unique]
Enforces uniqueness, enables
find()
method
#[auto_inc]
Auto-generates unique integer values when inserting 0
#[index(btree)]
Creates a B-tree index for efficient lookups
#[default(value)]
Default value for migrations (must be const-evaluable)
属性描述
#[primary_key]
行的唯一标识符(每个表最多一个)
#[unique]
强制唯一性,启用
find()
方法
#[auto_inc]
插入0时自动生成唯一整数值
#[index(btree)]
创建B树索引以提高查询效率
#[default(value)]
迁移时的默认值(必须是可常量计算的)

Supported Column Types

支持的列类型

Primitives:
u8
,
u16
,
u32
,
u64
,
u128
,
u256
,
i8
,
i16
,
i32
,
i64
,
i128
,
i256
,
f32
,
f64
,
bool
,
String
SpacetimeDB Types:
Identity
,
ConnectionId
,
Timestamp
,
Uuid
,
ScheduleAt
Collections:
Vec<T>
,
Option<T>
,
Result<T, E>
where inner types are also supported
Custom Types: Any struct/enum with
#[derive(SpacetimeType)]
基本类型
u8
,
u16
,
u32
,
u64
,
u128
,
u256
,
i8
,
i16
,
i32
,
i64
,
i128
,
i256
,
f32
,
f64
,
bool
,
String
SpacetimeDB类型
Identity
,
ConnectionId
,
Timestamp
,
Uuid
,
ScheduleAt
集合类型
Vec<T>
,
Option<T>
,
Result<T, E>
(内部类型也必须支持)
自定义类型:任何派生了
#[derive(SpacetimeType)]
的结构体/枚举

Insert Returns the Row

插入操作返回插入的行

rust
// Insert and get the auto-generated ID
let row = ctx.db.task().insert(Task {
    id: 0,  // Placeholder for auto_inc
    owner_id: ctx.sender,
    title: "New task".to_string(),
    created_at: ctx.timestamp,
});
let new_id = row.id;  // Get the actual ID

rust
// 插入并获取自动生成的ID
let row = ctx.db.task().insert(Task {
    id: 0,  // auto_inc会填充该值
    owner_id: ctx.sender,
    title: "New task".to_string(),
    created_at: ctx.timestamp,
});
let new_id = row.id;  // 获取实际生成的ID

Data Visibility and Row-Level Security

数据可见性与行级安全性

public
flag exposes ALL rows to ALL clients.
ScenarioPattern
Everyone sees all rows
#[table(name = x, public)]
Users see only their dataPrivate table + row-level security
public
标识会将所有行暴露给所有客户端。
场景实现方式
所有人可见所有行
#[table(name = x, public)]
用户仅能看到自己的数据私有表 + 行级安全性

Private Table (default)

私有表(默认)

rust
// No public flag — only server can read
#[table(name = secret_data)]
pub struct SecretData { ... }
rust
// 无public标识 — 仅服务器可读取
#[table(name = secret_data)]
pub struct SecretData { ... }

Row-Level Security (RLS)

行级安全性(RLS)

Use RLS to filter which rows each client can see:
rust
// Use row-level security for per-user visibility
#[table(name = player_data, public)]
#[rls(filter = |ctx, row| row.owner_id == ctx.sender)]
pub struct PlayerData {
    #[primary_key]
    pub id: u64,
    pub owner_id: Identity,
    pub data: String,
}
With RLS, clients can subscribe to the table but only see rows where the filter returns
true
for their identity.

使用RLS过滤每个客户端能看到的行:
rust
// 使用行级安全性实现用户级可见性
#[table(name = player_data, public)]
#[rls(filter = |ctx, row| row.owner_id == ctx.sender)]
pub struct PlayerData {
    #[primary_key]
    pub id: u64,
    pub owner_id: Identity,
    pub data: String,
}
启用RLS后,客户端可以订阅表,但只能看到过滤器对其身份返回
true
的行。

Reducers

Reducer

Reducers are transactional functions that modify database state. They run inside the database and are the only way to mutate tables.
Reducer是修改数据库状态的事务性函数。它们运行在数据库内部,是唯一可以修改表的方式。

Basic Reducer

基础Reducer

rust
#[spacetimedb::reducer]
pub fn create_player(ctx: &ReducerContext, name: String) -> Result<(), String> {
    if name.is_empty() {
        return Err("Name cannot be empty".to_string());
    }

    ctx.db.player().insert(Player {
        id: 0,  // auto_inc assigns the value
        name,
        score: 0,
    });

    Ok(())
}
rust
#[spacetimedb::reducer]
pub fn create_player(ctx: &ReducerContext, name: String) -> Result<(), String> {
    if name.is_empty() {
        return Err("名称不能为空".to_string());
    }

    ctx.db.player().insert(Player {
        id: 0,  // auto_inc会分配值
        name,
        score: 0,
    });

    Ok(())
}

Reducer Rules

Reducer规则

  1. First parameter must be
    &ReducerContext
  2. Additional parameters must implement
    SpacetimeType
  3. Return
    ()
    ,
    Result<(), String>
    , or
    Result<(), E>
    where
    E: Display
  4. All changes roll back on panic or
    Err
    return
  5. Reducers run in isolation from concurrent reducers
  6. Cannot make network requests or access filesystem
  7. Must import
    Table
    trait for table operations:
    use spacetimedb::Table;
  1. 第一个参数必须是
    &ReducerContext
  2. 额外参数必须实现
    SpacetimeType
  3. 返回值可以是
    ()
    Result<(), String>
    Result<(), E>
    (其中
    E: Display
  4. 发生panic或返回
    Err
    时,所有修改都会回滚
  5. Reducer与并发Reducer隔离运行
  6. 不能进行网络请求或文件系统访问
  7. 必须导入
    Table
    trait才能进行表操作:
    use spacetimedb::Table;

ReducerContext

ReducerContext

The
ReducerContext
provides access to the database and caller information.
ReducerContext
提供对数据库和调用者信息的访问。

Properties

属性

rust
#[spacetimedb::reducer]
pub fn example(ctx: &ReducerContext) {
    // Database access
    let _table = ctx.db.player();

    // Caller identity (always present)
    let caller: Identity = ctx.sender;

    // Connection ID (None for scheduled/system reducers)
    let conn: Option<ConnectionId> = ctx.connection_id;

    // Invocation timestamp
    let when: Timestamp = ctx.timestamp;

    // Module's own identity
    let module_id: Identity = ctx.identity();

    // Random number generation (deterministic)
    let random_val: u32 = ctx.random();

    // UUID generation
    let uuid = ctx.new_uuid_v4().unwrap();  // Random UUID
    let uuid = ctx.new_uuid_v7().unwrap();  // Timestamp-based UUID

    // Check if caller is internal (scheduled reducer)
    if ctx.sender_auth().is_internal() {
        // Called by scheduler, not external client
    }
}
rust
#[spacetimedb::reducer]
pub fn example(ctx: &ReducerContext) {
    // 数据库访问
    let _table = ctx.db.player();

    // 调用者身份(始终存在)
    let caller: Identity = ctx.sender;

    // 连接ID(调度/系统Reducer为None)
    let conn: Option<ConnectionId> = ctx.connection_id;

    // 调用时间戳
    let when: Timestamp = ctx.timestamp;

    // 模块自身的身份
    let module_id: Identity = ctx.identity();

    // 随机数生成(确定性)
    let random_val: u32 = ctx.random();

    // UUID生成
    let uuid = ctx.new_uuid_v4().unwrap();  // 随机UUID
    let uuid = ctx.new_uuid_v7().unwrap();  // 基于时间戳的UUID

    // 检查调用者是否为内部(调度Reducer)
    if ctx.sender_auth().is_internal() {
        // 由调度器调用,而非外部客户端
    }
}

Table Operations

表操作

Insert

插入

rust
// Insert returns the row with auto_inc values populated
let player = ctx.db.player().insert(Player {
    id: 0,  // auto_inc fills this
    name: "Alice".to_string(),
    score: 100,
});
log::info!("Created player with id: {}", player.id);
rust
// 插入操作返回填充了auto_inc值的行
let player = ctx.db.player().insert(Player {
    id: 0,  // auto_inc会填充该值
    name: "Alice".to_string(),
    score: 100,
});
log::info!("创建玩家,ID为:{}", player.id);

Find by Unique/Primary Key

通过唯一/主键查找

rust
// find() returns Option<RowType>
if let Some(player) = ctx.db.player().id().find(123) {
    log::info!("Found: {}", player.name);
}
rust
// find()返回Option<RowType>
if let Some(player) = ctx.db.player().id().find(123) {
    log::info!("找到玩家:{}", player.name);
}

Filter by Indexed Column

通过索引列过滤

rust
// filter() returns an iterator
for player in ctx.db.player().name().filter("Alice") {
    log::info!("Player {}: score {}", player.id, player.score);
}

// Range queries (Rust range syntax)
for player in ctx.db.player().score().filter(50..=100) {
    log::info!("{} has score {}", player.name, player.score);
}
rust
// filter()返回迭代器
for player in ctx.db.player().name().filter("Alice") {
    log::info!("玩家{}:分数{}", player.id, player.score);
}

// 范围查询(Rust范围语法)
for player in ctx.db.player().score().filter(50..=100) {
    log::info!("{}的分数为{}", player.name, player.score);
}

Update

更新

Updates require a unique column. Find the row, modify it, then call
update()
:
rust
if let Some(mut player) = ctx.db.player().id().find(123) {
    player.score += 10;
    ctx.db.player().id().update(player);
}
更新操作需要唯一列。找到行,修改后调用
update()
rust
if let Some(mut player) = ctx.db.player().id().find(123) {
    player.score += 10;
    ctx.db.player().id().update(player);
}

Delete

删除

rust
// Delete by unique key
ctx.db.player().id().delete(&123);

// Delete by indexed column (returns count)
let deleted = ctx.db.player().name().delete("Alice");
log::info!("Deleted {} rows", deleted);

// Delete by range
ctx.db.player().score().delete(..50);  // Delete all with score < 50
rust
// 通过唯一键删除
ctx.db.player().id().delete(&123);

// 通过索引列删除(返回删除行数)
let deleted = ctx.db.player().name().delete("Alice");
log::info!("删除了{}行", deleted);

// 通过范围删除
ctx.db.player().score().delete(..50);  // 删除所有分数<50的行

Iterate All Rows

遍历所有行

rust
for player in ctx.db.player().iter() {
    log::info!("{}: {}", player.name, player.score);
}

// Count rows
let total = ctx.db.player().count();
rust
for player in ctx.db.player().iter() {
    log::info!("{}:{}", player.name, player.score);
}

// 统计行数
let total = ctx.db.player().count();

Indexes

索引

Single-Column Index

单列索引

rust
#[spacetimedb::table(name = player, public)]
pub struct Player {
    #[primary_key]
    id: u64,
    #[index(btree)]
    level: u32,
    name: String,
}
rust
#[spacetimedb::table(name = player, public)]
pub struct Player {
    #[primary_key]
    id: u64,
    #[index(btree)]
    level: u32,
    name: String,
}

Multi-Column Index

多列索引

rust
#[spacetimedb::table(
    name = score,
    public,
    index(name = by_player_level, btree(columns = [player_id, level]))
)]
pub struct Score {
    player_id: u32,
    level: u32,
    points: i64,
}
rust
#[spacetimedb::table(
    name = score,
    public,
    index(name = by_player_level, btree(columns = [player_id, level]))
)]
pub struct Score {
    player_id: u32,
    level: u32,
    points: i64,
}

Querying Multi-Column Indexes

查询多列索引

rust
// Prefix match (first column only)
for score in ctx.db.score().by_player_level().filter(&123u32) {
    log::info!("Level {}: {} points", score.level, score.points);
}

// Full match
for score in ctx.db.score().by_player_level().filter((123u32, 5u32)) {
    log::info!("Points: {}", score.points);
}

// Prefix with range on second column
for score in ctx.db.score().by_player_level().filter((123u32, 1u32..=10u32)) {
    log::info!("Level {}: {} points", score.level, score.points);
}
rust
// 前缀匹配(仅第一列)
for score in ctx.db.score().by_player_level().filter(&123u32) {
    log::info!("等级{}:{}分", score.level, score.points);
}

// 完全匹配
for score in ctx.db.score().by_player_level().filter((123u32, 5u32)) {
    log::info!("分数:{}", score.points);
}

// 前缀+第二列范围查询
for score in ctx.db.score().by_player_level().filter((123u32, 1u32..=10u32)) {
    log::info!("等级{}:{}分", score.level, score.points);
}

Identity and Authentication

身份与认证

Storing User Identity

存储用户身份

rust
#[spacetimedb::table(name = user_profile, public)]
pub struct UserProfile {
    #[primary_key]
    identity: Identity,
    display_name: String,
    created_at: Timestamp,
}

#[spacetimedb::reducer]
pub fn create_profile(ctx: &ReducerContext, display_name: String) -> Result<(), String> {
    // Check if profile already exists
    if ctx.db.user_profile().identity().find(ctx.sender).is_some() {
        return Err("Profile already exists".to_string());
    }

    ctx.db.user_profile().insert(UserProfile {
        identity: ctx.sender,
        display_name,
        created_at: ctx.timestamp,
    });

    Ok(())
}
rust
#[spacetimedb::table(name = user_profile, public)]
pub struct UserProfile {
    #[primary_key]
    identity: Identity,
    display_name: String,
    created_at: Timestamp,
}

#[spacetimedb::reducer]
pub fn create_profile(ctx: &ReducerContext, display_name: String) -> Result<(), String> {
    // 检查是否已存在该用户的资料
    if ctx.db.user_profile().identity().find(ctx.sender).is_some() {
        return Err("该用户资料已存在".to_string());
    }

    ctx.db.user_profile().insert(UserProfile {
        identity: ctx.sender,
        display_name,
        created_at: ctx.timestamp,
    });

    Ok(())
}

Verifying Caller Identity

验证调用者身份

rust
#[spacetimedb::reducer]
pub fn update_my_profile(ctx: &ReducerContext, new_name: String) -> Result<(), String> {
    // Only allow users to update their own profile
    if let Some(mut profile) = ctx.db.user_profile().identity().find(ctx.sender) {
        profile.display_name = new_name;
        ctx.db.user_profile().identity().update(profile);
        Ok(())
    } else {
        Err("Profile not found".to_string())
    }
}
rust
#[spacetimedb::reducer]
pub fn update_my_profile(ctx: &ReducerContext, new_name: String) -> Result<(), String> {
    // 仅允许用户更新自己的资料
    if let Some(mut profile) = ctx.db.user_profile().identity().find(ctx.sender) {
        profile.display_name = new_name;
        ctx.db.user_profile().identity().update(profile);
        Ok(())
    } else {
        Err("用户资料未找到".to_string())
    }
}

Lifecycle Reducers

生命周期Reducer

Init Reducer

初始化Reducer

Runs once when the module is first published or database is cleared:
rust
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) -> Result<(), String> {
    log::info!("Database initializing...");

    // Set up default data
    if ctx.db.config().count() == 0 {
        ctx.db.config().insert(Config {
            key: "version".to_string(),
            value: "1.0.0".to_string(),
        });
    }

    Ok(())
}
首次发布模块或清除数据库时运行一次:
rust
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) -> Result<(), String> {
    log::info!("数据库初始化中...");

    // 设置默认数据
    if ctx.db.config().count() == 0 {
        ctx.db.config().insert(Config {
            key: "version".to_string(),
            value: "1.0.0".to_string(),
        });
    }

    Ok(())
}

Client Connected

客户端连接

Runs when a client establishes a connection:
rust
#[spacetimedb::reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> {
    log::info!("Client connected: {}", ctx.sender);

    // connection_id is guaranteed to be Some
    let conn_id = ctx.connection_id.unwrap();

    // Create or update user session
    if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
        ctx.db.user().identity().update(User { online: true, ..user });
    } else {
        ctx.db.user().insert(User {
            identity: ctx.sender,
            online: true,
            name: None,
        });
    }

    Ok(())
}
客户端建立连接时运行:
rust
#[spacetimedb::reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> {
    log::info!("客户端已连接:{}", ctx.sender);

    // connection_id此时一定为Some
    let conn_id = ctx.connection_id.unwrap();

    // 创建或更新用户会话
    if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
        ctx.db.user().identity().update(User { online: true, ..user });
    } else {
        ctx.db.user().insert(User {
            identity: ctx.sender,
            online: true,
            name: None,
        });
    }

    Ok(())
}

Client Disconnected

客户端断开连接

Runs when a client connection terminates:
rust
#[spacetimedb::reducer(client_disconnected)]
pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> {
    log::info!("Client disconnected: {}", ctx.sender);

    if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
        ctx.db.user().identity().update(User { online: false, ..user });
    }

    Ok(())
}
客户端连接终止时运行:
rust
#[spacetimedb::reducer(client_disconnected)]
pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> {
    log::info!("客户端已断开连接:{}", ctx.sender);

    if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
        ctx.db.user().identity().update(User { online: false, ..user });
    }

    Ok(())
}

Scheduled Reducers

调度Reducer

Schedule reducers to run at specific times or intervals.
调度Reducer用于在特定时间或间隔运行。

Define a Schedule Table

定义调度表

rust
use spacetimedb::ScheduleAt;
use std::time::Duration;

#[spacetimedb::table(name = game_tick_schedule, scheduled(game_tick))]
pub struct GameTickSchedule {
    #[primary_key]
    #[auto_inc]
    scheduled_id: u64,
    scheduled_at: ScheduleAt,
}

#[spacetimedb::reducer]
fn game_tick(ctx: &ReducerContext, schedule: GameTickSchedule) {
    // Verify this is an internal call (from scheduler)
    if !ctx.sender_auth().is_internal() {
        log::warn!("External call to scheduled reducer rejected");
        return;
    }

    // Game logic here
    log::info!("Game tick at {:?}", ctx.timestamp);
}
rust
use spacetimedb::ScheduleAt;
use std::time::Duration;

#[spacetimedb::table(name = game_tick_schedule, scheduled(game_tick))]
pub struct GameTickSchedule {
    #[primary_key]
    #[auto_inc]
    scheduled_id: u64,
    scheduled_at: ScheduleAt,
}

#[spacetimedb::reducer]
fn game_tick(ctx: &ReducerContext, schedule: GameTickSchedule) {
    // 验证是否为内部调用(来自调度器)
    if !ctx.sender_auth().is_internal() {
        log::warn!("拒绝外部调用调度Reducer");
        return;
    }

    // 游戏逻辑写在这里
    log::info!("游戏Tick触发于{:?}", ctx.timestamp);
}

Scheduling at Intervals

按间隔调度

rust
#[spacetimedb::reducer]
fn start_game_loop(ctx: &ReducerContext) {
    // Schedule game tick every 100ms
    ctx.db.game_tick_schedule().insert(GameTickSchedule {
        scheduled_id: 0,
        scheduled_at: ScheduleAt::Interval(Duration::from_millis(100).into()),
    });
}
rust
#[spacetimedb::reducer]
fn start_game_loop(ctx: &ReducerContext) {
    // 每100ms调度一次游戏Tick
    ctx.db.game_tick_schedule().insert(GameTickSchedule {
        scheduled_id: 0,
        scheduled_at: ScheduleAt::Interval(Duration::from_millis(100).into()),
    });
}

Scheduling at Specific Times

在特定时间调度

rust
#[spacetimedb::reducer]
fn schedule_reminder(ctx: &ReducerContext, delay_secs: u64) {
    let run_at = ctx.timestamp + Duration::from_secs(delay_secs);

    ctx.db.reminder_schedule().insert(ReminderSchedule {
        scheduled_id: 0,
        scheduled_at: ScheduleAt::Time(run_at),
        message: "Time's up!".to_string(),
    });
}

rust
#[spacetimedb::reducer]
fn schedule_reminder(ctx: &ReducerContext, delay_secs: u64) {
    let run_at = ctx.timestamp + Duration::from_secs(delay_secs);

    ctx.db.reminder_schedule().insert(ReminderSchedule {
        scheduled_id: 0,
        scheduled_at: ScheduleAt::Time(run_at),
        message: "时间到了!".to_string(),
    });
}

Procedures (Beta)

Procedure(测试版)

Procedures are for side effects (HTTP, filesystem) that reducers can't do.
Procedures are currently unstable. Enable with:
toml
undefined
Procedure用于处理Reducer无法完成的副作用(HTTP请求、文件系统操作)。
Procedure目前处于不稳定状态。需要启用以下特性:
toml
undefined

Cargo.toml

Cargo.toml

[dependencies] spacetimedb = { version = "1.*", features = ["unstable"] }

```rust
use spacetimedb::{procedure, ProcedureContext};

// Simple procedure
#[procedure]
fn add_numbers(_ctx: &mut ProcedureContext, a: u32, b: u32) -> u64 {
    a as u64 + b as u64
}

// Procedure with database access
#[procedure]
fn save_external_data(ctx: &mut ProcedureContext, url: String) -> Result<(), String> {
    // HTTP request (allowed in procedures, not reducers)
    let data = fetch_from_url(&url)?;

    // Database access requires explicit transaction
    ctx.try_with_tx(|tx| {
        tx.db.external_data().insert(ExternalData {
            id: 0,
            content: data,
        });
        Ok(())
    })?;

    Ok(())
}
[dependencies] spacetimedb = { version = "1.*", features = ["unstable"] }

```rust
use spacetimedb::{procedure, ProcedureContext};

// 简单Procedure
#[procedure]
fn add_numbers(_ctx: &mut ProcedureContext, a: u32, b: u32) -> u64 {
    a as u64 + b as u64
}

// 带数据库访问的Procedure
#[procedure]
fn save_external_data(ctx: &mut ProcedureContext, url: String) -> Result<(), String> {
    // HTTP请求(允许在Procedure中执行,Reducer中不行)
    let data = fetch_from_url(&url)?;

    // 数据库访问需要显式事务
    ctx.try_with_tx(|tx| {
        tx.db.external_data().insert(ExternalData {
            id: 0,
            content: data,
        });
        Ok(())
    })?;

    Ok(())
}

Key Differences from Reducers

与Reducer的主要区别

ReducersProcedures
&ReducerContext
(immutable)
&mut ProcedureContext
(mutable)
Direct
ctx.db
access
Must use
ctx.with_tx()
No HTTP/networkHTTP allowed
No return valuesCan return data

ReducersProcedures
&ReducerContext
(不可变)
&mut ProcedureContext
(可变)
直接访问
ctx.db
必须使用
ctx.with_tx()
不允许HTTP/网络操作允许HTTP操作
无返回值可以返回数据

Error Handling

错误处理

Sender Errors (Expected)

发送方错误(预期内)

Return errors for invalid client input:
rust
#[spacetimedb::reducer]
pub fn transfer_credits(
    ctx: &ReducerContext,
    to_user: Identity,
    amount: u32,
) -> Result<(), String> {
    let sender = ctx.db.user().identity().find(ctx.sender)
        .ok_or("Sender not found")?;

    if sender.credits < amount {
        return Err("Insufficient credits".to_string());
    }

    // Perform transfer...
    Ok(())
}
对于无效的客户端输入,返回错误:
rust
#[spacetimedb::reducer]
pub fn transfer_credits(
    ctx: &ReducerContext,
    to_user: Identity,
    amount: u32,
) -> Result<(), String> {
    let sender = ctx.db.user().identity().find(ctx.sender)
        .ok_or("发送方未找到")?;

    if sender.credits < amount {
        return Err("余额不足".to_string());
    }

    // 执行转账操作...
    Ok(())
}

Programmer Errors (Bugs)

程序员错误(Bug)

Use panic for unexpected states that indicate bugs:
rust
#[spacetimedb::reducer]
pub fn process_data(ctx: &ReducerContext, data: Vec<u8>) {
    // This should never happen - indicates a bug
    assert!(!data.is_empty(), "Unexpected empty data");

    // Use expect for operations that should always succeed
    let parsed = parse_data(&data).expect("Failed to parse data");
}
对于表示Bug的意外状态,使用panic:
rust
#[spacetimedb::reducer]
pub fn process_data(ctx: &ReducerContext, data: Vec<u8>) {
    // 这永远不应该发生 — 表示存在Bug
    assert!(!data.is_empty(), "意外的空数据");

    // 对于应该始终成功的操作,使用expect
    let parsed = parse_data(&data).expect("数据解析失败");
}

Custom Types

自定义类型

Define custom types using
#[derive(SpacetimeType)]
:
rust
use spacetimedb::SpacetimeType;

#[derive(SpacetimeType)]
pub enum PlayerStatus {
    Active,
    Idle,
    Away,
}

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

#[spacetimedb::table(name = player, public)]
pub struct Player {
    #[primary_key]
    id: u64,
    status: PlayerStatus,
    position: Position,
}
使用
#[derive(SpacetimeType)]
定义自定义类型:
rust
use spacetimedb::SpacetimeType;

#[derive(SpacetimeType)]
pub enum PlayerStatus {
    Active,
    Idle,
    Away,
}

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

#[spacetimedb::table(name = player, public)]
pub struct Player {
    #[primary_key]
    id: u64,
    status: PlayerStatus,
    position: Position,
}

Multiple Tables from Same Type

同一类型创建多个表

Apply multiple
#[spacetimedb::table]
attributes to create separate tables with the same schema:
rust
#[spacetimedb::table(name = online_player, public)]
#[spacetimedb::table(name = offline_player)]
pub struct Player {
    #[primary_key]
    identity: Identity,
    name: String,
}

#[spacetimedb::reducer]
fn player_logout(ctx: &ReducerContext) {
    if let Some(player) = ctx.db.online_player().identity().find(ctx.sender) {
        ctx.db.offline_player().insert(player.clone());
        ctx.db.online_player().identity().delete(&ctx.sender);
    }
}
通过添加多个
#[spacetimedb::table]
属性,使用相同的结构体创建不同的表:
rust
#[spacetimedb::table(name = online_player, public)]
#[spacetimedb::table(name = offline_player)]
pub struct Player {
    #[primary_key]
    identity: Identity,
    name: String,
}

#[spacetimedb::reducer]
fn player_logout(ctx: &ReducerContext) {
    if let Some(player) = ctx.db.online_player().identity().find(ctx.sender) {
        ctx.db.offline_player().insert(player.clone());
        ctx.db.online_player().identity().delete(&ctx.sender);
    }
}

Logging

日志

Use the
log
crate for debug output. View logs with
spacetime logs <database>
:
rust
log::trace!("Detailed trace info");
log::debug!("Debug information");
log::info!("General information");
log::warn!("Warning message");
log::error!("Error occurred");
Never use
println!
,
eprintln!
, or
dbg!
in modules.
使用
log
crate输出调试信息。使用
spacetime logs <database>
查看日志:
rust
log::trace!("详细跟踪信息");
log::debug!("调试信息");
log::info!("常规信息");
log::warn!("警告消息");
log::error!("发生错误");
永远不要在模块中使用
println!
eprintln!
dbg!

Common Patterns

常见模式

Player Session Management

玩家会话管理

rust
#[spacetimedb::table(name = player, public)]
pub struct Player {
    #[primary_key]
    identity: Identity,
    name: Option<String>,
    online: bool,
    last_seen: Timestamp,
}

#[spacetimedb::reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) {
    match ctx.db.player().identity().find(ctx.sender) {
        Some(player) => {
            ctx.db.player().identity().update(Player {
                online: true,
                last_seen: ctx.timestamp,
                ..player
            });
        }
        None => {
            ctx.db.player().insert(Player {
                identity: ctx.sender,
                name: None,
                online: true,
                last_seen: ctx.timestamp,
            });
        }
    }
}

#[spacetimedb::reducer(client_disconnected)]
pub fn on_disconnect(ctx: &ReducerContext) {
    if let Some(player) = ctx.db.player().identity().find(ctx.sender) {
        ctx.db.player().identity().update(Player {
            online: false,
            last_seen: ctx.timestamp,
            ..player
        });
    }
}
rust
#[spacetimedb::table(name = player, public)]
pub struct Player {
    #[primary_key]
    identity: Identity,
    name: Option<String>,
    online: bool,
    last_seen: Timestamp,
}

#[spacetimedb::reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) {
    match ctx.db.player().identity().find(ctx.sender) {
        Some(player) => {
            ctx.db.player().identity().update(Player {
                online: true,
                last_seen: ctx.timestamp,
                ..player
            });
        }
        None => {
            ctx.db.player().insert(Player {
                identity: ctx.sender,
                name: None,
                online: true,
                last_seen: ctx.timestamp,
            });
        }
    }
}

#[spacetimedb::reducer(client_disconnected)]
pub fn on_disconnect(ctx: &ReducerContext) {
    if let Some(player) = ctx.db.player().identity().find(ctx.sender) {
        ctx.db.player().identity().update(Player {
            online: false,
            last_seen: ctx.timestamp,
            ..player
        });
    }
}

Sequential ID Generation (Gap-Free)

连续ID生成(无间隙)

Auto-increment may have gaps after crashes. For strictly sequential IDs:
rust
#[spacetimedb::table(name = counter)]
pub struct Counter {
    #[primary_key]
    name: String,
    value: u64,
}

#[spacetimedb::reducer]
fn create_invoice(ctx: &ReducerContext, amount: u64) -> Result<(), String> {
    let mut counter = ctx.db.counter().name().find(&"invoice".to_string())
        .unwrap_or(Counter { name: "invoice".to_string(), value: 0 });

    counter.value += 1;
    ctx.db.counter().name().update(counter.clone());

    ctx.db.invoice().insert(Invoice {
        invoice_number: counter.value,
        amount,
    });

    Ok(())
}
崩溃后自动递增可能会产生间隙。如需严格连续的ID:
rust
#[spacetimedb::table(name = counter)]
pub struct Counter {
    #[primary_key]
    name: String,
    value: u64,
}

#[spacetimedb::reducer]
fn create_invoice(ctx: &ReducerContext, amount: u64) -> Result<(), String> {
    let mut counter = ctx.db.counter().name().find(&"invoice".to_string())
        .unwrap_or(Counter { name: "invoice".to_string(), value: 0 });

    counter.value += 1;
    ctx.db.counter().name().update(counter.clone());

    ctx.db.invoice().insert(Invoice {
        invoice_number: counter.value,
        amount,
    });

    Ok(())
}

Owner-Only Reducers

仅所有者可访问的Reducer

rust
#[spacetimedb::table(name = admin)]
pub struct Admin {
    #[primary_key]
    identity: Identity,
}

#[spacetimedb::reducer]
fn admin_action(ctx: &ReducerContext) -> Result<(), String> {
    if ctx.db.admin().identity().find(ctx.sender).is_none() {
        return Err("Not authorized".to_string());
    }

    // Admin-only logic here
    Ok(())
}
rust
#[spacetimedb::table(name = admin)]
pub struct Admin {
    #[primary_key]
    identity: Identity,
}

#[spacetimedb::reducer]
fn admin_action(ctx: &ReducerContext) -> Result<(), String> {
    if ctx.db.admin().identity().find(ctx.sender).is_none() {
        return Err("未授权访问".to_string());
    }

    // 仅管理员可执行的逻辑
    Ok(())
}

Build and Deploy

构建与部署

bash
undefined
bash
undefined

Build the module

构建模块

spacetime build
spacetime build

Deploy to local instance

部署到本地实例

spacetime publish my_database
spacetime publish my_database

Deploy with database clear (DESTROYS DATA)

部署并清除数据库(会销毁所有数据)

spacetime publish my_database --delete-data
spacetime publish my_database --delete-data

View logs

查看日志

spacetime logs my_database
spacetime logs my_database

Call a reducer

调用Reducer

spacetime call my_database create_player "Alice"
spacetime call my_database create_player "Alice"

Run SQL query

执行SQL查询

spacetime sql my_database "SELECT * FROM player"
spacetime sql my_database "SELECT * FROM player"

Generate bindings

生成绑定代码

spacetime generate --lang rust --out-dir <client>/src/module_bindings --project-path <backend-dir>
undefined
spacetime generate --lang rust --out-dir <client>/src/module_bindings --project-path <backend-dir>
undefined

Important Constraints

重要约束

  1. No Global State: Static/global variables are undefined behavior across reducer calls
  2. No Side Effects: Reducers cannot make network requests or file I/O
  3. Deterministic Execution: Use
    ctx.random()
    and
    ctx.new_uuid_*()
    for randomness
  4. Transactional: All reducer changes roll back on failure
  5. Isolated: Reducers don't see concurrent changes until commit
  1. 无全局状态:静态/全局变量在Reducer调用之间的行为未定义
  2. 无副作用:Reducer不能进行网络请求或文件I/O操作
  3. 确定性执行:使用
    ctx.random()
    ctx.new_uuid_*()
    生成随机数
  4. 事务性:Reducer执行失败时,所有修改都会回滚
  5. 隔离性:Reducer在提交前无法看到并发修改