mcpField
S
spacetimedb-concepts

spacetimedb-concepts

github.com/clockworklabs数据与数据库25.4k126 次安装今天更新
代码仓库

关于 spacetimedb-concepts

spacetimedb-concepts 是使用 SpacetimeDB 构建和调试应用的参考技能。SpacetimeDB 是可在 WebAssembly 模块中运行应用逻辑的关系型数据库;该技能介绍表、事务 Reducer、订阅、身份验证及客户端集成。

主要功能

  1. 01说明表与 Reducer 的设计
  2. 02介绍客户端实时订阅
  3. 03讲解模块生命周期与发布
  4. 04提供身份验证和授权指引
  5. 05包含实现与调试检查清单

适用场景

  1. 01规划数据表和修改数据的 Reducer
  2. 02将客户端视图连接到订阅数据
  3. 03排查模块未发布或未被调用的问题
  4. 04构建实时游戏或协作应用

安装

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

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

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

详细介绍

SpacetimeDB 核心概念

SpacetimeDB 是一个同时也是服务器的关系型数据库。它允许你通过 WebAssembly 模块将应用逻辑直接上传到数据库中,从而彻底省去传统的 Web/游戏服务器层。


重要规则(请先阅读)

以下五条规则有助于避免最常见的 SpacetimeDB 错误:

  1. Reducer 是事务性的 — 它们不会向调用方返回数据。使用订阅读取数据。
  2. Reducer 必须是确定性的 — 不得使用文件系统、网络、计时器或随机数。所有状态都必须来自表。
  3. 通过表/订阅读取数据 — 不要依赖 Reducer 的返回值。客户端通过已订阅的查询获取数据。
  4. 自动递增 ID 并非连续 — 出现间隙很正常,不要用它排序。请使用时间戳或显式的序列列。
  5. ctx.sender() 是经过身份验证的主体 — 绝不要信任作为参数传入的身份。授权时务必使用 ctx.sender()。

功能实现检查清单

实现一个涉及后端和客户端的功能时:

  1. 后端: 定义用于存储数据的表
  2. 后端: 定义用于修改数据的 Reducer
  3. 客户端: 订阅相关表
  4. 客户端: 在 UI 中调用 Reducer — 不要跳过这一步
  5. 客户端: 从表中渲染数据

常见错误: 构建了后端表和 Reducer,却忘记在客户端接入并调用它们。


调试检查清单

遇到问题时:

  1. SpacetimeDB 服务是否正在运行?(spacetime start)
  2. 模块是否已发布?(spacetime publish)
  3. 是否已生成客户端绑定?(spacetime generate)
  4. 检查服务日志是否有错误(spacetime logs <db-name>)
  5. 客户端是否实际调用了 Reducer?

CLI 命令

spacetime start
spacetime publish <db-name> --module-path <module-path>
spacetime publish <db-name> --clear-database -y --module-path <module-path>
spacetime generate --lang <lang> --out-dir <out> --module-path <module-path>
spacetime logs <db-name>

SpacetimeDB 是什么

SpacetimeDB 将数据库和应用服务器合并为一个可部署单元。客户端直接连接到数据库,并在其中执行应用逻辑。该系统针对需要极致速度和最低延迟的实时应用进行了优化。

主要特征:

  • 内存中执行:应用状态由内存提供,以实现极低延迟的访问
  • 持久化存储:数据会自动持久化到预写日志(WAL),以保证持久性
  • 实时同步:更改会自动推送给已订阅的客户端
  • 单一部署:无需管理独立的服务器、容器或基础设施

