SpacetimeDB 连接生命周期事件验证:深入解析 sdk-test-connect-disconnect 测试模块的设计与实现
2026/9/13 20:22:41 网站建设 项目流程

SpacetimeDB 连接生命周期事件验证:深入解析 sdk-test-connect-disconnect 测试模块的设计与实现

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

导读

SpacetimeDB 是一个将游戏服务器逻辑编译进数据库引擎的实时数据库,客户端通过 WebSocket 与模块通信。在多人应用(如玩家上线/下线、在线状态展示)中,服务端能否及时感知"客户端何时连接、何时断开"至关重要。本文以仓库中的 sdk-test-connect-disconnect 测试模块为主线,完整拆解 SpacetimeDB 如何通过生命周期 reducer(client_connected/client_disconnected)在模块侧捕获 WebSocket 连接的建立与断开,并通过 SDK 侧回调与订阅机制验证事件可观察性。读完本文,你将掌握生命周期 reducer 的声明方式、客户端回调与订阅的完整调用链,以及如何在 Rust 与 C# 两种模块语言之间保持测试行为一致。

一、测试模块的定位:验证连接事件的"可观察性"

sdk-test-connect-disconnect是 SpacetimeDB 仓库中modules/目录下的一个测试模块。根据其 README,该模块的唯一目标非常聚焦:

This module tests that we can observeconnect/disconnectevents for WebSocket connections.

它要回答的核心问题是:当一个 WebSocket 客户端连接上 SpacetimeDB 实例、随后又断开时,模块代码能否感知到这一事件,并产生可被其他客户端订阅观察到的数据变更。这正是构建在线状态、会话追踪、玩家上下线通知等功能的基础能力验证。

仓库为它配套了三层实现:

层级路径作用
模块(Rust)modules/sdk-test-connect-disconnect/src/lib.rs定义表与生命周期 reducer
模块(C# 镜像)modules/sdk-test-connect-disconnect-cs/Lib.cs同行为的 C# 实现,需保持同步
SDK 测试客户端sdks/rust/tests/connect_disconnect_client通过 Rust SDK 实际连接、订阅、断言

二、模块源码:生命周期 reducer 与事件表

模块主体仅有 lib.rs 一个文件,逻辑非常精简,却完整展示了 SpacetimeDB 生命周期事件的标准用法。

2.1 两张"事件表"

use spacetimedb::{Identity, ReducerContext, Table}; #[spacetimedb::table(accessor = connected, public)] pub struct Connected { identity: Identity, } #[spacetimedb::table(accessor = disconnected, public)] pub struct Disconnected { identity: Identity, }
  • 每张表只有一列identity: Identity,用于记录触发事件客户端的身份(Identity是 SpacetimeDB 中客户端身份的全局唯一标识)。
  • 属性accessor = connected为表指定了在ReducerContext上的访问器名(即ctx.db.connected())。
  • public属性使表对订阅客户端可见——这是后续测试客户端能够订阅SELECT * FROM connected的前提。

2.2 两个生命周期 reducer

#[spacetimedb::reducer(client_connected)] pub fn identity_connected(ctx: &ReducerContext) { ctx.db.connected().insert(Connected { identity: ctx.sender() }); } #[spacetimedb::reducer(client_disconnected)] pub fn identity_disconnected(ctx: &ReducerContext) { ctx.db.disconnected().insert(Disconnected { identity: ctx.sender() }); }

关键点在于 reducer 标记参数client_connectedclient_disconnected。在 bindings 宏的文档(crates/bindings/src/lib.rs)中,SpacetimeDB 对这两个生命周期 reducer 的语义有明确规定:

  • #[spacetimedb::reducer(client_connected)]:当客户端连接上模块时执行,发送者的身份可通过ReducerContext的 sender 值取得;
  • #[spacetimedb::reducer(client_disconnected)]:当客户端断开时执行,同样可从 sender 取得身份;
  • 错误语义存在差异:如果在client_connectedreducer 中出错,该客户端将被断开;而如果在client_disconnectedreducer 中出错,客户端仍会被记录为已断开(断开事件本身不可回滚)。

由此可见,这两个 reducer 就是 SpacetimeDB 模块感知 WebSocket 连接生命周期的官方入口。本模块的处理方式是把事件落成一行数据(insert),从而让事件"可观察"——这正是测试要验证的目标。

三、C# 镜像模块:跨语言行为一致性

README 中特别注明:

Also mirrored as a C# version atmodules/sdk-test-connect-disconnect-cs,so must be kept in sync.

C# 版本 Lib.cs 用 SpacetimeDB C# 模块语法实现了完全相同的逻辑:

[SpacetimeDB.Table(Accessor = "connected", Public = true)] public partial struct Connected { public Identity identity; } [SpacetimeDB.Table(Accessor = "disconnected", Public = true)] public partial struct Disconnected { public Identity identity; } static partial class Module { [SpacetimeDB.Reducer(ReducerKind.ClientConnected)] public static void identity_connected(ReducerContext ctx) { ctx.Db.connected.Insert(new Connected { identity = ctx.Sender}); } [SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)] public static void identity_disconnected(ReducerContext ctx) { ctx.Db.disconnected.Insert(new Disconnected { identity = ctx.Sender}); } }

