spacetimedb-typescript
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSpacetimeDB TypeScript SDK
SpacetimeDB TypeScript SDK
Build real-time TypeScript clients that connect directly to SpacetimeDB modules. The SDK provides type-safe database access, automatic synchronization, and reactive updates for web apps, Node.js, Deno, Bun, and other JavaScript runtimes.
构建可直接连接SpacetimeDB模块的实时TypeScript客户端。该SDK为Web应用、Node.js、Deno、Bun及其他JavaScript运行时提供类型安全的数据库访问、自动同步和响应式更新功能。
HALLUCINATED APIs — DO NOT USE
虚构的API — 请勿使用
These APIs DO NOT EXIST. LLMs frequently hallucinate them.
typescript
// WRONG PACKAGE — does not exist
import { SpacetimeDBClient } from "@clockworklabs/spacetimedb-sdk";
// WRONG — these methods don't exist
SpacetimeDBClient.connect(...);
SpacetimeDBClient.call("reducer_name", [...]);
connection.call("reducer_name", [arg1, arg2]);
// WRONG — positional reducer arguments
conn.reducers.doSomething("value"); // WRONG!这些API并不存在,是大语言模型经常虚构出来的。
typescript
// WRONG PACKAGE — does not exist
import { SpacetimeDBClient } from "@clockworklabs/spacetimedb-sdk";
// WRONG — these methods don't exist
SpacetimeDBClient.connect(...);
SpacetimeDBClient.call("reducer_name", [...]);
connection.call("reducer_name", [arg1, arg2]);
// WRONG — positional reducer arguments
conn.reducers.doSomething("value"); // WRONG!CORRECT PATTERNS:
正确用法示例:
typescript
// CORRECT IMPORTS
import { DbConnection, tables } from './module_bindings'; // Generated!
import { SpacetimeDBProvider, useTable, Identity } from 'spacetimedb/react';
// CORRECT REDUCER CALLS — object syntax, not positional!
conn.reducers.doSomething({ value: 'test' });
conn.reducers.updateItem({ itemId: 1n, newValue: 42 });
// CORRECT DATA ACCESS — useTable returns [rows, isLoading]
const [items, isLoading] = useTable(tables.item);typescript
// CORRECT IMPORTS
import { DbConnection, tables } from './module_bindings'; // Generated!
import { SpacetimeDBProvider, useTable, Identity } from 'spacetimedb/react';
// CORRECT REDUCER CALLS — object syntax, not positional!
conn.reducers.doSomething({ value: 'test' });
conn.reducers.updateItem({ itemId: 1n, newValue: 42 });
// CORRECT DATA ACCESS — useTable returns [rows, isLoading]
const [items, isLoading] = useTable(tables.item);DO NOT:
请勿:
- Invent hooks like ,
useItems()— useuseData()useTable(tables.tableName) - Import from fake packages — only ,
spacetimedb,spacetimedb/react./module_bindings
- 自定义钩子如、
useItems()— 请使用useData()useTable(tables.tableName) - 导入不存在的包 — 仅可导入、
spacetimedb、spacetimedb/react./module_bindings
Common Mistakes Table
常见错误对照表
Server-side errors
服务端错误
| Wrong | Right | Error |
|---|---|---|
Missing | Create | "could not detect language" |
Missing | Create | "TsconfigNotFound" |
Entrypoint not at | Use | Module won't bundle |
| | "reading 'tag'" error |
Index without | | "reading 'tag'" error |
| | "does not exist in type 'Range'" |
| | TypeError |
| | "Property 'id' is missing" |
| | |
| Just use | "name is used for multiple entities" |
| Import spacetimedb from index.ts | Import from schema.ts | "Cannot access before initialization" |
Multi-column index | Use single-column index | PANIC or silent empty results |
| Use index lookups only | Views can't scan tables |
| | Procedures need explicit transactions |
| | Wrong context variable |
| 错误做法 | 正确做法 | 错误信息 |
|---|---|---|
缺少 | 创建 | "could not detect language" |
缺少 | 创建 | "TsconfigNotFound" |
入口文件不在 | 使用 | 模块无法打包 |
在COLUMNS(第二个参数)中设置 | 在OPTIONS(第一个参数)中设置 | "reading 'tag'" error |
索引未指定 | 添加 | "reading 'tag'" error |
使用 | 使用 | "does not exist in type 'Range'" |
在唯一列上调用 | 在唯一列上调用 | TypeError |
| | "Property 'id' is missing" |
| | |
同时使用 | 仅使用 | "name is used for multiple entities" |
| 从index.ts导入spacetimedb | 从schema.ts导入 | "Cannot access before initialization" |
对多列索引使用 | 使用单列索引 | 程序崩溃或返回空结果 |
在视图中使用 | 仅使用索引查询 | 视图无法扫描整张表 |
在过程中使用 | 使用 | 过程需要显式事务 |
在过程事务中使用 | 使用 | 上下文变量错误 |
Client-side errors
客户端错误
| Wrong | Right | Error |
|---|---|---|
| | 404 / missing subpath |
| | Wrong reducer syntax |
Inline | | Reconnects every render |
| | Tuple destructuring |
| Optimistic UI updates | Let subscriptions drive state | Desync issues |
| | Wrong prop name |
| 错误做法 | 正确做法 | 错误信息 |
|---|---|---|
使用 | 使用 | 404 / 缺少子路径 |
| | Reducer语法错误 |
内联 | 使用 | 每次渲染都会重新连接 |
| | 需使用元组解构 |
| 乐观UI更新 | 由订阅驱动状态 | 数据不一致问题 |
| | 属性名称错误 |
Hard Requirements
硬性要求
- DO NOT edit generated bindings — regenerate with
spacetime generate - Reducers are transactional — they do not return data
- Reducers must be deterministic — no filesystem, network, timers, random
- Reducer calls use object syntax — not positional args
{ param: 'value' } - Import from
DbConnection— not from./module_bindingsspacetimedb - useTable returns a tuple —
const [rows, isLoading] = useTable(tables.myTable) - Memoize connectionBuilder — wrap in to prevent reconnects
useMemo(() => ..., []) - Views can only use index lookups — is not allowed in views
.iter()
- 请勿编辑生成的绑定代码 — 请使用重新生成
spacetime generate - Reducer是事务性的 — 它们不会返回数据
- Reducer必须是确定性的 — 不可使用文件系统、网络、定时器或随机数
- 调用Reducer必须使用对象语法 — 如,而非位置参数
{ param: 'value' } - 从导入
./module_bindings— 不可从DbConnection导入spacetimedb - useTable返回元组 —
const [rows, isLoading] = useTable(tables.myTable) - 对connectionBuilder进行记忆化 — 用包裹以避免重复连接
useMemo(() => ..., []) - 视图仅可使用索引查询 — 不允许在视图中使用
.iter()
Installation
安装步骤
bash
npm install spacetimedbbash
npm install spacetimedbor
or
pnpm add spacetimedb
pnpm add spacetimedb
or
or
yarn add spacetimedb
For Node.js 18-21, install the `undici` peer dependency:
```bash
npm install spacetimedb undiciNode.js 22+ and browser environments work out of the box.
yarn add spacetimedb
对于Node.js 18-21版本,需安装`undici` peer依赖:
```bash
npm install spacetimedb undiciNode.js 22+版本及浏览器环境可直接使用,无需额外依赖。
Generating Type Bindings
生成类型绑定
Before using the SDK, generate TypeScript bindings from your SpacetimeDB module:
bash
spacetime generate --lang typescript --out-dir ./src/module_bindings --project-path ./serverThis creates a directory with:
module_bindings- - Main exports including
index.ts,DbConnection,tables,reducersquery - Type definitions for each table (e.g., ,
player_table.ts)user_table.ts - Type definitions for each reducer (e.g., )
create_player_reducer.ts - Custom type definitions (e.g., )
point_type.ts
使用SDK前,请从SpacetimeDB模块生成TypeScript绑定:
bash
spacetime generate --lang typescript --out-dir ./src/module_bindings --project-path ./server该命令会创建目录,包含:
module_bindings- - 主导出文件,包含
index.ts、DbConnection、tables、reducersquery - 各表的类型定义(如、
player_table.ts)user_table.ts - 各Reducer的类型定义(如)
create_player_reducer.ts - 自定义类型定义(如)
point_type.ts
Basic Connection Setup
基础连接配置
typescript
import { DbConnection } from './module_bindings';
const connection = DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('my_database')
.onConnect((conn, identity, token) => {
console.log('Connected with identity:', identity.toHexString());
// Store token for reconnection
localStorage.setItem('spacetimedb_token', token);
// Subscribe to tables after connection
conn.subscriptionBuilder().subscribe('SELECT * FROM player');
})
.onDisconnect((ctx) => {
console.log('Disconnected');
})
.onConnectError((ctx, error) => {
console.error('Connection error:', error);
})
.build();typescript
import { DbConnection } from './module_bindings';
const connection = DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('my_database')
.onConnect((conn, identity, token) => {
console.log('Connected with identity:', identity.toHexString());
// 存储token用于重连
localStorage.setItem('spacetimedb_token', token);
// 连接后订阅表
conn.subscriptionBuilder().subscribe('SELECT * FROM player');
})
.onDisconnect((ctx) => {
console.log('Disconnected');
})
.onConnectError((ctx, error) => {
console.error('Connection error:', error);
})
.build();Connection Builder Options
连接构建器选项
typescript
DbConnection.builder()
// Required: SpacetimeDB server URI
.withUri('ws://localhost:3000')
// Required: Database module name or address
.withModuleName('my_database')
// Optional: Authentication token for reconnection
.withToken(localStorage.getItem('spacetimedb_token') ?? undefined)
// Optional: Enable compression (default: 'gzip')
.withCompression('gzip') // or 'none'
// Optional: Light mode reduces network traffic
.withLightMode(true)
// Optional: Wait for durable writes before receiving updates
.withConfirmedReads(true)
// Connection lifecycle callbacks
.onConnect((conn, identity, token) => { /* ... */ })
.onDisconnect((ctx, error) => { /* ... */ })
.onConnectError((ctx, error) => { /* ... */ })
.build();typescript
DbConnection.builder()
// 必填:SpacetimeDB服务器URI
.withUri('ws://localhost:3000')
// 必填:数据库模块名称或地址
.withModuleName('my_database')
// 可选:用于重连的认证token
.withToken(localStorage.getItem('spacetimedb_token') ?? undefined)
// 可选:启用压缩(默认值:'gzip')
.withCompression('gzip') // 或'none'
// 可选:轻量模式可减少网络流量
.withLightMode(true)
// 可选:在接收更新前等待持久化写入完成
.withConfirmedReads(true)
// 连接生命周期回调
.onConnect((conn, identity, token) => { /* ... */ })
.onDisconnect((ctx, error) => { /* ... */ })
.onConnectError((ctx, error) => { /* ... */ })
.build();Subscribing to Tables
订阅表数据
Subscriptions sync table data to the client cache. Use SQL queries to filter what data you receive.
订阅功能可将表数据同步到客户端缓存。使用SQL查询过滤需要接收的数据。
Basic Subscription
基础订阅
typescript
connection.subscriptionBuilder()
.onApplied((ctx) => {
console.log('Subscription applied, cache is ready');
})
.onError((ctx, error) => {
console.error('Subscription error:', error);
})
.subscribe('SELECT * FROM player');typescript
connection.subscriptionBuilder()
.onApplied((ctx) => {
console.log('Subscription applied, cache is ready');
})
.onError((ctx, error) => {
console.error('Subscription error:', error);
})
.subscribe('SELECT * FROM player');Multiple Queries
多查询订阅
typescript
connection.subscriptionBuilder()
.subscribe([
'SELECT * FROM player',
'SELECT * FROM game_state',
'SELECT * FROM message WHERE room_id = 1'
]);typescript
connection.subscriptionBuilder()
.subscribe([
'SELECT * FROM player',
'SELECT * FROM game_state',
'SELECT * FROM message WHERE room_id = 1'
]);Typed Query Builder
类型安全查询构建器
Use the generated object for type-safe queries:
querytypescript
import { query } from './module_bindings';
// Simple query - selects all rows
connection.subscriptionBuilder()
.subscribe(query.player.build());
// Query with WHERE clause
connection.subscriptionBuilder()
.subscribe(
query.player
.where(row => row.name.eq('Alice'))
.build()
);
// Complex conditions
connection.subscriptionBuilder()
.subscribe(
query.player
.where(row => row.score.gte(100))
.where(row => row.isActive.eq(true))
.build()
);使用生成的对象进行类型安全的查询:
querytypescript
import { query } from './module_bindings';
// 简单查询 - 选择所有行
connection.subscriptionBuilder()
.subscribe(query.player.build());
// 带WHERE子句的查询
connection.subscriptionBuilder()
.subscribe(
query.player
.where(row => row.name.eq('Alice'))
.build()
);
// 复杂条件查询
connection.subscriptionBuilder()
.subscribe(
query.player
.where(row => row.score.gte(100))
.where(row => row.isActive.eq(true))
.build()
);Subscribe to All Tables
订阅所有表
For development or small datasets:
typescript
connection.subscriptionBuilder().subscribeToAllTables();适用于开发环境或小型数据集:
typescript
connection.subscriptionBuilder().subscribeToAllTables();Unsubscribing
取消订阅
typescript
const handle = connection.subscriptionBuilder()
.onApplied(() => console.log('Subscribed'))
.subscribe('SELECT * FROM player');
// Later, unsubscribe
handle.unsubscribe();
// Or with callback when complete
handle.unsubscribeThen((ctx) => {
console.log('Unsubscribed successfully');
});typescript
const handle = connection.subscriptionBuilder()
.onApplied(() => console.log('Subscribed'))
.subscribe('SELECT * FROM player');
// 后续取消订阅
handle.unsubscribe();
// 或在取消完成后执行回调
handle.unsubscribeThen((ctx) => {
console.log('Unsubscribed successfully');
});Accessing Table Data
访问表数据
After subscription, access cached data through :
connection.dbtypescript
// Iterate all rows
for (const player of connection.db.player.iter()) {
console.log(player.name, player.score);
}
// Convert to array
const players = Array.from(connection.db.player.iter());
// Count rows
const count = connection.db.player.count();
// Find by primary key (if table has one)
const player = connection.db.player.id.find(42);
// Find by indexed column
const alice = connection.db.player.name.find('Alice');订阅完成后,可通过访问缓存数据:
connection.dbtypescript
// 遍历所有行
for (const player of connection.db.player.iter()) {
console.log(player.name, player.score);
}
// 转换为数组
const players = Array.from(connection.db.player.iter());
// 统计行数
const count = connection.db.player.count();
// 通过主键查找(如果表有主键)
const player = connection.db.player.id.find(42);
// 通过索引列查找
const alice = connection.db.player.name.find('Alice');Table Event Callbacks
表事件回调
Listen for real-time changes to table data:
typescript
// Row inserted
connection.db.player.onInsert((ctx, player) => {
console.log('New player:', player.name);
});
// Row deleted
connection.db.player.onDelete((ctx, player) => {
console.log('Player left:', player.name);
});
// Row updated (requires primary key on table)
connection.db.player.onUpdate((ctx, oldPlayer, newPlayer) => {
console.log(`${oldPlayer.name} score: ${oldPlayer.score} -> ${newPlayer.score}`);
});
// Remove callbacks
const onInsertCb = (ctx, player) => console.log(player);
connection.db.player.onInsert(onInsertCb);
connection.db.player.removeOnInsert(onInsertCb);监听表数据的实时变化:
typescript
// 行插入事件
connection.db.player.onInsert((ctx, player) => {
console.log('New player:', player.name);
});
// 行删除事件
connection.db.player.onDelete((ctx, player) => {
console.log('Player left:', player.name);
});
// 行更新事件(要求表有主键)
connection.db.player.onUpdate((ctx, oldPlayer, newPlayer) => {
console.log(`${oldPlayer.name} score: ${oldPlayer.score} -> ${newPlayer.score}`);
});
// 移除回调
const onInsertCb = (ctx, player) => console.log(player);
connection.db.player.onInsert(onInsertCb);
connection.db.player.removeOnInsert(onInsertCb);Event Context
事件上下文
Callbacks receive an with information about the event:
EventContexttypescript
connection.db.player.onInsert((ctx, player) => {
// Access to database
const allPlayers = Array.from(ctx.db.player.iter());
// Check event type
if (ctx.event.tag === 'Reducer') {
const { callerIdentity, reducer, status } = ctx.event.value;
console.log(`Triggered by reducer: ${reducer.name}`);
}
// Call other reducers
ctx.reducers.sendMessage({ playerId: player.id, text: 'Welcome!' });
});回调函数会接收包含事件信息的:
EventContexttypescript
connection.db.player.onInsert((ctx, player) => {
// 访问数据库
const allPlayers = Array.from(ctx.db.player.iter());
// 检查事件类型
if (ctx.event.tag === 'Reducer') {
const { callerIdentity, reducer, status } = ctx.event.value;
console.log(`Triggered by reducer: ${reducer.name}`);
}
// 调用其他Reducer
ctx.reducers.sendMessage({ playerId: player.id, text: 'Welcome!' });
});Calling Reducers
调用Reducer
Reducers are server-side functions that modify the database. CRITICAL: Use object syntax, not positional arguments.
typescript
// CORRECT: Object syntax
connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });
// WRONG: Positional arguments
// connection.reducers.createPlayer('Alice', { x: 0, y: 0 }); // DO NOT DO THIS
// Listen for reducer results
connection.reducers.onCreatePlayer((ctx, args) => {
const { callerIdentity, status, timestamp, energyConsumed } = ctx.event;
if (status.tag === 'Committed') {
console.log('Player created successfully');
} else if (status.tag === 'Failed') {
console.error('Failed:', status.value);
}
});
// Remove reducer callback
connection.reducers.removeOnCreatePlayer(callback);Reducer是用于修改数据库的服务端函数。重要:必须使用对象语法,而非位置参数。
typescript
// 正确:对象语法
connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });
// 错误:位置参数
// connection.reducers.createPlayer('Alice', { x: 0, y: 0 }); // 请勿这样做
// 监听Reducer执行结果
connection.reducers.onCreatePlayer((ctx, args) => {
const { callerIdentity, status, timestamp, energyConsumed } = ctx.event;
if (status.tag === 'Committed') {
console.log('Player created successfully');
} else if (status.tag === 'Failed') {
console.error('Failed:', status.value);
}
});
// 移除Reducer回调
connection.reducers.removeOnCreatePlayer(callback);Snake_case to camelCase conversion
蛇形命名转驼峰命名
- Server:
spacetimedb.reducer('do_something', ...) - Client:
conn.reducers.doSomething({ ... })
- 服务端:
spacetimedb.reducer('do_something', ...) - 客户端:
conn.reducers.doSomething({ ... })
Reducer Flags
Reducer标志
Control how the server handles reducer calls:
typescript
// NoSuccessNotify: Don't send TransactionUpdate on success (reduces traffic)
connection.setReducerFlags.movePlayer('NoSuccessNotify');
// FullUpdate: Always send full TransactionUpdate (default)
connection.setReducerFlags.movePlayer('FullUpdate');控制服务端处理Reducer调用的方式:
typescript
// NoSuccessNotify: 成功时不发送TransactionUpdate(减少流量)
connection.setReducerFlags.movePlayer('NoSuccessNotify');
// FullUpdate: 始终发送完整的TransactionUpdate(默认值)
connection.setReducerFlags.movePlayer('FullUpdate');Views
视图
Views provide filtered access to private table data based on the connected user.
视图可根据连接用户的身份,提供对私有表数据的过滤访问。
ViewContext vs AnonymousViewContext
ViewContext vs AnonymousViewContext
typescript
// ViewContext — has ctx.sender, result varies per user (computed per-subscriber)
spacetimedb.view({ name: 'my_items', public: true }, t.array(Item.rowType), (ctx) => {
return [...ctx.db.item.by_owner.filter(ctx.sender)];
});
// AnonymousViewContext — no ctx.sender, same result for everyone (shared, better perf)
spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(LeaderboardRow), (ctx) => {
return [...ctx.db.player.by_score.filter(/* top scores */)];
});typescript
// ViewContext — 包含ctx.sender,结果因用户而异(每个订阅者单独计算)
spacetimedb.view({ name: 'my_items', public: true }, t.array(Item.rowType), (ctx) => {
return [...ctx.db.item.by_owner.filter(ctx.sender)];
});
// AnonymousViewContext — 无ctx.sender,所有用户结果相同(共享计算,性能更好)
spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(LeaderboardRow), (ctx) => {
return [...ctx.db.player.by_score.filter(/* top scores */)];
});CRITICAL: Views can only use index lookups
重要:视图仅可使用索引查询
typescript
// WRONG — views cannot use .iter()
spacetimedb.view(
{ name: 'my_data_wrong', public: true },
t.array(PrivateData.rowType),
(ctx) => [...ctx.db.privateData.iter()] // NOT ALLOWED
);
// RIGHT — use index lookup
spacetimedb.view(
{ name: 'my_data', public: true },
t.array(PrivateData.rowType),
(ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)]
);typescript
// 错误 — 视图中不能使用.iter()
spacetimedb.view(
{ name: 'my_data_wrong', public: true },
t.array(PrivateData.rowType),
(ctx) => [...ctx.db.privateData.iter()] // 不允许
);
// 正确 — 使用索引查询
spacetimedb.view(
{ name: 'my_data', public: true },
t.array(PrivateData.rowType),
(ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)]
);Subscribing to Views
订阅视图
Views require explicit subscription:
typescript
conn.subscriptionBuilder().subscribe([
'SELECT * FROM public_table',
'SELECT * FROM my_data', // Views need explicit SQL!
]);视图需要显式订阅:
typescript
conn.subscriptionBuilder().subscribe([
'SELECT * FROM public_table',
'SELECT * FROM my_data', // 视图需要显式SQL!
]);Procedures (Beta)
过程(Beta版)
Procedures are for side effects (HTTP requests, etc.) that reducers can't do.
Procedures are currently in beta. API may change.
过程用于处理Reducer无法完成的副作用(如HTTP请求等)。
目前过程处于Beta阶段,API可能会发生变化。
Defining a procedure
定义过程
typescript
spacetimedb.procedure(
'fetch_external_data',
{ url: t.string() },
t.string(), // return type
(ctx, { url }) => {
const response = ctx.http.fetch(url);
return response.text();
}
);typescript
spacetimedb.procedure(
'fetch_external_data',
{ url: t.string() },
t.string(), // 返回类型
(ctx, { url }) => {
const response = ctx.http.fetch(url);
return response.text();
}
);CRITICAL: Database access in procedures
重要:过程中的数据库访问
Procedures don't have . Use for database access.
ctx.dbctx.withTx()typescript
spacetimedb.procedure('save_fetched_data', { url: t.string() }, t.unit(), (ctx, { url }) => {
// Fetch external data (outside transaction)
const response = ctx.http.fetch(url);
const data = response.text();
// WRONG — ctx.db doesn't exist in procedures
// ctx.db.myTable.insert({ ... });
// RIGHT — use ctx.withTx() for database access
ctx.withTx(tx => {
tx.db.myTable.insert({
id: 0n,
content: data,
fetchedAt: tx.timestamp,
fetchedBy: tx.sender,
});
});
return {};
});过程中没有,请使用进行数据库访问。
ctx.dbctx.withTx()typescript
spacetimedb.procedure('save_fetched_data', { url: t.string() }, t.unit(), (ctx, { url }) => {
// 外部数据获取(事务外)
const response = ctx.http.fetch(url);
const data = response.text();
// 错误 — 过程中不存在ctx.db
// ctx.db.myTable.insert({ ... });
// 正确 — 使用ctx.withTx()进行数据库访问
ctx.withTx(tx => {
tx.db.myTable.insert({
id: 0n,
content: data,
fetchedAt: tx.timestamp,
fetchedBy: tx.sender,
});
});
return {};
});Key differences from reducers
与Reducer的主要区别
| Reducers | Procedures |
|---|---|
| Must use |
| Automatic transaction | Manual transaction management |
| No HTTP/network | |
| No return values to caller | Can return data to caller |
| Reducers | Procedures |
|---|---|
可直接使用 | 必须使用 |
| 自动事务管理 | 手动事务管理 |
| 不支持HTTP/网络操作 | 支持 |
| 无法向调用者返回值 | 可向调用者返回数据 |
Identity and Authentication
身份与认证
typescript
import { Identity } from 'spacetimedb';
// Get current identity
const identity = connection.identity;
console.log(identity?.toHexString());
// Compare identities
if (identity?.isEqual(otherIdentity)) {
console.log('Same user');
}
// Create from hex string
const parsed = Identity.fromString('0x1234...');
// Zero identity
const zero = Identity.zero();
// Compare identities using toHexString()
const isOwner = row.ownerId.toHexString() === myIdentity.toHexString();typescript
import { Identity } from 'spacetimedb';
// 获取当前身份
const identity = connection.identity;
console.log(identity?.toHexString());
// 比较身份
if (identity?.isEqual(otherIdentity)) {
console.log('Same user');
}
// 从十六进制字符串创建
const parsed = Identity.fromString('0x1234...');
// 零值身份
const zero = Identity.zero();
// 使用toHexString()比较身份
const isOwner = row.ownerId.toHexString() === myIdentity.toHexString();Persisting Authentication
持久化认证信息
typescript
// On connect, save the token
.onConnect((conn, identity, token) => {
localStorage.setItem('auth_token', token);
localStorage.setItem('identity', identity.toHexString());
})
// On reconnect, use saved token
.withToken(localStorage.getItem('auth_token') ?? undefined)typescript
// 连接成功时保存token
.onConnect((conn, identity, token) => {
localStorage.setItem('auth_token', token);
localStorage.setItem('identity', identity.toHexString());
})
// 重连时使用保存的token
.withToken(localStorage.getItem('auth_token') ?? undefined)Stale token handling
过期token处理
typescript
const onConnectError = (_ctx: ErrorContext, err: Error) => {
if (err.message?.includes('Unauthorized') || err.message?.includes('401')) {
localStorage.removeItem('auth_token');
window.location.reload();
}
};typescript
const onConnectError = (_ctx: ErrorContext, err: Error) => {
if (err.message?.includes('Unauthorized') || err.message?.includes('401')) {
localStorage.removeItem('auth_token');
window.location.reload();
}
};React Integration
React集成
The SDK includes React hooks for reactive UI updates.
SDK包含用于响应式UI更新的React钩子。
Provider Setup
Provider配置
tsx
import React, { useMemo } from 'react';
import ReactDOM from 'react-dom/client';
import { SpacetimeDBProvider } from 'spacetimedb/react';
import { DbConnection, query } from './module_bindings';
import App from './App';
function Root() {
// CRITICAL: Memoize to prevent reconnects on every render
const connectionBuilder = useMemo(() =>
DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('my_game')
.withToken(localStorage.getItem('auth_token') || undefined)
.onConnect((conn, identity, token) => {
console.log('Connected:', identity.toHexString());
localStorage.setItem('auth_token', token);
conn.subscriptionBuilder().subscribe(query.player.build());
})
.onDisconnect(() => console.log('Disconnected'))
.onConnectError((ctx, err) => console.error('Error:', err)),
[] // Empty deps - only create once
);
return (
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
<App />
</SpacetimeDBProvider>
);
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Root />
</React.StrictMode>
);tsx
import React, { useMemo } from 'react';
import ReactDOM from 'react-dom/client';
import { SpacetimeDBProvider } from 'spacetimedb/react';
import { DbConnection, query } from './module_bindings';
import App from './App';
function Root() {
// 重要:记忆化以避免每次渲染都重新连接
const connectionBuilder = useMemo(() =>
DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('my_game')
.withToken(localStorage.getItem('auth_token') || undefined)
.onConnect((conn, identity, token) => {
console.log('Connected:', identity.toHexString());
localStorage.setItem('auth_token', token);
conn.subscriptionBuilder().subscribe(query.player.build());
})
.onDisconnect(() => console.log('Disconnected'))
.onConnectError((ctx, err) => console.error('Error:', err)),
[] // 空依赖数组 - 仅创建一次
);
return (
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
<App />
</SpacetimeDBProvider>
);
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Root />
</React.StrictMode>
);useSpacetimeDB Hook
useSpacetimeDB钩子
Access connection state:
tsx
import { useSpacetimeDB } from 'spacetimedb/react';
function ConnectionStatus() {
const { isActive, identity, token, connectionId, connectionError } = useSpacetimeDB();
if (connectionError) {
return <div>Error: {connectionError.message}</div>;
}
if (!isActive) {
return <div>Connecting...</div>;
}
return <div>Connected as {identity?.toHexString()}</div>;
}访问连接状态:
tsx
import { useSpacetimeDB } from 'spacetimedb/react';
function ConnectionStatus() {
const { isActive, identity, token, connectionId, connectionError } = useSpacetimeDB();
if (connectionError) {
return <div>Error: {connectionError.message}</div>;
}
if (!isActive) {
return <div>Connecting...</div>;
}
return <div>Connected as {identity?.toHexString()}</div>;
}useTable Hook
useTable钩子
Subscribe to table data with reactive updates. CRITICAL: Returns a tuple .
[rows, isLoading]tsx
import { useTable, where, eq } from 'spacetimedb/react';
import { tables } from './module_bindings';
function PlayerList() {
// CORRECT: Tuple destructuring
const [players, isLoading] = useTable(tables.player);
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{players.map(player => (
<li key={player.id}>{player.name}: {player.score}</li>
))}
</ul>
);
}
function FilteredPlayerList() {
// Filtered players with callbacks
const [activePlayers, isLoading] = useTable(
tables.player,
where(eq('isActive', true)),
{
onInsert: (player) => console.log('Player joined:', player.name),
onDelete: (player) => console.log('Player left:', player.name),
onUpdate: (oldPlayer, newPlayer) => {
console.log(`${oldPlayer.name} updated`);
},
}
);
return (
<ul>
{activePlayers.map(player => (
<li key={player.id}>{player.name}</li>
))}
</ul>
);
}订阅表数据并实现响应式更新。重要:返回元组。
[rows, isLoading]tsx
import { useTable, where, eq } from 'spacetimedb/react';
import { tables } from './module_bindings';
function PlayerList() {
// 正确:元组解构
const [players, isLoading] = useTable(tables.player);
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{players.map(player => (
<li key={player.id}>{player.name}: {player.score}</li>
))}
</ul>
);
}
function FilteredPlayerList() {
// 带回调的过滤玩家列表
const [activePlayers, isLoading] = useTable(
tables.player,
where(eq('isActive', true)),
{
onInsert: (player) => console.log('Player joined:', player.name),
onDelete: (player) => console.log('Player left:', player.name),
onUpdate: (oldPlayer, newPlayer) => {
console.log(`${oldPlayer.name} updated`);
},
}
);
return (
<ul>
{activePlayers.map(player => (
<li key={player.id}>{player.name}</li>
))}
</ul>
);
}useReducer Hook
useReducer钩子
Call reducers from components:
tsx
import { useReducer } from 'spacetimedb/react';
import { reducers } from './module_bindings';
function CreatePlayerForm() {
const createPlayer = useReducer(reducers.createPlayer);
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// CORRECT: Object syntax
createPlayer({ name, location: { x: 0, y: 0 } });
setName('');
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<button type="submit">Create Player</button>
</form>
);
}在组件中调用Reducer:
tsx
import { useReducer } from 'spacetimedb/react';
import { reducers } from './module_bindings';
function CreatePlayerForm() {
const createPlayer = useReducer(reducers.createPlayer);
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// 正确:对象语法
createPlayer({ name, location: { x: 0, y: 0 } });
setName('');
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<button type="submit">Create Player</button>
</form>
);
}Vue Integration
Vue集成
The SDK includes Vue composables:
typescript
import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/vue';Usage is similar to React hooks.
SDK包含Vue组合式函数:
typescript
import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/vue';用法与React钩子类似。
Svelte Integration
Svelte集成
The SDK includes Svelte stores:
typescript
import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/svelte';SDK包含Svelte存储:
typescript
import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/svelte';Server-Side Usage (Node.js, Deno, Bun)
服务端使用(Node.js, Deno, Bun)
The SDK works in server-side JavaScript runtimes:
typescript
import { DbConnection } from './module_bindings';
async function main() {
const connection = DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('my_database')
.onConnect((conn, identity, token) => {
console.log('Connected:', identity.toHexString());
conn.subscriptionBuilder()
.onApplied(() => {
// Process data
for (const player of conn.db.player.iter()) {
console.log(player);
}
})
.subscribe('SELECT * FROM player');
})
.build();
}
main();SDK可在服务端JavaScript运行时中使用:
typescript
import { DbConnection } from './module_bindings';
async function main() {
const connection = DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('my_database')
.onConnect((conn, identity, token) => {
console.log('Connected:', identity.toHexString());
conn.subscriptionBuilder()
.onApplied(() => {
// 处理数据
for (const player of conn.db.player.iter()) {
console.log(player);
}
})
.subscribe('SELECT * FROM player');
})
.build();
}
main();Timestamps
时间戳
Server-side
服务端
typescript
import { Timestamp, ScheduleAt } from 'spacetimedb';
// Current time
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
// Future time (add microseconds)
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n; // 5 minutestypescript
import { Timestamp, ScheduleAt } from 'spacetimedb';
// 当前时间
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
// 未来时间(添加微秒)
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n; // 5分钟Client-side (CRITICAL)
客户端(重要)
Timestamps are objects, not numbers:
typescript
// WRONG
const date = new Date(row.createdAt);
const date = new Date(Number(row.createdAt / 1000n));
// RIGHT
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));时间戳是对象,而非数字:
typescript
// 错误
const date = new Date(row.createdAt);
const date = new Date(Number(row.createdAt / 1000n));
// 正确
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));ScheduleAt on client
客户端的ScheduleAt
typescript
// ScheduleAt is a tagged union
if (scheduleAt.tag === 'Time') {
const date = new Date(Number(scheduleAt.value.microsSinceUnixEpoch / 1000n));
}typescript
// ScheduleAt是标签联合类型
if (scheduleAt.tag === 'Time') {
const date = new Date(Number(scheduleAt.value.microsSinceUnixEpoch / 1000n));
}Scheduled Tables
调度表
typescript
// Scheduled table MUST use scheduledId and scheduledAt columns
export const CleanupJob = table({
name: 'cleanup_job',
scheduled: 'run_cleanup' // reducer name
}, {
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
targetId: t.u64(), // Your custom data
});
// Scheduled reducer receives full row as arg
spacetimedb.reducer('run_cleanup', { arg: CleanupJob.rowType }, (ctx, { arg }) => {
// arg.scheduledId, arg.targetId available
// Row is auto-deleted after reducer completes
});
// Schedule a job
import { ScheduleAt } from 'spacetimedb';
const futureTime = ctx.timestamp.microsSinceUnixEpoch + 60_000_000n; // 60 seconds
ctx.db.cleanupJob.insert({
scheduledId: 0n,
scheduledAt: ScheduleAt.time(futureTime),
targetId: someId
});
// Cancel a job by deleting the row
ctx.db.cleanupJob.scheduledId.delete(jobId);typescript
// 调度表必须包含scheduledId和scheduledAt列
export const CleanupJob = table({
name: 'cleanup_job',
scheduled: 'run_cleanup' // reducer名称
}, {
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
targetId: t.u64(), // 自定义数据
});
// 调度Reducer会接收完整行作为参数
spacetimedb.reducer('run_cleanup', { arg: CleanupJob.rowType }, (ctx, { arg }) => {
// 可访问arg.scheduledId、arg.targetId
// Reducer执行完成后该行会自动删除
});
// 调度任务
import { ScheduleAt } from 'spacetimedb';
const futureTime = ctx.timestamp.microsSinceUnixEpoch + 60_000_000n; // 60秒
ctx.db.cleanupJob.insert({
scheduledId: 0n,
scheduledAt: ScheduleAt.time(futureTime),
targetId: someId
});
// 通过删除行取消任务
ctx.db.cleanupJob.scheduledId.delete(jobId);Error Handling
错误处理
Connection Errors
连接错误
typescript
DbConnection.builder()
.onConnectError((ctx, error) => {
console.error('Failed to connect:', error.message);
// Implement retry logic
setTimeout(() => {
// Rebuild connection
}, 5000);
})
.build();typescript
DbConnection.builder()
.onConnectError((ctx, error) => {
console.error('Failed to connect:', error.message);
// 实现重试逻辑
setTimeout(() => {
// 重建连接
}, 5000);
})
.build();Subscription Errors
订阅错误
typescript
connection.subscriptionBuilder()
.onError((ctx, error) => {
console.error('Subscription failed:', error.message);
})
.subscribe('SELECT * FROM player');typescript
connection.subscriptionBuilder()
.onError((ctx, error) => {
console.error('Subscription failed:', error.message);
})
.subscribe('SELECT * FROM player');Reducer Errors
Reducer错误
typescript
connection.reducers.onCreatePlayer((ctx, args) => {
const { status } = ctx.event;
switch (status.tag) {
case 'Committed':
console.log('Success');
break;
case 'Failed':
console.error('Reducer failed:', status.value);
break;
case 'OutOfEnergy':
console.error('Out of energy');
break;
}
});typescript
connection.reducers.onCreatePlayer((ctx, args) => {
const { status } = ctx.event;
switch (status.tag) {
case 'Committed':
console.log('Success');
break;
case 'Failed':
console.error('Reducer failed:', status.value);
break;
case 'OutOfEnergy':
console.error('Out of energy');
break;
}
});Disconnecting
断开连接
typescript
// Gracefully disconnect
connection.disconnect();typescript
// 优雅断开连接
connection.disconnect();Type Reference
类型参考
Core Types
核心类型
typescript
import {
Identity, // User identity (256-bit)
ConnectionId, // Connection identifier
Timestamp, // SpacetimeDB timestamp
TimeDuration, // Duration type
Uuid, // UUID type
} from 'spacetimedb';typescript
import {
Identity, // 用户身份(256位)
ConnectionId, // 连接标识符
Timestamp, // SpacetimeDB时间戳
TimeDuration, // 时长类型
Uuid, // UUID类型
} from 'spacetimedb';Generated Types
生成的类型
typescript
// From your module_bindings
import {
DbConnection, // Connection class
DbConnectionBuilder, // Builder class
SubscriptionBuilder, // Subscription builder
SubscriptionHandle, // Subscription handle
EventContext, // Event callback context
ReducerEventContext, // Reducer callback context
ErrorContext, // Error callback context
tables, // Table accessors for useTable
reducers, // Reducer definitions for useReducer
query, // Typed query builder
// Your custom types
Player,
Point,
// ... etc
} from './module_bindings';typescript
// 来自module_bindings
import {
DbConnection, // 连接类
DbConnectionBuilder, // 构建器类
SubscriptionBuilder, // 订阅构建器
SubscriptionHandle, // 订阅句柄
EventContext, // 事件回调上下文
ReducerEventContext, // Reducer回调上下文
ErrorContext, // 错误回调上下文
tables, // 用于useTable的表访问器
reducers, // 用于useReducer的Reducer定义
query, // 类型安全查询构建器
// 自定义类型
Player,
Point,
// ... 其他
} from './module_bindings';Commands
命令
bash
undefinedbash
undefinedStart local server
启动本地服务器
spacetime start
spacetime start
Publish module
发布模块
spacetime publish <module-name> --project-path <backend-dir>
spacetime publish <module-name> --project-path <backend-dir>
Clear database and republish
清空数据库并重新发布
spacetime publish <module-name> --clear-database -y --project-path <backend-dir>
spacetime publish <module-name> --clear-database -y --project-path <backend-dir>
Generate bindings
生成绑定
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --project-path <backend-dir>
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --project-path <backend-dir>
View logs
查看日志
spacetime logs <module-name>
undefinedspacetime logs <module-name>
undefinedBest Practices
最佳实践
-
Store auth tokens: Save the token fromfor seamless reconnection.
onConnect -
Subscribe after connect: Set up subscriptions in thecallback.
onConnect -
Use typed queries: Prefer thebuilder over raw SQL strings for type safety.
query -
Handle all connection states: Implement,
onConnect, andonDisconnect.onConnectError -
Use light mode for high-frequency updates: Enablefor games or real-time apps.
.withLightMode(true) -
Unsubscribe when done: Clean up subscriptions when components unmount or data is no longer needed.
-
Use primary keys: Define primary keys on tables to enablecallbacks.
onUpdate -
Memoize connectionBuilder: Always wrap into prevent reconnects.
useMemo() -
Let subscriptions drive state: Avoid optimistic updates; let the server be the source of truth.
-
存储认证token:保存返回的token以实现无缝重连。
onConnect -
连接后再订阅:在回调中设置订阅。
onConnect -
使用类型安全查询:优先使用构建器而非原始SQL字符串以保证类型安全。
query -
处理所有连接状态:实现、
onConnect和onDisconnect回调。onConnectError -
高频更新场景使用轻量模式:游戏或实时应用中启用。
.withLightMode(true) -
不再需要时取消订阅:组件卸载或不再需要数据时清理订阅。
-
使用主键:为表定义主键以启用回调。
onUpdate -
对connectionBuilder进行记忆化:始终用包裹以避免重复连接。
useMemo() -
由订阅驱动状态:避免乐观更新,以服务端作为数据的唯一来源。
Common Patterns
常见模式
Reconnection Logic
重连逻辑
typescript
function createConnection(token?: string) {
return DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('my_database')
.withToken(token)
.onConnect((conn, identity, newToken) => {
localStorage.setItem('token', newToken);
setupSubscriptions(conn);
})
.onDisconnect(() => {
// Reconnect after delay
setTimeout(() => {
createConnection(localStorage.getItem('token') ?? undefined);
}, 3000);
})
.build();
}typescript
function createConnection(token?: string) {
return DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('my_database')
.withToken(token)
.onConnect((conn, identity, newToken) => {
localStorage.setItem('token', newToken);
setupSubscriptions(conn);
})
.onDisconnect(() => {
// 延迟后重连
setTimeout(() => {
createConnection(localStorage.getItem('token') ?? undefined);
}, 3000);
})
.build();
}Optimistic Updates
乐观更新
typescript
function PlayerScore({ player }) {
const updateScore = useReducer(reducers.updateScore);
const [optimisticScore, setOptimisticScore] = useState(player.score);
const handleClick = () => {
setOptimisticScore(prev => prev + 1);
updateScore({ playerId: player.id, delta: 1 });
};
// Sync with actual data
useEffect(() => {
setOptimisticScore(player.score);
}, [player.score]);
return <div onClick={handleClick}>Score: {optimisticScore}</div>;
}typescript
function PlayerScore({ player }) {
const updateScore = useReducer(reducers.updateScore);
const [optimisticScore, setOptimisticScore] = useState(player.score);
const handleClick = () => {
setOptimisticScore(prev => prev + 1);
updateScore({ playerId: player.id, delta: 1 });
};
// 与实际数据同步
useEffect(() => {
setOptimisticScore(player.score);
}, [player.score]);
return <div onClick={handleClick}>Score: {optimisticScore}</div>;
}Filtering with Multiple Conditions
多条件过滤
typescript
// Using query builder
query.player
.where(row => row.team.eq('red'))
.where(row => row.score.gte(100))
.build();
// Using React hooks
const [redTeamHighScorers] = useTable(
tables.player,
where(eq('team', 'red')), // Additional filtering in client
);
const filtered = redTeamHighScorers.filter(p => p.score >= 100);typescript
// 使用查询构建器
query.player
.where(row => row.team.eq('red'))
.where(row => row.score.gte(100))
.build();
// 使用React钩子
const [redTeamHighScorers] = useTable(
tables.player,
where(eq('team', 'red')), // 客户端额外过滤
);
const filtered = redTeamHighScorers.filter(p => p.score >= 100);Project Structure
项目结构
Server (backend/spacetimedb/
)
backend/spacetimedb/服务端(backend/spacetimedb/
)
backend/spacetimedb/src/schema.ts -> Tables, export spacetimedb
src/index.ts -> Reducers, lifecycle, import schema
package.json -> { "type": "module", "dependencies": { "spacetimedb": "^1.11.0" } }
tsconfig.json -> Standard configsrc/schema.ts -> 定义表结构并导出spacetimedb
src/index.ts -> 定义Reducer、生命周期,导入schema
package.json -> { "type": "module", "dependencies": { "spacetimedb": "^1.11.0" } }
tsconfig.json -> 标准配置Avoiding circular imports
避免循环导入
schema.ts -> defines tables AND exports spacetimedb
index.ts -> imports spacetimedb from ./schema, defines reducersschema.ts -> 定义表结构并导出spacetimedb
index.ts -> 从./schema导入spacetimedb,定义ReducerClient (client/
)
client/客户端(client/
)
client/src/module_bindings/ -> Generated (spacetime generate)
src/main.tsx -> Provider, connection setup
src/App.tsx -> UI components
src/config.ts -> MODULE_NAME, SPACETIMEDB_URIsrc/module_bindings/ -> 生成的绑定代码(spacetime generate)
src/main.tsx -> Provider、连接配置
src/App.tsx -> UI组件
src/config.ts -> MODULE_NAME、SPACETIMEDB_URI