
SpacetimeDB Reducer Context 完全指南数据库读写、调用者身份与确定性随机数的核心上下文【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB在 SpacetimeDB 模块开发中每一个 reducer 的第一个参数都是一个特殊的ReducerContext上下文对象。它既是访问模块数据库的唯一入口也携带调用者身份、连接 ID、时间戳等元信息并提供全节点一致的确定性随机数生成器。本文基于当前仓库官方文档与源码实现完整梳理 Reducer Context 的每一项能力覆盖 TypeScript / C# / Rust / C 四种服务端语言帮助你在编写 reducer、定时任务与鉴权逻辑时正确使用这一核心参数。Reducer Context 是什么每个 reducer 都会接收一个特殊的上下文参数作为第一个参数。该上下文提供了对数据库的读写访问、关于调用者的信息以及随机数生成等附加工具。Reducer context 是访问表、执行数据库操作、以及获取当前 reducer 调用元数据的必需入口。在 crates/bindings/src/lib.rs 中Rust 侧的定义明确写道This must be the first argument of the reducer. Clients of the module will only see arguments after theReducerContext.也就是说客户端可见的函数参数从第二个参数才开始Context 本身由运行时注入客户端无法伪造。Rust 侧该结构体被打上#[non_exhaustive]标记其字段包括sender、timestamp、connection_id、sender_auth以及提供表访问能力的db: Local当启用rand08feature 时还包含懒初始化的rng: OnceCellStdbRng和用于生成 UUIDv7 的单调计数器counter_uuid: Cellu32。在数据库内核层crates/datastore/src/execution_context.rs还存在一个宿主侧的ReducerContext结构记录 reducer 名称、调用者身份caller_identity、调用者连接caller_connection_id、调用时间戳以及 BSATN 编码的参数arg_bsatn。这份信息会被写入持久化的 commitlog通过FromReducerContext for txdata::Inputs转换是分布式一致性得以保证的原始输入数据。访问数据库ctx.db 是唯一的读写入口Reducer context 的首要用途是为模块提供数据库表的访问能力。四种语言都以ctx.db或ctx.Db为根通过代码生成出的表访问器执行insert、iter、find、update等操作。TypeScriptimport { schema, table, t } from spacetimedb/server; const user table( { name: user, public: true }, { id: t.u64().primaryKey().autoInc(), name: t.string(), } ); const spacetimedb schema({ user }); export default spacetimedb; export const createUser spacetimedb.reducer({ name: t.string() }, (ctx, { name }) { ctx.db.user.insert({ id: 0n, name }); });C#using SpacetimeDB; public static partial class Module { [SpacetimeDB.Table] public partial struct User { [SpacetimeDB.PrimaryKey] [SpacetimeDB.AutoInc] public ulong id; public string name; } [SpacetimeDB.Reducer] public static void CreateUser(ReducerContext ctx, string name) { ctx.Db.User.Insert(new User { id 0, name name }); } }[!NOTE] C# 表访问器使用 PascalCasectx.Db.User、ctx.Db.Player。访问器名称由 codegen 从表名推导而来。在 crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs 的测试夹具中可以看到同样的模式ctx.Db.PublicTable.Insert(data)之后用ctx.Db.PublicTable.Iter()遍历全部行。该夹具还验证了多表访问器的生成——同一个行类型可以通过[SpacetimeDB.Table(Accessor MultiTable1, ...)]、[SpacetimeDB.Table(Accessor MultiTable2)]注册到两张表随后在 reducer 中分别以ctx.Db.MultiTable1与ctx.Db.MultiTable2访问见 Lib.cs。Rust[!WARNING] 每个使用了ctx.db.*.insert()、.iter()、.get_by_id()等操作的 reducer必须在其导入中包含Tableuse spacetimedb::{..., Table};否则会得到编译错误no method named insert found。use spacetimedb::{table, reducer, ReducerContext, Table}; #[table(accessor user)] pub struct User { #[primary_key] #[auto_inc] id: u64, name: String, } #[reducer] fn create_user(ctx: ReducerContext, name: String) { ctx.db.user().insert(User { id: 0, name }); }关于为何必须导入Table在 crates/bindings/src/lib.rs 的文档注释中解释得十分清楚——db字段的类型Local本身看起来没有任何方法#[table]宏利用 Rust 的 trait 系统为这一类型生成表访问器。Tabletrait 正是这些生成方法的载体缺少该导入便无法解析insert、iter等方法。C#include spacetimedb.h using namespace SpacetimeDB; struct User { uint64_t id; std::string name; }; SPACETIMEDB_STRUCT(User, id, name); SPACETIMEDB_TABLE(User, user, Public); FIELD_PrimaryKeyAutoInc(user, id); SPACETIMEDB_REDUCER(create_user, ReducerContext ctx, std::string name) { ctx.db[user].insert(User{0, name}); return Ok(); }[!NOTE] C 模块需要较新的模块版本支持ReducerContext增强版。C 侧上下文定义见 crates/bindings-cpp/include/spacetimedb/reducer_context.h其中db是DatabaseContext类型支持按表名索引访问ctx.db[user]。调用者信息sender、Connection ID 与 Timestamp上下文提供了谁在何时调用了这个 reducer的信息。Sender Identity调用者身份每次 reducer 调用都关联一个调用者身份。典型场景是按调用者身份查找其玩家记录并更新分数TypeScriptimport { schema, table, t } from spacetimedb/server; const player table( { name: player, public: true }, { identity: t.identity().primaryKey(), name: t.string(), score: t.u32(), } ); const spacetimedb schema({ player }); export default spacetimedb; export const updateScore spacetimedb.reducer({ newScore: t.u32() }, (ctx, { newScore }) { // Get the callers identity const caller ctx.sender; // Find and update their player record const existingPlayer ctx.db.player.identity.find(caller); if (existingPlayer) { ctx.db.player.identity.update({ ...existingPlayer, score: newScore, }); } });C#using SpacetimeDB; public static partial class Module { [SpacetimeDB.Table] public partial struct Player { [SpacetimeDB.PrimaryKey] public Identity Identity; public string Name; public uint Score; } [SpacetimeDB.Reducer] public static void UpdateScore(ReducerContext ctx, uint newScore) { // Get the callers identity Identity caller ctx.Sender; // Find and update their player record if (ctx.Db.Player.Identity.Find(caller) is Player player) { player.Score newScore; ctx.Db.Player.Identity.Update(player); } } }Rustuse spacetimedb::{table, reducer, ReducerContext, Identity, Table}; #[table(accessor player)] pub struct Player { #[primary_key] identity: Identity, name: String, score: u32, } #[reducer] fn update_score(ctx: ReducerContext, new_score: u32) { // Get the callers identity let caller ctx.sender(); // Find and update their player record if let Some(mut player) ctx.db.player().identity().find(caller) { player.score new_score; ctx.db.player().identity().update(player); } }C#include spacetimedb.h using namespace SpacetimeDB; struct Player { Identity identity; std::string name; uint32_t score; }; SPACETIMEDB_STRUCT(Player, identity, name, score); SPACETIMEDB_TABLE(Player, player, Public); FIELD_PrimaryKey(player, identity); SPACETIMEDB_REDUCER(update_score, ReducerContext ctx, uint32_t new_score) { // Get the callers identity auto caller ctx.sender(); // Find and update their player record if (auto player ctx.db[player_identity].find(caller)) { player-score new_score; ctx.db[player_identity].update(*player); } return Ok(); }Rust 侧的sender字段是私有的通过sender()方法暴露见 crates/bindings/src/lib.rsC 侧同样将sender_设为私有并以sender()方法访问见 reducer_context.h。Connection ID连接 ID连接 ID 标识了调用该 reducer 的具体客户端连接可用于跟踪会话或实现基于连接的独立状态。[!NOTE] 连接 ID 仅当 reducer 调用与某个客户端连接相关联时才存在。由init调用的 reducer、定时scheduledreducer以及部分 CLI 或内部调用可能没有连接 ID。client-connected与client-disconnectedreducer 会收到正在打开或关闭的那个连接的连接 ID。这一约束在 Rust 源码中被如实反映connection_id字段的类型是OptionConnectionId文档注释注明对于由宿主自动调用的某些 reducer包括init和 scheduled reducers将为Nonecrates/bindings/src/lib.rs。C 侧则使用std::optionalConnectionId表示可能缺失的值。Timestamp时间戳时间戳标明 reducer 被调用的时刻。该值在 reducer 的整个执行过程中保持一致适合用来给事件打时间戳或实现基于时间的逻辑。C# 测试夹具展示了一个典型用法在initreducer 中用ctx.Timestamp new TimeDuration(10_000_000)计算10 秒后的调度时间并插入定时任务表见 Lib.cs。在 Rust 侧timestamp: Timestamp是公开字段与宿主内核写入 commitlog 的timestamp对应crates/datastore/src/execution_context.rs。确定性随机数生成ctx.rng / ctx.random上下文提供确定性且可复现的随机数生成器确保 reducer 执行在分布式系统的所有节点上保持一致。[!WARNING] 绝不使用外部随机数生成器例如不基于上下文的 C#Random。这些是非确定性的会导致不同节点产生不同结果破坏共识。凡是 reducer 逻辑需要随机值都应使用上下文提供的随机 APITypeScriptconst fraction ctx.random(); // [0.0, 1.0) const roll ctx.random.integerInRange(1, 6); // inclusive const bytes ctx.random.fill(new Uint8Array(16));C#double fraction ctx.Rng.NextDouble(); // [0.0, 1.0) int roll ctx.Rng.Next(1, 7); // [1, 7)Rustuse spacetimedb::rand::Rng; let value: u32 ctx.random(); let roll: u32 ctx.rng().gen_range(1..6);Cauto rng ctx.rng(); int32_t roll rng.gen_range(1, 6); // inclusive原理以 reducer 时间戳为种子为什么这套随机是确定性的源码给出了精确答案。在 crates/bindings/src/rng.rs 中ReducerContext::randomT()等价于rand::random()但底层使用StdbRngReducerContext::rng()通过self.rng.get_or_init(|| StdbRng::seed_from_ts(self.timestamp))懒初始化StdbRng::seed_from_ts用StdRng::seed_from_u64(timestamp.to_micros_since_unix_epoch() as u64)播种rng.rs。因为所有节点对同一次 reducer 调用共享相同的时间戳它们会推导出完全一致的随机序列从而保证状态收敛与共识达成。C 头文件中同样实现了以 timestamp 懒初始化StdbRng的等价逻辑reducer_context.h。两个必须注意的边界非加密安全StdbRng与randcrate 的StdRng使用相同的 PRNG但因其种子是公开可知的时间戳不能用于加密场景见 rng.rs。需要更高粒度可复现性时可以自行用StdRng播种。状态必须入库不得把 RNG 或其他任何状态存进 WASM 全局变量或跨 reducer 调用的侧信道——所有跨调用持久化的状态必须存放在数据库中见 rng.rs。这保证了状态回放与确定性执行的一致性。Module Identity让模块引用自己上下文还能获取模块自身的身份module identity当 reducer 需要引用数据库本身时很有用。在 SpacetimeDB 2.x 中定时scheduledreducer 与 procedures 默认是私有的因此通常无需把 sender 与模块身份比较来防止普通客户端直接调用它们。如果你同时需要定时触发与客户端可调用两个入口正确做法是保持定时函数私有再单独定义一个公开 reducer 包装共享逻辑。四种语言的定时任务示例完整如下TypeScriptimport { schema, table, t } from spacetimedb/server; const scheduledTask table( { name: scheduled_task }, { taskId: t.u64().primaryKey().autoInc(), scheduledAt: t.scheduleAt(), message: t.string(), } ); const spacetimedb schema({ scheduledTask }); export default spacetimedb; export const sendReminder spacetimedb.reducer( { onSchedule: scheduledTask }, { arg: scheduledTask.rowType }, (_ctx, { arg }) { console.log(Reminder: ${arg.message}); } );C#using SpacetimeDB; public static partial class Module { [SpacetimeDB.Table(Accessor ScheduledTask, Scheduled nameof(SendReminder), ScheduledAt nameof(ScheduledAt))] public partial struct ScheduledTask { [SpacetimeDB.PrimaryKey] [SpacetimeDB.AutoInc] public ulong TaskId; public ScheduleAt ScheduledAt; public string Message; } [SpacetimeDB.Reducer] public static void SendReminder(ReducerContext _ctx, ScheduledTask task) { Log.Info($Reminder: {task.Message}); } }Rustuse spacetimedb::{table, reducer, ReducerContext, ScheduleAt}; #[table(accessor scheduled_task, scheduled(send_reminder))] pub struct ScheduledTask { #[primary_key] #[auto_inc] task_id: u64, scheduled_at: ScheduleAt, message: String, } #[reducer] fn send_reminder(_ctx: ReducerContext, task: ScheduledTask) { spacetimedb::log::info!(Reminder: {}, task.message); }C#include spacetimedb.h using namespace SpacetimeDB; struct ScheduledTask { uint64_t task_id; ScheduleAt scheduled_at; std::string message; }; SPACETIMEDB_STRUCT(ScheduledTask, task_id, scheduled_at, message); SPACETIMEDB_TABLE(ScheduledTask, scheduled_task, Private); FIELD_PrimaryKeyAutoInc(scheduled_task, task_id); // Register the table for scheduling (column 1 scheduled_at field, 0-based index) SPACETIMEDB_SCHEDULE(scheduled_task, 1, send_reminder); SPACETIMEDB_REDUCER(send_reminder, ReducerContext _ctx, ScheduledTask task) { LOG_INFO(Reminder: task.message); return Ok(); }[!NOTE] 定时任务表通常声明为PrivateC 的SPACETIMEDB_TABLE(..., Private)与 TypeScript 中不带public: true的表定义这正是定时 reducer 默认私有的体现。源码实现一次宿主调用读取模块身份Rust 侧database_identity()方法crates/bindings/src/lib.rs并非查询系统表而是通过宿主调用spacetimedb_bindings_sys::identity()直接从InstanceEnv读取模块身份。源码注释指出查系统表既笨重模块代码没有检查系统表的工具又慢多次宿主调用会命中 datastore而单个宿主调用不经过 datastore。另有一个已弃用的identity()方法请改用database_identity()。Context Properties 完整参考各语言中 Context 的完整属性与方法对照如下。TypeScriptPropertyTypeDescriptiondbDbViewAccess to the modules database tablessenderIdentityIdentity of the callersenderAuthAuthCtxAuthorization context for the caller (includes JWT claims and internal call detection)connectionIdConnectionId \| nullConnection ID of the caller, if availabletimestampTimestampTime when the reducer was invokedrandomRandomRandom number generator (deterministic, seeded by SpacetimeDB)C#PropertyTypeDescriptionDbDbViewAccess to the modules database tablesSenderIdentityIdentity of the callerSenderAuthAuthCtxAuthorization context for the caller (includes JWT claims and internal call detection)ConnectionIdConnectionId?Connection ID of the caller, if availableTimestampTimestampTime when the reducer was invokedRngRandomRandom number generatorDatabaseIdentityIdentityThe modules identityRustPropertyTypeDescriptiondbLocalAccess to the modules database tablessenderIdentityIdentity of the callerconnection_idOptionConnectionIdConnection ID of the caller, if availabletimestampTimestampTime when the reducer was invokedMethods:database_identity() - Identity- Get the modules identityrng() - StdbRng- Get the random number generatorrandomT() - T- Generate a single random valuesender_auth() - AuthCtx- Get authorization context for the caller (includes JWT claims and internal call detection)CPropertyTypeDescriptiondb[table]TableTAccess to a specific tables operationssenderIdentityIdentity of the callertimestampTimestampTime when the reducer was invokedconnection_idstd::optionalConnectionIdConnection ID of the caller, if availableMethods:database_identity() - Identity- Get the modules identityrng() - StdbRng- Get the random number generator (deterministic and reproducible)sender_auth() - const AuthCtx- Get authorization context for the caller (includes JWT claims and internal call detection)[!NOTE] C 使用std::optional表示可能不存在的connection_id。rng()返回的随机数生成器在所有节点上以一致的种子初始化。进阶SenderAuth 与内部调用检测除文档主表外各语言的senderAuth/sender_auth()还暴露了AuthCtx类型其源码实现在 crates/bindings/src/lib.rs。理解它的内部结构有助于正确实现鉴权is_internal标志AuthCtx::internal()表示该调用由数据库内部发起典型场景即 scheduled reducerfrom_connection_id()则按连接 ID 懒加载该连接的 JWT。ReducerContext::new在构造时会根据connection_id是否存在自动选择二者lib.rs这正解释了定时 reducer 没有连接 ID 也没有 JWT这一行为。懒加载 JWT claimshas_jwt()判断是否存在 JWT内部调用恒为 falsejwt()加载并解析 claims。JwtClaims提供了subject()sub、issuer()iss、audience()aud、identity()由isssub推导以及raw_payload()读取自定义 claims等方法lib.rs。结合前文可知AuthCtx与database_identity()可以组合使用由于 2.x 的定时 reducer 与 procedures 默认私有普通的防止客户端直接调用内部函数已由运行时保证而当你需要更细粒度的 JWT 自定义声明鉴权时sender_auth()就是标准入口。小结Reducer Context 是 SpacetimeDB 模块开发中绕不开的第一参数通过ctx.db读写表通过ctx.sender/ctx.connectionId/ctx.timestamp感知调用来源与时机通过ctx.random/ctx.rng获得节点间一致的确定性随机通过database_identity()让模块引用自身并通过senderAuth完成基于 JWT 的鉴权。其中随机数的确定性来源于以 reducer 时间戳为种子的实现crates/bindings/src/rng.rs这是分布式共识的基石而绝不使用外部 RNG状态必须入库则是开发中必须遵守的两条铁律。掌握这份参考表与各语言的完整示例即可在四种服务端语言中熟练编写安全、确定、可复现的 reducer 逻辑。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考