mcpField
T
typescript-server

typescript-server

github.com/clockworklabs开发25.4k79 次安装今天更新
代码仓库

关于 typescript-server

用于构建 SpacetimeDB 服务端模块的 TypeScript SDK 参考资料,介绍 Schema、表、Reducer、视图、Procedure、HTTP 处理和数据库操作,面向模块开发者。

主要功能

  1. 01Schema 与表定义
  2. 02Reducer 与生命周期钩子
  3. 03索引数据库操作
  4. 04过程式视图与查询构建视图
  5. 05Procedure 与 HTTP 处理
  6. 06自定义类型与定时表

适用场景

  1. 01定义模块的数据库 Schema
  2. 02实现 Reducer 与生命周期逻辑
  3. 03通过视图提供筛选数据
  4. 04添加 Procedure 或 HTTP 端点

安装

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

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

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

详细介绍

SpacetimeDB TypeScript SDK 参考

模块结构

使用 table() 构建表,使用 schema() 绑定表,并将其作为默认导出。Reducer 和生命周期钩子使用 export const 导出:

import { schema, table, t } from 'spacetimedb/server';

const scoreRecord = table(
  { name: 'score_record', public: true },
  {
    id: t.u64().primaryKey().autoInc(),
    owner: t.identity(),
    value: t.u32(),
  }
);

const spacetimedb = schema({ scoreRecord });  // ONE object, not spread args
export default spacetimedb;

export const addRecord = spacetimedb.reducer(
  { value: t.u32() },
  (ctx, { value }) => {
    ctx.db.scoreRecord.insert({ id: 0n, owner: ctx.sender, value });
  }
);

schema({...}) 中只能包含表定义。用作 reducer 参数或视图返回类型的行和对象构建器不属于 schema 条目。

命名的运行时导出仅用于向 SpacetimeDB 注册的值,例如 reducer、生命周期钩子、视图、过程、HTTP 导出和可见性筛选器。普通辅助函数和常量不要导出。

导入

Schema 构建器和模块导出来自 spacetimedb/server。ScheduleAt、Timestamp 和 ConnectionId 等运行时值类来自根 spacetimedb 包;Range 来自 spacetimedb/server:

import {
  schema, table, t, SenderError,
  type InferSchema, type ReducerCtx,
} from 'spacetimedb/server';
import { ConnectionId, ScheduleAt, Timestamp } from 'spacetimedb';
import { Range } from 'spacetimedb/server';

表

table(OPTIONS, COLUMNS) 接受两个参数。name 字段必须使用 snake_case:

const entity = table(
  { name: 'entity', public: true },
  {
    identity: t.identity().primaryKey(),
    name: t.string(),
    active: t.bool(),
  }
);

选项:name(snake_case,推荐)、public: true、event: true、indexes: [...]

ctx.db 访问器与传入 schema({...}) 的键完全相同:schema({ scoreRecord }) -> ctx.db.scoreRecord。TypeScript 标识符和访问器使用 camelCase。需要使用不同于 TypeScript 标识符的规范数据库名称时,请使用显式的 name: 'snake_case' 字符串。

列类型

每一列都是一个 t 构建器值:

构建器JS 类型备注
t.u8() / t.u16() / t.u32()number
t.i8() / t.i16() / t.i32()number
t.u64()bigint使用 0n 字面量
t.i64()bigint使用 0n 字面量
t.u128() / t.i128() / t.u256() / t.i256()bigint
t.f64() / t.f32()number
t.bool()boolean
t.string()string
t.identity()Identity
t.connectionId()ConnectionId
t.timestamp()Timestamp
t.timeDuration()TimeDuration
t.scheduleAt()ScheduleAt

修饰符:.primaryKey()、.autoInc()、.unique()、.index('btree')、.default(value)。

仅对新增且兼容迁移的字段使用 .default(value)。不要为主键、唯一列或自动递增列设置默认值。

可选列:nickname: t.option(t.string())

