SpacetimeDB Submodules 完整指南:用命名空间复用数据库逻辑(TypeScript)

发布时间:2026/9/12 20:26:04
SpacetimeDB Submodules 完整指南:用命名空间复用数据库逻辑(TypeScript) SpacetimeDB Submodules 完整指南用命名空间复用数据库逻辑TypeScript【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDBSubmodule子模块是 SpacetimeDB 提供的一种模块级复用机制一个模块可以被另一个模块的数据库以“库”的形式引入其表、reducer、procedure、视图与 HTTP 处理器在消费方指定的命名空间namespace下注册与消费方自身的表及其他子模块天然隔离。本文以官方核心概念文档 00600-submodules.md 为主线结合仓库中 TypeScript 绑定、代码生成器与测试用例的源码实现完整讲解如何编写子模块、注册命名空间、跨上下文调用函数、嵌套组合以及客户端侧调用方式帮助你在不协调表名的前提下把可复用的数据库逻辑打包成任意消费者可直接集成的库。:::note 子模块目前仅在 TypeScript 中受支持spacetimedb/serverRust、C# 与 C 的支持即将到来。本文所有示例均为 TypeScript。 :::什么是 Submodule一个submodule是一个可以被包含进另一个模块数据库的 SpacetimeDB 模块。子模块的表与函数会在你选择的命名空间下注册从而与消费方自己的表以及其他子模块保持隔离。子模块的核心价值在于把可复用的数据库逻辑打包成库任何消费方都可以集成它而无需协调表名。例如你可以把身份认证、会话管理、支付等通用逻辑写成独立模块多个业务数据库通过不同命名空间引入同一份实现互不冲突。从实现层面看子模块在模块定义ModuleDef中以Submodules段存在。仓库测试 schema_submodules.test.ts 验证了这一点消费方构建 RawModuleDefV10 后raw.sections中会出现tag Submodules的段其中携带namespace与完整的子模块moduleconst raw consumer.buildRawModuleDefV10({}); const submodules raw.sections.find( section section.tag Submodules )?.value; expect(submodules).toHaveLength(1); expect(submodules?.[0]?.namespace).toBe(myauth);编写一个 Submodule一个子模块就是一个普通的 SpacetimeDB 模块——没有任何特殊标记区分它是否是子模块。你需要将 schema 作为default export导出将消费方需要注册的每个函数reducer、procedure、视图、HTTP 处理器全部具名导出。以下是一个完整的认证库示例来自官方文档的auth_lib/src/index.ts// auth_lib/src/index.ts import { schema, table, t, SyncResponse, Router, type ReducerCtx } from spacetimedb/server; const users table( { name: users, public: true }, { identity: t.identity().primaryKey(), username: t.string() } ); const sessions table( { name: sessions }, { id: t.u64().primaryKey().autoInc(), userIdentity: t.identity(), token: t.string(), } ); const spacetimedb schema({ users, sessions }); export default spacetimedb; export const verifyToken spacetimedb.reducer( { token: t.string() }, (ctx, { token }) { /* ... */ } ); export const sessionCount spacetimedb.procedure( t.u64(), (ctx) ctx.withTx(tx tx.db.sessions.count()) ); export const activeSessions spacetimedb.anonymousView( { name: active_sessions, public: true }, t.array(sessions.rowType), (ctx) [...ctx.db.sessions.iter()] ); export const health spacetimedb.httpHandler( (_ctx, _req) new SyncResponse(ok) ); export const router spacetimedb.httpRouter( new Router().get(/health, health) );注意以下几个要点default export 是 schemaexport default spacetimedb这是消费方 schema 处理子模块时识别其表结构的入口具名导出全部可注册函数verifyTokenreducer、sessionCountprocedure、activeSessions匿名视图、healthHTTP 处理器、routerHTTP 路由。源码层面消费方注册子模块时会对整个 module-namespace 对象执行“导出扫描”见 schema.ts 的buildSubmoduleDispatch它把子模块的 reducers、procedures、anonViews、views、tables、typespace 以及嵌套的subDispatches全部收集进SubmoduleDispatchInfo同一文件可以双重身份既可以作为独立数据库发布也可以被其他模块作为子模块引入。模块自身不需要声明扮演哪个角色——这是“子模块即普通模块”设计的关键。使用一个 Submodule消费方consumer控制命名空间名称把子模块的 module-namespace 对象以你选择的别名传给schema({ ... })。// my-database/src/index.ts import { schema, table } from spacetimedb/server; import * as authLib from auth_lib; const players table({ name: players, public: true }, { /* ... */ }); const spacetimedb schema({ players, myauth: authLib, // 将 auth_lib 注册到命名空间 myauth }); export default spacetimedb;消费方唯一必需的 JS 导出是export default spacetimedb注册子模块后其全部 reducer、procedure、视图、定时表scheduled tables与 HTTP 处理器会自动注册。测试 schema_submodules.test.ts 验证了子模块的 Schedules 段也会被正确解析sessionCleanupTick定时表与cleanExpiredSessionsreducer 出现在子模块的Schedules段中。必须使用import * as而不是 default importimport authLib from auth_lib; // ❌ 只暴露 schema丢失所有具名导出 import * as authLib from auth_lib; // ✅ 正确写法只做 default import 时拿到的是子模块的 schema 对象其具名导出reducers、procedures、views、handlers全部丢失。子模块 walker 需要完整的 module-namespace 对象如果用了错误的导入形式会得到一个明确的报错。仓库源码 schema.ts 中的实现印证了这一点当 schema 条目直接是Schema实例即默认导入的结果时抛出schema entry myauth looks like a default import; use import * as myauth from ... so the submodule can see the librarys named reducer exports.对应测试见 schema_submodules.test.ts 的rejects default-import style submodules with a clear error。访问子模块的表与视图子模块的表出现在ctx.db上的一个命名空间字段中字段名与你选择的别名一致。子模块导出的视图从客户端视角看与表行为一致可以在订阅和 SQL 查询中以namespace.view_name访问其中view_name是规范的 snake_case 名称——TypeScript 中的activeSessions在 SQL 中是active_sessions。生成的客户端绑定则暴露 camelCase 访问器。export const example spacetimedb.reducer((ctx) { // 消费方自己的表无命名空间 for (const player of ctx.db.players.iter()) { /* ... */ } // 子模块的表注册为 myauth const user ctx.db.myauth.users.identity.find(ctx.sender); for (const session of ctx.db.myauth.sessions.iter()) { /* ... */ } });从源码看ctx.db的命名空间字段与ctx.as别名代理由 runtime.ts 中的ReducerCtxImpl承载db是根DbViewas是子模块别名视图集合。测试 ctx_as.test.ts 验证了ctx.as.myauth.db.sessions可访问并且ctx.as.myauth.sender与父上下文的sender相同。调用子模块函数子模块可以暴露 reducer、procedure、视图与 HTTP 处理器供消费方在自己的函数中调用。由于子模块的上下文类型与消费方的上下文类型不同需要先用ctx.as.alias收窄上下文再传给子模块函数。从 Reducer 中调用使用ctx.as.alias调用子模块 reducer或调用以子模块自身 schema 为类型的普通辅助函数// auth_lib: 以子模块自身 schema 为类型的普通辅助函数 export function sessionCountHelper(ctx: ReducerCtxtypeof spacetimedb): number { return ctx.db.sessions.count(); } // my-database: 在消费方 reducer 中调用子模块 reducer 与辅助函数 export const onLogin spacetimedb.reducer({ token: t.string() }, (ctx, { token }) { // 调用子模块 reducer authLib.verifyToken(ctx.as.myauth, { token }); // 调用子模块辅助函数 const count authLib.sessionCountHelper(ctx.as.myauth); console.log(Active sessions: ${count}); });关于ctx.as.myauth需要理解两点它是一个收窄到myauth命名空间的ReducerCtx与父上下文共享sender、timestamp、connectionId但它的ctx.db指向ctx.db.myauth。源码实现见 runtime.ts 的buildAliasCtxMap——它为每个SubmoduleDispatchInfo构建一个委托代理对象getter 会转发sender、timestamp、connectionId、identity、random、http等属性而db则是基于子模块 dispatch 信息构建的命名空间视图buildDbViewForDispatch对于通过子模块自身 schema 注册的 reducer即schema.reducer(...)当它们被直接调用时宿主会自动传入收窄后的上下文。ctx.as只在消费方显式调用子模块函数时才需要。从 Procedure 中调用使用ctx.as.alias把子模块作用域的ProcedureCtx传给子模块 procedure。要在 procedure 内部调用子模块 reducer需要先用ctx.withTx开启事务再用tx.as.alias收窄// 调用子模块 procedure export const stats spacetimedb.procedure( t.u64(), (ctx) authLib.sessionCount(ctx.as.myauth) ); // 在 withTx 块内调用子模块 reducer export const transactAndCount spacetimedb.procedure( { token: t.string() }, t.u64(), (ctx, { token }) { ctx.withTx(tx { // tx 是根 ReducerCtx收窄到子模块命名空间 authLib.verifyToken(tx.as.myauth, { token }); }); return authLib.sessionCount(ctx.as.myauth); } );源码层面withTx内部新建的tx是一个根级ReducerCtxImpl通过 assignTxAliasViews 为它挂上as别名代理而子模块作用域上下文自己的withTx见 runtime.ts则会直接构建指向该命名空间 db 视图的新事务上下文并继续携带subAs以支持更深层的嵌套命名空间。从 HTTP Handler 中调用把ctx.as.alias和请求一起传给子模块的 HTTP handler 函数然后把结果注册到消费方自己的 router 上import { Router } from spacetimedb/server; // 将 /health 路由委托给子模块的 handler export const healthCheck spacetimedb.httpHandler((ctx, req) { return authLib.health(ctx.as.myauth, req); }); export const router spacetimedb.httpRouter( new Router().get(/health, healthCheck) );注意子模块自身定义的httpRouter不会自动生效详见下文“限制”一节因此这里必须由消费方显式委托并注册路由。多个与嵌套子模块多个子模块可以自由组合每个子模块拥有自己的命名空间因此子模块之间的命名冲突不可能发生import * as authLib from auth_lib; import * as paymentLib from payment_lib; const spacetimedb schema({ players, myauth: authLib, payments: paymentLib, }); export default spacetimedb;子模块本身也可以用同样的语法包含其他子模块嵌套。嵌套子模块的表在顶层消费方中以两级路径出现// auth_lib 以 sessions 为别名引入 session_lib const authSchema schema({ users, sessions: sessionLib }); export default authSchema; // 消费方以 myauth 为别名引入 auth_lib // session_lib 的表位于 ctx.db.myauth.sessions.table这一行为在源码与测试中都有明确印证SubmoduleDispatchInfo中的subDispatches字段承载嵌套关系见 schema.ts测试 schema_submodules.test.ts 验证了嵌套子模块的 dispatches 会深度优先扁平化consumer.submoduleDispatchInfos[0]是myauth含authReducer其subDispatches[0]是myauth.baz含bazReducer测试 ctx_as.test.ts 验证了ctx.as.myauth.as.baz.db.bazTable的链式访问且每一级都继承根上下文的sender。生命周期 reducerinit、clientConnected、clientDisconnected是例外它们只允许出现在根模块中。任何包含定义了生命周期 reducer 的子模块的模块都会发布失败。客户端订阅客户端订阅使用与服务器端访问相同的命名空间结构子模块表与视图以namespace.name形式查询。conn.subscriptionBuilder().subscribe(tables [ tables.players.build(), // public.players tables.myauth.users.build(), // myauth.users tables.myauth.activeSessions.build(), // myauth.activeSessions 视图 ]);从客户端调用子模块的 Reducer 与 Procedure子模块的 reducer 与 procedure 通过全限定名标识命名空间与函数名之间用.分隔。客户端 SDK在生成的绑定中子模块的表、视图、reducer、procedure 以嵌套对象的形式出现在命名空间别名之下。tables、reducers、procedures三个导出都反映同样的嵌套结构。代码生成器 typescript.rs 中可以看到生成绑定会为子模块命名空间导入公共表行类型、视图行类型、reducer 参数类型与 procedure 参数类型submodule_ns_path按命名空间前缀组织文件路径。React hooks 用法import { tables, reducers, procedures } from ./module_bindings; import { useTable, useReducer, useProcedure } from spacetimedb/react; // 订阅子模块表与视图 const [users] useTable(tables.myauth.users); const [activeSessions] useTable(tables.myauth.activeSessions); // 调用子模块 reducer const verifyToken useReducer(reducers.myauth.verifyToken); verifyToken({ token: abc123 }); // 调用子模块 procedure const sessionCount useProcedure(procedures.myauth.sessionCount); sessionCount().then(count console.log(Sessions: ${count}));Vanilla非 React用法import { DbConnection, tables, reducers, procedures } from ./module_bindings; const conn DbConnection.builder() .withUri(SPACETIMEDB_URI) .withDatabaseName(my-database) .onConnect(ctx { ctx.subscriptionBuilder() .subscribe([tables.myauth.users, tables.myauth.activeSessions]); }) .build(); conn.reducers.myauth.verifyToken({ token: abc123 });HTTP APIPOST /v1/database/my-database/call/myauth.verify_tokenCLIspacetime call my-database myauth.verify_token {token: abc123}命名空间前缀就是你所选的别名.之后的函数名是子模块导出名的规范 snake_case 形式——TypeScript 中的verifyToken在网络上传输时是verify_token。生成的客户端绑定则如前所述暴露 camelCase 访问器。命名空间名称规则你选择的别名会成为SQL 层面的命名空间名称必须满足 SpacetimeDB 标识符规则以字母或下划线开头后续字符可以是字母、数字或下划线最长63 个字符解析时大小写不敏感case-insensitive一个子模块在每个消费方模块中最多只能注册一个别名。从 identifier.rs 的实现可以看到SpacetimeDB 的标识符遵循 Unicode 标准附录 UAX #15NFC 规范化与 UAX #31XID_Start 起始规则并禁止使用 PostgreSQL 保留字reserved_identifiers.txt中列出因此命名空间别名的校验比“仅限 ASCII”更严格——建议在实际使用中遵循小写 snake_case 以避免大小写解析歧义。保留命名空间不可用作子模块别名public、st、spacetimedb以及pg_*前缀。限制子模块的 router 不会自动生效子模块可以定义自己的httpRouter但作为子模块使用时该 router 会被忽略——只有消费方的根 router 会被使用。若要暴露子模块的 HTTP 处理器必须按上文“从 HTTP Handler 中调用”一节的方式使用ctx.as.alias显式地把它们注册到消费方的 router 上。小结子模块 普通模块 具名导出全部函数 default 导出 schema同一份代码可独立发布也可被引入消费方用别名控制命名空间必须import * as以保留具名导出ctx.as.alias是跨上下文调用的关键收窄到子模块作用域后调用其 reducer、procedure、辅助函数与 HTTP handlerprocedure 内调用 reducer 需先withTx再tx.as.alias多级嵌套自由组合命名冲突天然隔离但生命周期 reducer 仅限根模块客户端侧通过命名空间限定的全限定名SDK 嵌套对象、HTTP API、CLI调用子模块函数。进一步阅读官方概念文档 00600-submodules.md、子模块 schema 处理实现 schema.ts、运行时别名代理 runtime.ts、行为测试 schema_submodules.test.ts 与 ctx_as.test.ts、绑定代码生成器 typescript.rs以及标识符校验规则 identifier.rs。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考