五大禅意原则

  1. 一切皆为表:整个应用状态都存储在表中。无需单独的缓存层、Redis 或需要同步的内存状态。
  2. 一切皆持久:SpacetimeDB 默认持久化状态(例如通过 WAL 支持的持久性)。
  3. 一切皆实时:客户端是服务器状态的副本。订阅数据后,数据就会自动流动。
  4. 一切皆事务性:每个 Reducer 都会原子执行。所有更改要么全部成功,要么全部回滚。
  5. 一切皆可编程:模块是真正的代码(Rust、C#、TypeScript),运行在数据库内部。

表

表存储 SpacetimeDB 中的所有数据。它们采用关系模型,并支持用于订阅的 SQL 查询。

定义表

表通过特定语言的属性定义。在 2.0 中,API 名称请使用 accessor(而不是 name):

Rust:

#[spacetimedb::table(accessor = player, public)]
pub struct Player {
    #[primary_key]
    #[auto_inc]
    id: u32,
    #[index(btree)]
    name: String,
    #[unique]
    email: String,
}

C#:

[SpacetimeDB.Table(Accessor = "Player", Public = true)]
public partial struct Player
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public uint Id;
    [SpacetimeDB.Index.BTree]
    public string Name;
    [SpacetimeDB.Unique]
    public string Email;
}

TypeScript:

const players = table(
  { name: 'players', public: true },
  {
    id: t.u32().primaryKey().autoInc(),
    name: t.string().index('btree'),
    email: t.string().unique(),
  }
);
表可见性
  • 私有表(默认):只能由 Reducer 和数据库所有者访问
  • 公共表:可通过订阅向客户端开放读取权限。写入仍需通过 Reducer。
表设计原则

根据访问模式而不是实体来组织数据:

分解式设计(推荐):

Player          PlayerState         PlayerStats
id         <--  player_id           player_id
name            position_x          total_kills
                position_y          total_deaths
                velocity_x          play_time

优点:减少带宽占用、提高缓存效率、便于演进架构、语义清晰。

Reducer

Reducer 是用于修改数据库状态的事务性函数。它们是客户端调用的主要修改途径;过程也可以通过运行显式事务来修改表。

主要属性
  • 事务性:在隔离的数据库事务中运行
  • 原子性:要么所有更改都成功,要么全部回滚
  • 隔离性:无法与外部世界交互(不能访问网络或文件系统)
  • 可调用:客户端通过远程过程调用来调用 Reducer
Reducer 的关键规则
  1. 不得使用全局状态:依赖静态变量属于未定义行为
  2. 不得产生副作用:Reducer 无法发起网络请求或访问文件
  3. 将状态存储在表中:所有持久化状态都必须存储在表中
  4. 不返回数据:Reducer 不向调用方返回数据——请使用订阅
  5. 必须具有确定性:不得使用随机数、计时器或外部 I/O
定义 Reducer

Rust:

#[spacetimedb::reducer]
pub fn create_user(ctx: &ReducerContext, name: String, email: String) -> Result<(), String> {
    if name.is_empty() {
        return Err("Name cannot be empty".to_string());
    }
    ctx.db.user().insert(User { id: 0, name, email });
    Ok(())
}

C#:

[SpacetimeDB.Reducer]
public static void CreateUser(ReducerContext ctx, string name, string email)
{
    if (string.IsNullOrEmpty(name))
        throw new ArgumentException("Name cannot be empty");
    ctx.Db.User.Insert(new User { Id = 0, Name = name, Email = email });
}
ReducerContext