Schema 构建器描述数据库的线缆类型;它们不是 TypeScript 类型名称。例如,t.u16() 值的 TypeScript 类型是 number,而不是转换成名为 u16 的类型的值。

索引

如果单列索引不需要命名访问器,请使用内联 .index('btree')。如果访问器有显式名称,或索引涵盖多列,请使用 indexes 条目。每个 indexes 条目都必须包含 columns;同一列上不要同时添加 .index('btree')。

// Inline (preferred for single-column):
authorId: t.u64().index('btree'),
// Access: ctx.db.post.authorId.filter(authorId);

// Multi-column (named):
indexes: [{ accessor: 'by_group_user', algorithm: 'btree', columns: ['groupId', 'userId'] }]
// Access: ctx.db.membership.by_group_user.filter([groupId, userId]);

优先使用多列索引,而不是按单列筛选后再循环。filter 接受按索引列顺序排列的数组;前缀扫描直接传入首个值:filter(groupId)。

已发布模块的入口文件必须将 schema 作为默认导出。如果将表(schema.ts)与 reducer/生命周期代码(index.ts)拆分,请在入口文件中重新导出:

// index.ts
export { default } from './schema';   // re-export the schema for the module entry

Reducer

使用 spacetimedb.reducer(...) 创建 reducer;导出名称将成为 reducer 名称:

export const createEntity = spacetimedb.reducer(
  { name: t.string(), age: t.i32() },
  (ctx, { name, age }) => {
    ctx.db.entity.insert({ identity: ctx.sender, name, age, active: true });
  }
);

// No arguments, just the callback:
export const doReset = spacetimedb.reducer((ctx) => { ... });

Reducer 参数接受任何列类型,包括自定义类型数组:{ splits: t.array(Split) }。结构化数据不要传递 JSON 字符串。

数据库操作

ctx.db.scoreRecord.insert({ id: 0n, owner: ctx.sender, value: 1 });  // Insert (0n for autoInc)
ctx.db.scoreRecord.id.find(recordId);                     // Find by PK → row | null
ctx.db.entity.identity.find(ctx.sender);                   // Find by unique column
[...ctx.db.post.authorId.filter(authorId)];                // Filter → spread to Array
[...ctx.db.entity.iter()];                                 // All rows → Array
ctx.db.scoreRecord.id.update({ ...existing, value: 2 });  // Update (spread + override)
ctx.db.scoreRecord.id.delete(recordId);                   // Delete by PK

通过表访问器插入(ctx.db.scoreRecord.insert(...))。主键、唯一键和索引访问器支持查找或修改现有行,但不提供 insert(...)。

insert(...) 会返回插入的行,包括数据库分配的自增字段。

主键或索引的访问器名称是声明的列名。例如,名为 eventId 的主键应通过 ctx.db.event.eventId 访问,而不是 ctx.db.event.id。

schema 值会注册模块导出,但不会公开数据库行。需要使用 ctx.db 的辅助函数应接收 context。

注意:iter() 和 filter() 会返回迭代器。使用展开语法将其转换为数组,以便调用 .sort()、.filter()、.map()。

生命周期钩子

必须使用 export const。单独调用会被静默忽略:

export const init = spacetimedb.init((ctx) => { ... });
export const onConnect = spacetimedb.clientConnected((ctx) => { ... });
export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { ... });

ctx.connectionId 的类型为 ConnectionId | null,包括在生命周期 context 中。将其传递给辅助函数或用作表键之前,请先进行检查。

Reducer Context API

ctx 是发送者身份、时间和随机数的唯一来源;模块中无法使用 stdlib 时钟和 RNG。让导出的回调推断其 context 类型。在辅助函数中,使用 ReducerCtx<InferSchema<typeof spacetimedb>>;不要将 context 标注为 any,因为这会抹除表行类型,并可能导致 bigint 表达式被推断为 number。

type Ctx = ReducerCtx<InferSchema<typeof spacetimedb>>;

