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!
// WRONG — old 1.0 patterns
spacetimedb.reducer('reducer_name', params, fn); // Use export const name = spacetimedb.reducer(params, fn)
schema(myTable); // Use schema({ myTable })
schema(t1, t2, t3); // Use schema({ t1, t2, t3 })
scheduled: 'run_cleanup' // Use scheduled: () => run_cleanup
.withModuleName('db') // Use .withDatabaseName('db') (2.0)
setReducerFlags.x('NoSuccessNotify') // Removed in 2.0这些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!
// WRONG — old 1.0 patterns
spacetimedb.reducer('reducer_name', params, fn); // Use export const name = spacetimedb.reducer(params, fn)
schema(myTable); // Use schema({ myTable })
schema(t1, t2, t3); // Use schema({ t1, t2, t3 })
scheduled: 'run_cleanup' // Use scheduled: () => run_cleanup
.withModuleName('db') // Use .withDatabaseName('db') (2.0)
setReducerFlags.x('NoSuccessNotify') // Removed in 2.0CORRECT PATTERNS:
正确用法示例:
typescript
// CORRECT IMPORTS
import { DbConnection, tables } from './module_bindings'; // Generated!
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { Identity } from 'spacetimedb';
// 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, isReady]
const [items, isReady] = useTable(tables.item);typescript
// CORRECT IMPORTS
import { DbConnection, tables } from './module_bindings'; // Generated!
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { Identity } from 'spacetimedb';
// 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, isReady]
const [items, isReady] = 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" |
Incorrect multi-column | Match index prefix/tuple shape | Empty results or range/type errors |
| Use index lookups only | Views can't scan tables |
| | Procedures need explicit transactions |
| 错误用法 | 正确用法 | 报错信息 |
|---|---|---|
缺失 | 新建 | "could not detect language" |
缺失 | 新建 | "TsconfigNotFound" |
入口文件不是 | 使用 | 模块无法打包 |
| | "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 |
|---|---|---|
Inline | | Reconnects every render |
| | Tuple destructuring |
| Optimistic UI updates | Let subscriptions drive state | Desync issues |
| | Wrong prop name |
| 错误用法 | 正确用法 | 报错信息 |
|---|---|---|
内联 | 使用 | 每次渲染都会重连 |
| | 元组解构错误 |
| 手动实现乐观UI更新 | 让订阅驱动状态更新 | 数据不同步问题 |
| 使用 | 属性名错误 |
Hard Requirements
硬性要求
- — use a single tables object; optional module settings are allowed as a second argument
schema({ table }) - Reducer/procedure names from exports — ; never
export const name = spacetimedb.reducer(params, fn)reducer('name', ...) - Reducer calls use object syntax — not positional args
{ param: 'value' } - Import from
DbConnection— not from./module_bindingsspacetimedb - DO NOT edit generated bindings — regenerate with
spacetime generate - Indexes go in OPTIONS (1st arg) — not in COLUMNS (2nd arg) of
table() - Use BigInt for u64/i64 fields — ,
0n, not1n,01 - Reducers are transactional — they do not return data
- Reducers must be deterministic — no filesystem, network, timers, random
- Views should use index lookups — causes severe performance issues
.iter() - Procedures need —
ctx.withTx()doesn't exist in proceduresctx.db - Sum type values — use not
{ tag: 'variant', value: payload }{ variant: payload } - Use — not
.withDatabaseName()(2.0).withModuleName()
- — 传入单个表对象作为参数,第二个参数可传入可选的模块配置
schema({ table }) - Reducer/存储过程名称来自导出声明 — 写法为,禁止使用
export const name = spacetimedb.reducer(params, fn)reducer('name', ...) - Reducer调用使用对象语法 — 格式为,不使用位置参数
{ param: 'value' } - 从导入
./module_bindings— 不要从DbConnection导入spacetimedb - 请勿编辑生成的绑定文件 — 使用重新生成
spacetime generate - 索引写在OPTIONS(第一个参数)中 — 不要写在的COLUMNS(第二个参数)中
table() - u64/i64字段使用BigInt — 写法为、
0n,不要使用1n、01 - Reducer是事务性的 — 不返回数据
- Reducer必须是确定性的 — 不能访问文件系统、网络、定时器、随机数
- 视图应使用索引查询 — 会导致严重性能问题
.iter() - 存储过程需要使用— 存储过程中不存在
ctx.withTx()ctx.db - Sum类型值格式 — 使用,不要使用
{ tag: 'variant', value: payload }{ variant: payload } - 使用— 2.0版本中已废弃
.withDatabaseName().withModuleName()
Installation
安装
bash
npm install spacetimedbFor Node.js environments without native fetch/WebSocket support, install .
undicibash
npm install spacetimedb如果Node.js环境没有原生fetch/WebSocket支持,请安装。
undiciGenerating Type Bindings
生成类型绑定
bash
spacetime generate --lang typescript --out-dir ./src/module_bindings --module-path ./serverbash
spacetime generate --lang typescript --out-dir ./src/module_bindings --module-path ./serverClient Connection
客户端连接
typescript
import { DbConnection } from './module_bindings';
const connection = DbConnection.builder()
.withUri('ws://localhost:3000')
.withDatabaseName('my_database')
.withToken(localStorage.getItem('spacetimedb_token') ?? undefined)
.onConnect((conn, identity, token) => {
// identity: your unique Identity for this database
console.log('Connected as:', identity.toHexString());
// Save token for reconnection (preserves identity across sessions)
localStorage.setItem('spacetimedb_token', token);
conn.subscriptionBuilder()
.onApplied(() => console.log('Cache ready'))
.subscribe('SELECT * FROM player');
})
.onDisconnect((ctx) => console.log('Disconnected'))
.onConnectError((ctx, error) => console.error('Connection failed:', error))
.build();typescript
import { DbConnection } from './module_bindings';
const connection = DbConnection.builder()
.withUri('ws://localhost:3000')
.withDatabaseName('my_database')
.withToken(localStorage.getItem('spacetimedb_token') ?? undefined)
.onConnect((conn, identity, token) => {
// identity: 你在该数据库的唯一身份标识
console.log('Connected as:', identity.toHexString());
// 保存token用于重连(跨会话保留身份)
localStorage.setItem('spacetimedb_token', token);
conn.subscriptionBuilder()
.onApplied(() => console.log('Cache ready'))
.subscribe('SELECT * FROM player');
})
.onDisconnect((ctx) => console.log('Disconnected'))
.onConnectError((ctx, error) => console.error('Connection failed:', error))
.build();Subscribing to Tables
订阅表数据
typescript
// Basic subscription
connection.subscriptionBuilder()
.onApplied((ctx) => console.log('Cache ready'))
.subscribe('SELECT * FROM player');
// Multiple queries
connection.subscriptionBuilder()
.subscribe(['SELECT * FROM player', 'SELECT * FROM game_state']);
// Subscribe to all tables (development only — cannot mix with Subscribe)
connection.subscriptionBuilder().subscribeToAllTables();
// Subscription handle for later unsubscribe
const handle = connection.subscriptionBuilder()
.onApplied(() => console.log('Subscribed'))
.subscribe('SELECT * FROM player');
handle.unsubscribeThen(() => console.log('Unsubscribed'));typescript
// 基础订阅
connection.subscriptionBuilder()
.onApplied((ctx) => console.log('Cache ready'))
.subscribe('SELECT * FROM player');
// 多查询订阅
connection.subscriptionBuilder()
.subscribe(['SELECT * FROM player', 'SELECT * FROM game_state']);
// 订阅所有表(仅开发环境使用 — 不能和普通Subscribe混用)
connection.subscriptionBuilder().subscribeToAllTables();
// 获取订阅句柄用于后续取消订阅
const handle = connection.subscriptionBuilder()
.onApplied(() => console.log('Subscribed'))
.subscribe('SELECT * FROM player');
handle.unsubscribeThen(() => console.log('Unsubscribed'));Accessing Table Data
访问表数据
typescript
for (const player of connection.db.player.iter()) { console.log(player.name); }
const players = Array.from(connection.db.player.iter());
const count = connection.db.player.count();
const player = connection.db.player.id.find(42n);typescript
for (const player of connection.db.player.iter()) { console.log(player.name); }
const players = Array.from(connection.db.player.iter());
const count = connection.db.player.count();
const player = connection.db.player.id.find(42n);Table Event Callbacks
表事件回调
typescript
connection.db.player.onInsert((ctx, player) => console.log('New:', player.name));
connection.db.player.onDelete((ctx, player) => console.log('Left:', player.name));
connection.db.player.onUpdate((ctx, old, new_) => console.log(`${old.score} -> ${new_.score}`));typescript
connection.db.player.onInsert((ctx, player) => console.log('新增玩家:', player.name));
connection.db.player.onDelete((ctx, player) => console.log('玩家离开:', player.name));
connection.db.player.onUpdate((ctx, old, new_) => console.log(`${old.score} -> ${new_.score}`));Calling Reducers
调用Reducer
CRITICAL: Use object syntax, not positional arguments.
typescript
connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });重要提示:使用对象语法,不要使用位置参数。
typescript
connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });Snake_case to camelCase conversion
蛇形命名转驼峰命名规则
- Server:
export const do_something = spacetimedb.reducer(...) - Client:
conn.reducers.doSomething({ ... })
- 服务端:
export const do_something = spacetimedb.reducer(...) - 客户端:
conn.reducers.doSomething({ ... })
Identity and Authentication
身份与认证
- and
identityare provided in thetokencallback (see Client Connection above)onConnect - for display or logging
identity.toHexString() - Omit for anonymous connection — server assigns a new identity
.withToken() - Pass a stale/invalid token: server issues a new identity and token in
onConnect
- 和
identity会在token回调中返回(参考上方客户端连接示例)onConnect - 使用用于展示或日志记录
identity.toHexString() - 省略可匿名连接 — 服务端会分配新的身份标识
.withToken() - 传入过期/无效的token: 服务端会在中返回新的身份标识和token
onConnect
Error Handling
错误处理
Connection-level errors (, ) are shown in the Client Connection example above.
.onConnectError.onDisconnecttypescript
// Subscription error
connection.subscriptionBuilder()
.onApplied(() => console.log('Subscribed'))
.onError((ctx) => console.error('Subscription error:', ctx.event))
.subscribe('SELECT * FROM player');连接级错误(、)的用法已在上方客户端连接示例中展示。
.onConnectError.onDisconnecttypescript
// 订阅错误处理
connection.subscriptionBuilder()
.onApplied(() => console.log('Subscribed'))
.onError((ctx) => console.error('Subscription error:', ctx.event))
.subscribe('SELECT * FROM player');Server-Side Module Development
服务端模块开发
Table Definition
表定义
typescript
import { schema, table, t } from 'spacetimedb/server';
export const Task = table({
name: 'task',
public: true,
indexes: [{ name: 'task_owner_id', algorithm: 'btree', columns: ['ownerId'] }]
}, {
id: t.u64().primaryKey().autoInc(),
ownerId: t.identity(),
title: t.string(),
createdAt: t.timestamp(),
});typescript
import { schema, table, t } from 'spacetimedb/server';
export const Task = table({
name: 'task',
public: true,
indexes: [{ name: 'task_owner_id', algorithm: 'btree', columns: ['ownerId'] }]
}, {
id: t.u64().primaryKey().autoInc(),
ownerId: t.identity(),
title: t.string(),
createdAt: t.timestamp(),
});Column types
列类型
typescript
t.identity() // User identity
t.u64() // Unsigned 64-bit integer (use for IDs)
t.string() // Text
t.bool() // Boolean
t.timestamp() // Timestamp
t.scheduleAt() // For scheduled tables only
t.object('Name', {}) // Product types (nested objects)
t.enum('Name', {}) // Sum types (tagged unions)
t.string().optional() // NullableBigInt syntax: All/u64fields usei64,0n, not1n,0.1
typescript
t.identity() // 用户身份标识
t.u64() // 无符号64位整数(用于ID)
t.string() // 文本
t.bool() // 布尔值
t.timestamp() // 时间戳
t.scheduleAt() // 仅用于定时表
t.object('Name', {}) // 产品类型(嵌套对象)
t.enum('Name', {}) // Sum类型(标记联合)
t.string().optional() // 可空字段BigInt语法规则:所有/u64字段使用i64、0n,不要使用1n、0。1
Schema export
Schema导出
typescript
const spacetimedb = schema({ Task, Player });
export default spacetimedb;typescript
const spacetimedb = schema({ Task, Player });
export default spacetimedb;Reducer Definition (2.0)
Reducer定义(2.0版本)
Name comes from the export — NOT from a string argument.
typescript
import spacetimedb from './schema';
import { t, SenderError } from 'spacetimedb/server';
export const create_task = spacetimedb.reducer(
{ title: t.string() },
(ctx, { title }) => {
if (!title) throw new SenderError('title required');
ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title, createdAt: ctx.timestamp });
}
);名称来自导出声明 — 不要通过字符串参数指定。
typescript
import spacetimedb from './schema';
import { t, SenderError } from 'spacetimedb/server';
export const create_task = spacetimedb.reducer(
{ title: t.string() },
(ctx, { title }) => {
if (!title) throw new SenderError('title required');
ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title, createdAt: ctx.timestamp });
}
);Update Pattern
更新模式
typescript
const existing = ctx.db.task.id.find(taskId);
if (!existing) throw new SenderError('Task not found');
ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp });typescript
const existing = ctx.db.task.id.find(taskId);
if (!existing) throw new SenderError('Task not found');
ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp });Lifecycle Hooks
生命周期钩子
typescript
spacetimedb.clientConnected((ctx) => { /* ctx.sender is the connecting identity */ });
spacetimedb.clientDisconnected((ctx) => { /* clean up */ });typescript
spacetimedb.clientConnected((ctx) => { /* ctx.sender是连接的用户身份 */ });
spacetimedb.clientDisconnected((ctx) => { /* 清理逻辑 */ });Event Tables (2.0)
事件表(2.0版本)
Reducer callbacks are removed in 2.0. Use event tables + instead.
onInserttypescript
export const DamageEvent = table(
{ name: 'damage_event', public: true, event: true },
{ target: t.identity(), amount: t.u32() }
);
export const deal_damage = spacetimedb.reducer(
{ target: t.identity(), amount: t.u32() },
(ctx, { target, amount }) => {
ctx.db.damageEvent.insert({ target, amount });
}
);Client subscribes and uses :
onInserttypescript
conn.db.damageEvent.onInsert((ctx, evt) => {
playDamageAnimation(evt.target, evt.amount);
});Event tables must be subscribed explicitly — they are excluded from .
subscribeToAllTables()2.0版本已移除Reducer回调,请使用事件表+替代。
onInserttypescript
export const DamageEvent = table(
{ name: 'damage_event', public: true, event: true },
{ target: t.identity(), amount: t.u32() }
);
export const deal_damage = spacetimedb.reducer(
{ target: t.identity(), amount: t.u32() },
(ctx, { target, amount }) => {
ctx.db.damageEvent.insert({ target, amount });
}
);客户端订阅后使用监听:
onInserttypescript
conn.db.damageEvent.onInsert((ctx, evt) => {
playDamageAnimation(evt.target, evt.amount);
});事件表需要显式订阅 — 不会被包含。
subscribeToAllTables()Views
视图
ViewContext vs AnonymousViewContext
ViewContext与AnonymousViewContext
typescript
// ViewContext — has ctx.sender, result varies per user
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 (better perf)
spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(Player.rowType), (ctx) => {
return ctx.from.player.where(p => p.score.gt(1000));
});Views can only use index lookups — is NOT allowed.
.iter()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(Player.rowType), (ctx) => {
return ctx.from.player.where(p => p.score.gt(1000));
});视图仅可使用索引查询 — 禁止使用。
.iter()Scheduled Tables
定时表
typescript
export const CleanupJob = table({
name: 'cleanup_job',
scheduled: () => run_cleanup // function returning the exported reducer
}, {
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
targetId: t.u64(),
});
export const run_cleanup = spacetimedb.reducer(
{ arg: CleanupJob.rowType },
(ctx, { arg }) => { /* arg.scheduledId, arg.targetId available */ }
);
// Schedule a job
import { ScheduleAt } from 'spacetimedb';
ctx.db.cleanupJob.insert({
scheduledId: 0n,
scheduledAt: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + 60_000_000n),
targetId: someId
});typescript
export const CleanupJob = table({
name: 'cleanup_job',
scheduled: () => run_cleanup // 返回导出的Reducer的函数
}, {
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
targetId: t.u64(),
});
export const run_cleanup = spacetimedb.reducer(
{ arg: CleanupJob.rowType },
(ctx, { arg }) => { /* 可访问arg.scheduledId, arg.targetId */ }
);
// 调度任务
import { ScheduleAt } from 'spacetimedb';
ctx.db.cleanupJob.insert({
scheduledId: 0n,
scheduledAt: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + 60_000_000n),
targetId: someId
});ScheduleAt on Client
客户端使用ScheduleAt
typescript
// ScheduleAt is a tagged union on the client
// { tag: 'Time', value: Timestamp } or { tag: 'Interval', value: TimeDuration }
const schedule = row.scheduledAt;
if (schedule.tag === 'Time') {
const date = new Date(Number(schedule.value.microsSinceUnixEpoch / 1000n));
}typescript
// ScheduleAt在客户端是标记联合类型
// 格式为{ tag: 'Time', value: Timestamp } 或 { tag: 'Interval', value: TimeDuration }
const schedule = row.scheduledAt;
if (schedule.tag === 'Time') {
const date = new Date(Number(schedule.value.microsSinceUnixEpoch / 1000n));
}Timestamps
时间戳
Server-side
服务端
typescript
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n;typescript
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n;Client-side
客户端
typescript
// Timestamps are objects with BigInt, not numbers
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));typescript
// 时间戳是包含BigInt的对象,不是数字
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));Procedures (Beta)
存储过程(Beta)
typescript
export const fetch_data = spacetimedb.procedure(
{ url: t.string() }, t.string(),
(ctx, { url }) => {
const response = ctx.http.fetch(url);
ctx.withTx(tx => { tx.db.myTable.insert({ id: 0n, content: response.text() }); });
return response.text();
}
);Procedures don't have — use .
ctx.dbctx.withTx(tx => tx.db...)typescript
export const fetch_data = spacetimedb.procedure(
{ url: t.string() }, t.string(),
(ctx, { url }) => {
const response = ctx.http.fetch(url);
ctx.withTx(tx => { tx.db.myTable.insert({ id: 0n, content: response.text() }); });
return response.text();
}
);存储过程中没有 — 请使用。
ctx.dbctx.withTx(tx => tx.db...)React Integration
React集成
tsx
import { useMemo } from 'react';
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { DbConnection, tables } from './module_bindings';
function Root() {
const connectionBuilder = useMemo(() =>
DbConnection.builder()
.withUri('ws://localhost:3000')
.withDatabaseName('my_game')
.withToken(localStorage.getItem('auth_token') || undefined)
.onConnect((conn, identity, token) => {
localStorage.setItem('auth_token', token);
conn.subscriptionBuilder().subscribe(tables.player);
}),
[]
);
return (
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
<App />
</SpacetimeDBProvider>
);
}
function PlayerList() {
const [players, isReady] = useTable(tables.player);
if (!isReady) return <div>Loading...</div>;
return <ul>{players.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}tsx
import { useMemo } from 'react';
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { DbConnection, tables } from './module_bindings';
function Root() {
const connectionBuilder = useMemo(() =>
DbConnection.builder()
.withUri('ws://localhost:3000')
.withDatabaseName('my_game')
.withToken(localStorage.getItem('auth_token') || undefined)
.onConnect((conn, identity, token) => {
localStorage.setItem('auth_token', token);
conn.subscriptionBuilder().subscribe(tables.player);
}),
[]
);
return (
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
<App />
</SpacetimeDBProvider>
);
}
function PlayerList() {
const [players, isReady] = useTable(tables.player);
if (!isReady) return <div>加载中...</div>;
return <ul>{players.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}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": "^2.0.0" } }
tsconfig.json -> Standard configsrc/schema.ts -> 表定义,导出spacetimedb
src/index.ts -> Reducer、生命周期逻辑,导入schema
package.json -> { "type": "module", "dependencies": { "spacetimedb": "^2.0.0" } }
tsconfig.json -> 标准配置Client (client/
)
client/客户端(client/
)
client/src/module_bindings/ -> Generated (spacetime generate)
src/main.tsx -> Provider, connection setup
src/App.tsx -> UI componentssrc/module_bindings/ -> 生成的绑定文件(通过spacetime generate生成)
src/main.tsx -> Provider、连接配置
src/App.tsx -> UI组件Commands
常用命令
bash
spacetime start
spacetime publish <module-name> --module-path <backend-dir>
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --module-path <backend-dir>
spacetime logs <module-name>bash
spacetime start
spacetime publish <module-name> --module-path <backend-dir>
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --module-path <backend-dir>
spacetime logs <module-name>