每个 Reducer 都会接收一个 ReducerContext,其中提供:

  • 数据库:ctx.db(Rust 字段、TS 属性)/ ctx.Db(C# 属性)
  • 发送方:ctx.sender()(Rust 方法)/ ctx.Sender(C# 属性)/ ctx.sender(TS 属性)
  • 连接 ID:ctx.connection_id()(Rust 方法)/ ctx.ConnectionId(C# 属性)/ ctx.connectionId(TS 属性)
  • 时间戳:ctx.timestamp(Rust 字段、TS 属性)/ ctx.Timestamp(C# 属性)

事件表(2.0)

事件表是向客户端广播特定于 Reducer 的数据的首选方式。

#[table(accessor = damage_event, public, event)]
pub struct DamageEvent {
    pub target: Identity,
    pub amount: u32,
}

#[reducer]
fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
    ctx.db.damage_event().insert(DamageEvent { target, amount });
}

客户端订阅事件表并使用 on_insert 回调。必须显式订阅事件表,且它们不会包含在 subscribe_to_all_tables() 中。

订阅

订阅会将数据库行实时同步到客户端。

订阅的工作方式
  1. 订阅:注册用于描述所需数据的 SQL 查询
  2. 接收初始数据:立即发送所有匹配的行
  3. 接收更新:订阅的行发生变化时实时接收更新
  4. 响应更改:使用回调(onInsert、onDelete、onUpdate)
订阅最佳实践
  1. 按生命周期分组订阅:将始终需要的数据与临时订阅分开
  2. 先订阅,再取消订阅:更新订阅时,先订阅新数据
  3. 避免查询重叠:返回重叠数据的不同查询会导致重复处理
  4. 使用索引:对已索引列的查询效率高;全表扫描开销很大

模块

模块是包含应用逻辑的 WebAssembly 程序包,在数据库内部运行。

模块组件
  • 表:定义数据架构
  • Reducer:定义可调用的状态修改函数
  • 视图:定义只读的计算查询
  • 事件表:向客户端广播特定于 Reducer 的数据(2.0)
  • 过程:(Beta)可产生副作用的函数(HTTP 请求)
模块语言

服务器端模块可使用以下语言编写:Rust、C#、TypeScript(beta)

模块生命周期
  1. 编写:使用所选语言定义表和 Reducer
  2. 编译:使用 SpacetimeDB CLI 构建为 WebAssembly
  3. 发布:使用 spacetime publish 上传到 SpacetimeDB 主机
  4. 热切换:重新发布以更新代码,无需断开客户端连接

身份

身份是 SpacetimeDB 基于 OpenID Connect(OIDC)的身份验证系统。

  • Identity:用户长期有效的全局唯一标识符。
  • ConnectionId:标识特定的客户端连接。
#[spacetimedb::reducer]
pub fn do_something(ctx: &ReducerContext) {
    let caller_identity = ctx.sender();  // Who is calling?
    // NEVER trust identity passed as a reducer argument
}
身份验证提供商

SpacetimeDB 支持许多 OIDC 提供商,包括 SpacetimeAuth(内置)、Auth0、Clerk、Keycloak、Google 和 GitHub。

何时使用 SpacetimeDB

理想用例
  • 实时游戏:大型多人在线游戏、多人游戏、回合制游戏
  • 协作应用:文档编辑、白板、设计工具
  • 聊天和消息传递:支持在线状态的实时通信
  • 实时仪表盘:流式分析和监控
关键决策因素

在以下情况下选择 SpacetimeDB:

  • 读写延迟低于 10 ms
  • 自动实时同步
  • 所有操作都有事务保障
  • 架构更简单(无需单独的缓存、队列或服务)
不太适用的场景
  • 批量分析:针对 OLTP 优化,而非 OLAP
  • 大型 Blob 存储:更适合结构化关系数据
  • 无状态 API:传统 REST API 无需实时同步

常见模式

在 reducer 中检查身份验证:

#[spacetimedb::reducer]
fn admin_action(ctx: &ReducerContext) -> Result<(), String> {
    let admin = ctx.db.admin().identity().find(&ctx.sender())
        .ok_or("Not an admin")?;
    Ok(())
}

计划任务 reducer:

#[spacetimedb::table(accessor = reminder, scheduled(send_reminder))]
pub struct Reminder {
    #[primary_key]
    #[auto_inc]
    id: u64,
    scheduled_at: ScheduleAt,
    message: String,
}

#[spacetimedb::reducer]
fn send_reminder(ctx: &ReducerContext, reminder: Reminder) {
    log::info!("Reminder: {}", reminder.message);
}

编辑行为

修改 SpacetimeDB 代码时:

  • 只进行必要的最小改动
  • 不要修改无关文件、配置或依赖项
  • 不要臆造新的 SpacetimeDB API——仅使用文档或此代码库中已有的 API

评论

支持 Markdown