对比可见:

语义Rust 写法C# 写法
表定义#[spacetimedb::table(accessor = connected, public)][SpacetimeDB.Table(Accessor = "connected", Public = true)]
连接生命周期 reducer#[spacetimedb::reducer(client_connected)][SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
断开生命周期 reducer#[spacetimedb::reducer(client_disconnected)][SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)]
插入事件行ctx.db.connected().insert(...)ctx.Db.connected.Insert(...)

保持两个模块同步的意义在于:同一个测试客户端可以在"用哪个模块生成的 bindings"上随机选择,从而验证 SDK 不会因模块实现语言不同而表现不一致(详见下文第五节)。

四、SDK 测试客户端:事件验证的完整调用链

真正验证"事件可观察"的是配套的 SDK 测试客户端 sdks/rust/tests/connect_disconnect_client。它的入口 main.rs 刻意保持薄薄一层,只读取环境变量并调度到共享的处理器,以便 native 与 wasm 两种模式执行完全相同的逻辑:

use connect_disconnect_client::test_handlers; fn main() { let db_name = std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env"); tokio::runtime::Runtime::new() .unwrap() .block_on(test_handlers::dispatch(&db_name)); }

核心逻辑在 test_handlers.rs 中,其测试流程与模块源码顶部注释的描述一一对应:

  1. 连接一次:通过DbConnection::builder()构建连接,注册on_connecton_disconnecton_connect_error回调;
  2. 订阅connected:在on_connect回调中发起订阅SELECT * FROM connected
  3. 断言一行数据:在on_applied回调中检查ctx.db.connected().count() == 1,确认服务端client_connectedreducer 确实写入了一行包含客户端身份的事件记录;
  4. 断开:调用connection.disconnect(),等待on_disconnect回调触发;
  5. 重连:建立第二个连接,订阅SELECT * FROM disconnected,再次断言表中恰好有一行。

4.1 关键回调与订阅 API 细节

on_disconnect回调中对连接状态与错误参数进行了双重要验证(test_handlers.rs):

.on_disconnect(move |ctx, error| { assert!( !ctx.is_active(), "on_disconnect callback, but `ctx.is_active()` is true" ); match error { Some(err) => disconnect_result(Err(anyhow::anyhow!("{err:?}"))), None => disconnect_result(Ok(())), } })
  • ctx.is_active()在断开回调中必须为false,用于验证连接状态机正确切换;
  • 断开错误参数:正常主动断开时为None,异常断开时为Some(err),测试分别给出不同结果。

订阅构建器(subscription builder)则展示了回调链的完整形态:

ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription failed: {error:?}")) .on_applied(move |ctx| { /* 校验 connected 表行数与内容 */ }) .subscribe("SELECT * FROM connected");

第二次连接(重连验证disconnected表)时,还演示了不依赖on_connect回调、直接通过new_connection.subscription_builder()发起订阅的写法,以及用anyhow::ensure!断言count() == 1的测试风格(test_handlers.rs)。

4.2 native 与 wasm 双模式适配

客户端通过条件编译同时支持原生与浏览器/wasm 两种运行形态:

#[cfg(not(target_arch = "wasm32"))] let join_handle = connection.run_threaded(); #[cfg(target_arch = "wasm32")] connection.run_background_task();
  • 原生模式下,连接在独立线程中运行,测试结束通过join_handle.join()等待;
  • wasm 模式下使用后台任务,并在断开后gloo_timers::future::TimeoutFuture::new(0).await主动让出一次事件循环,确保排队的 disconnect 变更在测试函数返回 Node 之前被处理。

连接构建函数同样分模式实现:原生用builder.build().unwrap(),wasm 用异步的builder.build().await.unwrap(),后者是为了避免在 WebSocket 回调有机会运行前阻塞事件循环(test_handlers.rs)。

五、测试如何注册与运行

5.1 在 SDK 测试套件中的注册

该测试注册在 Rust SDK 的集成测试 sdks/rust/tests/test.rs 中,名为connect_disconnect_callbacks

#[test] fn connect_disconnect_callbacks() { const CONNECT_DISCONNECT_CLIENT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/connect_disconnect_client"); super::platform_test_builder(CONNECT_DISCONNECT_CLIENT, None) .with_name(concat!("connect-disconnect-callback-", stringify!($lang))) .with_module(concat!("sdk-test-connect-disconnect", $suffix)) .with_language("rust") .with_generate_private_items(true) .with_bindings_dir("src/module_bindings") .build() .run(); }

其中值得注意的实现细节:

  • .with_module(concat!("sdk-test-connect-disconnect", $suffix)):通过宏参数让同一测试既能对 Rust 模块(sdk-test-connect-disconnect)运行,也能对 C# 模块(sdk-test-connect-disconnect-cs)运行;
  • .with_generate_private_items(true):由于各语言模块对生命周期 reducer 的私有化程度尚未完全统一,生成绑定时刻意包含 private 项,以保证跨模块生成结果一致(test.rs 中注释对此有说明);
  • 测试客户端 README(sdks/rust/tests/connect_disconnect_client/README.md)明确指出:绑定在当前是在两个模块中"任意选择一个"生成的,因此这两个测试不用于测试代码生成,而是专用于验证模块侧 connect/disconnect 事件在 WebSocket 连接建立/断开时确实触发、且客户端能观察到这些事件产生的数据变更。

5.2 运行命令

按原文档说明,执行方式为(在仓库根目录):

# Will run both Rust/C# modules cargo test -p spacetimedb-sdk connect

connect关键字可同时命中connect_disconnect_callbacks(Rust 模块)与connect_disconnect_callbacks_csharp(C# 模块)两个测试用例。测试框架(spacetimedb_testing::sdk)会负责发布模块、启动测试客户端并执行断言。

5.3 测试客户端的运行模式

从 test.rs 的platform_test_builder可以看到测试客户端的两种运行方式:

  • 原生模式:编译命令为cargo build,运行命令为cargo run
  • browser/wasm 模式--features browser):以cargo build --target wasm32-unknown-unknown --no-default-features --features browser编译出 wasm 产物,经wasm-bindgen --target nodejs转换后,由node --experimental-websocket在 Node.js 中执行,通过环境变量SPACETIME_SDK_TEST_DB_NAMESPACETIME_SDK_TEST_SERVER_URL传入数据库名与服务端地址。

六、bindings 的重新生成方法

测试客户端的绑定代码存放在 sdks/rust/tests/connect_disconnect_client/src/module_bindings(内含connected_table.rsidentity_connected_reducer.rsidentity_disconnected_reducer.rs等由spacetime generate产出的文件)。当模块表结构或 reducer 签名发生变化时,需要在客户端目录下重新生成,完整命令为:

mkdir -p src/module_bindings spacetime generate --lang rust \ --out-dir src/module_bindings \ --module-path ../../../../modules/sdk-test-connect-disconnect
  • --module-path指向模块源码目录(这里是相对connect_disconnect_client目录的路径);
  • 生成后即可通过crate::module_bindings::*使用ConnectedDisconnected表访问器以及自动生成的DbConnection类型。

七、从测试反推的生产实践要点

这个测试模块虽然短小,却浓缩了 SpacetimeDB 生命周期事件编程的几个核心实践:

  1. 事件落表,数据可观测:将连接/断开事件转换为表行插入,是让事件可被订阅、可被追溯的标准做法。在线状态功能可以在此基础上扩展(如增加connected_at时间戳、disconnected_at字段,或对同一身份去重)。
  2. sender 即身份ReducerContext::sender()(C# 为ctx.Sender)提供触发事件的客户端Identity,是生命周期 reducer 中获取客户端身份的唯一标准途径,其正确性由该测试的订阅断言直接背书。
  3. 错误语义要分清client_connected中出错会断开客户端,而client_disconnected中出错不影响断开结果——生产代码应在连接回调中格外谨慎,避免因初始化逻辑失败把正常用户踢下线。
  4. 跨语言一致性:Rust 与 C# 模块共享同一套测试客户端与断言逻辑,确保生命周期语义在不同语言绑定之间不漂移,这也是新语言模块接入时的对照样板。

结语

sdk-test-connect-disconnect以最小的代码体量,验证了 SpacetimeDB 连接生命周期机制从"模块侧 reducer 捕获"到"客户端回调感知、订阅观察数据"的完整闭环。其测试用例connect_disconnect_callbacks的注册位置(sdks/rust/tests/test.rs)、客户端断言细节(sdks/rust/tests/connect_disconnect_client/src/test_handlers.rs)以及生命周期 reducer 的官方语义说明(crates/bindings/src/lib.rs),共同构成了理解 SpacetimeDB 实时连接模型最直观的入门教材。

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询