function findRecord(ctx: Ctx, id: bigint) {
  return ctx.db.scoreRecord.id.find(id);
}
// Auth: ctx.sender is the caller's Identity
if (!row.owner.equals(ctx.sender)) throw new SenderError('unauthorized');

// ctx.connectionId: the per-connection id, NULLABLE (ConnectionId | null) — null-check before use.
// One Identity can hold several connections (multiple tabs/devices).
if (ctx.connectionId) { /* ... */ }

// Server timestamp (deterministic per reducer call)
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });

// Deterministic RNG
const f: number = ctx.random();                          // [0.0, 1.0)
const roll: number = ctx.random.integerInRange(1, 6);    // safe JS number bounds/result, inclusive
const storedRoll: bigint = BigInt(roll);                 // convert the result for an i64/u64 column
const bytes: Uint8Array = ctx.random.fill(new Uint8Array(16));

// Client: Timestamp → Date
new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));

不要从字符串构造 Identity 值(例如 'hex' as Identity):序列化会失败并导致模块终止。Identity 来自 ctx.sender 或 t.identity() 列。

导入 spacetimedb 中的 ConnectionId 后,使用 new ConnectionId(value) 从其数值表示构造 ConnectionId。

导入 spacetimedb 中的 Timestamp 后,使用 new Timestamp(micros) 构造精确时间戳。包含端点的索引范围使用 spacetimedb/server 中的 Range:

import { Timestamp } from 'spacetimedb';
import { Range } from 'spacetimedb/server';

ctx.db.shipment.deliverBy.filter(new Range(
  { tag: 'included', value: new Timestamp(1_000n) },
  { tag: 'included', value: new Timestamp(2_000n) },
));

定时表

表的 scheduled 选项所引用的 reducer 或 procedure 必须导出。

import { ScheduleAt } from 'spacetimedb';   // ScheduleAt comes from the root package

const tickTimer = table({
  name: 'tick_timer',
}, {
  scheduledId: t.u64().primaryKey().autoInc(),
  scheduledAt: t.scheduleAt(),
});

export const tick = spacetimedb.reducer(
  { onSchedule: tickTimer },
  { timer: tickTimer.rowType },
  (ctx, { timer }) => { /* timer row auto-deleted after this runs */ }
);

// `onSchedule` also works for scheduled procedures whose return type is `t.unit()`.
// Legacy table-side scheduling, `scheduled: (): any => tick`, still works but is not
// recommended for new code because it forces a forward reference.

// One-time: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + delayMicros)
// Repeating: ScheduleAt.interval(60_000_000n)
// Read time back from a scheduleAt value (tagged union):
//   const micros = at.tag === 'time' ? at.value : at.value.microsSinceUnixEpoch;  // bigint

自定义类型

// Product type (struct):
const Position = t.object('Position', { x: t.i32(), y: t.i32() });
const entity = table({ name: 'entity' }, {
  id: t.u64().primaryKey().autoInc(),
  pos: Position,
});

// Sum type (tagged union):
const Shape = t.enum('Shape', {
  circle: t.i32(),
  rectangle: t.object('Rect', { w: t.i32(), h: t.i32() }),
});
// Values: { tag: 'circle', value: 10 }

视图

客户端订阅视图后,只会收到该视图返回的行。对于按查看者控制访问权限,应使用按用户划分的视图 (以 ctx.sender 为键):删除其依赖的行(例如成员资格行)时,该视图向该客户端公开的行也会自动移除。

t.row(...) 和 t.object(...) 会返回 schema 构建器,而不是 TypeScript 运行时行类型。让视图回调推断其结果,或标注单独声明的结构类型,例如 Array<{ sku: bigint; label: string }>。命名输出类型不得复用视图访问器生成的 PascalCase 名称(例如,应将 DiscountedProduct 留给 discounted_product 视图)。

spacetimedb.view(...) 和 spacetimedb.anonymousView(...) 都接受三个参数:视图选项、声明的返回 schema 和回调。

// Anonymous view (same for all clients):
export const activeUsers = spacetimedb.anonymousView(
  { name: 'active_users', public: true },
  t.array(entity.rowType),
  (ctx) => [...ctx.db.entity.iter()].filter(e => e.active)
);

// Per-user view (varies by ctx.sender):
export const myProfile = spacetimedb.view(
  { name: 'my_profile', public: true },
  t.option(entity.rowType),
  (ctx) => ctx.db.entity.identity.find(ctx.sender) ?? undefined
);

对于过程式视图的主键,使用 t.row 定义输出,并为其字段标记 .primaryKey():

const CatalogKey = t.row('CatalogKey', {
  sku: t.u64().primaryKey(),
  label: t.string(),
});

查询构建器视图使用 ctx.from,并直接返回查询。由于查询会返回行集,因此应将其返回 schema 声明为 t.array(tableName.rowType)。使用 where 编写谓词;若结果应包含具有匹配左侧行的右侧行,则使用 rightSemijoin:

ctx => ctx.from.article.where(article => article.published.eq(true))
ctx => ctx.from.subscription.rightSemijoin(
  ctx.from.account,
  (subscription, account) => subscription.accountId.eq(account.id)
)

方法名称决定返回哪一侧:A.leftSemijoin(B, ...) 返回 A 中的行,而 A.rightSemijoin(B, ...) 返回 B 中的行。要在一个表有匹配项时返回另一个表的行,请将要返回的表放在对应一侧。半连接不会投影两个表中的组合列。

过程式视图通过 ctx.db 读取数据,并返回数组等物化值。ctx.from 中的查询构建器值会直接返回;它们不是迭代器,不能展开、循环处理,也不能与数组方法混用。当结果是由多个表组合而成的自定义行时,请使用过程式视图。

客户端可见性筛选器

export const privateNoteFilter = spacetimedb.clientVisibilityFilter.sql(
  'SELECT * FROM owned_row WHERE owner = :sender'
);

过程与 HTTP

spacetimedb 是调用 schema({...}) 返回的本地架构值;它不是可从 spacetimedb/server 导入的具名导出。

过程声明参数和返回值类型:

const Result = t.object('Result', { value: t.string() });

export const inspect = spacetimedb.procedure(
  { input: t.string() },
  Result,
  (_ctx, { input }) => ({ value: input })
);

过程回调是同步的。不要将其标记为 async,也不要使用 await;直接返回声明的值。返回类型为 t.unit() 的过程会返回 {}。

TypeScript 的出站 HTTP 使用 ctx.http.fetch(url, options),包括非 GET 请求;它不提供 get() 或 post() 等便捷方法。响应提供数值型 status、headers.get(name) 和 text() API。

t.array(t.u8()) 的值是 number[]。将其转换为 new Uint8Array(value) 后,再用作二进制请求体。

过程和处理程序使用 ctx.withTx(tx => ...) 开启短时数据库事务。在开启事务前执行网络 I/O;回调中只应包含数据库操作。

定时过程使用与定时 reducer 相同的 onSchedule 绑定。过程必须导出,返回 t.unit(),并将定时行作为参数接收。

入站 HTTP 使用 httpHandler、httpRouter、Router 和 SyncResponse:

httpHandler 和 httpRouter 是本地 spacetimedb 架构值上的方法,而非具名导入。

import { Router, SyncResponse } from 'spacetimedb/server';

export const health = spacetimedb.httpHandler((_ctx, _request) =>
  new SyncResponse('ok', {
    status: 200,
    headers: { 'content-type': 'text/plain' },
  })
);
export const routes = spacetimedb.httpRouter(new Router().get('/health', health));

处理程序是同步的:直接返回 SyncResponse,不要将回调标记为 async。将导出的 httpHandler(...) 值传给路由器,而非其原始回调。路由器负责选择路径,处理程序则使用 request.text() 等 API 读取请求数据;Request 没有 path 属性。处理程序上下文不提供 ctx.db;如果处理程序需要事务性数据库访问,请使用 ctx.withTx(tx => ...)。

评论

支持 Markdown